[CalendarServer-changes] [5794] CalendarServer/branches/new-store

source_changes at macosforge.org source_changes at macosforge.org
Mon Jun 21 14:16:22 PDT 2010


Revision: 5794
          http://trac.macosforge.org/projects/calendarserver/changeset/5794
Author:   glyph at apple.com
Date:     2010-06-21 14:16:20 -0700 (Mon, 21 Jun 2010)
Log Message:
-----------
Move almost all calendar-related deletion operations into the back-end.

Modified Paths:
--------------
    CalendarServer/branches/new-store/twistedcaldav/method/delete_common.py
    CalendarServer/branches/new-store/twistedcaldav/method/put_common.py
    CalendarServer/branches/new-store/twistedcaldav/method/report_common.py
    CalendarServer/branches/new-store/twistedcaldav/resource.py
    CalendarServer/branches/new-store/twistedcaldav/scheduling/processing.py
    CalendarServer/branches/new-store/twistedcaldav/static.py
    CalendarServer/branches/new-store/twistedcaldav/storebridge.py
    CalendarServer/branches/new-store/txcaldav/calendarstore/file.py

Modified: CalendarServer/branches/new-store/twistedcaldav/method/delete_common.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/method/delete_common.py	2010-06-21 16:47:35 UTC (rev 5793)
+++ CalendarServer/branches/new-store/twistedcaldav/method/delete_common.py	2010-06-21 21:16:20 UTC (rev 5794)
@@ -29,15 +29,10 @@
 from twext.web2.http import HTTPError, StatusResponse
 
 from twext.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
 
-from twistedcaldav.caldavxml import caldav_namespace, ScheduleTag
-from twistedcaldav.config import config
-from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.method.report_common import applyToAddressBookCollections, applyToCalendarCollections
-from twistedcaldav.resource import isCalendarCollectionResource, \
-    isPseudoCalendarCollectionResource, isAddressBookCollectionResource
-from twistedcaldav.scheduling.implicit import ImplicitScheduler
+from twistedcaldav.memcachelock import MemcacheLockTimeoutError
+from twistedcaldav.method.report_common import applyToAddressBookCollections
+from twistedcaldav.resource import isAddressBookCollectionResource
 
 log = Logger()
 
@@ -54,238 +49,8 @@
         self.internal_request = internal_request
         self.allowImplicitSchedule = allowImplicitSchedule
 
-    def validIfScheduleMatch(self):
-        """
-        Check for If-ScheduleTag-Match header behavior.
-        """
 
-        # Only when a direct request
-        if not self.internal_request:
-            header = self.request.headers.getHeader("If-Schedule-Tag-Match")
-            if header:
-                # Do "precondition" test
-                matched = False
-                if self.resource.exists() and self.resource.hasDeadProperty(ScheduleTag):
-                    scheduletag = self.resource.readDeadProperty(ScheduleTag)
-                    matched = (scheduletag == header)
-                if not matched:
-                    log.debug("If-Schedule-Tag-Match: header value '%s' does not match resource value '%s'" % (header, scheduletag,))
-                    raise HTTPError(responsecode.PRECONDITION_FAILED)
-
-            elif config.Scheduling.CalDAV.ScheduleTagCompatibility:
-                # Actually by the time we get here the pre-condition will already have been tested and found to be OK
-                # (CalDAVFile.checkPreconditions) so we can ignore this case.
-                pass
-
     @inlineCallbacks
