[CalendarServer-changes] [10122] CalendarServer/branches/users/cdaboo/managed-attachments
source_changes at macosforge.org
source_changes at macosforge.org
Tue Dec 4 08:48:49 PST 2012
Revision: 10122
http://trac.calendarserver.org//changeset/10122
Author: cdaboo at apple.com
Date: 2012-12-04 08:48:48 -0800 (Tue, 04 Dec 2012)
Log Message:
-----------
Support managed attachments with sharing. Support MOVE with managed attachments.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_addressbook_common.py
CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py
CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py
CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_sql.py
CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py
CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/file.py
CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py
CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/test/util.py
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_addressbook_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_addressbook_common.py 2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_addressbook_common.py 2012-12-04 16:48:48 UTC (rev 10122)
@@ -45,18 +45,18 @@
log = Logger()
class StoreAddressObjectResource(object):
-
+
class UIDReservation(object):
-
+
def __init__(self, index, uid, uri):
self.reserved = False
self.index = index
self.uid = uid
self.uri = uri
-
+
@inlineCallbacks
def reserve(self):
-
+
# Lets use a deferred for this and loop a few times if we cannot reserve so that we give
# time to whoever has the reservation to finish and release it.
failure_count = 0
@@ -68,34 +68,35 @@
except ReservationError:
self.reserved = False
failure_count += 1
-
+
pause = Deferred()
def _timedDeferred():
pause.callback(True)
reactor.callLater(0.5, _timedDeferred) #@UndefinedVariable
yield pause
-
+
if self.uri and not self.reserved:
raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (self.uri,)))
-
+
@inlineCallbacks
def unreserve(self):
if self.reserved:
yield self.index.unreserveUID(self.uid)
self.reserved = False
+
def __init__(
self,
request,
source=None, source_uri=None, sourceparent=None, sourceadbk=False, deletesource=False,
destination=None, destination_uri=None, destinationparent=None, destinationadbk=True,
vcard=None,
- indexdestination = True,
+ indexdestination=True,
returnData=False,
):
"""
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.
@@ -110,7 +111,7 @@
@param deletesource: True if the source resource is to be deleted on successful completion, False otherwise.
@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
@@ -131,7 +132,7 @@
log.err("vcard=%s\n" % (vcard,))
log.err("deletesource=%s\n" % (deletesource,))
raise
-
+
self.request = request
self.sourceadbk = sourceadbk
self.destinationadbk = destinationadbk
@@ -146,7 +147,7 @@
self.deletesource = deletesource
self.indexdestination = indexdestination
self.returnData = returnData
-
+
self.access = None
@@ -161,7 +162,7 @@
result, message = self.validResourceName()
if not result:
log.err(message)
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
+ raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
# Valid collection size check on the destination parent resource
result, message = (yield self.validCollectionSize())
@@ -182,9 +183,9 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(carddav_namespace, "supported-address-data"),
- "Invalid content-type",
+ message,
))
-
+
# At this point we need the calendar data to do more tests
self.vcard = (yield self.source.vCard())
else:
@@ -198,7 +199,7 @@
(carddav_namespace, "valid-address-data"),
"Could not parse vCard",
))
-
+
# Valid vcard data check
result, message = self.validAddressDataCheck()
if not result:
@@ -206,9 +207,9 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(carddav_namespace, "valid-address-data"),
- description=message
+ message
))
-
+
# Valid vcard data for CalDAV check
result, message = self.validCardDAVDataCheck()
if not result:
@@ -216,7 +217,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(carddav_namespace, "valid-addressbook-object-resource"),
- "Invalid vCard data",
+ message,
))
# Must have a valid UID at this point
@@ -244,12 +245,13 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(carddav_namespace, "max-resource-size"),
- "Address data too large",
+ message,
))
# Check access
returnValue(None)
-
+
+
def validResourceName(self):
"""
Make sure that the resource name for the new resource is valid.
@@ -259,10 +261,11 @@
filename = self.destination.name()
if filename.startswith("."):
result = False
- message = "File name %s not allowed in vcard collection" % (filename,)
+ message = "Resource name %s not allowed in vcard collection" % (filename,)
return result, message
-
+
+
def validContentType(self):
"""
Make sure that the content-type of the source resource is text/vcard.
@@ -276,7 +279,8 @@
message = "MIME type %s not allowed in vcard collection" % (content_type,)
return result, message
-
+
+
@inlineCallbacks
def validCollectionSize(self):
"""
@@ -291,7 +295,8 @@
message = "Too many resources in collection %s" % (self.destinationparent,)
returnValue((result, message,))
-
+
+
def validAddressDataCheck(self):
"""
Check that the calendar data is valid iCalendar.
@@ -309,9 +314,10 @@
except ValueError, e:
result = False
message = "Invalid vcard data: %s" % (e,)
-
+
return result, message
-
+
+
def validCardDAVDataCheck(self):
"""
Check that the vcard data is valid vCard.
@@ -325,9 +331,10 @@
except ValueError, e:
result = False
message = "vCard data does not conform to CardDAV requirements: %s" % (e,)
-
+
return result, message
-
+
+
def validSizeCheck(self):
"""
Make sure that the content-type of the source resource is text/vcard.
@@ -373,7 +380,7 @@
# the other PUT tries to reserve and fails but no index entry exists yet.
if rname is None:
rname = "<<Unknown Resource>>"
-
+
result = False
message = "Address book resource %s already exists with same UID %s" % (rname, uid)
else:
@@ -384,7 +391,7 @@
rname = self.destination.name()
result = False
message = "Cannot overwrite vcard resource %s with different UID %s" % (rname, olduid)
-
+
returnValue((result, message, rname))
@@ -408,9 +415,10 @@
self.destination.newStoreProperties().update(sourceProperties)
else:
response = (yield self.doStorePut())
-
+
returnValue(response)
+
@inlineCallbacks
def doStorePut(self):
@@ -418,6 +426,7 @@
response = (yield self.destination.storeStream(stream))
returnValue(response)
+
@inlineCallbacks
def doSourceDelete(self):
# Delete the source resource
@@ -425,6 +434,7 @@
log.debug("Source removed %s" % (self.source,))
returnValue(None)
+
@inlineCallbacks
def run(self):
"""
@@ -435,12 +445,12 @@
try:
reservation = None
-
+
# Handle all validation operations here.
yield self.fullValidation()
# Reservation and UID conflict checking is next.
- if self.destinationadbk:
+ if self.destinationadbk:
# Reserve UID
self.destination_index = self.destinationparent.index()
reservation = StoreAddressObjectResource.UIDReservation(
@@ -448,7 +458,7 @@
)
if self.indexdestination:
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 vcard data to two different resource URIs.
result, message, rname = yield self.noUIDConflict(self.uid)
@@ -466,18 +476,109 @@
),
"UID already used in another resource",
))
-
+
# Do the actual put or copy
response = (yield self.doStore())
-
+
if reservation:
yield reservation.unreserve()
-
+
returnValue(response)
-
+
except Exception, err:
if reservation:
yield reservation.unreserve()
raise err
+
+
+ @inlineCallbacks
+ def moveValidation(self):
+ """
+ Do full validation of source and destination calendar data.
+ """
+
+ # Valid resource name check
+ result, message = self.validResourceName()
+ if not result:
+ log.err(message)
+ raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
+
+ # Valid collection size check on the destination parent resource
+ result, message = (yield self.validCollectionSize())
+ if not result:
+ log.err(message)
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ customxml.MaxResources(),
+ message,
+ ))
+
+ returnValue(None)
+
+
+ @inlineCallbacks
+ def doStoreMove(self):
+
+ # Do move
+ response = (yield self.source.storeMove(self.request, self.destinationparent, self.destination._name))
+ returnValue(response)
+
+
+ @inlineCallbacks
+ def move(self):
+ """
+ Function that does common MOVE behavior.
+
+ @return: a Deferred with a status response result.
+ """
+
+ try:
+ reservation = None
+
+ # Handle all validation operations here.
+ yield self.moveValidation()
+
+ # Reservation and UID conflict checking is next.
+
+ # Reserve UID
+ self.destination_index = self.destinationparent.index()
+ reservation = StoreAddressObjectResource.UIDReservation(
+ self.destination_index, self.source.uid(), self.destination_uri
+ )
+ if self.indexdestination:
+ 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 vcard data to two different resource URIs.
+ result, message, rname = yield self.noUIDConflict(self.source.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")
+ )
+ )
+ ),
+ "UID already used in another resource",
+ ))
+
+ # Do the actual put or copy
+ response = (yield self.doStoreMove())
+
+ if reservation:
+ yield reservation.unreserve()
+
+ returnValue(response)
+
+ except Exception, err:
+
+ if reservation:
+ yield reservation.unreserve()
+
+ raise err
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py 2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py 2012-12-04 16:48:48 UTC (rev 10122)
@@ -220,7 +220,7 @@
result, message = self.validResourceName()
if not result:
log.err(message)
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
+ raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
# Valid collection size check on the destination parent resource
result, message = (yield self.validCollectionSize())
@@ -229,7 +229,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
customxml.MaxResources(),
- "Too many resources in collection",
+ message,
))
# Valid data sizes - do before parsing the data
@@ -241,7 +241,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "max-resource-size"),
- "Calendar data too large",
+ message,
))
else:
# Valid calendar data size check
@@ -251,7 +251,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "max-resource-size"),
- "Calendar data too large",
+ message,
))
if not self.sourcecal:
@@ -263,7 +263,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "supported-calendar-data"),
- "Invalid content-type for data",
+ message,
))
# At this point we need the calendar data to do more tests
@@ -274,7 +274,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-calendar-data"),
- description="Can't parse calendar data"
+ "Can't parse calendar data"
))
else:
try:
@@ -285,7 +285,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-calendar-data"),
- description="Can't parse calendar data"
+ "Can't parse calendar data"
))
# Possible timezone stripping
@@ -302,7 +302,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-calendar-data"),
- description=message
+ message
))
# Valid calendar data for CalDAV check
@@ -312,7 +312,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "valid-calendar-object-resource"),
- "Invalid calendar data",
+ message,
))
# Valid calendar component for check
@@ -322,7 +322,7 @@
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "supported-component"),
- "Invalid calendar data",
+ message,
))
# Valid attendee list size check
@@ -333,7 +333,7 @@
ErrorResponse(
responsecode.FORBIDDEN,
MaxAttendeesPerInstance.fromString(str(config.MaxAttendeesPerInstance)),
- "Too many attendees in calendar data",
+ message,
)
)
@@ -1269,3 +1269,121 @@
))
else:
raise err
+
+
+ @inlineCallbacks
+ def moveValidation(self):
+ """
+ Do full validation of source and destination calendar data.
+ """
+
+ # Basic validation
+ self.validIfScheduleMatch()
+
+ # Valid resource name check
+ result, message = self.validResourceName()
+ if not result:
+ log.err(message)
+ raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
+
+ # Valid collection size check on the destination parent resource
+ result, message = (yield self.validCollectionSize())
+ if not result:
+ log.err(message)
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ customxml.MaxResources(),
+ message,
+ ))
+
+ # Check that moves to shared calendars are OK
+ yield self.validCopyMoveOperation()
+
+ returnValue(None)
+
+
+ @inlineCallbacks
+ def doStoreMove(self):
+
+ # Do move
+ response = (yield self.source.storeMove(self.request, self.destinationparent, self.destination._name))
+ returnValue(response)
+
+
+ @inlineCallbacks
+ def move(self):
+ """
+ Function that does common MOVE behavior.
+
+ @return: a Deferred with a status response result.
+ """
+
+ try:
+ reservation = None
+
+ # Handle all validation operations here.
+ self.calendar = (yield self.source.iCalendarForUser(self.request))
+ yield self.moveValidation()
+
+ # Reservation and UID conflict checking is next.
+
+ # Reserve UID
+ self.destination_index = self.destinationparent.index()
+ reservation = StoreCalendarObjectResource.UIDReservation(
+ self.destination_index, self.source.uid(), self.destination_uri,
+ self.internal_request or self.isiTIP,
+ 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.source.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")
+ )
+ )
+ ),
+ "UID already exists",
+ ))
+
+ # Do the actual put or copy
+ response = (yield self.doStoreMove())
+
+ if reservation:
+ yield reservation.unreserve()
+
+ returnValue(response)
+
+ except Exception, err:
+
+ if reservation:
+ yield reservation.unreserve()
+
+ if isinstance(err, InvalidOverriddenInstanceError):
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "valid-calendar-data"),
+ description="Invalid overridden instance"
+ ))
+ elif isinstance(err, TooManyInstancesError):
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ 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/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py 2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py 2012-12-04 16:48:48 UTC (rev 10122)
@@ -41,12 +41,13 @@
from twext.web2.http_headers import ETag, MimeType, MimeDisposition
from twext.web2.dav.http import ErrorResponse, ResponseQueue, MultiStatusResponse
from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.dav.resource import TwistedACLInheritable, AccessDeniedError
+from twext.web2.dav.resource import TwistedACLInheritable, AccessDeniedError, \
+ davPrivilegeSet
from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
from twext.web2.responsecode import (
FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE, SERVICE_UNAVAILABLE
-)
+, INTERNAL_SERVER_ERROR)
from twistedcaldav import customxml, carddavxml, caldavxml
from twistedcaldav.cache import CacheStoreNotifier, ResponseCacheMixin, \
@@ -69,6 +70,7 @@
from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
from pycalendar.datetime import PyCalendarDateTime
import uuid
+from twext.web2.filter.location import addLocation
"""
Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
@@ -148,7 +150,7 @@
fromParent = kw.get('fromParent')
# FIXME: direct unit tests
def wrap(thunk):
- def authAndContinue(self, request):
+ def authAndContinue(self, request, *args, **kwargs):
if permissions:
d = self.authorize(request, permissions)
else:
@@ -161,7 +163,7 @@
lambda parent:
parent.authorize(request, fromParent)
)
- d.addCallback(lambda whatever: thunk(self, request))
+ d.addCallback(lambda whatever: thunk(self, request, *args, **kwargs))
return d
return authAndContinue
return wrap
@@ -1632,8 +1634,9 @@
def __init__(self, parent, *a, **kw):
kw.update(principalCollections=parent.principalCollections())
super(AttachmentsCollection, self).__init__(*a, **kw)
- self._newStoreHome = parent._newStoreHome
- parent.propagateTransaction(self)
+ self.parent = parent
+ self._newStoreHome = self.parent._newStoreHome
+ self.parent.propagateTransaction(self)
def isCollection(self):
@@ -1665,7 +1668,65 @@
return self._newStoreHome.getAllAttachmentNames()
+ def supportedPrivileges(self, request):
+ # Just DAV standard privileges - no CalDAV ones
+ return succeed(davPrivilegeSet)
+
+ def defaultAccessControlList(self):
+ """
+ Only read privileges allowed for managed attachments.
+ """
+ myPrincipal = self.parent.principalForRecord()
+
+ read_privs = (
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+ )
+
+ aces = (
+ # Inheritable access for the resource's associated principal.
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(myPrincipal.principalURL())),
+ davxml.Grant(*read_privs),
+ davxml.Protected(),
+ TwistedACLInheritable(),
+ ),
+ )
+
+ # Give read access to config.ReadPrincipals
+ aces += config.ReadACEs
+
+ # Give all access to config.AdminPrincipals
+ aces += config.AdminACEs
+
+ if config.EnableProxyPrincipals:
+ aces += (
+ # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-read/"))),
+ davxml.Grant(*read_privs),
+ davxml.Protected(),
+ TwistedACLInheritable(),
+ ),
+ # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-write users.
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write/"))),
+ davxml.Grant(*read_privs),
+ davxml.Protected(),
+ TwistedACLInheritable(),
+ ),
+ )
+
+ return davxml.ACL(*aces)
+
+
+ def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+ # Permissions here are fixed, and are not subject to inheritance rules, etc.
+ return succeed(self.defaultAccessControlList())
+
+
+
class CalendarAttachment(_NewStoreFileMetaDataHelper, _GetChildHelper):
def __init__(self, calendarObject, attachment, attachmentName, managed, **kw):
@@ -1779,7 +1840,106 @@
return False
+ def supportedPrivileges(self, request):
+ # Just DAV standard privileges - no CalDAV ones
+ return succeed(davPrivilegeSet)
+
+ @inlineCallbacks
+ def accessControlList(self, request, *a, **kw):
+ """
+ Special case managed attachments, but not dropbox (which is handled by parent collection).
+ All principals identified as ATTENDEEs on the event for this attachment
+ may read it. Also include proxies of ATTENDEEs. Ignore unknown attendees.
+ """
+
+ originalACL = yield super(CalendarAttachment, self).accessControlList(request, *a, **kw)
+ if not self._managed or not self.exists():
+ returnValue(originalACL)
+ originalACEs = list(originalACL.children)
+
+ # Look at attendees
+ if self._newStoreCalendarObject is None:
+ self._newStoreCalendarObject = (yield self._newStoreAttachment.objectResource())
+
+ cuas = (yield self._newStoreCalendarObject.component()).getAttendees()
+ newACEs = []
+ for calendarUserAddress in cuas:
+ principal = self.principalForCalendarUserAddress(
+ calendarUserAddress
+ )
+ if principal is None:
+ continue
+
+ principalURL = principal.principalURL()
+ privileges = (
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+ )
+ newACEs.append(davxml.ACE(
+ davxml.Principal(davxml.HRef(principalURL)),
+ davxml.Grant(*privileges),
+ davxml.Protected(),
+ ))
+ newACEs.append(davxml.ACE(
+ davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-write/"))),
+ davxml.Grant(*privileges),
+ davxml.Protected(),
+ ))
+ newACEs.append(davxml.ACE(
+ davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-read/"))),
+ davxml.Grant(*privileges),
+ davxml.Protected(),
+ ))
+
+ # Now also need sharees
+ newACEs.extend((yield self.sharedManagedACEs()))
+
+ returnValue(davxml.ACL(*tuple(originalACEs + newACEs)))
+
+
+ @inlineCallbacks
+ def sharedManagedACEs(self):
+
+ aces = ()
+ calendars = yield self._newStoreCalendarObject._parentCollection.asShared()
+ for calendar in calendars:
+
+ read_privs = (
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+ )
+
+ principal = self.principalForUID(calendar._home.uid())
+ aces += (
+ # Specific access for the resource's associated principal.
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(principal.principalURL())),
+ davxml.Grant(*read_privs),
+ davxml.Protected(),
+ ),
+ )
+
+ if config.EnableProxyPrincipals:
+ aces += (
+ # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-read/"))),
+ davxml.Grant(*read_privs),
+ davxml.Protected(),
+ ),
+ # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-write/"))),
+ davxml.Grant(*read_privs),
+ davxml.Protected(),
+ ),
+ )
+
+ returnValue(aces)
+
+
+
class NoParent(CalDAVResource):
def http_MKCALENDAR(self, request):
@@ -1828,6 +1988,10 @@
return succeed(self._newStoreObject.size())
+ def uid(self):
+ return self._newStoreObject.uid()
+
+
def component(self):
return self._newStoreObject.component()
@@ -1857,6 +2021,72 @@
return self.storeRemove(request, True, request.uri)
+ @inlineCallbacks
+ def http_MOVE(self, request):
+ """
+ MOVE for object resources.
+ """
+
+ # Do some pre-flight checks - must exist, must be move to another
+ # CommonHomeChild in the same Home, destination resource must not exist
+ if not self.exists():
+ log.debug("Resource not found: %s" % (self,))
+ raise HTTPError(NOT_FOUND)
+
+ parent = (yield request.locateResource(parentForURL(request.uri)))
+
+ #
+ # Find the destination resource
+ #
+ destination_uri = request.headers.getHeader("destination")
+ overwrite = request.headers.getHeader("overwrite", True)
+
+ if not destination_uri:
+ msg = "No destination header in MOVE request."
+ log.err(msg)
+ raise HTTPError(StatusResponse(BAD_REQUEST, msg))
+
+ destination = (yield request.locateResource(destination_uri))
+ if destination is None:
+ msg = "Destination of MOVE does not exist: %s" % (destination_uri,)
+ log.debug(msg)
+ raise HTTPError(StatusResponse(BAD_REQUEST, msg))
+ if destination.exists():
+ if overwrite:
+ msg = "Cannot overwrite existing resource with a MOVE"
+ log.debug(msg)
+ raise HTTPError(StatusResponse(FORBIDDEN, msg))
+ else:
+ msg = "Cannot MOVE to existing resource without overwrite flag enabled"
+ log.debug(msg)
+ raise HTTPError(StatusResponse(PRECONDITION_FAILED, msg))
+
+ # Check for parent calendar collection
+ destination_uri = urlsplit(destination_uri)[2]
+ destinationparent = (yield request.locateResource(parentForURL(destination_uri)))
+ if not isinstance(destinationparent, _CommonHomeChildCollectionMixin):
+ msg = "Destination of MOVE is not valid: %s" % (destination_uri,)
+ log.debug(msg)
+ raise HTTPError(StatusResponse(FORBIDDEN, msg))
+ if parentForURL(parentForURL(destination_uri)) != parentForURL(parentForURL(request.uri)):
+ msg = "Can only MOVE within the same home collection: %s" % (destination_uri,)
+ log.debug(msg)
+ raise HTTPError(StatusResponse(FORBIDDEN, msg))
+
+ #
+ # Check authentication and access controls
+ #
+ yield parent.authorize(request, (davxml.Unbind(),))
+ yield destinationparent.authorize(request, (davxml.Bind(),))
+
+ # May need to add a location header
+ addLocation(request, destination_uri)
+
+ storer = self.storeResource(request, parent, destination, destination_uri, destinationparent)
+ result = (yield storer.move())
+ returnValue(result)
+
+
def http_PROPPATCH(self, request):
"""
No dead properties allowed on object resources.
@@ -1867,6 +2097,13 @@
return FORBIDDEN
+ def storeResource(self, request, parent, destination, destination_uri, destaination_parent):
+ """
+ Create the appropriate StoreXXX class for storing of data.
+ """
+ raise NotImplementedError
+
+
@inlineCallbacks
def storeStream(self, stream):
@@ -1906,6 +2143,28 @@
@inlineCallbacks
+ def storeMove(self, request, destinationparent, destination_name):
+ """
+ Move this object to a different parent.
+
+ @param request:
+ @type request: L{twext.web2.iweb.IRequest}
+ @param destinationparent: Parent to move to
+ @type destinationparent: L{CommonHomeChild}
+ @param destination_name: name of new resource
+ @type destination_name: C{str}
+ """
+
+ try:
+ yield self._newStoreObject.moveTo(destinationparent._newStoreObject, destination_name)
+ except Exception, e:
+ log.err(e)
+ raise HTTPError(INTERNAL_SERVER_ERROR)
+
+ returnValue(CREATED)
+
+
+ @inlineCallbacks
def storeRemove(self, request, implicitly, where):
"""
Delete this object.
@@ -2042,6 +2301,21 @@
raise HTTPError(PRECONDITION_FAILED)
+ def storeResource(self, request, parent, destination, destination_uri, destaination_parent):
+ return StoreCalendarObjectResource(
+ request=request,
+ source=self,
+ source_uri=request.uri,
+ sourceparent=parent,
+ sourcecal=True,
+ deletesource=True,
+ destination=destination,
+ destination_uri=destination_uri,
+ destinationparent=destaination_parent,
+ destinationcal=True,
+ )
+
+
@inlineCallbacks
def storeRemove(self, request, implicitly, where):
"""
@@ -2125,6 +2399,7 @@
returnValue(NO_CONTENT)
+ @requiresPermissions(davxml.WriteContent())
@inlineCallbacks
def POST_handler_attachment(self, request, action):
"""
@@ -2450,7 +2725,22 @@
vCard = _CommonObjectResource.component
+ def storeResource(self, request, parent, destination, destination_uri, destination_parent):
+ return StoreAddressObjectResource(
+ request=request,
+ source=self,
+ source_uri=request.uri,
+ sourceparent=parent,
+ sourceadbk=True,
+ deletesource=True,
+ destination=destination,
+ destination_uri=destination_uri,
+ destinationparent=destination_parent,
+ destinationadbk=True,
+ )
+
+
class _NotificationChildHelper(object):
"""
Methods for things which are like notification objects.
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py 2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py 2012-12-04 16:48:48 UTC (rev 10122)
@@ -2223,19 +2223,19 @@
def load(cls, txn, managedID):
attco = schema.ATTACHMENT_CALENDAR_OBJECT
rows = (yield Select(
- [attco.ATTACHMENT_ID, ],
+ [attco.ATTACHMENT_ID, attco.CALENDAR_OBJECT_RESOURCE_ID, ],
From=attco,
Where=(attco.MANAGED_ID == managedID),
).on(txn))
- aids = [row[0] for row in rows] if rows is not None else ()
- if len(aids) == 0:
+ if len(rows) == 0:
returnValue(None)
- elif len(aids) != 1:
+ elif len(rows) != 1:
raise AttachmentStoreValidManagedID
- attachment = cls(txn, aids[0], None, None)
+ attachment = cls(txn, rows[0][0], None, None)
attachment = (yield attachment.initFromStore())
attachment._managedID = managedID
+ attachment._objectResourceID = rows[0][1]
returnValue(attachment)
@@ -2319,6 +2319,17 @@
return self._managedID
+ @inlineCallbacks
+ def objectResource(self):
+ """
+ Return the calendar object resource associated with this attachment.
+ """
+
+ home = (yield self._txn.calendarHomeWithResourceID(self._ownerHomeID))
+ obj = (yield home.objectResourceWithID(self._objectResourceID))
+ returnValue(obj)
+
+
@property
def _path(self):
# Use directory hashing scheme based on MD5 of attachmentID
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_sql.py 2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_sql.py 2012-12-04 16:48:48 UTC (rev 10122)
@@ -30,13 +30,13 @@
from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
from txdav.base.propertystore.base import PropertyName
-from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests,\
+from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests, \
test_event_text
from txdav.caldav.datastore.test.test_file import setUpCalendarStore
from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource
from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator
-from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT,\
+from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
_BIND_STATUS_ACCEPTED
from txdav.common.datastore.test.util import buildStore, populateCalendarsFrom
from txdav.common.icommondatastore import NoSuchObjectResourceError
@@ -63,8 +63,9 @@
self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
yield self.populate()
- self.nowYear = {"now":PyCalendarDateTime.getToday().getYear()}
+ self.nowYear = {"now": PyCalendarDateTime.getToday().getYear()}
+
@inlineCallbacks
def populate(self):
yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
@@ -185,7 +186,7 @@
backed calendar. We need to test what happens when there is "bad" calendar data
present in the file-backed calendar with a broken recurrence-id that we can fix.
"""
-
+
self.storeUnderTest().setMigrating(True)
fromCalendar = yield (yield self.fileTransaction().calendarHomeWithUID(
"home_bad")).calendarWithName("calendar_fix_recurrence")
@@ -306,7 +307,7 @@
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % self.nowYear)
-
+
toResource = yield toCalendar.calendarObjectWithName("3.ics")
caldata = yield toResource.component()
self.assertEqual(str(caldata), """BEGIN:VCALENDAR
@@ -353,7 +354,8 @@
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % self.nowYear)
-
+
+
@inlineCallbacks
def test_migrateDuplicateAttachmentsCalendarFromFile(self):
"""
@@ -371,6 +373,7 @@
self.assertEqual(ok, 3)
self.assertEqual(bad, 0)
+
@inlineCallbacks
def test_migrateCalendarFromFile_Transparency(self):
"""
@@ -385,10 +388,10 @@
yield _migrateCalendar(fromCalendar, toCalendar,
lambda x: x.component())
- filter = caldavxml.Filter(
+ filter = caldavxml.Filter(
caldavxml.ComponentFilter(
caldavxml.ComponentFilter(
- caldavxml.TimeRange(start="%(now)s0201T000000Z" % self.nowYear, end="%(now)s0202T000000Z" % self.nowYear),
+ caldavxml.TimeRange(start="%(now)s0201T000000Z" % self.nowYear, end="%(now)s0202T000000Z" % self.nowYear),
name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
),
name="VCALENDAR",
@@ -403,6 +406,7 @@
self.assertEquals(uid, "uid4")
self.assertEquals(transp, 'T')
+
@inlineCallbacks
def test_migrateHomeFromFile(self):
"""
@@ -410,7 +414,7 @@
backend to another; in this specific case, from the file-based backend
to the SQL-based backend.
"""
-
+
# Need to turn of split calendar behavior just for this test
self.patch(config, "RestrictCalendarsToOneComponentType", False)
@@ -467,9 +471,10 @@
continue
result = yield calendar.getSupportedComponents()
supported_components.add(result)
-
+
self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
+
@inlineCallbacks
def test_migrateHomeNoSplits(self):
"""
@@ -494,24 +499,25 @@
continue
result = yield calendar.getSupportedComponents()
supported_components.add(result)
-
+
self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
+
def test_calendarHomeVersion(self):
"""
The DATAVERSION column for new calendar homes must match the
CALENDAR-DATAVERSION value.
"""
-
+
home = yield self.transactionUnderTest().calendarHomeWithUID("home_version")
self.assertTrue(home is not None)
yield self.transactionUnderTest().commit
-
+
txn = yield self.transactionUnderTest()
version = yield txn.calendarserverValue("CALENDAR-DATAVERSION")[0][0]
ch = schema.CALENDAR_HOME
homeVersion = yield Select(
- [ch.DATAVERSION,],
+ [ch.DATAVERSION, ],
From=ch,
Where=ch.OWNER_UID == "home_version",
).on(txn)[0][0]
@@ -695,6 +701,7 @@
yield d1
yield d2
+
@inlineCallbacks
def test_datetimes(self):
calendarStore = self._sqlCalendarStore
@@ -714,6 +721,7 @@
self.assertEqual(obj.created(), datetimeMktime(datetime.datetime(2011, 2, 7, 11, 22, 47)))
self.assertEqual(obj.modified(), datetimeMktime(datetime.datetime(2011, 2, 8, 11, 22, 47)))
+
@inlineCallbacks
def test_notificationsProvisioningConcurrency(self):
"""
@@ -753,6 +761,7 @@
self.assertNotEqual(notification_uid1_1, None)
self.assertNotEqual(notification_uid1_2, None)
+
@inlineCallbacks
def test_removeCalendarPropertiesOnDelete(self):
"""
@@ -766,7 +775,7 @@
calendar = yield home.createCalendarWithName(name)
resourceID = calendar._resourceID
calendarProperties = calendar.properties()
-
+
prop = caldavxml.CalendarDescription.fromString("Calendar to be removed")
calendarProperties[PropertyName.fromElement(prop)] = prop
yield self.commit()
@@ -794,6 +803,7 @@
self.assertEqual(len(tuple(rows)), 0)
yield self.commit()
+
@inlineCallbacks
def test_removeCalendarObjectPropertiesOnDelete(self):
"""
@@ -839,6 +849,7 @@
self.assertEqual(len(tuple(rows)), 0)
yield self.commit()
+
@inlineCallbacks
def test_removeInboxObjectPropertiesOnDelete(self):
"""
@@ -849,7 +860,7 @@
# Create calendar object and add a property
home = yield self.homeUnderTest()
inbox = yield home.createCalendarWithName("inbox")
-
+
name = "test.ics"
component = VComponent.fromString(test_event_text)
metadata = {
@@ -891,6 +902,7 @@
self.assertEqual(len(tuple(rows)), 0)
yield self.commit()
+
@inlineCallbacks
def test_directShareCreateConcurrency(self):
"""
@@ -942,22 +954,23 @@
yield d1
yield d2
+
@inlineCallbacks
def test_transferSharingDetails(self):
"""
Test Calendar._transferSharingDetails to make sure sharing details are transferred.
"""
-
+
shareeHome = yield self.transactionUnderTest().calendarHomeWithUID("home_splits_shared")
calendar = yield (yield self.transactionUnderTest().calendarHomeWithUID(
"home_splits")).calendarWithName("calendar_1")
-
+
# Fake a shared binding on the original calendar
bind = calendar._bindSchema
_bindCreate = Insert({
bind.HOME_RESOURCE_ID: shareeHome._resourceID,
- bind.RESOURCE_ID: calendar._resourceID,
+ bind.RESOURCE_ID: calendar._resourceID,
bind.RESOURCE_NAME: "shared_1",
bind.MESSAGE: "Shared to you",
bind.BIND_MODE: _BIND_MODE_DIRECT,
@@ -984,34 +997,36 @@
self.assertTrue(sharedCalendar is not None)
self.assertEqual(sharedCalendar._resourceID, newcalendar._resourceID)
+
@inlineCallbacks
def test_moveCalendarObjectResource(self):
"""
Test Calendar._transferSharingDetails to make sure sharing details are transferred.
"""
-
+
calendar1 = yield (yield self.transactionUnderTest().calendarHomeWithUID(
"home_splits")).calendarWithName("calendar_1")
calendar2 = yield (yield self.transactionUnderTest().calendarHomeWithUID(
"home_splits")).calendarWithName("calendar_2")
-
+
child = yield calendar2.calendarObjectWithName("5.ics")
-
+
yield calendar2.moveObjectResource(child, calendar1)
-
+
child = yield calendar2.calendarObjectWithName("5.ics")
self.assertTrue(child is None)
-
+
child = yield calendar1.calendarObjectWithName("5.ics")
self.assertTrue(child is not None)
+
@inlineCallbacks
def test_splitCalendars(self):
"""
Test Calendar.splitCollectionByComponentTypes to make sure components are split out,
sync information is updated.
"""
-
+
# calendar_2 add a dead property to make sure it gets copied over
home = yield self.transactionUnderTest().calendarHomeWithUID("home_splits")
calendar2 = yield home.calendarWithName("calendar_2")
@@ -1031,7 +1046,7 @@
child = yield home.calendarWithName("calendar_1-vtodo")
self.assertTrue(child is None)
- calendar1 = yield home.calendarWithName("calendar_1")
+ calendar1 = yield home.calendarWithName("calendar_1")
children = yield calendar1.listCalendarObjects()
self.assertEqual(len(children), 3)
new_sync_token1 = yield calendar1.syncToken()
@@ -1043,7 +1058,7 @@
# calendar_2 does split
home = yield self.transactionUnderTest().calendarHomeWithUID("home_splits")
- calendar2 = yield home.calendarWithName("calendar_2")
+ calendar2 = yield home.calendarWithName("calendar_2")
original_sync_token2 = yield calendar2.syncToken()
yield calendar2.splitCollectionByComponentTypes()
yield self.commit()
@@ -1062,7 +1077,7 @@
self.assertTrue(pkey in calendar2_vtodo.properties())
self.assertEqual(str(calendar2_vtodo.properties()[pkey]), "A birthday calendar")
- calendar2 = yield home.calendarWithName("calendar_2")
+ calendar2 = yield home.calendarWithName("calendar_2")
children = yield calendar2.listCalendarObjects()
self.assertEqual(len(children), 3)
new_sync_token2 = yield calendar2.syncToken()
@@ -1075,13 +1090,14 @@
self.assertTrue(pkey in calendar2.properties())
self.assertEqual(str(calendar2.properties()[pkey]), "A birthday calendar")
+
@inlineCallbacks
def test_noSplitCalendars(self):
"""
Test CalendarHome.splitCalendars to make sure we end up with at least two collections
with different supported components.
"""
-
+
# Do split
home = yield self.transactionUnderTest().calendarHomeWithUID("home_no_splits")
calendars = yield home.calendars()
@@ -1098,23 +1114,24 @@
continue
result = yield calendar.getSupportedComponents()
supported_components.add(result)
-
+
self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
+
@inlineCallbacks
def test_resourceLock(self):
"""
Test CommonObjectResource.lock to make sure it locks, raises on missing resource,
and raises when locked and wait=False used.
"""
-
+
# Valid object
resource = yield self.calendarObjectUnderTest()
-
+
# Valid lock
yield resource.lock()
self.assertTrue(resource._locked)
-
+
# Setup a new transaction to verify the lock and also verify wait behavior
newTxn = self._sqlCalendarStore.newTransaction()
newResource = yield self.calendarObjectUnderTest(txn=newTxn)
@@ -1129,19 +1146,19 @@
# Commit existing transaction and verify we can get the lock using
yield self.commit()
-
+
resource = yield self.calendarObjectUnderTest()
yield resource.lock()
self.assertTrue(resource._locked)
-
+
# Setup a new transaction to verify the lock but pass in an alternative txn directly
newTxn = self._sqlCalendarStore.newTransaction()
-
+
# FIXME: not sure why, but without this statement here, this portion of the test fails in a funny way.
# Basically the query in the try block seems to execute twice, failing each time, one of which is caught,
# and the other not - causing the test to fail. Seems like some state on newTxn is not being initialized?
yield self.calendarObjectUnderTest("2.ics", txn=newTxn)
-
+
try:
yield resource.lock(wait=False, useTxn=newTxn)
except:
@@ -1169,29 +1186,30 @@
"""
Test CalendarObjectResource.recurrenceMinMax to make sure it handles a None value.
"""
-
+
# Valid object
resource = yield self.calendarObjectUnderTest()
-
+
# Valid lock
rMin, rMax = yield resource.recurrenceMinMax()
self.assertEqual(rMin, None)
self.assertEqual(rMax, None)
+
@inlineCallbacks
def test_notExpandedWithin(self):
"""
Test PostgresLegacyIndexEmulator.notExpandedWithin to make sure it returns the correct
result based on the ranges passed in.
"""
-
+
self.patch(config, "FreeBusyIndexDelayedExpand", False)
# Create the index on a new calendar
home = yield self.homeUnderTest()
newcalendar = yield home.createCalendarWithName("index_testing")
index = PostgresLegacyIndexEmulator(newcalendar)
-
+
# Create the calendar object to use for testing
nowYear = self.nowYear["now"]
caldata = """BEGIN:VCALENDAR
@@ -1287,15 +1305,15 @@
instances = yield calendarObject.instances()
self.assertNotEqual(len(instances), 0)
yield self.commit()
-
+
# Re-add event with re-indexing
calendar = yield self.calendarUnderTest()
calendarObject = yield self.calendarObjectUnderTest("indexing.ics")
yield calendarObject.setComponent(component)
instances2 = yield calendarObject.instances()
self.assertNotEqual(
- sorted(instances, key=lambda x:x[0])[0],
- sorted(instances2, key=lambda x:x[0])[0],
+ sorted(instances, key=lambda x: x[0])[0],
+ sorted(instances2, key=lambda x: x[0])[0],
)
yield self.commit()
@@ -1306,13 +1324,14 @@
yield calendarObject.setComponent(component)
instances3 = yield calendarObject.instances()
self.assertEqual(
- sorted(instances2, key=lambda x:x[0])[0],
- sorted(instances3, key=lambda x:x[0])[0],
+ sorted(instances2, key=lambda x: x[0])[0],
+ sorted(instances3, key=lambda x: x[0])[0],
)
-
+
yield calendar.removeCalendarObjectWithName("indexing.ics")
yield self.commit()
+
@inlineCallbacks
def test_loadObjectResourcesWithName(self):
"""
@@ -1324,19 +1343,19 @@
def _tests(cal):
resources = yield cal.objectResourcesWithNames(("1.ics",))
self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics",)))
-
+
resources = yield cal.objectResourcesWithNames(("1.ics", "2.ics",))
self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics",)))
-
+
resources = yield cal.objectResourcesWithNames(("1.ics", "2.ics", "3.ics",))
self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics", "3.ics",)))
-
+
resources = yield cal.objectResourcesWithNames(("1.ics", "2.ics", "3.ics", "4.ics",))
self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics", "3.ics", "4.ics",)))
-
+
resources = yield cal.objectResourcesWithNames(("bogus1.ics",))
self.assertEqual(set([resource.name() for resource in resources]), set())
-
+
resources = yield cal.objectResourcesWithNames(("bogus1.ics", "2.ics",))
self.assertEqual(set([resource.name() for resource in resources]), set(("2.ics",)))
@@ -1347,7 +1366,7 @@
# Adjust batch size and try again
self.patch(CommonObjectResource, "BATCH_LOAD_SIZE", 2)
yield _tests(cal)
-
+
yield self.commit()
# Tests on inbox - resources with properties
@@ -1392,13 +1411,27 @@
self.assertEqual(resources[0].properties()[PropertyName.fromElement(prop)], prop)
resources = yield inbox.objectResourcesWithNames(("1.ics", "2.ics",))
- resources.sort(key=lambda x:x._name)
+ resources.sort(key=lambda x: x._name)
prop = caldavxml.CalendarDescription.fromString("p1")
self.assertEqual(resources[0].properties()[PropertyName.fromElement(prop)], prop)
prop = caldavxml.CalendarDescription.fromString("p2")
self.assertEqual(resources[1].properties()[PropertyName.fromElement(prop)], prop)
resources = yield inbox.objectResourcesWithNames(("bogus1.ics", "2.ics",))
- resources.sort(key=lambda x:x._name)
+ resources.sort(key=lambda x: x._name)
prop = caldavxml.CalendarDescription.fromString("p2")
self.assertEqual(resources[0].properties()[PropertyName.fromElement(prop)], prop)
+
+
+ @inlineCallbacks
+ def test_objectResourceWithID(self):
+ """
+ L{ICalendarHome.objectResourceWithID} will return the calendar object..
+ """
+ home = yield self.homeUnderTest()
+ calendarObject = (yield home.objectResourceWithID(9999))
+ self.assertEquals(calendarObject, None)
+
+ obj = (yield self.calendarObjectUnderTest())
+ calendarObject = (yield home.objectResourceWithID(obj._resourceID))
+ self.assertNotEquals(calendarObject, None)
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py 2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py 2012-12-04 16:48:48 UTC (rev 10122)
@@ -298,7 +298,19 @@
"""
+ def objectResourceWithID(rid):
+ """
+ Return the calendar object resource with the specified ID, assumed to be a child of
+ a calendar collection within this home.
+ @param rid: resource id of object to find
+ @type rid: C{int}
+
+ @return: L{ICalendar} or C{None} if not found
+ """
+
+
+
class ICalendar(INotifier, IShareableCollection, IDataStoreObject):
"""
Calendar
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/file.py 2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/file.py 2012-12-04 16:48:48 UTC (rev 10122)
@@ -291,7 +291,7 @@
returnValue([kv[1] for kv in sorted(self._determineMemo(storeType, None).items(), key=lambda x: x[0])])
- @memoizedKey("uid", _determineMemo)
+ @memoizedKey("uid", _determineMemo, deferredResult=False)
def homeWithUID(self, storeType, uid, create=False):
if uid.startswith("."):
return None
@@ -302,7 +302,7 @@
return self._homeClass[storeType].homeWithUID(self, uid, create, storeType == ECALENDARTYPE)
- @memoizedKey("uid", "_notificationHomes")
+ @memoizedKey("uid", "_notificationHomes", deferredResult=False)
def notificationsWithUID(self, uid, home=None):
if home is None:
@@ -678,6 +678,15 @@
return results
+ def objectResourceWithID(self, rid):
+ """
+ Return all child object resources with the specified resource-ID.
+ """
+
+ # File store does not have resource ids.
+ raise NotImplementedError
+
+
def quotaUsedBytes(self):
try:
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py 2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py 2012-12-04 16:48:48 UTC (rev 10122)
@@ -1009,13 +1009,13 @@
"""
# TODO: see if there is a better way to import Attachment
- from txdav.caldav.datastore.sql import Attachment
+ from txdav.caldav.datastore.sql import DropBoxAttachment
results = (yield self.orphanedAttachments(batchSize=batchSize))
count = 0
for dropboxID, path in results:
- attachment = Attachment(self, dropboxID, path)
- (yield attachment.remove())
+ attachment = (yield DropBoxAttachment.load(self, dropboxID, path))
+ yield attachment.remove()
count += 1
returnValue(count)
@@ -1590,6 +1590,34 @@
returnValue(results)
+ @classmethod
+ def _objectResourceIDQuery(cls):
+ obj = cls._objectSchema
+ return Select(
+ [obj.PARENT_RESOURCE_ID],
+ From=obj,
+ Where=(obj.RESOURCE_ID == Parameter("resourceID")),
+ )
+
+
+ @inlineCallbacks
+ def objectResourceWithID(self, rid):
+ """
+ Return all child object resources with the specified resource-ID.
+ """
+ rows = (yield self._objectResourceIDQuery().on(
+ self._txn, resourceID=rid
+ ))
+ if rows and len(rows) == 1:
+ child = (yield self.childWithID(rows[0][0]))
+ objectResource = (
+ yield child.objectResourceWithID(rid)
+ )
+ returnValue(objectResource)
+
+ returnValue(None)
+
+
@classproperty
def _quotaQuery(cls): #@NoSelf
meta = cls._homeMetaDataSchema
@@ -3291,13 +3319,18 @@
@classproperty
- def _moveParentUpdateQuery(cls): #@NoSelf
+ def _moveParentUpdateQuery(cls, adjustName=False): #@NoSelf
"""
DAL query to update a child to be in a new parent.
"""
obj = cls._objectSchema
+ cols = {
+ obj.PARENT_RESOURCE_ID: Parameter("newParentID")
+ }
+ if adjustName:
+ cols[obj.RESOURCE_NAME] = Parameter("newName")
return Update(
- {obj.PARENT_RESOURCE_ID: Parameter("newParentID")},
+ cols,
Where=obj.RESOURCE_ID == Parameter("resourceID")
)
@@ -3311,7 +3344,7 @@
@inlineCallbacks
- def moveObjectResource(self, child, newparent):
+ def moveObjectResource(self, child, newparent, newname=None):
"""
Move a child of this collection into another collection without actually removing/re-inserting the data.
Make sure sync and cache details for both collections are updated.
@@ -3323,11 +3356,19 @@
@type child: L{CommonObjectResource}
@param newparent: the parent to move to
@type newparent: L{CommonHomeChild}
+ @param newname: new name to use in new parent
+ @type newname: C{str} or C{None} for existing name
"""
+ if newname and newname.startswith("."):
+ raise ObjectResourceNameNotAllowedError(newname)
+
name = child.name()
uid = child.uid()
+ if newname is None:
+ newname = name
+
# Clean this collections cache and signal sync change
self._objects.pop(name, None)
self._objects.pop(uid, None)
@@ -3335,17 +3376,32 @@
yield self._deleteRevision(name)
yield self.notifyChanged()
- # Adjust the child to be a child of the new parent and update ancillary tables
- yield self._moveParentUpdateQuery.on(
+ # Handle cases where move is within the same collection or to a different collection
+ # with/without a name change
+ obj = self._objectSchema
+ cols = {}
+ if newparent._resourceID != self._resourceID:
+ cols[obj.PARENT_RESOURCE_ID] = Parameter("newParentID")
+ if newname != name:
+ cols[obj.RESOURCE_NAME] = Parameter("newName")
+ yield Update(
+ cols,
+ Where=obj.RESOURCE_ID == Parameter("resourceID")
+ ).on(
self._txn,
+ resourceID=child._resourceID,
newParentID=newparent._resourceID,
- resourceID=child._resourceID
+ newName=newname,
)
- yield self._movedObjectResource(child, newparent)
+
+ # Only signal a move when parent is different
+ if newparent._resourceID != self._resourceID:
+ yield self._movedObjectResource(child, newparent)
+
child._parentCollection = newparent
# Signal sync change on new collection
- yield newparent._insertRevision(name)
+ yield newparent._insertRevision(newname)
yield newparent.notifyChanged()
@@ -3859,6 +3915,21 @@
return Delete(cls._objectSchema, Where=cls._objectSchema.RESOURCE_ID == Parameter("resourceID"))
+ def moveTo(self, destination, name):
+ """
+ Move object to another collection.
+
+ @param destination: parent collection to move to
+ @type destination: L{CommonHomeChild}
+ @param name: new name in destination
+ @type name: C{str} or C{None} to use existing name
+ """
+
+ if name and name.startswith("."):
+ raise ObjectResourceNameNotAllowedError(name)
+ return self._parentCollection.moveObjectResource(self, destination, name)
+
+
@inlineCallbacks
def remove(self):
yield self._deleteQuery.on(self._txn, NoSuchObjectResourceError,
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/test/util.py 2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/test/util.py 2012-12-04 16:48:48 UTC (rev 10122)
@@ -126,7 +126,8 @@
reactor.addSystemEventTrigger("before", "shutdown", cp.stopService)
cds = CommonDataStore(
cp.connection, StubNotifierFactory(),
- attachmentRoot, quota=staticQuota
+ attachmentRoot, "",
+ quota=staticQuota
)
return cds
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121204/ad98eefa/attachment-0001.html>
More information about the calendarserver-changes
mailing list