[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