Revision: 6463 http://trac.macosforge.org/projects/calendarserver/changeset/6463 Author: cdaboo@apple.com Date: 2010-10-25 09:48:50 -0700 (Mon, 25 Oct 2010) Log Message: ----------- Fixing up some missing yields. Re-worked object resource meta-data (md5, created, modified etc) to pre-cache from the DB when read, to avoid having to use Deferred's to read them later. This allowed a bunch of yield's to be removed. API for object resource creation changed to add an initFromStore method that handles all the meta-data pre-caching (as well as the property pre-cache too). Modified Paths: -------------- CalendarServer/trunk/twext/web2/dav/resource.py CalendarServer/trunk/twext/web2/static.py CalendarServer/trunk/twext/web2/stream.py CalendarServer/trunk/twistedcaldav/extensions.py CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py CalendarServer/trunk/twistedcaldav/method/put_common.py CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py CalendarServer/trunk/twistedcaldav/resource.py CalendarServer/trunk/twistedcaldav/storebridge.py CalendarServer/trunk/txdav/base/datastore/file.py CalendarServer/trunk/txdav/base/datastore/sql.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/carddav/datastore/file.py CalendarServer/trunk/txdav/carddav/datastore/sql.py CalendarServer/trunk/txdav/carddav/datastore/test/common.py CalendarServer/trunk/txdav/common/datastore/file.py CalendarServer/trunk/txdav/common/datastore/sql.py CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql CalendarServer/trunk/txdav/common/datastore/sql_tables.py Modified: CalendarServer/trunk/twext/web2/dav/resource.py =================================================================== --- CalendarServer/trunk/twext/web2/dav/resource.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/twext/web2/dav/resource.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -231,7 +231,7 @@ returnValue(davxml.ResourceType.empty) #@UndefinedVariable if name == "getetag": - etag = yield self.etag() + etag = self.etag() if etag is None: returnValue(None) returnValue(davxml.GETETag(etag.generate())) @@ -253,13 +253,13 @@ returnValue(davxml.GETContentLength(str(length))) if name == "getlastmodified": - lastModified = yield self.lastModified() + lastModified = self.lastModified() if lastModified is None: returnValue(None) returnValue(davxml.GETLastModified.fromDate(lastModified)) if name == "creationdate": - creationDate = yield self.creationDate() + creationDate = self.creationDate() if creationDate is None: returnValue(None) returnValue(davxml.CreationDate.fromDate(creationDate)) Modified: CalendarServer/trunk/twext/web2/static.py =================================================================== --- CalendarServer/trunk/twext/web2/static.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/twext/web2/static.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -105,8 +105,8 @@ http.checkPreconditions( request, entityExists = self.exists(), - etag = (yield self.etag()), - lastModified = (yield self.lastModified()), + etag = self.etag(), + lastModified = self.lastModified(), ) # Check per-method preconditions @@ -135,8 +135,8 @@ # (necessarily) to the resource content, so they depend on the # request method, and therefore can't be set here. for (header, value) in ( - ("etag", (yield self.etag())), - ("last-modified", (yield self.lastModified())), + ("etag", self.etag()), + ("last-modified", self.lastModified()), ): if value is not None: response.headers.setHeader(header, value) Modified: CalendarServer/trunk/twext/web2/stream.py =================================================================== --- CalendarServer/trunk/twext/web2/stream.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/twext/web2/stream.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -654,7 +654,7 @@ return defer.fail(f) return None else: - deferred = self.deferred = Deferred() + deferred = self.deferred = Deferred(noInlineCallbackDebugging=True) if self.producer is not None and (not self.streamingProducer or self.producerPaused): self.producerPaused = False Modified: CalendarServer/trunk/twistedcaldav/extensions.py =================================================================== --- CalendarServer/trunk/twistedcaldav/extensions.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/twistedcaldav/extensions.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -522,7 +522,7 @@ even = Alternator() for name in sorted((yield self.listChildren())): - child = self.getChild(name) + child = (yield maybeDeferred(self.getChild, name)) url, name, size, lastModified, contentType = self.getChildDirectoryEntry(child, name, request) Modified: CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py =================================================================== --- CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -33,8 +33,6 @@ from twext.web2.dav import davxml from twext.web2.dav.element.base import dav_namespace from twext.web2.dav.http import ErrorResponse -from twext.web2.dav.resource import TwistedGETContentMD5 -from twext.web2.dav.stream import MD5StreamWrapper from twext.web2.dav.util import joinURL, parentForURL from twext.web2.http import HTTPError from twext.web2.http import StatusResponse @@ -376,14 +374,9 @@ if self.vcarddata is None: self.vcarddata = str(self.vcard) - md5 = MD5StreamWrapper(MemoryStream(self.vcarddata)) - response = (yield self.destination.storeStream(md5)) + stream = MemoryStream(self.vcarddata) + response = (yield self.destination.storeStream(stream)) - # Finish MD5 calculation and write dead property - md5.close() - md5 = md5.getMD5() - self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5)) - returnValue(response) @inlineCallbacks Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py =================================================================== --- CalendarServer/trunk/twistedcaldav/method/put_common.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -35,8 +35,6 @@ from twext.web2.dav.element.base import dav_namespace from twext.web2.dav.element.base import PCDATAElement -from twext.web2.dav.resource import TwistedGETContentMD5 -from twext.web2.dav.stream import MD5StreamWrapper from twext.web2.http import HTTPError from twext.web2.http import StatusResponse from twext.web2.http_headers import generateContentType, MimeType @@ -769,14 +767,9 @@ if self.calendardata is None: self.calendardata = str(self.calendar) - md5 = MD5StreamWrapper(MemoryStream(self.calendardata)) - response = yield self.destination.storeStream(md5) + stream = MemoryStream(self.calendardata) + response = yield self.destination.storeStream(stream) - # Finish MD5 calculation and write dead property - md5.close() - md5 = md5.getMD5() - self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5)) - returnValue(response) @inlineCallbacks @@ -937,7 +930,7 @@ etags = self.destination.readDeadProperty(TwistedScheduleMatchETags).children else: etags = () - etags += (davxml.GETETag.fromString((yield self.destination.etag()).tag),) + etags += (davxml.GETETag.fromString(self.destination.etag().tag),) self.destination.writeDeadProperty(TwistedScheduleMatchETags(*etags)) else: self.destination.removeDeadProperty(TwistedScheduleMatchETags) Modified: CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py =================================================================== --- CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/twistedcaldav/method/report_multiget_common.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -31,7 +31,7 @@ from twext.web2.dav.util import joinURL from twext.web2.http import HTTPError, StatusResponse -from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred from twistedcaldav import carddavxml from twistedcaldav.caldavxml import caldav_namespace @@ -171,7 +171,8 @@ for href in resources: resource_uri = str(href) name = unquote(resource_uri[resource_uri.rfind("/") + 1:]) - if not self._isChildURI(request, resource_uri) or self.getChild(name) is None: + child = (yield maybeDeferred(self.getChild, name)) + if not self._isChildURI(request, resource_uri) or child is None or not child.exists(): responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND))) else: valid_names.append(name) Modified: CalendarServer/trunk/twistedcaldav/resource.py =================================================================== --- CalendarServer/trunk/twistedcaldav/resource.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/twistedcaldav/resource.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -40,8 +40,7 @@ from twext.web2.dav.davxml import SyncCollection from twext.web2.dav.http import ErrorResponse -from twisted.internet import reactor -from twisted.internet.defer import Deferred, succeed, maybeDeferred, fail +from twisted.internet.defer import succeed, maybeDeferred, fail from twisted.internet.defer import inlineCallbacks, returnValue from twext.web2 import responsecode, http, http_headers @@ -971,61 +970,6 @@ def findAddressBookCollections(self, depth, request, callback, privileges=None): return self.findSpecialCollections(carddavxml.AddressBook, depth, request, callback, privileges) - def findSpecialCollections(self, type, depth, request, callback, privileges=None): - assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,) - - def checkPrivilegesError(failure): - failure.trap(AccessDeniedError) - - reactor.callLater(0, getChild) - - def checkPrivileges(child): - if privileges is None: - return child - - ca = child.checkPrivileges(request, privileges) - ca.addCallback(lambda ign: child) - return ca - - def gotChild(child, childpath): - if child.isSpecialCollection(type): - callback(child, childpath) - - # No more regular collections - #elif child.isCollection(): - # if depth == "infinity": - # fc = child.findSpecialCollections(type, depth, request, callback, privileges) - # fc.addCallback(lambda x: reactor.callLater(0, getChild)) - # return fc - - reactor.callLater(0, getChild) - - def getChild(): - try: - childname = children.pop() - except IndexError: - completionDeferred.callback(None) - else: - childpath = joinURL(basepath, childname) - child = request.locateResource(childpath) - child.addCallback(checkPrivileges) - child.addCallbacks(gotChild, checkPrivilegesError, (childpath,)) - child.addErrback(completionDeferred.errback) - - completionDeferred = Deferred() - - if depth != "0" and self.isCollection(): - basepath = request.urlForResource(self) - children = [] - def gotChildren(childNames): - children[:] = list(childNames) - getChild() - maybeDeferred(self.listChildren).addCallback(gotChildren) - else: - completionDeferred.callback(None) - - return completionDeferred - @inlineCallbacks def findSpecialCollectionsFaster(self, type, depth, request, callback, privileges=None): assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,) @@ -1037,7 +981,7 @@ child = (yield request.locateResource(childpath)) if privileges: try: - child.checkPrivileges(request, privileges) + yield child.checkPrivileges(request, privileges) except AccessDeniedError: continue if child.isSpecialCollection(type): @@ -1046,6 +990,8 @@ if depth == "infinity": yield child.findSpecialCollectionsFaster(type, depth, request, callback, privileges) + findSpecialCollections = findSpecialCollectionsFaster + def createdCalendar(self, request): """ See L{ICalDAVResource.createCalendar}. @@ -1118,7 +1064,6 @@ returnValue(False) - @inlineCallbacks def iCalendarForUser(self, request, name=None): if name is not None: Modified: CalendarServer/trunk/twistedcaldav/storebridge.py =================================================================== --- CalendarServer/trunk/twistedcaldav/storebridge.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/twistedcaldav/storebridge.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -20,8 +20,6 @@ L{txdav.carddav.iaddressbookstore} and those in L{twistedcaldav}. """ -import hashlib - from urlparse import urlsplit from twisted.internet.defer import succeed, inlineCallbacks, returnValue,\ @@ -56,7 +54,6 @@ from twistedcaldav.scheduling.implicit import ImplicitScheduler from twistedcaldav.vcard import Component as VCard -from txdav.common.icommondatastore import NoSuchObjectResourceError from txdav.base.propertystore.base import PropertyName log = Logger() @@ -162,32 +159,10 @@ return self._newStoreObject.name() if self._newStoreObject is not None else None - @inlineCallbacks def etag(self): - # FIXME: far too slow to be used for real, but I needed something to - # placate the etag computation in the case where the file doesn't exist - # yet (an uncommitted transaction creating this calendar file) + return ETag(self._newStoreObject.md5()) if self._newStoreObject is not None else None - if self._newStoreObject is None: - returnValue(None) - # FIXME: direct tests - try: - md5 = yield self._newStoreObject.md5() - if md5: - returnValue(ETag(md5)) - else: - returnValue(ETag( - hashlib.new("md5", (yield self.text())).hexdigest(), - weak=False - )) - except NoSuchObjectResourceError: - # FIXME: a workaround for the fact that DELETE still rudely vanishes - # the calendar object out from underneath the store, and doesn't - # call storeRemove. - returnValue(None) - - def contentType(self): return self._newStoreObject.contentType() if self._newStoreObject is not None else None @@ -342,9 +317,8 @@ return self._newStoreCalendar.name() - @inlineCallbacks def etag(self): - returnValue(ETag((yield self._newStoreCalendar.md5()))) + return ETag(self._newStoreCalendar.md5()) def lastModified(self): @@ -552,7 +526,7 @@ @inlineCallbacks def listChildren(self): l = [] - for attachment in (self._newStoreCalendarObject.attachments()): + for attachment in (yield self._newStoreCalendarObject.attachments()): l.append(attachment.name()) returnValue(l) @@ -670,11 +644,9 @@ self._newStoreAttachment = self._newStoreObject = attachment - @inlineCallbacks def etag(self): # FIXME: test - md5 = yield self._newStoreAttachment.md5() - returnValue(ETag(md5)) + return ETag(self._newStoreAttachment.md5()) def contentType(self): @@ -758,9 +730,8 @@ return self._newStoreCalendar.name() - @inlineCallbacks def etag(self): - returnValue(ETag((yield self._newStoreCalendar.md5()))) + return ETag(self._newStoreCalendar.md5()) def lastModified(self): @@ -1480,9 +1451,8 @@ return self._newStoreAddressBook.name() - @inlineCallbacks def etag(self): - returnValue(ETag((yield self._newStoreAddressBook.md5()))) + return ETag(self._newStoreAddressBook.md5()) def lastModified(self): @@ -2140,29 +2110,9 @@ return True - @inlineCallbacks def etag(self): - # FIXME: far too slow to be used for real, but I needed something to - # placate the etag computation in the case where the file doesn't exist - # yet (an uncommited transaction creating this calendar file) + return ETag(self._newStoreObject.md5()) - # FIXME: direct tests - try: - md5 = yield self._newStoreObject.md5() - if md5: - returnValue(ETag(md5)) - else: - returnValue(ETag( - hashlib.new("md5", (yield self.text())).hexdigest(), - weak=False - )) - except NoSuchObjectResourceError: - # FIXME: a workaround for the fact that DELETE still rudely vanishes - # the calendar object out from underneath the store, and doesn't - # call storeRemove. - returnValue(None) - - def contentType(self): return self._newStoreObject.contentType() Modified: CalendarServer/trunk/txdav/base/datastore/file.py =================================================================== --- CalendarServer/trunk/txdav/base/datastore/file.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/base/datastore/file.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -28,8 +28,8 @@ from twext.web2.dav.element.rfc2518 import GETContentType from twext.web2.dav.resource import TwistedGETContentMD5 +from twisted.python import hashlib - from zope.interface.declarations import implements def isValidName(name): @@ -223,7 +223,15 @@ try: return str(self.properties()[PropertyName.fromElement(TwistedGETContentMD5)]) except KeyError: - return None + # FIXME: Strictly speaking we should not need to read the data as the md5 property should always be + # present. However, our unit tests use static files for their data store and those currently + # do not include the md5 xattr. + try: + data = self._path.open().read() + except IOError: + return None + md5 = hashlib.md5(data).hexdigest() + return md5 def size(self): """ Modified: CalendarServer/trunk/txdav/base/datastore/sql.py =================================================================== --- CalendarServer/trunk/txdav/base/datastore/sql.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/base/datastore/sql.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -18,7 +18,7 @@ Logic common to SQL implementations. """ -from twisted.internet.defer import Deferred +from twisted.internet.defer import Deferred, succeed from inspect import getargspec def _getarg(argname, argspec, args, kw): @@ -54,19 +54,21 @@ -def memoized(keyArgument, memoAttribute): +def memoized(keyArgument, memoAttribute, deferredResult=True): """ Decorator which memoizes the result of a method on that method's instance. @param keyArgument: The name of the 'key' argument. - @type keyArgument: C{str} @param memoAttribute: The name of the attribute on the instance which should be used for memoizing the result of this method; the attribute itself must be a dictionary. - @type memoAttribute: C{str} + + @param deferredResult: Whether the result must be a deferred. + @type keyArgument: C{bool} + """ def decorate(thunk): # cheater move to try to get the right argspec from inlineCallbacks. @@ -83,25 +85,17 @@ memo = getattr(self, memoAttribute) key = _getarg(keyArgument, spec, a, kw) if key in memo: - result = memo[key] - else: - result = thunk(*a, **kw) - if result is not None: - memo[key] = result + memoed = memo[key] + return succeed(memoed) if deferredResult else memoed + result = thunk(*a, **kw) if isinstance(result, Deferred): - # clone the Deferred so that the old one keeps its result. - # FIXME: cancellation? - returnResult = Deferred() - def relayAndPreserve(innerResult): - if innerResult is None and key in memo and memo[key] is result: - # The result was None, call it again. - del memo[key] - returnResult.callback(innerResult) - return innerResult - result.addBoth(relayAndPreserve) - return returnResult + def memoResult(finalResult): + if finalResult is not None: + memo[key] = finalResult + return finalResult + result.addCallback(memoResult) + elif result is not None: + memo[key] = result return result return outer return decorate - - Modified: CalendarServer/trunk/txdav/caldav/datastore/file.py =================================================================== --- CalendarServer/trunk/txdav/caldav/datastore/file.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/caldav/datastore/file.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -69,6 +69,9 @@ from zope.interface import implements +contentTypeKey = PropertyName.fromElement(GETContentType) +md5key = PropertyName.fromElement(TwistedGETContentMD5) + CalendarStore = CommonDataStore CalendarStoreTransaction = CommonStoreTransaction @@ -250,14 +253,19 @@ if self._path.exists(): backup = hidden(self._path.temporarySibling()) self._path.moveTo(backup) + + componentText = str(component) fh = self._path.open("w") try: # FIXME: concurrency problem; if this write is interrupted # halfway through, the underlying file will be corrupt. - fh.write(str(component)) + fh.write(componentText) finally: fh.close() + md5 = hashlib.md5(componentText).hexdigest() + self.properties()[md5key] = TwistedGETContentMD5.fromString(md5) + # Now re-write the original properties on the updated file self.properties().flush() @@ -427,9 +435,6 @@ -contentTypeKey = PropertyName.fromElement(GETContentType) -md5key = PropertyName.fromElement(TwistedGETContentMD5) - class AttachmentStorageTransport(object): implements(ITransport) Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py =================================================================== --- CalendarServer/trunk/txdav/caldav/datastore/sql.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/caldav/datastore/sql.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -227,8 +227,12 @@ class CalendarObject(CommonObjectResource): implements(ICalendarObject) - _objectTable = CALENDAR_OBJECT_TABLE + def __init__(self, calendar, name, uid): + super(CalendarObject, self).__init__(calendar, name, uid) + self._objectTable = CALENDAR_OBJECT_TABLE + + @property def _calendar(self): return self._parentCollection @@ -294,14 +298,20 @@ organizer = "" # CALENDAR_OBJECT table update + self._md5 = hashlib.md5(componentText).hexdigest() + self._size = len(componentText) if inserting: - self._resourceID = (yield self._txn.execSQL( + self._resourceID, self._created, self._modified = ( + yield self._txn.execSQL( """ insert into CALENDAR_OBJECT - (CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX) + (CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX, MD5) values - (%s, %s, %s, %s, %s, %s, %s, %s) - returning RESOURCE_ID + (%s, %s, %s, %s, %s, %s, %s, %s, %s) + returning + RESOURCE_ID, + CREATED, + MODIFIED """, # FIXME: correct ATTACHMENTS_MODE based on X-APPLE- # DROPBOX @@ -314,16 +324,18 @@ _ATTACHMENTS_MODE_WRITE, organizer, normalizeForIndex(instances.limit) if instances.limit else None, + self._md5, ] - ))[0][0] + ))[0] else: yield self._txn.execSQL( """ update CALENDAR_OBJECT set - (ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX, MODIFIED) + (ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX, MD5, MODIFIED) = - (%s, %s, %s, %s, %s, %s, timezone('UTC', CURRENT_TIMESTAMP)) - where RESOURCE_ID = %s + (%s, %s, %s, %s, %s, %s, %s, timezone('UTC', CURRENT_TIMESTAMP)) + where RESOURCE_ID = %s + returning MODIFIED """, # should really be filling out more fields: ORGANIZER, # ORGANIZER_OBJECT, a correct ATTACHMENTS_MODE based on X-APPLE- @@ -335,7 +347,8 @@ _ATTACHMENTS_MODE_WRITE, organizer, normalizeForIndex(instances.limit) if instances.limit else None, - self._resourceID + self._md5, + self._resourceID, ] ) @@ -485,12 +498,9 @@ @inlineCallbacks def attachmentWithName(self, name): attachment = Attachment(self, name) - if (yield attachment._populate()): - returnValue(attachment) - else: - returnValue(None) + attachment = (yield attachment.initFromStore()) + returnValue(attachment) - @inlineCallbacks def attendeesCanManageAttachments(self): returnValue((yield self.component()).hasPropertyInAnyComponent( @@ -569,15 +579,23 @@ @inlineCallbacks def loseConnection(self): self.attachment._path.setContent(self.buf) - contentTypeString = generateContentType(self.contentType) - yield self._txn.execSQL( + self.attachment._contentType = self.contentType + self.attachment._md5 = self.hash.hexdigest() + self.attachment._size = len(self.buf) + self.attachment._created, self.attachment._modified = (yield self._txn.execSQL( """ update ATTACHMENT set CONTENT_TYPE = %s, SIZE = %s, MD5 = %s, - MODIFIED = timezone('UTC', CURRENT_TIMESTAMP) WHERE PATH = %s + MODIFIED = timezone('UTC', CURRENT_TIMESTAMP) + where PATH = %s + returning CREATED, MODIFIED """, - [contentTypeString, len(self.buf), - self.hash.hexdigest(), self.attachment.name()] - ) + [ + generateContentType(self.contentType), + self.attachment._size, + self.attachment._md5, + self.attachment.name() + ] + ))[0] @@ -596,7 +614,7 @@ @inlineCallbacks - def _populate(self): + def initFromStore(self): """ Execute necessary SQL queries to retrieve attributes. @@ -609,13 +627,13 @@ [self._name] ) if not rows: - returnValue(False) + returnValue(None) self._contentType = MimeType.fromString(rows[0][0]) self._size = rows[0][1] self._md5 = rows[0][2] self._created = datetimeMktime(datetime.datetime.strptime(rows[0][3], "%Y-%m-%d %H:%M:%S.%f")) self._modified = datetimeMktime(datetime.datetime.strptime(rows[0][4], "%Y-%m-%d %H:%M:%S.%f")) - returnValue(True) + returnValue(self) def name(self): Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py =================================================================== --- CalendarServer/trunk/txdav/caldav/datastore/test/common.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -295,13 +295,16 @@ @inlineCallbacks - def test_notificationObjectModified(self): + def test_notificationObjectMetaData(self): """ - The objects retrieved from the notification home have a C{modified} - method which returns the timestamp of their last modification. + The objects retrieved from the notification home have various + methods which return metadata values. """ notification = yield self.notificationUnderTest() - self.assertIsInstance((yield notification.modified()), int) + self.assertIsInstance(notification.md5(), basestring) + self.assertIsInstance(notification.size(), int) + self.assertIsInstance(notification.created(), int) + self.assertIsInstance(notification.modified(), int) @inlineCallbacks @@ -650,6 +653,21 @@ @inlineCallbacks + def test_calendarObjectMetaData(self): + """ + The objects retrieved from the calendar have a variou + methods which return metadata values. + """ + calendar = yield self.calendarObjectUnderTest() + self.assertIsInstance(calendar.name(), basestring) + self.assertIsInstance(calendar.uid(), basestring) + self.assertIsInstance(calendar.md5(), basestring) + self.assertIsInstance(calendar.size(), int) + self.assertIsInstance(calendar.created(), int) + self.assertIsInstance(calendar.modified(), int) + + + @inlineCallbacks def test_component(self): """ L{ICalendarObject.component} returns a L{VComponent} describing the Modified: CalendarServer/trunk/txdav/carddav/datastore/file.py =================================================================== --- CalendarServer/trunk/txdav/carddav/datastore/file.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/carddav/datastore/file.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -29,8 +29,11 @@ from errno import ENOENT -from twext.web2.dav.element.rfc2518 import ResourceType +from twext.web2.dav.element.rfc2518 import ResourceType, GETContentType +from twext.web2.dav.resource import TwistedGETContentMD5 +from twisted.python import hashlib + from twistedcaldav.sharing import InvitesDatabase from twistedcaldav.vcard import Component as VComponent, InvalidVCardDataError from txdav.carddav.datastore.index_file import AddressBookIndex as OldIndex @@ -50,6 +53,9 @@ from zope.interface import implements +contentTypeKey = PropertyName.fromElement(GETContentType) +md5key = PropertyName.fromElement(TwistedGETContentMD5) + AddressBookStore = CommonDataStore AddressBookStoreTransaction = CommonStoreTransaction @@ -177,14 +183,19 @@ if self._path.exists(): backup = hidden(self._path.temporarySibling()) self._path.moveTo(backup) + + componentText = str(component) fh = self._path.open("w") try: # FIXME: concurrency problem; if this write is interrupted # halfway through, the underlying file will be corrupt. - fh.write(str(component)) + fh.write(componentText) finally: fh.close() + md5 = hashlib.md5(componentText).hexdigest() + self.properties()[md5key] = TwistedGETContentMD5.fromString(md5) + # Now re-write the original properties on the updated file self.properties().flush() Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py =================================================================== --- CalendarServer/trunk/txdav/carddav/datastore/sql.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/carddav/datastore/sql.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -28,6 +28,7 @@ from zope.interface.declarations import implements from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.python import hashlib from twext.web2.dav.element.rfc2518 import ResourceType from twext.web2.http_headers import MimeType @@ -160,9 +161,9 @@ implements(IAddressBookObject) - def __init__(self, name, addressbook, resid, uid): + def __init__(self, addressbook, name, uid): - super(AddressBookObject, self).__init__(name, addressbook, resid, uid) + super(AddressBookObject, self).__init__(addressbook, name, uid) self._objectTable = ADDRESSBOOK_OBJECT_TABLE @@ -201,35 +202,44 @@ self._objectText = componentText # ADDRESSBOOK_OBJECT table update + self._md5 = hashlib.md5(componentText).hexdigest() + self._size = len(componentText) if inserting: - self._resourceID = (yield self._txn.execSQL( + self._resourceID, self._created, self._modified = ( + yield self._txn.execSQL( """ insert into ADDRESSBOOK_OBJECT - (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, VCARD_TEXT, VCARD_UID) + (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, VCARD_TEXT, VCARD_UID, MD5) values - (%s, %s, %s, %s) - returning RESOURCE_ID + (%s, %s, %s, %s, %s) + returning + RESOURCE_ID, + CREATED, + MODIFIED """, [ self._addressbook._resourceID, self._name, componentText, component.resourceUID(), + self._md5, ] - ))[0][0] + ))[0] else: yield self._txn.execSQL( """ update ADDRESSBOOK_OBJECT set - (VCARD_TEXT, VCARD_UID, MODIFIED) + (VCARD_TEXT, VCARD_UID, MD5, MODIFIED) = - (%s, %s, timezone('UTC', CURRENT_TIMESTAMP)) - where RESOURCE_ID = %s + (%s, %s, %s, timezone('UTC', CURRENT_TIMESTAMP)) + where RESOURCE_ID = %s + returning MODIFIED """, [ componentText, component.resourceUID(), - self._resourceID + self._md5, + self._resourceID, ] ) Modified: CalendarServer/trunk/txdav/carddav/datastore/test/common.py =================================================================== --- CalendarServer/trunk/txdav/carddav/datastore/test/common.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/carddav/datastore/test/common.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -538,6 +538,21 @@ @inlineCallbacks + def test_addressbookObjectMetaData(self): + """ + The objects retrieved from the addressbook have various + methods which return metadata values. + """ + adbk = yield self.addressbookObjectUnderTest() + self.assertIsInstance(adbk.name(), basestring) + self.assertIsInstance(adbk.uid(), basestring) + self.assertIsInstance(adbk.md5(), basestring) + self.assertIsInstance(adbk.size(), int) + self.assertIsInstance(adbk.created(), int) + self.assertIsInstance(adbk.modified(), int) + + + @inlineCallbacks def test_component(self): """ L{IAddressBookObject.component} returns a L{VComponent} describing the Modified: CalendarServer/trunk/txdav/common/datastore/file.py =================================================================== --- CalendarServer/trunk/txdav/common/datastore/file.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/common/datastore/file.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -23,8 +23,10 @@ from twext.web2.dav.element.rfc2518 import ResourceType, GETContentType, HRef from twext.web2.dav.element.rfc5842 import ResourceID from twext.web2.http_headers import generateContentType, MimeType +from twext.web2.dav.resource import TwistedGETContentMD5 from twisted.python.util import FancyEqMixin +from twisted.python import hashlib from twistedcaldav import customxml from twistedcaldav.customxml import NotificationType @@ -945,6 +947,12 @@ return self._parentCollection + def created(self): + if not self._path.exists(): + from twisted.internet import reactor + return int(reactor.seconds()) + return super(NotificationObject, self).created() + def modified(self): if not self._path.exists(): from twisted.internet import reactor @@ -961,6 +969,7 @@ ) self._xmldata = xmldata + md5 = hashlib.md5(xmldata).hexdigest() def do(): backup = None @@ -991,6 +1000,7 @@ props = self.properties() props[PropertyName(*GETContentType.qname())] = GETContentType.fromString(generateContentType(MimeType("text", "xml", params={"charset":"utf-8"}))) props[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype) + props[PropertyName.fromElement(TwistedGETContentMD5)] = TwistedGETContentMD5.fromString(md5) # FIXME: the property store's flush() method may already have been Modified: CalendarServer/trunk/txdav/common/datastore/sql.py =================================================================== --- CalendarServer/trunk/txdav/common/datastore/sql.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/common/datastore/sql.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -619,7 +619,7 @@ else: notifier = None child = self._childClass(self, name, resourceID, notifier) - yield child._loadPropertyStore() + yield child.initFromStore() returnValue(child) @@ -863,6 +863,8 @@ self._home = home self._name = name self._resourceID = resourceID + self._created = None + self._modified = None self._objects = {} self._notifier = notifier @@ -870,6 +872,21 @@ self._invites = None # Derived classes need to set this + @inlineCallbacks + def initFromStore(self): + """ + Initialise this object from the store. We read in and cache all the extra metadata + from the DB to avoid having to do DB queries for those individually later. + """ + + self._created, self._modified = (yield self._txn.execSQL( + "select %(column_CREATED)s, %(column_MODIFIED)s from %(name)s " + "where %(column_RESOURCE_ID)s = %%s" % self._homeChildTable, + [self._resourceID] + ))[0] + + yield self._loadPropertyStore() + @property def _txn(self): return self._home._txn @@ -940,48 +957,26 @@ @memoized('name', '_objects') - @inlineCallbacks def objectResourceWithName(self, name): - rows = yield self._txn.execSQL( - "select %(column_RESOURCE_ID)s, %(column_UID)s from %(name)s " - "where %(column_RESOURCE_NAME)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable, - [name, self._resourceID] - ) - if not rows: - returnValue(None) - [resid, uid] = rows[0] - returnValue((yield self._makeObjectResource(name, resid, uid))) + return self._makeObjectResource(name, None) + @memoized('uid', '_objects') + def objectResourceWithUID(self, uid): + return self._makeObjectResource(None, uid) + + @inlineCallbacks - def _makeObjectResource(self, name, resid, uid): + def _makeObjectResource(self, name, uid): """ - Create an instance of C{self._objectResourceClass}. + We create the empty object first then have it initialize itself from the store """ - objectResource = yield self._objectResourceClass( - name, self, resid, uid - ) - yield objectResource._loadPropertyStore() + objectResource = self._objectResourceClass(self, name, uid) + objectResource = (yield objectResource.initFromStore()) returnValue(objectResource) - @memoized('uid', '_objects') @inlineCallbacks - def objectResourceWithUID(self, uid): - rows = yield self._txn.execSQL( - "select %(column_RESOURCE_ID)s, %(column_RESOURCE_NAME)s " - "from %(name)s where %(column_UID)s = %%s " - "and %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable, - [uid, self._resourceID] - ) - if not rows: - returnValue(None) - resid = rows[0][0] - name = rows[0][1] - returnValue((yield self._makeObjectResource(name, resid, uid))) - - - @inlineCallbacks def createObjectResourceWithName(self, name, component): if name.startswith("."): raise ObjectResourceNameNotAllowedError(name) @@ -995,9 +990,7 @@ if rows: raise ObjectResourceNameAlreadyExistsError() - objectResource = ( - yield self._makeObjectResource(name, None, component.resourceUID()) - ) + objectResource = self._objectResourceClass(self, name, None) yield objectResource.setComponent(component, inserting=True) # Note: setComponent triggers a notification, so we don't need to @@ -1261,26 +1254,14 @@ return 0 - @inlineCallbacks def created(self): - created = (yield self._txn.execSQL( - "select %(column_CREATED)s from %(name)s " - "where %(column_RESOURCE_ID)s = %%s" % self._homeChildTable, - [self._resourceID] - ))[0][0] - utc = datetime.datetime.strptime(created, "%Y-%m-%d %H:%M:%S.%f") - returnValue(datetimeMktime(utc)) + utc = datetime.datetime.strptime(self._created, "%Y-%m-%d %H:%M:%S.%f") + return datetimeMktime(utc) - @inlineCallbacks def modified(self): - modified = (yield self._txn.execSQL( - "select %(column_MODIFIED)s from %(name)s " - "where %(column_RESOURCE_ID)s = %%s" % self._homeChildTable, - [self._resourceID] - ))[0][0] - utc = datetime.datetime.strptime(modified, "%Y-%m-%d %H:%M:%S.%f") - returnValue(datetimeMktime(utc)) + utc = datetime.datetime.strptime(self._modified, "%Y-%m-%d %H:%M:%S.%f") + return datetimeMktime(utc) def notifierID(self, label="default"): @@ -1310,15 +1291,73 @@ _objectTable = None - def __init__(self, name, parent, resid, uid): + def __init__(self, parent, name, uid): + self._parentCollection = parent + self._resourceID = None self._name = name - self._parentCollection = parent - self._resourceID = resid + self._uid = uid + self._md5 = None + self._size = None + self._created = None + self._modified = None self._objectText = None - self._uid = uid @inlineCallbacks + def initFromStore(self): + """ + Initialise this object from the store. We read in and cache all the extra metadata + from the DB to avoid having to do DB queries for those individually later. Either the + name or uid is present, so we have to tweak the query accordingly. + + @return: L{self} if object exists in the DB, else C{None} + """ + + if self._name: + rows = yield self._txn.execSQL(""" + select + %(column_RESOURCE_ID)s, + %(column_RESOURCE_NAME)s, + %(column_UID)s, + %(column_MD5)s, + character_length(%(column_TEXT)s), + %(column_CREATED)s, + %(column_MODIFIED)s + from %(name)s + where %(column_RESOURCE_NAME)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s + """ % self._objectTable, + [self._name, self._parentCollection._resourceID] + ) + else: + rows = yield self._txn.execSQL(""" + select + %(column_RESOURCE_ID)s, + %(column_RESOURCE_NAME)s, + %(column_UID)s, + %(column_MD5)s, + character_length(%(column_TEXT)s), + %(column_CREATED)s, + %(column_MODIFIED)s + from %(name)s + where %(column_UID)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s + """ % self._objectTable, + [self._uid, self._parentCollection._resourceID] + ) + if rows: + (self._resourceID, + self._name, + self._uid, + self._md5, + self._size, + self._created, + self._modified,) = tuple(rows[0]) + yield self._loadPropertyStore() + returnValue(self) + else: + returnValue(None) + + + @inlineCallbacks def _loadPropertyStore(self): props = yield PropertyStore.load( self.uid(), @@ -1378,39 +1417,21 @@ def md5(self): - return None + return self._md5 - @inlineCallbacks def size(self): - size = (yield self._txn.execSQL( - "select character_length(%(column_TEXT)s) from %(name)s " - "where %(column_RESOURCE_ID)s = %%s" % self._objectTable, - [self._resourceID] - ))[0][0] - returnValue(size) + return self._size - @inlineCallbacks def created(self): - created = (yield self._txn.execSQL( - "select %(column_CREATED)s from %(name)s " - "where %(column_RESOURCE_ID)s = %%s" % self._objectTable, - [self._resourceID] - ))[0][0] - utc = datetime.datetime.strptime(created, "%Y-%m-%d %H:%M:%S.%f") - returnValue(datetimeMktime(utc)) + utc = datetime.datetime.strptime(self._created, "%Y-%m-%d %H:%M:%S.%f") + return datetimeMktime(utc) - @inlineCallbacks def modified(self): - modified = (yield self._txn.execSQL( - "select %(column_MODIFIED)s from %(name)s " - "where %(column_RESOURCE_ID)s = %%s" % self._objectTable, - [self._resourceID] - ))[0][0] - utc = datetime.datetime.strptime(modified, "%Y-%m-%d %H:%M:%S.%f") - returnValue(datetimeMktime(utc)) + utc = datetime.datetime.strptime(self._modified, "%Y-%m-%d %H:%M:%S.%f") + return datetimeMktime(utc) @inlineCallbacks @@ -1502,17 +1523,13 @@ @memoized('uid', '_notifications') @inlineCallbacks def notificationObjectWithUID(self, uid): - rows = (yield self._txn.execSQL( - "select RESOURCE_ID from NOTIFICATION " - "where NOTIFICATION_UID = %s and NOTIFICATION_HOME_RESOURCE_ID = %s", - [uid, self._resourceID])) - if rows: - resourceID = rows[0][0] - no = NotificationObject(self, uid, resourceID) - yield no._loadPropertyStore() - returnValue(no) - else: - returnValue(None) + """ + We create the empty object first then have it initialize itself from the store + """ + + no = NotificationObject(self, uid) + no = (yield no.initFromStore()) + returnValue(no) @inlineCallbacks @@ -1521,7 +1538,7 @@ inserting = False notificationObject = yield self.notificationObjectWithUID(uid) if notificationObject is None: - notificationObject = NotificationObject(self, uid, None) + notificationObject = NotificationObject(self, uid) inserting = True yield notificationObject.setData(uid, xmltype, xmldata, inserting=inserting) if inserting: @@ -1693,15 +1710,47 @@ compareAttributes = '_resourceID _home'.split() - def __init__(self, home, uid, resourceID): + def __init__(self, home, uid): self._home = home self._uid = uid - self._resourceID = resourceID + self._resourceID = None + self._md5 = None + self._size = None + self._created = None + self._modified = None - def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self._resourceID) + @inlineCallbacks + def initFromStore(self): + """ + Initialise this object from the store. We read in and cache all the extra metadata + from the DB to avoid having to do DB queries for those individually later. + + @return: L{self} if object exists in the DB, else C{None} + """ + rows = (yield self._txn.execSQL(""" + select + RESOURCE_ID, + MD5, + character_length(XML_DATA), + CREATED, + MODIFIED + from NOTIFICATION + where NOTIFICATION_UID = %s and NOTIFICATION_HOME_RESOURCE_ID = %s + """, + [self._uid, self._home._resourceID])) + if rows: + (self._resourceID, + self._md5, + self._size, + self._created, + self._modified,) = tuple(rows[0]) + yield self._loadPropertyStore() + returnValue(self) + else: + returnValue(None) @property def _txn(self): @@ -1722,21 +1771,37 @@ @inlineCallbacks def setData(self, uid, xmltype, xmldata, inserting=False): + """ + Set the object resource data and update and cached metadata. + """ xmltypeString = xmltype.toxml() + self._md5 = hashlib.md5(xmldata).hexdigest() + self._size = len(xmldata) if inserting: - rows = yield self._txn.execSQL( - "insert into NOTIFICATION (NOTIFICATION_HOME_RESOURCE_ID, NOTIFICATION_UID, XML_TYPE, XML_DATA) " - "values (%s, %s, %s, %s) returning RESOURCE_ID", - [self._home._resourceID, uid, xmltypeString, xmldata] + rows = yield self._txn.execSQL(""" + insert into NOTIFICATION + (NOTIFICATION_HOME_RESOURCE_ID, NOTIFICATION_UID, XML_TYPE, XML_DATA, MD5) + values + (%s, %s, %s, %s, %s) + returning + RESOURCE_ID, + CREATED, + MODIFIED + """, + [self._home._resourceID, uid, xmltypeString, xmldata, self._md5] ) - self._resourceID = rows[0][0] + self._resourceID, self._created, self._modified = rows[0] yield self._loadPropertyStore() else: - yield self._txn.execSQL( - "update NOTIFICATION set XML_TYPE = %s, XML_DATA = %s " - "where NOTIFICATION_HOME_RESOURCE_ID = %s and NOTIFICATION_UID = %s", - [xmltypeString, xmldata, self._home._resourceID, uid]) + rows = yield self._txn.execSQL(""" + update NOTIFICATION + set XML_TYPE = %s, XML_DATA = %s, MD5 = %s + where NOTIFICATION_HOME_RESOURCE_ID = %s and NOTIFICATION_UID = %s + returning MODIFIED + """, + [xmltypeString, xmldata, self._md5, self._home._resourceID, uid]) + self._modified = rows[0][0] self.properties()[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype) @@ -1787,40 +1852,22 @@ return MimeType.fromString("text/xml") - @inlineCallbacks def md5(self): - returnValue(hashlib.md5((yield self.xmldata())).hexdigest()) + return self._md5 - @inlineCallbacks def size(self): - size = (yield self._txn.execSQL( - "select character_length(XML_DATA) from NOTIFICATION " - "where RESOURCE_ID = %s", - [self._resourceID] - ))[0][0] - returnValue(size) + return self._size - @inlineCallbacks def created(self): - created = (yield self._txn.execSQL( - "select CREATED from NOTIFICATION " - "where RESOURCE_ID = %s", - [self._resourceID] - ))[0][0] - utc = datetime.datetime.strptime(created, "%Y-%m-%d %H:%M:%S.%f") - returnValue(datetimeMktime(utc)) + utc = datetime.datetime.strptime(self._created, "%Y-%m-%d %H:%M:%S.%f") + return datetimeMktime(utc) - @inlineCallbacks def modified(self): - modified = (yield self._txn.execSQL( - "select MODIFIED from NOTIFICATION " - "where RESOURCE_ID = %s", [self._resourceID] - ))[0][0] - utc = datetime.datetime.strptime(modified, "%Y-%m-%d %H:%M:%S.%f") - returnValue(datetimeMktime(utc)) + utc = datetime.datetime.strptime(self._modified, "%Y-%m-%d %H:%M:%S.%f") + return datetimeMktime(utc) Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql =================================================================== --- CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql 2010-10-25 16:48:50 UTC (rev 6463) @@ -55,6 +55,7 @@ NOTIFICATION_UID varchar(255) not null, XML_TYPE varchar not null, XML_DATA varchar not null, + MD5 char(32) not null, CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP), MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP), @@ -126,6 +127,7 @@ ORGANIZER varchar(255), ORGANIZER_OBJECT integer references CALENDAR_OBJECT, RECURRANCE_MAX date, -- maximum date that recurrences have been expanded to. + MD5 char(32) not null, CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP), MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP), @@ -292,6 +294,7 @@ RESOURCE_NAME varchar(255) not null, VCARD_TEXT text not null, VCARD_UID varchar(255) not null, + MD5 char(32) not null, CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP), MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP), Modified: CalendarServer/trunk/txdav/common/datastore/sql_tables.py =================================================================== --- CalendarServer/trunk/txdav/common/datastore/sql_tables.py 2010-10-25 16:38:03 UTC (rev 6462) +++ CalendarServer/trunk/txdav/common/datastore/sql_tables.py 2010-10-25 16:48:50 UTC (rev 6463) @@ -113,6 +113,7 @@ "column_RESOURCE_NAME" : "RESOURCE_NAME", "column_TEXT" : "ICALENDAR_TEXT", "column_UID" : "ICALENDAR_UID", + "column_MD5" : "MD5", "column_CREATED" : "CREATED", "column_MODIFIED" : "MODIFIED", } @@ -124,6 +125,7 @@ "column_RESOURCE_NAME" : "RESOURCE_NAME", "column_TEXT" : "VCARD_TEXT", "column_UID" : "VCARD_UID", + "column_MD5" : "MD5", "column_CREATED" : "CREATED", "column_MODIFIED" : "MODIFIED", }