[CalendarServer-changes] [5548] CalendarServer/branches/users/wsanchez/transations/txcaldav

source_changes at macosforge.org source_changes at macosforge.org
Thu Apr 29 17:45:21 PDT 2010


Revision: 5548
          http://trac.macosforge.org/projects/calendarserver/changeset/5548
Author:   glyph at apple.com
Date:     2010-04-29 17:45:16 -0700 (Thu, 29 Apr 2010)
Log Message:
-----------
Give an 'undo' to setComponent; make _path private; add a couple of tests for "undo"

Modified Paths:
--------------
    CalendarServer/branches/users/wsanchez/transations/txcaldav/calendarstore/file.py
    CalendarServer/branches/users/wsanchez/transations/txcaldav/calendarstore/test/test_file.py
    CalendarServer/branches/users/wsanchez/transations/txcaldav/icalendarstore.py

Modified: CalendarServer/branches/users/wsanchez/transations/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/users/wsanchez/transations/txcaldav/calendarstore/file.py	2010-04-29 20:46:49 UTC (rev 5547)
+++ CalendarServer/branches/users/wsanchez/transations/txcaldav/calendarstore/file.py	2010-04-30 00:45:16 UTC (rev 5548)
@@ -37,7 +37,7 @@
 from twext.python.vcomponent import VComponent
 from twext.python.vcomponent import InvalidICalendarDataError
 
-from txdav.idav import AbortedTransactionError
+# from txdav.idav import AbortedTransactionError
 from txdav.propertystore.xattr import PropertyStore
 
 from txcaldav.icalendarstore import ICalendarStoreTransaction
@@ -56,7 +56,16 @@
 from twistedcaldav.index import Index as OldIndex
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 
+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(".")
+
+
 class CalendarStore(LoggingMixIn):
     implements(ICalendarStore)
 
