summaryrefslogtreecommitdiff
path: root/common/src/leap/soledad/common/l2db/remote/http_database.py
blob: 7512379f5545752e3cace760479c25520b354e98 (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
# Copyright 2011 Canonical Ltd.
#
# This file is part of u1db.
#
# u1db is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# u1db 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with u1db.  If not, see <http://www.gnu.org/licenses/>.

"""HTTPDatabase to access a remote db over the HTTP API."""

import json
import uuid

from leap.soledad.common.l2db import (
    Database,
    Document,
    errors)
from leap.soledad.common.l2db.remote import (
    http_client,
    http_errors,
    http_target)


DOCUMENT_DELETED_STATUS = http_errors.wire_description_to_status[
    errors.DOCUMENT_DELETED]


class HTTPDatabase(http_client.HTTPClientBase, Database):
    """Implement the Database API to a remote HTTP server."""

    def __init__(self, url, document_factory=None, creds=None):
        super(HTTPDatabase, self).__init__(url, creds=creds)
        self._factory = document_factory or Document

    def set_document_factory(self, factory):
        self._factory = factory

    @staticmethod
    def open_database(url, create):
        db = HTTPDatabase(url)
        db.open(create)
        return db

    @staticmethod
    def delete_database(url):
        db = HTTPDatabase(url)
        db._delete()
        db.close()

    def open(self, create):
        if create:
            self._ensure()
        else:
            self._check()

    def _check(self):
        return self._request_json('GET', [])[0]

    def _ensure(self):
        self._request_json('PUT', [], {}, {})

    def _delete(self):
        self._request_json('DELETE', [], {}, {})

    def put_doc(self, doc):
        if doc.doc_id is None:
            raise errors.InvalidDocId()
        params = {}
        if doc.rev is not None:
            params['old_rev'] = doc.rev
        res, headers = self._request_json('PUT', ['doc', doc.doc_id], params,
                                          doc.get_json(), 'application/json')
        doc.rev = res['rev']
        return res['rev']

    def get_doc(self, doc_id, include_deleted=False):
        try:
            res, headers = self._request(
                'GET', ['doc', doc_id], {"include_deleted": include_deleted})
        except errors.DocumentDoesNotExist:
            return None
        except errors.HTTPError, e:
            if (e.status == DOCUMENT_DELETED_STATUS and
                    'x-u1db-rev' in e.headers):
                        res = None
                        headers = e.headers
            else:
                raise
        doc_rev = headers['x-u1db-rev']
        has_conflicts = json.loads(headers['x-u1db-has-conflicts'])
        doc = self._factory(doc_id, doc_rev, res)
        doc.has_conflicts = has_conflicts
        return doc

    def _build_docs(self, res):
        for doc_dict in json.loads(res):
            doc = self._factory(
                doc_dict['doc_id'], doc_dict['doc_rev'], doc_dict['content'])
            doc.has_conflicts = doc_dict['has_conflicts']
            yield doc

    def get_docs(self, doc_ids, check_for_conflicts=True,
                 include_deleted=False):
        if not doc_ids:
            return []
        doc_ids = ','.join(doc_ids)
        res, headers = self._request(
            'GET', ['docs'], {
                "doc_ids": doc_ids, "include_deleted": include_deleted,
                "check_for_conflicts": check_for_conflicts})
        return self._build_docs(res)

    def get_all_docs(self, include_deleted=False):
        res, headers = self._request(
            'GET', ['all-docs'], {"include_deleted": include_deleted})
        gen = -1
        if 'x-u1db-generation' in headers:
            gen = int(headers['x-u1db-generation'])
        return gen, list(self._build_docs(res))

    def _allocate_doc_id(self):
        return 'D-%s' % (uuid.uuid4().hex,)

    def create_doc(self, content, doc_id=None):
        if not isinstance(content, dict):
            raise errors.InvalidContent
        json_string = json.dumps(content)
        return self.create_doc_from_json(json_string, doc_id)

    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)
        return new_doc

    def delete_doc(self, doc):
        if doc.doc_id is None:
            raise errors.InvalidDocId()
        params = {'old_rev': doc.rev}
        res, headers = self._request_json(
            'DELETE', ['doc', doc.doc_id], params)
        doc.make_tombstone()
        doc.rev = res['rev']

    def get_sync_target(self):
        st = http_target.HTTPSyncTarget(self._url.geturl())
        st._creds = self._creds
        return st