-    def deleteResource(self, delresource, deluri, parent):
-        """
-        Delete a plain resource which may be a collection - but only one not containing
-        calendar resources.
-
-        @param delresource:
-        @type delresource:
-        @param deluri:
-        @type deluri:
-        @param parent:
-        @type parent:
-        """
-
-        # Do quota checks before we start deleting things
-        myquota = (yield delresource.quota(self.request))
-        if myquota is not None:
-            old_size = (yield delresource.quotaSize(self.request))
-        else:
-            old_size = 0
-
-        # Do delete
-        response = (yield delete(deluri, delresource.fp, self.depth))
-
-        # Adjust quota
-        if myquota is not None:
-            yield delresource.quotaSizeAdjust(self.request, -old_size)
-
-        if response == responsecode.NO_CONTENT:
-            if isPseudoCalendarCollectionResource(parent):
-                newrevision = (yield parent.bumpSyncToken())
-                index = parent.index()
-                index.deleteResource(delresource.fp.basename(), newrevision)
-                
-        returnValue(response)
-
-    @inlineCallbacks
-    def deleteCalendarResource(self, delresource, deluri, parent):
-        """
-        Delete a single calendar resource and do implicit scheduling actions if required.
-
-        @param delresource:
-        @type delresource:
-        @param deluri:
-        @type deluri:
-        @param parent:
-        @type parent:
-        """
-
-        # TODO: need to use transaction based delete on live scheduling object resources
-        # as the iTIP operation may fail and may need to prevent the delete from happening.
-
-        # Do If-Schedule-Tag-Match behavior first
-        self.validIfScheduleMatch()
-
-        # Do quota checks before we start deleting things
-        myquota = (yield delresource.quota(self.request))
-        if myquota is not None:
-            old_size = (yield delresource.quotaSize(self.request))
-        else:
-            old_size = 0
-
-        scheduler = None
-        lock = None
-        if not self.internal_request and self.allowImplicitSchedule:
-            # Get data we need for implicit scheduling
-            calendar = (yield delresource.iCalendarForUser(self.request))
-            scheduler = ImplicitScheduler()
-            do_implicit_action, _ignore = (yield scheduler.testImplicitSchedulingDELETE(self.request, delresource, calendar))
-            if do_implicit_action:
-                lock = MemcacheLock("ImplicitUIDLock", calendar.resourceUID(), timeout=60.0)
-
-        try:
-            if lock:
-                yield lock.acquire()
-
-            # Do delete
-            response = (yield delete(deluri, delresource.fp, self.depth))
-
-            # Adjust quota
-            if myquota is not None:
-                yield delresource.quotaSizeAdjust(self.request, -old_size)
-
-            if response == responsecode.NO_CONTENT:
-                newrevision = (yield parent.bumpSyncToken())
-                index = parent.index()
-                index.deleteResource(delresource.fp.basename(), newrevision)
-
-                # Do scheduling
-                if scheduler and not self.internal_request and self.allowImplicitSchedule:
-                    yield scheduler.doImplicitScheduling()
-    
-        except MemcacheLockTimeoutError:
-            raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use on the server." % (deluri,)))
-
-        finally:
-            if lock:
-                yield lock.clean()
-
-        returnValue(response)
-
-    @inlineCallbacks
-    def deleteCalendar(self, delresource, deluri, parent):
-        """
-        Delete an entire calendar collection by deleting each child resource in turn to
-        ensure that proper implicit scheduling actions occur.
-        
-        This has to emulate the behavior in fileop.delete in that any errors need to be
-        reported back in a multistatus response.
-        """
-
-        # Not allowed to delete the default calendar
-        default = (yield delresource.isDefaultCalendar(self.request))
-        if default:
-            log.err("Cannot DELETE default calendar: %s" % (delresource,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "default-calendar-delete-allowed",)))
-
-        if self.depth != "infinity":
-            msg = "Client sent illegal depth header value for DELETE: %s" % (self.depth,)
-            log.err(msg)
-            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
-
-        # Check virtual share first
-        isVirtual = yield delresource.isVirtualShare(self.request)
-        if isVirtual:
-            log.debug("Removing shared calendar %s" % (delresource,))
-            yield delresource.removeVirtualShare(self.request)
-            returnValue(responsecode.NO_CONTENT)
-
-        log.debug("Deleting calendar %s" % (delresource.fp.path,))
-
-        errors = ResponseQueue(deluri, "DELETE", responsecode.NO_CONTENT)
-
-        for childname in delresource.listChildren():
-
-            childurl = joinURL(deluri, childname)
-            child = (yield self.request.locateChildResource(delresource, childname))
-
-            try:
-                yield self.deleteCalendarResource(child, childurl, delresource)
-            except:
-                log.err()
-                errors.add(childurl, responsecode.BAD_REQUEST)
-
-        # Now do normal delete
-
-        # Handle sharing
-        wasShared = (yield delresource.isShared(self.request))
-        if wasShared:
-            yield delresource.downgradeFromShare(self.request)
-
-        # Change CTag
-        yield delresource.bumpSyncToken()
-        more_responses = (yield self.deleteResource(delresource, deluri, parent))
-
-        if isinstance(more_responses, MultiStatusResponse):
-            # Merge errors
-            errors.responses.update(more_responses.children)
-
-        response = errors.response()
-
-        if response == responsecode.NO_CONTENT:
-            # Do some clean up
-            yield delresource.deletedCalendar(self.request)
-
-        returnValue(response)
-
-    @inlineCallbacks
-    def deleteCollection(self):
-        """
-        Delete a regular collection with special processing for any calendar collections
-        contained within it.
-        """
-        if self.depth != "infinity":
-            msg = "Client sent illegal depth header value for DELETE: %s" % (self.depth,)
-            log.err(msg)
-            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
-
-        log.debug("Deleting collection %s" % (self.resource.fp.path,))
-
-        errors = ResponseQueue(self.resource_uri, "DELETE", responsecode.NO_CONTENT)
-
-        @inlineCallbacks
-        def doDeleteCalendar(delresource, deluri):
-
-            delparent = (yield delresource.locateParent(self.request, deluri))
-
-            response = (yield self.deleteCalendar(delresource, deluri, delparent))
-
-            if isinstance(response, MultiStatusResponse):
-                # Merge errors
-                errors.responses.update(response.children)
-
-            returnValue(True)
-
-        yield applyToCalendarCollections(self.resource, self.request, self.resource_uri, self.depth, doDeleteCalendar, None)
-
-        # Now do normal delete
-        more_responses = (yield self.deleteResource(self.resource, self.resource_uri, self.parent))
-
-        if isinstance(more_responses, MultiStatusResponse):
-            # Merge errors
-            errors.responses.update(more_responses.children)
-
-        response = errors.response()
-
-        returnValue(response)
-
-    @inlineCallbacks
     def deleteAddressBookResource(self, delresource, deluri, parent):
         """
         Delete a single addressbook resource and do implicit scheduling actions if required.
@@ -427,23 +192,19 @@
 
     @inlineCallbacks
     def run(self):
-
-        if isCalendarCollectionResource(self.parent):
-            response = (yield self.deleteCalendarResource(self.resource, self.resource_uri, self.parent))
-
-        elif isCalendarCollectionResource(self.resource):
-            response = (yield self.deleteCalendar(self.resource, self.resource_uri, self.parent))
-
-        elif isAddressBookCollectionResource(self.parent):
+        if isAddressBookCollectionResource(self.parent):
             response = (yield self.deleteAddressBookResource(self.resource, self.resource_uri, self.parent))
-
         elif isAddressBookCollectionResource(self.resource):
             response = (yield self.deleteAddressBook(self.resource, self.resource_uri, self.parent))
-
-        elif self.resource.isCollection():
-            response = (yield self.deleteCollection())
-
         else:
-            response = (yield self.deleteResource(self.resource, self.resource_uri, self.parent))
+            # FIXME: this code-path shouldn't actually be used, as the things
+            # with storeRemove on them also have their own http_DELETEs.
+            response = (
+                yield self.resource.storeRemove(
+                    self.request, 
+                    not self.internal_request and self.allowImplicitSchedule,
+                    self.resource_uri
+                )
+            )
 
         returnValue(response)

Modified: CalendarServer/branches/new-store/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/method/put_common.py	2010-06-21 16:47:35 UTC (rev 5793)
+++ CalendarServer/branches/new-store/twistedcaldav/method/put_common.py	2010-06-21 21:16:20 UTC (rev 5794)
@@ -61,7 +61,6 @@
 from twistedcaldav.instance import TooManyInstancesError,\
     InvalidOverriddenInstanceError
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.method.delete_common import DeleteResource
 from twistedcaldav.scheduling.implicit import ImplicitScheduler
 
 log = Logger()
@@ -193,8 +192,8 @@
         self.processing_organizer = processing_organizer
 
         self.access = None
-        self.newrevision = None
 
+
     @inlineCallbacks
     def fullValidation(self):
         """