@@ -66,14 +75,14 @@
         """
         assert isinstance(path, FilePath)
 
-        self.path = path
+        self._path = path
 
         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)
+        return "<%s: %s>" % (self.__class__.__name__, self._path.path)
 
     def newTransaction(self):
         return Transaction(self)
@@ -121,7 +130,7 @@
 
         assert len(uid) >= 4
 
-        childPath1 = self.calendarStore.path.child(uid[0:2])
+        childPath1 = self.calendarStore._path.child(uid[0:2])
         childPath2 = childPath1.child(uid[2:4])
         childPath3 = childPath2.child(uid)
 
@@ -156,22 +165,22 @@
     implements(ICalendarHome)
 
     def __init__(self, path, calendarStore, transaction):
-        self.path = path
+        self._path = path
         self.calendarStore = calendarStore
         self._transaction = transaction
         self._newCalendars = {}
         self._removedCalendars = set()
 
     def __repr__(self):
-        return "<%s: %s>" % (self.__class__.__name__, self.path)
+        return "<%s: %s>" % (self.__class__.__name__, self._path)
 
     def uid(self):
-        return self.path.basename()
+        return self._path.basename()
 
     def calendars(self):
         return set(self._newCalendars.itervalues()) | set(
             self.calendarWithName(name)
-            for name in self.path.listdir()
+            for name in self._path.listdir()
             if not name.startswith(".")
         )
 
@@ -185,7 +194,7 @@
         if name.startswith("."):
             return None
 
-        childPath = self.path.child(name)
+        childPath = self._path.child(name)
         if childPath.isdir():
             return Calendar(childPath, self)
         else:
@@ -195,7 +204,7 @@
         if name.startswith("."):
             raise CalendarNameNotAllowedError(name)
 
-        childPath = self.path.child(name)
+        childPath = self._path.child(name)
 
         if name not in self._removedCalendars and childPath.isdir():
             raise CalendarAlreadyExistsError(name)
@@ -212,14 +221,14 @@
                 raise
 
         self._transaction.addOperation(do)
-        self._newCalendars[name] = Calendar(self.path.child(name), self)
+        self._newCalendars[name] = Calendar(self._path.child(name), self)
 
     def removeCalendarWithName(self, name):
         if name.startswith(".") or name in self._removedCalendars:
             raise NoSuchCalendarError(name)
 
         self._removedCalendars.add(name)
-        childPath = self.path.child(name)
+        childPath = self._path.child(name)
         if name not in self._newCalendars and not childPath.isdir():
             raise NoSuchCalendarError(name)
 
@@ -254,22 +263,26 @@
     def properties(self):
         # raise NotImplementedError()
         if not hasattr(self, "_properties"):
-            self._properties = PropertyStore(self.path)
+            self._properties = PropertyStore(self._path)
         return self._properties
 
 
 class Calendar(LoggingMixIn):
+    """
+    File-based implementation of L{ICalendar}.
+    """
     implements(ICalendar)
 
     def __init__(self, path, calendarHome):
-        self.path = path
+        self._path = path
         self.calendarHome = calendarHome
         self._transaction = calendarHome._transaction
         self._newCalendarObjects = {}
+        self._cachedCalendarObjects = {}
         self._removedCalendarObjects = set()
 
     def __repr__(self):
-        return "<%s: %s>" % (self.__class__.__name__, self.path.path)
+        return "<%s: %s>" % (self.__class__.__name__, self._path.path)
 
     def index(self):
         if not hasattr(self, "_index"):
@@ -277,7 +290,7 @@
         return self._index
 
     def name(self):
-        return self.path.basename()
+        return self._path.basename()
 
     def ownerCalendarHome(self):
         return self.calendarHome
@@ -297,51 +310,67 @@
             self.calendarObjectWithName(name)
             for name in (
                 set(self._newCalendarObjects.iterkeys()) |
-                set(name for name in self.path.listdir() if not name.startswith("."))
+                set(name for name in self._path.listdir() if not name.startswith("."))
             )
         )
 
+
     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)
+
+        calendarObjectPath = self._path.child(name)
         if calendarObjectPath.isfile():
-            return CalendarObject(calendarObjectPath, self)
+            obj = CalendarObject(calendarObjectPath, 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():
+        for calendarObjectPath in self._path.children():
+            if not _isValidName(calendarObjectPath.basename()):
+                continue
             obj = CalendarObject(calendarObjectPath, self)
             if obj.component().resourceUID() == uid:
                 return obj
 
+
     def createCalendarObjectWithName(self, name, component):
         if name.startswith("."):
             raise CalendarObjectNameNotAllowedError(name)
 
-        calendarObjectPath = self.path.child(name)
+        calendarObjectPath = self._path.child(name)
         if calendarObjectPath.exists():
             raise CalendarObjectNameAlreadyExistsError(name)
 
         calendarObject = CalendarObject(calendarObjectPath, self)
         calendarObject.setComponent(component)
+        self._cachedCalendarObjects[name] = calendarObject
 
+
     def removeCalendarObjectWithName(self, name):
         if name.startswith("."):
             raise NoSuchCalendarObjectError(name)
 
-        calendarObjectPath = self.path.child(name)
+        calendarObjectPath = self._path.child(name)
         if calendarObjectPath.isfile():
+            self._removedCalendarObjects.add(name)
+            # FIXME: test for undo
             calendarObjectPath.remove()
         else:
             raise NoSuchCalendarObjectError(name)
 
     def removeCalendarObjectWithUID(self, uid):
-        self.removeCalendarObjectWithName(self.calendarObjectWithUID(uid).path.basename())
+        self.removeCalendarObjectWithName(self.calendarObjectWithUID(uid)._path.basename())
 
     def syncToken(self):
         raise NotImplementedError()
@@ -380,23 +409,31 @@
     def properties(self):
         raise NotImplementedError()
         if not hasattr(self, "_properties"):
-            self._properties = PropertyStore(self.path)
+            self._properties = PropertyStore(self._path)
         return self._properties
 
 
 class CalendarObject(LoggingMixIn):
+    """
+    @ivar path: The path of the .ics file on disk
+
+    @type path: L{FilePath}
+    """
     implements(ICalendarObject)
 
     def __init__(self, path, calendar):
-        self.path = path
+        self._path = path
         self.calendar = calendar
 
+
     def __repr__(self):
-        return "<%s: %s>" % (self.__class__.__name__, self.path.path)
+        return "<%s: %s>" % (self.__class__.__name__, self._path.path)
 
+
     def name(self):
-        return self.path.basename()
+        return self._path.basename()
 
+
     def setComponent(self, component):
         if not isinstance(component, VComponent):
             raise TypeError(VComponent)
@@ -420,12 +457,27 @@
         if hasattr(self, "_text"):
             del self._text
 
-        fh = self.path.open("w")
-        try:
-            fh.write(str(component))
-        finally:
-            fh.close()
+        def do():
+            backup = None
+            if self._path.exists():
+                backup = self._path.temporarySibling()
+                self._path.moveTo(backup)
+            fh = self._path.open("w")
+            try:
+                # FIXME: concurrency problem; if this write is interrupted
+                # halfway through, the underlying file will be corrupt.
+                fh.write(str(component))
+            finally:
+                fh.close()
+            def undo():
+                if backup:
+                    backup.moveTo(self._path)
+                else:
+                    self._path.remove()
+            return undo
+        self.calendar._transaction.addOperation(do)
 
+
     def component(self):
         if not hasattr(self, "_component"):
             text = self.iCalendarText()
@@ -435,7 +487,7 @@
             except InvalidICalendarDataError, e:
                 raise InternalDataStoreError(
                     "File corruption detected (%s) in file: %s"
-                    % (e, self.path.path)
+                    % (e, self._path.path)
                 )
 
             del self._text
@@ -455,10 +507,12 @@
                 return str(self._component)
 
             try:
-                fh = self.path.open()
+                fh = self._path.open()
             except IOError, e:
                 if e[0] == errno.ENOENT:
                     raise NoSuchCalendarObjectError(self)
+                else:
+                    raise
 
             try:
                 text = fh.read()
@@ -471,7 +525,7 @@
             ):
                 raise InternalDataStoreError(
                     "File corruption detected (improper start) in file: %s"
-                    % (self.path.path,)
+                    % (self._path.path,)
                 )
 
             self._text = text
@@ -494,7 +548,7 @@
     def properties(self):
         raise NotImplementedError()
         if not hasattr(self, "_properties"):
-            self._properties = PropertyStore(self.path)
+            self._properties = PropertyStore(self._path)
         return self._properties
 
 
@@ -509,7 +563,7 @@
         """
         def __init__(self, calendar):
             self.calendar = calendar
