[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