@@ -328,34 +327,30 @@
         """
         Check for If-ScheduleTag-Match header behavior.
         """
-        
         # Only when a direct request
         self.schedule_tag_match = False
         if not self.isiTIP and not self.internal_request:
             header = self.request.headers.getHeader("If-Schedule-Tag-Match")
             if header:
-                # Do "precondition" test
-                
-                # If COPY/MOVE get Schedule-Tag on source, else use destination
-                def _getScheduleTag(resource):
-                    return resource.readDeadProperty(ScheduleTag) if resource.exists() and resource.hasDeadProperty(ScheduleTag) else None
-
-                scheduletag = _getScheduleTag(self.source if self.source else self.destination)
-                if scheduletag != header:
-                    log.debug("If-Schedule-Tag-Match: header value '%s' does not match resource value '%s'" % (header, scheduletag,))
-                    raise HTTPError(responsecode.PRECONDITION_FAILED)
+                # If COPY/MOVE get Schedule-Tag on source, PUT use destination
+                if self.source:
+                    matcher = self.source
+                    self.source.validIfScheduleMatch(self.request)
+                else:
+                    matcher = self.destination
+                matcher.validIfScheduleMatch(self.request)
                 self.schedule_tag_match = True
-            
             elif config.Scheduling.CalDAV.ScheduleTagCompatibility:
                 # Compatibility with old clients. Policy:
                 #
                 # 1. If If-Match header is not present, never do smart merge.
-                # 2. If If-Match is present and the specified ETag is considered a "weak" match to the
-                #    current Schedule-Tag, then do smart merge, else reject with a 412.
+                # 2. If If-Match is present and the specified ETag is
+                #    considered a "weak" match to the current Schedule-Tag,
+                #    then do smart merge, else reject with a 412.
                 #
-                # Actually by the time we get here the pre-condition will already have been tested and found to be OK,
-                # so we can just always do smart merge now if If-Match is present.
-
+                # Actually by the time we get here the pre-condition will
+                # already have been tested and found to be OK, so we can just
+                # always do smart merge now if If-Match is present.
                 self.schedule_tag_match = self.request.headers.getHeader("If-Match") is not None
 
     def validResourceName(self):
@@ -713,13 +708,26 @@
         yield self.mergePerUserData()
 
         # Do put or copy based on whether source exists
-        if self.source is not None:
+        source = self.source
+        if source is not None:
+            # Retrieve information from the source, in case we have to delete
+            # it.
+            sourceProperties = dict(source.newStoreProperties().iteritems())
+            if not implicit:
+                # Only needed in implicit case; see below.
+                sourceText = source.iCalendarText()
+
+            # Delete the original source if needed (for example, if this is a
+            # same-calendar MOVE of a calendar object, implemented as an
+            # effective DELETE-then-PUT).
+            if self.deletesource:
+                yield self.doSourceDelete()
+
             if implicit:
                 response = (yield self.doStorePut())
