OLD | NEW |
| (Empty) |
1 #!/usr/bin/python2.4 | |
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """An implementation of the server side of the Chromium sync protocol. | |
7 | |
8 The details of the protocol are described mostly by comments in the protocol | |
9 buffer definition at chrome/browser/sync/protocol/sync.proto. | |
10 """ | |
11 | |
12 import operator | |
13 import random | |
14 import threading | |
15 | |
16 import autofill_specifics_pb2 | |
17 import bookmark_specifics_pb2 | |
18 import preference_specifics_pb2 | |
19 import theme_specifics_pb2 | |
20 import typed_url_specifics_pb2 | |
21 import sync_pb2 | |
22 | |
23 # An enumeration of the various kinds of data that can be synced. | |
24 # Over the wire, this enumeration is not used: a sync object's type is | |
25 # inferred by which EntitySpecifics extension it has. But in the context | |
26 # of a program, it is useful to have an enumeration. | |
27 ALL_TYPES = ( | |
28 TOP_LEVEL, # The type of the 'Google Chrome' folder. | |
29 BOOKMARK, | |
30 AUTOFILL, | |
31 TYPED_URL, | |
32 PREFERENCE, | |
33 # PASSWORD, # Disabled since there's no specifics proto. | |
34 # SESSION, | |
35 THEME) = range(6) | |
36 | |
37 # Given a sync type from ALL_TYPES, find the extension token corresponding | |
38 # to that datatype. Note that TOP_LEVEL has no such token. | |
39 SYNC_TYPE_TO_EXTENSION = { | |
40 BOOKMARK: bookmark_specifics_pb2.bookmark, | |
41 AUTOFILL: autofill_specifics_pb2.autofill, | |
42 TYPED_URL: typed_url_specifics_pb2.typed_url, | |
43 PREFERENCE: preference_specifics_pb2.preference, | |
44 # PASSWORD: password_specifics_pb2.password, # Disabled | |
45 # SESSION: session_specifics_pb2.session, # Disabled | |
46 THEME: theme_specifics_pb2.theme, | |
47 } | |
48 | |
49 # The parent ID used to indicate a top-level node. | |
50 ROOT_ID = '0' | |
51 | |
52 def GetEntryType(entry): | |
53 """Extract the sync type from a SyncEntry. | |
54 | |
55 Args: | |
56 entry: A SyncEntity protobuf object whose type to determine. | |
57 Returns: | |
58 A value from ALL_TYPES if the entry's type can be determined, or None | |
59 if the type cannot be determined. | |
60 """ | |
61 if entry.server_defined_unique_tag == 'google_chrome': | |
62 return TOP_LEVEL | |
63 entry_types = GetEntryTypesFromSpecifics(entry.specifics) | |
64 if not entry_types: | |
65 return None | |
66 # It is presupposed that the entry has at most one specifics extension | |
67 # present. If there is more than one, either there's a bug, or else | |
68 # the caller should use GetEntryTypes. | |
69 if len(entry_types) > 1: | |
70 raise 'GetEntryType called with multiple extensions present.' | |
71 return entry_types[0] | |
72 | |
73 def GetEntryTypesFromSpecifics(specifics): | |
74 """Determine the sync types indicated by an EntitySpecifics's extension(s). | |
75 | |
76 If the specifics have more than one recognized extension (as commonly | |
77 happens with the requested_types field of GetUpdatesMessage), all types | |
78 will be returned. Callers must handle the possibility of the returned | |
79 value having more than one item. | |
80 | |
81 Args: | |
82 specifics: A EntitySpecifics protobuf message whose extensions to | |
83 enumerate. | |
84 Returns: | |
85 A list of the sync types (values from ALL_TYPES) assocated with each | |
86 recognized extension of the specifics message. | |
87 """ | |
88 entry_types = [] | |
89 for data_type, extension in SYNC_TYPE_TO_EXTENSION.iteritems(): | |
90 if specifics.HasExtension(extension): | |
91 entry_types.append(data_type) | |
92 return entry_types | |
93 | |
94 def GetRequestedTypes(get_updates_message): | |
95 """Determine the sync types requested by a client GetUpdates operation.""" | |
96 types = GetEntryTypesFromSpecifics( | |
97 get_updates_message.requested_types) | |
98 if types: | |
99 types.append(TOP_LEVEL) | |
100 return types | |
101 | |
102 def GetDefaultEntitySpecifics(data_type): | |
103 """Get an EntitySpecifics having a sync type's default extension value. | |
104 """ | |
105 specifics = sync_pb2.EntitySpecifics() | |
106 if data_type in SYNC_TYPE_TO_EXTENSION: | |
107 extension_handle = SYNC_TYPE_TO_EXTENSION[data_type] | |
108 specifics.Extensions[extension_handle].SetInParent() | |
109 return specifics | |
110 | |
111 def DeepCopyOfProto(proto): | |
112 """Return a deep copy of a protocol buffer.""" | |
113 new_proto = type(proto)() | |
114 new_proto.MergeFrom(proto) | |
115 return new_proto | |
116 | |
117 | |
118 class PermanentItem(object): | |
119 """A specification of one server-created permanent item. | |
120 | |
121 Attributes: | |
122 tag: A known-to-the-client value that uniquely identifies a server-created | |
123 permanent item. | |
124 name: The human-readable display name for this item. | |
125 parent_tag: The tag of the permanent item's parent. If ROOT_ID, indicates | |
126 a top-level item. Otherwise, this must be the tag value of some other | |
127 server-created permanent item. | |
128 sync_type: A value from ALL_TYPES, giving the datatype of this permanent | |
129 item. This controls which types of client GetUpdates requests will | |
130 cause the permanent item to be created and returned. | |
131 """ | |
132 | |
133 def __init__(self, tag, name, parent_tag, sync_type): | |
134 self.tag = tag | |
135 self.name = name | |
136 self.parent_tag = parent_tag | |
137 self.sync_type = sync_type | |
138 | |
139 class SyncDataModel(object): | |
140 """Models the account state of one sync user. | |
141 """ | |
142 _BATCH_SIZE = 100 | |
143 | |
144 # Specify all the permanent items that a model might need. | |
145 _PERMANENT_ITEM_SPECS = [ | |
146 PermanentItem('google_chrome', name='Google Chrome', | |
147 parent_tag=ROOT_ID, sync_type=TOP_LEVEL), | |
148 PermanentItem('google_chrome_bookmarks', name='Bookmarks', | |
149 parent_tag='google_chrome', sync_type=BOOKMARK), | |
150 PermanentItem('bookmark_bar', name='Bookmark Bar', | |
151 parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK), | |
152 PermanentItem('other_bookmarks', name='Other Bookmarks', | |
153 parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK), | |
154 PermanentItem('google_chrome_preferences', name='Preferences', | |
155 parent_tag='google_chrome', sync_type=PREFERENCE), | |
156 PermanentItem('google_chrome_autofill', name='Autofill', | |
157 parent_tag='google_chrome', sync_type=AUTOFILL), | |
158 # TODO(nick): Disabled since the protocol does not support them yet. | |
159 # PermanentItem('google_chrome_passwords', name='Passwords', | |
160 # parent_tag='google_chrome', sync_type=PASSWORD), | |
161 # PermanentItem('google_chrome_sessions', name='Sessions', | |
162 # parent_tag='google_chrome', SESSION), | |
163 PermanentItem('google_chrome_themes', name='Themes', | |
164 parent_tag='google_chrome', sync_type=THEME), | |
165 PermanentItem('google_chrome_typed_urls', name='Typed URLs', | |
166 parent_tag='google_chrome', sync_type=TYPED_URL), | |
167 ] | |
168 | |
169 def __init__(self): | |
170 self._version = 0 | |
171 | |
172 # Monotonically increasing version number. The next object change will | |
173 # take on this value + 1. | |
174 self._entries = {} | |
175 | |
176 # TODO(nick): uuid.uuid1() is better, but python 2.5 only. | |
177 self.store_birthday = '%0.30f' % random.random() | |
178 | |
179 def _SaveEntry(self, entry): | |
180 """Insert or update an entry in the change log, and give it a new version. | |
181 | |
182 The ID fields of this entry are assumed to be valid server IDs. This | |
183 entry will be updated with a new version number and sync_timestamp. | |
184 | |
185 Args: | |
186 entry: The entry to be added or updated. | |
187 """ | |
188 self._version = self._version + 1 | |
189 entry.version = self._version | |
190 entry.sync_timestamp = self._version | |
191 | |
192 # Preserve the originator info, which the client is not required to send | |
193 # when updating. | |
194 base_entry = self._entries.get(entry.id_string) | |
195 if base_entry: | |
196 entry.originator_cache_guid = base_entry.originator_cache_guid | |
197 entry.originator_client_item_id = base_entry.originator_client_item_id | |
198 | |
199 self._entries[entry.id_string] = DeepCopyOfProto(entry) | |
200 | |
201 def _ServerTagToId(self, tag): | |
202 """Determine the server ID from a server-unique tag. | |
203 | |
204 The resulting value is guaranteed not to collide with the other ID | |
205 generation methods. | |
206 | |
207 Args: | |
208 tag: The unique, known-to-the-client tag of a server-generated item. | |
209 """ | |
210 if tag and tag != ROOT_ID: | |
211 return '<server tag>%s' % tag | |
212 else: | |
213 return tag | |
214 | |
215 def _ClientTagToId(self, tag): | |
216 """Determine the server ID from a client-unique tag. | |
217 | |
218 The resulting value is guaranteed not to collide with the other ID | |
219 generation methods. | |
220 | |
221 Args: | |
222 tag: The unique, opaque-to-the-server tag of a client-tagged item. | |
223 """ | |
224 return '<client tag>%s' % tag | |
225 | |
226 def _ClientIdToId(self, client_guid, client_item_id): | |
227 """Compute a unique server ID from a client-local ID tag. | |
228 | |
229 The resulting value is guaranteed not to collide with the other ID | |
230 generation methods. | |
231 | |
232 Args: | |
233 client_guid: A globally unique ID that identifies the client which | |
234 created this item. | |
235 client_item_id: An ID that uniquely identifies this item on the client | |
236 which created it. | |
237 """ | |
238 # Using the client ID info is not required here (we could instead generate | |
239 # a random ID), but it's useful for debugging. | |
240 return '<server ID originally>%s/%s' % (client_guid, client_item_id) | |
241 | |
242 def _WritePosition(self, entry, parent_id, prev_id=None): | |
243 """Convert from a relative position into an absolute, numeric position. | |
244 | |
245 Clients specify positions using the predecessor-based references; the | |
246 server stores and reports item positions using sparse integer values. | |
247 This method converts from the former to the latter. | |
248 | |
249 Args: | |
250 entry: The entry for which to compute a position. Its ID field are | |
251 assumed to be server IDs. This entry will have its parent_id_string | |
252 and position_in_parent fields updated; its insert_after_item_id field | |
253 will be cleared. | |
254 parent_id: The ID of the entry intended as the new parent. | |
255 prev_id: The ID of the entry intended as the new predecessor. If this | |
256 is None, or an ID of an object which is not a child of the new parent, | |
257 the entry will be positioned at the end (right) of the ordering. If | |
258 the empty ID (''), this will be positioned at the front (left) of the | |
259 ordering. Otherwise, the entry will be given a position_in_parent | |
260 value placing it just after (to the right of) the new predecessor. | |
261 """ | |
262 PREFERRED_GAP = 2 ** 20 | |
263 # Compute values at the beginning or end. | |
264 def ExtendRange(current_limit_entry, sign_multiplier): | |
265 if current_limit_entry.id_string == entry.id_string: | |
266 step = 0 | |
267 else: | |
268 step = sign_multiplier * PREFERRED_GAP | |
269 return current_limit_entry.position_in_parent + step | |
270 | |
271 siblings = [x for x in self._entries.values() | |
272 if x.parent_id_string == parent_id and not x.deleted] | |
273 siblings = sorted(siblings, key=operator.attrgetter('position_in_parent')) | |
274 if prev_id == entry.id_string: | |
275 prev_id = '' | |
276 if not siblings: | |
277 # First item in this container; start in the middle. | |
278 entry.position_in_parent = 0 | |
279 elif prev_id == '': | |
280 # A special value in the protocol. Insert at first position. | |
281 entry.position_in_parent = ExtendRange(siblings[0], -1) | |
282 else: | |
283 # Consider items along with their successors. | |
284 for a, b in zip(siblings, siblings[1:]): | |
285 if a.id_string != prev_id: | |
286 continue | |
287 elif b.id_string == entry.id_string: | |
288 # We're already in place; don't change anything. | |
289 entry.position_in_parent = b.position_in_parent | |
290 else: | |
291 # Interpolate new position between two others. | |
292 entry.position_in_parent = ( | |
293 a.position_in_parent * 7 + b.position_in_parent) / 8 | |
294 break | |
295 else: | |
296 # Insert at end. Includes the case where prev_id is None. | |
297 entry.position_in_parent = ExtendRange(siblings[-1], +1) | |
298 | |
299 entry.parent_id_string = parent_id | |
300 entry.ClearField('insert_after_item_id') | |
301 | |
302 def _ItemExists(self, id_string): | |
303 """Determine whether an item exists in the changelog.""" | |
304 return id_string in self._entries | |
305 | |
306 def _CreatePermanentItem(self, spec): | |
307 """Create one permanent item from its spec, if it doesn't exist. | |
308 | |
309 The resulting item is added to the changelog. | |
310 | |
311 Args: | |
312 spec: A PermanentItem object holding the properties of the item to create. | |
313 """ | |
314 id_string = self._ServerTagToId(spec.tag) | |
315 if self._ItemExists(id_string): | |
316 return | |
317 print 'Creating permanent item: %s' % spec.name | |
318 entry = sync_pb2.SyncEntity() | |
319 entry.id_string = id_string | |
320 entry.non_unique_name = spec.name | |
321 entry.name = spec.name | |
322 entry.server_defined_unique_tag = spec.tag | |
323 entry.folder = True | |
324 entry.deleted = False | |
325 entry.specifics.CopyFrom(GetDefaultEntitySpecifics(spec.sync_type)) | |
326 self._WritePosition(entry, self._ServerTagToId(spec.parent_tag)) | |
327 self._SaveEntry(entry) | |
328 | |
329 def _CreatePermanentItems(self, requested_types): | |
330 """Ensure creation of all permanent items for a given set of sync types. | |
331 | |
332 Args: | |
333 requested_types: A list of sync data types from ALL_TYPES. | |
334 Permanent items of only these types will be created. | |
335 """ | |
336 for spec in self._PERMANENT_ITEM_SPECS: | |
337 if spec.sync_type in requested_types: | |
338 self._CreatePermanentItem(spec) | |
339 | |
340 def GetChangesFromTimestamp(self, requested_types, timestamp): | |
341 """Get entries which have changed since a given timestamp, oldest first. | |
342 | |
343 The returned entries are limited to being _BATCH_SIZE many. The entries | |
344 are returned in strict version order. | |
345 | |
346 Args: | |
347 requested_types: A list of sync data types from ALL_TYPES. | |
348 Only items of these types will be retrieved; others will be filtered | |
349 out. | |
350 timestamp: A timestamp / version number. Only items that have changed | |
351 more recently than this value will be retrieved; older items will | |
352 be filtered out. | |
353 Returns: | |
354 A tuple of (version, entries). Version is a new timestamp value, which | |
355 should be used as the starting point for the next query. Entries is the | |
356 batch of entries meeting the current timestamp query. | |
357 """ | |
358 if timestamp == 0: | |
359 self._CreatePermanentItems(requested_types) | |
360 change_log = sorted(self._entries.values(), | |
361 key=operator.attrgetter('version')) | |
362 new_changes = [x for x in change_log if x.version > timestamp] | |
363 # Pick batch_size new changes, and then filter them. This matches | |
364 # the RPC behavior of the production sync server. | |
365 batch = new_changes[:self._BATCH_SIZE] | |
366 if not batch: | |
367 # Client is up to date. | |
368 return (timestamp, []) | |
369 | |
370 # Restrict batch to requested types. Tombstones are untyped | |
371 # and will always get included. | |
372 filtered = [] | |
373 for x in batch: | |
374 if (GetEntryType(x) in requested_types) or x.deleted: | |
375 filtered.append(DeepCopyOfProto(x)) | |
376 # The new client timestamp is the timestamp of the last item in the | |
377 # batch, even if that item was filtered out. | |
378 return (batch[-1].version, filtered) | |
379 | |
380 def _CheckVersionForCommit(self, entry): | |
381 """Perform an optimistic concurrency check on the version number. | |
382 | |
383 Clients are only allowed to commit if they report having seen the most | |
384 recent version of an object. | |
385 | |
386 Args: | |
387 entry: A sync entity from the client. It is assumed that ID fields | |
388 have been converted to server IDs. | |
389 Returns: | |
390 A boolean value indicating whether the client's version matches the | |
391 newest server version for the given entry. | |
392 """ | |
393 if entry.id_string in self._entries: | |
394 if (self._entries[entry.id_string].version != entry.version and | |
395 not self._entries[entry.id_string].deleted): | |
396 # Version mismatch that is not a tombstone recreation. | |
397 return False | |
398 else: | |
399 if entry.version != 0: | |
400 # Edit to an item that does not exist. | |
401 return False | |
402 return True | |
403 | |
404 def _CheckParentIdForCommit(self, entry): | |
405 """Check that the parent ID referenced in a SyncEntity actually exists. | |
406 | |
407 Args: | |
408 entry: A sync entity from the client. It is assumed that ID fields | |
409 have been converted to server IDs. | |
410 Returns: | |
411 A boolean value indicating whether the entity's parent ID is an object | |
412 that actually exists (and is not deleted) in the current account state. | |
413 """ | |
414 if entry.parent_id_string == ROOT_ID: | |
415 # This is generally allowed. | |
416 return True | |
417 if entry.parent_id_string not in self._entries: | |
418 print 'Warning: Client sent unknown ID. Should never happen.' | |
419 return False | |
420 if entry.parent_id_string == entry.id_string: | |
421 print 'Warning: Client sent circular reference. Should never happen.' | |
422 return False | |
423 if self._entries[entry.parent_id_string].deleted: | |
424 # This can happen in a race condition between two clients. | |
425 return False | |
426 if not self._entries[entry.parent_id_string].folder: | |
427 print 'Warning: Client sent non-folder parent. Should never happen.' | |
428 return False | |
429 return True | |
430 | |
431 def _RewriteIdsAsServerIds(self, entry, cache_guid, commit_session): | |
432 """Convert ID fields in a client sync entry to server IDs. | |
433 | |
434 A commit batch sent by a client may contain new items for which the | |
435 server has not generated IDs yet. And within a commit batch, later | |
436 items are allowed to refer to earlier items. This method will | |
437 generate server IDs for new items, as well as rewrite references | |
438 to items whose server IDs were generated earlier in the batch. | |
439 | |
440 Args: | |
441 entry: The client sync entry to modify. | |
442 cache_guid: The globally unique ID of the client that sent this | |
443 commit request. | |
444 commit_session: A dictionary mapping the original IDs to the new server | |
445 IDs, for any items committed earlier in the batch. | |
446 """ | |
447 if entry.version == 0: | |
448 if entry.HasField('client_defined_unique_tag'): | |
449 # When present, this should determine the item's ID. | |
450 new_id = self._ClientTagToId(entry.client_defined_unique_tag) | |
451 else: | |
452 new_id = self._ClientIdToId(cache_guid, entry.id_string) | |
453 entry.originator_cache_guid = cache_guid | |
454 entry.originator_client_item_id = entry.id_string | |
455 commit_session[entry.id_string] = new_id # Remember the remapping. | |
456 entry.id_string = new_id | |
457 if entry.parent_id_string in commit_session: | |
458 entry.parent_id_string = commit_session[entry.parent_id_string] | |
459 if entry.insert_after_item_id in commit_session: | |
460 entry.insert_after_item_id = commit_session[entry.insert_after_item_id] | |
461 | |
462 def CommitEntry(self, entry, cache_guid, commit_session): | |
463 """Attempt to commit one entry to the user's account. | |
464 | |
465 Args: | |
466 entry: A SyncEntity protobuf representing desired object changes. | |
467 cache_guid: A string value uniquely identifying the client; this | |
468 is used for ID generation and will determine the originator_cache_guid | |
469 if the entry is new. | |
470 commit_session: A dictionary mapping client IDs to server IDs for any | |
471 objects committed earlier this session. If the entry gets a new ID | |
472 during commit, the change will be recorded here. | |
473 Returns: | |
474 A SyncEntity reflecting the post-commit value of the entry, or None | |
475 if the entry was not committed due to an error. | |
476 """ | |
477 entry = DeepCopyOfProto(entry) | |
478 | |
479 # Generate server IDs for this entry, and write generated server IDs | |
480 # from earlier entries into the message's fields, as appropriate. The | |
481 # ID generation state is stored in 'commit_session'. | |
482 self._RewriteIdsAsServerIds(entry, cache_guid, commit_session) | |
483 | |
484 # Perform the optimistic concurrency check on the entry's version number. | |
485 # Clients are not allowed to commit unless they indicate that they've seen | |
486 # the most recent version of an object. | |
487 if not self._CheckVersionForCommit(entry): | |
488 return None | |
489 | |
490 # Check the validity of the parent ID; it must exist at this point. | |
491 # TODO(nick): Implement cycle detection and resolution. | |
492 if not self._CheckParentIdForCommit(entry): | |
493 return None | |
494 | |
495 # At this point, the commit is definitely going to happen. | |
496 | |
497 # Deletion works by storing a limited record for an entry, called a | |
498 # tombstone. A sync server must track deleted IDs forever, since it does | |
499 # not keep track of client knowledge (there's no deletion ACK event). | |
500 if entry.deleted: | |
501 # Only the ID, version and deletion state are preserved on a tombstone. | |
502 # TODO(nick): Does the production server not preserve the type? Not | |
503 # doing so means that tombstones cannot be filtered based on | |
504 # requested_types at GetUpdates time. | |
505 tombstone = sync_pb2.SyncEntity() | |
506 tombstone.id_string = entry.id_string | |
507 tombstone.deleted = True | |
508 tombstone.name = '' # 'name' is a required field; we're stuck with it. | |
509 entry = tombstone | |
510 else: | |
511 # Comments in sync.proto detail how the representation of positional | |
512 # ordering works: the 'insert_after_item_id' field specifies a | |
513 # predecessor during Commit operations, but the 'position_in_parent' | |
514 # field provides an absolute ordering in GetUpdates contexts. Here | |
515 # we convert from the former to the latter. Specifically, we'll | |
516 # generate a numeric position placing the item just after the object | |
517 # identified by 'insert_after_item_id', and then clear the | |
518 # 'insert_after_item_id' field so that it's not sent back to the client | |
519 # during later GetUpdates requests. | |
520 if entry.HasField('insert_after_item_id'): | |
521 self._WritePosition(entry, entry.parent_id_string, | |
522 entry.insert_after_item_id) | |
523 else: | |
524 self._WritePosition(entry, entry.parent_id_string) | |
525 | |
526 # Preserve the originator info, which the client is not required to send | |
527 # when updating. | |
528 base_entry = self._entries.get(entry.id_string) | |
529 if base_entry and not entry.HasField("originator_cache_guid"): | |
530 entry.originator_cache_guid = base_entry.originator_cache_guid | |
531 entry.originator_client_item_id = base_entry.originator_client_item_id | |
532 | |
533 # Commit the change. This also updates the version number. | |
534 self._SaveEntry(entry) | |
535 # TODO(nick): Handle recursive deletion. | |
536 return entry | |
537 | |
538 class TestServer(object): | |
539 """An object to handle requests for one (and only one) Chrome Sync account. | |
540 | |
541 TestServer consumes the sync command messages that are the outermost | |
542 layers of the protocol, performs the corresponding actions on its | |
543 SyncDataModel, and constructs an appropropriate response message. | |
544 """ | |
545 | |
546 def __init__(self): | |
547 # The implementation supports exactly one account; its state is here. | |
548 self.account = SyncDataModel() | |
549 self.account_lock = threading.Lock() | |
550 | |
551 def HandleCommand(self, raw_request): | |
552 """Decode and handle a sync command from a raw input of bytes. | |
553 | |
554 This is the main entry point for this class. It is safe to call this | |
555 method from multiple threads. | |
556 | |
557 Args: | |
558 raw_request: An iterable byte sequence to be interpreted as a sync | |
559 protocol command. | |
560 Returns: | |
561 A tuple (response_code, raw_response); the first value is an HTTP | |
562 result code, while the second value is a string of bytes which is the | |
563 serialized reply to the command. | |
564 """ | |
565 self.account_lock.acquire() | |
566 try: | |
567 request = sync_pb2.ClientToServerMessage() | |
568 request.MergeFromString(raw_request) | |
569 contents = request.message_contents | |
570 | |
571 response = sync_pb2.ClientToServerResponse() | |
572 response.error_code = sync_pb2.ClientToServerResponse.SUCCESS | |
573 response.store_birthday = self.account.store_birthday | |
574 | |
575 if contents == sync_pb2.ClientToServerMessage.AUTHENTICATE: | |
576 print 'Authenticate' | |
577 # We accept any authentication token, and support only one account. | |
578 # TODO(nick): Mock out the GAIA authentication as well; hook up here. | |
579 response.authenticate.user.email = 'syncjuser@chromium' | |
580 response.authenticate.user.display_name = 'Sync J User' | |
581 elif contents == sync_pb2.ClientToServerMessage.COMMIT: | |
582 print 'Commit' | |
583 self.HandleCommit(request.commit, response.commit) | |
584 elif contents == sync_pb2.ClientToServerMessage.GET_UPDATES: | |
585 print ('GetUpdates from timestamp %d' % | |
586 request.get_updates.from_timestamp) | |
587 self.HandleGetUpdates(request.get_updates, response.get_updates) | |
588 return (200, response.SerializeToString()) | |
589 finally: | |
590 self.account_lock.release() | |
591 | |
592 def HandleCommit(self, commit_message, commit_response): | |
593 """Respond to a Commit request by updating the user's account state. | |
594 | |
595 Commit attempts stop after the first error, returning a CONFLICT result | |
596 for any unattempted entries. | |
597 | |
598 Args: | |
599 commit_message: A sync_pb.CommitMessage protobuf holding the content | |
600 of the client's request. | |
601 commit_response: A sync_pb.CommitResponse protobuf into which a reply | |
602 to the client request will be written. | |
603 """ | |
604 commit_response.SetInParent() | |
605 batch_failure = False | |
606 session = {} # Tracks ID renaming during the commit operation. | |
607 guid = commit_message.cache_guid | |
608 for entry in commit_message.entries: | |
609 server_entry = None | |
610 if not batch_failure: | |
611 # Try to commit the change to the account. | |
612 server_entry = self.account.CommitEntry(entry, guid, session) | |
613 | |
614 # An entryresponse is returned in both success and failure cases. | |
615 reply = commit_response.entryresponse.add() | |
616 if not server_entry: | |
617 reply.response_type = sync_pb2.CommitResponse.CONFLICT | |
618 reply.error_message = 'Conflict.' | |
619 batch_failure = True # One failure halts the batch. | |
620 else: | |
621 reply.response_type = sync_pb2.CommitResponse.SUCCESS | |
622 # These are the properties that the server is allowed to override | |
623 # during commit; the client wants to know their values at the end | |
624 # of the operation. | |
625 reply.id_string = server_entry.id_string | |
626 if not server_entry.deleted: | |
627 reply.parent_id_string = server_entry.parent_id_string | |
628 reply.position_in_parent = server_entry.position_in_parent | |
629 reply.version = server_entry.version | |
630 reply.name = server_entry.name | |
631 reply.non_unique_name = server_entry.non_unique_name | |
632 | |
633 def HandleGetUpdates(self, update_request, update_response): | |
634 """Respond to a GetUpdates request by querying the user's account. | |
635 | |
636 Args: | |
637 update_request: A sync_pb.GetUpdatesMessage protobuf holding the content | |
638 of the client's request. | |
639 update_response: A sync_pb.GetUpdatesResponse protobuf into which a reply | |
640 to the client request will be written. | |
641 """ | |
642 update_response.SetInParent() | |
643 requested_types = GetRequestedTypes(update_request) | |
644 new_timestamp, entries = self.account.GetChangesFromTimestamp( | |
645 requested_types, update_request.from_timestamp) | |
646 | |
647 # If the client is up to date, we are careful not to set the | |
648 # new_timestamp field. | |
649 if new_timestamp != update_request.from_timestamp: | |
650 update_response.new_timestamp = new_timestamp | |
651 for e in entries: | |
652 reply = update_response.entries.add() | |
653 reply.CopyFrom(e) | |
OLD | NEW |