# Local status cache virtual folder # Copyright (C) 2002-2016 John Goerzen & contributors. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from sys import exc_info import os import threading import six from .Base import BaseFolder class LocalStatusFolder(BaseFolder): """LocalStatus backend implemented as a plain text file.""" cur_version = 2 magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT %d" def __init__(self, name, repository): self.sep = '.' #needs to be set before super.__init__() super(LocalStatusFolder, self).__init__(name, repository) self.root = repository.root self.filename = os.path.join(self.getroot(), self.getfolderbasename()) self.savelock = threading.Lock() # Should we perform fsyncs as often as possible? self.doautosave = self.config.getdefaultboolean( "general", "fsync", False) # Interface from BaseFolder def storesmessages(self): return 0 def isnewfolder(self): return not os.path.exists(self.filename) # Interface from BaseFolder def getfullname(self): return self.filename # Interface from BaseFolder def msglist_item_initializer(self, uid): return {'uid': uid, 'flags': set(), 'labels': set(), 'time': 0, 'mtime': 0} def readstatus_v1(self, fp): """Read status folder in format version 1. Arguments: - fp: I/O object that points to the opened database file. """ for line in fp: line = line.strip() try: uid, flags = line.split(':') uid = int(uid) flags = set(flags) except ValueError as e: errstr = ("Corrupt line '%s' in cache file '%s'"% (line, self.filename)) self.ui.warn(errstr) six.reraise(ValueError, ValueError(errstr), exc_info()[2]) self.messagelist[uid] = self.msglist_item_initializer(uid) self.messagelist[uid]['flags'] = flags def readstatus(self, fp): """Read status file in the current format. Arguments: - fp: I/O object that points to the opened database file. """ for line in fp: line = line.strip() try: uid, flags, mtime, labels = line.split('|') uid = int(uid) flags = set(flags) mtime = int(mtime) labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0]) except ValueError as e: errstr = "Corrupt line '%s' in cache file '%s'"% \ (line, self.filename) self.ui.warn(errstr) six.reraise(ValueError, ValueError(errstr), exc_info()[2]) self.messagelist[uid] = self.msglist_item_initializer(uid) self.messagelist[uid]['flags'] = flags self.messagelist[uid]['mtime'] = mtime self.messagelist[uid]['labels'] = labels # Interface from BaseFolder def cachemessagelist(self): if self.isnewfolder(): self.dropmessagelistcache() return # Loop as many times as version, and update format. for i in range(1, self.cur_version + 1): self.dropmessagelistcache() cachefd = open(self.filename, "rt") line = cachefd.readline().strip() # Format is up to date. break. if line == (self.magicline % self.cur_version): break # Convert from format v1. elif line == (self.magicline % 1): self.ui._msg('Upgrading LocalStatus cache from version 1 ' 'to version 2 for %s:%s'% (self.repository, self)) self.readstatus_v1(cachefd) cachefd.close() self.save() # NOTE: Add other format transitions here in the future. # elif line == (self.magicline % 2): # self.ui._msg(u'Upgrading LocalStatus cache from version 2' # 'to version 3 for %s:%s'% (self.repository, self)) # self.readstatus_v2(cache) # cache.close() # cache.save() # Something is wrong. else: errstr = "Unrecognized cache magicline in '%s'" % self.filename self.ui.warn(errstr) raise ValueError(errstr) if not line: # The status file is empty - should not have happened, # but somehow did. errstr = "Cache file '%s' is empty."% self.filename self.ui.warn(errstr) cachefd.close() return assert(line == (self.magicline % self.cur_version)) self.readstatus(cachefd) cachefd.close() def openfiles(self): pass # Closing files is done on a per-transaction basis. def closefiles(self): pass # Closing files is done on a per-transaction basis. def purge(self): """Remove any pre-existing database.""" try: os.unlink(self.filename) except OSError as e: self.ui.debug('', "could not remove file %s: %s"% (self.filename, e)) def save(self): """Save changed data to disk. For this backend it is the same as saveall.""" self.saveall() def saveall(self): """Saves the entire messagelist to disk.""" with self.savelock: cachefd = open(self.filename + ".tmp", "wt") cachefd.write((self.magicline % self.cur_version) + "\n") for msg in self.messagelist.values(): flags = ''.join(sorted(msg['flags'])) labels = ', '.join(sorted(msg['labels'])) cachefd.write("%s|%s|%d|%s\n" % (msg['uid'], flags, msg['mtime'], labels)) cachefd.flush() if self.doautosave: os.fsync(cachefd.fileno()) cachefd.close() os.rename(self.filename + ".tmp", self.filename) if self.doautosave: fd = os.open(os.path.dirname(self.filename), os.O_RDONLY) os.fsync(fd) os.close(fd) # Interface from BaseFolder def savemessage(self, uid, content, flags, rtime, mtime=0, labels=set()): """Writes a new message, with the specified uid. See folder/Base for detail. Note that savemessage() does not check against dryrun settings, so you need to ensure that savemessage is never called in a dryrun mode.""" if uid < 0: # We cannot assign a uid. return uid if self.uidexists(uid): # already have it self.savemessageflags(uid, flags) return uid self.messagelist[uid] = self.msglist_item_initializer(uid) self.messagelist[uid]['flags'] = flags self.messagelist[uid]['time'] = rtime self.messagelist[uid]['mtime'] = mtime self.messagelist[uid]['labels'] = labels self.save() return uid # Interface from BaseFolder def getmessageflags(self, uid): return self.messagelist[uid]['flags'] # Interface from BaseFolder def getmessagetime(self, uid): return self.messagelist[uid]['time'] # Interface from BaseFolder def savemessageflags(self, uid, flags): self.messagelist[uid]['flags'] = flags self.save() def savemessagelabels(self, uid, labels, mtime=None): self.messagelist[uid]['labels'] = labels if mtime: self.messagelist[uid]['mtime'] = mtime self.save() def savemessageslabelsbulk(self, labels): """Saves labels from a dictionary in a single database operation.""" for uid, lb in labels.items(): self.messagelist[uid]['labels'] = lb self.save() def addmessageslabels(self, uids, labels): for uid in uids: self.messagelist[uid]['labels'] = self.messagelist[uid]['labels'] | labels self.save() def deletemessageslabels(self, uids, labels): for uid in uids: self.messagelist[uid]['labels'] = self.messagelist[uid]['labels'] - labels self.save() def getmessagelabels(self, uid): return self.messagelist[uid]['labels'] def savemessagesmtimebulk(self, mtimes): """Saves mtimes from the mtimes dictionary in a single database operation.""" for uid, mt in mtimes.items(): self.messagelist[uid]['mtime'] = mt self.save() def getmessagemtime(self, uid): return self.messagelist[uid]['mtime'] # Interface from BaseFolder def deletemessage(self, uid): self.deletemessages([uid]) # Interface from BaseFolder def deletemessages(self, uidlist): # Weed out ones not in self.messagelist uidlist = [uid for uid in uidlist if uid in self.messagelist] if not len(uidlist): return for uid in uidlist: del(self.messagelist[uid]) self.save()