[CalendarServer-changes] [7204] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 16 18:27:44 PDT 2011


Revision: 7204
          http://trac.macosforge.org/projects/calendarserver/changeset/7204
Author:   cdaboo at apple.com
Date:     2011-03-16 18:27:43 -0700 (Wed, 16 Mar 2011)
Log Message:
-----------
Support bulk POST operations.

Modified Paths:
--------------
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/twext/web2/dav/element/base.py
    CalendarServer/trunk/twistedcaldav/caldavxml.py
    CalendarServer/trunk/twistedcaldav/carddavxml.py
    CalendarServer/trunk/twistedcaldav/customxml.py
    CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/method/put_common.py
    CalendarServer/trunk/twistedcaldav/sharing.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/twistedcaldav/vcard.py

Property Changed:
----------------
    CalendarServer/trunk/
    CalendarServer/trunk/txdav/caldav/datastore/index_file.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py
    CalendarServer/trunk/txdav/carddav/datastore/index_file.py
    CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py


Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2011-03-17 01:27:43 UTC (rev 7204)
@@ -806,6 +806,10 @@
     <key>EnableTimezoneService</key>
     <true/>
 
+    <!-- Batch Upload via POST -->
+    <key>EnableBatchUpload</key>
+    <true/>
+
     <!-- Shared Calendars & Address Books -->
     <key>Sharing</key>
     <dict>

Modified: CalendarServer/trunk/twext/web2/dav/element/base.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/element/base.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twext/web2/dav/element/base.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -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/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -340,6 +340,8 @@
         else:
             raise ValueError("Not a calendar: %s" % (calendar,))
 
+    fromTextData = fromCalendar
+
     def __init__(self, *children, **attributes):
         super(CalendarData, self).__init__(*children, **attributes)
 
@@ -433,6 +435,8 @@
         else:
             return None
 
+    generateComponent = calendar
+
     def calendarData(self):
         """
         Returns the calendar data derived from this element.

Modified: CalendarServer/trunk/twistedcaldav/carddavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/carddavxml.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/carddavxml.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -200,6 +200,8 @@
         """
         return clazz(davxml.PCDATAElement(addressdata))
 
+    fromTextData = fromAddressData
+
     def __init__(self, *children, **attributes):
         super(AddressData, self).__init__(*children, **attributes)
 
@@ -280,6 +282,8 @@
         else:
             return None
 
