[CalendarServer-changes] [5823] CalendarServer/branches/new-store/txcaldav/calendarstore

source_changes at macosforge.org source_changes at macosforge.org
Wed Jun 30 15:42:57 PDT 2010


Revision: 5823
          http://trac.macosforge.org/projects/calendarserver/changeset/5823
Author:   glyph at apple.com
Date:     2010-06-30 15:42:56 -0700 (Wed, 30 Jun 2010)
Log Message:
-----------
attachments backend and some tests for it (although not nearly enough)

Modified Paths:
--------------
    CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
    CalendarServer/branches/new-store/txcaldav/calendarstore/test/common.py

Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/file.py	2010-06-30 22:39:29 UTC (rev 5822)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/file.py	2010-06-30 22:42:56 UTC (rev 5823)
@@ -27,22 +27,32 @@
     "CalendarObject",
 ]
 
+import hashlib
+
 from errno import ENOENT
 
+from twisted.internet.interfaces import ITransport
+from twisted.python.failure import Failure
+from txdav.propertystore.xattr import PropertyStore
+
 from twext.python.vcomponent import InvalidICalendarDataError
 from twext.python.vcomponent import VComponent
-from twext.web2.dav.element.rfc2518 import ResourceType
+# from twext.web2.dav.resource import TwistedGETContentMD5
+from twext.web2.dav.element.rfc2518 import ResourceType, GETContentType
 
+
 from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
 from twistedcaldav.index import Index as OldIndex, IndexSchedule as OldInboxIndex
 from twistedcaldav.sharing import InvitesDatabase
 
+from txcaldav.icalendarstore import IAttachment
 from txcaldav.icalendarstore import ICalendar, ICalendarObject
 from txcaldav.icalendarstore import ICalendarHome
 
-from txdav.common.datastore.file import CommonDataStore, CommonStoreTransaction,\
+
+from txdav.common.datastore.file import CommonDataStore, CommonStoreTransaction, \
     CommonHome, CommonHomeChild, CommonObjectResource
-from txdav.common.icommondatastore import InvalidObjectResourceError,\
+from txdav.common.icommondatastore import InvalidObjectResourceError, \
     NoSuchObjectResourceError, InternalDataStoreError
 from txdav.datastore.file import writeOperation, hidden
 from txdav.propertystore.base import PropertyName
@@ -61,11 +71,30 @@
 
         self._childClass = Calendar
 
-    calendars = CommonHome.children
-    calendarWithName = CommonHome.childWithName
+
+    def calendarWithName(self, name):
+        if name == 'dropbox':
+            # "dropbox" is a file storage area, not a calendar.
+            return None
+        else:
+            return self.childWithName(name)
+
+
     createCalendarWithName = CommonHome.createChildWithName
     removeCalendarWithName = CommonHome.removeChildWithName
+    calendars = CommonHome.children
 
+
+    def calendarObjectWithDropboxID(self, dropboxID):
+        """
+        Implement lookup with brute-force scanning.
+        """
+        for calendar in self.calendars():
+            for calendarObject in calendar.calendarObjects():
+                if dropboxID == calendarObject.dropboxID():
+                    return calendarObject
+
+
     @property
     def _calendarStore(self):
         return self._dataStore
@@ -145,6 +174,7 @@
 
     def __init__(self, name, calendar):
         super(CalendarObject, self).__init__(name, calendar)
+        self._attachments = {}
 
     @property
     def _calendar(self):
@@ -268,6 +298,140 @@
         return self.component().getOrganizer()
 
 