-                self.source.copyDeadPropertiesTo(self.destination)
             else:
-                response = (yield self.destination.storeStream(MemoryStream(self.source.iCalendarText())))
-                self.source.copyDeadPropertiesTo(self.destination)
+                response = (yield self.destination.storeStream(MemoryStream(sourceText)))
+            self.destination.newStoreProperties().update(sourceProperties)
         else:
             response = (yield self.doStorePut())
     
@@ -752,16 +760,9 @@
 
     @inlineCallbacks
     def doSourceDelete(self):
-        # Delete index for original item
-        if self.sourcecal:
-            self.newrevision = (yield self.sourceparent.bumpSyncToken())
-            self.source_index.deleteResource(self.source.fp.basename(), self.newrevision)
-            log.debug("Source index removed %s" % (self.source.fp.path,))
-
         # Delete the source resource
-        self.source.storeRemove()
+        yield self.source.storeRemove(self.request, False, self.source_uri)
         log.debug("Source removed %s" % (self.source.fp.path,))
-  
         returnValue(None)
 
     @inlineCallbacks
@@ -787,63 +788,7 @@
 
         returnValue(None)
 
-    def doSourceIndexRecover(self):
-        """
-        Do source resource indexing. This only gets called when restoring
-        the source after its index has been deleted.
-        
-        @return: None if successful, ErrorResponse on failure
-        """
-        
-        # Add or update the index for this resource.
-        try:
-            self.source_index.addResource(self.source.fp.basename(), self.calendar, self.newrevision)
-        except TooManyInstancesError, ex:
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                    NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
-                ))
-            return None
 
