Esempio n. 1
0
class Log:
    """ Log management class
    """

    def __init__(self, config):
        self.config = config
        dataDir     = config['dataDir']
        os.makedirs(dataDir, exist_ok=True)
        self.git    = Git(dataDir)

        # Setup and register storage engine
        XmlStorage.dataDir = dataDir
        XmlStorage.git     = self.git
        Record.engine      = XmlStorage

    def lastLog(self):
        """ Fetch the most recent log record
        """
        return Record.engine.lastLog()

    def add(self, interactive=False, **fields):
        """ Add a log record to the system
        When interactive is True, ask data for subject, time, scene,
        people, tag, and log data from the use interactively, the
        provided arguments are used as the default value for user's
        choice.
        """

        if interactive:
            fields = self.collectLogInfo(**fields)
        author = '%s <%s>' % (self.config['authorName'],
                              self.config['authorEmail'])
        fields['author'] = author
        assert self.checkRequirement(**fields), "field data not sufficient"
        fields = Record.engine.convertFields(fields.items())
        record = Record(**fields)
        record.save()

    def checkRequirement(self, **args):
        """ Check if all required fields are provided
        """
        desc = Record.fields.items()
        keys = [k for k, v in desc if v.get('required') == True]
        for key in keys:
            if not args.get(key):
                print('%s is required' % key, file=sys.stderr)
                return False
        return True

    def makeLogEntry(self, *junk, **args):
        id     = args.pop('id')
        author = args.pop('author')
        fields = self.collectLogInfo(**args)
        fields['id']     = id
        fields['author'] = author
        assert self.checkRequirement(**fields), "field data not sufficient"
        fields = Record.engine.convertFields(fields.items())
        return Record(**fields)

    def collectLogs(self, ids=None, filter=None):
        """ Walk through all log records, collect those
        that passed the filter function matching. Return
        a generator which yields Record instances.
        """
        if not filter:
            filter = lambda record: True
        if not ids:
            ids = Record.allIds()
        for id in ids:
            record = Record.load(id)
            if filter(record):
                yield record

    def delete(self, ids, force=False, preAction=None, postAction=None):
        """ Delete the logs whose id is in 'ids', partial
        ID is acceptable, so that 297aacc is the equivalent
        of 297aacc3863171ed86ba89a2ea0e88f9c4d99d48.
        """
        allIds = Record.allIds()
        ids = [storedId for storedId in allIds for id in ids
                    if id and storedId.startswith(id)]
        if force:
            preAction = lambda x: True
        if not preAction:  preAction  = self.preActionOfDelete
        if not postAction: postAction = self.postActionOfDelete
        Record.engine.delete(ids, preAction, postAction)

    def edit(self, id, preAction=None):
        """ Edit the log of the given id
        """
        ids = Record.matchId(id)
        if not ids:
            print('%s not found' % id, file=sys.stderr)
            return
        elif len(ids) > 1:
            prompt   = 'multiple match, which one? '
            junk, id = interact.printAndPick(ids, lineMode=True, prompt=prompt)
            assert id, 'invalid id: "%s"' % id
        else:
            id = ids[0]

        if not preAction:
            preAction = lambda x: x
        oldRecord = Record.load(id)
        items = oldRecord.elements().items()
        elements  = dict(oldRecord.elements())
        elements  = self.preActionOfEdit(elements)
        newRecord = self.makeLogEntry(**elements)
        newRecord.save(oldRecord=oldRecord)

    def perror(self, msg):
        print(msg, file=sys.stderr)

    def push(self, remote):
        """ Sync with the git server

        Push using shadow-git tools, first shadow-push,
        if rejected because of unfetched update on the
        server side, do a shadow-fetch and shadow-merge,
        then a shadow-push again. In case of conflict
        that can not be automatically resolved, exit,
        after manually solved the conflict, user can
        then try to push again.
        """
        if not self.preActionOfPushAndFetch(remote):
            return False

        for i in range(2):              # try twice at most
            print('pushing...')
            stat, msg = self.git.shadowPush(remote)
            if stat == Git.TOFETCH:
                print('push rejected, need to fetch')
                if self.fetch(remote):
                    continue
            elif stat == Git.UNKNOWN:
                self.perror('unknown error:\n' + msg.decode())
            elif stat == Git.SUCCESS:
                print('push done.')
                return True
            return False


    def preActionOfPushAndFetch(self, remote):
        """ Actions to carry out before push/fetch
        """
        if not self.git.shadowInit():
            return False
        if not self.git.setRemote(remote):
            return False
        return True


    def fetch(self, remote):
        """ Fetch from the git server
        """
        if not self.preActionOfPushAndFetch(remote):
            return False

        print('fetching...')
        stat, msg = self.git.shadowFetch(remote)
        if not stat:
            self.perror('fetch failed:\n' + msg.decode())
        else:
            print('merging...')
            stat, msg = self.git.shadowMerge(remote)
            if stat == Git.SUCCESS:
                print('fetch done.')
                return True
            elif stat == Git.UNKNOWN:
                self.perror('unknown error:\n' + msg.decode())
            elif stat == Git.CONFLICT:
                self.perror('automatic merge failed, fix the conflict, and retry')
        return False


    def recentLogs(self, count):
        """ Fetch the last 'count' logs that newly
        added or changed, use git for better speed.
        """
        return Record.engine.lastLogs(count)


    """ Methods defined below are Record definition specific,
    subclasses shall redefine/extend these methods according
    to the Record fields definition, or add more others.
    """

    def makeRequests(self, *, record=None, time=None, host=None,
            protocol=None, port=None, user=None, password=None, **junk):
        """ Create the necessary requests data for collecting
        information for a Record from the user interactively.
        """
        if record:      # a Record instance provided
            time      =  record.time
            host      =  record.host
            protocol  =  record.protocol
            port      =  record.port
            user      =  record.user
            password  =  record.password
        else:
            time      = time if time else timeutils.isodatetime()
            host      =  host      if  host      else  ''
            protocol  =  protocol  if  protocol  else  ''
            port      =  port      if  port      else  ''
            user      =  user      if  user      else  ''
            password  =  password  if  password  else  ''

        requests = []
        # arguments: name, default, datatype, reader, desc
        requests.append(applib.makeOneRequest('time',     time,     str, None, 'time'))
        requests.append(applib.makeOneRequest('host',     host,     str, None, 'host'))
        requests.append(applib.makeOneRequest('protocol', protocol, str, None, 'protocol'))
        requests.append(applib.makeOneRequest('port',     port,     str, None, 'port'))
        requests.append(applib.makeOneRequest('user',     user,     str, None, 'user'))
        requests.append(applib.makeOneRequest('password', password, str, None, 'password'))
        return requests

    def collectLogInfo(self, *junk, **args):
        """ Collect Record fields' info interactively
        'data' in the args must be a bytes which can
        be decoded using utf8, binary data that is
        not utf8 encoded, is not applicable.
        """
        comment = args.pop('comment')
        desc    = args.pop('desc')

        # read desc and comment from editor
        if desc:
            iData = desc.encode()
            if comment:
                iData += b'\n\n' + comment
        else:
            iData = b''
        oData    = editContent(iData).decode()
        msgLines = oData.split('\n\n')
        desc     = msgLines.pop(0).strip()
        comment  = '\n\n'.join(msgLines)

        # empty desc, abort
        assert desc != '', "aborting due to empty desc"

        # read other info
        requests = self.makeRequests(**args)
        i        = interact.readMany(requests)
        time     = i['time']
        host     = i['host']
        protocol = i['protocol']
        port     = i['port']
        user     = i['user']
        password = i['password']

        return dict(desc=desc, time=time, host=host, protocol=protocol,
                      port=port, user=user, password=password, comment=comment)

    def preActionOfDelete(self, record):
        """ Confirm before deleting
        """
        msg = 'delete %s: %s? ' % (record.id, record.desc)
        ans = interact.readstr(msg, default='N')
        return ans == 'y'

    def postActionOfDelete(self, record):
        print('deleted %s' % record.id)

    def preActionOfEdit(self, elements):
        """ Convert data before editing
        """
        conv = Record.getConv('time', toRecord=False)
        elements['time']    = conv(elements['time'])
        elements['comment'] = elements['comment'].encode()
        return elements