LocalStatus.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. # Local status cache virtual folder
  2. # Copyright (C) 2002-2016 John Goerzen & contributors.
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  17. from sys import exc_info
  18. import os
  19. import threading
  20. from .Base import BaseFolder
  21. class LocalStatusFolder(BaseFolder):
  22. """LocalStatus backend implemented as a plain text file."""
  23. cur_version = 2
  24. magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT %d"
  25. def __init__(self, name, repository):
  26. self.sep = '.' # needs to be set before super.__init__()
  27. super(LocalStatusFolder, self).__init__(name, repository)
  28. self.root = repository.root
  29. self.filename = os.path.join(self.getroot(), self.getfolderbasename())
  30. self.savelock = threading.Lock()
  31. # Should we perform fsyncs as often as possible?
  32. self.doautosave = self.config.getdefaultboolean(
  33. "general", "fsync", False)
  34. # Interface from BaseFolder
  35. def storesmessages(self):
  36. return 0
  37. def isnewfolder(self):
  38. return not os.path.exists(self.filename)
  39. # Interface from BaseFolder
  40. def getfullname(self):
  41. return self.filename
  42. # Interface from BaseFolder
  43. def msglist_item_initializer(self, uid):
  44. return {'uid': uid, 'flags': set(), 'labels': set(), 'time': 0, 'mtime': 0}
  45. def readstatus_v1(self, fp):
  46. """Read status folder in format version 1.
  47. Arguments:
  48. - fp: I/O object that points to the opened database file.
  49. """
  50. for line in fp:
  51. line = line.strip()
  52. try:
  53. uid, flags = line.split(':')
  54. uid = int(uid)
  55. flags = set(flags)
  56. except ValueError:
  57. errstr = ("Corrupt line '%s' in cache file '%s'" %
  58. (line, self.filename))
  59. self.ui.warn(errstr)
  60. raise ValueError(errstr, exc_info()[2])
  61. self.messagelist[uid] = self.msglist_item_initializer(uid)
  62. self.messagelist[uid]['flags'] = flags
  63. def readstatus(self, fp):
  64. """Read status file in the current format.
  65. Arguments:
  66. - fp: I/O object that points to the opened database file.
  67. """
  68. for line in fp:
  69. line = line.strip()
  70. try:
  71. uid, flags, mtime, labels = line.split('|')
  72. uid = int(uid)
  73. flags = set(flags)
  74. mtime = int(mtime)
  75. labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0])
  76. except ValueError:
  77. errstr = "Corrupt line '%s' in cache file '%s'" % \
  78. (line, self.filename)
  79. self.ui.warn(errstr)
  80. raise ValueError(errstr, exc_info()[2])
  81. self.messagelist[uid] = self.msglist_item_initializer(uid)
  82. self.messagelist[uid]['flags'] = flags
  83. self.messagelist[uid]['mtime'] = mtime
  84. self.messagelist[uid]['labels'] = labels
  85. # Interface from BaseFolder
  86. def cachemessagelist(self):
  87. if self.isnewfolder():
  88. self.dropmessagelistcache()
  89. return
  90. # Loop as many times as version, and update format.
  91. for i in range(1, self.cur_version + 1):
  92. self.dropmessagelistcache()
  93. cachefd = open(self.filename, "rt")
  94. line = cachefd.readline().strip()
  95. # Format is up to date. break.
  96. if line == (self.magicline % self.cur_version):
  97. break
  98. # Convert from format v1.
  99. elif line == (self.magicline % 1):
  100. self.ui._msg('Upgrading LocalStatus cache from version 1 '
  101. 'to version 2 for %s:%s' % (self.repository, self))
  102. self.readstatus_v1(cachefd)
  103. cachefd.close()
  104. self.save()
  105. # NOTE: Add other format transitions here in the future.
  106. # elif line == (self.magicline % 2):
  107. # self.ui._msg(u'Upgrading LocalStatus cache from version 2'
  108. # 'to version 3 for %s:%s'% (self.repository, self))
  109. # self.readstatus_v2(cache)
  110. # cache.close()
  111. # cache.save()
  112. # Something is wrong.
  113. else:
  114. errstr = "Unrecognized cache magicline in '%s'" % self.filename
  115. self.ui.warn(errstr)
  116. raise ValueError(errstr)
  117. if not line:
  118. # The status file is empty - should not have happened,
  119. # but somehow did.
  120. errstr = "Cache file '%s' is empty." % self.filename
  121. self.ui.warn(errstr)
  122. cachefd.close()
  123. return
  124. assert (line == (self.magicline % self.cur_version))
  125. self.readstatus(cachefd)
  126. cachefd.close()
  127. def openfiles(self):
  128. pass # Closing files is done on a per-transaction basis.
  129. def closefiles(self):
  130. pass # Closing files is done on a per-transaction basis.
  131. def purge(self):
  132. """Remove any pre-existing database."""
  133. try:
  134. os.unlink(self.filename)
  135. except OSError as e:
  136. self.ui.debug('', "could not remove file %s: %s" %
  137. (self.filename, e))
  138. def save(self):
  139. """Save changed data to disk. For this backend it is the same as saveall."""
  140. self.saveall()
  141. def saveall(self):
  142. """Saves the entire messagelist to disk."""
  143. with self.savelock:
  144. cachefd = open(self.filename + ".tmp", "wt")
  145. cachefd.write((self.magicline % self.cur_version) + "\n")
  146. for msg in list(self.messagelist.values()):
  147. flags = ''.join(sorted(msg['flags']))
  148. labels = ', '.join(sorted(msg['labels']))
  149. cachefd.write("%s|%s|%d|%s\n" % (msg['uid'], flags, msg['mtime'], labels))
  150. cachefd.flush()
  151. if self.doautosave:
  152. os.fsync(cachefd.fileno())
  153. cachefd.close()
  154. os.rename(self.filename + ".tmp", self.filename)
  155. if self.doautosave:
  156. fd = os.open(os.path.dirname(self.filename), os.O_RDONLY)
  157. os.fsync(fd)
  158. os.close(fd)
  159. # Interface from BaseFolder
  160. def savemessage(self, uid, msg, flags, rtime, mtime=0, labels=None):
  161. """Writes a new message, with the specified uid.
  162. See folder/Base for detail. Note that savemessage() does not
  163. check against dryrun settings, so you need to ensure that
  164. savemessage is never called in a dryrun mode."""
  165. if labels is None:
  166. labels = set()
  167. if uid < 0:
  168. # We cannot assign a uid.
  169. return uid
  170. if self.uidexists(uid): # already have it
  171. self.savemessageflags(uid, flags)
  172. return uid
  173. self.messagelist[uid] = self.msglist_item_initializer(uid)
  174. self.messagelist[uid]['flags'] = flags
  175. self.messagelist[uid]['time'] = rtime
  176. self.messagelist[uid]['mtime'] = mtime
  177. self.messagelist[uid]['labels'] = labels
  178. self.save()
  179. return uid
  180. # Interface from BaseFolder
  181. def getmessageflags(self, uid):
  182. return self.messagelist[uid]['flags']
  183. # Interface from BaseFolder
  184. def getmessagetime(self, uid):
  185. return self.messagelist[uid]['time']
  186. # Interface from BaseFolder
  187. def savemessageflags(self, uid, flags):
  188. self.messagelist[uid]['flags'] = flags
  189. self.save()
  190. def savemessagelabels(self, uid, labels, mtime=None):
  191. self.messagelist[uid]['labels'] = labels
  192. if mtime:
  193. self.messagelist[uid]['mtime'] = mtime
  194. self.save()
  195. def savemessageslabelsbulk(self, labels):
  196. """Saves labels from a dictionary in a single database operation."""
  197. for uid, lb in list(labels.items()):
  198. self.messagelist[uid]['labels'] = lb
  199. self.save()
  200. def addmessageslabels(self, uids, labels):
  201. for uid in uids:
  202. self.messagelist[uid]['labels'] = self.messagelist[uid]['labels'] | labels
  203. self.save()
  204. def deletemessageslabels(self, uids, labels):
  205. for uid in uids:
  206. self.messagelist[uid]['labels'] = self.messagelist[uid]['labels'] - labels
  207. self.save()
  208. def getmessagelabels(self, uid):
  209. return self.messagelist[uid]['labels']
  210. def savemessagesmtimebulk(self, mtimes):
  211. """Saves mtimes from the mtimes dictionary in a single database operation."""
  212. for uid, mt in list(mtimes.items()):
  213. self.messagelist[uid]['mtime'] = mt
  214. self.save()
  215. def getmessagemtime(self, uid):
  216. return self.messagelist[uid]['mtime']
  217. # Interface from BaseFolder
  218. def deletemessage(self, uid):
  219. self.deletemessages([uid])
  220. # Interface from BaseFolder
  221. def deletemessages(self, uidlist):
  222. # Weed out ones not in self.messagelist
  223. uidlist = [uid for uid in uidlist if uid in self.messagelist]
  224. if not len(uidlist):
  225. return
  226. for uid in uidlist:
  227. del (self.messagelist[uid])
  228. self.save()