+    def createAttachmentWithName(self, name, contentType):
+        """
+        
+        """
+        # Make a (temp, remember rollbacks) file in dropbox-land
+        attachment = Attachment(self, name)
+        self._attachments[name] = attachment
+        return attachment.store(contentType)
+
+
+    def attachmentWithName(self, name):
+        """
+        
+        """
+        # Attachments can be local or remote, but right now we only care about
+        # local.  So we're going to base this on the listing of files in the
+        # dropbox and not on the calendar data.  However, we COULD examine the
+        # 'attach' properties.
+
+        if name in self._attachments:
+            return self._attachments[name]
+        return Attachment(self, name)
+        # But, ahem.
+
+
+    def dropboxID(self):
+        """
+        
+        """
+        component = self.component()
+        for subcomp in component.subcomponents():
+            dropboxProperty = subcomp.getProperty("X-APPLE-DROPBOX")
+            if dropboxProperty is not None:
+                componentDropboxID = dropboxProperty.value().split("/")[-1]
+                return componentDropboxID
+        # FIXME: direct tests
+        return self.uid() + ".dropbox"
+
+
+    def _dropboxPath(self):
+        dropboxPath = self._parentCollection._home._path.child(
+            "dropbox"
+        ).child(self.dropboxID())
+        if not dropboxPath.isdir():
+            dropboxPath.makedirs()
+        return dropboxPath
+
+
+    def attachments(self):
+        # See comment on attachmentWithName.
+        return [Attachment(self, name)
+                for name in self._dropboxPath().listdir()]
+
+
+
+class AttachmentStorageTransport(object):
+
+    implements(ITransport)
+
+    def __init__(self, attachment):
+        """
+        
+        @param attachment:
+        @type attachment:
+        """
+        self._attachment = attachment
+        self._file = self._attachment._computePath().open("w")
+
+
+    def write(self, data):
+        # FIXME: multiple chunks
+        self._file.write(data)
+
+
+    def loseConnection(self):
+        # FIXME: do anything
+        self._file.close()
+        # TwistedGETContentMD5.fromString(md5)
+
+
+contentTypeKey = PropertyName.fromString(GETContentType.sname())
+# md5key = PropertyName.fromString(TwistedGETContentMD5.sname())
+
+class Attachment(object):
+    """
+    
+    """
+
+    implements(IAttachment)
+
+    def __init__(self, calendarObject, name):
+        self._calendarObject = calendarObject
+        self._name = name
+
+
+    def name(self):
+        return self._name
+
+
+    def _properties(self):
+        # Not exposed 
+        return PropertyStore(self._computePath())
+
+
+    def contentType(self):
+        return self._properties()[contentTypeKey].children[0]
+
+
+    def store(self, contentType):
+        ast = AttachmentStorageTransport(self)
+        props = self._properties()
+        props[contentTypeKey] = GETContentType(contentType)
+        props.flush()
+        return ast
+
+
+    def retrieve(self, protocol):
+        # FIXME: makeConnection
+        # FIXME: actually stream
+        protocol.dataReceived(self._computePath().getContent())
+        # FIXME: ConnectionDone
+        protocol.connectionLost(Failure(NotImplementedError()))
+
+
+    def md5(self):
+        return hashlib.md5(self._computePath().getContent()).hexdigest()
+
+
+    def _computePath(self):
+        dropboxPath = self._calendarObject._dropboxPath()
+        return dropboxPath.child(self.name())
+
+
+
 class CalendarStubResource(object):
     """
     Just enough resource to keep the calendar's sql DB classes going.

Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/test/common.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/test/common.py	2010-06-30 22:39:29 UTC (rev 5822)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/test/common.py	2010-06-30 22:42:56 UTC (rev 5823)
@@ -14,6 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from twisted.internet.defer import Deferred, inlineCallbacks
+from twisted.internet.protocol import Protocol
 """
 Tests for common calendar store API functions.
 """
@@ -34,7 +36,7 @@
 
 from txcaldav.icalendarstore import (
     ICalendarObject, ICalendarHome,
-    ICalendar)
+    ICalendar, IAttachment)
 
 from twext.python.filepath import CachingFilePath as FilePath
 from twext.web2.dav import davxml
@@ -204,6 +206,11 @@
         self.lastTransaction = None
 
 
+    def tearDown(self):
+        if self.lastTransaction is not None:
+            self.commit()
+
+
     def homeUnderTest(self):
         """
         Get the calendar home detailed by C{requirements['home1']}.
@@ -720,7 +727,7 @@
         self.assertProvides(ICalendarHome, calendarHome)
         # A concurrent transaction shouldn't be able to read it yet:
         self.assertIdentical(readOtherTxn(), None)
-        txn.commit()
+        self.commit()
         # But once it's committed, other transactions should see it.
         self.assertProvides(ICalendarHome, readOtherTxn())
 
@@ -799,7 +806,7 @@
         propertyContent = WebDAVUnknownElement("sample content")
         propertyContent.name = propertyName.name
         propertyContent.namespace = propertyName.namespace
-        
+
         self.calendarObjectUnderTest().properties()[
             propertyName] = propertyContent
         self.commit()
@@ -828,3 +835,144 @@
         )
 
 
