Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(253)

Side by Side Diff: net/tools/testserver/chromiumsync.py

Issue 1622012: Python sync server impl, for test (Closed)
Patch Set: Fixed gyp bug ( :) ) Created 10 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « net/socket/ssl_test_util.cc ('k') | net/tools/testserver/chromiumsync_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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)
OLDNEW
« no previous file with comments | « net/socket/ssl_test_util.cc ('k') | net/tools/testserver/chromiumsync_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698