File: LocalStatus.py

package info (click to toggle)
offlineimap3 0.0~git20210225.1e7ef9e%2Bdfsg-4
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 1,328 kB
  • sloc: python: 7,974; sh: 548; makefile: 81
file content (281 lines) | stat: -rw-r--r-- 9,897 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# 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
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:
                errstr = ("Corrupt line '%s' in cache file '%s'" %
                          (line, self.filename))
                self.ui.warn(errstr)
                raise 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:
                errstr = "Corrupt line '%s' in cache file '%s'" % \
                         (line, self.filename)
                self.ui.warn(errstr)
                raise 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 list(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, msg, flags, rtime, mtime=0, labels=None):
        """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 labels is None:
            labels = set()

        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 list(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 list(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()