From 0093b9b7cf79b0e2e83cc76783152cc56c98f572 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 5 Jun 2015 11:21:17 -0400 Subject: [feature] add post-sync hooks using twisted plugins implementing a generic plugin interface to allow other modules to react to soledad syncs, receiving a list of document ids that they've subscribed to. - Resolves: #6996 - Releases: 0.7.1 --- .gitignore | 1 + client/src/leap/soledad/client/adbapi.py | 1 + client/src/leap/soledad/client/api.py | 37 ++++++++++++++++++++++++++++ client/src/leap/soledad/client/interfaces.py | 31 +++++++++++++++++++++++ client/src/leap/soledad/client/sqlcipher.py | 13 +++++++++- client/src/leap/soledad/client/sync.py | 16 ++++++++++++ 6 files changed, 98 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c502541e..6a0003cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +dropin.cache *.log *.pyc dist/ diff --git a/client/src/leap/soledad/client/adbapi.py b/client/src/leap/soledad/client/adbapi.py index 5b882bbe..4f75695f 100644 --- a/client/src/leap/soledad/client/adbapi.py +++ b/client/src/leap/soledad/client/adbapi.py @@ -217,6 +217,7 @@ class U1DBConnectionPool(adbapi.ConnectionPool): """ meth = getattr(trans, meth) return meth(*args, **kw) + # XXX should return a fetchall? def _runInteraction(self, interaction, *args, **kw): """ diff --git a/client/src/leap/soledad/client/api.py b/client/src/leap/soledad/client/api.py index 76d6acc3..63b1dfc0 100644 --- a/client/src/leap/soledad/client/api.py +++ b/client/src/leap/soledad/client/api.py @@ -42,6 +42,7 @@ except ImportError: from StringIO import StringIO from u1db.remote import http_client from u1db.remote.ssl_match_hostname import match_hostname +from twisted.plugin import getPlugins from zope.interface import implements from leap.common.config import get_path_prefix @@ -70,6 +71,28 @@ Soledad client and server. """ SOLEDAD_CERT = None +# A whitelist of modules from where to collect plugins dynamically. +# For the moment restricted to leap namespace, but the idea is that we can pass +# other "trusted" modules as options to the initialization of soledad. +PLUGGABLE_LEAP_MODULES = ('mail', 'keymanager') + + +# TODO move to leap.common + +def collect_plugins(interface): + """ + Traverse a whitelist of modules and collect all the plugins that implement + the passed interface. + """ + plugins = [] + for namespace in PLUGGABLE_LEAP_MODULES: + try: + module = __import__('leap.%s.plugins' % namespace, fromlist='.') + plugins = plugins + list(getPlugins(interface, module)) + except ImportError: + pass + return plugins + class Soledad(object): """ @@ -656,6 +679,20 @@ class Soledad(object): defer_decryption=defer_decryption) def _sync_callback(local_gen): + self._last_received_docs = self._dbsyncer.received_docs + print "***" + print "LAST RECEIVED (API)", self._last_received_docs + print "***" + received_doc_ids = self._dbsyncer.received_docs + + # Post-Sync Hooks + synced_plugin = soledad_interfaces.ISoledadPostSyncPlugin + if received_doc_ids: + suitable_plugins = collect_plugins(synced_plugin) + for plugin in suitable_plugins: + # TODO filter the doc_ids here + plugin.process_received_docs(received_doc_ids) + soledad_events.emit( soledad_events.SOLEDAD_DONE_DATA_SYNC, self.uuid) return local_gen diff --git a/client/src/leap/soledad/client/interfaces.py b/client/src/leap/soledad/client/interfaces.py index 4f7b0779..14b34d24 100644 --- a/client/src/leap/soledad/client/interfaces.py +++ b/client/src/leap/soledad/client/interfaces.py @@ -19,6 +19,37 @@ Interfaces used by the Soledad Client. """ from zope.interface import Interface, Attribute +# +# Plugins +# + + +class ISoledadPostSyncPlugin(Interface): + """ + I implement the minimal methods and attributes for a plugin that can be + called after a soledad synchronization has ended. + """ + + def process_received_docs(self, doc_id_list): + """ + Do something with the passed list of doc_ids received after the last + sync. + + :param doc_id_list: a list of strings for the received doc_ids + """ + + watched_doc_types = Attribute(""" + a tuple of the watched doc types for this plugin. So far, the + `doc-types` convention is just the preffix of the doc_id, which is + basically its first character, followed by a dash. So, for instance, + `M-` is used for meta-docs in mail, and `F-` is used for flag-docs in + mail. For now there's no central register of all the doc-types + used.""") + + +# +# Soledad storage +# class ILocalStorage(Interface): """ diff --git a/client/src/leap/soledad/client/sqlcipher.py b/client/src/leap/soledad/client/sqlcipher.py index b2025130..75d786a6 100644 --- a/client/src/leap/soledad/client/sqlcipher.py +++ b/client/src/leap/soledad/client/sqlcipher.py @@ -456,6 +456,9 @@ class SQLCipherU1DBSync(SQLCipherDatabase): self._syncers = {} + # Storage for the documents received during a sync + self.received_docs = [] + self.running = False self._sync_threadpool = None self._initialize_sync_threadpool() @@ -587,8 +590,16 @@ class SQLCipherU1DBSync(SQLCipherDatabase): # the following context manager blocks until the syncing lock can be # acquired. with self._syncer(url, creds=creds) as syncer: + + def _record_received_docs(result): + # beware, closure. syncer is in scope. + self.received_docs = syncer.received_docs + return result + # XXX could mark the critical section here... - return syncer.sync(defer_decryption=defer_decryption) + d = syncer.sync(defer_decryption=defer_decryption) + d.addCallback(_record_received_docs) + return d @contextmanager def _syncer(self, url, creds=None): diff --git a/client/src/leap/soledad/client/sync.py b/client/src/leap/soledad/client/sync.py index 53172f31..917c21ea 100644 --- a/client/src/leap/soledad/client/sync.py +++ b/client/src/leap/soledad/client/sync.py @@ -39,6 +39,7 @@ class SoledadSynchronizer(Synchronizer): Also modified to allow for interrupting the synchronization process. """ + received_docs = [] @defer.inlineCallbacks def sync(self, defer_decryption=True): @@ -62,6 +63,7 @@ class SoledadSynchronizer(Synchronizer): :rtype: twisted.internet.defer.Deferred """ sync_target = self.sync_target + self.received_docs = [] # get target identifier, its current generation, # and its last-seen database generation for this source @@ -123,12 +125,14 @@ class SoledadSynchronizer(Synchronizer): changed_doc_ids = [doc_id for doc_id, _, _ in changes] docs_to_send = self.source.get_docs( changed_doc_ids, check_for_conflicts=False, include_deleted=True) + ids_sent = [] docs_by_generation = [] idx = 0 for doc in docs_to_send: _, gen, trans = changes[idx] docs_by_generation.append((doc, gen, trans)) idx += 1 + ids_sent.append(doc.doc_id) # exchange documents and try to insert the returned ones with # the target, return target synced-up-to gen. @@ -151,6 +155,18 @@ class SoledadSynchronizer(Synchronizer): self._syncing_info = info yield self.complete_sync() + _, _, changes = self.source.whats_changed(target_my_gen) + changed_doc_ids = [doc_id for doc_id, _, _ in changes] + + print "--------------------------" + print "SENT", ids_sent + print "CHANGED_DOC_IDS", changed_doc_ids + + just_received = list(set(changed_doc_ids) - set(ids_sent)) + print "RECEIVED:", just_received + print "--------------------------" + + self.received_docs = just_received defer.returnValue(my_gen) def complete_sync(self): -- cgit v1.2.3