-            self.fp = self.calendar.path
+            self.fp = self.calendar._path
 
         def isCalendarCollection(self):
             return True

Modified: CalendarServer/branches/users/wsanchez/transations/txcaldav/calendarstore/test/test_file.py
===================================================================
--- CalendarServer/branches/users/wsanchez/transations/txcaldav/calendarstore/test/test_file.py	2010-04-29 20:46:49 UTC (rev 5547)
+++ CalendarServer/branches/users/wsanchez/transations/txcaldav/calendarstore/test/test_file.py	2010-04-30 00:45:16 UTC (rev 5548)
@@ -152,15 +152,18 @@
     calendarPath = test.root.child("store")
     storePath.copyTo(calendarPath)
 
+
     test.calendarStore = CalendarStore(calendarPath)
     test.txn = test.calendarStore.newTransaction()
     assert test.calendarStore is not None, "No calendar store?"
 
+
 def setUpHome1(test):
     setUpCalendarStore(test)
     test.home1 = test.txn.calendarHomeWithUID("home1")
     assert test.home1 is not None, "No calendar home?"
 
+
 def setUpCalendar1(test):
     setUpHome1(test)
     test.calendar1 = test.home1.calendarWithName("calendar_1")
@@ -185,8 +188,8 @@
         Ivars are correctly initialized.
         """
         self.failUnless(
-            isinstance(self.calendarStore.path, FilePath),
-            self.calendarStore.path
+            isinstance(self.calendarStore._path, FilePath),
+            self.calendarStore._path
         )
 
     def test_calendarHomeWithUID_exists(self):
@@ -217,9 +220,9 @@
         )
 
         self.failUnless(isinstance(calendarHome, CalendarHome))
-        self.failIf(calendarHome.path.isdir())
+        self.failIf(calendarHome._path.isdir())
         txn.commit()
-        self.failUnless(calendarHome.path.isdir())
+        self.failUnless(calendarHome._path.isdir())
 
     def test_calendarHomeWithUID_create_exists(self):
         """
@@ -258,8 +261,8 @@
         Ivars are correctly initialized.
         """
         self.failUnless(
-            isinstance(self.home1.path, FilePath),
-            self.home1.path
+            isinstance(self.home1._path, FilePath),
+            self.home1._path
         )
         self.assertEquals(
             self.home1.calendarStore,
@@ -277,7 +280,7 @@
         Find all of the calendars.
         """
         # Add a dot directory to make sure we don't find it
-        self.home1.path.child(".foo").createDirectory()
+        self.home1._path.child(".foo").createDirectory()
 
         calendars = tuple(self.home1.calendars())
 
@@ -310,7 +313,7 @@
         implementation, so no calendar names may start with ".".
         """
         name = ".foo"
-        self.home1.path.child(name).createDirectory()
+        self.home1._path.child(name).createDirectory()
         self.assertEquals(self.home1.calendarWithName(name), None)
 
     def test_createCalendarWithName_absent(self):
@@ -367,7 +370,7 @@
         implementation, so no calendar names may start with ".".
         """
         name = ".foo"