-    def doDestinationIndex(self):
-        """
-        Do destination resource indexing, replacing any index previous stored.
-        
-        @return: None if successful, ErrorResponse on failure
-        """
-        # Add or update the index for this resource.
-        caltoindex = self.calendar
-        try:
-            self.destination_index.addResource(self.destination.fp.basename(), caltoindex, self.newrevision)
-            log.debug("Destination indexed %s" % (self.destination.fp.path,))
-        except TooManyInstancesError, ex:
-            log.err("Cannot index calendar resource as there are too many recurrence instances %s" % self.destination)
-            raise HTTPError(ErrorResponse(
-                responsecode.FORBIDDEN,
-                NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
-            ))
-        except (ValueError, TypeError), ex:
-            msg = "Cannot index calendar resource: %s" % (ex,)
-            log.err(msg)
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description=msg))
-
-        content_type = self.request.headers.getHeader("content-type")
-        if not self.internal_request and content_type is not None:
-            self.destination.writeDeadProperty(davxml.GETContentType.fromString(generateContentType(content_type)))
-        else:
-            self.destination.writeDeadProperty(davxml.GETContentType.fromString(generateContentType(MimeType("text", "calendar", params={"charset":"utf-8"}))))
-        return None
-
-    def doRemoveDestinationIndex(self):
-        """
-        Remove any existing destination index.
-        """
-        
-        # Delete index for original item
-        if self.destinationcal:
-            self.destination_index.deleteResource(self.destination.fp.basename(), None)
-            log.debug("Destination index removed %s" % (self.destination.fp.path,))
-
     @inlineCallbacks
     def run(self):
         """
@@ -854,7 +799,7 @@
 
         try:
             reservation = None
-            
+
             # Handle all validation operations here.
             yield self.fullValidation()
 
@@ -872,15 +817,16 @@
                 if not self.isiTIP: 
                     result, message, rname = self.noUIDConflict(self.uid) 
                     if not result: 
-                        log.err(message) 
-                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, 
-                            NoUIDConflict(davxml.HRef.fromString(joinURL(parentForURL(self.destination_uri), rname.encode("utf-8")))) 
-                        ))
+                        log.err(message)
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN,
+                            NoUIDConflict(davxml.HRef.fromString(
+                                joinURL(parentForURL(self.destination_uri),
+                                        rname.encode("utf-8"))))))
 
 
             # Get current quota state.
             yield self.checkQuota()
-    
+
             # Handle RRULE truncation
             rruleChanged = self.truncateRecurrence()
 
@@ -902,8 +848,7 @@
             
                     # Now forcibly delete the event
                     if self.destination.exists():
-                        deleter = DeleteResource(self.request, self.destination, self.destination_uri, self.destinationparent, "0", internal_request=True)
-                        yield deleter.run()
+                        yield self.destination.storeRemove(self.request, False, self.destination_uri)
                     else:
                         msg = "Attendee cannot create event for Organizer: %s" % (implicit_result,)
                         log.err(msg)
@@ -918,23 +863,9 @@
             else:
                 is_scheduling_resource, data_changed, did_implicit_action = implicit_result
 
-            """
-            Handle actual store operations here.
-            
-            The order in which this is done is import:
-                
-                1. Do store operation for new data
-                2. Delete source and source index if needed
-                3. Do new indexing if needed
-                
-            Note that we need to remove the source index BEFORE doing the destination index to cover the
-            case of a resource being 'renamed', i.e. moved within the same collection. Since the index UID
-            column must be unique in SQL, we cannot add the new index before remove the old one.
-            """
-    
             # Do the actual put or copy
             response = (yield self.doStore(data_changed))
-            
+
             # Must not set ETag in response if data changed
             if did_implicit_action or rruleChanged:
                 def _removeEtag(request, response):
@@ -1001,19 +932,16 @@
                 elif not self.destinationcal:
                     self.destination.removeDeadProperty(TwistedCalendarHasPrivateCommentsProperty)                
 
-            # Delete the original source if needed.
-            if self.deletesource:
-                yield self.doSourceDelete()
-    
-            # Index the new resource if storing to a calendar.
+            # Remember the resource's content-type.
             if self.destinationcal:
-                self.newrevision = (yield self.destinationparent.bumpSyncToken())
-                result = self.doDestinationIndex()
-                if result is not None:
-                    # FIXME: transaction needs to be rolled back; should we have
-                    # ErrorResponse detection in renderHTTP?  Hmm. -glyph
-                    returnValue(result)
-    
+                content_type = self.request.headers.getHeader("content-type")
+                if self.internal_request or content_type is None:
+                    content_type = MimeType("text", "calendar",
+                                            params={"charset":"utf-8"})
+                self.destination.writeDeadProperty(
+                    davxml.GETContentType.fromString(generateContentType(content_type))
+                )
+
             # Delete the original source if needed.
             if self.deletesource:
                 yield self.doSourceQuotaCheck()

Modified: CalendarServer/branches/new-store/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/method/report_common.py	2010-06-21 16:47:35 UTC (rev 5793)
+++ CalendarServer/branches/new-store/twistedcaldav/method/report_common.py	2010-06-21 21:16:20 UTC (rev 5794)
@@ -557,10 +557,6 @@
     
                 if calendar.mainType() == "VEVENT":
                     processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
-                    
-                    # Lets also force an index rebuild for this resource so that next time we have the fbtype set
-                    calresource.index().addResource(name, calendar, None, reCreate=True)
-
                 elif calendar.mainType() == "VFREEBUSY":
                     processFreeBusyFreeBusy(calendar, fbinfo, timerange)
                 elif calendar.mainType() == "VAVAILABILITY":

Modified: CalendarServer/branches/new-store/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/resource.py	2010-06-21 16:47:35 UTC (rev 5793)
+++ CalendarServer/branches/new-store/twistedcaldav/resource.py	2010-06-21 21:16:20 UTC (rev 5794)
@@ -209,8 +209,12 @@
                 else:
                     self._associatedTransaction.commit()
             return result
+        def failed(failure):
+            print 'renderHTTP failed!  FIXME PLEASE: handle errors here!'
+            failure.printTraceback()
+            return failure
         # FIXME: needs a failure handler (that rolls back the transaction)
-        return d.addCallback(succeeded)
+        return d.addCallback(succeeded).addErrback(failed)
 
     # Begin transitional new-store resource interface:
 
@@ -234,7 +238,7 @@
                                   (self,))
         
     
-    def storeRemove(self):
+    def storeRemove(self, *a, **kw):
         """
         Remove this resource from storage.
         """

Modified: CalendarServer/branches/new-store/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/scheduling/processing.py	2010-06-21 16:47:35 UTC (rev 5793)
+++ CalendarServer/branches/new-store/twistedcaldav/scheduling/processing.py	2010-06-21 21:16:20 UTC (rev 5794)
@@ -659,14 +659,11 @@
         @param name: the resource name to write into, or {None} to write a new resource.
         @type name: C{str}
         """
-        
-        from twistedcaldav.method.delete_common import DeleteResource
         delchild = collection.getChild(name)
         childURL = joinURL(collURL, name)
         self.request._rememberResource(delchild, childURL)
+        yield delchild.storeRemove(self.request, False, childURL)
 
