summaryrefslogtreecommitdiff
path: root/backends
diff options
context:
space:
mode:
Diffstat (limited to 'backends')
-rw-r--r--backends/couch.py56
-rw-r--r--backends/leap_backend.py57
-rw-r--r--backends/objectstore.py9
-rw-r--r--backends/openstack.py98
-rw-r--r--backends/sqlcipher.py41
5 files changed, 110 insertions, 151 deletions
diff --git a/backends/couch.py b/backends/couch.py
index c8dadfa8..d349efaf 100644
--- a/backends/couch.py
+++ b/backends/couch.py
@@ -1,10 +1,17 @@
+# general imports
import uuid
from base64 import b64encode, b64decode
+import re
+# u1db
from u1db import errors
from u1db.sync import LocalSyncTarget
from u1db.backends.inmemory import InMemoryIndex
+from u1db.remote.server_state import ServerState
+from u1db.errors import DatabaseDoesNotExist
+# couchdb
from couchdb.client import Server, Document as CouchDocument
from couchdb.http import ResourceNotFound
+# leap
from leap.soledad.backends.objectstore import ObjectStore
from leap.soledad.backends.leap_backend import LeapDocument
@@ -14,9 +21,29 @@ except ImportError:
import json # noqa
+class InvalidURLError(Exception):
+ pass
+
+
class CouchDatabase(ObjectStore):
"""A U1DB implementation that uses Couch as its persistence layer."""
+ @classmethod
+ def open_database(cls, url, create):
+ # get database from url
+ m = re.match('(^https?://[^/]+)/(.+)$', url)
+ if not m:
+ raise InvalidURLError
+ url = m.group(1)
+ dbname = m.group(2)
+ server = Server(url=url)
+ try:
+ server[dbname]
+ except ResourceNotFound:
+ if not create:
+ raise DatabaseDoesNotExist()
+ return cls(url, dbname)
+
def __init__(self, url, database, replica_uid=None, full_commit=True,
session=None):
"""Create a new Couch data container."""
@@ -29,13 +56,13 @@ class CouchDatabase(ObjectStore):
self._dbname = database
# this will ensure that transaction and sync logs exist and are
# up-to-date.
- self.set_document_factory(LeapDocument)
try:
self._database = self._server[database]
except ResourceNotFound:
self._server.create(database)
self._database = self._server[database]
- super(CouchDatabase, self).__init__(replica_uid=replica_uid)
+ super(CouchDatabase, self).__init__(replica_uid=replica_uid,
+ document_factory=LeapDocument)
#-------------------------------------------------------------------------
# methods from Database
@@ -215,3 +242,28 @@ class CouchSyncTarget(LocalSyncTarget):
self._db._set_replica_gen_and_trans_id(
source_replica_uid, source_replica_generation,
source_replica_transaction_id)
+
+
+class CouchServerState(ServerState):
+ """
+ Inteface of the WSGI server with the CouchDB backend.
+ """
+
+ def __init__(self, couch_url):
+ self.couch_url = couch_url
+
+ def open_database(self, dbname):
+ # TODO: open couch
+ from leap.soledad.backends.couch import CouchDatabase
+ return CouchDatabase.open_database(self.couch_url + '/' + dbname,
+ create=False)
+
+ def ensure_database(self, dbname):
+ from leap.soledad.backends.couch import CouchDatabase
+ db = CouchDatabase.open_database(self.couch_url + '/' + dbname,
+ create=True)
+ return db, db._replica_uid
+
+ def delete_database(self, dbname):
+ from leap.soledad.backends.couch import CouchDatabase
+ CouchDatabase.delete_database(self.couch_url + '/' + dbname)
diff --git a/backends/leap_backend.py b/backends/leap_backend.py
index f73698f2..c3c52ee6 100644
--- a/backends/leap_backend.py
+++ b/backends/leap_backend.py
@@ -26,6 +26,8 @@ class DocumentEncryptionFailed(Exception):
class LeapDocument(Document):
"""
+ Encryptable and syncable document.
+
LEAP Documents are standard u1db documents with cabability of returning an
encrypted version of the document json string as well as setting document
content based on an encrypted version of json string.
@@ -41,7 +43,7 @@ class LeapDocument(Document):
def get_encrypted_json(self):
"""
- Returns document's json serialization encrypted with user's public key.
+ Return document's json serialization encrypted with user's public key.
"""
if not self._soledad:
raise NoSoledadInstance()
@@ -71,47 +73,28 @@ class LeapDocument(Document):
doc="Determine if document should be synced with server."
)
+ # Returning the revision as string solves the following exception in
+ # Twisted web:
+ # exceptions.TypeError: Can only pass-through bytes on Python 2
+ def _get_rev(self):
+ if self._rev is None:
+ return None
+ return str(self._rev)
-class LeapDatabase(HTTPDatabase):
- """Implement the HTTP remote database API to a Leap server."""
+ def _set_rev(self, rev):
+ self._rev = rev
- def __init__(self, url, document_factory=None, creds=None, soledad=None):
- super(LeapDatabase, self).__init__(url, creds=creds)
- self._soledad = soledad
- self._factory = LeapDocument
-
- @staticmethod
- def open_database(url, create):
- db = LeapDatabase(url)
- db.open(create)
- return db
-
- @staticmethod
- def delete_database(url):
- db = LeapDatabase(url)
- db._delete()
- db.close()
-
- def _allocate_doc_id(self):
- """Generate a unique identifier for this document."""
- return 'D-' + uuid.uuid4().hex # 'D-' stands for document
-
- def get_sync_target(self):
- st = LeapSyncTarget(self._url.geturl())
- st._creds = self._creds
- return st
-
- def create_doc_from_json(self, content, doc_id=None):
- if doc_id is None:
- doc_id = self._allocate_doc_id()
- res, headers = self._request_json('PUT', ['doc', doc_id], {},
- content, 'application/json')
- new_doc = self._factory(doc_id, res['rev'], content,
- soledad=self._soledad)
- return new_doc
+ rev = property(
+ _get_rev,
+ _set_rev,
+ doc="Wrapper to ensure `doc.rev` is always returned as bytes.")
class LeapSyncTarget(HTTPSyncTarget):
+ """
+ A SyncTarget that encrypts data before sending and decrypts data after
+ receiving.
+ """
def __init__(self, url, creds=None, soledad=None):
super(LeapSyncTarget, self).__init__(url, creds)
diff --git a/backends/objectstore.py b/backends/objectstore.py
index 588fc7a1..199107af 100644
--- a/backends/objectstore.py
+++ b/backends/objectstore.py
@@ -7,8 +7,13 @@ class ObjectStore(InMemoryDatabase):
A backend for storing u1db data in an object store.
"""
- def __init__(self, replica_uid=None):
- super(ObjectStore, self).__init__(replica_uid)
+ @classmethod
+ def open_database(cls, url, create, document_factory=None):
+ raise NotImplementedError(cls.open_database)
+
+ def __init__(self, replica_uid=None, document_factory=None):
+ super(ObjectStore, self).__init__(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()
diff --git a/backends/openstack.py b/backends/openstack.py
deleted file mode 100644
index a9615736..00000000
--- a/backends/openstack.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# TODO: this backend is not tested yet.
-from u1db.remote.http_target import HTTPSyncTarget
-import swiftclient
-from soledad.backends.objectstore import ObjectStore
-
-
-class OpenStackDatabase(ObjectStore):
- """A U1DB implementation that uses OpenStack as its persistence layer."""
-
- def __init__(self, auth_url, user, auth_key, container):
- """Create a new OpenStack data container."""
- self._auth_url = auth_url
- self._user = user
- self._auth_key = auth_key
- self._container = container
- self._connection = swiftclient.Connection(self._auth_url, self._user,
- self._auth_key)
- self._get_auth()
- # this will ensure transaction and sync logs exist and are up-to-date.
- super(OpenStackDatabase, self).__init__()
-
- #-------------------------------------------------------------------------
- # 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.
- """
- 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:
- return None
-
- def get_all_docs(self, include_deleted=False):
- """Get all documents from the database."""
- generation = self._get_generation()
- results = []
- _, doc_ids = self._connection.get_container(self._container,
- full_listing=True)
- for doc_id in doc_ids:
- 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):
- 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)
-
- def get_sync_target(self):
- return OpenStackSyncTarget(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 OpenStackSyncTarget
- return Synchronizer(self, OpenStackSyncTarget(url, creds=creds)).sync(
- autocreate=autocreate)
-
- #-------------------------------------------------------------------------
- # OpenStack specific methods
- #-------------------------------------------------------------------------
-
- def _get_auth(self):
- self._url, self._auth_token = self._connection.get_auth()
- return self._url, self.auth_token
-
-
-class OpenStackSyncTarget(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/backends/sqlcipher.py b/backends/sqlcipher.py
index 6cebcf7d..9108f73a 100644
--- a/backends/sqlcipher.py
+++ b/backends/sqlcipher.py
@@ -31,7 +31,7 @@ from u1db import (
from leap.soledad.backends.leap_backend import LeapDocument
-def open(path, password, create=True, document_factory=None):
+def open(path, password, create=True, document_factory=None, soledad=None):
"""Open a database at the given location.
Will raise u1db.errors.DatabaseDoesNotExist if create=False and the
@@ -45,7 +45,8 @@ def open(path, password, create=True, document_factory=None):
:return: An instance of Database.
"""
return SQLCipherDatabase.open_database(
- path, password, create=create, document_factory=document_factory)
+ path, password, create=create, document_factory=document_factory,
+ soledad=soledad)
class DatabaseIsNotEncrypted(Exception):
@@ -64,14 +65,23 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase):
def set_pragma_key(cls, db_handle, key):
db_handle.cursor().execute("PRAGMA key = '%s'" % key)
- def __init__(self, sqlite_file, password, document_factory=None):
+ def __init__(self, sqlite_file, password, document_factory=None,
+ soledad=None):
"""Create a new sqlcipher file."""
self._check_if_db_is_encrypted(sqlite_file)
self._db_handle = dbapi2.connect(sqlite_file)
SQLCipherDatabase.set_pragma_key(self._db_handle, password)
self._real_replica_uid = None
self._ensure_schema()
- self._factory = document_factory or LeapDocument
+ self._soledad = soledad
+
+ def factory(doc_id=None, rev=None, json='{}', has_conflicts=False,
+ encrypted_json=None, syncable=True):
+ return LeapDocument(doc_id=doc_id, rev=rev, json=json,
+ has_conflicts=has_conflicts,
+ encrypted_json=encrypted_json,
+ syncable=syncable, soledad=self._soledad)
+ self.set_document_factory(factory)
def _check_if_db_is_encrypted(self, sqlite_file):
if not os.path.exists(sqlite_file):
@@ -86,7 +96,8 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase):
pass
@classmethod
- def _open_database(cls, sqlite_file, password, document_factory=None):
+ def _open_database(cls, sqlite_file, password, document_factory=None,
+ soledad=None):
if not os.path.isfile(sqlite_file):
raise errors.DatabaseDoesNotExist()
tries = 2
@@ -108,14 +119,16 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase):
tries -= 1
time.sleep(cls.WAIT_FOR_PARALLEL_INIT_HALF_INTERVAL)
return SQLCipherDatabase._sqlite_registry[v](
- sqlite_file, password, document_factory=document_factory)
+ sqlite_file, password, document_factory=document_factory,
+ soledad=soledad)
@classmethod
def open_database(cls, sqlite_file, password, create, backend_cls=None,
- document_factory=None):
+ document_factory=None, soledad=None):
try:
return cls._open_database(sqlite_file, password,
- document_factory=document_factory)
+ document_factory=document_factory,
+ soledad=soledad)
except errors.DatabaseDoesNotExist:
if not create:
raise
@@ -123,16 +136,20 @@ class SQLCipherDatabase(SQLitePartialExpandDatabase):
# default is SQLCipherPartialExpandDatabase
backend_cls = SQLCipherDatabase
return backend_cls(sqlite_file, password,
- document_factory=document_factory)
+ document_factory=document_factory,
+ soledad=soledad)
- def sync(self, url, creds=None, autocreate=True, soledad=None):
+ def sync(self, url, creds=None, autocreate=True):
"""
Synchronize encrypted documents with remote replica exposed at url.
"""
from u1db.sync import Synchronizer
from leap.soledad.backends.leap_backend import LeapSyncTarget
- return Synchronizer(self, LeapSyncTarget(url, creds=creds),
- soledad=self._soledad).sync(autocreate=autocreate)
+ return Synchronizer(
+ self,
+ LeapSyncTarget(url,
+ creds=creds,
+ soledad=self._soledad)).sync(autocreate=autocreate)
def _extra_schema_init(self, c):
c.execute(