Initial sync-token and sync-collection support

Use the etag of the collection as the sync token and tell the client that the token is invalid when the collection changed.
This commit is contained in:
Unrud 2017-06-02 12:44:23 +02:00
parent 6bb0e9d956
commit f2b415c4a6
3 changed files with 49 additions and 5 deletions

View File

@ -821,7 +821,8 @@ class Application:
else: else:
collection = item.collection collection = item.collection
headers = {"Content-Type": "text/xml; charset=%s" % self.encoding} headers = {"Content-Type": "text/xml; charset=%s" % self.encoding}
xml_answer = xmlutils.report( status, xml_answer = xmlutils.report(
base_prefix, path, xml_content, collection) base_prefix, path, xml_content, collection)
return (client.MULTI_STATUS, headers, if status == client.PRECONDITION_FAILED:
self._write_xml_content(xml_answer)) return PRECONDITION_FAILED
return (status, headers, self._write_xml_content(xml_answer))

View File

@ -300,6 +300,24 @@ class BaseCollection:
""" """
raise NotImplementedError raise NotImplementedError
def sync(self, old_token=None):
"""Get the current sync token and changed items for synchronization.
``old_token`` an old sync token which is used as the base of the
delta update. If sync token is missing, all items are returned.
ValueError is raised for invalid or old tokens.
WARNING: This simple default implementation treats all sync-token as
invalid. It adheres to the specification but some clients
(e.g. InfCloud) don't like it. Subclasses should provide a
more sophisticated implementation.
"""
token = "http://radicale.org/ns/sync/%s" % self.etag.strip("\"")
if old_token:
raise ValueError("Sync token are not supported")
return token, self.list()
def list(self): def list(self):
"""List collection items.""" """List collection items."""
raise NotImplementedError raise NotImplementedError

View File

@ -617,6 +617,7 @@ def _propfind_response(base_prefix, path, item, props, user, write=False,
if is_collection: if is_collection:
prop200.append(ET.Element(_tag("CS", "getctag"))) prop200.append(ET.Element(_tag("CS", "getctag")))
prop200.append(ET.Element(_tag("D", "sync-token")))
prop200.append(ET.Element(_tag("C", "calendar-timezone"))) prop200.append(ET.Element(_tag("C", "calendar-timezone")))
prop200.append(ET.Element(_tag("D", "displayname"))) prop200.append(ET.Element(_tag("D", "displayname")))
prop200.append(ET.Element(_tag("ICAL", "calendar-color"))) prop200.append(ET.Element(_tag("ICAL", "calendar-color")))
@ -732,6 +733,11 @@ def _propfind_response(base_prefix, path, item, props, user, write=False,
element.text = item.etag element.text = item.etag
else: else:
is404 = True is404 = True
elif tag == _tag("D", "sync-token"):
if is_leaf:
element.text, _ = item.sync()
else:
is404 = True
else: else:
human_tag = _tag_from_clark(tag) human_tag = _tag_from_clark(tag)
meta = item.get_meta(human_tag) meta = item.get_meta(human_tag)
@ -841,7 +847,7 @@ def report(base_prefix, path, xml_request, collection):
# support for them) and stops working if an error code is returned. # support for them) and stops working if an error code is returned.
collection.logger.warning("Unsupported REPORT method %r on %r " collection.logger.warning("Unsupported REPORT method %r on %r "
"requested", root.tag, path) "requested", root.tag, path)
return multistatus return client.MULTI_STATUS, multistatus
prop_element = root.find(_tag("D", "prop")) prop_element = root.find(_tag("D", "prop"))
props = ( props = (
[prop.tag for prop in prop_element] [prop.tag for prop in prop_element]
@ -860,6 +866,25 @@ def report(base_prefix, path, xml_request, collection):
else: else:
collection.logger.warning("Skipping invalid path %r in REPORT " collection.logger.warning("Skipping invalid path %r in REPORT "
"request on %r", href_path, path) "request on %r", href_path, path)
elif root.tag == _tag("D", "sync-collection"):
old_sync_token_element = root.find(_tag("D", "sync-token"))
old_sync_token = ""
if old_sync_token_element is not None and old_sync_token_element.text:
old_sync_token = old_sync_token_element.text.strip()
collection.logger.debug("Client provided sync token: %r",
old_sync_token)
try:
sync_token, names = collection.sync(old_sync_token)
except ValueError as e:
# Invalid sync token
collection.logger.info("Client provided invalid sync token %r: %s",
old_sync_token, e, exc_info=True)
return client.PRECONDITION_FAILED, None
hreferences = ("/" + posixpath.join(collection.path, n) for n in names)
# Append current sync token to response
sync_token_element = ET.Element(_tag("D", "sync-token"))
sync_token_element.text = sync_token
multistatus.append(sync_token_element)
else: else:
hreferences = (path,) hreferences = (path,)
filters = ( filters = (
@ -931,7 +956,7 @@ def report(base_prefix, path, xml_request, collection):
base_prefix, uri, found_props=found_props, base_prefix, uri, found_props=found_props,
not_found_props=not_found_props, found_item=True)) not_found_props=not_found_props, found_item=True))
return multistatus return client.MULTI_STATUS, multistatus
def _item_response(base_prefix, href, found_props=(), not_found_props=(), def _item_response(base_prefix, href, found_props=(), not_found_props=(),