-        deleter = DeleteResource(self.request, delchild, childURL, collection, "0", internal_request=True)
-        yield deleter.run()
 
     def changeAttendeePartstat(self, attendees, partstat):
         """

Modified: CalendarServer/branches/new-store/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/static.py	2010-06-21 16:47:35 UTC (rev 5793)
+++ CalendarServer/branches/new-store/twistedcaldav/static.py	2010-06-21 21:16:20 UTC (rev 5794)
@@ -1272,17 +1272,6 @@
         ScheduleFile.__init__(self, path, parent)
         ScheduleInboxResource.__init__(self, parent)
 
-    def provision(self):
-        if self.provisionFile():
-
-            # Initialize CTag on the calendar collection
-            self.bumpSyncToken()
-
-            # Initialize the index
-            self.index().create()
-
-        return super(ScheduleInboxFile, self).provision()
-
     def __repr__(self):
         return "<%s (calendar inbox collection): %s>" % (self.__class__.__name__, self.fp.path)
 
@@ -1491,8 +1480,6 @@
         return succeed(True)
 
     def _deleteNotification(self, request, rname):
-        
-        # TODO: use the generic DeleteResource api so that quota, sync-token etc all get changed properly
         childfp = self.fp.child(rname)
         return delete("", childfp)
 

Modified: CalendarServer/branches/new-store/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/storebridge.py	2010-06-21 16:47:35 UTC (rev 5793)
+++ CalendarServer/branches/new-store/twistedcaldav/storebridge.py	2010-06-21 21:16:20 UTC (rev 5794)
@@ -30,10 +30,13 @@
 from twext.python import vcomponent
 
 from twext.web2.http_headers import ETag
-from twext.web2.responsecode import FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, \
-    CONFLICT
+from twext.web2.dav.http import ErrorResponse, ResponseQueue
+from twext.web2.responsecode import (
+    FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
+    BAD_REQUEST)
+from twext.python.log import Logger
 from twext.web2.dav.resource import TwistedGETContentMD5
-from twext.web2.dav.util import parentForURL, allDataFromStream
+from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL
 from twext.web2.http import HTTPError, StatusResponse
 
 from twistedcaldav.static import CalDAVFile, ScheduleInboxFile
@@ -43,7 +46,15 @@
 from txcarddav.iaddressbookstore import NoSuchAddressBookObjectError
 from twistedcaldav.vcard import Component as VCard
 
+from twistedcaldav.caldavxml import ScheduleTag, caldav_namespace
+from twistedcaldav.scheduling.implicit import ImplicitScheduler
+from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 
+from twisted.python.log import err as logDefaultException
+
+log = Logger()
+
+
 class _NewStorePropertiesWrapper(object):
     """
     Wrap a new-style property store (a L{txdav.idav.IPropertyStore}) in the old-
@@ -227,6 +238,123 @@
         self._initializeWithCalendar(calendar, home)
 
 
+    def isCalendarCollection(self):
+        """
+        Yes, it is a calendar collection.
+        """
+        return True
+
+
+    def http_DELETE(self, request):
+        """
+        Override http_DELETE to validate 'depth' header. 
+        """
+        depth = request.headers.getHeader("depth", "infinity")
+        if depth != "infinity":
+            msg = "illegal depth header for DELETE on collection: %s" % (
+                depth,
+            )
+            log.err(msg)
+            raise HTTPError(StatusResponse(BAD_REQUEST, msg))
+        return self.storeRemove(request, True, request.uri)
+
+
+    @inlineCallbacks
+    def storeRemove(self, request, implicitly, where):
+        """
+        Delete this calendar collection resource, first deleting each contained
+        calendar resource.
+
+        This has to emulate the behavior in fileop.delete in that any errors
+        need to be reported back in a multistatus response.
+
+        @param request: The request used to locate child resources.  Note that
+            this is the request which I{triggered} the C{DELETE}, but which may
+            not actually be a C{DELETE} request itself.
+
+        @type request: L{twext.web2.iweb.IRequest}
+
+        @param implicitly: Should implicit scheduling operations be triggered
+            as a resut of this C{DELETE}?
+
+        @type implicitly: C{bool}
+
+        @param where: the URI at which the resource is being deleted.
+        @type where: C{str}
+
+        @return: an HTTP response suitable for sending to a client (or
+            including in a multi-status).
+
+         @rtype: something adaptable to L{twext.web2.iweb.IResponse}
+        """
+
+        # Not allowed to delete the default calendar
+        default = (yield self.isDefaultCalendar(request))
+        if default:
+            log.err("Cannot DELETE default calendar: %s" % (self,))
+            raise HTTPError(ErrorResponse(FORBIDDEN,
+                            (caldav_namespace,
+                             "default-calendar-delete-allowed",)))
+
+        # Is this a sharee's view of a shared calendar?  If so, they can't do
+        # scheduling onto it, so just delete it and move on.
+        isVirtual = yield self.isVirtualShare(request)
+        if isVirtual:
+            log.debug("Removing shared calendar %s" % (self,))
+            yield self.removeVirtualShare(request)
+            returnValue(NO_CONTENT)
+
+        log.debug("Deleting calendar %s" % (self,))
+
+        # 'deluri' is this resource's URI; I should be able to synthesize it
+        # from 'self'.
+
+        errors = ResponseQueue(where, "DELETE", NO_CONTENT)
+
+        for childname in self.listChildren():
+
+            childurl = joinURL(where, childname)
+
+            # FIXME: use a more specific API; we should know what this child
+            # resource is, and not have to look it up.  (Sharing information
+            # needs to move into the back-end first, though.)
+            child = (yield request.locateChildResource(self, childname))
+
+            try:
+                yield child.storeRemove(request, implicitly, childurl)
+            except:
+                logDefaultException()
+                errors.add(childurl, BAD_REQUEST)
+
+        # Now do normal delete
+
+        # Handle sharing
+        wasShared = (yield self.isShared(request))
+        if wasShared:
+            yield self.downgradeFromShare(request)
+
+        # Actually delete it.
+        self._newStoreParentHome.removeCalendarWithName(
+            self._newStoreCalendar.name()
+        )
+        self.__class__ = ProtoCalendarCollectionFile
+        del self._newStoreCalendar
+
+        # FIXME: handle exceptions, possibly like this:
+
+        #        if isinstance(more_responses, MultiStatusResponse):
+        #            # Merge errors
+        #            errors.responses.update(more_responses.children)
+
+        response = errors.response()
+
+        if response == NO_CONTENT:
+            # Do some clean up
+            yield self.deletedCalendar(request)
+
+        returnValue(response)
+
+
     def isCollection(self):
         return True
 
