[CalendarServer-changes] [5796] CalendarServer/branches/new-store
source_changes at macosforge.org
source_changes at macosforge.org
Tue Jun 22 12:35:26 PDT 2010
Revision: 5796
http://trac.macosforge.org/projects/calendarserver/changeset/5796
Author: cdaboo at apple.com
Date: 2010-06-22 12:35:25 -0700 (Tue, 22 Jun 2010)
Log Message:
-----------
Factor out common calendar/addressbook data store behaviors.
Modified Paths:
--------------
CalendarServer/branches/new-store/twistedcaldav/method/delete_common.py
CalendarServer/branches/new-store/twistedcaldav/method/put_addressbook_common.py
CalendarServer/branches/new-store/twistedcaldav/storebridge.py
CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
CalendarServer/branches/new-store/txcaldav/calendarstore/test/common.py
CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py
CalendarServer/branches/new-store/txcaldav/icalendarstore.py
CalendarServer/branches/new-store/txcarddav/addressbookstore/file.py
CalendarServer/branches/new-store/txcarddav/addressbookstore/test/common.py
CalendarServer/branches/new-store/txcarddav/addressbookstore/test/test_file.py
CalendarServer/branches/new-store/txcarddav/iaddressbookstore.py
CalendarServer/branches/new-store/txdav/propertystore/base.py
CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py
Added Paths:
-----------
CalendarServer/branches/new-store/txdav/common/
CalendarServer/branches/new-store/txdav/common/__init__.py
CalendarServer/branches/new-store/txdav/common/datastore/
CalendarServer/branches/new-store/txdav/common/datastore/__init__.py
CalendarServer/branches/new-store/txdav/common/datastore/file.py
CalendarServer/branches/new-store/txdav/common/icommondatastore.py
CalendarServer/branches/new-store/txdav/datastore/
CalendarServer/branches/new-store/txdav/datastore/__init__.py
CalendarServer/branches/new-store/txdav/datastore/file.py
Modified: CalendarServer/branches/new-store/twistedcaldav/method/delete_common.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/method/delete_common.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/twistedcaldav/method/delete_common.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -22,18 +22,9 @@
__all__ = ["DeleteResource"]
from twisted.internet.defer import inlineCallbacks, returnValue
-from twext.web2 import responsecode
-from twext.web2.dav.fileop import delete
-from twext.web2.dav.http import ResponseQueue, MultiStatusResponse
-from twext.web2.dav.util import joinURL
-from twext.web2.http import HTTPError, StatusResponse
from twext.python.log import Logger
-from twistedcaldav.memcachelock import MemcacheLockTimeoutError
-from twistedcaldav.method.report_common import applyToAddressBookCollections
-from twistedcaldav.resource import isAddressBookCollectionResource
-
log = Logger()
class DeleteResource(object):
@@ -51,160 +42,15 @@
@inlineCallbacks
- def deleteAddressBookResource(self, delresource, deluri, parent):
- """
- Delete a single addressbook resource and do implicit scheduling actions if required.
-
- @param delresource:
- @type delresource:
- @param deluri:
- @type deluri:
- @param parent:
- @type parent:
- """
-
- # TODO: need to use transaction based delete on live scheduling object resources
- # as the iTIP operation may fail and may need to prevent the delete from happening.
-
- # Do quota checks before we start deleting things
- myquota = (yield delresource.quota(self.request))
- if myquota is not None:
- old_size = (yield delresource.quotaSize(self.request))
- else:
- old_size = 0
-
- try:
-
- # Do delete
- response = (yield delete(deluri, delresource.fp, self.depth))
-
- # Adjust quota
- if myquota is not None:
- yield delresource.quotaSizeAdjust(self.request, -old_size)
-
- if response == responsecode.NO_CONTENT:
- newrevision = (yield parent.bumpSyncToken())
- index = parent.index()
- index.deleteResource(delresource.fp.basename(), newrevision)
-
- except MemcacheLockTimeoutError:
- raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use on the server." % (deluri,)))
-
-
- returnValue(response)
-
- @inlineCallbacks
- def deleteAddressBook(self, delresource, deluri, parent):
- """
- Delete an entire addressbook collection by deleting each child resource in turn to
- ensure that proper implicit scheduling actions occur.
-
- This has to emulate the behavior in fileop.delete in that any errors need to be
- reported back in a multistatus response.
- """
-
-
- if self.depth != "infinity":
- msg = "Client sent illegal depth header value for DELETE: %s" % (self.depth,)
- log.err(msg)
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
-
- # Check virtual share first
- isVirtual = yield delresource.isVirtualShare(self.request)
- if isVirtual:
- log.debug("Removing shared address book %s" % (delresource,))
- yield delresource.removeVirtualShare(self.request)
- returnValue(responsecode.NO_CONTENT)
-
- log.debug("Deleting addressbook %s" % (delresource.fp.path,))
-
- errors = ResponseQueue(deluri, "DELETE", responsecode.NO_CONTENT)
-
- for childname in delresource.listChildren():
-
- childurl = joinURL(deluri, childname)
- child = (yield self.request.locateChildResource(delresource, childname))
-
- try:
- yield self.deleteAddressBookResource(child, childurl, delresource)
- except:
- errors.add(childurl, responsecode.BAD_REQUEST)
-
- # Now do normal delete
-
- # Handle sharing
- wasShared = (yield delresource.isShared(self.request))
- if wasShared:
- yield delresource.downgradeFromShare(self.request)
-
- yield delresource.bumpSyncToken()
- more_responses = (yield self.deleteResource(delresource, deluri, parent))
-
- if isinstance(more_responses, MultiStatusResponse):
- # Merge errors
- errors.responses.update(more_responses.children)
-
- response = errors.response()
-
- returnValue(response)
-
- @inlineCallbacks
- def deleteCollectionAB(self):
- # XXX CSCS-MERGE this needs to be merged into deleteCollection
- """
- Delete a regular collection with special processing for any addressbook collections
- contained within it.
- """
- if self.depth != "infinity":
- msg = "Client sent illegal depth header value for DELETE: %s" % (self.depth,)
- log.err(msg)
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
-
- log.debug("Deleting collection %s" % (self.resource.fp.path,))
-
- errors = ResponseQueue(self.resource_uri, "DELETE", responsecode.NO_CONTENT)
-
- @inlineCallbacks
- def doDeleteAddressBook(delresource, deluri):
-
- delparent = (yield delresource.locateParent(self.request, deluri))
-
- response = (yield self.deleteAddressBook(delresource, deluri, delparent))
-
- if isinstance(response, MultiStatusResponse):
- # Merge errors
- errors.responses.update(response.children)
-
- returnValue(True)
-
- yield applyToAddressBookCollections(self.resource, self.request, self.resource_uri, self.depth, doDeleteAddressBook, None)
-
- # Now do normal delete
- more_responses = (yield self.deleteResource(self.resource, self.resource_uri, self.parent))
-
- if isinstance(more_responses, MultiStatusResponse):
- # Merge errors
- errors.responses.update(more_responses.children)
-
- response = errors.response()
-
- returnValue(response)
-
- @inlineCallbacks
def run(self):
- if isAddressBookCollectionResource(self.parent):
- response = (yield self.deleteAddressBookResource(self.resource, self.resource_uri, self.parent))
- elif isAddressBookCollectionResource(self.resource):
- response = (yield self.deleteAddressBook(self.resource, self.resource_uri, self.parent))
- else:
- # FIXME: this code-path shouldn't actually be used, as the things
- # with storeRemove on them also have their own http_DELETEs.
- response = (
- yield self.resource.storeRemove(
- self.request,
- not self.internal_request and self.allowImplicitSchedule,
- self.resource_uri
- )
+ # FIXME: this code-path shouldn't actually be used, as the things
+ # with storeRemove on them also have their own http_DELETEs.
+ response = (
+ yield self.resource.storeRemove(
+ self.request,
+ not self.internal_request and self.allowImplicitSchedule,
+ self.resource_uri
)
+ )
returnValue(response)
Modified: CalendarServer/branches/new-store/twistedcaldav/method/put_addressbook_common.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/method/put_addressbook_common.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/twistedcaldav/method/put_addressbook_common.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -34,6 +34,7 @@
from twext.web2.dav.util import joinURL, parentForURL
from twext.web2.http import HTTPError
from twext.web2.http import StatusResponse
+from twext.web2.http_headers import MimeType, generateContentType
from twext.web2.stream import MemoryStream
from twistedcaldav.config import config
@@ -145,7 +146,6 @@
self.indexdestination = indexdestination
self.access = None
- self.newrevision = None
def fullValidation(self):
"""
@@ -343,9 +343,21 @@
@inlineCallbacks
def doStore(self):
# Do put or copy based on whether source exists
- if self.source is not None:
- response = (yield self.destination.storeStream(MemoryStream(self.source.vCardText())))
- self.source.copyDeadPropertiesTo(self.destination)
+ source = self.source
+ if source is not None:
+ # Retrieve information from the source, in case we have to delete
+ # it.
+ sourceProperties = dict(source.newStoreProperties().iteritems())
+ sourceText = source.vCardText()
+
+ # Delete the original source if needed (for example, if this is a
+ # same-calendar MOVE of a calendar object, implemented as an
+ # effective DELETE-then-PUT).
+ if self.deletesource:
+ yield self.doSourceDelete()
+
+ response = (yield self.destination.storeStream(MemoryStream(sourceText)))
+ self.destination.newStoreProperties().update(sourceProperties)
else:
response = (yield self.doStorePut())
@@ -368,16 +380,9 @@
@inlineCallbacks
def doSourceDelete(self):
- # Delete index for original item
- if self.sourceadbk:
- self.newrevision = (yield self.sourceparent.bumpSyncToken())
- self.source_index.deleteResource(self.source.fp.basename(), self.newrevision)
- log.debug("Source index removed %s" % (self.source.fp.path,))
-
# Delete the source resource
- self.source.storeRemove()
+ yield self.source.storeRemove(self.request, self.source_uri)
log.debug("Source removed %s" % (self.source.fp.path,))
-
returnValue(None)
@inlineCallbacks
@@ -403,46 +408,6 @@
returnValue(None)
- def doSourceIndexRecover(self):
- """
- Do source resource indexing. This only gets called when restoring
- the source after its index has been deleted.
-
- @return: None if successful, ErrorResponse on failure
- """
-
- # Add or update the index for this resource.
- self.source_index.addResource(self.source.fp.basename(), self.vcard, self.newrevision)
-
- def doDestinationIndex(self, vcardtoindex):
- """
- Do destination resource indexing, replacing any index previous stored.
-
- @return: None if successful, ErrorResponse on failure
- """
-
- # Add or update the index for this resource.
- try:
- self.destination_index.addResource(self.destination.fp.basename(), vcardtoindex, self.newrevision)
- log.debug("Destination indexed %s" % (self.destination.fp.path,))
- except (ValueError, TypeError), ex:
- msg = "Cannot index vcard resource: %s" % (ex,)
- log.err(msg)
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (carddav_namespace, "valid-address-data"), description=msg))
-
- self.destination.writeDeadProperty(davxml.GETContentType.fromString("text/vcard"))
- return None
-
- def doRemoveDestinationIndex(self):
- """
- Remove any existing destination index.
- """
-
- # Delete index for original item
- if self.destinationadbk:
- self.destination_index.deleteResource(self.destination.fp.basename(), None)
- log.debug("Destination index removed %s" % (self.destination.fp.path,))
-
@inlineCallbacks
def run(self):
"""
@@ -461,7 +426,9 @@
if self.destinationadbk:
# Reserve UID
self.destination_index = self.destinationparent.index()
- reservation = StoreAddressObjectResource.UIDReservation(self.destination_index, self.uid, self.destination_uri)
+ reservation = StoreAddressObjectResource.UIDReservation(
+ self.destination_index, self.uid, self.destination_uri
+ )
if self.indexdestination:
yield reservation.reserve()
@@ -477,36 +444,19 @@
# Get current quota state.
yield self.checkQuota()
- """
- Handle actual store operations here.
-
- The order in which this is done is import:
-
- 1. Do store operation for new data
- 2. Delete source and source index if needed
- 3. Do new indexing if needed
-
- Note that we need to remove the source index BEFORE doing the destination index to cover the
- case of a resource being 'renamed', i.e. moved within the same collection. Since the index UID
- column must be unique in SQL, we cannot add the new index before remove the old one.
- """
-
# Do the actual put or copy
response = (yield self.doStore())
- # Delete the original source if needed.
- if self.deletesource:
- yield self.doSourceDelete()
-
- # Index the new resource if storing to a vcard.
+ # Remember the resource's content-type.
if self.destinationadbk:
- self.newrevision = (yield self.destinationparent.bumpSyncToken())
- result = self.doDestinationIndex(self.vcard)
- if result is not None:
- # FIXME: transaction needs to be rolled back; should we have
- # ErrorResponse detection in renderHTTP? Hmm. -glyph
- returnValue(result)
-
+ content_type = self.request.headers.getHeader("content-type")
+ if content_type is None:
+ content_type = MimeType("text", "vcard",
+ params={"charset":"utf-8"})
+ self.destination.writeDeadProperty(
+ davxml.GETContentType.fromString(generateContentType(content_type))
+ )
+
# Delete the original source if needed.
if self.deletesource:
yield self.doSourceQuotaCheck()
Modified: CalendarServer/branches/new-store/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/storebridge.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/twistedcaldav/storebridge.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -26,25 +26,25 @@
from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+from twext.python import vcomponent
from twext.python.filepath import CachingFilePath as FilePath
-from twext.python import vcomponent
+from twext.python.log import Logger
from twext.web2.http_headers import ETag
from twext.web2.dav.http import ErrorResponse, ResponseQueue
from twext.web2.responsecode import (
FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
BAD_REQUEST)
-from twext.python.log import Logger
+from twext.web2.dav import davxml
from twext.web2.dav.resource import TwistedGETContentMD5
from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL
from twext.web2.http import HTTPError, StatusResponse
from twistedcaldav.static import CalDAVFile, ScheduleInboxFile
+from twistedcaldav.vcard import Component as VCard
+from txdav.common.icommondatastore import NoSuchObjectResourceError
from txdav.propertystore.base import PropertyName
-from txcaldav.icalendarstore import NoSuchCalendarObjectError
-from txcarddav.iaddressbookstore import NoSuchAddressBookObjectError
-from twistedcaldav.vcard import Component as VCard
from twistedcaldav.caldavxml import ScheduleTag, caldav_namespace
from twistedcaldav.scheduling.implicit import ImplicitScheduler
@@ -238,6 +238,9 @@
self._initializeWithCalendar(calendar, home)
+ def isCollection(self):
+ return True
+
def isCalendarCollection(self):
"""
Yes, it is a calendar collection.
@@ -245,10 +248,20 @@
return True
+ @inlineCallbacks
def http_DELETE(self, request):
"""
Override http_DELETE to validate 'depth' header.
"""
+
+ #
+ # Check authentication and access controls
+ #
+ parentURL = parentForURL(request.uri)
+ parent = (yield request.locateResource(parentURL))
+
+ yield parent.authorize(request, (davxml.Unbind(),))
+
depth = request.headers.getHeader("depth", "infinity")
if depth != "infinity":
msg = "illegal depth header for DELETE on collection: %s" % (
@@ -256,7 +269,8 @@
)
log.err(msg)
raise HTTPError(StatusResponse(BAD_REQUEST, msg))
- return self.storeRemove(request, True, request.uri)
+ response = (yield self.storeRemove(request, True, request.uri))
+ returnValue(response)
@inlineCallbacks
@@ -355,9 +369,6 @@
returnValue(response)
- def isCollection(self):
- return True
-
def http_COPY(self, request):
"""
Copying of calendar collections isn't allowed.
@@ -542,7 +553,7 @@
hashlib.new("md5", self.iCalendarText()).hexdigest(),
weak=False
)
- except NoSuchCalendarObjectError:
+ 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.
@@ -564,6 +575,24 @@
@inlineCallbacks
+ def http_DELETE(self, request):
+ """
+ Override http_DELETE to validate 'depth' header.
+ """
+
+ #
+ # Check authentication and access controls
+ #
+ parentURL = parentForURL(request.uri)
+ parent = (yield request.locateResource(parentURL))
+
+ yield parent.authorize(request, (davxml.Unbind(),))
+
+ response = (yield self.storeRemove(request, True, request.uri))
+ returnValue(response)
+
+
+ @inlineCallbacks
def storeStream(self, stream):
# FIXME: direct tests
component = vcomponent.VComponent.fromString(
@@ -626,8 +655,11 @@
# resources as the iTIP operation may fail and may need to prevent the
# delete from happening.
+ isinbox = self._newStoreObject._calendar.name() == "inbox"
+
# Do If-Schedule-Tag-Match behavior first
- self.validIfScheduleMatch(request)
+ if not isinbox:
+ self.validIfScheduleMatch(request)
# Do quota checks before we start deleting things
myquota = (yield self.quota(request))
@@ -638,7 +670,7 @@
scheduler = None
lock = None
- if implicitly:
+ if not isinbox and implicitly:
# Get data we need for implicit scheduling
calendar = (yield self.iCalendarForUser(request))
scheduler = ImplicitScheduler()
@@ -675,7 +707,7 @@
yield self.quotaSizeAdjust(request, -old_size)
# Do scheduling
- if implicitly:
+ if not isinbox and implicitly:
yield scheduler.doImplicitScheduling()
except MemcacheLockTimeoutError:
@@ -831,6 +863,116 @@
def isCollection(self):
return True
+ def isAddressBookCollection(self):
+ """
+ Yes, it is a calendar collection.
+ """
+ return True
+
+
+ @inlineCallbacks
+ def http_DELETE(self, request):
+ """
+ Override http_DELETE to validate 'depth' header.
+ """
+
+ #
+ # Check authentication and access controls
+ #
+ parentURL = parentForURL(request.uri)
+ parent = (yield request.locateResource(parentURL))
+
+ yield parent.authorize(request, (davxml.Unbind(),))
+
+ depth = request.headers.getHeader("depth", "infinity")
+ if depth != "infinity":
+ msg = "illegal depth header for DELETE on collection: %s" % (
+ depth,
+ )
+ log.err(msg)
+ raise HTTPError(StatusResponse(BAD_REQUEST, msg))
+ response = (yield self.storeRemove(request, request.uri))
+ returnValue(response)
+
+
+ @inlineCallbacks
+ def storeRemove(self, request, where):
+ """
+ Delete this addressbook collection resource, first deleting each contained
+ addressbook resource.
+
+ This has to emulate the behavior in fileop.delete in that any errors
+ need to be reported back in a multistatus response.
+
+ @param request: The request used to locate child resources. Note that
+ this is the request which I{triggered} the C{DELETE}, but which may
+ not actually be a C{DELETE} request itself.
+
+ @type request: L{twext.web2.iweb.IRequest}
+
+ @param where: the URI at which the resource is being deleted.
+ @type where: C{str}
+
+ @return: an HTTP response suitable for sending to a client (or
+ including in a multi-status).
+
+ @rtype: something adaptable to L{twext.web2.iweb.IResponse}
+ """
+
+ # Check virtual share first
+ isVirtual = yield self.isVirtualShare(request)
+ if isVirtual:
+ log.debug("Removing shared calendar %s" % (self,))
+ yield self.removeVirtualShare(request)
+ returnValue(NO_CONTENT)
+
+ log.debug("Deleting addressbook %s" % (self,))
+
+ # 'deluri' is this resource's URI; I should be able to synthesize it
+ # from 'self'.
+
+ errors = ResponseQueue(where, "DELETE", NO_CONTENT)
+
+ for childname in self.listChildren():
+
+ childurl = joinURL(where, childname)
+
+ # FIXME: use a more specific API; we should know what this child
+ # resource is, and not have to look it up. (Sharing information
+ # needs to move into the back-end first, though.)
+ child = (yield request.locateChildResource(self, childname))
+
+ try:
+ yield child.storeRemove(request, childurl)
+ except:
+ logDefaultException()
+ errors.add(childurl, BAD_REQUEST)
+
+ # Now do normal delete
+
+ # Handle sharing
+ wasShared = (yield self.isShared(request))
+ if wasShared:
+ yield self.downgradeFromShare(request)
+
+ # Actually delete it.
+ self._newStoreParentHome.removeAddressBookWithName(
+ self._newStoreAddressBook.name()
+ )
+ self.__class__ = ProtoAddressBookCollectionFile
+ del self._newStoreAddressBook
+
+ # FIXME: handle exceptions, possibly like this:
+
+ # if isinstance(more_responses, MultiStatusResponse):
+ # # Merge errors
+ # errors.responses.update(more_responses.children)
+
+ response = errors.response()
+
+ returnValue(response)
+
+
def http_COPY(self, request):
"""
Copying of addressbook collections isn't allowed.
@@ -964,8 +1106,8 @@
# a public way to do this? or maybe we should just have a real queue.
objectName = self._newStoreObject.name()
Name = self._newStoreObject._addressbook.name()
- homeUID = self._newStoreObject._addressbook._Home.uid()
- store = self._newStoreObject._transaction._Store
+ homeUID = self._newStoreObject._addressbook._addressbookHome.uid()
+ store = self._newStoreObject._transaction._addressbookStore
txn = store.newTransaction()
newObject = (txn.HomeWithUID(homeUID)
.addressbookWithName(Name)
@@ -999,7 +1141,7 @@
hashlib.new("md5", self.vCardText()).hexdigest(),
weak=False
)
- except NoSuchAddressBookObjectError:
+ except NoSuchObjectResourceError:
# FIXME: a workaround for the fact that DELETE still rudely vanishes
# the addressbook object out from underneath the store, and doesn't
# call storeRemove.
@@ -1021,6 +1163,24 @@
@inlineCallbacks
+ def http_DELETE(self, request):
+ """
+ Override http_DELETE to validate 'depth' header.
+ """
+
+ #
+ # Check authentication and access controls
+ #
+ parentURL = parentForURL(request.uri)
+ parent = (yield request.locateResource(parentURL))
+
+ yield parent.authorize(request, (davxml.Unbind(),))
+
+ response = (yield self.storeRemove(request, request.uri))
+ returnValue(response)
+
+
+ @inlineCallbacks
def storeStream(self, stream):
# FIXME: direct tests
component = VCard.fromString(
@@ -1030,18 +1190,42 @@
returnValue(NO_CONTENT)
- def storeRemove(self):
+ @inlineCallbacks
+ def storeRemove(self, request, where):
"""
Remove this addressbook object.
"""
- # FIXME: public attribute please
- self._newStoreObject._addressbook.removeAddressBookObjectWithName(self._newStoreObject.name())
- # FIXME: clean this up with a 'transform' method
- self._newStoreParentAddressBook = self._newStoreObject._addressbook
- del self._newStoreObject
- self.__class__ = ProtoAddressBookObjectFile
+ # Do quota checks before we start deleting things
+ myquota = (yield self.quota(request))
+ if myquota is not None:
+ old_size = (yield self.quotaSize(request))
+ else:
+ old_size = 0
+ try:
+ storeAddressBook = self._newStoreObject._addressbook
+
+ # Do delete
+
+ # FIXME: public attribute please
+ storeAddressBook.removeAddressBookObjectWithName(self._newStoreObject.name())
+
+ # FIXME: clean this up with a 'transform' method
+ self._newStoreParentAddressBook = storeAddressBook
+ del self._newStoreObject
+ self.__class__ = ProtoAddressBookObjectFile
+
+ # Adjust quota
+ if myquota is not None:
+ yield self.quotaSizeAdjust(request, -old_size)
+
+ except MemcacheLockTimeoutError:
+ raise HTTPError(StatusResponse(CONFLICT, "Resource: %s currently in use on the server." % (where,)))
+
+ returnValue(NO_CONTENT)
+
+
def _initializeWithObject(self, Object):
self._newStoreObject = Object
self._dead_properties = _NewStorePropertiesWrapper(
Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/file.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/file.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -26,98 +26,30 @@
"CalendarObject",
]
-from uuid import uuid4
-from errno import EEXIST, ENOENT
+from errno import ENOENT
-from zope.interface import implements
-
-from twisted.python.util import FancyEqMixin
-
-from twisted.internet.defer import succeed
-
-from twisted.python import log
-from twext.python.log import LoggingMixIn
-from twext.python.vcomponent import VComponent
from twext.python.vcomponent import InvalidICalendarDataError
+from twext.python.vcomponent import VComponent
from twext.web2.dav.element.rfc2518 import ResourceType
-from txdav.propertystore.xattr import PropertyStore
-from txdav.propertystore.base import PropertyName
-PN = PropertyName.fromString
+from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
+from twistedcaldav.index import Index as OldIndex, IndexSchedule as OldInboxIndex
-from txcaldav.icalendarstore import ICalendarStoreTransaction
-from txcaldav.icalendarstore import ICalendarStore, ICalendarHome
from txcaldav.icalendarstore import ICalendar, ICalendarObject
-from txcaldav.icalendarstore import CalendarNameNotAllowedError
-from txcaldav.icalendarstore import CalendarObjectNameNotAllowedError
-from txcaldav.icalendarstore import CalendarAlreadyExistsError
-from txcaldav.icalendarstore import CalendarObjectNameAlreadyExistsError
-from txcaldav.icalendarstore import NoSuchCalendarError
-from txcaldav.icalendarstore import NoSuchCalendarObjectError
-from txcaldav.icalendarstore import InvalidCalendarComponentError
-from txcaldav.icalendarstore import InternalDataStoreError
+from txcaldav.icalendarstore import ICalendarStore, ICalendarHome
+from txcaldav.icalendarstore import ICalendarStoreTransaction
-from twistedcaldav.caldavxml import ScheduleCalendarTransp, Transparent, Opaque
-from twistedcaldav.customxml import GETCTag
+from txdav.common.datastore.file import CommonDataStore, CommonStoreTransaction,\
+ CommonHome, CommonHomeChild, CommonObjectResource
+from txdav.common.icommondatastore import InvalidObjectResourceError,\
+ NoSuchObjectResourceError, InternalDataStoreError
+from txdav.datastore.file import writeOperation, hidden
+from txdav.propertystore.base import PropertyName
-from twistedcaldav.index import Index as OldIndex, IndexSchedule as OldInboxIndex
+from zope.interface import implements
-def _isValidName(name):
+class CalendarStore(CommonDataStore):
"""
- Determine if the given string is a valid name. i.e. does it conflict with
- any of the other entities which may be on the filesystem?
-
- @param name: a name which might be given to a calendar.
- """
- return not name.startswith(".")
-
-
-def _hidden(path):
- return path.sibling('.' + path.basename())
-
-
-_unset = object()
-
-class _cached(object):
- """
- This object is a decorator for a 0-argument method which should be called
- only once, and its result cached so that future invocations just return the
- same result without calling the underlying method again.
-
- @ivar thunk: the function to call to generate a cached value.
- """
-
- def __init__(self, thunk):
- self.thunk = thunk
-
-
- def __get__(self, oself, owner):
- def inner():
- cacheKey = "_" + self.thunk.__name__ + "_cached"
- cached = getattr(oself, cacheKey, _unset)
- if cached is _unset:
- value = self.thunk(oself)
- setattr(oself, cacheKey, value)
- return value
- else:
- return cached
- return inner
-
-
-
-def _writeOperation(thunk):
- # FIXME: tests
- def inner(self, *a, **kw):
- if self._transaction._termination is not None:
- raise RuntimeError(
- "%s.%s is a write operation, but transaction already %s"
- % (self, thunk.__name__, self._transaction._termination))
- return thunk(self, *a, **kw)
- return inner
-
-
-class CalendarStore(LoggingMixIn):
- """
An implementation of L{ICalendarObject} backed by a
L{twext.python.filepath.CachingFilePath}.
@@ -132,45 +64,12 @@
@param path: a L{FilePath} pointing at a directory on disk.
"""
- self._path = path
+ super(CalendarStore, self).__init__(path)
+ self._transactionClass = CalendarStoreTransaction
-# if not path.isdir():
- # FIXME: Add CalendarStoreNotFoundError?
-# raise NotFoundError("No such calendar store")
- def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self._path.path)
-
- def newTransaction(self):
- """
- Create a new filesystem-based transaction.
-
- @see Transaction
- """
- return Transaction(self)
-
-
-
-class _CommitTracker(object):
+class CalendarStoreTransaction(CommonStoreTransaction):
"""
- Diagnostic tool to find transactions that were never committed.
- """
-
- def __init__(self):
- self.done = False
- self.info = []
-
- def __del__(self):
- if not self.done and self.info:
- print '**** UNCOMMITTED TRANSACTION BEING GARBAGE COLLECTED ****'
- for info in self.info:
- print ' ', info
- print '---- END OF OPERATIONS'
-
-
-
-class Transaction(LoggingMixIn):
- """
In-memory implementation of
Note that this provides basic 'undo' support, but not truly transactional
@@ -188,289 +87,44 @@
@type calendarStore: L{CalendarStore}
"""
- self._calendarStore = calendarStore
- self._termination = None
- self._operations = []
- self._tracker = _CommitTracker()
- self._calendarHomes = {}
+ super(CalendarStoreTransaction, self).__init__(calendarStore)
+ self._homeClass = CalendarHome
+ calendarHomeWithUID = CommonStoreTransaction.homeWithUID
- def addOperation(self, operation, name):
- self._operations.append(operation)
- self._tracker.info.append(name)
+ def creatingHome(self, home):
+ home.createCalendarWithName("calendar")
+ defaultCal = home.calendarWithName("calendar")
+ props = defaultCal.properties()
+ props[PropertyName(*ScheduleCalendarTransp.qname())] = ScheduleCalendarTransp(
+ Opaque())
+ home.createCalendarWithName("inbox")
- def _terminate(self, mode):
- """
- Check to see if this transaction has already been terminated somehow,
- either via committing or aborting, and if not, note that it has been
- terminated.
-
- @param mode: The manner of the termination of this transaction.
-
- @type mode: C{str}
-
- @raise RuntimeError: This transaction has already been terminated.
- """
- if self._termination is not None:
- raise RuntimeError("already %s" % (self._termination,))
- self._termination = mode
- self._tracker.done = True
-
-
- def abort(self):
- self._terminate("aborted")
-
-
- def commit(self):
- self._terminate("committed")
-
- self.committed = True
- undos = []
-
- for operation in self._operations:
- try:
- undo = operation()
- if undo is not None:
- undos.append(undo)
- except:
- log.err()
- for undo in undos:
- try:
- undo()
- except:
- log.err()
- raise
-
-
- def calendarHomeWithUID(self, uid, create=False):
- if (uid, self) in self._calendarHomes:
- return self._calendarHomes[(uid, self)]
-
- if uid.startswith("."):
- return None
-
- assert len(uid) >= 4
-
- childPath1 = self._calendarStore._path.child(uid[0:2])
- childPath2 = childPath1.child(uid[2:4])
- childPath3 = childPath2.child(uid)
- def createDirectory(path):
- try:
- path.createDirectory()
- except (IOError, OSError), e:
- if e.errno != EEXIST:
- # Ignore, in case someone else created the
- # directory while we were trying to as well.
- raise
-
- creating = False
- if create:
- if not childPath2.isdir():
- if not childPath1.isdir():
- createDirectory(childPath1)
- createDirectory(childPath2)
- if childPath3.isdir():
- calendarPath = childPath3
- else:
- creating = True
- calendarPath = childPath3.temporarySibling()
- createDirectory(calendarPath)
- def do():
- def lastly():
- calendarPath.moveTo(childPath3)
- # calendarHome._path = calendarPath
- # do this _after_ all other file operations
- calendarHome._path = childPath3
- return lambda : None
- self.addOperation(lastly, "create home finalize")
- return lambda : None
- self.addOperation(do, "create home UID %r" % (uid,))
-
- elif not childPath3.isdir():
- return None
- else:
- calendarPath = childPath3
-
- calendarHome = CalendarHome(calendarPath, self._calendarStore, self)
- self._calendarHomes[(uid, self)] = calendarHome
- if creating:
- calendarHome.createCalendarWithName("calendar")
- defaultCal = calendarHome.calendarWithName("calendar")
- props = defaultCal.properties()
- props[PN(ScheduleCalendarTransp.sname())] = ScheduleCalendarTransp(
- Opaque())
- calendarHome.createCalendarWithName("inbox")
- return calendarHome
-
-
-
-class CalendarHome(LoggingMixIn):
+class CalendarHome(CommonHome):
implements(ICalendarHome)
def __init__(self, path, calendarStore, transaction):
- self._calendarStore = calendarStore
- self._path = path
- self._transaction = transaction
- self._newCalendars = {}
- self._removedCalendars = set()
- self._cachedCalendars = {}
+ super(CalendarHome, self).__init__(path, calendarStore, transaction)
+ self._childClass = Calendar
- def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self._path)
+ calendars = CommonHome.children
+ calendarWithName = CommonHome.childWithName
+ createCalendarWithName = CommonHome.createChildWithName
+ removeCalendarWithName = CommonHome.removeChildWithName
+ @property
+ def _calendarStore(self):
+ return self._dataStore
- def uid(self):
- return self._path.basename()
-
- def _updateSyncToken(self, reset=False):
- "Stub for updating sync token."
- # FIXME: actually update something
-
-
- def calendars(self):
- return set(self._newCalendars.itervalues()) | set(
- self.calendarWithName(name)
- for name in self._path.listdir()
- if not name.startswith(".")
- )
-
- def calendarWithName(self, name):
- calendar = self._newCalendars.get(name)
- if calendar is not None:
- return calendar
- if name in self._removedCalendars:
- return None
- if name in self._cachedCalendars:
- return self._cachedCalendars[name]
-
- if name.startswith("."):
- return None
-
- childPath = self._path.child(name)
- if childPath.isdir():
- existingCalendar = Calendar(name, self)
- self._cachedCalendars[name] = existingCalendar
- return existingCalendar
- else:
- return None
-
-
- @_writeOperation
- def createCalendarWithName(self, name):
- if name.startswith("."):
- raise CalendarNameNotAllowedError(name)
-
- childPath = self._path.child(name)
-
- if name not in self._removedCalendars and childPath.isdir():
- raise CalendarAlreadyExistsError(name)
-
- temporary = _hidden(childPath.temporarySibling())
- temporary.createDirectory()
- # In order for the index to work (which is doing real file ops on disk
- # via SQLite) we need to create a real directory _immediately_.
-
- # FIXME: some way to roll this back.
-
- c = self._newCalendars[name] = Calendar(temporary.basename(), self, name)
- c.retrieveOldIndex().create()
- def do():
- try:
- props = c.properties()
- temporary.moveTo(childPath)
- c._name = name
- # FIXME: _lots_ of duplication of work here.
- props.path = childPath
- props.flush()
- except (IOError, OSError), e:
- if e.errno == EEXIST and childPath.isdir():
- raise CalendarAlreadyExistsError(name)
- raise
- # FIXME: direct tests, undo for index creation
- # Return undo
- return lambda: childPath.remove()
-
- self._transaction.addOperation(do, "create calendar %r" % (name,))
- props = c.properties()
- CalendarType = ResourceType.calendar #@UndefinedVariable
- props[PN(ResourceType.sname())] = CalendarType
-
- # Calendars are initially transparent to freebusy. FIXME: freebusy
- # needs more structured support than this.
- props[PN(ScheduleCalendarTransp.sname())] = ScheduleCalendarTransp(
- Transparent())
- # FIXME: there's no need for 'flush' to be a public method of the
- # property store any more. It should just be transactional like
- # everything else; the API here would better be expressed as
- # c.properties().participateInTxn(txn)
- # FIXME: return c # maybe ?
-
-
- @_writeOperation
- def removeCalendarWithName(self, name):
- if name.startswith(".") or name in self._removedCalendars:
- raise NoSuchCalendarError(name)
-
- self._removedCalendars.add(name)
- childPath = self._path.child(name)
- if name not in self._newCalendars and not childPath.isdir():
- raise NoSuchCalendarError(name)
-
- def do(transaction=self._transaction):
- for i in xrange(1000):
- trash = childPath.sibling("._del_%s_%d" % (childPath.basename(), i))
- if not trash.exists():
- break
- else:
- raise InternalDataStoreError("Unable to create trash target for calendar at %s" % (childPath,))
-
- try:
- childPath.moveTo(trash)
- except (IOError, OSError), e:
- if e.errno == ENOENT:
- raise NoSuchCalendarError(name)
- raise
-
- def cleanup():
- try:
- trash.remove()
- except Exception, e:
- self.log_error("Unable to delete trashed calendar at %s: %s" % (trash.fp, e))
-
- transaction.addOperation(cleanup, "remove calendar backup %r" % (name,))
-
- def undo():
- trash.moveTo(childPath)
-
- return undo
- # FIXME: direct tests
- self._transaction.addOperation(
- do, "prepare calendar remove %r" % (name,)
- )
-
-
- # @_cached
- def properties(self):
- # FIXME: needs tests for actual functionality
- # FIXME: needs to be cached
- # FIXME: transaction tests
- props = PropertyStore(self._path)
- self._transaction.addOperation(props.flush, "flush home properties")
- return props
-
-
-
-class Calendar(LoggingMixIn, FancyEqMixin):
+class Calendar(CommonHomeChild):
"""
File-based implementation of L{ICalendar}.
"""
implements(ICalendar)
- compareAttributes = '_name _calendarHome _transaction'.split()
-
def __init__(self, name, calendarHome, realName=None):
"""
Initialize a calendar pointing at a path on disk.
@@ -486,179 +140,33 @@
will eventually have on disk.
@type realName: C{str}
"""
- self._name = name
- self._calendarHome = calendarHome
- self._transaction = calendarHome._transaction
- self._newCalendarObjects = {}
- self._cachedCalendarObjects = {}
- self._removedCalendarObjects = set()
- self._index = Index(self)
- self._renamedName = realName
+ super(Calendar, self).__init__(name, calendarHome, realName)
+ self._index = Index(self)
+ self._objectResourceClass = CalendarObject
+
@property
- def _path(self):
- return self._calendarHome._path.child(self._name)
+ def _calendarHome(self):
+ return self._home
+ def resourceType(self):
+ return ResourceType.calendar
- def retrieveOldIndex(self):
- """
- Retrieve the old Index object.
- """
- return self._index._oldIndex
+ ownerCalendarHome = CommonHomeChild.ownerHome
+ calendarObjects = CommonHomeChild.objectResources
+ calendarObjectWithName = CommonHomeChild.objectResourceWithName
+ calendarObjectWithUID = CommonHomeChild.objectResourceWithUID
+ createCalendarObjectWithName = CommonHomeChild.createObjectResourceWithName
+ removeCalendarObjectWithName = CommonHomeChild.removeObjectResourceWithName
+ removeCalendarObjectWithUID = CommonHomeChild.removeObjectResourceWithUID
+ calendarObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
- def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self._path.path)
-
-
- def name(self):
- if self._renamedName is not None:
- return self._renamedName
- return self._path.basename()
-
-
- _renamedName = None
-
- @_writeOperation
- def rename(self, name):
- self._updateSyncToken()
- oldName = self.name()
- self._renamedName = name
- self._calendarHome._newCalendars[name] = self
- self._calendarHome._removedCalendars.add(oldName)
- def doIt():
- self._path.moveTo(self._path.sibling(name))
- return lambda : None # FIXME: revert
- self._transaction.addOperation(doIt, "rename calendar %r -> %r" %
- (oldName, name))
-
-
- def ownerCalendarHome(self):
- return self._calendarHome
-
-
- def calendarObjects(self):
- return sorted((
- self.calendarObjectWithName(name)
- for name in (
- set(self._newCalendarObjects.iterkeys()) |
- set(name for name in self._path.listdir()
- if not name.startswith(".")) -
- set(self._removedCalendarObjects)
- )),
- key=lambda calObj: calObj.name()
- )
-
-
- def calendarObjectWithName(self, name):
- if name in self._removedCalendarObjects:
- return None
- if name in self._newCalendarObjects:
- return self._newCalendarObjects[name]
- if name in self._cachedCalendarObjects:
- return self._cachedCalendarObjects[name]
-
- calendarObjectPath = self._path.child(name)
- if calendarObjectPath.isfile():
- obj = CalendarObject(name, self)
- self._cachedCalendarObjects[name] = obj
- return obj
- else:
- return None
-
-
- def calendarObjectWithUID(self, uid):
- # FIXME: This _really_ needs to be inspecting an index, not parsing
- # every resource.
- for calendarObjectPath in self._path.children():
- if not _isValidName(calendarObjectPath.basename()):
- continue
- obj = CalendarObject(calendarObjectPath.basename(), self)
- if obj.component().resourceUID() == uid:
- if obj.name() in self._removedCalendarObjects:
- return None
- return obj
-
-
- @_writeOperation
- def createCalendarObjectWithName(self, name, component):
- if name.startswith("."):
- raise CalendarObjectNameNotAllowedError(name)
-
- calendarObjectPath = self._path.child(name)
- if calendarObjectPath.exists():
- raise CalendarObjectNameAlreadyExistsError(name)
-
- calendarObject = CalendarObject(name, self)
- calendarObject.setComponent(component)
- self._cachedCalendarObjects[name] = calendarObject
-
-
- @_writeOperation
- def removeCalendarObjectWithName(self, name):
- newRevision = self._updateSyncToken() # FIXME: Test
- self.retrieveOldIndex().deleteResource(name, newRevision)
- if name.startswith("."):
- raise NoSuchCalendarObjectError(name)
-
- calendarObjectPath = self._path.child(name)
- if calendarObjectPath.isfile():
- self._removedCalendarObjects.add(name)
- # FIXME: test for undo
- def do():
- calendarObjectPath.remove()
- return lambda: None
- self._transaction.addOperation(do, "remove calendar object %r" %
- (name,))
- else:
- raise NoSuchCalendarObjectError(name)
-
-
- @_writeOperation
- def removeCalendarObjectWithUID(self, uid):
- self.removeCalendarObjectWithName(
- self.calendarObjectWithUID(uid)._path.basename())
-
-
- def _updateSyncToken(self, reset=False):
- # FIXME: add locking a-la CalDAVFile.bumpSyncToken
- # FIXME: tests for desired concurrency properties
- ctag = PropertyName.fromString(GETCTag.sname())
- props = self.properties()
- token = props.get(ctag)
- if token is None or reset:
- caluuid = uuid4()
- revision = 1
- else:
- # FIXME: no direct tests for update
- token = str(token)
- caluuid, revision = token.split("#", 1)
- revision = int(revision) + 1
- token = "%s#%d" % (caluuid, revision)
- props[ctag] = GETCTag(token)
- # FIXME: no direct tests for commit
- succeed(token)
-
-
def calendarObjectsInTimeRange(self, start, end, timeZone):
raise NotImplementedError()
- def calendarObjectsSinceToken(self, token):
- raise NotImplementedError()
-
- # FIXME: property writes should be a write operation
- @_cached
- def properties(self):
- # FIXME: needs direct tests - only covered by calendar store tests
- # FIXME: transactions
- props = PropertyStore(self._path)
- self._transaction.addOperation(props.flush,
- "flush calendar properties")
- return props
-
-
def _doValidate(self, component):
# FIXME: should be separate class, not separate case!
if self.name() == 'inbox':
@@ -667,8 +175,7 @@
component.validateForCalDAV()
-
-class CalendarObject(LoggingMixIn):
+class CalendarObject(CommonObjectResource):
"""
@ivar _path: The path of the .ics file on disk
@@ -677,58 +184,44 @@
implements(ICalendarObject)
def __init__(self, name, calendar):
- self._name = name
- self._calendar = calendar
- self._transaction = calendar._transaction
- self._component = None
+ super(CalendarObject, self).__init__(name, calendar)
-
@property
- def _path(self):
- return self._calendar._path.child(self._name)
+ def _calendar(self):
+ return self._parentCollection
-
- def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self._path.path)
-
-
- def name(self):
- return self._path.basename()
-
-
- @_writeOperation
+ @writeOperation
def setComponent(self, component):
-
- newRevision = self._calendar._updateSyncToken() # FIXME: test
- self._calendar.retrieveOldIndex().addResource(
- self.name(), component, newRevision
- )
-
if not isinstance(component, VComponent):
raise TypeError(type(component))
try:
if component.resourceUID() != self.uid():
- raise InvalidCalendarComponentError(
+ raise InvalidObjectResourceError(
"UID may not change (%s != %s)" % (
component.resourceUID(), self.uid()
)
)
- except NoSuchCalendarObjectError:
+ except NoSuchObjectResourceError:
pass
try:
self._calendar._doValidate(component)
except InvalidICalendarDataError, e:
- raise InvalidCalendarComponentError(e)
+ raise InvalidObjectResourceError(e)
+ newRevision = self._calendar._updateSyncToken() # FIXME: test
+ self._calendar.retrieveOldIndex().addResource(
+ self.name(), component, newRevision
+ )
+
self._component = component
# FIXME: needs to clear text cache
def do():
backup = None
if self._path.exists():
- backup = _hidden(self._path.temporarySibling())
+ backup = hidden(self._path.temporarySibling())
self._path.moveTo(backup)
fh = self._path.open("w")
try:
@@ -781,7 +274,7 @@
fh = self._path.open()
except IOError, e:
if e[0] == ENOENT:
- raise NoSuchCalendarObjectError(self)
+ raise NoSuchObjectResourceError(self)
else:
raise
@@ -814,14 +307,7 @@
def organizer(self):
return self.component().getOrganizer()
- @_cached
- def properties(self):
- props = PropertyStore(self._path)
- self._transaction.addOperation(props.flush, "object properties flush")
- return props
-
-
class Index(object):
#
# OK, here's where we get ugly.
Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/test/common.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/test/common.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/test/common.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -25,11 +25,15 @@
from txdav.idav import IPropertyStore
from txdav.propertystore.base import PropertyName
+from txdav.common.icommondatastore import HomeChildNameAlreadyExistsError
+from txdav.common.icommondatastore import InvalidObjectResourceError
+from txdav.common.icommondatastore import NoSuchHomeChildError
+from txdav.common.icommondatastore import NoSuchObjectResourceError
+from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError
+
from txcaldav.icalendarstore import (
ICalendarStore, ICalendarStoreTransaction, ICalendarObject, ICalendarHome,
- ICalendar, InvalidCalendarComponentError,
- CalendarObjectNameAlreadyExistsError, CalendarAlreadyExistsError,
- NoSuchCalendarError, NoSuchCalendarObjectError)
+ ICalendar)
from twext.python.filepath import CachingFilePath as FilePath
from twext.web2.dav import davxml
@@ -385,7 +389,7 @@
"""
for name in home1_calendarNames:
self.assertRaises(
- CalendarAlreadyExistsError,
+ HomeChildNameAlreadyExistsError,
self.homeUnderTest().createCalendarWithName, name
)
@@ -408,7 +412,7 @@
Attempt to remove an non-existing calendar should raise.
"""
home = self.homeUnderTest()
- self.assertRaises(NoSuchCalendarError,
+ self.assertRaises(NoSuchHomeChildError,
home.removeCalendarWithName, "xyzzy")
@@ -519,7 +523,7 @@
"""
calendar = self.calendarUnderTest()
self.assertRaises(
- NoSuchCalendarObjectError,
+ NoSuchObjectResourceError,
calendar.removeCalendarObjectWithName, "xyzzy"
)
@@ -652,7 +656,7 @@
given name already exists in that calendar.
"""
self.assertRaises(
- CalendarObjectNameAlreadyExistsError,
+ ObjectResourceNameAlreadyExistsError,
self.calendarUnderTest().createCalendarObjectWithName,
"1.ics", VComponent.fromString(event4_text)
)
@@ -665,7 +669,7 @@
text.
"""
self.assertRaises(
- InvalidCalendarComponentError,
+ InvalidObjectResourceError,
self.calendarUnderTest().createCalendarObjectWithName,
"new", VComponent.fromString(event4notCalDAV_text)
)
@@ -678,7 +682,7 @@
"""
calendarObject = self.calendarObjectUnderTest()
self.assertRaises(
- InvalidCalendarComponentError,
+ InvalidObjectResourceError,
calendarObject.setComponent,
VComponent.fromString(event4notCalDAV_text)
)
@@ -693,7 +697,7 @@
component = VComponent.fromString(event4_text)
calendarObject = calendar1.calendarObjectWithName("1.ics")
self.assertRaises(
- InvalidCalendarComponentError,
+ InvalidObjectResourceError,
calendarObject.setComponent, component
)
Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/test/test_file.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -23,11 +23,11 @@
from twext.python.vcomponent import VComponent
-from txcaldav.icalendarstore import CalendarNameNotAllowedError
-from txcaldav.icalendarstore import CalendarObjectNameNotAllowedError
-from txcaldav.icalendarstore import CalendarObjectUIDAlreadyExistsError
-from txcaldav.icalendarstore import NoSuchCalendarError
-from txcaldav.icalendarstore import NoSuchCalendarObjectError
+from txdav.common.icommondatastore import HomeChildNameNotAllowedError
+from txdav.common.icommondatastore import ObjectResourceNameNotAllowedError
+from txdav.common.icommondatastore import ObjectResourceUIDAlreadyExistsError
+from txdav.common.icommondatastore import NoSuchHomeChildError
+from txdav.common.icommondatastore import NoSuchObjectResourceError
from txcaldav.calendarstore.file import CalendarStore, CalendarHome
from txcaldav.calendarstore.file import Calendar, CalendarObject
@@ -134,7 +134,7 @@
implementation, so no calendar names may start with ".".
"""
self.assertRaises(
- CalendarNameNotAllowedError,
+ HomeChildNameNotAllowedError,
self.home1.createCalendarWithName, ".foo"
)
@@ -147,7 +147,7 @@
name = ".foo"
self.home1._path.child(name).createDirectory()
self.assertRaises(
- NoSuchCalendarError,
+ NoSuchHomeChildError,
self.home1.removeCalendarWithName, name
)
@@ -231,7 +231,7 @@
".".
"""
self.assertRaises(
- CalendarObjectNameNotAllowedError,
+ ObjectResourceNameNotAllowedError,
self.calendar1.createCalendarObjectWithName,
".foo", VComponent.fromString(event4_text)
)
@@ -247,7 +247,7 @@
assert self.calendar1.calendarObjectWithName(name) is None
component = VComponent.fromString(event1modified_text)
self.assertRaises(
- CalendarObjectUIDAlreadyExistsError,
+ ObjectResourceUIDAlreadyExistsError,
self.calendar1.createCalendarObjectWithName,
name, component
)
@@ -273,7 +273,7 @@
name = ".foo"
self.calendar1._path.child(name).touch()
self.assertRaises(
- NoSuchCalendarObjectError,
+ NoSuchObjectResourceError,
self.calendar1.removeCalendarObjectWithName, name
)
@@ -312,14 +312,13 @@
"""
Commit the current transaction, but add an operation that will cause it
to fail at the end. Finally, refresh all attributes with a new
- transaction so that further oparations can be performed in a valid
+ transaction so that further operations can be performed in a valid
context.
"""
def fail():
raise RuntimeError("oops")
self.txn.addOperation(fail, "dummy failing operation")
self.assertRaises(RuntimeError, self.txn.commit)
- self.assertEquals(len(self.flushLoggedErrors(RuntimeError)), 1)
self._refresh()
@@ -366,7 +365,7 @@
Attempt to remove an non-existing calendar object should raise.
"""
self.assertRaises(
- NoSuchCalendarObjectError,
+ NoSuchObjectResourceError,
self.calendar1.removeCalendarObjectWithUID, "xyzzy"
)
Modified: CalendarServer/branches/new-store/txcaldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/icalendarstore.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txcaldav/icalendarstore.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -24,21 +24,6 @@
__all__ = [
- # Exceptions
- "CalendarStoreError",
- "NameNotAllowedError",
- "CalendarNameNotAllowedError",
- "CalendarObjectNameNotAllowedError",
- "AlreadyExistsError",
- "CalendarAlreadyExistsError",
- "CalendarObjectNameAlreadyExistsError",
- "CalendarObjectUIDAlreadyExistsError",
- "NotFoundError",
- "NoSuchCalendarError",
- "NoSuchCalendarObjectError",
- "InvalidCalendarComponentError",
- "InternalDataStoreError",
-
# Classes
"ICalendarStore",
"ICalendarHome",
@@ -58,76 +43,6 @@
# from txdav.idav import ITransaction
#
-# Exceptions
-#
-
-class CalendarStoreError(RuntimeError):
- """
- Calendar store generic error.
- """
-
-class NameNotAllowedError(CalendarStoreError):
- """
- Attempt to create an object with a name that is not allowed.
- """
-
-class CalendarNameNotAllowedError(NameNotAllowedError):
- """
- Calendar name not allowed.
- """
-
-class CalendarObjectNameNotAllowedError(NameNotAllowedError):
- """
- Calendar object name not allowed.
- """
-
-class AlreadyExistsError(CalendarStoreError):
- """
- Attempt to create an object that already exists.
- """
-
-class CalendarAlreadyExistsError(AlreadyExistsError):
- """
- Calendar already exists.
- """
-
-class CalendarObjectNameAlreadyExistsError(AlreadyExistsError):
- """
- A calendar object with the requested name already exists.
- """
-
-class CalendarObjectUIDAlreadyExistsError(AlreadyExistsError):
- """
- A calendar object with the requested UID already exists.
- """
-
-class NotFoundError(CalendarStoreError):
- """
- Requested data not found.
- """
-
-class NoSuchCalendarError(NotFoundError):
- """
- The requested calendar does not exist.
- """
-
-class NoSuchCalendarObjectError(NotFoundError):
- """
- The requested calendar object does not exist.
- """
-
-class InvalidCalendarComponentError(CalendarStoreError):
- """
- Invalid calendar component.
- """
-
-class InternalDataStoreError(CalendarStoreError):
- """
- Uh, oh.
- """
-
-
-#
# Interfaces
#
Modified: CalendarServer/branches/new-store/txcarddav/addressbookstore/file.py
===================================================================
--- CalendarServer/branches/new-store/txcarddav/addressbookstore/file.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txcarddav/addressbookstore/file.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -26,96 +26,27 @@
"AddressBookObject",
]
-from uuid import uuid4
-from errno import EEXIST, ENOENT
+from errno import ENOENT
-from zope.interface import implements
-
-from twisted.python.util import FancyEqMixin
-
-from twisted.internet.defer import succeed
-
-from twisted.python import log
-from twext.python.log import LoggingMixIn
from twext.web2.dav.element.rfc2518 import ResourceType
-from txdav.propertystore.xattr import PropertyStore
-from txdav.propertystore.base import PropertyName
-PN = PropertyName.fromString
+from twistedcaldav.vcard import Component as VComponent, InvalidVCardDataError
+from twistedcaldav.vcardindex import AddressBookIndex as OldIndex
-from txcarddav.iaddressbookstore import IAddressBookStoreTransaction
-from txcarddav.iaddressbookstore import IAddressBookStore, IAddressBookHome
from txcarddav.iaddressbookstore import IAddressBook, IAddressBookObject
-from txcarddav.iaddressbookstore import AddressBookNameNotAllowedError
-from txcarddav.iaddressbookstore import AddressBookObjectNameNotAllowedError
-from txcarddav.iaddressbookstore import AddressBookAlreadyExistsError
-from txcarddav.iaddressbookstore import AddressBookObjectNameAlreadyExistsError
-from txcarddav.iaddressbookstore import NoSuchAddressBookError
-from txcarddav.iaddressbookstore import NoSuchAddressBookObjectError
-from txcarddav.iaddressbookstore import InvalidAddressBookComponentError
-from txcarddav.iaddressbookstore import InternalDataStoreError
+from txcarddav.iaddressbookstore import IAddressBookStore, IAddressBookHome
+from txcarddav.iaddressbookstore import IAddressBookStoreTransaction
-from twistedcaldav.customxml import GETCTag
+from txdav.common.datastore.file import CommonDataStore, CommonHome,\
+ CommonStoreTransaction, CommonHomeChild, CommonObjectResource
+from txdav.common.icommondatastore import InvalidObjectResourceError,\
+ NoSuchObjectResourceError, InternalDataStoreError
+from txdav.datastore.file import hidden, writeOperation
-from twistedcaldav.vcardindex import AddressBookIndex as OldIndex
-from twistedcaldav.vcard import Component as VComponent, InvalidVCardDataError
+from zope.interface import implements
-def _isValidName(name):
+class AddressBookStore(CommonDataStore):
"""
- Determine if the given string is a valid name. i.e. does it conflict with
- any of the other entities which may be on the filesystem?
-
- @param name: a name which might be given to a addressbook.
- """
- return not name.startswith(".")
-
-
-def _hidden(path):
- return path.sibling('.' + path.basename())
-
-
-_unset = object()
-
-class _cached(object):
- """
- This object is a decorator for a 0-argument method which should be called
- only once, and its result cached so that future invocations just return the
- same result without calling the underlying method again.
-
- @ivar thunk: the function to call to generate a cached value.
- """
-
- def __init__(self, thunk):
- self.thunk = thunk
-
-
- def __get__(self, oself, owner):
- def inner():
- cacheKey = "_" + self.thunk.__name__ + "_cached"
- cached = getattr(oself, cacheKey, _unset)
- if cached is _unset:
- value = self.thunk(oself)
- setattr(oself, cacheKey, value)
- return value
- else:
- return cached
- return inner
-
-
-
-def _writeOperation(thunk):
- # FIXME: tests
- def inner(self, *a, **kw):
- if self._transaction._termination is not None:
- raise RuntimeError(
- "%s.%s is a write operation, but transaction already %s"
- % (self, thunk.__name__, self._transaction._termination))
- return thunk(self, *a, **kw)
- return inner
-
-
-class AddressBookStore(LoggingMixIn):
- """
An implementation of L{IAddressBookObject} backed by a
L{twext.python.filepath.CachingFilePath}.
@@ -130,45 +61,12 @@
@param path: a L{FilePath} pointing at a directory on disk.
"""
- self._path = path
+ super(AddressBookStore, self).__init__(path)
+ self._transactionClass = AddressBookStoreTransaction
-# if not path.isdir():
- # FIXME: Add AddressBookStoreNotFoundError?
-# raise NotFoundError("No such addressbook store")
- def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self._path.path)
-
- def newTransaction(self):
- """
- Create a new filesystem-based transaction.
-
- @see Transaction
- """
- return Transaction(self)
-
-
-
-class _CommitTracker(object):
+class AddressBookStoreTransaction(CommonStoreTransaction):
"""
- Diagnostic tool to find transactions that were never committed.
- """
-
- def __init__(self):
- self.done = False
- self.info = []
-
- def __del__(self):
- if not self.done and self.info:
- print '**** UNCOMMITTED TRANSACTION BEING GARBAGE COLLECTED ****'
- for info in self.info:
- print ' ', info
- print '---- END OF OPERATIONS'
-
-
-
-class Transaction(LoggingMixIn):
- """
In-memory implementation of
Note that this provides basic 'undo' support, but not truly transactional
@@ -186,270 +84,40 @@
@type addressbookStore: L{AddressBookStore}
"""
- self._addressbookStore = addressbookStore
- self._termination = None
- self._operations = []
- self._tracker = _CommitTracker()
- self._addressbookHomes = {}
+ super(AddressBookStoreTransaction, self).__init__(addressbookStore)
+ self._homeClass = AddressBookHome
- def addOperation(self, operation, name):
- self._operations.append(operation)
- self._tracker.info.append(name)
+ addressbookHomeWithUID = CommonStoreTransaction.homeWithUID
+ def creatingHome(self, home):
+ home.createAddressBookWithName("addressbook")
- def _terminate(self, mode):
- """
- Check to see if this transaction has already been terminated somehow,
- either via committing or aborting, and if not, note that it has been
- terminated.
-
- @param mode: The manner of the termination of this transaction.
-
- @type mode: C{str}
-
- @raise RuntimeError: This transaction has already been terminated.
- """
- if self._termination is not None:
- raise RuntimeError("already %s" % (self._termination,))
- self._termination = mode
- self._tracker.done = True
-
-
- def abort(self):
- self._terminate("aborted")
-
-
- def commit(self):
- self._terminate("committed")
-
- self.committed = True
- undos = []
-
- for operation in self._operations:
- try:
- undo = operation()
- if undo is not None:
- undos.append(undo)
- except:
- log.err()
- for undo in undos:
- try:
- undo()
- except:
- log.err()
- raise
-
-
- def addressbookHomeWithUID(self, uid, create=False):
- if (uid, self) in self._addressbookHomes:
- return self._addressbookHomes[(uid, self)]
-
- if uid.startswith("."):
- return None
-
- assert len(uid) >= 4
-
- childPath1 = self._addressbookStore._path.child(uid[0:2])
- childPath2 = childPath1.child(uid[2:4])
- childPath3 = childPath2.child(uid)
- def createDirectory(path):
- try:
- path.createDirectory()
- except (IOError, OSError), e:
- if e.errno != EEXIST:
- # Ignore, in case someone else created the
- # directory while we were trying to as well.
- raise
-
- creating = False
- if create:
- if not childPath2.isdir():
- if not childPath1.isdir():
- createDirectory(childPath1)
- createDirectory(childPath2)
- if childPath3.isdir():
- addressbookPath = childPath3
- else:
- creating = True
- addressbookPath = childPath3.temporarySibling()
- createDirectory(addressbookPath)
- def do():
- def lastly():
- addressbookPath.moveTo(childPath3)
- # addressbookHome._path = addressbookPath
- # do this _after_ all other file operations
- addressbookHome._path = childPath3
- return lambda : None
- self.addOperation(lastly, "create home finalize")
- return lambda : None
- self.addOperation(do, "create home UID %r" % (uid,))
-
- elif not childPath3.isdir():
- return None
- else:
- addressbookPath = childPath3
-
- addressbookHome = AddressBookHome(addressbookPath, self._addressbookStore, self)
- self._addressbookHomes[(uid, self)] = addressbookHome
- if creating:
- addressbookHome.createAddressBookWithName("addressbook")
- return addressbookHome
-
-
-
-class AddressBookHome(LoggingMixIn):
+class AddressBookHome(CommonHome):
implements(IAddressBookHome)
def __init__(self, path, addressbookStore, transaction):
- self._addressbookStore = addressbookStore
- self._path = path
- self._transaction = transaction
- self._newAddressBooks = {}
- self._removedAddressBooks = set()
+ super(AddressBookHome, self).__init__(path, addressbookStore, transaction)
+ self._childClass = AddressBook
- def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self._path)
+ addressbooks = CommonHome.children
+ addressbookWithName = CommonHome.childWithName
+ createAddressBookWithName = CommonHome.createChildWithName
+ removeAddressBookWithName = CommonHome.removeChildWithName
+ @property
+ def _addressbookStore(self):
+ return self._dataStore
- def uid(self):
- return self._path.basename()
- def _updateSyncToken(self, reset=False):
- "Stub for updating sync token."
- # FIXME: actually update something
-
-
- def addressbooks(self):
- return set(self._newAddressBooks.itervalues()) | set(
- self.addressbookWithName(name)
- for name in self._path.listdir()
- if not name.startswith(".")
- )
-
- def addressbookWithName(self, name):
- addressbook = self._newAddressBooks.get(name)
- if addressbook is not None:
- return addressbook
- if name in self._removedAddressBooks:
- return None
-
- if name.startswith("."):
- return None
-
- childPath = self._path.child(name)
- if childPath.isdir():
- return AddressBook(name, self)
- else:
- return None
-
-
- @_writeOperation
- def createAddressBookWithName(self, name):
- if name.startswith("."):
- raise AddressBookNameNotAllowedError(name)
-
- childPath = self._path.child(name)
-
- if name not in self._removedAddressBooks and childPath.isdir():
- raise AddressBookAlreadyExistsError(name)
-
- temporary = _hidden(childPath.temporarySibling())
- temporary.createDirectory()
- # In order for the index to work (which is doing real file ops on disk
- # via SQLite) we need to create a real directory _immediately_.
-
- # FIXME: some way to roll this back.
-
- c = self._newAddressBooks[name] = AddressBook(temporary.basename(), self, name)
- c.retrieveOldIndex().create()
- def do():
- try:
- props = c.properties()
- temporary.moveTo(childPath)
- c._name = name
- # FIXME: _lots_ of duplication of work here.
- props.path = childPath
- props.flush()
- except (IOError, OSError), e:
- if e.errno == EEXIST and childPath.isdir():
- raise AddressBookAlreadyExistsError(name)
- raise
- # FIXME: direct tests, undo for index creation
- # Return undo
- return lambda: childPath.remove()
-
- self._transaction.addOperation(do, "create addressbook %r" % (name,))
- props = c.properties()
- AddressBookType = ResourceType.addressbook #@UndefinedVariable
- props[PN(ResourceType.sname())] = AddressBookType
-
- # FIXME: there's no need for 'flush' to be a public method of the
- # property store any more. It should just be transactional like
- # everything else; the API here would better be expressed as
- # c.properties().participateInTxn(txn)
- # FIXME: return c # maybe ?
-
- @_writeOperation
- def removeAddressBookWithName(self, name):
- if name.startswith(".") or name in self._removedAddressBooks:
- raise NoSuchAddressBookError(name)
-
- self._removedAddressBooks.add(name)
- childPath = self._path.child(name)
- if name not in self._newAddressBooks and not childPath.isdir():
- raise NoSuchAddressBookError(name)
-
- def do(transaction=self._transaction):
- for i in xrange(1000):
- trash = childPath.sibling("._del_%s_%d" % (childPath.basename(), i))
- if not trash.exists():
- break
- else:
- raise InternalDataStoreError("Unable to create trash target for addressbook at %s" % (childPath,))
-
- try:
- childPath.moveTo(trash)
- except (IOError, OSError), e:
- if e.errno == ENOENT:
- raise NoSuchAddressBookError(name)
- raise
-
- def cleanup():
- try:
- trash.remove()
- except Exception, e:
- self.log_error("Unable to delete trashed addressbook at %s: %s" % (trash.fp, e))
-
- transaction.addOperation(cleanup, "remove addressbook %r" % (name,))
-
- def undo():
- trash.moveTo(childPath)
-
- return undo
-
-
- # @_cached
- def properties(self):
- # FIXME: needs tests for actual functionality
- # FIXME: needs to be cached
- # FIXME: transaction tests
- props = PropertyStore(self._path)
- self._transaction.addOperation(props.flush, "flush home properties")
- return props
-
-
-
-class AddressBook(LoggingMixIn, FancyEqMixin):
+class AddressBook(CommonHomeChild):
"""
File-based implementation of L{IAddressBook}.
"""
implements(IAddressBook)
- compareAttributes = '_name _addressbookHome _transaction'.split()
-
def __init__(self, name, addressbookHome, realName=None):
"""
Initialize an addressbook pointing at a path on disk.
@@ -465,234 +133,80 @@
will eventually have on disk.
@type realName: C{str}
"""
- self._name = name
- self._addressbookHome = addressbookHome
- self._transaction = addressbookHome._transaction
- self._newAddressBookObjects = {}
- self._cachedAddressBookObjects = {}
- self._removedAddressBookObjects = set()
+
+ super(AddressBook, self).__init__(name, addressbookHome, realName)
+
self._index = Index(self)
- self._renamedName = realName
+ self._objectResourceClass = AddressBookObject
-
@property
- def _path(self):
- return self._addressbookHome._path.child(self._name)
+ def _addressbookHome(self):
+ return self._home
+ def resourceType(self):
+ return ResourceType.addressbook
- def retrieveOldIndex(self):
- """
- Retrieve the old Index object.
- """
- return self._index._oldIndex
+ addressbooks = CommonHome.children
+ ownerAddressBookHome = CommonHomeChild.ownerHome
+ addressbookObjects = CommonHomeChild.objectResources
+ addressbookObjectWithName = CommonHomeChild.objectResourceWithName
+ addressbookObjectWithUID = CommonHomeChild.objectResourceWithUID
+ createAddressBookObjectWithName = CommonHomeChild.createObjectResourceWithName
+ removeAddressBookObjectWithName = CommonHomeChild.removeObjectResourceWithName
+ removeAddressBookObjectWithUID = CommonHomeChild.removeObjectResourceWithUID
+ addressbookObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
- def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self._path.path)
-
-
- def name(self):
- if self._renamedName is not None:
- return self._renamedName
- return self._path.basename()
-
-
- _renamedName = None
-
- @_writeOperation
- def rename(self, name):
- oldName = self.name()
- self._renamedName = name
- self._addressbookHome._newAddressBooks[name] = self
- self._addressbookHome._removedAddressBooks.add(oldName)
- def doIt():
- self._path.moveTo(self._path.sibling(name))
- return lambda : None # FIXME: revert
- self._transaction.addOperation(doIt, "rename addressbook %r -> %r" %
- (oldName, name))
-
-
- def ownerAddressBookHome(self):
- return self._addressbookHome
-
-
- def addressbookObjects(self):
- return sorted((
- self.addressbookObjectWithName(name)
- for name in (
- set(self._newAddressBookObjects.iterkeys()) |
- set(name for name in self._path.listdir()
- if not name.startswith(".")) -
- set(self._removedAddressBookObjects)
- )),
- key=lambda calObj: calObj.name()
- )
-
-
- def addressbookObjectWithName(self, name):
- if name in self._removedAddressBookObjects:
- return None
- if name in self._newAddressBookObjects:
- return self._newAddressBookObjects[name]
- if name in self._cachedAddressBookObjects:
- return self._cachedAddressBookObjects[name]
-
- addressbookObjectPath = self._path.child(name)
- if addressbookObjectPath.isfile():
- obj = AddressBookObject(name, self)
- self._cachedAddressBookObjects[name] = obj
- return obj
- else:
- return None
-
-
- def addressbookObjectWithUID(self, uid):
- # FIXME: This _really_ needs to be inspecting an index, not parsing
- # every resource.
- for addressbookObjectPath in self._path.children():
- if not _isValidName(addressbookObjectPath.basename()):
- continue
- obj = AddressBookObject(addressbookObjectPath.basename(), self)
- if obj.component().resourceUID() == uid:
- if obj.name() in self._removedAddressBookObjects:
- return None
- return obj
-
-
- @_writeOperation
- def createAddressBookObjectWithName(self, name, component):
- if name.startswith("."):
- raise AddressBookObjectNameNotAllowedError(name)
-
- addressbookObjectPath = self._path.child(name)
- if addressbookObjectPath.exists():
- raise AddressBookObjectNameAlreadyExistsError(name)
-
- addressbookObject = AddressBookObject(name, self)
- addressbookObject.setComponent(component)
- self._cachedAddressBookObjects[name] = addressbookObject
-
-
- @_writeOperation
- def removeAddressBookObjectWithName(self, name):
- if name.startswith("."):
- raise NoSuchAddressBookObjectError(name)
-
- addressbookObjectPath = self._path.child(name)
- if addressbookObjectPath.isfile():
- self._removedAddressBookObjects.add(name)
- # FIXME: test for undo
- def do():
- addressbookObjectPath.remove()
- return lambda: None
- self._transaction.addOperation(do, "remove addressbook object %r" %
- (name,))
- else:
- raise NoSuchAddressBookObjectError(name)
-
-
- @_writeOperation
- def removeAddressBookObjectWithUID(self, uid):
- self.removeAddressBookObjectWithName(
- self.addressbookObjectWithUID(uid)._path.basename())
-
-
- def syncToken(self):
- raise NotImplementedError()
-
-
- def _updateSyncToken(self, reset=False):
- # FIXME: add locking a-la CalDAVFile.bumpSyncToken
- # FIXME: tests for desired concurrency properties
- ctag = PropertyName.fromString(GETCTag.sname())
- props = self.properties()
- token = props.get(ctag)
- if token is None or reset:
- adbkuuid = uuid4()
- revision = 1
- else:
- adbkuuid, revision = token.split("#", 1)
- revision = int(revision) + 1
- token = "%s#%d" % (adbkuuid, revision)
- props[ctag] = GETCTag(token)
- # FIXME: no direct tests for commit
- succeed(token)
-
-
- def addressbookObjectsSinceToken(self, token):
- raise NotImplementedError()
-
-
- # FIXME: property writes should be a write operation
- @_cached
- def properties(self):
- # FIXME: needs direct tests - only covered by addressbook store tests
- # FIXME: transactions
- props = PropertyStore(self._path)
- self._transaction.addOperation(props.flush, "flush addressbook properties")
- return props
-
-
def _doValidate(self, component):
component.validForCardDAV()
-
-class AddressBookObject(LoggingMixIn):
+class AddressBookObject(CommonObjectResource):
"""
- @ivar _path: The path of the .vcf file on disk
-
- @type _path: L{FilePath}
"""
implements(IAddressBookObject)
def __init__(self, name, addressbook):
- self._name = name
- self._addressbook = addressbook
- self._transaction = addressbook._transaction
- self._component = None
+ super(AddressBookObject, self).__init__(name, addressbook)
+
@property
- def _path(self):
- return self._addressbook._path.child(self._name)
+ def _addressbook(self):
+ return self._parentCollection
-
- def __repr__(self):
- return "<%s: %s>" % (self.__class__.__name__, self._path.path)
-
-
- def name(self):
- return self._path.basename()
-
-
- @_writeOperation
+ @writeOperation
def setComponent(self, component):
if not isinstance(component, VComponent):
raise TypeError(type(component))
try:
if component.resourceUID() != self.uid():
- raise InvalidAddressBookComponentError(
+ raise InvalidObjectResourceError(
"UID may not change (%s != %s)" % (
component.resourceUID(), self.uid()
)
)
- except NoSuchAddressBookObjectError:
+ except NoSuchObjectResourceError:
pass
try:
self._addressbook._doValidate(component)
except InvalidVCardDataError, e:
- raise InvalidAddressBookComponentError(e)
+ raise InvalidObjectResourceError(e)
+ newRevision = self._addressbook._updateSyncToken() # FIXME: test
+ self._addressbook.retrieveOldIndex().addResource(
+ self.name(), component, newRevision
+ )
+
self._component = component
# FIXME: needs to clear text cache
def do():
backup = None
if self._path.exists():
- backup = _hidden(self._path.temporarySibling())
+ backup = hidden(self._path.temporarySibling())
self._path.moveTo(backup)
fh = self._path.open("w")
try:
@@ -708,10 +222,12 @@
self._path.remove()
return undo
self._transaction.addOperation(do, "set addressbook component %r" % (self.name(),))
+
# Mark all properties as dirty, so they will be re-added to the
# temporary file when the main file is deleted. NOTE: if there were a
# temporary file and a rename() as there should be, this should really
# happen after the write but before the rename.
+
self.properties().update(self.properties())
# FIXME: the property store's flush() method may already have been
# added to the transaction, but we need to add it again to make sure it
@@ -743,7 +259,7 @@
fh = self._path.open()
except IOError, e:
if e[0] == ENOENT:
- raise NoSuchAddressBookObjectError(self)
+ raise NoSuchObjectResourceError(self)
else:
raise
@@ -768,14 +284,7 @@
self._uid = self.component().resourceUID()
return self._uid
- @_cached
- def properties(self):
- props = PropertyStore(self._path)
- self._transaction.addOperation(props.flush, "object properties flush")
- return props
-
-
class Index(object):
#
# OK, here's where we get ugly.
Modified: CalendarServer/branches/new-store/txcarddav/addressbookstore/test/common.py
===================================================================
--- CalendarServer/branches/new-store/txcarddav/addressbookstore/test/common.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txcarddav/addressbookstore/test/common.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -25,11 +25,16 @@
from txdav.idav import IPropertyStore
from txdav.propertystore.base import PropertyName
+from txdav.common.icommondatastore import HomeChildNameAlreadyExistsError
+from txdav.common.icommondatastore import InvalidObjectResourceError
+from txdav.common.icommondatastore import NoSuchHomeChildError
+from txdav.common.icommondatastore import NoSuchObjectResourceError
+from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError
+
from txcarddav.iaddressbookstore import (
IAddressBookStore, IAddressBookStoreTransaction, IAddressBookObject, IAddressBookHome,
- IAddressBook, InvalidAddressBookComponentError,
- AddressBookObjectNameAlreadyExistsError, AddressBookAlreadyExistsError,
- NoSuchAddressBookError, NoSuchAddressBookObjectError)
+ IAddressBook,
+)
from twistedcaldav.vcard import Component as VComponent
from twext.python.filepath import CachingFilePath as FilePath
@@ -346,7 +351,7 @@
"""
for name in home1_addressbookNames:
self.assertRaises(
- AddressBookAlreadyExistsError,
+ HomeChildNameAlreadyExistsError,
self.homeUnderTest().createAddressBookWithName, name
)
@@ -369,7 +374,7 @@
Attempt to remove an non-existing addressbook should raise.
"""
home = self.homeUnderTest()
- self.assertRaises(NoSuchAddressBookError,
+ self.assertRaises(NoSuchHomeChildError,
home.removeAddressBookWithName, "xyzzy")
@@ -480,7 +485,7 @@
"""
addressbook = self.addressbookUnderTest()
self.assertRaises(
- NoSuchAddressBookObjectError,
+ NoSuchObjectResourceError,
addressbook.removeAddressBookObjectWithName, "xyzzy"
)
@@ -600,7 +605,7 @@
given name already exists in that addressbook.
"""
self.assertRaises(
- AddressBookObjectNameAlreadyExistsError,
+ ObjectResourceNameAlreadyExistsError,
self.addressbookUnderTest().createAddressBookObjectWithName,
"1.vcf", VComponent.fromString(vcard4_text)
)
@@ -613,7 +618,7 @@
text.
"""
self.assertRaises(
- InvalidAddressBookComponentError,
+ InvalidObjectResourceError,
self.addressbookUnderTest().createAddressBookObjectWithName,
"new", VComponent.fromString(vcard4notCardDAV_text)
)
@@ -626,7 +631,7 @@
"""
addressbookObject = self.addressbookObjectUnderTest()
self.assertRaises(
- InvalidAddressBookComponentError,
+ InvalidObjectResourceError,
addressbookObject.setComponent,
VComponent.fromString(vcard4notCardDAV_text)
)
@@ -641,7 +646,7 @@
component = VComponent.fromString(vcard4_text)
addressbookObject = addressbook1.addressbookObjectWithName("1.vcf")
self.assertRaises(
- InvalidAddressBookComponentError,
+ InvalidObjectResourceError,
addressbookObject.setComponent, component
)
Modified: CalendarServer/branches/new-store/txcarddav/addressbookstore/test/test_file.py
===================================================================
--- CalendarServer/branches/new-store/txcarddav/addressbookstore/test/test_file.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txcarddav/addressbookstore/test/test_file.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -23,11 +23,11 @@
from twistedcaldav.vcard import Component as VComponent
-from txcarddav.iaddressbookstore import AddressBookNameNotAllowedError
-from txcarddav.iaddressbookstore import AddressBookObjectNameNotAllowedError
-from txcarddav.iaddressbookstore import AddressBookObjectUIDAlreadyExistsError
-from txcarddav.iaddressbookstore import NoSuchAddressBookError
-from txcarddav.iaddressbookstore import NoSuchAddressBookObjectError
+from txdav.common.icommondatastore import HomeChildNameNotAllowedError
+from txdav.common.icommondatastore import ObjectResourceNameNotAllowedError
+from txdav.common.icommondatastore import ObjectResourceUIDAlreadyExistsError
+from txdav.common.icommondatastore import NoSuchHomeChildError
+from txdav.common.icommondatastore import NoSuchObjectResourceError
from txcarddav.addressbookstore.file import AddressBookStore, AddressBookHome
from txcarddav.addressbookstore.file import AddressBook, AddressBookObject
@@ -134,7 +134,7 @@
implementation, so no addressbook names may start with ".".
"""
self.assertRaises(
- AddressBookNameNotAllowedError,
+ HomeChildNameNotAllowedError,
self.home1.createAddressBookWithName, ".foo"
)
@@ -147,7 +147,7 @@
name = ".foo"
self.home1._path.child(name).createDirectory()
self.assertRaises(
- NoSuchAddressBookError,
+ NoSuchHomeChildError,
self.home1.removeAddressBookWithName, name
)
@@ -231,7 +231,7 @@
".".
"""
self.assertRaises(
- AddressBookObjectNameNotAllowedError,
+ ObjectResourceNameNotAllowedError,
self.addressbook1.createAddressBookObjectWithName,
".foo", VComponent.fromString(vcard4_text)
)
@@ -247,7 +247,7 @@
assert self.addressbook1.addressbookObjectWithName(name) is None
component = VComponent.fromString(vcard1modified_text)
self.assertRaises(
- AddressBookObjectUIDAlreadyExistsError,
+ ObjectResourceUIDAlreadyExistsError,
self.addressbook1.createAddressBookObjectWithName,
name, component
)
@@ -273,7 +273,7 @@
name = ".foo"
self.addressbook1._path.child(name).touch()
self.assertRaises(
- NoSuchAddressBookObjectError,
+ NoSuchObjectResourceError,
self.addressbook1.removeAddressBookObjectWithName, name
)
@@ -319,7 +319,6 @@
raise RuntimeError("oops")
self.txn.addOperation(fail, "dummy failing operation")
self.assertRaises(RuntimeError, self.txn.commit)
- self.assertEquals(len(self.flushLoggedErrors(RuntimeError)), 1)
self._refresh()
@@ -366,7 +365,7 @@
Attempt to remove an non-existing addressbook object should raise.
"""
self.assertRaises(
- NoSuchAddressBookObjectError,
+ NoSuchObjectResourceError,
self.addressbook1.removeAddressBookObjectWithUID, "xyzzy"
)
Modified: CalendarServer/branches/new-store/txcarddav/iaddressbookstore.py
===================================================================
--- CalendarServer/branches/new-store/txcarddav/iaddressbookstore.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txcarddav/iaddressbookstore.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -25,21 +25,6 @@
from txdav.idav import ITransaction
__all__ = [
- # Exceptions
- "AddressBookStoreError",
- "NameNotAllowedError",
- "AddressBookNameNotAllowedError",
- "AddressBookObjectNameNotAllowedError",
- "AlreadyExistsError",
- "AddressBookAlreadyExistsError",
- "AddressBookObjectNameAlreadyExistsError",
- "AddressBookObjectUIDAlreadyExistsError",
- "NotFoundError",
- "NoSuchAddressBookError",
- "NoSuchAddressBookObjectError",
- "InvalidAddressBookComponentError",
- "InternalDataStoreError",
-
# Classes
"IAddressBookStore",
"IAddressBookHome",
@@ -48,76 +33,6 @@
]
#
-# Exceptions
-#
-
-class AddressBookStoreError(RuntimeError):
- """
- AddressBook store generic error.
- """
-
-class NameNotAllowedError(AddressBookStoreError):
- """
- Attempt to create an object with a name that is not allowed.
- """
-
-class AddressBookNameNotAllowedError(NameNotAllowedError):
- """
- AddressBook name not allowed.
- """
-
-class AddressBookObjectNameNotAllowedError(NameNotAllowedError):
- """
- AddressBook object name not allowed.
- """
-
-class AlreadyExistsError(AddressBookStoreError):
- """
- Attempt to create an object that already exists.
- """
-
-class AddressBookAlreadyExistsError(AlreadyExistsError):
- """
- AddressBook already exists.
- """
-
-class AddressBookObjectNameAlreadyExistsError(AlreadyExistsError):
- """
- A addressbook object with the requested name already exists.
- """
-
-class AddressBookObjectUIDAlreadyExistsError(AlreadyExistsError):
- """
- A addressbook object with the requested UID already exists.
- """
-
-class NotFoundError(AddressBookStoreError):
- """
- Requested data not found.
- """
-
-class NoSuchAddressBookError(NotFoundError):
- """
- The requested addressbook does not exist.
- """
-
-class NoSuchAddressBookObjectError(NotFoundError):
- """
- The requested addressbook object does not exist.
- """
-
-class InvalidAddressBookComponentError(AddressBookStoreError):
- """
- Invalid addressbook component.
- """
-
-class InternalDataStoreError(AddressBookStoreError):
- """
- Uh, oh.
- """
-
-
-#
# Interfaces
#
Added: CalendarServer/branches/new-store/txdav/common/__init__.py
===================================================================
Added: CalendarServer/branches/new-store/txdav/common/datastore/__init__.py
===================================================================
Added: CalendarServer/branches/new-store/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/new-store/txdav/common/datastore/file.py (rev 0)
+++ CalendarServer/branches/new-store/txdav/common/datastore/file.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -0,0 +1,528 @@
+# -*- test-case-name: txdav.datastore.test.test_file -*-
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from errno import EEXIST, ENOENT
+
+from twext.python.log import LoggingMixIn
+from twext.web2.dav.element.rfc2518 import ResourceType
+
+from txdav.datastore.file import DataStoreTransaction, DataStore, writeOperation,\
+ hidden, isValidName, cached
+from txdav.propertystore.base import PropertyName
+from txdav.propertystore.xattr import PropertyStore
+from txdav.common.icommondatastore import HomeChildNameNotAllowedError,\
+ HomeChildNameAlreadyExistsError, NoSuchHomeChildError,\
+ InternalDataStoreError, ObjectResourceNameNotAllowedError,\
+ ObjectResourceNameAlreadyExistsError, NoSuchObjectResourceError
+from twisted.python.util import FancyEqMixin
+from twistedcaldav.customxml import GETCTag
+from uuid import uuid4
+
+"""
+Common utility functions for a file based datastore.
+"""
+
+class CommonDataStore(DataStore):
+ """
+ Generic data store.
+ """
+ pass
+
+class CommonStoreTransaction(DataStoreTransaction):
+ """
+ In-memory implementation of
+
+ Note that this provides basic 'undo' support, but not truly transactional
+ operations.
+ """
+
+ _homeClass = None
+
+ def __init__(self, dataStore):
+ """
+ Initialize a transaction; do not call this directly, instead call
+ L{DataStore.newTransaction}.
+
+ @param dataStore: The store that created this transaction.
+
+ @type dataStore: L{DataStore}
+ """
+ super(CommonStoreTransaction, self).__init__(dataStore)
+ self._homes = {}
+
+
+ def homeWithUID(self, uid, create=False):
+ if (uid, self) in self._homes:
+ return self._homes[(uid, self)]
+
+ if uid.startswith("."):
+ return None
+
+ assert len(uid) >= 4
+
+ childPath1 = self._dataStore._path.child(uid[0:2])
+ childPath2 = childPath1.child(uid[2:4])
+ childPath3 = childPath2.child(uid)
+ def createDirectory(path):
+ try:
+ path.createDirectory()
+ except (IOError, OSError), e:
+ if e.errno != EEXIST:
+ # Ignore, in case someone else created the
+ # directory while we were trying to as well.
+ raise
+
+ creating = False
+ if create:
+ if not childPath2.isdir():
+ if not childPath1.isdir():
+ createDirectory(childPath1)
+ createDirectory(childPath2)
+ if childPath3.isdir():
+ homePath = childPath3
+ else:
+ creating = True
+ homePath = childPath3.temporarySibling()
+ createDirectory(homePath)
+ def do():
+ def lastly():
+ homePath.moveTo(childPath3)
+ # home._path = homePath
+ # do this _after_ all other file operations
+ home._path = childPath3
+ return lambda : None
+ self.addOperation(lastly, "create home finalize")
+ return lambda : None
+ self.addOperation(do, "create home UID %r" % (uid,))
+
+ elif not childPath3.isdir():
+ return None
+ else:
+ homePath = childPath3
+
+ home = self._homeClass(homePath, self._dataStore, self)
+ self._homes[(uid, self)] = home
+ if creating:
+ self.creatingHome(home)
+ return home
+
+ def creatingHome(self, home):
+ raise NotImplementedError
+
+class CommonHome(LoggingMixIn):
+
+ _childClass = None
+
+ def __init__(self, path, dataStore, transaction):
+ self._dataStore = dataStore
+ self._path = path
+ self._transaction = transaction
+ self._newChildren = {}
+ self._removedChildren = set()
+ self._cachedChildren = {}
+
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self._path)
+
+
+ def uid(self):
+ return self._path.basename()
+
+
+ def _updateSyncToken(self, reset=False):
+ "Stub for updating sync token."
+ # FIXME: actually update something
+
+
+ def children(self):
+ return set(self._newChildren.itervalues()) | set(
+ self.childWithName(name)
+ for name in self._path.listdir()
+ if not name.startswith(".")
+ )
+
+ def childWithName(self, name):
+ child = self._newChildren.get(name)
+ if child is not None:
+ return child
+ if name in self._removedChildren:
+ return None
+ if name in self._cachedChildren:
+ return self._cachedChildren[name]
+
+ if name.startswith("."):
+ return None
+
+ childPath = self._path.child(name)
+ if childPath.isdir():
+ existingChild = self._childClass(name, self)
+ self._cachedChildren[name] = existingChild
+ return existingChild
+ else:
+ return None
+
+
+ @writeOperation
+ def createChildWithName(self, name):
+ if name.startswith("."):
+ raise HomeChildNameNotAllowedError(name)
+
+ childPath = self._path.child(name)
+
+ if name not in self._removedChildren and childPath.isdir():
+ raise HomeChildNameAlreadyExistsError(name)
+
+ temporary = hidden(childPath.temporarySibling())
+ temporary.createDirectory()
+ # In order for the index to work (which is doing real file ops on disk
+ # via SQLite) we need to create a real directory _immediately_.
+
+ # FIXME: some way to roll this back.
+
+ c = self._newChildren[name] = self._childClass(temporary.basename(), self, name)
+ c.retrieveOldIndex().create()
+ def do():
+ try:
+ props = c.properties()
+ temporary.moveTo(childPath)
+ c._name = name
+ # FIXME: _lots_ of duplication of work here.
+ props.path = childPath
+ props.flush()
+ except (IOError, OSError), e:
+ if e.errno == EEXIST and childPath.isdir():
+ raise HomeChildNameAlreadyExistsError(name)
+ raise
+ # FIXME: direct tests, undo for index creation
+ # Return undo
+ return lambda: childPath.remove()
+
+ self._transaction.addOperation(do, "create child %r" % (name,))
+ props = c.properties()
+ props[PropertyName(*ResourceType.qname())] = c.resourceType()
+ self.createdChild(c)
+
+ def createdChild(self, child):
+ pass
+
+ @writeOperation
+ def removeChildWithName(self, name):
+ if name.startswith(".") or name in self._removedChildren:
+ raise NoSuchHomeChildError(name)
+
+ self._removedChildren.add(name)
+ childPath = self._path.child(name)
+ if name not in self._newChildren and not childPath.isdir():
+ raise NoSuchHomeChildError(name)
+
+ def do(transaction=self._transaction):
+ for i in xrange(1000):
+ trash = childPath.sibling("._del_%s_%d" % (childPath.basename(), i))
+ if not trash.exists():
+ break
+ else:
+ raise InternalDataStoreError("Unable to create trash target for child at %s" % (childPath,))
+
+ try:
+ childPath.moveTo(trash)
+ except (IOError, OSError), e:
+ if e.errno == ENOENT:
+ raise NoSuchHomeChildError(name)
+ raise
+
+ def cleanup():
+ try:
+ trash.remove()
+ except Exception, e:
+ self.log_error("Unable to delete trashed child at %s: %s" % (trash.fp, e))
+
+ transaction.addOperation(cleanup, "remove child backup %r" % (name,))
+
+ def undo():
+ trash.moveTo(childPath)
+
+ return undo
+
+ # FIXME: direct tests
+ self._transaction.addOperation(
+ do, "prepare child remove %r" % (name,)
+ )
+
+ # @cached
+ def properties(self):
+ # FIXME: needs tests for actual functionality
+ # FIXME: needs to be cached
+ # FIXME: transaction tests
+ props = PropertyStore(self._path)
+ self._transaction.addOperation(props.flush, "flush home properties")
+ return props
+
+
+class CommonHomeChild(LoggingMixIn, FancyEqMixin):
+ """
+ """
+
+ compareAttributes = '_name _home _transaction'.split()
+
+ _objectResourceClass = None
+
+ def __init__(self, name, home, realName=None):
+ """
+ Initialize an home child pointing at a path on disk.
+
+ @param name: the subdirectory of home where this child
+ resides.
+ @type name: C{str}
+
+ @param home: the home containing this child.
+ @type home: L{CommonHome}
+
+ @param realName: If this child was just created, the name which it
+ will eventually have on disk.
+ @type realName: C{str}
+ """
+ self._name = name
+ self._home = home
+ self._transaction = home._transaction
+ self._newObjectResources = {}
+ self._cachedObjectResources = {}
+ self._removedObjectResources = set()
+ self._index = None # Derived classes need to set this
+ self._renamedName = realName
+
+
+ @property
+ def _path(self):
+ return self._home._path.child(self._name)
+
+
+ def resourceType(self):
+ return NotImplementedError
+
+ def retrieveOldIndex(self):
+ """
+ Retrieve the old Index object.
+ """
+ return self._index._oldIndex
+
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self._path.path)
+
+
+ def name(self):
+ if self._renamedName is not None:
+ return self._renamedName
+ return self._path.basename()
+
+
+ _renamedName = None
+
+ @writeOperation
+ def rename(self, name):
+ self._updateSyncToken()
+ oldName = self.name()
+ self._renamedName = name
+ self._home._newChildren[name] = self
+ self._home._removedChildren.add(oldName)
+ def doIt():
+ self._path.moveTo(self._path.sibling(name))
+ return lambda : None # FIXME: revert
+ self._transaction.addOperation(doIt, "rename home child %r -> %r" %
+ (oldName, name))
+
+
+ def ownerHome(self):
+ return self._home
+
+
+ def objectResources(self):
+ return sorted((
+ self.objectResourceWithName(name)
+ for name in (
+ set(self._newObjectResources.iterkeys()) |
+ set(name for name in self._path.listdir()
+ if not name.startswith(".")) -
+ set(self._removedObjectResources)
+ )),
+ key=lambda calObj: calObj.name()
+ )
+
+
+ def objectResourceWithName(self, name):
+ if name in self._removedObjectResources:
+ return None
+ if name in self._newObjectResources:
+ return self._newObjectResources[name]
+ if name in self._cachedObjectResources:
+ return self._cachedObjectResources[name]
+
+ objectResourcePath = self._path.child(name)
+ if objectResourcePath.isfile():
+ obj = self._objectResourceClass(name, self)
+ self._cachedObjectResources[name] = obj
+ return obj
+ else:
+ return None
+
+
+ def objectResourceWithUID(self, uid):
+ # FIXME: This _really_ needs to be inspecting an index, not parsing
+ # every resource.
+ for objectResourcePath in self._path.children():
+ if not isValidName(objectResourcePath.basename()):
+ continue
+ obj = self._objectResourceClass(objectResourcePath.basename(), self)
+ if obj.component().resourceUID() == uid:
+ if obj.name() in self._removedObjectResources:
+ return None
+ return obj
+
+
+ @writeOperation
+ def createObjectResourceWithName(self, name, component):
+ if name.startswith("."):
+ raise ObjectResourceNameNotAllowedError(name)
+
+ objectResourcePath = self._path.child(name)
+ if objectResourcePath.exists():
+ raise ObjectResourceNameAlreadyExistsError(name)
+
+ objectResource = self._objectResourceClass(name, self)
+ objectResource.setComponent(component)
+ self._cachedObjectResources[name] = objectResource
+
+
+ @writeOperation
+ def removeObjectResourceWithName(self, name):
+ if name.startswith("."):
+ raise NoSuchObjectResourceError(name)
+
+ newRevision = self._updateSyncToken() # FIXME: Test
+ self.retrieveOldIndex().deleteResource(name, newRevision)
+
+ objectResourcePath = self._path.child(name)
+ if objectResourcePath.isfile():
+ self._removedObjectResources.add(name)
+ # FIXME: test for undo
+ def do():
+ objectResourcePath.remove()
+ return lambda: None
+ self._transaction.addOperation(do, "remove object resource object %r" %
+ (name,))
+ else:
+ raise NoSuchObjectResourceError(name)
+
+
+ @writeOperation
+ def removeObjectResourceWithUID(self, uid):
+ self.removeObjectResourceWithName(
+ self.objectResourceWithUID(uid)._path.basename())
+
+
+ def syncToken(self):
+ raise NotImplementedError()
+
+
+ def _updateSyncToken(self, reset=False):
+ # FIXME: add locking a-la CalDAVFile.bumpSyncToken
+ # FIXME: tests for desired concurrency properties
+ ctag = PropertyName.fromString(GETCTag.sname())
+ props = self.properties()
+ token = props.get(ctag)
+ if token is None or reset:
+ tokenuuid = uuid4()
+ revision = 1
+ else:
+ # FIXME: no direct tests for update
+ token = str(token)
+ tokenuuid, revision = token.split("#", 1)
+ revision = int(revision) + 1
+ token = "%s#%d" % (tokenuuid, revision)
+ props[ctag] = GETCTag(token)
+ # FIXME: no direct tests for commit
+ return revision
+
+
+ def objectResourcesSinceToken(self, token):
+ raise NotImplementedError()
+
+
+ # FIXME: property writes should be a write operation
+ @cached
+ def properties(self):
+ # FIXME: needs direct tests - only covered by store tests
+ # FIXME: transactions
+ props = PropertyStore(self._path)
+ self._transaction.addOperation(props.flush, "flush object resource properties")
+ return props
+
+
+ def _doValidate(self, component):
+ raise NotImplementedError
+
+
+class CommonObjectResource(LoggingMixIn):
+ """
+ @ivar _path: The path of the file on disk
+
+ @type _path: L{FilePath}
+ """
+
+ def __init__(self, name, parent):
+ self._name = name
+ self._parentCollection = parent
+ self._transaction = parent._transaction
+ self._component = None
+
+
+ @property
+ def _path(self):
+ return self._parentCollection._path.child(self._name)
+
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self._path.path)
+
+
+ def name(self):
+ return self._path.basename()
+
+
+ @writeOperation
+ def setComponent(self, component):
+ raise NotImplementedError
+
+
+ def component(self):
+ raise NotImplementedError
+
+
+ def vCardText(self):
+ raise NotImplementedError
+
+
+ def uid(self):
+ raise NotImplementedError
+
+ @cached
+ def properties(self):
+ props = PropertyStore(self._path)
+ self._transaction.addOperation(props.flush, "object properties flush")
+ return props
+
Added: CalendarServer/branches/new-store/txdav/common/icommondatastore.py
===================================================================
--- CalendarServer/branches/new-store/txdav/common/icommondatastore.py (rev 0)
+++ CalendarServer/branches/new-store/txdav/common/icommondatastore.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -0,0 +1,105 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Common store interfaces
+"""
+
+__all__ = [
+ # Exceptions
+ "CommonStoreError",
+ "NameNotAllowedError",
+ "HomeChildNameNotAllowedError",
+ "ObjectResourceNameNotAllowedError",
+ "AlreadyExistsError",
+ "HomeChildNameAlreadyExistsError",
+ "ObjectResourceNameAlreadyExistsError",
+ "ObjectResourceUIDAlreadyExistsError",
+ "NotFoundError",
+ "NoSuchHomeChildError",
+ "NoSuchObjectResourceError",
+ "InvalidObjectResourceError",
+ "InternalDataStoreError",
+]
+
+#
+# Exceptions
+#
+
+class CommonStoreError(RuntimeError):
+ """
+ Store generic error.
+ """
+
+class NameNotAllowedError(CommonStoreError):
+ """
+ Attempt to create an object with a name that is not allowed.
+ """
+
+class HomeChildNameNotAllowedError(NameNotAllowedError):
+ """
+ Home child name not allowed.
+ """
+
+class ObjectResourceNameNotAllowedError(NameNotAllowedError):
+ """
+ Object resource name not allowed.
+ """
+
+class AlreadyExistsError(CommonStoreError):
+ """
+ Attempt to create an object that already exists.
+ """
+
+class HomeChildNameAlreadyExistsError(AlreadyExistsError):
+ """
+ Home child already exists.
+ """
+
+class ObjectResourceNameAlreadyExistsError(AlreadyExistsError):
+ """
+ An object resource with the requested name already exists.
+ """
+
+class ObjectResourceUIDAlreadyExistsError(AlreadyExistsError):
+ """
+ An object resource with the requested UID already exists.
+ """
+
+class NotFoundError(CommonStoreError):
+ """
+ Requested data not found.
+ """
+
+class NoSuchHomeChildError(NotFoundError):
+ """
+ The requested home child does not exist.
+ """
+
+class NoSuchObjectResourceError(NotFoundError):
+ """
+ The requested object resource does not exist.
+ """
+
+class InvalidObjectResourceError(CommonStoreError):
+ """
+ Invalid object resource data.
+ """
+
+class InternalDataStoreError(CommonStoreError):
+ """
+ Uh, oh.
+ """
Added: CalendarServer/branches/new-store/txdav/datastore/__init__.py
===================================================================
--- CalendarServer/branches/new-store/txdav/datastore/__init__.py (rev 0)
+++ CalendarServer/branches/new-store/txdav/datastore/__init__.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+WebDAV data store for Twisted.
+"""
Added: CalendarServer/branches/new-store/txdav/datastore/file.py
===================================================================
--- CalendarServer/branches/new-store/txdav/datastore/file.py (rev 0)
+++ CalendarServer/branches/new-store/txdav/datastore/file.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -0,0 +1,194 @@
+# -*- test-case-name: txdav.datastore.test.test_file -*-
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.log import LoggingMixIn
+
+"""
+Common utility functions for a file based datastore.
+"""
+
+def isValidName(name):
+ """
+ Determine if the given string is a valid name. i.e. does it conflict with
+ any of the other entities which may be on the filesystem?
+
+ @param name: a name which might be given to a calendar.
+ """
+ return not name.startswith(".")
+
+
+def hidden(path):
+ return path.sibling('.' + path.basename())
+
+
+_unset = object()
+
+class cached(object):
+ """
+ This object is a decorator for a 0-argument method which should be called
+ only once, and its result cached so that future invocations just return the
+ same result without calling the underlying method again.
+
+ @ivar thunk: the function to call to generate a cached value.
+ """
+
+ def __init__(self, thunk):
+ self.thunk = thunk
+
+
+ def __get__(self, oself, owner):
+ def inner():
+ cacheKey = "_" + self.thunk.__name__ + "_cached"
+ cached = getattr(oself, cacheKey, _unset)
+ if cached is _unset:
+ value = self.thunk(oself)
+ setattr(oself, cacheKey, value)
+ return value
+ else:
+ return cached
+ return inner
+
+
+
+def writeOperation(thunk):
+ # FIXME: tests
+ def inner(self, *a, **kw):
+ if self._transaction._termination is not None:
+ raise RuntimeError(
+ "%s.%s is a write operation, but transaction already %s"
+ % (self, thunk.__name__, self._transaction._termination))
+ return thunk(self, *a, **kw)
+ return inner
+
+class DataStore(LoggingMixIn):
+ """
+ Generic data store.
+ """
+
+ _transactionClass = None # Derived class must set this
+
+ def __init__(self, path):
+ """
+ Create a calendar store.
+
+ @param path: a L{FilePath} pointing at a directory on disk.
+ """
+ self._path = path
+
+# if not path.isdir():
+ # FIXME: Add DataStoreNotFoundError?
+# raise NotFoundError("No such data store")
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self._path.path)
+
+ def newTransaction(self):
+ """
+ Create a new transaction.
+
+ @see Transaction
+ """
+ return self._transactionClass(self)
+
+
+
+class _CommitTracker(object):
+ """
+ Diagnostic tool to find transactions that were never committed.
+ """
+
+ def __init__(self):
+ self.done = False
+ self.info = []
+
+ def __del__(self):
+ if not self.done and self.info:
+ print '**** UNCOMMITTED TRANSACTION BEING GARBAGE COLLECTED ****'
+ for info in self.info:
+ print ' ', info
+ print '---- END OF OPERATIONS'
+
+
+
+class DataStoreTransaction(LoggingMixIn):
+ """
+ In-memory implementation of a data store transaction.
+ """
+
+ def __init__(self, dataStore):
+ """
+ Initialize a transaction; do not call this directly, instead call
+ L{CalendarStore.newTransaction}.
+
+ @param calendarStore: The store that created this transaction.
+
+ @type calendarStore: L{CalendarStore}
+ """
+ self._dataStore = dataStore
+ self._termination = None
+ self._operations = []
+ self._tracker = _CommitTracker()
+
+
+ def addOperation(self, operation, name):
+ self._operations.append(operation)
+ self._tracker.info.append(name)
+
+
+ def _terminate(self, mode):
+ """
+ Check to see if this transaction has already been terminated somehow,
+ either via committing or aborting, and if not, note that it has been
+ terminated.
+
+ @param mode: The manner of the termination of this transaction.
+
+ @type mode: C{str}
+
+ @raise RuntimeError: This transaction has already been terminated.
+ """
+ if self._termination is not None:
+ raise RuntimeError("already %s" % (self._termination,))
+ self._termination = mode
+ self._tracker.done = True
+
+
+ def abort(self):
+ self._terminate("aborted")
+
+
+ def commit(self):
+ self._terminate("committed")
+
+ self.committed = True
+ undos = []
+
+ for operation in self._operations:
+ try:
+ undo = operation()
+ if undo is not None:
+ undos.append(undo)
+ except:
+ self.log_debug("Undoing DataStoreTransaction")
+ for undo in undos:
+ try:
+ undo()
+ except:
+ self.log_error("Cannot undo DataStoreTransaction")
+ raise
+
+
Modified: CalendarServer/branches/new-store/txdav/propertystore/base.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/base.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txdav/propertystore/base.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -103,7 +103,7 @@
def __contains__(self, key):
raise NotImplementedError()
- def __setitem__(key, value):
+ def __setitem__(self, key, value):
raise NotImplementedError()
def __iter__(self):
Modified: CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py 2010-06-22 19:28:26 UTC (rev 5795)
+++ CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py 2010-06-22 19:35:25 UTC (rev 5796)
@@ -21,7 +21,6 @@
from twext.python.filepath import CachingFilePath as FilePath
from txdav.propertystore.base import PropertyName
-from txdav.propertystore.test.base import propertyName
from txdav.propertystore.test import base
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100622/530ec128/attachment-0001.html>
More information about the calendarserver-changes
mailing list