[CalendarServer-changes] [6702] CalendarServer/branches/users/cdaboo/batchupload-6699
source_changes at macosforge.org
source_changes at macosforge.org
Mon Dec 20 07:12:54 PST 2010
Revision: 6702
http://trac.macosforge.org/projects/calendarserver/changeset/6702
Author: cdaboo at apple.com
Date: 2010-12-20 07:12:50 -0800 (Mon, 20 Dec 2010)
Log Message:
-----------
Batch upload requests.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/batchupload-6699/conf/caldavd-test.plist
CalendarServer/branches/users/cdaboo/batchupload-6699/twext/web2/dav/element/base.py
CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/caldavxml.py
CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/carddavxml.py
CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/customxml.py
CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/directory/opendirectorybacker.py
CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/ical.py
CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/sharing.py
CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/stdconfig.py
CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/storebridge.py
CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/vcard.py
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/conf/caldavd-test.plist 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/conf/caldavd-test.plist 2010-12-20 15:12:50 UTC (rev 6702)
@@ -705,6 +705,10 @@
<key>EnableTimezoneService</key>
<true/>
+ <!-- Batch Upload via POST -->
+ <key>EnableBatchUpload</key>
+ <true/>
+
<!-- Shared Calendars & Address Books -->
<key>Sharing</key>
<dict>
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twext/web2/dav/element/base.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twext/web2/dav/element/base.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twext/web2/dav/element/base.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -483,6 +483,13 @@
PCDATAElement: (0, None),
}
+ @classmethod
+ def fromQname(cls, namespace, name):
+ child = cls()
+ child.namespace = namespace
+ child.name = name
+ return child
+
def qname(self):
return (self.namespace, self.name)
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/caldavxml.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/caldavxml.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -433,6 +433,8 @@
else:
return None
+ generateComponent = calendar
+
def calendarData(self):
"""
Returns the calendar data derived from this element.
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/carddavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/carddavxml.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/carddavxml.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -280,6 +280,8 @@
else:
return None
+ generateComponent = address
+
def addressData(self):
"""
Returns an address component derived from this element.
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/customxml.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/customxml.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -978,7 +978,83 @@
namespace = calendarserver_namespace
name = "link"
+mm_namespace = "http://me.com/_namespace/"
+class Multiput (davxml.WebDAVElement):
+ namespace = mm_namespace
+ name = "multiput"
+
+ allowed_children = {
+ (mm_namespace, "resource") : (1, None),
+ }
+
+class Resource (davxml.WebDAVElement):
+ namespace = mm_namespace
+ name = "resource"
+
+ allowed_children = {
+ (davxml, "href") : (0, 1),
+ (mm_namespace, "if-match") : (0, 1),
+ (davxml, "set") : (0, 1),
+ (davxml, "remove") : (0, 1),
+ (mm_namespace, "delete") : (0, 1),
+ }
+
+class IfMatch (davxml.WebDAVElement):
+ namespace = mm_namespace
+ name = "if-match"
+
+ allowed_children = {
+ (davxml, "getetag") : (1, 1),
+ }
+
+class Delete (davxml.WebDAVEmptyElement):
+ namespace = mm_namespace
+ name = "delete"
+
+
+class BulkRequests (davxml.WebDAVElement):
+ namespace = mm_namespace
+ name = "bulk-requests"
+ hidden = True
+ protected = True
+
+ allowed_children = {
+ (mm_namespace, "simple") : (0, 1),
+ (mm_namespace, "crud") : (0, 1),
+ }
+
+class Simple (davxml.WebDAVElement):
+ namespace = mm_namespace
+ name = "simple"
+ hidden = True
+ protected = True
+
+ allowed_children = {
+ (mm_namespace, "max-resources") : (1, 1),
+ (mm_namespace, "max-bytes") : (1, 1),
+ }
+
+class CRUD (davxml.WebDAVElement):
+ namespace = mm_namespace
+ name = "crud"
+ hidden = True
+ protected = True
+
+ allowed_children = {
+ (mm_namespace, "max-resources") : (1, 1),
+ (mm_namespace, "max-bytes") : (1, 1),
+ }
+
+class MaxResources (davxml.WebDAVTextElement):
+ namespace = mm_namespace
+ name = "max-resources"
+
+class MaxBytes (davxml.WebDAVTextElement):
+ namespace = mm_namespace
+ name = "max-bytes"
+
+
##
# Extensions to davxml.ResourceType
##
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/directory/opendirectorybacker.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/directory/opendirectorybacker.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/directory/opendirectorybacker.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -44,7 +44,7 @@
from twext.web2.dav.element.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
from twext.web2.dav.resource import DAVPropertyMixIn
from twext.web2.dav.util import joinURL
-from twext.web2.http_headers import MimeType, generateContentType
+from twext.web2.http_headers import MimeType, generateContentType, ETag
from twistedcaldav import customxml, carddavxml
@@ -1908,7 +1908,7 @@
#print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
return result
elif name == "getetag":
- result = davxml.GETETag( hashlib.md5(self.vCardText()).hexdigest() )
+ result = davxml.GETETag( ETag(hashlib.md5(self.vCardText()).hexdigest()).generate() )
#print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
return result
elif name == "getcontenttype":
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/ical.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/ical.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -283,6 +283,37 @@
extraRestrictedProperties = ("SUMMARY", "LOCATION",)
@classmethod
+ def allFromString(clazz, string):
+ """
+ Construct a L{Component} from a string.
+ @param string: a string containing iCalendar data.
+ @return: a C{list} of L{Component}s representing the components described by
+ C{string}.
+ """
+ if type(string) is unicode:
+ string = string.encode("utf-8")
+ return clazz.allFromStream(StringIO.StringIO(string))
+
+ @classmethod
+ def allFromStream(clazz, stream):
+ """
+ Construct possibly multiple L{Component}s from a stream.
+ @param stream: a C{read()}able stream containing iCalendar data.
+ @return: a C{list} of L{Component}s representing the components described by
+ C{stream}.
+ """
+
+ results = []
+ try:
+ for vobject in readComponents(stream):
+ results.append(clazz(None, vobject=vobject))
+ return results
+ except vParseError, e:
+ raise InvalidICalendarDataError(e)
+ except StopIteration, e:
+ raise InvalidICalendarDataError(e)
+
+ @classmethod
def fromString(clazz, string):
"""
Construct a L{Component} from a string.
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/sharing.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/sharing.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -597,170 +597,169 @@
# Add to collections
yield notifications.deleteNotifictionMessageByUID(request, record.inviteuid)
- def xmlPOSTNoAuth(self, encoding, request):
- def _handleErrorResponse(error):
- if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
- return error.value.response
- return error
+ @inlineCallbacks
+ def _xmlHandleInvite(self, request, docroot):
+ yield self.authorize(request, (davxml.Read(), davxml.Write()))
+ result = (yield self._handleInvite(request, docroot))
+ returnValue(result)
+
+ def _handleInvite(self, request, invitedoc):
+ def _handleInviteSet(inviteset):
+ userid = None
+ cn = None
+ access = None
+ summary = None
+ for item in inviteset.children:
+ if isinstance(item, davxml.HRef):
+ userid = str(item)
+ continue
+ if isinstance(item, customxml.CommonName):
+ cn = str(item)
+ continue
+ if isinstance(item, customxml.InviteSummary):
+ summary = str(item)
+ continue
+ if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
+ access = item
+ continue
+ if userid and access and summary:
+ return (userid, cn, access, summary)
+ else:
+ error_text = []
+ if userid is None:
+ error_text.append("missing href")
+ if access is None:
+ error_text.append("missing access")
+ if summary is None:
+ error_text.append("missing summary")
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (customxml.calendarserver_namespace, "valid-request"),
+ "%s: %s" % (", ".join(error_text), inviteset,),
+ ))
- def _handleInvite(invitedoc):
- def _handleInviteSet(inviteset):
- userid = None
- cn = None
+ def _handleInviteRemove(inviteremove):
+ userid = None
+ access = []
+ for item in inviteremove.children:
+ if isinstance(item, davxml.HRef):
+ userid = str(item)
+ continue
+ if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
+ access.append(item)
+ continue
+ if userid is None:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (customxml.calendarserver_namespace, "valid-request"),
+ "missing href: %s" % (inviteremove,),
+ ))
+ if len(access) == 0:
access = None
- summary = None
- for item in inviteset.children:
- if isinstance(item, davxml.HRef):
- userid = str(item)
- continue
- if isinstance(item, customxml.CommonName):
- cn = str(item)
- continue
- if isinstance(item, customxml.InviteSummary):
- summary = str(item)
- continue
- if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
- access = item
- continue
- if userid and access and summary:
- return (userid, cn, access, summary)
- else:
- error_text = []
- if userid is None:
- error_text.append("missing href")
- if access is None:
- error_text.append("missing access")
- if summary is None:
- error_text.append("missing summary")
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (customxml.calendarserver_namespace, "valid-request"),
- "%s: %s" % (", ".join(error_text), inviteset,),
- ))
+ else:
+ access = set(access)
+ return (userid, access)
- def _handleInviteRemove(inviteremove):
- userid = None
- access = []
- for item in inviteremove.children:
- if isinstance(item, davxml.HRef):
- userid = str(item)
- continue
- if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
- access.append(item)
- continue
- if userid is None:
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (customxml.calendarserver_namespace, "valid-request"),
- "missing href: %s" % (inviteremove,),
- ))
- if len(access) == 0:
- access = None
- else:
- access = set(access)
- return (userid, access)
+ def _autoShare(isShared, request):
+ if not isShared:
+ self.upgradeToShare()
- def _autoShare(isShared, request):
- if not isShared:
- self.upgradeToShare()
-
- @inlineCallbacks
- def _processInviteDoc(_, request):
- setDict, removeDict, updateinviteDict = {}, {}, {}
- okusers = set()
- badusers = set()
- for item in invitedoc.children:
- if isinstance(item, customxml.InviteSet):
- userid, cn, access, summary = _handleInviteSet(item)
- setDict[userid] = (cn, access, summary)
+ @inlineCallbacks
+ def _processInviteDoc(_, request):
+ setDict, removeDict, updateinviteDict = {}, {}, {}
+ okusers = set()
+ badusers = set()
+ for item in invitedoc.children:
+ if isinstance(item, customxml.InviteSet):
+ userid, cn, access, summary = _handleInviteSet(item)
+ setDict[userid] = (cn, access, summary)
+
+ # Validate each userid on add only
+ (okusers if self.validUserIDForShare(userid) else badusers).add(userid)
+ elif isinstance(item, customxml.InviteRemove):
+ userid, access = _handleInviteRemove(item)
+ removeDict[userid] = access
- # Validate each userid on add only
- (okusers if self.validUserIDForShare(userid) else badusers).add(userid)
- elif isinstance(item, customxml.InviteRemove):
- userid, access = _handleInviteRemove(item)
- removeDict[userid] = access
-
- # Treat removed userids as valid as we will fail invalid ones silently
- okusers.add(userid)
+ # Treat removed userids as valid as we will fail invalid ones silently
+ okusers.add(userid)
- # Only make changes if all OK
- if len(badusers) == 0:
- # Special case removing and adding the same user and treat that as an add
- sameUseridInRemoveAndSet = [u for u in removeDict.keys() if u in setDict]
- for u in sameUseridInRemoveAndSet:
- removeACL = removeDict[u]
- cn, newACL, summary = setDict[u]
- updateinviteDict[u] = (cn, removeACL, newACL, summary)
- del removeDict[u]
- del setDict[u]
- for userid, access in removeDict.iteritems():
- result = (yield self.uninviteUserToShare(userid, access, request))
- (okusers if result else badusers).add(userid)
- for userid, (cn, access, summary) in setDict.iteritems():
- result = (yield self.inviteUserToShare(userid, cn, access, summary, request))
- (okusers if result else badusers).add(userid)
- for userid, (cn, removeACL, newACL, summary) in updateinviteDict.iteritems():
- result = (yield self.inviteUserUpdateToShare(userid, cn, removeACL, newACL, summary, request))
- (okusers if result else badusers).add(userid)
+ # Only make changes if all OK
+ if len(badusers) == 0:
+ # Special case removing and adding the same user and treat that as an add
+ sameUseridInRemoveAndSet = [u for u in removeDict.keys() if u in setDict]
+ for u in sameUseridInRemoveAndSet:
+ removeACL = removeDict[u]
+ cn, newACL, summary = setDict[u]
+ updateinviteDict[u] = (cn, removeACL, newACL, summary)
+ del removeDict[u]
+ del setDict[u]
+ for userid, access in removeDict.iteritems():
+ result = (yield self.uninviteUserToShare(userid, access, request))
+ (okusers if result else badusers).add(userid)
+ for userid, (cn, access, summary) in setDict.iteritems():
+ result = (yield self.inviteUserToShare(userid, cn, access, summary, request))
+ (okusers if result else badusers).add(userid)
+ for userid, (cn, removeACL, newACL, summary) in updateinviteDict.iteritems():
+ result = (yield self.inviteUserUpdateToShare(userid, cn, removeACL, newACL, summary, request))
+ (okusers if result else badusers).add(userid)
- # Do a final validation of the entire set of invites
- yield self.validateInvites()
-
- # Create the multistatus response - only needed if some are bad
- if badusers:
- xml_responses = []
- xml_responses.extend([
- davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.FAILED_DEPENDENCY))
- for userid in sorted(okusers)
- ])
- xml_responses.extend([
- davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.FORBIDDEN))
- for userid in sorted(badusers)
- ])
-
- #
- # Return response
- #
- returnValue(MultiStatusResponse(xml_responses))
- else:
- returnValue(responsecode.OK)
-
+ # Do a final validation of the entire set of invites
+ yield self.validateInvites()
+
+ # Create the multistatus response - only needed if some are bad
+ if badusers:
+ xml_responses = []
+ xml_responses.extend([
+ davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.FAILED_DEPENDENCY))
+ for userid in sorted(okusers)
+ ])
+ xml_responses.extend([
+ davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.FORBIDDEN))
+ for userid in sorted(badusers)
+ ])
+
+ #
+ # Return response
+ #
+ returnValue(MultiStatusResponse(xml_responses))
+ else:
+ returnValue(responsecode.OK)
- return self.isShared(request).addCallback(_autoShare, request).addCallback(_processInviteDoc, request)
+ return self.isShared(request).addCallback(_autoShare, request).addCallback(_processInviteDoc, request)
- def _getData(data):
- try:
- doc = davxml.WebDAVDocument.fromString(data)
- except ValueError, e:
- self.log_error("Error parsing doc (%s) Doc:\n %s" % (str(e), data,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+ @inlineCallbacks
+ def _xmlHandleInviteReply(self, request, docroot):
+ yield self.authorize(request, (davxml.Read(), davxml.Write()))
+ result = (yield self._handleInviteReply(request, docroot))
+ returnValue(result)
+
+ def _handleInviteReply(self, request, docroot):
+ raise NotImplementedError
- root = doc.root_element
- xmlDocHanders = {
- customxml.InviteShare: _handleInvite,
- }
- if type(root) in xmlDocHanders:
- return xmlDocHanders[type(root)](root).addErrback(_handleErrorResponse)
- else:
- self.log_error("Unsupported XML (%s)" % (root,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+ @inlineCallbacks
+ def xmlRequestHandler(self, request):
+
+ # Need to read the data and get the root element first
+ xmldata = (yield allDataFromStream(request.stream))
+ try:
+ doc = davxml.WebDAVDocument.fromString(xmldata)
+ except ValueError, e:
+ self.log_error("Error parsing doc (%s) Doc:\n %s" % (str(e), xmldata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
- return allDataFromStream(request.stream).addCallback(_getData)
+ root = doc.root_element
+ if type(root) in self.xmlDocHanders:
+ result = (yield self.xmlDocHanders[type(root)](self, request, root))
+ returnValue(result)
+ else:
+ self.log_error("Unsupported XML (%s)" % (root,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
- def xmlPOSTPreconditions(self, _, request):
- if request.headers.hasHeader("Content-Type"):
- mimetype = request.headers.getHeader("Content-Type")
- if mimetype.mediaType in ("application", "text",) and mimetype.mediaSubtype == "xml":
- encoding = mimetype.params["charset"] if "charset" in mimetype.params else "utf8"
- return succeed(encoding)
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+ xmlDocHanders = {
+ customxml.InviteShare: _xmlHandleInvite,
+ customxml.InviteReply: _xmlHandleInviteReply,
+ }
- def xmlPOSTAuth(self, request):
- d = self.authorize(request, (davxml.Read(), davxml.Write()))
- d.addCallback(self.xmlPOSTPreconditions, request)
- d.addCallback(self.xmlPOSTNoAuth, request)
- return d
-
def POST_handler_content_type(self, request, contentType):
if self.isCollection():
if contentType:
@@ -773,8 +772,8 @@
return succeed(responsecode.FORBIDDEN)
_postHandlers = {
- ("application", "xml") : xmlPOSTAuth,
- ("text", "xml") : xmlPOSTAuth,
+ ("application", "xml") : xmlRequestHandler,
+ ("text", "xml") : xmlRequestHandler,
}
inviteAccessMapToXML = {
@@ -1134,63 +1133,37 @@
# Add to collections
yield notifications.addNotification(request, notificationUID, xmltype, xmldata)
- def xmlPOSTNoAuth(self, encoding, request):
+ def _handleInviteReply(self, request, invitereplydoc):
+ """ Handle a user accepting or declining a sharing invite """
+ hostUrl = None
+ accepted = None
+ summary = None
+ replytoUID = None
+ for item in invitereplydoc.children:
+ if isinstance(item, customxml.InviteStatusAccepted):
+ accepted = True
+ elif isinstance(item, customxml.InviteStatusDeclined):
+ accepted = False
+ elif isinstance(item, customxml.InviteSummary):
+ summary = str(item)
+ elif isinstance(item, customxml.HostURL):
+ for hosturlItem in item.children:
+ if isinstance(hosturlItem, davxml.HRef):
+ hostUrl = str(hosturlItem)
+ elif isinstance(item, customxml.InReplyTo):
+ replytoUID = str(item)
+
+ if accepted is None or hostUrl is None or replytoUID is None:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (customxml.calendarserver_namespace, "valid-request"),
+ "missing required XML elements",
+ ))
+ if accepted:
+ return self.acceptInviteShare(request, hostUrl, replytoUID, displayname=summary)
+ else:
+ return self.declineShare(request, hostUrl, replytoUID)
- def _handleErrorResponse(error):
- if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
- return error.value.response
- return error
-
- def _handleInviteReply(invitereplydoc):
- """ Handle a user accepting or declining a sharing invite """
- hostUrl = None
- accepted = None
- summary = None
- replytoUID = None
- for item in invitereplydoc.children:
- if isinstance(item, customxml.InviteStatusAccepted):
- accepted = True
- elif isinstance(item, customxml.InviteStatusDeclined):
- accepted = False
- elif isinstance(item, customxml.InviteSummary):
- summary = str(item)
- elif isinstance(item, customxml.HostURL):
- for hosturlItem in item.children:
- if isinstance(hosturlItem, davxml.HRef):
- hostUrl = str(hosturlItem)
- elif isinstance(item, customxml.InReplyTo):
- replytoUID = str(item)
-
- if accepted is None or hostUrl is None or replytoUID is None:
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (customxml.calendarserver_namespace, "valid-request"),
- "missing required XML elements",
- ))
- if accepted:
- return self.acceptInviteShare(request, hostUrl, replytoUID, displayname=summary)
- else:
- return self.declineShare(request, hostUrl, replytoUID)
-
- def _getData(data):
- try:
- doc = davxml.WebDAVDocument.fromString(data)
- except ValueError, e:
- print "Error parsing doc (%s) Doc:\n %s" % (str(e), data,)
- raise
-
- root = doc.root_element
- xmlDocHanders = {
- customxml.InviteReply: _handleInviteReply,
- }
- if type(root) in xmlDocHanders:
- return xmlDocHanders[type(root)](root).addErrback(_handleErrorResponse)
- else:
- self.log_error("Unsupported XML (%s)" % (root,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
-
- return allDataFromStream(request.stream).addCallback(_getData)
-
class SharedCollectionRecord(object):
def __init__(self, shareuid, sharetype, hosturl, localname, summary):
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/stdconfig.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/stdconfig.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -354,6 +354,7 @@
"EnableDropBox" : False, # Calendar Drop Box
"EnablePrivateEvents" : False, # Private Events
"EnableTimezoneService" : False, # Timezone service
+ "EnableBatchUpload" : False, # POST batch uploads
"Sharing": {
"Enabled" : False, # Overall on/off switch
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/storebridge.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/storebridge.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -15,55 +15,48 @@
# limitations under the License.
##
-"""
-Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
-L{txdav.carddav.iaddressbookstore} and those in L{twistedcaldav}.
-"""
-
-from urlparse import urlsplit
-
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue,\
- maybeDeferred
-from twisted.internet.protocol import Protocol
-from twisted.python.log import err as logDefaultException
-from twisted.python.util import FancyEqMixin
-
from twext.python.log import Logger
-
from twext.web2 import responsecode
from twext.web2.dav import davxml
-from twext.web2.dav.element.base import dav_namespace
-from twext.web2.dav.http import ErrorResponse, ResponseQueue
+from twext.web2.dav.element.base import dav_namespace, WebDAVUnknownElement
+from twext.web2.dav.http import ErrorResponse, ResponseQueue, MultiStatusResponse
from twext.web2.dav.noneprops import NonePropertyStore
from twext.web2.dav.resource import TwistedACLInheritable, AccessDeniedError
-from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, \
- davXMLFromStream
+from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
from twext.web2.http import HTTPError, StatusResponse, Response
from twext.web2.http_headers import ETag, MimeType
-from twext.web2.responsecode import (
- FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
- BAD_REQUEST, OK,
-)
+from twext.web2.responsecode import FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED, BAD_REQUEST, OK
from twext.web2.stream import ProducerStream, readStream, MemoryStream
-
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue, maybeDeferred
+from twisted.internet.protocol import Protocol
+from twisted.python.hashlib import md5
+from twisted.python.log import err as logDefaultException
+from twisted.python.util import FancyEqMixin
+from twistedcaldav import customxml, carddavxml, caldavxml
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.config import config
+from twistedcaldav.ical import Component as VCalendar, Property as VProperty
from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.notifications import NotificationCollectionResource, \
- NotificationResource
+from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
+from twistedcaldav.notifications import NotificationCollectionResource, NotificationResource
from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource
from twistedcaldav.schedule import ScheduleInboxResource
from twistedcaldav.scheduling.implicit import ImplicitScheduler
-from twistedcaldav.ical import Component as VCalendar
-from twistedcaldav.ical import Property as VProperty
from twistedcaldav.vcard import Component as VCard
-
+from twistedcaldav.vcard import Property as VCardProperty
from txdav.base.propertystore.base import PropertyName
from txdav.common.icommondatastore import NoSuchObjectResourceError
+from urlparse import urlsplit
+import time
+"""
+Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
+L{txdav.carddav.iaddressbookstore} and those in L{twistedcaldav}.
+"""
+
log = Logger()
-
class _NewStorePropertiesWrapper(object):
"""
Wrap a new-style property store (a L{txdav.idav.IPropertyStore}) in the old-
@@ -465,6 +458,384 @@
yield self._newStoreObject.rename(basename)
returnValue(NO_CONTENT)
+ def liveProperties(self):
+ baseProperties = ()
+ if config.EnableBatchUpload:
+ baseProperties += (
+ customxml.BulkRequests.qname(),
+ )
+ return super(_CommonHomeChildCollectionMixin, self).liveProperties() + baseProperties
+
+ @inlineCallbacks
+ def _readGlobalProperty(self, qname, property, request):
+
+ if config.EnableBatchUpload and qname == customxml.BulkRequests.qname():
+ returnValue(customxml.BulkRequests(
+ customxml.Simple(
+ customxml.MaxResources.fromString("100"),
+ customxml.MaxBytes.fromString("%d" % (10*1024*1024,)),
+ ),
+ customxml.CRUD(
+ customxml.MaxResources.fromString("100"),
+ customxml.MaxBytes.fromString("%d" % (10*1024*1024,)),
+ ),
+ ))
+ else:
+ result = (yield super(_CommonHomeChildCollectionMixin, self)._readGlobalProperty(qname, property, request))
+ returnValue(result)
+
+ @inlineCallbacks
+ def checkCTagPrecondition(self, request):
+ if request.headers.hasHeader("If"):
+ iffy = request.headers.getRawHeaders("If")[0]
+ prefix = "<%sctag/" % (customxml.mm_namespace,)
+ if prefix in iffy:
+ testctag = iffy[iffy.find(prefix):]
+ testctag = testctag[len(prefix):]
+ testctag = testctag.split(">", 1)[0]
+ ctag = (yield self.getSyncToken())
+ if testctag != ctag:
+ raise HTTPError(StatusResponse(responsecode.PRECONDITION_FAILED, "CTag pre-condition failure"))
+
+ def checkReturnChanged(self, request):
+ if request.headers.hasHeader("X-MobileMe-DAV-Options"):
+ return_changed = request.headers.getRawHeaders("X-MobileMe-DAV-Options")[0]
+ return ("return-changed-data" in return_changed)
+ else:
+ return False
+
+ @requiresPermissions(davxml.Bind())
+ @inlineCallbacks
+ def simpleBatchPOST(self, request):
+
+ # If CTag precondition
+ yield self.checkCTagPrecondition(request)
+
+ # Look for return changed data option
+ return_changed = self.checkReturnChanged(request)
+
+ # Read in all data
+ data = (yield allDataFromStream(request.stream))
+
+ components = self.componentsFromData(data)
+
+ # Build response
+ xmlresponses = []
+ for component in components:
+
+ code = None
+ error = None
+ dataChanged = None
+ try:
+ componentdata = str(component)
+
+ # Create a new name if one was not provided
+ name = md5(str(componentdata) + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
+
+ # Get a resource for the new item
+ newchildURL = joinURL(request.path, name)
+ newchild = (yield request.locateResource(newchildURL))
+ yield self.storeResourceData(request, newchild, newchildURL, componentdata)
+
+ # FIXME: figure out return_changed behavior
+ if return_changed:
+ pass
+# note = component.getProperty("NOTE")
+# if note is None:
+# component.addProperty(VCardProperty("NOTE", component.resourceUID()))
+# else:
+# note.setValue(component.resourceUID())
+# dataChanged = str(component)
+
+ except HTTPError, e:
+ # Extract the pre-condition
+ code = e.response.code
+ if isinstance(e.response, ErrorResponse):
+ error = e.response.error
+ error = (error.namespace, error.name,)
+ except Exception:
+ code = responsecode.BAD_REQUEST
+
+ if code is None:
+
+ if not return_changed or not dataChanged:
+ xmlresponses.append(
+ davxml.PropertyStatusResponse(
+ davxml.HRef.fromString(newchildURL),
+ davxml.PropertyStatus(
+ davxml.PropertyContainer(
+ davxml.GETETag.fromString(newchild.etag().generate()),
+ customxml.UID.fromString(component.resourceUID()),
+ ),
+ davxml.Status.fromResponseCode(responsecode.OK),
+ )
+ )
+ )
+ else:
+ xmlresponses.append(
+ davxml.PropertyStatusResponse(
+ davxml.HRef.fromString(newchildURL),
+ davxml.PropertyStatus(
+ davxml.PropertyContainer(
+ davxml.GETETag.fromString(newchild.etag().generate()),
+ self.xmlDataElementType().fromAddressData(dataChanged),
+ ),
+ davxml.Status.fromResponseCode(responsecode.OK),
+ )
+ )
+ )
+
+ else:
+ xmlresponses.append(
+ davxml.StatusResponse(
+ davxml.HRef.fromString(""),
+ davxml.Status.fromResponseCode(code),
+ davxml.Error(
+ WebDAVUnknownElement.fromQname(*error),
+ customxml.UID.fromString(component.resourceUID()),
+ ) if error else None,
+ )
+ )
+
+ result = MultiStatusResponse(xmlresponses)
+
+ newctag = (yield self.getSyncToken())
+ result.headers.setRawHeaders("CTag", (newctag,))
+
+ # Setup some useful logging
+ request.submethod = "Simple batch"
+ if not hasattr(request, "extendedLogItems"):
+ request.extendedLogItems = {}
+ request.extendedLogItems["rcount"] = len(xmlresponses)
+
+ returnValue(result)
+
+ @inlineCallbacks
+ def crudBatchPOST(self, request, xmlroot):
+
+ # Need to force some kind of overall authentication on the request
+ yield self.authorize(request, (davxml.Read(), davxml.Write(),))
+
+ # If CTag precondition
+ yield self.checkCTagPrecondition(request)
+
+ # Look for return changed data option
+ return_changed = self.checkReturnChanged(request)
+
+ # Build response
+ xmlresponses = []
+ checkedBindPrivelege = None
+ checkedUnbindPrivelege = None
+ for xmlchild in xmlroot.children:
+
+ # Determine the multiput operation: create, update, delete
+ href = xmlchild.childOfType(davxml.HRef.qname())
+ set = xmlchild.childOfType(davxml.Set.qname())
+ xmldata = set.childOfType(self.xmlDataElementType().qname()) if set is not None else None
+ if href is None:
+
+ # Do privilege check on collection once
+ if checkedBindPrivelege is None:
+ try:
+ yield self.authorize(request, (davxml.Bind(),))
+ checkedBindPrivelege = True
+ except HTTPError, e:
+ checkedBindPrivelege = e
+
+ # Create operations
+ yield self.crudCreate(request, xmldata.generateComponent(), xmlresponses, return_changed, checkedBindPrivelege)
+ else:
+ delete = xmlchild.childOfType(customxml.Delete.qname())
+ ifmatch = xmlchild.childOfType(customxml.IfMatch.qname())
+ if ifmatch:
+ ifmatch = str(ifmatch.children[0]) if len(ifmatch.children) == 1 else None
+ if delete is None:
+ yield self.crudUpdate(request, str(href), xmldata.generateComponent(), ifmatch, return_changed, xmlresponses)
+ else:
+ # Do privilege check on collection once
+ if checkedUnbindPrivelege is None:
+ try:
+ yield self.authorize(request, (davxml.Unbind(),))
+ checkedUnbindPrivelege = True
+ except HTTPError, e:
+ checkedUnbindPrivelege = e
+
+ yield self.crudDelete(request, str(href), ifmatch, xmlresponses, checkedUnbindPrivelege);
+
+ result = MultiStatusResponse(xmlresponses)
+
+ newctag = (yield self.getSyncToken())
+ result.headers.setRawHeaders("CTag", (newctag,))
+
+ # Setup some useful logging
+ request.submethod = "CRUD batch"
+ if not hasattr(request, "extendedLogItems"):
+ request.extendedLogItems = {}
+ request.extendedLogItems["rcount"] = len(xmlresponses)
+
+ returnValue(result)
+
+ @inlineCallbacks
+ def crudCreate(self, request, component, xmlresponses, return_changed, hasPrivilege):
+
+ code = None
+ error = None
+ try:
+ componentdata = str(component)
+ if isinstance(hasPrivilege, HTTPError):
+ raise hasPrivilege
+
+ # Create a new name if one was not provided
+ name = md5(str(componentdata) + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
+
+ # Get a resource for the new item
+ newchildURL = joinURL(request.path, name)
+ newchild = (yield request.locateResource(newchildURL))
+ yield self.storeResourceData(request, newchild, newchildURL, componentdata)
+
+ # FIXME: figure out return_changed behavior
+
+ except HTTPError, e:
+ # Extract the pre-condition
+ code = e.response.code
+ if isinstance(e.response, ErrorResponse):
+ error = e.response.error
+ error = (error.namespace, error.name,)
+
+ except Exception:
+ code = responsecode.BAD_REQUEST
+
+ if code is None:
+ xmlresponses.append(
+ davxml.PropertyStatusResponse(
+ davxml.HRef.fromString(newchildURL),
+ davxml.PropertyStatus(
+ davxml.PropertyContainer(
+ davxml.GETETag.fromString(newchild.etag().generate()),
+ customxml.UID.fromString(component.resourceUID()),
+ ),
+ davxml.Status.fromResponseCode(responsecode.OK),
+ )
+ )
+ )
+ else:
+ xmlresponses.append(
+ davxml.StatusResponse(
+ davxml.HRef.fromString(""),
+ davxml.Status.fromResponseCode(code),
+ davxml.Error(
+ WebDAVUnknownElement.fromQname(*error),
+ customxml.UID.fromString(component.resourceUID()),
+ ) if error else None,
+ )
+ )
+
+ @inlineCallbacks
+ def crudUpdate(self, request, href, component, ifmatch, return_changed, xmlresponses):
+ code = None
+ error = None
+ try:
+ componentdata = str(component)
+
+ updateResource = (yield request.locateResource(href))
+ if not updateResource.exists():
+ raise HTTPError(responsecode.NOT_FOUND)
+
+ # Check privilege
+ yield updateResource.authorize(request, (davxml.Write(),))
+
+ # Check if match
+ if ifmatch and ifmatch != updateResource.etag().generate():
+ raise HTTPError(responsecode.PRECONDITION_FAILED)
+
+ yield self.storeResourceData(request, updateResource, href, componentdata)
+
+ # FIXME: figure out return_changed behavior
+
+ except HTTPError, e:
+ # Extract the pre-condition
+ code = e.response.code
+ if isinstance(e.response, ErrorResponse):
+ error = e.response.error
+ error = (error.namespace, error.name,)
+
+ except Exception:
+ code = responsecode.BAD_REQUEST
+
+ if code is None:
+ xmlresponses.append(
+ davxml.PropertyStatusResponse(
+ davxml.HRef.fromString(href),
+ davxml.PropertyStatus(
+ davxml.PropertyContainer(
+ davxml.GETETag.fromString(updateResource.etag().generate()),
+ ),
+ davxml.Status.fromResponseCode(responsecode.OK),
+ )
+ )
+ )
+ else:
+ xmlresponses.append(
+ davxml.StatusResponse(
+ davxml.HRef.fromString(href),
+ davxml.Status.fromResponseCode(code),
+ davxml.Error(
+ WebDAVUnknownElement.fromQname(*error),
+ ) if error else None,
+ )
+ )
+
+ @inlineCallbacks
+ def crudDelete(self, request, href, ifmatch, xmlresponses, hasPrivilege):
+ code = None
+ error = None
+ try:
+ if isinstance(hasPrivilege, HTTPError):
+ raise hasPrivilege
+
+ deleteResource = (yield request.locateResource(href))
+ if not deleteResource.exists():
+ raise HTTPError(responsecode.NOT_FOUND)
+
+ # Check if match
+ if ifmatch and ifmatch != deleteResource.etag().generate():
+ raise HTTPError(responsecode.PRECONDITION_FAILED)
+
+ yield deleteResource.storeRemove(
+ request,
+ True,
+ href,
+ )
+
+ except HTTPError, e:
+ # Extract the pre-condition
+ code = e.response.code
+ if isinstance(e.response, ErrorResponse):
+ error = e.response.error
+ error = (error.namespace, error.name,)
+
+ except Exception:
+ code = responsecode.BAD_REQUEST
+
+ if code is None:
+ xmlresponses.append(
+ davxml.StatusResponse(
+ davxml.HRef.fromString(href),
+ davxml.Status.fromResponseCode(responsecode.OK),
+ )
+ )
+ else:
+ xmlresponses.append(
+ davxml.StatusResponse(
+ davxml.HRef.fromString(href),
+ davxml.Status.fromResponseCode(code),
+ davxml.Error(
+ WebDAVUnknownElement.fromQname(*error),
+ ) if error else None,
+ )
+ )
+
+
class CalendarCollectionResource(_CommonHomeChildCollectionMixin, CalDAVResource):
"""
Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
@@ -482,6 +853,9 @@
self._initializeWithHomeChild(calendar, home)
self._name = calendar.name() if calendar else name
+ if config.EnableBatchUpload:
+ self._postHandlers[("text", "calendar")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
+ self.xmlDocHanders[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
def __repr__(self):
return "<Calendar Collection Resource %r:%r %s>" % (
@@ -560,8 +934,31 @@
createCalendarCollection = _CommonHomeChildCollectionMixin.createCollection
+ @classmethod
+ def componentsFromData(cls, data):
+ return VCalendar.allFromString(data)
+ @classmethod
+ def resourceSuffix(cls):
+ return ".ics"
+
+ @classmethod
+ def xmlDataElementType(cls):
+ return caldavxml.CalendarData
+
@inlineCallbacks
+ def storeResourceData(self, request, newchild, newchildURL, data):
+ storer = StoreCalendarObjectResource(
+ request = request,
+ destination = newchild,
+ destination_uri = newchildURL,
+ destinationcal = True,
+ destinationparent = self,
+ calendar = data,
+ )
+ yield storer.run()
+
+ @inlineCallbacks
def storeRemove(self, request, implicitly, where):
"""
Delete this calendar collection resource, first deleting each contained
@@ -1358,6 +1755,9 @@
self._initializeWithHomeChild(addressbook, home)
self._name = addressbook.name() if addressbook else name
+ if config.EnableBatchUpload:
+ self._postHandlers[("text", "vcard")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
+ self.xmlDocHanders[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
def __repr__(self):
return "<AddressBook Collection Resource %r:%r %s>" % (
@@ -1380,7 +1780,32 @@
createAddressBookCollection = _CommonHomeChildCollectionMixin.createCollection
+ @classmethod
+ def componentsFromData(cls, data):
+ return VCard.allFromString(data)
+ @classmethod
+ def resourceSuffix(cls):
+ return ".vcf"
+
+ @classmethod
+ def xmlDataElementType(cls):
+ return carddavxml.AddressData
+
+ @inlineCallbacks
+ def storeResourceData(self, request, newchild, newchildURL, data):
+ storer = StoreAddressObjectResource(
+ request = request,
+ sourceadbk = False,
+ destination = newchild,
+ destination_uri = newchildURL,
+ destinationadbk = True,
+ destinationparent = self,
+ vcard = data,
+ )
+ yield storer.run()
+
+
class GlobalAddressBookCollectionResource(GlobalAddressBookResource, AddressBookCollectionResource):
"""
Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
Modified: CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/vcard.py
===================================================================
--- CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/vcard.py 2010-12-17 20:12:23 UTC (rev 6701)
+++ CalendarServer/branches/users/cdaboo/batchupload-6699/twistedcaldav/vcard.py 2010-12-20 15:12:50 UTC (rev 6702)
@@ -142,6 +142,37 @@
raise InvalidVCardDataError(e)
@classmethod
+ def allFromString(clazz, string):
+ """
+ Construct a L{Component} from a string.
+ @param string: a string containing vCard data.
+ @return: a C{list} of L{Component}s representing the components described by
+ C{string}.
+ """
+ if type(string) is unicode:
+ string = string.encode("utf-8")
+ return clazz.allFromStream(StringIO.StringIO(string))
+
+ @classmethod
+ def allFromStream(clazz, stream):
+ """
+ Construct possibly multiple L{Component}s from a stream.
+ @param stream: a C{read()}able stream containing vCard data.
+ @return: a C{list} of L{Component}s representing the components described by
+ C{stream}.
+ """
+
+ results = []
+ try:
+ for vobject in readComponents(stream):
+ results.append(clazz(None, vobject=vobject))
+ return results
+ except vParseError, e:
+ raise InvalidVCardDataError(e)
+ except StopIteration, e:
+ raise InvalidVCardDataError(e)
+
+ @classmethod
def fromIStream(clazz, stream):
"""
Construct a L{Component} from a stream.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20101220/1a504d5f/attachment-0001.html>
More information about the calendarserver-changes
mailing list