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
|
# Copyright 2011-2012 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/>.
"""SyncTarget API implementation to a remote HTTP server."""
try:
import simplejson as json
except ImportError:
import json # noqa
from u1db import (
Document,
SyncTarget,
)
from u1db.errors import (
BrokenSyncStream,
)
from u1db.remote import (
http_client,
utils,
)
class HTTPSyncTarget(http_client.HTTPClientBase, SyncTarget):
"""Implement the SyncTarget api to a remote HTTP server."""
@staticmethod
def connect(url):
return HTTPSyncTarget(url)
def get_sync_info(self, source_replica_uid):
self._ensure_connection()
res, _ = self._request_json('GET', ['sync-from', source_replica_uid])
return (res['target_replica_uid'], res['target_replica_generation'],
res['target_replica_transaction_id'],
res['source_replica_generation'], res['source_transaction_id'])
def record_sync_info(self, source_replica_uid, source_replica_generation,
source_transaction_id):
self._ensure_connection()
if self._trace_hook: # for tests
self._trace_hook('record_sync_info')
self._request_json('PUT', ['sync-from', source_replica_uid], {},
{'generation': source_replica_generation,
'transaction_id': source_transaction_id})
def _parse_sync_stream(self, data, return_doc_cb, ensure_callback=None):
parts = data.splitlines() # one at a time
if not parts or parts[0] != '[':
raise BrokenSyncStream
data = parts[1:-1]
comma = False
if data:
line, comma = utils.check_and_strip_comma(data[0])
res = json.loads(line)
if ensure_callback and 'replica_uid' in res:
ensure_callback(res['replica_uid'])
for entry in data[1:]:
if not comma: # missing in between comma
raise BrokenSyncStream
line, comma = utils.check_and_strip_comma(entry)
entry = json.loads(line)
doc = Document(entry['id'], entry['rev'], entry['content'])
return_doc_cb(doc, entry['gen'], entry['trans_id'])
if parts[-1] != ']':
try:
partdic = json.loads(parts[-1])
except ValueError:
pass
else:
if isinstance(partdic, dict):
self._error(partdic)
raise BrokenSyncStream
if not data or comma: # no entries or bad extra comma
raise BrokenSyncStream
return res
def sync_exchange(self, docs_by_generations, source_replica_uid,
last_known_generation, last_known_trans_id,
return_doc_cb, ensure_callback=None):
self._ensure_connection()
if self._trace_hook: # for tests
self._trace_hook('sync_exchange')
url = '%s/sync-from/%s' % (self._url.path, source_replica_uid)
self._conn.putrequest('POST', url)
self._conn.putheader('content-type', 'application/x-u1db-sync-stream')
for header_name, header_value in self._sign_request('POST', url, {}):
self._conn.putheader(header_name, header_value)
entries = ['[']
size = 1
def prepare(**dic):
entry = comma + '\r\n' + json.dumps(dic)
entries.append(entry)
return len(entry)
comma = ''
size += prepare(
last_known_generation=last_known_generation,
last_known_trans_id=last_known_trans_id,
ensure=ensure_callback is not None)
comma = ','
for doc, gen, trans_id in docs_by_generations:
size += prepare(id=doc.doc_id, rev=doc.rev, content=doc.get_json(),
gen=gen, trans_id=trans_id)
entries.append('\r\n]')
size += len(entries[-1])
self._conn.putheader('content-length', str(size))
self._conn.endheaders()
for entry in entries:
self._conn.send(entry)
entries = None
data, _ = self._response()
res = self._parse_sync_stream(data, return_doc_cb, ensure_callback)
data = None
return res['new_generation'], res['new_transaction_id']
# for tests
_trace_hook = None
def _set_trace_hook_shallow(self, cb):
self._trace_hook = cb
|