From b3090f710e3777bad2a9f996444e5099883c9f03 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 10 Dec 2012 12:05:31 -0200 Subject: Add CouchDB u1db backend. --- src/leap/soledad/README | 2 + src/leap/soledad/__init__.py | 5 +- src/leap/soledad/backends/couchdb.py | 97 ++++++++++++++++++++++++++++++++ src/leap/soledad/backends/objectstore.py | 26 +++++++++ src/leap/soledad/backends/openstack.py | 20 ++----- 5 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 src/leap/soledad/backends/couchdb.py diff --git a/src/leap/soledad/README b/src/leap/soledad/README index 894ce6af..97976b01 100644 --- a/src/leap/soledad/README +++ b/src/leap/soledad/README @@ -11,7 +11,9 @@ Soledad depends on the following python libraries: * u1db 0.1.4 [1] * python-swiftclient 1.2.0 [2] * python-gnupg 0.3.1 [3] + * CouchDB 0.8 [4] [1] http://pypi.python.org/pypi/u1db/0.1.4 [2] http://pypi.python.org/pypi/python-swiftclient/1.2.0 [3] http://pypi.python.org/pypi/python-gnupg/0.3.1 +[4] http://pypi.python.org/pypi/CouchDB/0.8 diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index 78f1f768..d07567b5 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -1,9 +1,6 @@ # License? -"""A U1DB implementation that uses OpenStack Swift as its persistence layer.""" - -from backends.leap import * -from backends.openstack import * +"""A U1DB implementation for using Object Stores as its persistence layer.""" import gnupg diff --git a/src/leap/soledad/backends/couchdb.py b/src/leap/soledad/backends/couchdb.py new file mode 100644 index 00000000..89b713f9 --- /dev/null +++ b/src/leap/soledad/backends/couchdb.py @@ -0,0 +1,97 @@ +from u1db import errors +from u1db.remote.http_target import HTTPSyncTarget +from couchdb import * +from soledad.backends.objectstore import ObjectStore + + +class CouchDatabase(ObjectStore): + """A U1DB implementation that uses Couch as its persistence layer.""" + + def __init__(self, url, database, full_commit=True, session=None): + """Create a new Couch data container.""" + self._url = url + self._full_commit = full_commit + self._session = session + self._server = couchdb.Server(url=self._url, + full_commit=self._full_commit, + session=self._session) + # this will ensure that transaction and sync logs exist and are + # up-to-date. + super(CouchDatabase, self) + self._database = self._server[database] + + #------------------------------------------------------------------------- + # implemented methods from Database + #------------------------------------------------------------------------- + + def _get_doc(self, doc_id, check_for_conflicts=False): + """Get just the document content, without fancy handling. + + Conflicts do not happen on server side, so there's no need to check + for them. + """ + cdoc = self._database.get(doc_id) + if cdoc is not None: + content = {} + for key, value in content: + if not key in ['_id', '_rev', '_u1db_rev']: + content[key] = value + doc = self._factory(doc_id=doc_id, rev=cdoc['_u1db_rev']) + doc.content = content + return doc + + def get_all_docs(self, include_deleted=False): + """Get all documents from the database.""" + generation = self._get_generation() + results = [] + for doc_id in self._database: + doc = self._get_doc(doc_id) + if doc.content is None and not include_deleted: + continue + results.append(doc) + return (generation, results) + + def _put_doc(self, doc, new_rev): + # map u1db metadata to couch + content = doc.content + content['_id'] = doc.doc_id + content['_u1db_rev'] = new_rev + self._database.save(doc.content) + + def get_sync_target(self): + return CouchSyncTarget(self) + + def close(self): + raise NotImplementedError(self.close) + + def sync(self, url, creds=None, autocreate=True): + from u1db.sync import Synchronizer + from u1db.remote.http_target import CouchSyncTarget + return Synchronizer(self, CouchSyncTarget(url, creds=creds)).sync( + autocreate=autocreate) + + #------------------------------------------------------------------------- + # Couch specific methods + #------------------------------------------------------------------------- + + # no specific methods so far. + +class CouchSyncTarget(HTTPSyncTarget): + + def get_sync_info(self, source_replica_uid): + source_gen, source_trans_id = self._db._get_replica_gen_and_trans_id( + source_replica_uid) + my_gen, my_trans_id = self._db._get_generation_info() + return ( + self._db._replica_uid, my_gen, my_trans_id, source_gen, + source_trans_id) + + def record_sync_info(self, source_replica_uid, source_replica_generation, + source_replica_transaction_id): + if self._trace_hook: + self._trace_hook('record_sync_info') + self._db._set_replica_gen_and_trans_id( + source_replica_uid, source_replica_generation, + source_replica_transaction_id) + + diff --git a/src/leap/soledad/backends/objectstore.py b/src/leap/soledad/backends/objectstore.py index e36df72d..456892b3 100644 --- a/src/leap/soledad/backends/objectstore.py +++ b/src/leap/soledad/backends/objectstore.py @@ -1,11 +1,17 @@ from u1db.backends import CommonBackend +from soledad import SyncLog, TransactionLog class ObjectStore(CommonBackend): def __init__(self): + # This initialization method should be called after the connection + # with the database is established, so it can ensure that u1db data is + # configured and up-to-date. + self.set_document_factory(LeapDocument) self._sync_log = SyncLog() self._transaction_log = TransactionLog() + self._ensure_u1db_data() #------------------------------------------------------------------------- # implemented methods from Database @@ -29,6 +35,26 @@ class ObjectStore(CommonBackend): return None return doc + def _put_doc(self, doc) + raise NotImplementedError(self._put_doc) + + def put_doc(self, doc) + # consistency check + if doc.doc_id is None: + raise errors.InvalidDocId() + self._check_doc_id(doc.doc_id) + self._check_doc_size(doc) + # put the document + new_rev = self._allocate_doc_rev(doc.rev) + self._put_doc(doc, new_rev) + doc.rev = new_rev + # update u1db generation and logs + new_gen = self._get_generation() + 1 + trans_id = self._allocate_transaction_id() + self._transaction_log.append((new_gen, doc.doc_id, trans_id)) + self._set_u1db_data() + return new_rev + def delete_doc(self, doc): old_doc = self._get_doc(doc.doc_id, check_for_conflicts=True) if old_doc is None: diff --git a/src/leap/soledad/backends/openstack.py b/src/leap/soledad/backends/openstack.py index f8563d81..5f2a2771 100644 --- a/src/leap/soledad/backends/openstack.py +++ b/src/leap/soledad/backends/openstack.py @@ -1,5 +1,4 @@ from u1db import errors -from u1db.backends import CommonBackend from u1db.remote.http_target import HTTPSyncTarget from swiftclient import client from soledad.backends.objectstore import ObjectStore @@ -10,16 +9,15 @@ class OpenStackDatabase(ObjectStore): def __init__(self, auth_url, user, auth_key, container): """Create a new OpenStack data container.""" - super(OpenStackDatabase, self) self._auth_url = auth_url self._user = user self._auth_key = auth_key self._container = container - self.set_document_factory(LeapDocument) self._connection = swiftclient.Connection(self._auth_url, self._user, self._auth_key) self._get_auth() - self._ensure_u1db_data() + # this will ensure transaction and sync logs exist and are up-to-date. + super(OpenStackDatabase, self) #------------------------------------------------------------------------- # implemented methods from Database @@ -33,6 +31,7 @@ class OpenStackDatabase(ObjectStore): """ try: response, contents = self._connection.get_object(self._container, doc_id) + # TODO: change revision to be a dictionary element? rev = response['x-object-meta-rev'] return self._factory(doc_id, rev, contents) except swiftclient.ClientException: @@ -51,21 +50,12 @@ class OpenStackDatabase(ObjectStore): results.append(doc) return (generation, results) - def put_doc(self, doc): - if doc.doc_id is None: - raise errors.InvalidDocId() - self._check_doc_id(doc.doc_id) - self._check_doc_size(doc) - # TODO: check for conflicts? + def _put_doc(self, doc, new_rev): new_rev = self._allocate_doc_rev(doc.rev) + # TODO: change revision to be a dictionary element? headers = { 'X-Object-Meta-Rev' : new_rev } self._connection.put_object(self._container, doc_id, doc.get_json(), headers=headers) - new_gen = self._get_generation() + 1 - trans_id = self._allocate_transaction_id() - self._transaction_log.append((new_gen, doc.doc_id, trans_id)) - self._set_u1db_data() - return new_rev def get_sync_target(self): return OpenStackSyncTarget(self) -- cgit v1.2.3