-        self.home1.path.child(name).createDirectory()
+        self.home1._path.child(name).createDirectory()
         self.assertRaises(
             NoSuchCalendarError,
             self.home1.removeCalendarWithName, name
@@ -391,7 +394,7 @@
         Ivars are correctly initialized.
         """
         self.failUnless(
-            isinstance(self.calendar1.path, FilePath),
+            isinstance(self.calendar1._path, FilePath),
             self.calendar1
         )
         self.failUnless(
@@ -417,7 +420,7 @@
 
     def _test_calendarObjects(self, which):
         # Add a dot file to make sure we don't find it
-        self.home1.path.child(".foo").createDirectory()
+        self.home1._path.child(".foo").createDirectory()
 
         methodName = "_calendarObjects_%s" % (which,)
         method = getattr(self.calendar1, methodName)
@@ -475,7 +478,7 @@
         ".".
         """
         name = ".foo.ics"
-        self.home1.path.child(name).touch()
+        self.home1._path.child(name).touch()
         self.assertEquals(self.calendar1.calendarObjectWithName(name), None)
 
     @featureUnimplemented
@@ -506,13 +509,14 @@
         Create a new calendar object.
         """
         name = "4.ics"
-        assert self.calendar1.calendarObjectWithName(name) is None
+        self.assertIdentical(self.calendar1.calendarObjectWithName(name), None)
         component = VComponent.fromString(event4_text)
         self.calendar1.createCalendarObjectWithName(name, component)
 
         calendarObject = self.calendar1.calendarObjectWithName(name)
         self.assertEquals(calendarObject.component(), component)
 
+
     def test_createCalendarObjectWithName_exists(self):
         """
         Attempt to create an existing calendar object should raise.
@@ -561,18 +565,21 @@
             "new", VComponent.fromString(event4notCalDAV_text)
         )
 
+
     def test_removeCalendarObjectWithName_exists(self):
         """
         Remove an existing calendar object.
         """
         for name in calendar1_objectNames:
-            assert self.calendar1.calendarObjectWithName(name) is not None
+            self.assertNotIdentical(
+                self.calendar1.calendarObjectWithName(name), None
+            )
             self.calendar1.removeCalendarObjectWithName(name)
