#!/usr/bin/python

# Copyright (c) 2009 Peter Palfrader
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import errno
import getopt
import logging
import logging.handlers
import os
import stat
import sys
import yaml

import psycopg2
import psycopg2.extras

def usage(err = False):
    if err:
        f = sys.stderr
        exit = 1
    else:
        f = sys.stdout
        exit = 0
    f.write("Usage: %s --config=<f> --action=list-archives\n" % (sys.argv[0]))
    f.write("       %s --config=<f> --action=add-archive archive=<archive>\n" % (sys.argv[0]))
    f.write("       %s --help|-h\n" % (sys.argv[0]))
    f.write("Options: --quiet --stdout\n")
    sys.exit(exit)

def readConfig(conffile):
    return yaml.load(open(conffile).read())

def setupLogger(conf, stdout, quiet):
    if quiet >= 2:
        loglevel = level=logging.WARN
    elif quiet >= 1:
        loglevel = level=logging.INFO
    else:
        loglevel = level=logging.DEBUG
    logging.getLogger('').setLevel(loglevel)

    if stdout:
        handler = logging.StreamHandler()
    else:
        handler = logging.handlers.TimedRotatingFileHandler(conf['filename'], 'midnight', 1, conf['keep'])
    handler.setFormatter( logging.Formatter('%(asctime)s %(levelname)s %(name)s: %(message)s') )
    logging.getLogger('').addHandler(handler)

class SnapshotDB:
    def __init__(self, conf):
        self.db=psycopg2.connect(conf['connectstring'])

    def execute(self, *args, **kw):
        c = self.db.cursor(cursor_factory=psycopg2.extras.DictCursor)
        c.execute(*args, **kw)
        return c

    def query_all(self, *args, **kw):
        c = self.execute(*args, **kw)
        return c.fetchall()

    def query_iter(self, *args, **kw):
        c = self.execute(*args, **kw)
        while True:
            row = c.fetchone()
            if row is None:
                break
            yield row

    def query_row(self, *args, **kw):
        c = self.execute(*args, **kw)
        result = c.fetchone()
        if not result is None:
            next = c.fetchone()
            if not next is None:
                raise "More than one returned in query_row() call"
        return result

    def begin(self):
        self.execute("BEGIN")
    def commit(self):
        self.execute("COMMIT")

    def insert(self, table, values, returning=None):
        cols = values.keys()
        vals = values.values()
        paramplaces = ['%s'] * len(cols)

        query = "INSERT INTO %s (%s) VALUES (%s)"%(table, ",".join(cols), ",".join(paramplaces))

        if returning is None:
            self.execute(query, vals)
            return
        else:
            query += " RETURNING %s"%(",".join(returning))
            return self.query_row(query, vals)

    def get_primarykey_name(self, table):
        # XXX
        return table+"_id"

    def insert_row(self, table, values):
        pk_name = self.get_primarykey_name(table);
        if pk_name in values:
            insert(table, values)
        else:
            results = self.insert(table, values, [pk_name])
            values[pk_name] = results[pk_name]

class TreeImporter:
    def __init__(self):
        self

class FSNode(dict):
    def __init__(self, type, path):
        super(FSNode, self).__init__()

        self['type'] = type
        self['path'] = self._fixup_path(path)
    def _fixup_path(self, path):
        if path == '.':
            return '/'
        elif path[0:1] == ".":
            return path[1:]
        else:
            raise "Unexpected path(%s) for fixup"%(path)
    def __str__(self):
        return "%s %s"%(self['type'], self['path'])

class FSNodeDirectory(FSNode):
    def __init__(self, path):
        super(FSNodeDirectory, self).__init__('d', path)

class FSNodeSymlink(FSNode):
    def __init__(self, path, target):
        super(FSNodeSymlink, self).__init__('l', path)
        self['target'] = target
    def __str__(self):
        return super(FSNodeSymlink, self).__str__()+" %s"%(self['target'])

class FSNodeRegular(FSNode):
    def __init__(self, path, size, mtime, ctime, digest=None):
        super(FSNodeRegular, self).__init__('l', path)
        self['size'] = size
        self['time'] = max(mtime, ctime)
        if digest is not None:
            self['digest'] = digest
    def __str__(self):
        if 'digest' in self:
            return super(FSNodeRegular, self).__str__()+" %d %d %s"%(self['size'], self['time'], self['digest'])
        else:
            return super(FSNodeRegular, self).__str__()+" %d %d"%(self['size'], self['time'])