+    generateComponent = address
+
     def addressData(self):
         """
         Returns an address component derived from this element.

Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/customxml.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -995,7 +995,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 MaxBulkResources (davxml.WebDAVTextElement):
+    namespace = mm_namespace
+    name = "max-resources"
+
+class MaxBulkBytes (davxml.WebDAVTextElement):
+    namespace = mm_namespace
+    name = "max-bytes"
+
+
 ##
 # Extensions to davxml.ResourceType
 ##

Modified: CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -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/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -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/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -815,6 +815,9 @@
     @inlineCallbacks
     def doStore(self, implicit):
 
+        # Stash the current calendar data as we may need to return it
+        self.returndata = str(self.calendar)
+
         # Always do the per-user data merge right before we store
         yield self.mergePerUserData()
 

Modified: CalendarServer/trunk/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharing.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/sharing.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -115,7 +115,7 @@
             raise HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
                 (customxml.calendarserver_namespace, "valid-request"),
-                "invalid share",
+                "Invalid share",
             ))
             
         record = yield self.invitesDB().recordForInviteUID(inviteUID)
@@ -123,7 +123,7 @@
             raise HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
                 (customxml.calendarserver_namespace, "valid-request"),
-                "invalid invitation uid: %s" % (inviteUID,),
+                "Invalid invitation uid: %s" % (inviteUID,),
             ))
         
         # Only certain states are sharer controlled
@@ -597,182 +597,177 @@
         # 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
-
-        def _handleInvite(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 _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
+    @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:
-                    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()
-
-            @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
-                        
-                        # 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)
-
-                # 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)
-
-        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,))
+                    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"),
-                    "Invalid XML",
+                    "%s: %s" % (", ".join(error_text), inviteset,),
                 ))
 
-            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,))
+        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"),
-                    "Unsupported XML",
+                    "Missing href: %s" % (inviteremove,),
                 ))
+            if len(access) == 0:
+                access = None
+            else:
+                access = set(access)
+            return (userid, access)
 
-        return allDataFromStream(request.stream).addCallback(_getData)
+        def _autoShare(isShared, request):
+            if not isShared:
+                self.upgradeToShare()
 
-    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"),
-            "Invalid request content-type",
-        ))
+        @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
+                    
+                    # Treat removed userids as valid as we will fail invalid ones silently
+                    okusers.add(userid)
 
-    def xmlPOSTAuth(self, request):
-        d = self.authorize(request, (davxml.Read(), davxml.Write()))
-        d.addCallback(self.xmlPOSTPreconditions, request)
-        d.addCallback(self.xmlPOSTNoAuth, request)
-        return d
+            # 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)
+
+        return self.isShared(request).addCallback(_autoShare, request).addCallback(_processInviteDoc, 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
+
+    @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"),
+                "Invalid XML",
+            ))
+
+        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"),
+                "Unsupported XML",
+            ))
+
+    xmlDocHanders = {
+        customxml.InviteShare: _xmlHandleInvite,
+        customxml.InviteReply: _xmlHandleInviteReply,          
+    }
+
     def POST_handler_content_type(self, request, contentType):
         if self.isCollection():
             if contentType:
@@ -785,8 +780,8 @@
         return succeed(responsecode.FORBIDDEN)
 
     _postHandlers = {
-        ("application", "xml") : xmlPOSTAuth,
-        ("text", "xml") : xmlPOSTAuth,
+        ("application", "xml") : xmlRequestHandler,
+        ("text", "xml") : xmlRequestHandler,
     }
 
 inviteAccessMapToXML = {
@@ -1146,67 +1141,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"),
-                    "Unsupported XML",
-                ))
-
-        return allDataFromStream(request.stream).addCallback(_getData)
-
 class SharedCollectionRecord(object):
     
     def __init__(self, shareuid, sharetype, hosturl, localname, summary):

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -436,6 +436,10 @@
     "EnableDropBox"           : False, # Calendar Drop Box
     "EnablePrivateEvents"     : False, # Private Events
     "EnableTimezoneService"   : False, # Timezone service
+
+    "EnableBatchUpload"       : False,    # POST batch uploads
+    "MaxResourcesBatchUpload" : 100,      # Maximum number of resources in a batch POST
+    "MaxBytesBatchUpload"     : 10485760, # Maximum size of a batch POST (10 MB)
     
     "Sharing": {
         "Enabled"             : False, # Overall on/off switch

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -15,59 +15,51 @@
 # 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.cache import CacheStoreNotifier, ResponseCacheMixin,\
     DisabledCacheNotifier
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
-from twistedcaldav import customxml
+from twistedcaldav.ical import Component as VCalendar, Property as VProperty,\
+    InvalidICalendarDataError
 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 Component as VCard, InvalidVCardDataError
 from txdav.base.propertystore.base import PropertyName
 from txdav.common.icommondatastore import NoSuchObjectResourceError
+from urlparse import urlsplit
+import time
 from txdav.idav import PropertyChangeNotAllowedError
 
+"""
+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-
@@ -230,6 +222,9 @@
         if config.MaxResourcesPerCollection:
             props += (customxml.MaxResources.qname(),)
 
+        if config.EnableBatchUpload:
+            props += (customxml.BulkRequests.qname(),)
+
         return props
 
     @inlineCallbacks
@@ -496,6 +491,377 @@
         yield self._newStoreObject.rename(basename)
         returnValue(NO_CONTENT)
 