@@ -368,16 +496,18 @@
     def isCollection(self):
         return False
 
+
     def inNewTransaction(self, request):
         """
-        Implicit auto-replies need to span multiple transactions.  Clean out the
-        given request's resource-lookup mapping, transaction, and re-look-up my
-        calendar object in a new transaction.
+        Implicit auto-replies need to span multiple transactions.  Clean out
+        the given request's resource-lookup mapping, transaction, and re-look-
+        up my calendar object in a new transaction.
 
         Return the new transaction so it can be committed.
         """
-        # FIXME: private names from 'file' implementation; maybe there should be
-        # a public way to do this?  or maybe we should just have a real queue.
+        # FIXME: private names from 'file' implementation; maybe there should
+        # be a public way to do this?  or maybe we should just have a real
+        # queue.
         objectName = self._newStoreObject.name()
         calendarName = self._newStoreObject._calendar.name()
         homeUID = self._newStoreObject._calendar._calendarHome.uid()
@@ -443,18 +573,124 @@
         returnValue(NO_CONTENT)
 
 
-    def storeRemove(self):
+    def validIfScheduleMatch(self, request):
         """
-        Remove this calendar object.
+        Check to see if the given request's C{If-Schedule-Tag-Match} header
+        matches this resource's schedule tag.
+
+        @raise HTTPError: if the tag does not match.
+
+        @return: None
         """
-        # FIXME: public attribute please
-        self._newStoreObject._calendar.removeCalendarObjectWithName(self._newStoreObject.name())
-        # FIXME: clean this up with a 'transform' method
-        self._newStoreParentCalendar = self._newStoreObject._calendar
-        del self._newStoreObject
-        self.__class__ = ProtoCalendarObjectFile
+        # Note, internal requests shouldn't issue this.
+        header = request.headers.getHeader("If-Schedule-Tag-Match")
+        if header:
+            # Do "precondition" test
+            matched = False
+            if self.hasDeadProperty(ScheduleTag):
+                scheduletag = self.readDeadProperty(ScheduleTag)
+                matched = (scheduletag == header)
+            if not matched:
+                log.debug(
+                    "If-Schedule-Tag-Match: header value '%s' does not match resource value '%s'" %
+                    (header, scheduletag,))
+                raise HTTPError(PRECONDITION_FAILED)
 
 
+    @inlineCallbacks
+    def storeRemove(self, request, implicitly, where):
+        """
+        Delete this calendar object and do implicit scheduling actions if
+        required.
+
+        @param request: Unused by this implementation; present for signature
+            compatibility with L{CalendarCollectionFile.storeRemove}.
+
+        @type request: L{twext.web2.iweb.IRequest}
+
+        @param implicitly: Should implicit scheduling operations be triggered
+            as a resut of this C{DELETE}?
+
+        @type implicitly: C{bool}
+
+        @param where: the URI at which the resource is being deleted.
+        @type where: C{str}
+
+        @return: an HTTP response suitable for sending to a client (or
+            including in a multi-status).
+
+         @rtype: something adaptable to L{twext.web2.iweb.IResponse}
+        """
+
+        # TODO: need to use transaction based delete on live scheduling object
+        # resources as the iTIP operation may fail and may need to prevent the
+        # delete from happening.
+
+        # Do If-Schedule-Tag-Match behavior first
+        self.validIfScheduleMatch(request)
+
+        # Do quota checks before we start deleting things
+        myquota = (yield self.quota(request))
+        if myquota is not None:
+            old_size = (yield self.quotaSize(request))
+        else:
+            old_size = 0
+
+        scheduler = None
+        lock = None
+        if implicitly:
+            # Get data we need for implicit scheduling
+            calendar = (yield self.iCalendarForUser(request))
+            scheduler = ImplicitScheduler()
+            do_implicit_action, _ignore = (
+                yield scheduler.testImplicitSchedulingDELETE(
+                    request, self, calendar
+                )
+            )
+            if do_implicit_action:
+                lock = MemcacheLock(
+                    "ImplicitUIDLock", calendar.resourceUID(), timeout=60.0
+                )
+
+        try:
+            if lock:
+                yield lock.acquire()
+
+            storeCalendar = self._newStoreObject._calendar
+            # Do delete
+
+            # FIXME: public attribute please.  Should ICalendar maybe just have
+            # a delete() method?
+            storeCalendar.removeCalendarObjectWithName(
+                self._newStoreObject.name()
+            )
+
+            # FIXME: clean this up with a 'transform' method
+            self._newStoreParentCalendar = storeCalendar
+            del self._newStoreObject
+            self.__class__ = ProtoCalendarObjectFile
+
+            # Adjust quota
+            if myquota is not None:
+                yield self.quotaSizeAdjust(request, -old_size)
+
+            # Do scheduling
+            if implicitly:
+                yield scheduler.doImplicitScheduling()
+
+        except MemcacheLockTimeoutError:
+            raise HTTPError(StatusResponse(
+                CONFLICT,
+                "Resource: %s currently in use on the server." % (where,))
+            )
+
+        finally:
+            if lock:
+                yield lock.clean()
+
+        returnValue(NO_CONTENT)
+
+
     def _initializeWithObject(self, calendarObject):
         self._newStoreObject = calendarObject
         self._dead_properties = _NewStorePropertiesWrapper(

Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/file.py	2010-06-21 16:47:35 UTC (rev 5793)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/file.py	2010-06-21 21:16:20 UTC (rev 5794)
@@ -314,6 +314,7 @@
         self._transaction = transaction
         self._newCalendars = {}
         self._removedCalendars = set()
+        self._cachedCalendars = {}
 
 
     def __repr__(self):
@@ -342,13 +343,17 @@
             return calendar
         if name in self._removedCalendars:
             return None
+        if name in self._cachedCalendars:
+            return self._cachedCalendars[name]
 
         if name.startswith("."):
             return None
 
         childPath = self._path.child(name)
         if childPath.isdir():
-            return Calendar(name, self)
+            existingCalendar = Calendar(name, self)
+            self._cachedCalendars[name] = existingCalendar
+            return existingCalendar
         else:
             return None
 
@@ -403,6 +408,7 @@
         # c.properties().participateInTxn(txn)
         # FIXME: return c # maybe ?
 
+
     @_writeOperation
     def removeCalendarWithName(self, name):
         if name.startswith(".") or name in self._removedCalendars:
@@ -434,12 +440,16 @@
                 except Exception, e:
                     self.log_error("Unable to delete trashed calendar at %s: %s" % (trash.fp, e))
 
-            transaction.addOperation(cleanup, "remove calendar %r" % (name,))
+            transaction.addOperation(cleanup, "remove calendar backup %r" % (name,))
 
             def undo():
                 trash.moveTo(childPath)
 
             return undo
+        # FIXME: direct tests
+        self._transaction.addOperation(
+            do, "prepare calendar remove %r" % (name,)
+        )
 
 
     # @_cached
@@ -512,6 +522,7 @@
 
     @_writeOperation
     def rename(self, name):
+        self._updateSyncToken()
         oldName = self.name()
         self._renamedName = name
         self._calendarHome._newCalendars[name] = self
@@ -586,6 +597,8 @@
 
     @_writeOperation
     def removeCalendarObjectWithName(self, name):
+        newRevision = self._updateSyncToken() # FIXME: Test
+        self.retrieveOldIndex().deleteResource(name, newRevision)
         if name.startswith("."):
             raise NoSuchCalendarObjectError(name)
 
@@ -608,10 +621,6 @@
             self.calendarObjectWithUID(uid)._path.basename())
 
 
