[PATCHv5 2/7] Experimental LocalStatus stored in SQLite database
Sebastian Spaeth
Sebastian at SSpaeth.de
Thu May 5 14:59:24 BST 2011
Based on patches by Stewart Smith, updated by Rob Browning.
plus:
- Inherit LocalStatusSQLFolder from LocalStatusFolder
This lets us remove all functions that are available via our ancestors
classes and are not needed.
- Don't fail if pysql import fails. Fail rather at runtime when needed.
- When creating the db file, create a metadata table which contains the
format version info, so we can upgrade nicely to other formats.
- Create an upgrade_db() function which allows us to upgrade from any
previous file format to the current one (including plain text)
Signed-off-by: Sebastian Spaeth <Sebastian at SSpaeth.de>
---
This should address Nicolas' concerns. I merged the previous 2 patches,
this got rid if intermittent space/tab problems and other problematic
code that was only in there during one commit.
offlineimap/folder/Base.py | 2 +-
offlineimap/folder/LocalStatus.py | 4 +-
offlineimap/folder/LocalStatusSQLite.py | 192 +++++++++++++++++++++++++++++++
3 files changed, 195 insertions(+), 3 deletions(-)
create mode 100644 offlineimap/folder/LocalStatusSQLite.py
diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py
index b3fd747..a5fd242 100644
--- a/offlineimap/folder/Base.py
+++ b/offlineimap/folder/Base.py
@@ -21,7 +21,7 @@ import os.path
import re
import traceback
-class BaseFolder:
+class BaseFolder(object):
def __init__(self):
self.ui = getglobalui()
diff --git a/offlineimap/folder/LocalStatus.py b/offlineimap/folder/LocalStatus.py
index 7390dc1..5d96c86 100644
--- a/offlineimap/folder/LocalStatus.py
+++ b/offlineimap/folder/LocalStatus.py
@@ -30,12 +30,12 @@ class LocalStatusFolder(BaseFolder):
self.config = config
self.dofsync = config.getdefaultboolean("general", "fsync", True)
self.filename = repository.getfolderfilename(name)
- self.messagelist = None
+ self.messagelist = {}
self.repository = repository
self.savelock = threading.Lock()
self.doautosave = 1
self.accountname = accountname
- BaseFolder.__init__(self)
+ super(LocalStatusFolder, self).__init__()
def getaccountname(self):
return self.accountname
diff --git a/offlineimap/folder/LocalStatusSQLite.py b/offlineimap/folder/LocalStatusSQLite.py
new file mode 100644
index 0000000..701592c
--- /dev/null
+++ b/offlineimap/folder/LocalStatusSQLite.py
@@ -0,0 +1,192 @@
+# Local status cache virtual folder: SQLite backend
+# Copyright (C) 2009-2011 Stewart Smith and 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
+import os.path
+import re
+from LocalStatus import LocalStatusFolder, magicline
+try:
+ from pysqlite2 import dbapi2 as sqlite
+except:
+ pass #fail only if needed later on, not on import
+
+class LocalStatusSQLiteFolder(LocalStatusFolder):
+ """LocalStatus backend implemented with an SQLite database"""
+ #current version of the db format
+ cur_version = 1
+
+ def __init__(self, root, name, repository, accountname, config):
+ super(LocalStatusSQLiteFolder, self).__init__(root, name,
+ repository,
+ accountname,
+ config)
+ #Try to establish connection
+ try:
+ self.connection = sqlite.connect(self.filename)
+ except NameError:
+ # sqlite import had failed
+ raise UserWarning('SQLite backend chosen, but no sqlite python '
+ 'bindings available. Please install.')
+
+ #Test if the db version is current enough and if the db is
+ #readable.
+ try:
+ self.cursor = self.connection.cursor()
+ self.cursor.execute("SELECT value from metadata WHERE key='db_version'")
+ except sqlite.DatabaseError:
+ #db file missing or corrupt, recreate it.
+ self.connection.close()
+ self.upgrade_db(0)
+ else:
+ # fetch db version and upgrade if needed
+ version = int(self.cursor.fetchone()[0])
+ self.cursor.close()
+ if version < LocalStatusSQLiteFolder.cur_version:
+ self.upgrade_db(version)
+ self.connection.close()
+
+ def upgrade_db(self, from_ver):
+ """Upgrade the sqlite format from version 'from_ver' to current"""
+ if from_ver == 0:
+ # from_ver==0: no db existent: plain text migration?
+ self.create_db()
+ # below was derived from repository.getfolderfilename() logic
+ plaintextfilename = os.path.join(
+ self.repository.account.getaccountmeta(),
+ 'LocalStatus',
+ re.sub('(^|\/)\.$','\\1dot', self.name))
+ # MIGRATE from plaintext if needed
+ if os.path.exists(plaintextfilename):
+ self.ui._msg('Migrating LocalStatus cache from plain text '
+ 'to sqlite database for %s:%s' %\
+ (self.repository, self))
+ file = open(plaintextfilename, "rt")
+ line = file.readline().strip()
+ assert(line == magicline)
+ connection = sqlite.connect(self.filename)
+ cursor = connection.cursor()
+ for line in file.xreadlines():
+ line = line.strip()
+ uid, flags = line.split(':')
+ uid = long(uid)
+ flags = [x for x in flags]
+ flags.sort()
+ flags = ''.join(flags)
+ self.cursor.execute('INSERT INTO status (id,flags) VALUES (?,?)',
+ (uid,flags))
+ file.close()
+ self.connection.commit()
+ os.rename(plaintextfilename, plaintextfilename + ".old")
+ self.connection.close()
+ # Future version upgrades come here...
+ # if from_ver <= 1: ... #upgrade from 1 to 2
+ # if from_ver <= 2: ... #upgrade from 2 to 3
+
+ def create_db(self):
+ """Create a new db file"""
+ self.ui._msg('Creating new Local Status db for %s:%s' \
+ % (self.repository, self))
+ connection = sqlite.connect(self.filename)
+ cursor = connection.cursor()
+ cursor.execute('CREATE TABLE metadata (key VARCHAR(50) PRIMARY KEY, value VARCHAR(128))')
+ cursor.execute("INSERT INTO metadata VALUES('db_version', '1')")
+ cursor.execute('CREATE TABLE status (id INTEGER PRIMARY KEY, flags VARCHAR(50))')
+ self.autosave() #commit if needed
+
+ def isnewfolder(self):
+ # testing the existence of the db file won't work. It is created
+ # as soon as this class instance was intitiated. So say it is a
+ # new folder when there are no messages at all recorded in it.
+ return self.getmessagecount() > 0
+
+ def deletemessagelist(self):
+ """delete all messages in the db"""
+ self.cursor.execute('DELETE FROM status')
+
+ def cachemessagelist(self):
+ self.messagelist = {}
+ self.cursor.execute('SELECT id,flags from status')
+ for row in self.cursor:
+ flags = [x for x in row[1]]
+ self.messagelist[row[0]] = {'uid': row[0], 'flags': flags}
+
+ def save(self):
+ #Noop in this backend
+ pass
+
+ def uidexists(self,uid):
+ self.cursor.execute('SELECT id FROM status WHERE id=:id',{'id': uid})
+ for row in self.cursor:
+ if(row[0]==uid):
+ return 1
+ return 0
+
+ def getmessageuidlist(self):
+ self.cursor.execute('SELECT id from status')
+ r = []
+ for row in self.cursor:
+ r.append(row[0])
+ return r
+
+ def getmessagecount(self):
+ self.cursor.execute('SELECT count(id) from status');
+ row = self.cursor.fetchone()
+ return row[0]
+
+ def savemessage(self, uid, content, flags, rtime):
+ 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] = {'uid': uid, 'flags': flags, 'time': rtime}
+ flags.sort()
+ flags = ''.join(flags)
+ self.cursor.execute('INSERT INTO status (id,flags) VALUES (?,?)',
+ (uid,flags))
+ self.autosave()
+ return uid
+
+ def getmessageflags(self, uid):
+ self.cursor.execute('SELECT flags FROM status WHERE id=:id',
+ {'id': uid})
+ for row in self.cursor:
+ flags = [x for x in row[0]]
+ return flags
+ assert False,"getmessageflags() called on non-existing message"
+
+ def getmessagetime(self, uid):
+ return self.messagelist[uid]['time']
+
+ def savemessageflags(self, uid, flags):
+ self.messagelist[uid] = {'uid': uid, 'flags': flags}
+ flags.sort()
+ flags = ''.join(flags)
+ self.cursor.execute('UPDATE status SET flags=? WHERE id=?',(flags,uid))
+ self.autosave()
+
+ 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])
+ #if self.uidexists(uid):
+ self.cursor.execute('DELETE FROM status WHERE id=:id', {'id': uid})
--
1.7.4.1
More information about the OfflineIMAP-project
mailing list