+    eventWithDropbox = "\r\n".join("""
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART;TZID=US/Eastern:20060101T100000
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:user01
+ATTENDEE;PARTSTAT=ACCEPTED:user01
+ATTACH;VALUE=URI:/calendars/users/home1/some-dropbox-id/some-dropbox-id/caldavd.plist
+X-APPLE-DROPBOX:/calendars/users/home1/dropbox/some-dropbox-id
+END:VEVENT
+END:VCALENDAR
+    """.strip().split("\n"))
+
+    def test_dropboxID(self):
+        """
+        L{ICalendarObject.dropboxID} should synthesize its dropbox from the X
+        -APPLE-DROPBOX property.
+        """
+        cal = self.calendarUnderTest()
+        cal.createCalendarObjectWithName("drop.ics", VComponent.fromString(
+                self.eventWithDropbox
+            )
+        )
+        obj = cal.calendarObjectWithName("drop.ics")
+        self.assertEquals(obj.dropboxID(), "some-dropbox-id")
+
+
+    def test_indexByDropboxProperty(self):
+        """
+        L{ICalendarHome.calendarObjectWithDropboxID} will return a calendar
+        object in the calendar home with the given final segment in its C{X
+        -APPLE-DROPBOX} property URI.
+        """
+        objName = "with-dropbox.ics"
+        cal = self.calendarUnderTest()
+        cal.createCalendarObjectWithName(
+            objName, VComponent.fromString(
+                self.eventWithDropbox
+            )
+        )
+        self.commit()
+        home = self.homeUnderTest()
+        cal = self.calendarUnderTest()
+        fromName = cal.calendarObjectWithName(objName)
+        fromDropbox = home.calendarObjectWithDropboxID("some-dropbox-id")
+        self.assertEquals(fromName, fromDropbox)
+
+
+    @inlineCallbacks
+    def createAttachmentTest(self, refresh):
+        """
+        Common logic for attachment-creation tests.
+        """
+        obj = self.calendarObjectUnderTest()
+        t = obj.createAttachmentWithName("new.attachment", "text/x-fixture")
+        t.write("new attachment")
+        t.write(" text")
+        t.loseConnection()
+        obj = refresh(obj)
+        class CaptureProtocol(Protocol):
+            buf = ''
+            def dataReceived(self, data):
+                self.buf += data
+            def connectionLost(self, reason):
+                self.deferred.callback(self.buf)
+        capture = CaptureProtocol()
+        capture.deferred = Deferred()
+        attachment = obj.attachmentWithName("new.attachment")
+        self.assertProvides(IAttachment, attachment)
+        attachment.retrieve(capture)
+        data = yield capture.deferred
+        self.assertEquals(data, "new attachment text")
+        self.assertEquals(attachment.contentType(), "text/x-fixture")
+        self.assertEquals(attachment.md5(), '50a9f27aeed9247a0833f30a631f1858')
+        self.assertEquals(
+            [attachment.name() for attachment in obj.attachments()],
+            ['new.attachment']
+        )
+
+
+    def test_createAttachment(self):
+        """
+        L{ICalendarObject.createAttachmentWithName} will store an
+        L{IAttachment} object that can be retrieved by
+        L{ICalendarObject.attachmentWithName}.
+        """
+        return self.createAttachmentTest(lambda x: x)
+
+
+    def test_createAttachmentCommit(self):
+        """
+        L{ICalendarObject.createAttachmentWithName} will store an
+        L{IAttachment} object that can be retrieved by
+        L{ICalendarObject.attachmentWithName} in subsequent transactions.
+        """
+        def refresh(obj):
+            self.commit()
+            return self.calendarObjectUnderTest()
+        return self.createAttachmentTest(refresh)
+
+
+    def test_noDropboxCalendar(self):
+        """
+        L{ICalendarObject.createAttachmentWithName} may create a directory
+        named 'dropbox', but this should not be seen as a calendar by
+        L{ICalendarHome.calendarWithName}.
+        """
+        obj = self.calendarObjectUnderTest()
+        t = obj.createAttachmentWithName("new.attachment", "text/plain")
+        t.write("new attachment text")
+        t.loseConnection()
+        self.commit()
+        self.assertEquals(self.homeUnderTest().calendarWithName("dropbox"),
+                          None)
+
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100630/f6c63ba0/attachment-0001.html>


More information about the calendarserver-changes mailing list