-    def syncToken(self):
-        raise NotImplementedError()
-
-
     def _updateSyncToken(self, reset=False):
         # FIXME: add locking a-la CalDAVFile.bumpSyncToken
         # FIXME: tests for desired concurrency properties
@@ -622,6 +631,8 @@
             caluuid = uuid4()
             revision = 1
         else:
+            # FIXME: no direct tests for update
+            token = str(token)
             caluuid, revision = token.split("#", 1)
             revision = int(revision) + 1
         token = "%s#%d" % (caluuid, revision)
@@ -643,10 +654,11 @@
         # FIXME: needs direct tests - only covered by calendar store tests
         # FIXME: transactions
         props = PropertyStore(self._path)
-        self._transaction.addOperation(props.flush, "flush calendar properties")
+        self._transaction.addOperation(props.flush,
+                                       "flush calendar properties")
         return props
-    
-    
+
+
     def _doValidate(self, component):
         # FIXME: should be separate class, not separate case!
         if self.name() == 'inbox':
@@ -686,6 +698,12 @@
 
     @_writeOperation
     def setComponent(self, component):
+
+        newRevision = self._calendar._updateSyncToken() # FIXME: test
+        self._calendar.retrieveOldIndex().addResource(
+            self.name(), component, newRevision
+        )
+
         if not isinstance(component, VComponent):
             raise TypeError(type(component))
 
@@ -726,11 +744,13 @@
                     self._path.remove()
             return undo
         self._transaction.addOperation(do, "set calendar component %r" % (self.name(),))
+
         # Mark all properties as dirty, so they will be re-added to the
         # temporary file when the main file is deleted. NOTE: if there were a
         # temporary file and a rename() as there should be, this should really
         # happen after the write but before the rename.
         self.properties().update(self.properties())
+
         # FIXME: the property store's flush() method may already have been
         # added to the transaction, but we need to add it again to make sure it
         # happens _after_ the new file has been written.  we may end up doing
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100621/2ea62f61/attachment-0001.html>


More information about the calendarserver-changes mailing list