# HG changeset patch # User Goffi # Date 1320012820 -3600 # Node ID 62b17854254e02dfa892dc6f5a7bdb6117965efc # Parent b109a79ac72f22d54d3b153f1002bacc4253ba50 database integration: first draft - using SQLite as backend /!\ Not finished yet, break execution. SàT CAN'T LAUNCH PROPERLY IN THE CURRENT STATE diff -r b109a79ac72f -r 62b17854254e src/core/sat_main.py --- a/src/core/sat_main.py Sat Oct 08 21:03:02 2011 +0200 +++ b/src/core/sat_main.py Sun Oct 30 23:13:40 2011 +0100 @@ -112,7 +112,6 @@ self.trigger = TriggerManager() #trigger are used to change SàT behaviour - self.bridge=DBusBridge() self.bridge.register("getVersion", lambda: self.get_const('client_version')) self.bridge.register("getProfileName", self.memory.getProfileName) @@ -148,7 +147,12 @@ self.bridge.register("getMenus", self.getMenus) self.bridge.register("getMenuHelp", self.getMenuHelp) self.bridge.register("callMenu", self.callMenu) + + self.memory.initialized.addCallback(self._postMemoryInit) + def _postMemoryInit(self, ignore): + """Method called after memory initialization is done""" + info(_("Memory initialised")) self._import_plugins() @@ -156,8 +160,8 @@ """Import all plugins found in plugins directory""" import sat.plugins plugins_path = os.path.dirname(sat.plugins.__file__) - plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob (os.path.join(plugins_path,"plugin*.py")))] - __plugins_to_import = {} #plugins will still have to import + plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob(os.path.join(plugins_path,"plugin*.py")))] + __plugins_to_import = {} #plugins we still have to import for plug in plug_lst: plugin_path = 'sat.plugins.'+plug __import__(plugin_path) @@ -215,37 +219,44 @@ if callback: callback() return - current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile, - jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile), profile), - self.memory.getParamA("Password", "Connection", profile_key = profile), - self.memory.getParamA("Server", "Connection", profile_key = profile), 5222) + + def afterMemoryInit(ignore): + """This part must be called when we have loaded individual parameters from memory""" + current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile, + jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile), profile), + self.memory.getParamA("Password", "Connection", profile_key = profile), + self.memory.getParamA("Server", "Connection", profile_key = profile), 5222) - if callback and errback: - current.getConnectionDeferred().addCallbacks(lambda x:callback(), errback) + if callback and errback: + current.getConnectionDeferred().addCallbacks(lambda x:callback(), errback) - current.messageProt = xmpp.SatMessageProtocol(self) - current.messageProt.setHandlerParent(current) - - current.roster = xmpp.SatRosterProtocol(self) - current.roster.setHandlerParent(current) + current.messageProt = xmpp.SatMessageProtocol(self) + current.messageProt.setHandlerParent(current) + + current.roster = xmpp.SatRosterProtocol(self) + current.roster.setHandlerParent(current) - current.presence = xmpp.SatPresenceProtocol(self) - current.presence.setHandlerParent(current) + current.presence = xmpp.SatPresenceProtocol(self) + current.presence.setHandlerParent(current) - current.fallBack = xmpp.SatFallbackHandler(self) - current.fallBack.setHandlerParent(current) + current.fallBack = xmpp.SatFallbackHandler(self) + current.fallBack.setHandlerParent(current) + + current.versionHandler = xmpp.SatVersionHandler(self.get_const('client_name'), + self.get_const('client_version')) + current.versionHandler.setHandlerParent(current) - current.versionHandler = xmpp.SatVersionHandler(self.get_const('client_name'), - self.get_const('client_version')) - current.versionHandler.setHandlerParent(current) + debug (_("setting plugins parents")) + + for plugin in self.plugins.iteritems(): + if plugin[1].is_handler: + plugin[1].getHandler(profile).setHandlerParent(current) - debug (_("setting plugins parents")) - - for plugin in self.plugins.iteritems(): - if plugin[1].is_handler: - plugin[1].getHandler(profile).setHandlerParent(current) + current.startService() - current.startService() + params_defer = self.memory.loadIndividualParams(profile).addCallback(afterMemoryInit) + if errback: + params_defer.addErrback(errback) def disconnect(self, profile_key): """disconnect from jabber server""" diff -r b109a79ac72f -r 62b17854254e src/plugins/plugin_misc_imap.py --- a/src/plugins/plugin_misc_imap.py Sat Oct 08 21:03:02 2011 +0200 +++ b/src/plugins/plugin_misc_imap.py Sun Oct 30 23:13:40 2011 +0100 @@ -444,7 +444,7 @@ debug (_("IMAP server connection lost (reason: %s)"), reason) def buildProtocol(self, addr): - debug ("Building protocole") + debug ("Building protocol") prot = protocol.ServerFactory.buildProtocol(self, addr) prot.portal = portal.Portal(ImapRealm(self.host)) prot.portal.registerChecker(SatProfileCredentialChecker(self.host)) diff -r b109a79ac72f -r 62b17854254e src/tools/memory.py --- a/src/tools/memory.py Sat Oct 08 21:03:02 2011 +0200 +++ b/src/tools/memory.py Sun Oct 30 23:13:40 2011 +0100 @@ -27,16 +27,17 @@ from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError from xml.dom import minidom from logging import debug, info, warning, error -import pdb from twisted.internet import defer from twisted.words.protocols.jabber import jid from sat.tools.xml_tools import paramsXml2xmlUI from sat.core.default_config import default_config +from sat.tools.sqlite import SqliteStorage SAVEFILE_PARAM_XML="/param" #xml parameters template SAVEFILE_PARAM_DATA="/param" #individual & general parameters; _ind and _gen suffixes will be added SAVEFILE_HISTORY="/history" SAVEFILE_PRIVATE="/private" #file used to store misc values (mainly for plugins) +SAVEFILE_DATABASE="/sat.db" class Param(): """This class manage parameters with xml""" @@ -75,26 +76,20 @@ """Load parameters template from file""" self.dom = minidom.parse(file) - def load_data(self, file): - """Load parameters data from file""" - file_ind = file + '_ind' - file_gen = file + '_gen' - - if os.path.exists(file_gen): - try: - with open(file_gen, 'r') as file_gen_pickle: - self.params_gen=pickle.load(file_gen_pickle) - debug(_("general params data loaded")) - except: - error (_("Can't load general params data !")) + def loadGenData(self, storage): + """Load general parameters data from storage + @param storage: xxxStorage object instance + @return: deferred triggered once params are loaded""" + return storage.loadGenParams(self.params_gen) - if os.path.exists(file_ind): - try: - with open(file_ind, 'r') as file_ind_pickle: - self.params=pickle.load(file_ind_pickle) - debug(_("individual params data loaded")) - except: - error (_("Can't load individual params data !")) + def loadIndData(self, storage, profile): + """Load individual parameters + set self.params cache + @param storage: xxxStorage object instance + @param profile: profile to load (*must exist*) + @return: deferred triggered once params are loaded""" + self.params[profile] = {} + return storage.loadIndParams(self.params, profile) def save_xml(self, file): """Save parameters template to xml file""" @@ -124,28 +119,25 @@ host.set_const('savefile_param_data', SAVEFILE_PARAM_DATA) host.registerGeneralCB("registerNewAccount", host.registerNewAccountCB) - def getProfilesList(self): - return self.params.keys() - def createProfile(self, name): """Create a new profile @param name: Name of the profile""" - if self.params.has_key(name): + if self.storage.hasProfile(name): info (_('The profile name already exists')) - return 1 + return True if not self.host.trigger.point("ProfileCreation", name): - return 0 + return False self.params[name]={} - return 0 + return False def deleteProfile(self, name): """Delete an existing profile @param name: Name of the profile""" - if not self.params.has_key(name): + if not self.storage.hasProfile(name): error (_('Trying to delete an unknown profile')) - return 1 + return True del self.params[name] - return 0 + return False def getProfileName(self, profile_key): """return profile according to profile_key @@ -159,10 +151,10 @@ default = self.host.memory.getPrivate('Profile_default') if not default or not default in self.params: info(_('No default profile, returning first one')) #TODO: manage real default profile - default = self.params.keys()[0] + default = self.getProfilesList()[0] self.host.memory.setPrivate('Profile_default', default) return default #FIXME: temporary, must use real default value, and fallback to first one if it doesn't exists - if not self.params.has_key(profile_key): + if not self.storage.hasProfile(profile_key): info (_('Trying to access an unknown profile')) return "" return profile_key @@ -396,6 +388,7 @@ if node[0] == 'general': self.params_gen[(category, name)] = value + self.storage.setGenParam(category, name, value) for profile in self.getProfilesList(): if self.host.isConnected(profile): self.host.bridge.paramUpdate(name, value, category, profile) @@ -408,14 +401,18 @@ if type=="button": print "clique",node.toxml() else: - self.params[profile][(category, name)] = value - self.host.bridge.paramUpdate(name, value, category, profile) #TODO: add profile in signal + if self.host.isConnected(profile): #key can not exists if profile is not connected + self.params[profile][(category, name)] = value + self.host.bridge.paramUpdate(name, value, category, profile) + self.storage.setIndParam(category, name, value, profile) class Memory: """This class manage all persistent informations""" def __init__(self, host): info (_("Memory manager init")) + self.initialized = defer.Deferred() + init_defers = [] #list of deferred to wait before initialization is finished self.host = host self.contacts={} self.presenceStatus={} @@ -429,7 +426,14 @@ self.config = self.parseMainConf() host.set_const('savefile_history', SAVEFILE_HISTORY) host.set_const('savefile_private', SAVEFILE_PRIVATE) - self.load() + host.set_const('savefile_database', SAVEFILE_DATABASE) + database_file = os.path.expanduser(self.getConfig('','local_dir')+ + self.host.get_const('savefile_database')) + self.loadFiles() + self.storage = SqliteStorage(database_file) + self.storage.initialized.addCallback(lambda ignore: self.load(init_defers)) + + defer.DeferredList(init_defers).chainDeferred(self.initialized) def parseMainConf(self): """look for main .ini configuration file, and parse it""" @@ -455,7 +459,8 @@ return os.path.expanduser(_value) if name.endswith('_path') or name.endswith('_dir') else _value - def load(self): + + def loadFiles(self): """Load parameters and all memory things from file/db""" param_file_xml = os.path.expanduser(self.getConfig('','local_dir')+ self.host.get_const('savefile_param_xml')) @@ -466,7 +471,7 @@ private_file = os.path.expanduser(self.getConfig('','local_dir')+ self.host.get_const('savefile_private')) - #parameters + #parameters template if os.path.exists(param_file_xml): try: self.params.load_xml(param_file_xml) @@ -478,12 +483,7 @@ info (_("No params template, using default template")) self.params.load_default_params() - try: - self.params.load_data(param_file_data) - debug(_("params loaded")) - except: - error (_("Can't load params !")) - + #history if os.path.exists(history_file): try: @@ -502,6 +502,22 @@ except: error (_("Can't load private values !")) + def load(self, init_defers): + """Load parameters and all memory things from db + @param init_defers: list of deferred to wait before parameters are loaded""" + #parameters data + init_defers.append(self.params.loadGenData(self.storage)) + + def loadIndividualParams(self, profile_key): + """Load individual parameters for a profile + @param profile_key: %(doc_profile_key)s""" + profile = self.getProfileName(profile_key) + if not profile: + error (_('Trying to load parameters for a non-existant profile')) + raise Exception("Profile doesn't exist") + return self.params.loadIndParams(profile) + + def save(self): """Save parameters and all memory things to file/db""" #TODO: need to encrypt files (at least passwords !) and set permissions @@ -525,7 +541,7 @@ debug(_("private values saved")) def getProfilesList(self): - return self.params.getProfilesList() + return self.storage.getProfilesList() def getProfileName(self, profile_key): diff -r b109a79ac72f -r 62b17854254e src/tools/sqlite.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/tools/sqlite.py Sun Oct 30 23:13:40 2011 +0100 @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +SAT: a jabber client +Copyright (C) 2009, 2010, 2011 Jérôme Poisson (goffi@goffi.org) + +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 3 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, see . +""" + + +from logging import debug, info, warning, error +from twisted.enterprise import adbapi +from twisted.internet import defer +import os.path + +class SqliteStorage(): + """This class manage storage with Sqlite database""" + + + def __init__(self, db_filename): + """Connect to the given database + @param db_filename: full path to the Sqlite database""" + self.initialized = defer.Deferred() #triggered when memory is fully initialised and ready + init_defers = [] #list of deferred we have to wait to before initialisation is complete + self.profiles={} #we keep cache for the profiles (key: profile name, value: profile id) + + info(_("Connecting database")) + new_base = not os.path.exists(db_filename) #do we have to create the database ? + self.dbpool = adbapi.ConnectionPool("sqlite3", db_filename, check_same_thread=False) + init_defers.append(self.dbpool.runOperation("PRAGMA foreign_keys = ON").addErrback(lambda x: error(_("Can't activate foreign keys")))) + if new_base: + info(_("The database is new, creating the tables")) + database_creation = [ + "CREATE TABLE profiles (id INTEGER PRIMARY KEY ASC, name TEXT, UNIQUE (name))", + "CREATE TABLE historic (id INTEGER PRIMARY KEY ASC, profile_id INTEGER, source TEXT, dest TEXT, source_res TEXT, dest_res TEXT, timestamp DATETIME, message TEXT, FOREIGN KEY(profile_id) REFERENCES profiles(id))", + "CREATE TABLE param_gen (category TEXT, name TEXT, value TEXT, PRIMARY KEY (category,name))", + "CREATE TABLE param_ind (category TEXT, name TEXT, profile_id INTEGER, value TEXT, PRIMARY KEY (category,name,profile_id), FOREIGN KEY(profile_id) REFERENCES profiles(id))"] + for op in database_creation: + d = self.dbpool.runOperation(op) + d.addErrback(lambda x: error(_("Error while creating tables in database [QUERY: %s]") % op )) + init_defers.append(d) + + def fillProfileCache(ignore): + d = self.dbpool.runQuery("SELECT name,id FROM profiles").addCallback(self._profilesCache) + d.chainDeferred(self.initialized) + + defer.DeferredList(init_defers).addCallback(fillProfileCache) + + #Profiles + def _profilesCache(self, profiles_result): + """Fill the profiles cache + @param profiles_result: result of the sql profiles query""" + for profile in profiles_result: + name, id = profile + self.profiles[name] = id + + def getProfilesList(self): + """"Return list of all registered profiles""" + return self.profiles.keys() + + def hasProfile(self, profile_name): + """return True if profile_name exists + @param profile_name: name of the profile to check""" + return self.profiles.has_key(profile_name) + + def createProfile(self, name): + """Create a new profile + @param name: name of the profile + @return: deferred triggered once profile is actually created""" + def getProfileId(ignore): + return self.dbpool.runQuery("SELECT (id) FROM profiles WHERE name = ?", (name,)) + + def profile_created(profile_id): + _id = profile_id[0][0] + self.profiles[name] = _id #we synchronise the cache + + d = self.dbpool.runQuery("INSERT INTO profiles(name) VALUES (?)", (name,)) + d.addCallback(getProfileId) + d.addCallback(profile_created) + d.addErrback(lambda ignore: error(_("Can't create profile %(name)s" % {"name":name}))) + return d + + #Params + def loadGenParams(self, params_gen): + """Load general parameters + @param params_gen: dictionary to fill + @return: deferred""" + def fillParams(result): + params_gen[(category, name)] = value + debug(_("loading general parameters from database")) + return self.dbpool.runQuery("SELECT category,name,value FROM param_gen").addCallback(fillParams) + + def loadIndParams(self, params_ind, profile): + """Load general parameters + @param params_ind: dictionary to fill + @param profile: a profile which *must* exist + @return: deferred""" + def fillParams(result): + params_ind[profile][(category, name)] = value + debug(_("loading individual parameters from database")) + d = self.dbpool.runQuery("SELECT category,name,value FROM param_gen WHERE profile_id=?", self.profiles[profile]) + d.addCallback(fillParams) + return d + + def setGenParam(self, category, name, value): + """Save the general parameters in database + @param category: category of the parameter + @param name: name of the parameter + @param value: value to set + @return: deferred""" + d = self.dbpool.runQuery("REPLACE INTO param_gen(category,name,value) VALUES (?,?,?)", (category, name, value)) + d.addErrback(lambda ignore: error(_("Can't set general parameter (%(category)s/%(name)s) in database" % {"category":category, "name":name}))) + return d + + def setIndParam(self, category, name, value, profile): + """Save the general parameters in database + @param category: category of the parameter + @param name: name of the parameter + @param value: value to set + @param profile: a profile which *must* exist + @return: deferred""" + d = self.dbpool.runQuery("REPLACE INTO param_ind(category,name,profile_id,value) VALUES (?,?,?,?)", (category, name, self.profiles[profile], value)) + d.addErrback(lambda ignore: error(_("Can't set individual parameter (%(category)s/%(name)s) for [%(profile)s] in database" % {"category":category, "name":name, "profile":profile}))) + return d