summaryrefslogtreecommitdiff
path: root/soledad_server/src/leap/soledad_server/objectstore.py
diff options
context:
space:
mode:
Diffstat (limited to 'soledad_server/src/leap/soledad_server/objectstore.py')
-rw-r--r--soledad_server/src/leap/soledad_server/objectstore.py296
1 files changed, 296 insertions, 0 deletions
diff --git a/soledad_server/src/leap/soledad_server/objectstore.py b/soledad_server/src/leap/soledad_server/objectstore.py
new file mode 100644
index 00000000..8afac3ec
--- /dev/null
+++ b/soledad_server/src/leap/soledad_server/objectstore.py
@@ -0,0 +1,296 @@
+# -*- coding: utf-8 -*-
+# objectstore.py
+# Copyright (C) 2013 LEAP
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+
+"""
+Abstract U1DB backend to handle storage using object stores (like CouchDB, for
+example).
+
+Right now, this is only used by CouchDatabase backend, but can also be
+extended to implement OpenStack or Amazon S3 storage, for example.
+
+See U1DB documentation for more information on how to use databases.
+"""
+
+from u1db.backends.inmemory import (
+ InMemoryDatabase,
+ InMemorySyncTarget,
+)
+from u1db import errors
+
+
+class ObjectStoreDatabase(InMemoryDatabase):
+ """
+ A backend for storing u1db data in an object store.
+ """
+
+ @classmethod
+ def open_database(cls, url, create, document_factory=None):
+ """
+ Open a U1DB database using an object store as backend.
+
+ @param url: the url of the database replica
+ @type url: str
+ @param create: should the replica be created if it does not exist?
+ @type create: bool
+ @param document_factory: A function that will be called with the same
+ parameters as Document.__init__.
+ @type document_factory: callable
+
+ @return: the database instance
+ @rtype: CouchDatabase
+ """
+ raise NotImplementedError(cls.open_database)
+
+ def __init__(self, replica_uid=None, document_factory=None):
+ """
+ Initialize the object store database.
+
+ @param replica_uid: an optional unique replica identifier
+ @type replica_uid: str
+ @param document_factory: A function that will be called with the same
+ parameters as Document.__init__.
+ @type document_factory: callable
+ """
+ InMemoryDatabase.__init__(
+ self,
+ replica_uid,
+ document_factory=document_factory)
+ # sync data in memory with data in object store
+ if not self._get_doc(self.U1DB_DATA_DOC_ID):
+ self._init_u1db_data()
+ self._fetch_u1db_data()
+
+ #-------------------------------------------------------------------------
+ # methods from Database
+ #-------------------------------------------------------------------------
+
+ def _set_replica_uid(self, replica_uid):
+ """
+ Force the replica_uid to be set.
+
+ @param replica_uid: The uid of the replica.
+ @type replica_uid: str
+ """
+ InMemoryDatabase._set_replica_uid(self, replica_uid)
+ self._store_u1db_data()
+
+ def _put_doc(self, doc):
+ """
+ Update a document.
+
+ This is called everytime we just want to do a raw put on the db (i.e.
+ without index updates, document constraint checks, and conflict
+ checks).
+
+ @param doc: The document to update.
+ @type doc: u1db.Document
+
+ @return: The new revision identifier for the document.
+ @rtype: str
+ """
+ raise NotImplementedError(self._put_doc)
+
+ def _get_doc(self, doc_id):
+ """
+ Get just the document content, without fancy handling.
+
+ @param doc_id: The unique document identifier
+ @type doc_id: str
+ @param include_deleted: If set to True, deleted documents will be
+ returned with empty content. Otherwise asking for a deleted
+ document will return None.
+ @type include_deleted: bool
+
+ @return: a Document object.
+ @type: u1db.Document
+ """
+ raise NotImplementedError(self._get_doc)
+
+ def get_all_docs(self, include_deleted=False):
+ """
+ Get the JSON content for all documents in the database.
+
+ @param include_deleted: If set to True, deleted documents will be
+ returned with empty content. Otherwise deleted documents will not
+ be included in the results.
+ @type include_deleted: bool
+
+ @return: (generation, [Document])
+ The current generation of the database, followed by a list of all
+ the documents in the database.
+ @rtype: tuple
+ """
+ raise NotImplementedError(self.get_all_docs)
+
+ def delete_doc(self, doc):
+ """
+ Mark a document as deleted.
+
+ @param doc: The document to mark as deleted.
+ @type doc: u1db.Document
+
+ @return: The new revision id of the document.
+ @type: str
+ """
+ old_doc = self._get_doc(doc.doc_id, check_for_conflicts=True)
+ if old_doc is None:
+ raise errors.DocumentDoesNotExist
+ if old_doc.rev != doc.rev:
+ raise errors.RevisionConflict()
+ if old_doc.is_tombstone():
+ raise errors.DocumentAlreadyDeleted
+ if old_doc.has_conflicts:
+ raise errors.ConflictedDoc()
+ new_rev = self._allocate_doc_rev(doc.rev)
+ doc.rev = new_rev
+ doc.make_tombstone()
+ self._put_and_update_indexes(old_doc, doc)
+ return new_rev
+
+ # index-related methods
+
+ def create_index(self, index_name, *index_expressions):
+ """
+ Create a named index, which can then be queried for future lookups.
+
+ See U1DB documentation for more information.
+
+ @param index_name: A unique name which can be used as a key prefix.
+ @param index_expressions: Index expressions defining the index
+ information.
+ """
+ raise NotImplementedError(self.create_index)
+
+ def delete_index(self, index_name):
+ """
+ Remove a named index.
+
+ Here we just guarantee that the new info will be stored in the backend
+ db after update.
+
+ @param index_name: The name of the index we are removing.
+ @type index_name: str
+ """
+ InMemoryDatabase.delete_index(self, index_name)
+ self._store_u1db_data()
+
+ def _replace_conflicts(self, doc, conflicts):
+ """
+ Set new conflicts for a document.
+
+ Here we just guarantee that the new info will be stored in the backend
+ db after update.
+
+ @param doc: The document with a new set of conflicts.
+ @param conflicts: The new set of conflicts.
+ @type conflicts: list
+ """
+ InMemoryDatabase._replace_conflicts(self, doc, conflicts)
+ self._store_u1db_data()
+
+ def _do_set_replica_gen_and_trans_id(self, other_replica_uid,
+ other_generation,
+ other_transaction_id):
+ """
+ Set the last-known generation and transaction id for the other
+ database replica.
+
+ Here we just guarantee that the new info will be stored in the backend
+ db after update.
+
+ @param other_replica_uid: The U1DB identifier for the other replica.
+ @type other_replica_uid: str
+ @param other_generation: The generation number for the other replica.
+ @type other_generation: int
+ @param other_transaction_id: The transaction id associated with the
+ generation.
+ @type other_transaction_id: str
+ """
+ InMemoryDatabase._do_set_replica_gen_and_trans_id(
+ self,
+ other_replica_uid,
+ other_generation,
+ other_transaction_id)
+ self._store_u1db_data()
+
+ #-------------------------------------------------------------------------
+ # implemented methods from CommonBackend
+ #-------------------------------------------------------------------------
+
+ def _put_and_update_indexes(self, old_doc, doc):
+ """
+ Update a document and all indexes related to it.
+
+ @param old_doc: The old version of the document.
+ @type old_doc: u1db.Document
+ @param doc: The new version of the document.
+ @type doc: u1db.Document
+ """
+ for index in self._indexes.itervalues():
+ if old_doc is not None and not old_doc.is_tombstone():
+ index.remove_json(old_doc.doc_id, old_doc.get_json())
+ if not doc.is_tombstone():
+ index.add_json(doc.doc_id, doc.get_json())
+ trans_id = self._allocate_transaction_id()
+ self._put_doc(doc)
+ self._transaction_log.append((doc.doc_id, trans_id))
+ self._store_u1db_data()
+
+ #-------------------------------------------------------------------------
+ # methods specific for object stores
+ #-------------------------------------------------------------------------
+
+ U1DB_DATA_DOC_ID = 'u1db_data'
+
+ def _fetch_u1db_data(self):
+ """
+ Fetch u1db configuration data from backend storage.
+
+ See C{_init_u1db_data} documentation.
+ """
+ NotImplementedError(self._fetch_u1db_data)
+
+ def _store_u1db_data(self):
+ """
+ Store u1db configuration data on backend storage.
+
+ See C{_init_u1db_data} documentation.
+ """
+ NotImplementedError(self._store_u1db_data)
+
+ def _init_u1db_data(self):
+ """
+ Initialize u1db configuration data on backend storage.
+
+ A U1DB database needs to keep track of all database transactions,
+ document conflicts, the generation of other replicas it has seen,
+ indexes created by users and so on.
+
+ In this implementation, all this information is stored in a special
+ document stored in the couch db with id equals to
+ CouchDatabse.U1DB_DATA_DOC_ID.
+
+ This method initializes the document that will hold such information.
+ """
+ NotImplementedError(self._init_u1db_data)
+
+
+class ObjectStoreSyncTarget(InMemorySyncTarget):
+ """
+ Functionality for using an ObjectStore as a synchronization target.
+ """