class FSReader:
    """FSReader gets a path to a place on the local filesystem.  It
       recurses through it, building a list of elements in this tree
       """
    def __init__(self, path):
        oldcwd = os.getcwd()
        self.root = path

    def append(self, type, path, **kw):
        h = { 'type': type,
              'path': path }
        for key in kw:
            h[key] = kw[key]
        self.filelist.append(h)

    def __iter__(self, path = '.'):
        fullpath = os.path.join(self.root, path)

        yield FSNodeDirectory(path)
        for filename in os.listdir(fullpath):
            element = os.path.join(path, filename)
            trueelement = os.path.join(self.root, element)

            statinfo = os.lstat(trueelement)
            mode = statinfo[stat.ST_MODE]

            if stat.S_ISDIR(mode):
                for i in self.__iter__(element):
                    yield i
            elif stat.S_ISLNK(mode):
                yield FSNodeSymlink(element, os.readlink(trueelement))
            elif stat.S_ISREG(mode):
                yield FSNodeRegular(element, statinfo[stat.ST_SIZE], statinfo[stat.ST_MTIME], statinfo[stat.ST_CTIME])
            else:
                log.warn("Ignoring %s which has unknown type", element)

class SnapshotImporter:
    def __init__(self, db, archive, reader):
        self.db = db
        self.reader = reader

        archive_row = self.db.query_row('SELECT archive_id FROM archive WHERE name=%s', [archive])
        if archive_row is None:
            log.error("Unknown archive %s"%archive)
            sys.exit(1)
        print archive_row['archive_id']

    def insert_node(self, parent):
        node = { 'first': self.mirror_run,
                 'last': self.mirror_run,
                 'parent': parent}
        self.db.insert_row('node', node)

    def imporx(self):
        pass

class Snapshot:
    def __init__(self, db, config):
        self.db = db
        #self.config = config['snapshot']

    def run(self, options):
        actions = { 'list-archives': self.list_archives,
                    'add-archive'  : self.add_archive,
                    'fs-list'      : self.fs_list,
                    'import-tree'  : self.import_tree,
                  }

        if not options['action'] in actions:
            log.error("Unknown action %s"%options['action'])
            sys.exit(1)
        actions[options['action']](options)

    def list_archives(self, options):
        for row in self.db.query_iter("""SELECT name FROM archive ORDER BY name"""):
            sys.stdout.write(row['name']+"\n")

    def add_archive(self, options):
        if options['archive'] is None:
            log.error("New --archive not given")
            sys.exit(1)
        self.db.begin()
        self.db.insert_row('archive', {'name': options['archive']})
        log.info("Added new archive %s."%options['archive'])
        self.db.commit()

    def fs_list(self, options):
        if options['path'] is None:
            log.error("--path not given")
            sys.exit(1)
        for l in FSReader(options['path']):
            print l

    def import_tree(self, options):
        if options['path'] is None:
            log.error("--path not given")
            sys.exit(1)
        if options['archive'] is None:
            log.error("--archive not given")
            sys.exit(1)
        reader = FSReader(options['path'])
        importer = SnapshotImporter(self.db, options['archive'], reader)

def main():
    longopts = []
    longopts.append("help")
    longopts.append("configfile=")
    longopts.append("quiet")
    longopts.append("stdout")
    longopts.append("action=")
    longopts.append("path=")
    longopts.append("archive=")
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hqs", longopts)
    except getopt.GetoptError, err:
        print str(err)
        usage(True)

    options = {
        'quiet':      0,
        'stdout':     False,
        'configfile': None,
        'action':     None,
        'path':       None,
        'archive':    None,
        }
    for opt, arg in opts:
        if opt in ("--help", "-h"):
            usage()
        elif opt in ("--quiet", "-q"):
            options['quiet'] += 1
        elif opt in ("--stdout", "-s"):
            options['stdout'] = True
        else:
            if opt[0:2] == "--" and opt[2:] in options:
                options[ opt[2:] ] = arg

    if len(args) > 0:
        usage(True)

    if options['configfile'] is None:
        usage(True)
    if options['action'] is None:
        usage(True)

    config = readConfig(options['configfile'])
    if not 'log' in config:
        sys.stderr.write("Config file does not define log keywords\n")
        sys.exit(1)
    setupLogger(config['log'], options['stdout'], options['quiet'])

    global log
    log = logging.getLogger("Main")

    if not 'db' in config:
        log.error("Config file does not define db keywords")
        sys.exit(1)
    db = SnapshotDB(config['db'])

    snapshot = Snapshot(db,config)
    snapshot.run(options)

if __name__ == "__main__":
   main()
# vim:set et:
# vim:set ts=4:
# vim:set shiftwidth=4:
