[CalendarServer-changes] [349] CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Thu Nov 2 07:43:53 PST 2006


Revision: 349
          http://trac.macosforge.org/projects/calendarserver/changeset/349
Author:   cdaboo at apple.com
Date:     2006-11-02 07:43:51 -0800 (Thu, 02 Nov 2006)

Log Message:
-----------
Preliminary drop box change notification support.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/customxml.py
    CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/delete.py
    CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/put.py
    CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/resource.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/notifications.py

Modified: CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/customxml.py	2006-11-02 01:45:49 UTC (rev 348)
+++ CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/customxml.py	2006-11-02 15:43:51 UTC (rev 349)
@@ -137,11 +137,11 @@
     allowed_children = {
         (twisted_dav_namespace, "action"     ): (1, 1),
         (twisted_dav_namespace, "time-stamp" ): (1, 1),
-        (twisted_dav_namespace, "auth-id"    ): (1, 1),
-        (twisted_dav_namespace, "old-etag"   ): (1, 1),
-        (twisted_dav_namespace, "new-etag"   ): (1, 1),
-        (twisted_dav_namespace, "old-uri"    ): (1, 1),
-        (twisted_dav_namespace, "new_uri"    ): (1, 1),
+        (twisted_dav_namespace, "auth-id"    ): (0, 1),
+        (twisted_dav_namespace, "old-etag"   ): (0, 1),
+        (twisted_dav_namespace, "new-etag"   ): (0, 1),
+        (twisted_dav_namespace, "old-uri"    ): (0, 1),
+        (twisted_dav_namespace, "new_uri"    ): (0, 1),
     }
 
 class Action (davxml.WebDAVElement):

Modified: CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/delete.py
===================================================================
--- CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/delete.py	2006-11-02 01:45:49 UTC (rev 348)
+++ CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/delete.py	2006-11-02 15:43:51 UTC (rev 349)
@@ -22,10 +22,15 @@
 
 __all__ = ["http_DELETE"]
 
-from twisted.internet.defer import maybeDeferred
+from twisted.internet.defer import deferredGenerator, waitForDeferred
 from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.util import parentForURL
 from twisted.web2.iweb import IResponse
 
+from twistedcaldav import customxml
+from twistedcaldav.dropbox import DropBox
+from twistedcaldav.notifications import Notification
 from twistedcaldav.resource import isPseudoCalendarCollectionResource
 
 def http_DELETE(self, request):
@@ -33,24 +38,43 @@
     # Override base DELETE request handling to ensure that the calendar
     # index file has the entry for the deleted calendar component removed.
     #
-    def deleteFromIndex(response):
-        response = IResponse(response)
+    # Also handle notifications in a drop box collection.
+    #
 
-        if response.code == responsecode.NO_CONTENT:
-            def deleteFromParent(parent):
-                if isPseudoCalendarCollectionResource(parent):
-                    index = parent.index()
-                    index.deleteResource(self.fp.basename())
+    parent = waitForDeferred(request.locateResource(parentForURL(request.uri)))
+    yield parent
+    parent = parent.getResult()
 
-                return response
-            
-            # Remove index entry if we are a child of a calendar collection
-            d = self.locateParent(request, request.uri)
-            d.addCallback(deleteFromParent)
-            return d
+    # May need old etag for notification
+    if DropBox.enabled and parent.isSpecialCollection(customxml.DropBox):
+        if self.exists() and self.etag() is not None:
+            oldETag = self.etag().generate()
+        else:
+            oldETag = None
 
-        return response
+    d = waitForDeferred(super(CalDAVFile, self).http_DELETE(request))
+    yield d
+    response = d.getResult()
 
-    d = maybeDeferred(super(CalDAVFile, self).http_DELETE, request)
-    d.addCallback(deleteFromIndex)
-    return d
+    if response == responsecode.NO_CONTENT:
+        if isPseudoCalendarCollectionResource(parent):
+            index = parent.index()
+            index.deleteResource(self.fp.basename())
+        elif DropBox.enabled and parent.isSpecialCollection(customxml.DropBox):
+            # We need to handle notificiations
+            authid = None
+            if isinstance(request.authnUser.children[0], davxml.HRef):
+                authid = str(request.authnUser.children[0])
+
+            principals = waitForDeferred(self.principalsWithReadPrivilege(request))
+            yield principals
+            principals = principals.getResult()
+
+            notification = Notification(action=Notification.ACTION_DELETED, authid=authid, oldETag=oldETag, oldURI=request.uri)
+            d = waitForDeferred(notification.doNotification(request, principals))
+            yield d
+            d.getResult()
+
+    yield response
+
+http_DELETE = deferredGenerator(http_DELETE)

Modified: CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/put.py	2006-11-02 01:45:49 UTC (rev 348)
+++ CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/put.py	2006-11-02 15:43:51 UTC (rev 349)
@@ -25,6 +25,7 @@
 from twisted.internet.defer import deferredGenerator, waitForDeferred
 from twisted.python import log
 from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
 from twisted.web2.dav.element.base import twisted_dav_namespace
 from twisted.web2.dav.http import ErrorResponse
 from twisted.web2.dav.util import allDataFromStream, parentForURL
@@ -34,6 +35,7 @@
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.dropbox import DropBox
 from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.notifications import Notification
 from twistedcaldav.resource import isPseudoCalendarCollectionResource
 
 def http_PUT(self, request):
@@ -74,6 +76,7 @@
             yield d
             yield d.getResult()
             return
+
         except ValueError, e:
             log.err("Error while handling (calendar) PUT: %s" % (e,))
             raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
@@ -81,6 +84,48 @@
     elif DropBox.enabled and parent.isSpecialCollection(customxml.DropBoxHome):
         # Cannot create resources in a drop box home collection
         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (twisted_dav_namespace, "valid-drop-box")))
+
+    elif DropBox.enabled and parent.isSpecialCollection(customxml.DropBox):
+        # We need to handle notificiations
+        
+        # We need the current etag
+        if self.exists() and self.etag() is not None:
+            oldETag = self.etag().generate()
+        else:
+            oldETag = None
+        
+        # Do the normal http_PUT behavior
+        d = waitForDeferred(super(CalDAVFile, self).http_PUT(request))
+        yield d
+        response = d.getResult()
+        
+        if response.code in (responsecode.OK, responsecode.CREATED, responsecode.NO_CONTENT):
+
+            authid = None
+            if isinstance(request.authnUser.children[0], davxml.HRef):
+                authid = str(request.authnUser.children[0])
+
+            if self.exists() and self.etag() is not None:
+                newETag = self.etag().generate()
+            else:
+                newETag = None
+            
+            principals = waitForDeferred(self.principalsWithReadPrivilege(request))
+            yield principals
+            principals = principals.getResult()
+
+            notification = Notification(action={
+                responsecode.OK         : Notification.ACTION_MODIFIED,
+                responsecode.CREATED    : Notification.ACTION_CREATED,
+                responsecode.NO_CONTENT : Notification.ACTION_MODIFIED,
+            }[response.code], authid=authid, oldETag=oldETag, newETag=newETag, oldURI=request.uri)
+            d = waitForDeferred(notification.doNotification(request, principals))
+            yield d
+            d.getResult()
+        
+        yield response
+        return
+
     else:
         d = waitForDeferred(super(CalDAVFile, self).http_PUT(request))
         yield d

Modified: CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/put_common.py	2006-11-02 01:45:49 UTC (rev 348)
+++ CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/method/put_common.py	2006-11-02 15:43:51 UTC (rev 349)
@@ -72,7 +72,7 @@
     @param destinationparent: the L{CalDAVFile} for the destination resource's parent collection.
     @param deletesource:      True if the source resource is to be deleted on successful completion, False otherwise.
     @param isiTIP:            True if relaxed calendar data validation is to be done, False otherwise.
