[CalendarServer-changes] [6837] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Tue Feb 1 09:32:43 PST 2011
Revision: 6837
http://trac.macosforge.org/projects/calendarserver/changeset/6837
Author: cdaboo at apple.com
Date: 2011-02-01 09:32:41 -0800 (Tue, 01 Feb 2011)
Log Message:
-----------
Fix dropbox so that sharees can add attachments - we have to re-write the dropbox path from the client to support that.
Also change the Attachment api to make those objects independent of the calendar object (but they need to track the calendar
home of the attachment owner for quota purposes).
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/method/put_common.py
CalendarServer/trunk/twistedcaldav/storebridge.py
CalendarServer/trunk/txdav/caldav/datastore/file.py
CalendarServer/trunk/txdav/caldav/datastore/sql.py
CalendarServer/trunk/txdav/caldav/datastore/test/common.py
CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
CalendarServer/trunk/txdav/caldav/datastore/util.py
CalendarServer/trunk/txdav/caldav/icalendarstore.py
CalendarServer/trunk/txdav/common/datastore/sql.py
CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql
CalendarServer/trunk/txdav/common/datastore/test/test_util.py
Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -23,6 +23,7 @@
import types
import uuid
+from urlparse import urlparse, urlunparse
from twisted.internet import reactor
from twisted.internet.defer import Deferred, inlineCallbacks, succeed
@@ -599,6 +600,69 @@
@inlineCallbacks
+ def dropboxPathNormalization(self):
+ """
+ Make sure sharees only use dropbox paths of the sharer.
+ """
+
+ # Only relevant if calendar is virtual share
+ changed = False
+ if self.destinationparent.isVirtualShare():
+
+ # Get all X-APPLE-DROPBOX's and ATTACH's that are http URIs
+ xdropboxes = self.calendar.getAllPropertiesInAnyComponent(
+ "X-APPLE-DROPBOX",
+ depth=1,
+ )
+ attachments = self.calendar.getAllPropertiesInAnyComponent(
+ "ATTACH",
+ depth=1,
+ )
+ attachments = [
+ attachment for attachment in attachments
+ if attachment.params().get("VALUE", ("TEXT",))[0] == "URI" and attachment.value().startswith("http")
+ ]
+
+ if len(xdropboxes) or len(attachments):
+
+ # Determine owner GUID
+ ownerPrincipal = (yield self.destinationparent.ownerPrincipal(self.request))
+ owner = ownerPrincipal.principalURL().split("/")[-2]
+
+ def uriNormalize(uri):
+ urichanged = False
+ scheme, netloc, path, params, query, fragment = urlparse(uri)
+ pathbits = path.split("/")
+ if pathbits[1] != "calendars":
+ pathbits[1] = "calendars"
+ urichanged = True
+ if pathbits[2] != "__uids__":
+ pathbits[2] = "__uids__"
+ urichanged = True
+ if pathbits[3] != owner:
+ pathbits[3] = owner
+ urichanged = True
+ if urichanged:
+ return urlunparse((scheme, netloc, "/".join(pathbits), params, query, fragment,))
+ return None
+
+ for xdropbox in xdropboxes:
+ uri = uriNormalize(xdropbox.value())
+ if uri:
+ xdropbox.setValue(uri)
+ changed = True
+ for attachment in attachments:
+ uri = uriNormalize(attachment.value())
+ if uri:
+ attachment.setValue(uri)
+ changed = True
+
+ if changed:
+ self.calendardata = None
+
+ returnValue(changed)
+
+ @inlineCallbacks
def noUIDConflict(self, uid):
"""
Check that the UID of the new calendar object conforms to the requirements of
@@ -906,6 +970,9 @@
# Preserve private comments
yield self.preservePrivateComments()
+ # Handle sharing dropbox normalization
+ dropboxChanged = (yield self.dropboxPathNormalization())
+
# Do scheduling
implicit_result = (yield self.doImplicitScheduling())
if isinstance(implicit_result, int):
@@ -948,7 +1015,7 @@
response = (yield self.doStore(data_changed))
# Must not set ETag in response if data changed
- if did_implicit_action or rruleChanged:
+ if did_implicit_action or rruleChanged or dropboxChanged:
def _removeEtag(request, response):
response.headers.removeHeader('etag')
return response
Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -1054,22 +1054,15 @@
if content_type is None:
content_type = MimeType("application", "octet-stream")
- if self._newStoreAttachment:
- t = self._newStoreAttachment.store(content_type)
- yield readStream(request.stream, t.write)
- yield t.loseConnection()
- returnValue(NO_CONTENT)
- else:
- t = yield self._newStoreCalendarObject.createAttachmentWithName(
+ creating = (self._newStoreAttachment is None)
+ if creating:
+ self._newStoreAttachment = self._newStoreObject = (yield self._newStoreCalendarObject.createAttachmentWithName(
self.attachmentName,
- content_type,
- )
- yield readStream(request.stream, t.write)
- self._newStoreAttachment = self._newStoreObject = yield self._newStoreCalendarObject.attachmentWithName(
- self.attachmentName
- )
- yield t.loseConnection()
- returnValue(CREATED)
+ ))
+ t = self._newStoreAttachment.store(content_type)
+ yield readStream(request.stream, t.write)
+ yield t.loseConnection()
+ returnValue(CREATED if creating else NO_CONTENT)
@requiresPermissions(davxml.Read())
@@ -1085,7 +1078,12 @@
stream.write(data)
def connectionLost(self, reason):
stream.finish()
- self._newStoreAttachment.retrieve(StreamProtocol())
+ try:
+ self._newStoreAttachment.retrieve(StreamProtocol())
+ except IOError, e:
+ log.error("Unable to read attachment: %s, due to: %s" % (self, e,))
+ raise HTTPError(responsecode.NOT_FOUND)
+
return Response(OK, {"content-type":self.contentType()}, stream)
Modified: CalendarServer/trunk/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/file.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/txdav/caldav/datastore/file.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -467,7 +467,7 @@
hasPrivateComment = property(_get_hasPrivateComment, _set_hasPrivateComment)
@inlineCallbacks
- def createAttachmentWithName(self, name, contentType):
+ def createAttachmentWithName(self, name):
"""
Implement L{ICalendarObject.removeAttachmentWithName}.
"""
@@ -475,7 +475,7 @@
dropboxPath = yield self._dropboxPath()
attachment = Attachment(self, name, dropboxPath)
self._attachments[name] = attachment
- returnValue(attachment.store(contentType))
+ returnValue(attachment)
@inlineCallbacks
Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -659,48 +659,21 @@
hasPrivateComment = property(_get_hasPrivateComment, _set_hasPrivateComment)
@inlineCallbacks
- def createAttachmentWithName(self, name, contentType):
+ def createAttachmentWithName(self, name):
+
+ # We need to know the resource_ID of the home collection of the owner (not sharee)
+ # of this event
+ sharerHomeID = (yield self._parentCollection.sharerHomeID())
+ returnValue((yield Attachment.create(self._txn, self._dropboxID, name, sharerHomeID)))
- try:
- self._attachmentPathRoot().makedirs()
- except:
- pass
-
- attachment = Attachment(self, name)
- yield self._txn.execSQL(
- """
- insert into ATTACHMENT (DROPBOX_ID, CONTENT_TYPE,
- SIZE, MD5, PATH) values (%s, %s, %s, %s, %s)
- """,
- [
- self._dropboxID, generateContentType(contentType), 0, "",
- name,
- ]
- )
- returnValue(attachment.store(contentType))
-
-
@inlineCallbacks
def removeAttachmentWithName(self, name):
attachment = (yield self.attachmentWithName(name))
- old_size = attachment.size()
- self._txn.postCommit(attachment._path.remove)
- yield self._txn.execSQL(
- """
- delete from ATTACHMENT where DROPBOX_ID = %s AND
- PATH = %s
- """, [self._dropboxID, name]
- )
+ yield attachment.remove()
- # Adjust quota
- yield self._calendar._home.adjustQuotaUsedBytes(-old_size)
-
- # Send change notification to home
- yield self._calendar._home.notifyChanged()
-
@inlineCallbacks
def attachmentWithName(self, name):
- attachment = Attachment(self, name)
+ attachment = Attachment(self._txn, self._dropboxID, name)
attachment = (yield attachment.initFromStore())
returnValue(attachment)
@@ -711,14 +684,6 @@
dropboxID = dropboxIDFromCalendarObject
- def _attachmentPathRoot(self):
- attachmentRoot = self._txn._store.attachmentsPath
-
- # Use directory hashing scheme based on owner user id
- homeName = self._calendar.ownerHome().name()
- return attachmentRoot.child(homeName[0:2]).child(homeName[2:4]).child(homeName).child(self.uid())
-
-
@inlineCallbacks
def attachments(self):
rows = yield self._txn.execSQL(
@@ -798,11 +763,13 @@
))[0]
)
- # Adjust quota
- yield self.attachment._calendarObject._calendar._home.adjustQuotaUsedBytes(self.attachment.size() - old_size)
-
- # Send change notification to home
- yield self.attachment._calendarObject._calendar._home.notifyChanged()
+ home = (yield self._txn.calendarHomeWithResourceID(self.attachment._ownerHomeID))
+ if home:
+ # Adjust quota
+ yield home.adjustQuotaUsedBytes(self.attachment.size() - old_size)
+
+ # Send change notification to home
+ yield home.notifyChanged()
def sqltime(value):
@@ -812,18 +779,54 @@
implements(IAttachment)
- def __init__(self, calendarObject, name):
- self._calendarObject = calendarObject
+ def __init__(self, txn, dropboxID, name, ownerHomeID=None):
+ self._txn = txn
+ self._dropboxID = dropboxID
self._name = name
+ self._ownerHomeID = ownerHomeID
self._size = 0
- @property
- def _txn(self):
- return self._calendarObject._txn
+ @classmethod
+ def _attachmentPathRoot(cls, txn, dropboxID):
+ attachmentRoot = txn._store.attachmentsPath
+
+ # Use directory hashing scheme based on MD5 of dropboxID
+ hasheduid = hashlib.md5(dropboxID).hexdigest()
+ return attachmentRoot.child(hasheduid[0:2]).child(hasheduid[2:4]).child(hasheduid)
+ @classmethod
@inlineCallbacks
+ def create(cls, txn, dropboxID, name, ownerHomeID):
+
+ # File system paths need to exist
+ try:
+ cls._attachmentPathRoot(txn, dropboxID).makedirs()
+ except:
+ pass
+
+ # Now create the DB entry
+ attachment = cls(txn, dropboxID, name, ownerHomeID)
+ yield txn.execSQL(
+ """
+ insert into ATTACHMENT
+ (CALENDAR_HOME_RESOURCE_ID, DROPBOX_ID, CONTENT_TYPE, SIZE, MD5, PATH)
+ values
+ (%s, %s, %s, %s, %s, %s)
+ """,
+ [
+ ownerHomeID,
+ dropboxID,
+ "",
+ 0,
+ "",
+ name,
+ ]
+ )
+ returnValue(attachment)
+
+ @inlineCallbacks
def initFromStore(self):
"""
Execute necessary SQL queries to retrieve attributes.
@@ -832,17 +835,20 @@
"""
rows = yield self._txn.execSQL(
"""
- select CONTENT_TYPE, SIZE, MD5, CREATED, MODIFIED from ATTACHMENT where PATH = %s
+ select CALENDAR_HOME_RESOURCE_ID, CONTENT_TYPE, SIZE, MD5, CREATED, MODIFIED
+ from ATTACHMENT
+ where DROPBOX_ID = %s and PATH = %s
""",
- [self._name]
+ [self._dropboxID, self._name]
)
if not rows:
returnValue(None)
- self._contentType = MimeType.fromString(rows[0][0])
- self._size = rows[0][1]
- self._md5 = rows[0][2]
- self._created = sqltime(rows[0][3])
- self._modified = sqltime(rows[0][4])
+ self._ownerHomeID = rows[0][0]
+ self._contentType = MimeType.fromString(rows[0][1])
+ self._size = rows[0][2]
+ self._md5 = rows[0][3]
+ self._created = sqltime(rows[0][4])
+ self._modified = sqltime(rows[0][5])
returnValue(self)
@@ -851,8 +857,11 @@
@property
def _path(self):
- attachmentPath = self._calendarObject._attachmentPathRoot()
- return attachmentPath.child(self.name())
+ attachmentRoot = self._txn._store.attachmentsPath
+
+ # Use directory hashing scheme based on MD5 of dropboxID
+ hasheduid = hashlib.md5(self._dropboxID).hexdigest()
+ return attachmentRoot.child(hasheduid[0:2]).child(hasheduid[2:4]).child(hasheduid).child(self.name())
def properties(self):
pass # stub
@@ -867,6 +876,26 @@
protocol.connectionLost(Failure(ConnectionLost()))
+ @inlineCallbacks
+ def remove(self):
+ old_size = self._size
+ self._txn.postCommit(self._path.remove)
+ yield self._txn.execSQL(
+ """
+ delete from ATTACHMENT
+ where DROPBOX_ID = %s and PATH = %s
+ """,
+ [self._dropboxID, self._name]
+ )
+
+ # Adjust quota
+ home = (yield self._txn.calendarHomeWithResourceID(self._ownerHomeID))
+ if home:
+ yield home.adjustQuotaUsedBytes(-old_size)
+
+ # Send change notification to home
+ yield home.notifyChanged()
+
# IDataStoreResource
def contentType(self):
return self._contentType
Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -1254,9 +1254,10 @@
Common logic for attachment-creation tests.
"""
obj = yield self.calendarObjectUnderTest()
- t = yield obj.createAttachmentWithName(
- "new.attachment", MimeType("text", "x-fixture")
+ attachment = yield obj.createAttachmentWithName(
+ "new.attachment",
)
+ t = attachment.store(MimeType("text", "x-fixture"))
t.write("new attachment")
t.write(" text")
yield t.loseConnection()
@@ -1345,9 +1346,10 @@
L{ICalendarHome.calendarWithName} or L{ICalendarHome.calendars}.
"""
obj = yield self.calendarObjectUnderTest()
- t = yield obj.createAttachmentWithName(
- "new.attachment", MimeType("text", "plain")
+ attachment = yield obj.createAttachmentWithName(
+ "new.attachment",
)
+ t = attachment.store(MimeType("text", "plain"))
t.write("new attachment text")
yield t.loseConnection()
yield self.commit()
Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -23,6 +23,7 @@
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.task import deferLater
from twisted.internet import reactor
+from twisted.python import hashlib
from twext.python.vcomponent import VComponent
from twext.web2.dav.element.rfc2518 import GETContentLanguage, ResourceType
@@ -120,10 +121,9 @@
attachmentRoot = (
yield self.calendarObjectUnderTest()
)._txn._store.attachmentsPath
- attachmentPath = attachmentRoot.child("ho").child("me").child("home1")
- attachmentPath = attachmentPath.child(
- (yield self.calendarObjectUnderTest()).uid()).child(
- "new.attachment")
+ obj = yield self.calendarObjectUnderTest()
+ hasheduid = hashlib.md5(obj._dropboxID).hexdigest()
+ attachmentPath = attachmentRoot.child(hasheduid[0:2]).child(hasheduid[2:4]).child(hasheduid).child("new.attachment")
self.assertTrue(attachmentPath.isfile())
Modified: CalendarServer/trunk/txdav/caldav/datastore/util.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/util.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/txdav/caldav/datastore/util.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -139,7 +139,8 @@
for attachment in (yield calendarObject.attachments()):
name = attachment.name()
ctype = attachment.contentType()
- transport = yield outObject.createAttachmentWithName(name, ctype)
+ newattachment = yield outObject.createAttachmentWithName(name)
+ transport = newattachment.store(ctype)
proto =_AttachmentMigrationProto(transport)
attachment.retrieve(proto)
yield proto.done
Modified: CalendarServer/trunk/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendarstore.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/txdav/caldav/icalendarstore.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -359,7 +359,7 @@
"""
- def createAttachmentWithName(name, contentType):
+ def createAttachmentWithName(name):
"""
Add an attachment to this calendar object.
@@ -368,11 +368,7 @@
@type name: C{str}
- @param contentType: The content-type of the data to store.
-
- @type contentType: L{twext.web2.http_headers.MimeType}
-
- @return: the same type as L{IAttachment.store} returns.
+ @return: the L{IAttachment}.
"""
Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -196,6 +196,24 @@
return self._homeClass[storeType].homeWithUID(self, uid, create)
+ @inlineCallbacks
+ def calendarHomeWithResourceID(self, rid):
+ uid = (yield self._homeClass[ECALENDARTYPE].homeUIDWithResourceID(self, rid))
+ if uid:
+ result = (yield self.calendarHomeWithUID(uid))
+ else:
+ result = None
+ returnValue(result)
+
+ @inlineCallbacks
+ def addressbookHomeWithResourceID(self, rid):
+ uid = (yield self._homeClass[EADDRESSBOOKTYPE].homeUIDWithResourceID(self, rid))
+ if uid:
+ result = (yield self.addressbookHomeWithUID(uid))
+ else:
+ result = None
+ returnValue(result)
+
@memoizedKey("uid", "_notificationHomes")
def notificationsWithUID(self, uid):
"""
@@ -389,6 +407,20 @@
yield home.createdHome()
returnValue(home)
+ @classmethod
+ @inlineCallbacks
+ def homeUIDWithResourceID(cls, txn, rid):
+
+ rows = (yield txn.execSQL(
+ "select %(column_OWNER_UID)s from %(name)s"
+ " where %(column_RESOURCE_ID)s = %%s" % cls._homeTable,
+ [rid]
+ ))
+ if rows:
+ returnValue(rows[0][0])
+ else:
+ returnValue(None)
+
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
@@ -1158,6 +1190,22 @@
def ownerHome(self):
return self._home
+ @inlineCallbacks
+ def sharerHomeID(self):
+
+ # If this is not shared then our home is what we want
+ if self._owned:
+ returnValue(self._home._resourceID)
+ else:
+ rid = (yield self._txn.execSQL("""
+ select %(column_HOME_RESOURCE_ID)s from %(name)s
+ where
+ %(column_RESOURCE_ID)s = %%s and
+ %(column_BIND_MODE)s = %%s
+ """ % self._bindTable,
+ [self._resourceID, _BIND_MODE_OWN]
+ ))[0][0]
+ returnValue(rid)
def setSharingUID(self, uid):
self.properties()._setPerUserUID(uid)
Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql 2011-02-01 17:32:41 UTC (rev 6837)
@@ -288,6 +288,7 @@
----------------
create table ATTACHMENT (
+ CALENDAR_HOME_RESOURCE_ID integer not null references CALENDAR_HOME,
DROPBOX_ID varchar(255) not null,
CONTENT_TYPE varchar(255) not null,
SIZE integer not null,
Modified: CalendarServer/trunk/txdav/common/datastore/test/test_util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/test_util.py 2011-02-01 02:02:11 UTC (rev 6836)
+++ CalendarServer/trunk/txdav/common/datastore/test/test_util.py 2011-02-01 17:32:41 UTC (rev 6837)
@@ -138,9 +138,10 @@
inObject = yield getSampleObj()
someAttachmentName = "some-attachment"
someAttachmentType = MimeType.fromString("application/x-custom-type")
- transport = yield inObject.createAttachmentWithName(
- someAttachmentName, someAttachmentType
+ attachment = yield inObject.createAttachmentWithName(
+ someAttachmentName,
)
+ transport = attachment.store(someAttachmentType)
someAttachmentData = "Here is some data for your attachment, enjoy."
transport.write(someAttachmentData)
transport.loseConnection()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110201/8029a21a/attachment-0001.html>
More information about the calendarserver-changes
mailing list