[CalendarServer-changes] [14401] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Tue Feb 10 14:49:34 PST 2015


Revision: 14401
          http://trac.calendarserver.org//changeset/14401
Author:   sagen at apple.com
Date:     2015-02-10 14:49:34 -0800 (Tue, 10 Feb 2015)
Log Message:
-----------
Sync token can optionally include a component derived from configuration, especially so that APNS push topic changes can be detected by clients

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/config.py
    CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/test/test_config.py

Modified: CalendarServer/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py	2015-02-10 21:54:08 UTC (rev 14400)
+++ CalendarServer/trunk/twistedcaldav/config.py	2015-02-10 22:49:34 UTC (rev 14401)
@@ -22,8 +22,9 @@
     "config",
 ]
 
+import copy
+import hashlib
 import os
-import copy
 
 class ConfigurationError(RuntimeError):
     """
@@ -262,6 +263,7 @@
 
         self._updating = False
         self._dirty = False
+        self._cachedSyncToken = None
 
 
     def load(self, configFile):
@@ -297,9 +299,78 @@
     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 mergeData(oldData, newData):
     """
     Merge two ConfigDict objects; oldData will be updated with all the keys

Modified: CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py	2015-02-10 21:54:08 UTC (rev 14400)
+++ CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py	2015-02-10 22:49:34 UTC (rev 14401)
@@ -87,7 +87,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 = []
@@ -105,6 +105,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/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2015-02-10 21:54:08 UTC (rev 14400)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2015-02-10 22:49:34 UTC (rev 14401)
@@ -1183,15 +1183,25 @@
 
     @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())
         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
@@ -1217,9 +1227,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

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2015-02-10 21:54:08 UTC (rev 14400)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2015-02-10 22:49:34 UTC (rev 14401)
@@ -475,6 +475,7 @@
     "EnableAddMember"             : True, # POST ;add-member extension
     "EnableSyncReport"            : True, # REPORT collection-sync
     "EnableSyncReportHome"        : True, # REPORT collection-sync on home collections
+    "EnableConfigSyncToken"       : False, # Sync token includes config component
     "EnableWellKnown"             : True, # /.well-known resource
     "EnableCalendarQueryExtended" : True, # Extended calendar-query REPORT
 
@@ -1554,6 +1555,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/trunk/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_config.py	2015-02-10 21:54:08 UTC (rev 14400)
+++ CalendarServer/trunk/twistedcaldav/test/test_config.py	2015-02-10 22:49:34 UTC (rev 14401)
@@ -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>
 """
@@ -635,3 +689,35 @@
         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())
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150210/160e9e1d/attachment-0001.html>


More information about the calendarserver-changes mailing list