[CalendarServer-changes] [10090] CalendarServer/branches/users/cdaboo/managed-attachments
source_changes at macosforge.org
source_changes at macosforge.org
Tue Nov 27 13:53:29 PST 2012
Revision: 10090
http://trac.calendarserver.org//changeset/10090
Author: cdaboo at apple.com
Date: 2012-11-27 13:53:28 -0800 (Tue, 27 Nov 2012)
Log Message:
-----------
Support managed attachments during PUT update.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py
CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py 2012-11-27 21:50:42 UTC (rev 10089)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py 2012-11-27 21:53:28 UTC (rev 10090)
@@ -42,6 +42,7 @@
from twext.python.log import Logger
from twext.web2.dav.http import ErrorResponse
+from txdav.caldav.icalendarstore import AttachmentStoreValidManagedID
from txdav.common.icommondatastore import ReservationError
from twistedcaldav.config import config
@@ -51,7 +52,7 @@
from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
from twistedcaldav.ical import Component, Property
-from twistedcaldav.instance import TooManyInstancesError,\
+from twistedcaldav.instance import TooManyInstancesError, \
InvalidOverriddenInstanceError
from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
from twistedcaldav.scheduling.implicit import ImplicitScheduler
@@ -61,7 +62,7 @@
class StoreCalendarObjectResource(object):
class UIDReservation(object):
-
+
def __init__(self, index, uid, uri, internal_request, transaction):
if internal_request:
self.lock = None
@@ -77,10 +78,10 @@
self.uid = uid
self.uri = uri
self.transaction = transaction
-
+
@inlineCallbacks
def reserve(self):
-
+
# Implicit lock
if self.lock:
try:
@@ -99,19 +100,19 @@
except ReservationError:
self.reserved = False
failure_count += 1
-
+
pause = Deferred()
def _timedDeferred():
pause.callback(True)
reactor.callLater(0.5, _timedDeferred)
yield pause
-
+
if self.uri and not self.reserved:
if self.lock:
# Can release immediately as nothing happened
yield self.lock.release()
raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use in calendar." % (self.uri,)))
-
+
@inlineCallbacks
def unreserve(self):
if self.reserved:
@@ -122,6 +123,7 @@
self.transaction.postCommit(self.lock.clean)
self.transaction.postAbort(self.lock.clean)
+
def __init__(
self,
request,
@@ -136,7 +138,7 @@
):
"""
Function that does common PUT/COPY/MOVE behavior.
-
+
@param request: the L{twext.web2.server.Request} for the current HTTP request.
@param source: the L{CalDAVResource} for the source resource to copy from, or None if source data
is to be read from the request.
@@ -155,7 +157,7 @@
@param processing_organizer: True if implicit processing for an organizer, False if for an attendee, None if not implicit processing.
@param returnData: True if the caller wants the actual data written to the store returned
"""
-
+
# Check that all arguments are valid
try:
assert destination is not None and destinationparent is not None and destination_uri is not None
@@ -199,6 +201,7 @@
self.hasPrivateComments = False
self.isScheduleResource = False
+
@inlineCallbacks
def fullValidation(self):
"""
@@ -262,7 +265,7 @@
(caldav_namespace, "supported-calendar-data"),
"Invalid content-type for data",
))
-
+
# At this point we need the calendar data to do more tests
try:
self.calendar = (yield self.source.iCalendarForUser(self.request))
@@ -400,7 +403,7 @@
msg = "Calendar-to-calendar %s with different homes are not supported" % ("moves" if self.deletesource else "copies",)
log.debug(msg)
raise HTTPError(StatusResponse(responsecode.FORBIDDEN, msg))
-
+
# Calendar to calendar moves where Organizer is present are not OK if the owners are different.
sourceowner = (yield self.sourceparent.ownerPrincipal(self.request))
destowner = (yield self.destinationparent.ownerPrincipal(self.request))
@@ -411,7 +414,7 @@
if organizer is None and self.destination.exists() and self.destinationcal:
oldCal = yield self.destination.iCalendar()
organizer = oldCal.getOrganizer()
-
+
if organizer is not None:
msg = "Calendar-to-calendar %s with an organizer property present and different owners are not supported" % ("moves" if self.deletesource else "copies",)
log.debug(msg)
@@ -448,6 +451,7 @@
# 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):
"""
Make sure that the resource name for the new resource is valid.
@@ -460,7 +464,8 @@
message = "File name %s not allowed in calendar collection" % (filename,)
return result, message
-
+
+
def validContentType(self):
"""
Make sure that the content-type of the source resource is text/calendar.
@@ -474,7 +479,8 @@
message = "MIME type %s not allowed in calendar collection" % (content_type,)
return result, message
-
+
+
def validContentLength(self):
"""
Make sure that the length of the source data is within bounds.
@@ -488,7 +494,8 @@
message = "File size %d bytes is larger than allowed limit %d bytes" % (calsize, config.MaxResourceSize)
return result, message
-
+
+
@inlineCallbacks
def validCollectionSize(self):
"""
@@ -503,7 +510,8 @@
message = "Too many resources in collection %s" % (self.destinationparent,)
returnValue((result, message,))
-
+
+
def validCalendarDataCheck(self):
"""
Check that the calendar data is valid iCalendar.
@@ -521,9 +529,10 @@
except ValueError, e:
result = False
message = "Invalid calendar data: %s" % (e,)
-
+
return result, message
-
+
+
def validCalDAVDataCheck(self):
"""
Check that the calendar data is valid as a CalDAV calendar object resource.
@@ -537,22 +546,24 @@
except ValueError, e:
result = False
message = "Calendar data does not conform to CalDAV requirements: %s" % (e,)
-
+
return result, message
-
+
+
def validComponentType(self):
"""
Make sure that any limits on the number of resources in a collection are enforced.
"""
result = True
message = ""
-
+
if not self.destinationparent.isSupportedComponent(self.calendar.mainType()):
result = False
message = "Invalid component type %s for calendar: %s" % (self.calendar.mainType(), self.destinationparent,)
return result, message
-
+
+
def validSizeCheck(self):
"""
Make sure that the content-type of the source resource is text/calendar.
@@ -569,12 +580,13 @@
return result, message
+
@inlineCallbacks
def validAttendeeListSizeCheck(self):
"""
Make sure that the Attendee list length is within bounds. We don't do this check for inbox because we
will assume that the limit has been applied on the PUT causing the iTIP message to be created.
-
+
FIXME: The inbox check might not take into account iSchedule stuff from outside. That needs to have
the max attendees check applied at the time of delivery.
"""
@@ -586,7 +598,7 @@
uniqueAttendees.add(attendee.value())
attendeeListLength = len(uniqueAttendees)
if attendeeListLength > config.MaxAttendeesPerInstance:
-
+
# Check to see whether we are increasing the count on an existing resource
if self.destination.exists() and self.destinationcal:
oldcalendar = (yield self.destination.iCalendarForUser(self.request))
@@ -596,20 +608,21 @@
oldAttendeeListLength = len(uniqueAttendees)
else:
oldAttendeeListLength = 0
-
+
if attendeeListLength > oldAttendeeListLength:
result = False
message = "Attendee list size %d is larger than allowed limit %d" % (attendeeListLength, config.MaxAttendeesPerInstance)
returnValue((result, message,))
+
def validAccess(self):
"""
Make sure that the X-CALENDARSERVER-ACCESS property is properly dealt with.
"""
-
+
if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
-
+
# Must be a value we know about
self.access = self.calendar.accessLevel(default=None)
if self.access is None:
@@ -618,11 +631,11 @@
(calendarserver_namespace, "valid-access-restriction"),
"Private event access level not allowed",
))
-
+
# Only DAV:owner is able to set the property to other than PUBLIC
if not self.internal_request:
def _callback(parent_owner):
-
+
authz = self.destinationparent.currentPrincipal(self.request)
if davxml.Principal(parent_owner) != authz and self.access != Component.ACCESS_PUBLIC:
raise HTTPError(ErrorResponse(
@@ -630,9 +643,9 @@
(calendarserver_namespace, "valid-access-restriction-change"),
"Private event access level change not allowed",
))
-
+
return None
-
+
d = self.destinationparent.owner(self.request)
d.addCallback(_callback)
return d
@@ -641,7 +654,7 @@
if not self.source and self.destination.exists() and self.destination.accessMode:
old_access = self.destination.accessMode
self.calendar.addProperty(Property(name=Component.ACCESS_PROPERTY, value=old_access))
-
+
return succeed(None)
@@ -659,7 +672,7 @@
"X-CALENDARSERVER-PRIVATE-COMMENT",
"X-CALENDARSERVER-ATTENDEE-COMMENT",
))
-
+
if old_has_private_comments and not self.hasPrivateComments:
# Transfer old comments to new calendar
log.debug("Private Comments properties were entirely removed by the client. Restoring existing properties.")
@@ -684,18 +697,18 @@
old_organizer = old_calendar.getOrganizerProperty()
new_attendees = self.calendar.getAttendees()
old_attendees = tuple(old_calendar.getAllAttendeeProperties())
-
+
new_completed = self.calendar.mainComponent().hasProperty("COMPLETED")
old_completed = old_calendar.mainComponent().hasProperty("COMPLETED")
-
+
if old_organizer and not new_organizer and len(old_attendees) > 0 and len(new_attendees) == 0:
# Transfer old organizer and attendees to new calendar
log.debug("Organizer and attendee properties were entirely removed by the client. Restoring existing properties.")
-
+
# Get the originator who is the owner of the calendar resource being modified
originatorPrincipal = (yield self.destination.ownerPrincipal(self.request))
originatorAddresses = originatorPrincipal.calendarUserAddresses()
-
+
for component in self.calendar.subcomponents():
if component.name() != "VTODO":
continue
@@ -708,15 +721,15 @@
if old_component.hasProperty("DTSTART"):
component.addProperty(old_component.getProperty("DTSTART"))
break
-
+
# Add organizer back in from previous resource
component.addProperty(old_organizer)
-
+
# Add attendees back in from previous resource
for anAttendee in old_attendees:
if component.hasProperty("COMPLETED") and anAttendee.value() in originatorAddresses:
anAttendee.setParameter("PARTSTAT", "COMPLETED")
- component.addProperty(anAttendee)
+ component.addProperty(anAttendee)
elif new_completed ^ old_completed and not self.internal_request:
# COMPLETED changed - sync up attendee state
@@ -727,15 +740,15 @@
# Transfer old organizer and attendees to new calendar
log.debug("Sync COMPLETED property change.")
-
+
# Get the originator who is the owner of the calendar resource being modified
originatorPrincipal = (yield self.destination.ownerPrincipal(self.request))
originatorAddresses = originatorPrincipal.calendarUserAddresses()
-
+
for component in self.calendar.subcomponents():
if component.name() != "VTODO":
continue
-
+
# Change owner partstat
for anAttendee in component.properties("ATTENDEE"):
if anAttendee.value() in originatorAddresses:
@@ -743,18 +756,18 @@
newpartstat = "COMPLETED" if component.hasProperty("COMPLETED") else "IN-PROCESS"
if newpartstat != oldpartstat:
anAttendee.setParameter("PARTSTAT", newpartstat)
-
+
@inlineCallbacks
def dropboxPathNormalization(self):
"""
Make sure sharees only use dropbox paths of the sharer.
"""
-
+
# Only relevant if calendar is sharee collection
changed = False
if self.destinationparent.isShareeCollection():
-
+
# Get all X-APPLE-DROPBOX's and ATTACH's that are http URIs
xdropboxes = self.calendar.getAllPropertiesInAnyComponent(
"X-APPLE-DROPBOX",
@@ -770,7 +783,7 @@
]
if len(xdropboxes) or len(attachments):
-
+
# Determine owner GUID
ownerPrincipal = (yield self.destinationparent.ownerPrincipal(self.request))
owner = ownerPrincipal.principalURL().split("/")[-2]
@@ -802,13 +815,14 @@
if uri:
attachment.setValue(uri)
changed = True
-
+
returnValue(changed)
+
def processAlarms(self):
"""
Remove duplicate alarms. Add a default alarm if required.
-
+
@return: indicate whether a change was made
@rtype: C{bool}
"""
@@ -825,7 +839,7 @@
# Check that we are creating and this is not the inbox
if not self.destinationcal or self.destination.exists() or self.isiTIP:
return changed
-
+
# Never add default alarms to calendar data in shared calendars
if self.destinationparent.isShareeCollection():
return changed
@@ -835,60 +849,61 @@
if self.calendar.mainType().upper() not in ("VEVENT", "VTODO"):
return changed
vevent = mtype == "VEVENT"
-
+
# Check timed or all-day
start, _ignore_end = self.calendar.mainComponent(allow_multiple=True).getEffectiveStartEnd()
if start is None:
# Yes VTODOs might have no DTSTART or DUE - in this case we do not add a default
return changed
timed = not start.isDateOnly()
-
+
# See if default exists and add using appropriate logic
alarm = self.destinationparent.getDefaultAlarm(vevent, timed)
if alarm:
changed = self.calendar.addAlarms(alarm)
return changed
+
@inlineCallbacks
- def noUIDConflict(self, uid):
- """
- Check that the UID of the new calendar object conforms to the requirements of
- CalDAV, i.e. it must be unique in the collection and we must not overwrite a
- different UID.
- @param uid: the UID for the resource being stored.
- @return: tuple: (True/False if the UID is valid, log message string,
- name of conflicted resource).
- """
-
- result = True
- message = ""
- rname = ""
+ def noUIDConflict(self, uid):
+ """
+ Check that the UID of the new calendar object conforms to the requirements of
+ CalDAV, i.e. it must be unique in the collection and we must not overwrite a
+ different UID.
+ @param uid: the UID for the resource being stored.
+ @return: tuple: (True/False if the UID is valid, log message string,
+ name of conflicted resource).
+ """
- # Adjust for a move into same calendar collection
- oldname = None
- if self.sourceparent and (self.sourceparent == self.destinationparent) and self.deletesource:
- oldname = self.source.name()
-
- # UID must be unique
- index = self.destinationparent.index()
- if not (yield index.isAllowedUID(uid, oldname, self.destination.name())):
- rname = yield index.resourceNameForUID(uid)
- # This can happen if two simultaneous PUTs occur with the same UID.
- # i.e. one PUT has reserved the UID but has not yet written the resource,
- # the other PUT tries to reserve and fails but no index entry exists yet.
- if rname is None:
- rname = "<<Unknown Resource>>"
- result = False
- message = "Calendar resource %s already exists with same UID %s" % (rname, uid)
- else:
- # Cannot overwrite a resource with different UID
- if self.destination.exists():
- olduid = yield index.resourceUIDForName(self.destination.name())
- if olduid != uid:
- rname = self.destination.name()
- result = False
- message = "Cannot overwrite calendar resource %s with different UID %s" % (rname, olduid)
-
+ result = True
+ message = ""
+ rname = ""
+
+ # Adjust for a move into same calendar collection
+ oldname = None
+ if self.sourceparent and (self.sourceparent == self.destinationparent) and self.deletesource:
+ oldname = self.source.name()
+
+ # UID must be unique
+ index = self.destinationparent.index()
+ if not (yield index.isAllowedUID(uid, oldname, self.destination.name())):
+ rname = yield index.resourceNameForUID(uid)
+ # This can happen if two simultaneous PUTs occur with the same UID.
+ # i.e. one PUT has reserved the UID but has not yet written the resource,
+ # the other PUT tries to reserve and fails but no index entry exists yet.
+ if rname is None:
+ rname = "<<Unknown Resource>>"
+ result = False
+ message = "Calendar resource %s already exists with same UID %s" % (rname, uid)
+ else:
+ # Cannot overwrite a resource with different UID
+ if self.destination.exists():
+ olduid = yield index.resourceUIDForName(self.destination.name())
+ if olduid != uid:
+ rname = self.destination.name()
+ result = False
+ message = "Cannot overwrite calendar resource %s with different UID %s" % (rname, olduid)
+
returnValue((result, message, rname))
@@ -901,7 +916,7 @@
# Do scheduling
if not self.isiTIP:
scheduler = ImplicitScheduler()
-
+
# Determine type of operation PUT, COPY or DELETE
if not self.source:
# PUT
@@ -938,7 +953,7 @@
self.calendar,
internal_request=self.internal_request,
))
-
+
if do_implicit_action and self.allowImplicitSchedule:
# Cannot do implicit in sharee's shared calendar
@@ -960,7 +975,7 @@
did_implicit_action = True
else:
is_scheduling_resource = False
-
+
returnValue((is_scheduling_resource, data_changed, did_implicit_action,))
@@ -1041,7 +1056,7 @@
# store as the store will "commit" the new value.
if self.access:
self.destination.accessMode = self.access
-
+
# Do not remove the property if access was not specified and we are storing in a calendar.
# This ensure that clients that do not preserve the iCalendar property do not cause access
# restrictions to be lost.
@@ -1076,7 +1091,6 @@
if change_scheduletag or not self.destination.scheduleTag:
self.destination.scheduleTag = str(uuid.uuid4())
-
# Handle weak etag compatibility
if config.Scheduling.CalDAV.ScheduleTagCompatibility:
if change_scheduletag:
@@ -1091,12 +1105,11 @@
etags += (hashlib.md5(data + (self.destination.scheduleTag if self.destination.scheduleTag else "")).hexdigest(),)
self.destination.scheduleEtags = etags
else:
- self.destination.scheduleEtags = ()
+ self.destination.scheduleEtags = ()
else:
self.destination.scheduleTag = ""
- self.destination.scheduleEtags = ()
+ self.destination.scheduleEtags = ()
-
if componentToStore is None:
stream = MemoryStream(data)
response = yield self.destination.storeStream(stream)
@@ -1107,10 +1120,11 @@
if self.isScheduleResource:
# Add a response header
- response.headers.setHeader("Schedule-Tag", self.destination.scheduleTag)
+ response.headers.setHeader("Schedule-Tag", self.destination.scheduleTag)
returnValue(response)
+
@inlineCallbacks
def doSourceDelete(self):
# Delete the source resource
@@ -1118,6 +1132,7 @@
log.debug("Source removed %s" % (self.source,))
returnValue(None)
+
@inlineCallbacks
def run(self):
"""
@@ -1142,11 +1157,11 @@
self.destination._associatedTransaction,
)
yield reservation.reserve()
- # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
- # try to write the same calendar data to two different resource URIs.
- if not self.isiTIP:
- result, message, rname = yield self.noUIDConflict(self.uid)
- if not result:
+ # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
+ # try to write the same calendar data to two different resource URIs.
+ if not self.isiTIP:
+ result, message, rname = yield self.noUIDConflict(self.uid)
+ if not result:
log.err(message)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
@@ -1161,10 +1176,9 @@
"UID already exists",
))
-
# Preserve private comments
yield self.preservePrivateComments()
-
+
# Fix broken VTODOs
yield self.replaceMissingToDoProperties()
@@ -1180,13 +1194,13 @@
if implicit_result == ImplicitScheduler.STATUS_ORPHANED_CANCELLED_EVENT:
if reservation:
yield reservation.unreserve()
-
+
returnValue(StatusResponse(responsecode.CREATED, "Resource created but immediately deleted by the server."))
elif implicit_result == ImplicitScheduler.STATUS_ORPHANED_EVENT:
if reservation:
yield reservation.unreserve()
-
+
# Now forcibly delete the event
if self.destination.exists():
yield self.destination.storeRemove(self.request, False, self.destination_uri)
@@ -1226,14 +1240,14 @@
if reservation:
yield reservation.unreserve()
-
+
returnValue(response)
-
+
except Exception, err:
if reservation:
yield reservation.unreserve()
-
+
if isinstance(err, InvalidOverriddenInstanceError):
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
@@ -1246,5 +1260,11 @@
MaxInstances.fromString(str(err.max_allowed)),
"Too many recurrence instances",
))
+ elif isinstance(err, AttachmentStoreValidManagedID):
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "valid-managed-id"),
+ "Invalid Managed-ID parameter in calendar data",
+ ))
else:
raise err
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py 2012-11-27 21:50:42 UTC (rev 10089)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py 2012-11-27 21:53:28 UTC (rev 10090)
@@ -782,6 +782,8 @@
self.scheduleEtags = metadata.get("scheduleEtags", "")
self.hasPrivateComment = metadata.get("hasPrivateComment", False)
+ self._cachedComponent = None
+
_allColumns = [
_objectSchema.RESOURCE_ID,
_objectSchema.RESOURCE_NAME,
@@ -831,17 +833,19 @@
@inlineCallbacks
- def setComponent(self, component, inserting=False):
+ def setComponent(self, component, inserting=False, attachmentsAlreadyProcessed=False):
validateCalendarComponent(self, self._calendar, component, inserting, self._txn._migrating)
- if inserting:
- changes = (yield self.creatingResourceCheckAttachments(component))
+ if not attachmentsAlreadyProcessed:
+ yield self._preProcessAttachmentsOnResourceChange(component, inserting)
yield self.updateDatabase(component, inserting=inserting)
+
+ if not attachmentsAlreadyProcessed:
+ yield self._postProcessAttachmentsOnResourceChange()
+
if inserting:
- if changes:
- yield self.createdResourceAttachments(changes)
yield self._calendar._insertRevision(self._name)
else:
yield self._calendar._updateRevision(self._name)
@@ -956,6 +960,7 @@
if not reCreate:
componentText = str(component)
self._objectText = componentText
+ self._cachedComponent = None
organizer = component.getOrganizer()
if not organizer:
organizer = ""
@@ -1134,31 +1139,35 @@
ideal but in theory we should have checked everything on the way in and
only allowed in good data.
"""
- text = yield self._text()
- try:
- component = VComponent.fromString(text)
- except InvalidICalendarDataError, e:
- # This is a really bad situation, so do raise
- raise InternalDataStoreError(
- "Data corruption detected (%s) in id: %s"
- % (e, self._resourceID)
- )
+ if self._cachedComponent is None:
+ text = yield self._text()
- # Fix any bogus data we can
- fixed, unfixed = component.validCalendarData(doFix=True, doRaise=False)
+ try:
+ component = VComponent.fromString(text)
+ except InvalidICalendarDataError, e:
+ # This is a really bad situation, so do raise
+ raise InternalDataStoreError(
+ "Data corruption detected (%s) in id: %s"
+ % (e, self._resourceID)
+ )
- if unfixed:
- self.log_error("Calendar data id=%s had unfixable problems:\n %s" %
- (self._resourceID, "\n ".join(unfixed),))
+ # Fix any bogus data we can
+ fixed, unfixed = component.validCalendarData(doFix=True, doRaise=False)
- if fixed:
- self.log_error("Calendar data id=%s had fixable problems:\n %s" %
- (self._resourceID, "\n ".join(fixed),))
+ if unfixed:
+ self.log_error("Calendar data id=%s had unfixable problems:\n %s" %
+ (self._resourceID, "\n ".join(unfixed),))
- returnValue(component)
+ if fixed:
+ self.log_error("Calendar data id=%s had fixable problems:\n %s" %
+ (self._resourceID, "\n ".join(fixed),))
+ self._cachedComponent = component
+ returnValue(self._cachedComponent)
+
+
@inlineCallbacks
def remove(self):
# Need to also remove attachments
@@ -1295,6 +1304,34 @@
hasPrivateComment = property(_get_hasPrivateComment, _set_hasPrivateComment)
@inlineCallbacks
+ def _preProcessAttachmentsOnResourceChange(self, component, inserting):
+ """
+ When component data is created or changed we need to look for changes related to managed attachments.
+
+ @param component: the new calendar data
+ @type component: L{Component}
+ @param inserting: C{True} if resource is being created
+ @type inserting: C{bool}
+ """
+ if inserting:
+ self._copyAttachments = (yield self.creatingResourceCheckAttachments(component))
+ self._removeAttachments = None
+ else:
+ self._copyAttachments, self._removeAttachments = (yield self.updatingResourceCheckAttachments(component))
+
+
+ @inlineCallbacks
+ def _postProcessAttachmentsOnResourceChange(self):
+ """
+ When component data has been created or changed we need to update managed attachment references to the resource.
+ """
+ if self._copyAttachments:
+ yield self.copyResourceAttachments(self._copyAttachments)
+ if self._removeAttachments:
+ yield self.removeResourceAttachments(self._removeAttachments)
+
+
+ @inlineCallbacks
def creatingResourceCheckAttachments(self, component):
"""
A new component is going to be stored. Check any ATTACH properties that may be present
@@ -1321,6 +1358,67 @@
@inlineCallbacks
+ def updatingResourceCheckAttachments(self, component):
+ """
+ A component is being changed. Check any ATTACH properties that may be present
+ to verify they owned by the organizer/owner of the resource and re-write the managed-ids.
+
+ @param component: calendar component about to be stored
+ @type component: L{Component}
+ """
+
+ # Retrieve all ATTACH properties with a MANAGED-ID in new data
+ newattached = collections.defaultdict(list)
+ newattachments = component.getAllPropertiesInAnyComponent("ATTACH", depth=1,)
+ for attachment in newattachments:
+ managed_id = attachment.parameterValue("MANAGED-ID")
+ if managed_id is not None:
+ newattached[managed_id].append(attachment)
+
+ # Retrieve all ATTACH properties with a MANAGED-ID in old data
+ oldcomponent = (yield self.component())
+ oldattached = collections.defaultdict(list)
+ oldattachments = oldcomponent.getAllPropertiesInAnyComponent("ATTACH", depth=1,)
+ for attachment in oldattachments:
+ managed_id = attachment.parameterValue("MANAGED-ID")
+ if managed_id is not None:
+ oldattached[managed_id].append(attachment)
+
+ # Punt if no managed attachments
+ if len(newattached) + len(oldattached) == 0:
+ returnValue((None, None,))
+
+ newattached_keys = set(newattached.keys())
+ oldattached_keys = set(oldattached.keys())
+
+ # Determine what was removed
+ removed = set(oldattached_keys) - set(newattached_keys)
+
+ # Determine what was added
+ added = set(newattached_keys) - set(oldattached_keys)
+ changed = {}
+ for managed_id in added:
+ changed[managed_id] = newattached[managed_id]
+
+ changes = yield self._addingManagedIDs(changed)
+
+ # Make sure existing data is not changed
+ same = oldattached_keys & newattached_keys
+ for managed_id in same:
+ newattachment = newattached[managed_id]
+ oldattachment = oldattached[managed_id][0]
+ for newattachment in newattached[managed_id]:
+ if newattachment != oldattachment:
+ newattachment.setParameter("MTAG", oldattachment.parameterValue("MTAG"))
+ newattachment.setParameter("FMTTYPE", oldattachment.parameterValue("FMTTYPE"))
+ newattachment.setParameter("FILENAME", oldattachment.parameterValue("FILENAME"))
+ newattachment.setParameter("SIZE", oldattachment.parameterValue("SIZE"))
+ newattachment.setValue(oldattachment.value())
+
+ returnValue((changes, removed,))
+
+
+ @inlineCallbacks
def _addingManagedIDs(self, attached):
# Now check each managed-id
changes = []
@@ -1328,6 +1426,8 @@
# Must be in the same home as this resource
hids = (yield ManagedAttachment.homeForManagedID(self._txn, managed_id))
+ if len(hids) == 0:
+ raise AttachmentStoreValidManagedID
if len(hids) != 1:
# This is a bad store error - there should be only one home associated with a managed-id
raise InternalDataStoreError
@@ -1354,13 +1454,30 @@
@inlineCallbacks
- def createdResourceAttachments(self, attached):
- # Now update the managed id references for each new one
+ def copyResourceAttachments(self, attached):
+ """
+ Copy an attachment reference for some other resource and link it to this resource.
+
+ @param attached: tuple of old, new managed ids for the attachments to copy
+ @type attached: C{tuple}
+ """
for old_id, new_id in attached:
- new_id = (yield ManagedAttachment.copyManagedID(self._txn, old_id, new_id, self._resourceID))
+ yield ManagedAttachment.copyManagedID(self._txn, old_id, new_id, self._resourceID)
@inlineCallbacks
+ def removeResourceAttachments(self, attached):
+ """
+ Remove an attachment reference for this resource.
+
+ @param attached: managed-ids to remove
+ @type attached: C{tuple}
+ """
+ for managed_id in attached:
+ yield self.removeManagedAttachmentWithID(managed_id)
+
+
+ @inlineCallbacks
def addAttachment(self, rids, content_type, filename, stream):
# First write the data stream
@@ -1397,7 +1514,7 @@
pass
# Store the data
- yield self.setComponent(calendar)
+ yield self.setComponent(calendar, attachmentsAlreadyProcessed=True)
returnValue((attachment, location,))
@@ -1448,7 +1565,7 @@
calendar.replaceAllPropertiesWithParameterMatch(attach, "MANAGED-ID", managed_id)
# Store the data
- yield self.setComponent(calendar)
+ yield self.setComponent(calendar, attachmentsAlreadyProcessed=True)
returnValue((attachment, location,))
@@ -1472,7 +1589,7 @@
pass
# Store the data
- yield self.setComponent(calendar)
+ yield self.setComponent(calendar, attachmentsAlreadyProcessed=True)
# Remove it - this will take care of actually removing it from the store if there are
# no more references to the attachment
@@ -2197,9 +2314,7 @@
attco.CALENDAR_OBJECT_RESOURCE_ID : referencedBy,
}).on(txn)
- returnValue(newManagedID)
-
def managedID(self):
return self._managedID
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121127/f10eccf3/attachment-0001.html>
More information about the calendarserver-changes
mailing list