[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