[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