[CalendarServer-changes] [15136] CalendarServer/branches/release/CalendarServer-5.4-dev/ twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Fri Sep 18 17:19:44 PDT 2015


Revision: 15136
          http://trac.calendarserver.org//changeset/15136
Author:   sagen at apple.com
Date:     2015-09-18 17:19:44 -0700 (Fri, 18 Sep 2015)
Log Message:
-----------
Back-port the changes which add a configuration portion to the sync token

Modified Paths:
--------------
    CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/config.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/method/report_sync_collection.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/resource.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/stdconfig.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/test/test_config.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/test/test_resource.py

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/config.py	2015-09-18 16:36:47 UTC (rev 15135)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/config.py	2015-09-19 00:19:44 UTC (rev 15136)
@@ -24,6 +24,7 @@
 
 import os
 import copy
+import hashlib
 
 class ConfigurationError(RuntimeError):
     """
@@ -192,7 +193,7 @@
         parts = attr.split(".")
         lastDict = self._data
         for part in parts[:-1]:
-            if not part in lastDict:
+            if part not in lastDict:
                 lastDict[attr] = ConfigDict()
             lastDict = lastDict.__getattr__(part)
         configItem = parts[-1]
@@ -259,6 +260,7 @@
 
         self._updating = False
         self._dirty = False
+        self._cachedSyncToken = None
 
 
     def load(self, configFile):
@@ -293,9 +295,88 @@
     def reset(self):
         self._data = ConfigDict(copy.deepcopy(self._provider.getDefaults()))
         self._dirty = True
+        self._syncTokenKeys = []
+        self._cachedSyncToken = None
 
 
+    def getKeyPath(self, keyPath):
+        """
+        Allows the getting of arbitrary nested dictionary keys via a single
+        dot-separated string.  For example, getKeyPath(self, "foo.bar.baz")
+        would fetch parent["foo"]["bar"]["baz"].  If any of the keys don't
+        exist, None is returned instead.
 
+        @param keyPath: a dot-delimited string specifying the path of keys to
+            traverse
+        @type keyPath: C{str}
+        @return: the value at keyPath
+        """
+        parent = self
+        parts = keyPath.split(".")
+        for part in parts[:-1]:
+            child = parent.get(part, None)
+            if child is None:
+                return None
+            parent = child
+        return parent.get(parts[-1], None)
+
+
+    def addSyncTokenKey(self, keyPath):
+        """
+        Indicates the specified key should be taken into account when generating
+        the sync token.  Also invalidates the (possibly) cached syncToken.
+
+        @param keyPath: a dot-delimited string specifying the path of keys to
+            traverse
+        @type keyPath: C{str}
+        """
+        if keyPath not in self._syncTokenKeys:
+            self._syncTokenKeys.append(keyPath)
+        self._cachedSyncToken = None
+
+
+    def syncToken(self):
+        """
+        Iterates the previously registered keys (sorted, so the order in which
+        the keys were registered doesn't affect the hash) and generates an MD5
+        hash of the combined values.  The hash is cached, and is invalidated
+        during a reload or if invalidateSyncToken is called.o
+
+        @return: the sync token
+        @rtype: C{str}
+        """
+        if self._cachedSyncToken is None:
+            pieces = []
+            self._syncTokenKeys.sort()
+            for key in self._syncTokenKeys:
+                value = self.getKeyPath(key)
+                if value is None:
+                    value = ""
+                pieces.append(key + ":" + str(value))
+            whole = "|".join(pieces)
+            self._cachedSyncToken = hashlib.md5(whole).hexdigest()
+        return self._cachedSyncToken
+
+
+    def invalidateSyncToken(self):
+        """
+        Invalidates the cached copy of the sync token.
+        """
+        self._cachedSyncToken = None
+
+
+    def joinToken(self, dataToken):
+        """
+        Joins the config sync token with the dataToken.  If EnableConfigSyncToken
+        is False, the original dataToken is just returned
+        """
+        if self.EnableConfigSyncToken:
+            configToken = self.syncToken()
+            return "{}/{}".format(dataToken, configToken)
+        else:
+            return dataToken
+
+
 def mergeData(oldData, newData):
     """
     Merge two ConfigDict objects; oldData will be updated with all the keys

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/method/report_sync_collection.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/method/report_sync_collection.py	2015-09-18 16:36:47 UTC (rev 15135)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/method/report_sync_collection.py	2015-09-19 00:19:44 UTC (rev 15136)
@@ -86,7 +86,7 @@
     # the child resource loop and supply those to the checkPrivileges on each child.
     filteredaces = (yield self.inheritedACEsforChildren(request))
 
-    changed, removed, notallowed, newtoken = yield self.whatchanged(sync_collection.sync_token, depth)
+    changed, removed, notallowed, newtoken, resourceChanged = yield self.whatchanged(sync_collection.sync_token, depth)
 
     # Now determine which valid resources are readable and which are not
     ok_resources = []
@@ -104,6 +104,9 @@
             inherited_aces=filteredaces
         )
 