-            self.assertEquals(
-                self.calendar1.calendarObjectWithName(name),
-                None
+            self.assertIdentical(
+                self.calendar1.calendarObjectWithName(name), None
             )
 
+
     def test_removeCalendarObjectWithName_absent(self):
         """
         Attempt to remove an non-existing calendar object should raise.
@@ -589,7 +596,7 @@
         ".".
         """
         name = ".foo"
-        self.calendar1.path.child(name).touch()
+        self.calendar1._path.child(name).touch()
         self.assertRaises(
             NoSuchCalendarObjectError,
             self.calendar1.removeCalendarObjectWithName, name
@@ -602,7 +609,6 @@
         """
         for name in calendar1_objectNames:
             uid = (u'uid' + name.rstrip(".ics"))
-            
             self.assertNotIdentical(self.calendar1.calendarObjectWithUID(uid),
                                     None)
             self.calendar1.removeCalendarObjectWithUID(uid)
@@ -616,6 +622,72 @@
             )
 
 
+    def _refresh(self):
+        """
+        Re-read the (committed) home1 and calendar1 objects in a new
+        transaction.
+        """
+        self.txn = self.calendarStore.newTransaction()
+        self.home1 = self.txn.calendarHomeWithUID("home1")
+        self.calendar1 = self.home1.calendarWithName("calendar_1")
+
+
+    def test_undoCreateCalendarObject(self):
+        """
+        If a calendar object is created as part of a transaction, it will be
+        removed if that transaction has to be aborted.
+        """
+        # Make sure that the calendar home is actually committed; rolling back
+        # calendar home creation will remove the whole directory.
+        self.txn.commit()
+        self._refresh()
+        self.calendar1.createCalendarObjectWithName(
+            "sample.ics",
+            VComponent.fromString(event4_text)
+        )
+        self._refresh()
+        self.assertIdentical(
+            self.calendar1.calendarObjectWithName("sample.ics"),
+            None
+        )
+
+
+    def doThenUndo(self):
+        """
+        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
+        context.
+        """
+        def fail():
+            raise RuntimeError("oops")
+        self.txn.addOperation(fail)
+        self.assertRaises(RuntimeError, self.txn.commit)
+        self._refresh()
+
+
+    def test_undoModifyCalendarObject(self):
+        """
+        If an existing calendar object is modified as part of a transaction, it
+        should be restored to its previous status if the transaction aborts.
+        """
+        originalComponent = self.calendar1.calendarObjectWithName(
+            "1.ics").component()
+        self.calendar1.calendarObjectWithName("1.ics").setComponent(
+            VComponent.fromString(event1modified_text)
+        )
+        # Sanity check.
+        self.assertEquals(
+            self.calendar1.calendarObjectWithName("1.ics").component(),
+            VComponent.fromString(event1modified_text)
+        )
+        self.doThenUndo()
+        self.assertEquals(
+            self.calendar1.calendarObjectWithName("1.ics").component(),
+            originalComponent
+        )
+
+
     @featureUnimplemented
     def test_removeCalendarObjectWithUID_absent(self):
         """
@@ -626,6 +698,7 @@
             self.calendar1.removeCalendarObjectWithUID, "xyzzy"
         )
 
+
     @testUnimplemented
     def test_syncToken(self):
         """
@@ -668,8 +741,8 @@
         Ivars are correctly initialized.
         """
         self.failUnless(
-            isinstance(self.object1.path, FilePath),
-            self.object1.path
+            isinstance(self.object1._path, FilePath),
+            self.object1._path
         )
         self.failUnless(
             isinstance(self.object1.calendar, Calendar),
@@ -682,15 +755,17 @@
         """
         self.assertEquals(self.object1.name(), "1.ics")
 
+
     def test_setComponent(self):
         """
-        Rewrite component.
+        L{CalendarObject.setComponent} changes the result of
+        L{CalendarObject.component} within the same transaction.
         """
         component = VComponent.fromString(event1modified_text)
 
         calendarObject = self.calendar1.calendarObjectWithName("1.ics")
-        oldComponent = calendarObject.component() # Trigger caching
-        assert component != oldComponent
+        oldComponent = calendarObject.component()
+        self.assertNotEqual(component, oldComponent)
         calendarObject.setComponent(component)
         self.assertEquals(calendarObject.component(), component)
 
@@ -698,6 +773,7 @@
         calendarObject = self.calendar1.calendarObjectWithName("1.ics")
         self.assertEquals(calendarObject.component(), component)
 
+
     def test_setComponent_uidchanged(self):
         component = VComponent.fromString(event4_text)
 
@@ -707,6 +783,7 @@
             calendarObject.setComponent, component
         )
 
+
     def test_setComponent_invalid(self):
         calendarObject = self.calendar1.calendarObjectWithName("1.ics")
         self.assertRaises(
@@ -715,6 +792,7 @@
             VComponent.fromString(event4notCalDAV_text)
         )
 
+
     def test_component(self):
         """
         Component is correct.

Modified: CalendarServer/branches/users/wsanchez/transations/txcaldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/wsanchez/transations/txcaldav/icalendarstore.py	2010-04-29 20:46:49 UTC (rev 5547)
+++ CalendarServer/branches/users/wsanchez/transations/txcaldav/icalendarstore.py	2010-04-30 00:45:16 UTC (rev 5548)
@@ -1,3 +1,4 @@
+# -*- test-case-name: txcaldav.calendarstore -*-
 ##
 # Copyright (c) 2010 Apple Inc. All rights reserved.
 #
@@ -18,6 +19,10 @@
 Calendar store interfaces
 """
 
+from zope.interface import Interface
+from txdav.idav import ITransaction
+
+
 __all__ = [
     # Exceptions
     "CalendarStoreError",
@@ -41,15 +46,17 @@
     "ICalendarObject",
 ]
 
-from datetime import datetime, date, tzinfo
 
-from zope.interface import Interface #, Attribute
+# The following imports are used by the L{} links below, but shouldn't actually
+# be imported.as they're not really needed.
 
-from twext.python.vcomponent import VComponent
+# from datetime import datetime, date, tzinfo
 
-from txdav.idav import IPropertyStore
-from txdav.idav import ITransaction
+# from twext.python.vcomponent import VComponent
 
+# from txdav.idav import IPropertyStore
+# from txdav.idav import ITransaction
+
 #
 # Exceptions
 #
@@ -158,14 +165,6 @@
     includes both calendars owned by the principal as well as
     calendars that have been shared with and accepts by the principal.
     """
-    # FIXME: We need a principal interface somewhere, possibly part of
-    # an idirectory rework.  IDirectoryRecord may be close...
-    #def owner():
-    #    """
-    #    Retrieve the owner principal for this calendar home.
-    #    @return: a ???
-    #    """
-
     def uid():
         """
         Retrieve the unique identifier for this calendar home.
@@ -345,6 +344,7 @@
     A calendar object decribes an event, to-do, or other iCalendar
     object.
     """
+
     def setComponent(component):
         """
         Rewrite this calendar object to match the given C{component}.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100429/747d7e57/attachment-0001.html>


More information about the calendarserver-changes mailing list