[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