+    if resourceChanged:
+        ok_resources.append((self, request.uri))
+
     for child, child_uri in ok_resources:
         href = element.HRef.fromString(child_uri)
         try:

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/resource.py	2015-09-18 16:36:47 UTC (rev 15135)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/resource.py	2015-09-19 00:19:44 UTC (rev 15136)
@@ -1252,15 +1252,27 @@
 
     @inlineCallbacks
     def whatchanged(self, client_token, depth):
+
+        client_data_token = None
+        client_config_token = None
+
+        if client_token:
+            if "/" in client_token:
+                client_data_token, client_config_token = client_token.split("/")
+            else:
+                client_data_token = client_token
+
         current_token = (yield self.getSyncToken())
+        if "/" in current_token:
+            current_token = current_token.split("/")[0]
         current_uuid, current_revision = current_token[6:].split("_", 1)
         current_revision = int(current_revision)
 
-        if client_token:
+        if client_data_token:
             try:
-                if not client_token.startswith("data:,"):
+                if not client_data_token.startswith("data:,"):
                     raise ValueError
-                caluuid, revision = client_token[6:].split("_", 1)
+                caluuid, revision = client_data_token[6:].split("_", 1)
                 revision = int(revision)
 
                 # Check client token validity
@@ -1286,9 +1298,19 @@
                 "Sync token not recognized",
             ))
 
-        returnValue((changed, removed, notallowed, current_token))
+        if config.EnableConfigSyncToken:
+            # Append the app-level portion of sync token (e.g. derived from config)
+            newConfigToken = config.syncToken()
+            current_token = "{}/{}".format(current_token, newConfigToken)
 
+            # If the config token changed, note that in the returned tuple
+            resourceChanged = (newConfigToken != client_config_token)
+        else:
+            resourceChanged = False
 
+        returnValue((changed, removed, notallowed, current_token, resourceChanged))
+
+
     def _indexWhatChanged(self, revision, depth):
         # Now handled directly by newstore
         raise NotImplementedError
@@ -1301,7 +1323,9 @@
         """
 
         internal_token = (yield self.getInternalSyncToken())
-        returnValue("data:,%s" % (internal_token,))
+        internal_token = "data:,%s" % (internal_token,)
+        token = config.joinToken(internal_token)
+        returnValue(token)
 
 
     def getInternalSyncToken(self):
@@ -2119,12 +2143,26 @@
     def _mergeSyncTokens(self, hometoken, notificationtoken):
         """
         Merge two sync tokens, choosing the higher revision number of the two,
-        but keeping the home resource-id intact.
+        but keeping the home resource-id intact.  If the config portion of
+        the token is present, it is also kept intact.
         """
+
+        if "/" in hometoken:
+            hometoken, configtoken = hometoken.split("/")
+        else:
+            configtoken = None
+
+        if "/" in notificationtoken:
+            notificationtoken = notificationtoken.split("/")[0]
+
         homekey, homerev = hometoken.split("_", 1)
         notrev = notificationtoken.split("_", 1)[1]
         if int(notrev) > int(homerev):
             hometoken = "%s_%s" % (homekey, notrev,)
+
+        if configtoken:
+            hometoken = "{}/{}".format(hometoken, configtoken)
+
         return hometoken
 
 

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/stdconfig.py	2015-09-18 16:36:47 UTC (rev 15135)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/stdconfig.py	2015-09-19 00:19:44 UTC (rev 15136)
@@ -563,6 +563,7 @@
     "EnableAddMember"             : True, # POST ;add-member extension
     "EnableSyncReport"            : True, # REPORT collection-sync
     "EnableSyncReportHome"        : True, # REPORT collection-sync on home collections
+    "EnableConfigSyncToken"       : True, # Sync token includes config component
     "EnableWellKnown"             : True, # /.well-known resource
     "EnableCalendarQueryExtended" : True, # Extended calendar-query REPORT
 
@@ -1483,6 +1484,12 @@
 
 
 def _updateNotifications(configDict, reloading=False):
+
+    # These three keys go into producing the config sync token
+    config.addSyncTokenKey("Notifications.Services.APNS.Enabled")
+    config.addSyncTokenKey("Notifications.Services.APNS.CalDAV.Topic")
+    config.addSyncTokenKey("Notifications.Services.APNS.CardDAV.Topic")
+
     # Reloading not supported -- requires process running as root
     if reloading:
         return

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/test/test_config.py	2015-09-18 16:36:47 UTC (rev 15135)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/test/test_config.py	2015-09-19 00:19:44 UTC (rev 15136)
@@ -66,6 +66,60 @@
     </dict>
   </dict>
 
+  <key>Notifications</key>
+  <dict>
+
+    <key>Services</key>
+    <dict>
+
+      <key>AMP</key>
+      <dict>
+        <key>Enabled</key>
+        <true/>
+        <key>Port</key>
+        <integer>62311</integer>
+        <key>EnableStaggering</key>
+        <false/>
+        <key>StaggerSeconds</key>
+        <integer>3</integer>
+      </dict>
+
+      <key>APNS</key>
+      <dict>
+        <key>CalDAV</key>
+        <dict>
+          <key>AuthorityChainPath</key>
+          <string>com.apple.calendar.chain.pem</string>
+          <key>CertificatePath</key>
+          <string>com.apple.calendar.cert.pem</string>
+          <key>PrivateKeyPath</key>
+          <string>com.apple.calendar.key.pem</string>
+          <key>Topic</key>
+          <string>calendar-topic</string>
+          <key>Passphrase</key>
+          <string>password</string>
+        </dict>
+        <key>CardDAV</key>
+        <dict>
+          <key>AuthorityChainPath</key>
+          <string>com.apple.contact.chain.pem</string>
+          <key>CertificatePath</key>
+          <string>com.apple.contact.cert.pem</string>
+          <key>PrivateKeyPath</key>
+          <string>com.apple.contact.key.pem</string>
+          <key>Topic</key>
+          <string>contact-topic</string>
+          <key>Passphrase</key>
+          <string>password</string>
+        </dict>
+        <key>Enabled</key>
+        <true/>
+      </dict>
+
+    </dict>
+
+  </dict>
+
 </dict>
 </plist>
 """