-    @return:                  status response.
+    @return:                  a Deferred with a status response result.
     """
     
     try:

Added: CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/notifications.py
===================================================================
--- CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/notifications.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/notifications.py	2006-11-02 15:43:51 UTC (rev 349)
@@ -0,0 +1,167 @@
+##
+# Copyright (c) 2006 Apple Computer, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# DRI: Cyrus Daboo, cdaboo at apple.com
+##
+from twistedcaldav.extensions import DAVFile
+from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import waitForDeferred
+from twisted.web2.dav.method import put_common
+from twisted.web2.dav import davxml
+from twistedcaldav import customxml
+from twisted.web2.dav.element.base import twisted_dav_namespace
+from twistedcaldav.extensions import DAVResource
+
+import datetime
+import md5
+import os
+import time
+
+"""
+Implements collection change notification functionality. Any change to the contents of a collection will
+result in a notification resource deposited into subscriber's notifications collection.
+"""
+
+__all__ = [
+    "Notification",
+    "NotificationResource",
+    "NotificationFile",
+]
+
+class Notification(object):
+    """
+    Encapsulates a notification message.
+    """
+    
+    ACTION_NONE        = 0
+    ACTION_CREATED     = 1
+    ACTION_MODIFIED    = 2
+    ACTION_DELETED     = 3
+    ACTION_COPIED_TO   = 4
+    ACTION_COPIED_FROM = 5
+    ACTION_MOVED_TO    = 6
+    ACTION_MOVED_FROM  = 7
+
+    def __init__(self, action, authid=None, oldETag=None, newETag=None, oldURI=None, newURI=None):
+        self.action = action
+        self.timestamp = datetime.datetime.utcnow()
+        self.authid = authid
+        self.oldETag = oldETag
+        self.newETag = newETag
+        self.oldURI = oldURI
+        self.newURI = newURI
+
+    def doNotification(self, request, principals):
+        """
+        Put the supplied noitification into the notification collection of the specified principal.
+        
+        @param request: L{Request} for request in progress.
+        @param principals: C{list} of L{davxml.Principal}'s to send notifications to.
+        """
+        
+        for principal in principals:
+            if not isinstance(principal.children[0], davxml.HRef):
+                continue
+            purl = str(principal.children[0])
+            d = waitForDeferred(request.locateResource(purl))
+            yield d
+            presource = d.getResult()
+
+            collectionURL = presource.notificationsURL()
+            if collectionURL is None:
+                continue
+            d = waitForDeferred(request.locateResource(collectionURL))
+            yield d
+            collection = d.getResult()
+
+            name = "%s.xml" % (md5.new(str(self) + str(time.time()) + collectionURL).hexdigest(),)
+            path = os.path.join(collection.fp.path, name)
+    
+            # Create new resource in the collection
+            child = NotificationFile(path=path)
+            collection.putChild(name, child)
+            d = waitForDeferred(request.locateChildResource(collection, name))    # This ensure the URI for the resource is mapped
+            yield d
+            child = d.getResult()
+
+            d = waitForDeferred(child.create(request, self))
+            yield d
+            d.getResult()
+        
+    doNotification = deferredGenerator(doNotification)
+
+class NotificationResource(DAVResource):
+    """
+    Resource that gets stored in a notification collection and which contains
+    the notification details in its content as well as via properties.
+    """
+
+    liveProperties = DAVResource.liveProperties + (
+        (twisted_dav_namespace, "action"      ),
+        (twisted_dav_namespace, "time-stamp"  ),
+        (twisted_dav_namespace, "auth-id"     ),
+        (twisted_dav_namespace, "old-etag"    ),
+        (twisted_dav_namespace, "new-etag"    ),
+        (twisted_dav_namespace, "old-uri"     ),
+        (twisted_dav_namespace, "new-uri"     ),
+    )
+
+class NotificationFile(DAVResource, DAVFile):
+
+    def __init__(self, path):
+        super(NotificationFile, self).__init__(path)
+
+    def create(self, request, notification):
+        """
+        Create the resource, fill out the body, and add properties.
+        """
+        
+        # Create body XML
+        elements = []
+        elements.append(customxml.Action(
+            {
+                Notification.ACTION_CREATED:     customxml.Created(),
+                Notification.ACTION_MODIFIED:    customxml.Modified(),
+                Notification.ACTION_DELETED:     customxml.Deleted(),
+                Notification.ACTION_COPIED_TO:   customxml.CopiedTo(),
+                Notification.ACTION_COPIED_FROM: customxml.CopiedFrom(),
+                Notification.ACTION_MOVED_TO:    customxml.MovedTo(),
+                Notification.ACTION_MOVED_FROM:  customxml.MovedFrom(),
+            }[notification.action]
+            ))
+
+        elements.append(customxml.TimeStamp.fromString(notification.timestamp))
+        if notification.authid:
+            elements.append(customxml.AuthID.fromString(notification.authid))
+        if notification.oldETag:
+            elements.append(customxml.OldETag.fromString(notification.oldETag))
+        if notification.newETag:
+            elements.append(customxml.NewETag.fromString(notification.newETag))
+        if notification.oldURI:
+            elements.append(customxml.OldURI(davxml.HRef.fromString(notification.oldURI)))
+        if notification.newURI:
+            elements.append(customxml.NewURI(davxml.HRef.fromString(notification.newURI)))
+                          
+        xml = customxml.Notification(*elements)
+        
+        d = waitForDeferred(put_common.storeResource(request, data=xml.toxml(), destination=self, destination_uri=request.urlForResource(self)))
+        yield d
+        d.getResult()
+
+        # Write properties
+        for element in elements:
+            self.writeDeadProperty(element)
+
+    create = deferredGenerator(create)

Modified: CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/resource.py	2006-11-02 01:45:49 UTC (rev 348)
+++ CalendarServer/branches/users/cdaboo/dropbox/twistedcaldav/resource.py	2006-11-02 15:43:51 UTC (rev 349)
@@ -286,6 +286,73 @@
 
     authorizationPrincipal = deferredGenerator(authorizationPrincipal)
 
+    def principalsWithReadPrivilege(self, request):
+        """
+        Return a list of principals that have read privilege to this resource.
+        
+        """
+        
+        # Procedure:
+        #
+        # 1. Get list of all principals referenced in the ACL for the resource.
+        # 2. Expand groups in the principal list to their user principals.
+        # 3. For each user principal see if it has read privilege and if so add to the list.
+    
+        # Step 1. Get ACL principals.
+        acl = waitForDeferred(self.accessControlList(request))
+        yield acl
+        acl = acl.getResult()
+    
+        # Check disabled
+        if acl is None:
+            yield []
+            return
+    
+        principals = set()
+        
+        for ace in acl.children:
+            # First see if the ace's principal affects the principal being tested.
+            # FIXME: support the DAV:invert operation
+    
+            principal = ace.principal
+            
+            principal = waitForDeferred(self.resolvePrincipal(principal.children[0], request))
+            yield principal
+            principal = principal.getResult()
+            if principal is None:
+                continue
+    
+            presource = waitForDeferred(request.locateResource(str(principal)))
+            yield presource
+            presource = presource.getResult()
+    
+            if not isinstance(presource, DAVPrincipalResource):
+                continue
+            
+            # Step 2. Expand groups.
+            members = presource.groupMembers()
+            
+            if members:
+                for member in members:
+                    principals.add(davxml.Principal(davxml.HRef.fromString(member)))
+            else:
+                principals.add(davxml.Principal(principal))
+                
+        # Step 3. Check privileges
+        result = []
+        for principal in principals:
+            try:
+                d = waitForDeferred(self.checkPrivileges(request, (davxml.Read,), principal=principal))
+                yield d
+                test = d.getResult()
+                if test is not None:
+                    result.append(principal)
+            except:
+                pass
+        yield result
+    
+    principalsWithReadPrivilege = deferredGenerator(principalsWithReadPrivilege)
+    
     ##
     # CalDAV
     ##
@@ -670,24 +737,21 @@
                         return caldavxml.ScheduleOutboxURL(davxml.HRef(url))
 
             elif namespace == twisted_dav_namespace:
-                from twistedcaldav.dropbox import DropBox
                 if name == "dropbox-home-URL":
-                    # Use the first calendar home only
-                    home = ""
-                    for url in self.calendarHomeURLs():
-                        home = joinURL(url, DropBox.dropboxName) + "/"
-                        break
-                    return customxml.DropBoxHomeURL(davxml.HRef(home))
+                    url = self.dropboxURL()
+                    if url is None:
+                        return None
+                    else:
+                        return customxml.DropBoxHomeURL(davxml.HRef(url))
 
                 if name == "notifications-URL":
                     # Use the first calendar home only
-                    home = ""
-                    for url in self.calendarHomeURLs():
-                        home = joinURL(url, DropBox.notifcationName) + "/"
-                        break
-                    return customxml.NotificationsURL(davxml.HRef(home))
+                    url = self.notificationsURL()
+                    if url is None:
+                        return None
+                    else:
+                        return customxml.NotificationsURL(davxml.HRef(url))
 
-
             return super(CalendarPrincipalResource, self).readProperty(property, request)
 
         return maybeDeferred(defer)
@@ -762,6 +826,30 @@
         else:
             return None
         
+    def dropboxURL(self):
+        """
+        @return: the drop box home collection URL for this principal.
+        """
+        # Use the first calendar home only
+        from twistedcaldav.dropbox import DropBox
+        url = None
+        for home in self.calendarHomeURLs():
+            url = joinURL(home, DropBox.dropboxName) + "/"
+            break
+        return url
+        
+    def notificationsURL(self):
+        """
+        @return: the notifications collection URL for this principal.
+        """
+        # Use the first calendar home only
+        from twistedcaldav.dropbox import DropBox
+        url = None
+        for home in self.calendarHomeURLs():
+            url = joinURL(home, DropBox.notifcationName) + "/"
+            break
+        return url
+
     def matchesCalendarUserAddress(self, request, address):
         """
         Determine whether this principal matches the supplied calendar user

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20061102/25f5eec8/attachment.html


More information about the calendarserver-changes mailing list