+    @inlineCallbacks
+    def _readGlobalProperty(self, qname, property, request):
+
+        if config.EnableBatchUpload and qname == customxml.BulkRequests.qname():
+            returnValue(customxml.BulkRequests(
+                customxml.Simple(
+                    customxml.MaxBulkResources.fromString(str(config.MaxResourcesBatchUpload)),
+                    customxml.MaxBulkBytes.fromString(str(config.MaxBytesBatchUpload)),
+                ),
+                customxml.CRUD(
+                    customxml.MaxBulkResources.fromString(str(config.MaxResourcesBatchUpload)),
+                    customxml.MaxBulkBytes.fromString(str(config.MaxBytesBatchUpload)),
+                ),
+            ))
+        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)
+        if components is None:
+            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body"))
+        
+        # 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))
+                dataChanged = (yield self.storeResourceData(request, newchild, newchildURL, componentdata))
+
+            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 dataChanged 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.PropertyStatusResponse(
+                            davxml.HRef.fromString(newchildURL),
+                            davxml.PropertyStatus(
+                                davxml.PropertyContainer(
+                                    davxml.GETETag.fromString(newchild.etag().generate()),
+                                    self.xmlDataElementType().fromTextData(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())
+            prop = set.childOfType(davxml.PropertyContainer.qname()) if set is not None else None
+            xmldata_root = prop if prop else set
+            xmldata = xmldata_root.childOfType(self.xmlDataElementType().qname()) if xmldata_root is not None else None
+            if href is None:
+                
+                if xmldata is None:
+                    raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body without a DAV:Href present"))
+                
+                # 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:
+                    if set is None:
+                        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body - no set of delete operation"))
+                    if xmldata is None:
+                        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body for set operation"))
+                    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,
+                )
+            )
+
+
     def notifierID(self, label="default"):
         self._newStoreObject.notifierID(label)
 
@@ -519,6 +885,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>" % (
@@ -597,8 +966,80 @@
 
     createCalendarCollection = _CommonHomeChildCollectionMixin.createCollection
 
+    @classmethod
+    def componentsFromData(cls, data):
+        """
+        Need to split a single VCALENDAR into separate ones based on UID with the
+        appropriate VTIEMZONES included.
+        """
+        
+        results = []
 
+        # Split into components by UID and TZID
+        try:
+            vcal =  VCalendar.fromString(data)
+        except InvalidICalendarDataError:
+            return None
+
+        by_uid = {}
+        by_tzid = {}
+        for subcomponent in vcal.subcomponents():
+            if subcomponent.name() == "VTIMEZONE":
+                by_tzid[subcomponent.propertyValue("TZID")] = subcomponent
+            else:
+                by_uid.setdefault(subcomponent.propertyValue("UID"), []).append(subcomponent)
+        
+        # Re-constitute as separate VCALENDAR objects
+        for components in by_uid.values():
+            
+            newvcal = VCalendar("VCALENDAR")
+            newvcal.addProperty(VProperty("PRODID", vcal.propertyValue("PRODID")))
+            
+            # Get the set of TZIDs and include them
+            tzids = set()
+            for component in components:
+                tzids.update(component.timezoneIDs())
+            for tzid in tzids:
+                try:
+                    tz = by_tzid[tzid]
+                    newvcal.addComponent(tz)
+                except KeyError:
+                    # We ignore the error and generate invalid ics which someone will
+                    # complain about at some point
+                    pass
+            
+            # Now add each component
+            for component in components:
+                newvcal.addComponent(component)
+ 
+            results.append(newvcal)
+        
+        return results
+
+    @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()
+        
+        returnValue(storer.returndata if hasattr(storer, "returndata") else None)
+            
+
+    @inlineCallbacks
     def storeRemove(self, request, implicitly, where):
         """
         Delete this calendar collection resource, first deleting each contained
@@ -1462,6 +1903,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>" % (
@@ -1484,7 +1928,36 @@
 
     createAddressBookCollection = _CommonHomeChildCollectionMixin.createCollection
 
+    @classmethod
+    def componentsFromData(cls, data):
+        try:
+            return VCard.allFromString(data)
+        except InvalidVCardDataError:
+            return None
 
+    @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()
+        
+        returnValue(storer.returndata if hasattr(storer, "returndata") else None)
+
 class GlobalAddressBookCollectionResource(GlobalAddressBookResource, AddressBookCollectionResource):
     """
     Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.

Modified: CalendarServer/trunk/twistedcaldav/vcard.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/vcard.py	2011-03-17 01:22:16 UTC (rev 7203)
+++ CalendarServer/trunk/twistedcaldav/vcard.py	2011-03-17 01:27:43 UTC (rev 7204)
@@ -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.


Property changes on: CalendarServer/trunk/txdav/caldav/datastore/index_file.py
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation/txdav/caldav/datastore/index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/caldav/datastore/index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/caldav/datastore/index_file.py:6167-6191
/CalendarServer/branches/new-store/txdav/caldav/datastore/index_file.py:5594-5934
/CalendarServer/branches/new-store-no-caldavfile/txdav/caldav/datastore/index_file.py:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/caldav/datastore/index_file.py:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/caldav/datastore/index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/caldav/datastore/index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/caldav/datastore/index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/caldav/datastore/index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/caldav/datastore/index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/caldav/datastore/index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/caldav/datastore/index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/caldav/datastore/index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/caldav/datastore/index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/caldav/datastore/index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/caldav/datastore/index_file.py:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/caldav/datastore/index_file.py:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/caldav/datastore/index_file.py:6369-6445
/CalendarServer/branches/users/glyph/oracle/txdav/caldav/datastore/index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/caldav/datastore/index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/caldav/datastore/index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/caldav/datastore/index_file.py:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/caldav/datastore/index_file.py:5084-5149
/CalendarServer/branches/users/sagen/locations-resources/txdav/caldav/datastore/index_file.py:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/caldav/datastore/index_file.py:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events/txdav/caldav/datastore/index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/caldav/datastore/index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/caldav/datastore/index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/caldav/datastore/index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/caldav/datastore/index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/index.py:6322-6394
   + /CalendarServer/branches/config-separation/txdav/caldav/datastore/index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/caldav/datastore/index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/caldav/datastore/index_file.py:6167-6191
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/caldav/datastore/index_file.py:5936-5981
/CalendarServer/branches/new-store-no-caldavfile/txdav/caldav/datastore/index_file.py:5911-5935
/CalendarServer/branches/new-store/txdav/caldav/datastore/index_file.py:5594-5934
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/caldav/datastore/index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/caldav/datastore/index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/caldav/datastore/index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/caldav/datastore/index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/caldav/datastore/index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/caldav/datastore/index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/caldav/datastore/index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/caldav/datastore/index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/caldav/datastore/index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/caldav/datastore/index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/caldav/datastore/index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/caldav/datastore/index_file.py:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/caldav/datastore/index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/caldav/datastore/index_file.py:6369
/CalendarServer/branches/users/glyph/oracle/txdav/caldav/datastore/index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/caldav/datastore/index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/caldav/datastore/index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/caldav/datastore/index_file.py:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/caldav/datastore/index_file.py:5084-5149
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/caldav/datastore/index_file.py:5052-5061
/CalendarServer/branches/users/sagen/locations-resources/txdav/caldav/datastore/index_file.py:5032-5051
/CalendarServer/branches/users/sagen/purge_old_events/txdav/caldav/datastore/index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/caldav/datastore/index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/caldav/datastore/index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/caldav/datastore/index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/caldav/datastore/index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/index.py:6322-6394


Property changes on: CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation/txdav/caldav/datastore/test/test_index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/caldav/datastore/test/test_index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/caldav/datastore/test/test_index_file.py:6167-6191
/CalendarServer/branches/new-store/txdav/caldav/datastore/test/test_index_file.py:5594-5934
/CalendarServer/branches/new-store-no-caldavfile/txdav/caldav/datastore/test/test_index_file.py:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/caldav/datastore/test/test_index_file.py:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/caldav/datastore/test/test_index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/caldav/datastore/test/test_index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/caldav/datastore/test/test_index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/caldav/datastore/test/test_index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/caldav/datastore/test/test_index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/caldav/datastore/test/test_index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/caldav/datastore/test/test_index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/caldav/datastore/test/test_index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/caldav/datastore/test/test_index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/caldav/datastore/test/test_index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/caldav/datastore/test/test_index_file.py:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/caldav/datastore/test/test_index_file.py:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/caldav/datastore/test/test_index_file.py:6369-6445
/CalendarServer/branches/users/glyph/oracle/txdav/caldav/datastore/test/test_index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/caldav/datastore/test/test_index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/caldav/datastore/test/test_index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/caldav/datastore/test/test_index_file.py:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/caldav/datastore/test/test_index_file.py:5084-5149
/CalendarServer/branches/users/sagen/locations-resources/txdav/caldav/datastore/test/test_index_file.py:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/caldav/datastore/test/test_index_file.py:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events/txdav/caldav/datastore/test/test_index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/caldav/datastore/test/test_index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/caldav/datastore/test/test_index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/caldav/datastore/test/test_index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/caldav/datastore/test/test_index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/test/test_index.py:6322-6394
   + /CalendarServer/branches/config-separation/txdav/caldav/datastore/test/test_index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/caldav/datastore/test/test_index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/caldav/datastore/test/test_index_file.py:6167-6191
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/caldav/datastore/test/test_index_file.py:5936-5981
/CalendarServer/branches/new-store-no-caldavfile/txdav/caldav/datastore/test/test_index_file.py:5911-5935
/CalendarServer/branches/new-store/txdav/caldav/datastore/test/test_index_file.py:5594-5934
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/caldav/datastore/test/test_index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/caldav/datastore/test/test_index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/caldav/datastore/test/test_index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/caldav/datastore/test/test_index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/caldav/datastore/test/test_index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/caldav/datastore/test/test_index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/caldav/datastore/test/test_index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/caldav/datastore/test/test_index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/caldav/datastore/test/test_index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/caldav/datastore/test/test_index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/caldav/datastore/test/test_index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/caldav/datastore/test/test_index_file.py:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/caldav/datastore/test/test_index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/caldav/datastore/test/test_index_file.py:6369
/CalendarServer/branches/users/glyph/oracle/txdav/caldav/datastore/test/test_index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/caldav/datastore/test/test_index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/caldav/datastore/test/test_index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/caldav/datastore/test/test_index_file.py:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/caldav/datastore/test/test_index_file.py:5084-5149
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/caldav/datastore/test/test_index_file.py:5052-5061
/CalendarServer/branches/users/sagen/locations-resources/txdav/caldav/datastore/test/test_index_file.py:5032-5051
/CalendarServer/branches/users/sagen/purge_old_events/txdav/caldav/datastore/test/test_index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/caldav/datastore/test/test_index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/caldav/datastore/test/test_index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/caldav/datastore/test/test_index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/caldav/datastore/test/test_index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/test/test_index.py:6322-6394


Property changes on: CalendarServer/trunk/txdav/carddav/datastore/index_file.py
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation/txdav/carddav/datastore/index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/carddav/datastore/index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/carddav/datastore/index_file.py:6167-6191
/CalendarServer/branches/new-store/txdav/carddav/datastore/index_file.py:5594-5934
/CalendarServer/branches/new-store-no-caldavfile/txdav/carddav/datastore/index_file.py:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/carddav/datastore/index_file.py:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/carddav/datastore/index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/carddav/datastore/index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/carddav/datastore/index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/carddav/datastore/index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/carddav/datastore/index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/carddav/datastore/index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/carddav/datastore/index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/carddav/datastore/index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/carddav/datastore/index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/carddav/datastore/index_file.py:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/carddav/datastore/index_file.py:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/carddav/datastore/index_file.py:6369-6445
/CalendarServer/branches/users/glyph/oracle/txdav/carddav/datastore/index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/carddav/datastore/index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/carddav/datastore/index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/carddav/datastore/index_file.py:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/carddav/datastore/index_file.py:5084-5149
/CalendarServer/branches/users/sagen/locations-resources/txdav/carddav/datastore/index_file.py:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/carddav/datastore/index_file.py:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events/txdav/carddav/datastore/index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/carddav/datastore/index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/carddav/datastore/index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/carddav/datastore/index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/carddav/datastore/index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/vcardindex.py:6322-6394
   + /CalendarServer/branches/config-separation/txdav/carddav/datastore/index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/carddav/datastore/index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/carddav/datastore/index_file.py:6167-6191
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/carddav/datastore/index_file.py:5936-5981
/CalendarServer/branches/new-store-no-caldavfile/txdav/carddav/datastore/index_file.py:5911-5935
/CalendarServer/branches/new-store/txdav/carddav/datastore/index_file.py:5594-5934
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/carddav/datastore/index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/carddav/datastore/index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/carddav/datastore/index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/carddav/datastore/index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/carddav/datastore/index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/carddav/datastore/index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/carddav/datastore/index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/carddav/datastore/index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/carddav/datastore/index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/carddav/datastore/index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/carddav/datastore/index_file.py:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/carddav/datastore/index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/carddav/datastore/index_file.py:6369
/CalendarServer/branches/users/glyph/oracle/txdav/carddav/datastore/index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/carddav/datastore/index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/carddav/datastore/index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/carddav/datastore/index_file.py:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/carddav/datastore/index_file.py:5084-5149
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/carddav/datastore/index_file.py:5052-5061
/CalendarServer/branches/users/sagen/locations-resources/txdav/carddav/datastore/index_file.py:5032-5051
/CalendarServer/branches/users/sagen/purge_old_events/txdav/carddav/datastore/index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/carddav/datastore/index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/carddav/datastore/index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/carddav/datastore/index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/carddav/datastore/index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/vcardindex.py:6322-6394


Property changes on: CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation/txdav/carddav/datastore/test/test_index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/carddav/datastore/test/test_index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/carddav/datastore/test/test_index_file.py:6167-6191
/CalendarServer/branches/new-store/txdav/carddav/datastore/test/test_index_file.py:5594-5934
/CalendarServer/branches/new-store-no-caldavfile/txdav/carddav/datastore/test/test_index_file.py:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/carddav/datastore/test/test_index_file.py:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/carddav/datastore/test/test_index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/carddav/datastore/test/test_index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/carddav/datastore/test/test_index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/carddav/datastore/test/test_index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/carddav/datastore/test/test_index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/carddav/datastore/test/test_index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/carddav/datastore/test/test_index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/carddav/datastore/test/test_index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/test/test_index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/carddav/datastore/test/test_index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/carddav/datastore/test/test_index_file.py:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/carddav/datastore/test/test_index_file.py:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/carddav/datastore/test/test_index_file.py:6369-6445
/CalendarServer/branches/users/glyph/oracle/txdav/carddav/datastore/test/test_index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/carddav/datastore/test/test_index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/carddav/datastore/test/test_index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/carddav/datastore/test/test_index_file.py:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/carddav/datastore/test/test_index_file.py:5084-5149
/CalendarServer/branches/users/sagen/locations-resources/txdav/carddav/datastore/test/test_index_file.py:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/carddav/datastore/test/test_index_file.py:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events/txdav/carddav/datastore/test/test_index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/carddav/datastore/test/test_index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/carddav/datastore/test/test_index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/carddav/datastore/test/test_index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/carddav/datastore/test/test_index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/test/test_vcardindex.py:6322-6394
   + /CalendarServer/branches/config-separation/txdav/carddav/datastore/test/test_index_file.py:4379-4443
/CalendarServer/branches/egg-info-351/txdav/carddav/datastore/test/test_index_file.py:4589-4625
/CalendarServer/branches/generic-sqlstore/txdav/carddav/datastore/test/test_index_file.py:6167-6191
/CalendarServer/branches/new-store-no-caldavfile-2/txdav/carddav/datastore/test/test_index_file.py:5936-5981
/CalendarServer/branches/new-store-no-caldavfile/txdav/carddav/datastore/test/test_index_file.py:5911-5935
/CalendarServer/branches/new-store/txdav/carddav/datastore/test/test_index_file.py:5594-5934
/CalendarServer/branches/users/cdaboo/batchupload-6699/txdav/carddav/datastore/test/test_index_file.py:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692/txdav/carddav/datastore/test/test_index_file.py:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627/txdav/carddav/datastore/test/test_index_file.py:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591/txdav/carddav/datastore/test/test_index_file.py:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464/txdav/carddav/datastore/test/test_index_file.py:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070/txdav/carddav/datastore/test/test_index_file.py:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187/txdav/carddav/datastore/test/test_index_file.py:5188-5440
/CalendarServer/branches/users/glyph/conn-limit/txdav/carddav/datastore/test/test_index_file.py:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge/txdav/carddav/datastore/test/test_index_file.py:4971-5080
/CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/test/test_index_file.py:6932-7023
/CalendarServer/branches/users/glyph/dont-start-postgres/txdav/carddav/datastore/test/test_index_file.py:6592-6614
/CalendarServer/branches/users/glyph/linux-tests/txdav/carddav/datastore/test/test_index_file.py:6893-6900
/CalendarServer/branches/users/glyph/more-deferreds-6/txdav/carddav/datastore/test/test_index_file.py:6322-6334
/CalendarServer/branches/users/glyph/more-deferreds-7/txdav/carddav/datastore/test/test_index_file.py:6369
/CalendarServer/branches/users/glyph/oracle/txdav/carddav/datastore/test/test_index_file.py:7106-7155
/CalendarServer/branches/users/glyph/sendfdport/txdav/carddav/datastore/test/test_index_file.py:5388-5424
/CalendarServer/branches/users/glyph/sharedpool/txdav/carddav/datastore/test/test_index_file.py:6490-6550
/CalendarServer/branches/users/glyph/sql-store/txdav/carddav/datastore/test/test_index_file.py:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted/txdav/carddav/datastore/test/test_index_file.py:5084-5149
/CalendarServer/branches/users/sagen/locations-resources-2/txdav/carddav/datastore/test/test_index_file.py:5052-5061
/CalendarServer/branches/users/sagen/locations-resources/txdav/carddav/datastore/test/test_index_file.py:5032-5051
/CalendarServer/branches/users/sagen/purge_old_events/txdav/carddav/datastore/test/test_index_file.py:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038/txdav/carddav/datastore/test/test_index_file.py:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066/txdav/carddav/datastore/test/test_index_file.py:4068-4075
/CalendarServer/branches/users/sagen/resources-2/txdav/carddav/datastore/test/test_index_file.py:5084-5093
/CalendarServer/branches/users/wsanchez/transations/txdav/carddav/datastore/test/test_index_file.py:5515-5593
/CalendarServer/trunk/twistedcaldav/test/test_vcardindex.py:6322-6394
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110316/360ad2da/attachment-0001.html>


More information about the calendarserver-changes mailing list