@@ -634,3 +688,36 @@
         config.load(self.testMaster)
         self.assertEquals(config.HTTPPort, 9008)
         self.assertEquals(config.SSLPort, 8443)
+
+
+
+    def testSyncToken(self):
+        config.load(self.testConfig)
+
+        # no sync token keys specified; need to empty this array here because
+        # stdconfig is registering keys automatically
+        config._syncTokenKeys = []
+        self.assertEquals("d41d8cd98f00b204e9800998ecf8427e", config.syncToken())
+
+        # add sync token keys (some with multiple levels)
+        config.addSyncTokenKey("DefaultLogLevel")
+        config.addSyncTokenKey("Notifications.Services.APNS.Enabled")
+        config.addSyncTokenKey("Notifications.Services.APNS.CalDAV.Topic")
+        config.addSyncTokenKey("Notifications.Services.APNS.CardDAV.Topic")
+        self.assertEquals("7473205187d7a6ff0c61a2b6b04053c5", config.syncToken())
+
+        # modify a sync token key value
+        config.Notifications.Services.APNS.CalDAV.Topic = "changed"
+        # direct manipulation of config requires explicit invalidation
+        self.assertEquals("7473205187d7a6ff0c61a2b6b04053c5", config.syncToken())
+        config.invalidateSyncToken()
+        self.assertEquals("4cdbb3841625d001dc768439f5a88cba", config.syncToken())
+
+        # add a non existent key (not an error because it could exist later)
+        config.addSyncTokenKey("Notifications.Services.APNS.CalDAV.NonExistent")
+        config.invalidateSyncToken()
+        self.assertEquals("2ffb128cee5a4b217cef82fd31ae7767", config.syncToken())
+
+        # reload automatically invalidates
+        config.reload()
+        self.assertEquals("a1c46c5aff1899658dac033ee8520b07", config.syncToken())
\ No newline at end of file

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/test/test_resource.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/test/test_resource.py	2015-09-18 16:36:47 UTC (rev 15135)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/test/test_resource.py	2015-09-19 00:19:44 UTC (rev 15136)
@@ -97,8 +97,14 @@
         resource = NotificationCollectionResource()
         self.assertTrue(('http://calendarserver.org/ns/', 'getctag') in resource.liveProperties())
 
+    def test_commonHomeResourceMergeSyncToken(self):
+        resource = CommonHomeResource(None, None, None, StubHome())
+        self.assertEquals(resource._mergeSyncTokens("1_2/A", "1_3/A"), "1_3/A")
+        self.assertEquals(resource._mergeSyncTokens("1_2", "1_3"), "1_3")
+        self.assertEquals(resource._mergeSyncTokens("1_4", "1_3"), "1_4")
 
 
+
 class OwnershipTests(TestCase):
     """
     L{CalDAVResource.isOwner} determines if the authenticated principal of the
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150918/9726a726/attachment-0001.html>


More information about the calendarserver-changes mailing list