summaryrefslogtreecommitdiff
path: root/src/leap/soledad/backends/objectstore.py
blob: 8afac3ec64ebf62f27f8ace0a6f29aeb80e1150f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
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.
    """