[CalendarServer-changes] [5862] CalendarServer/branches/new-store

source_changes at macosforge.org source_changes at macosforge.org
Thu Jul 8 19:37:58 PDT 2010


Revision: 5862
          http://trac.macosforge.org/projects/calendarserver/changeset/5862
Author:   cdaboo at apple.com
Date:     2010-07-08 19:37:58 -0700 (Thu, 08 Jul 2010)
Log Message:
-----------
New property store supports per-user properties.

Modified Paths:
--------------
    CalendarServer/branches/new-store/twistedcaldav/sharing.py
    CalendarServer/branches/new-store/twistedcaldav/test/test_calendarquery.py
    CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
    CalendarServer/branches/new-store/txcarddav/addressbookstore/file.py
    CalendarServer/branches/new-store/txdav/common/datastore/file.py
    CalendarServer/branches/new-store/txdav/propertystore/base.py
    CalendarServer/branches/new-store/txdav/propertystore/none.py
    CalendarServer/branches/new-store/txdav/propertystore/test/base.py
    CalendarServer/branches/new-store/txdav/propertystore/test/test_none.py
    CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py
    CalendarServer/branches/new-store/txdav/propertystore/xattr.py

Modified: CalendarServer/branches/new-store/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/sharing.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/twistedcaldav/sharing.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -206,6 +206,11 @@
         self._isVirtualShare = True
         self._shareePrincipal = shareePrincipal
         self._share = share
+        
+        if hasattr(self, "_newStoreCalendar"):
+            self._newStoreCalendar.setSharingUID(self._shareePrincipal.principalUID())
+        elif hasattr(self, "_newStoreAddressBook"):
+            self._newStoreAddressBook.setSharingUID(self._shareePrincipal.principalUID())
 
     def isVirtualShare(self, request):
         """ Return True if this is a shared calendar collection """

Modified: CalendarServer/branches/new-store/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/branches/new-store/twistedcaldav/test/test_calendarquery.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/twistedcaldav/test/test_calendarquery.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -293,6 +293,8 @@
 
         if os.path.exists(calendar_path): rmdir(calendar_path)
 
+        mkrequest = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
+
         def do_report(response):
             response = IResponse(response)
 
@@ -323,6 +325,4 @@
 
             return self.send(request, do_test)
 
-        request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
-
-        return self.send(request, do_report)
+        return self.send(mkrequest, do_report)

Modified: CalendarServer/branches/new-store/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/new-store/txcaldav/calendarstore/file.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/txcaldav/calendarstore/file.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -41,6 +41,7 @@
 from twext.web2.dav.element.rfc2518 import ResourceType, GETContentType
 
 
+from twistedcaldav import caldavxml, customxml
 from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
 from twistedcaldav.index import Index as OldIndex, IndexSchedule as OldInboxIndex
 from twistedcaldav.sharing import InvitesDatabase
@@ -85,7 +86,7 @@
 
     def calendars(self):
         for child in self.children():
-            if child.name() == 'dropbox':
+            if child.name() in ('dropbox', 'notification'):
                 continue
             yield child
 
@@ -167,6 +168,20 @@
         raise NotImplementedError()
 
 
+    def initPropertyStore(self, props):
+        # Setup peruser special properties
+        props.setSpecialProperties(
+            (
+                PropertyName.fromElement(caldavxml.CalendarDescription),
+                PropertyName.fromElement(caldavxml.CalendarTimeZone),
+            ),
+            (
+                PropertyName.fromElement(customxml.GETCTag),
+                PropertyName.fromElement(caldavxml.SupportedCalendarComponentSet),
+                PropertyName.fromElement(caldavxml.ScheduleCalendarTransp),
+            ),
+        )
+
     def _doValidate(self, component):
         # FIXME: should be separate class, not separate case!
         if self.name() == 'inbox':
@@ -175,7 +190,6 @@
             component.validateForCalDAV()
 
 
-
 class CalendarObject(CommonObjectResource):
     """
     @ivar _path: The path of the .ics file on disk
@@ -223,6 +237,10 @@
         # FIXME: needs to clear text cache
 
         def do():
+            # Mark all properties as dirty, so they can be added back
+            # to the newly updated file.
+            self.properties().update(self.properties())
+
             backup = None
             if self._path.exists():
                 backup = hidden(self._path.temporarySibling())
@@ -234,6 +252,10 @@
                 fh.write(str(component))
             finally:
                 fh.close()
+                
+            # Now re-write the original properties on the updated file
+            self.properties().flush()
+
             def undo():
                 if backup:
                     backup.moveTo(self._path)
@@ -242,20 +264,6 @@
             return undo
         self._transaction.addOperation(do, "set calendar 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
-        # happens _after_ the new file has been written.  we may end up doing
-        # the work multiple times, and external callers to property-
-        # manipulation methods won't work.
-        self._transaction.addOperation(self.properties().flush, "post-update property flush")
-
-
     def component(self):
         if self._component is not None:
             return self._component
@@ -372,8 +380,25 @@
         return [Attachment(self, name)
                 for name in self._dropboxPath().listdir()]
 
+    def initPropertyStore(self, props):
+        # Setup peruser special properties
+        props.setSpecialProperties(
+            (
+            ),
+            (
+                PropertyName.fromElement(customxml.TwistedCalendarAccessProperty),
+                PropertyName.fromElement(customxml.TwistedSchedulingObjectResource),
+                PropertyName.fromElement(caldavxml.ScheduleTag),
+                PropertyName.fromElement(customxml.TwistedScheduleMatchETags),
+                PropertyName.fromElement(customxml.TwistedCalendarHasPrivateCommentsProperty),
+                PropertyName.fromElement(caldavxml.Originator),
+                PropertyName.fromElement(caldavxml.Recipient),
+                PropertyName.fromElement(customxml.ScheduleChanges),
+            ),
+        )
 
 
+
 class AttachmentStorageTransport(object):
 
     implements(ITransport)
@@ -420,7 +445,11 @@
 
     def _properties(self):
         # Not exposed 
-        return PropertyStore(self._computePath())
+        return PropertyStore(
+            self._calendarObject._parentCollection._home.peruser_uid(),
+            self._calendarObject._parentCollection._home.uid(),
+            self._computePath()
+        )
 
 
     def contentType(self):

Modified: CalendarServer/branches/new-store/txcarddav/addressbookstore/file.py
===================================================================
--- CalendarServer/branches/new-store/txcarddav/addressbookstore/file.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/txcarddav/addressbookstore/file.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -44,7 +44,10 @@
 from txdav.common.icommondatastore import InvalidObjectResourceError,\
     NoSuchObjectResourceError, InternalDataStoreError
 from txdav.datastore.file import hidden, writeOperation
+from txdav.propertystore.base import PropertyName
 
+from twistedcaldav import customxml, carddavxml
+
 from zope.interface import implements
 
 AddressBookStore = CommonDataStore
@@ -117,6 +120,17 @@
     addressbookObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
 
 
+    def initPropertyStore(self, props):
+        # Setup peruser special properties
+        props.setSpecialProperties(
+            (
+                PropertyName.fromElement(carddavxml.AddressBookDescription),
+            ),
+            (
+                PropertyName.fromElement(customxml.GETCTag),
+            ),
+        )
+
     def _doValidate(self, component):
         component.validForCardDAV()
 
@@ -164,6 +178,10 @@
         # FIXME: needs to clear text cache
 
         def do():
+            # Mark all properties as dirty, so they can be added back
+            # to the newly updated file.
+            self.properties().update(self.properties())
+
             backup = None
             if self._path.exists():
                 backup = hidden(self._path.temporarySibling())
@@ -175,6 +193,10 @@
                 fh.write(str(component))
             finally:
                 fh.close()
+
+            # Now re-write the original properties on the updated file
+            self.properties().flush()
+
             def undo():
                 if backup:
                     backup.moveTo(self._path)
@@ -183,20 +205,7 @@
             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
-        # happens _after_ the new file has been written.  we may end up doing
-        # the work multiple times, and external callers to property-
-        # manipulation methods won't work.
-        self._transaction.addOperation(self.properties().flush, "post-update property flush")
-
-
     def component(self):
         if self._component is not None:
             return self._component

Modified: CalendarServer/branches/new-store/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/new-store/txdav/common/datastore/file.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/txdav/common/datastore/file.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -19,32 +19,33 @@
 Common utility functions for a file based datastore.
 """
 
-from errno import EEXIST, ENOENT
-
 from twext.python.log import LoggingMixIn
 from twext.web2.dav.element.rfc2518 import ResourceType, GETContentType
+from twext.web2.http_headers import generateContentType, MimeType
 
-from txdav.datastore.file import DataStoreTransaction, DataStore, writeOperation, \
-    hidden, isValidName, cached
-from txdav.propertystore.base import PropertyName
-from txdav.propertystore.xattr import PropertyStore
+from twisted.python.util import FancyEqMixin
+
+from twistedcaldav import customxml
+from twistedcaldav.customxml import GETCTag, NotificationType
+from twistedcaldav.notifications import NotificationRecord
+from twistedcaldav.notifications import NotificationsDatabase as OldNotificationIndex
+from twistedcaldav.sharing import SharedCollectionsDatabase
+
 from txdav.common.icommondatastore import HomeChildNameNotAllowedError, \
     HomeChildNameAlreadyExistsError, NoSuchHomeChildError, \
     InternalDataStoreError, ObjectResourceNameNotAllowedError, \
     ObjectResourceNameAlreadyExistsError, NoSuchObjectResourceError
-
-from twisted.python.util import FancyEqMixin
-from twistedcaldav.customxml import GETCTag, NotificationType
-from uuid import uuid4
-from zope.interface import implements, directlyProvides
 from txdav.common.inotifications import INotificationCollection, \
     INotificationObject
-from twistedcaldav.notifications import NotificationRecord
-from twistedcaldav.notifications import NotificationsDatabase as OldNotificationIndex
-from twext.web2.http_headers import generateContentType, MimeType
-from twistedcaldav.sharing import SharedCollectionsDatabase
+from txdav.datastore.file import DataStoreTransaction, DataStore, writeOperation, \
+    hidden, isValidName, cached
 from txdav.idav import IDataStore
+from txdav.propertystore.base import PropertyName
+from txdav.propertystore.xattr import PropertyStore
 
+from errno import EEXIST, ENOENT
+from uuid import uuid4
+from zope.interface import implements, directlyProvides
 
 ECALENDARTYPE = 0
 EADDRESSBOOKTYPE = 1
@@ -261,7 +262,7 @@
 
     def __init__(self, uid, path, dataStore, transaction):
         self._dataStore = dataStore
-        self._uid = uid
+        self._uid = self._peruser_uid = uid
         self._path = path
         self._transaction = transaction
         self._shares = SharedCollectionsDatabase(StubResource(self))
@@ -277,6 +278,8 @@
     def uid(self):
         return self._uid
 
+    def peruser_uid(self):
+        return self._peruser_uid
 
     def _updateSyncToken(self, reset=False):
         "Stub for updating sync token."
@@ -408,7 +411,7 @@
         # FIXME: needs tests for actual functionality
         # FIXME: needs to be cached
         # FIXME: transaction tests
-        props = PropertyStore(self._path)
+        props = PropertyStore(self.peruser_uid(), self.uid(), self._path)
         self._transaction.addOperation(props.flush, "flush home properties")
         return props
 
@@ -438,6 +441,7 @@
         """
         self._name = name
         self._home = home
+        self._peruser_uid = home._peruser_uid
         self._transaction = home._transaction
         self._newObjectResources = {}
         self._cachedObjectResources = {}
@@ -497,6 +501,9 @@
     def ownerHome(self):
         return self._home
 
+    def setSharingUID(self, uid):
+        self._peruser_uid = uid
+        self.properties().setPerUserUID(uid)
 
     def objectResources(self):
         return sorted((
@@ -615,11 +622,20 @@
     def properties(self):
         # FIXME: needs direct tests - only covered by store tests
         # FIXME: transactions
-        props = PropertyStore(self._path)
+        props = PropertyStore(
+            self._peruser_uid,
+            self._home.uid(),
+            self._path
+        )
+        self.initPropertyStore(props)
+
         self._transaction.addOperation(props.flush, "flush object resource properties")
         return props
 
 
+    def initPropertyStore(self, props):
+        pass
+
     def _doValidate(self, component):
         raise NotImplementedError
 
@@ -669,7 +685,11 @@
 
     @cached
     def properties(self):
-        props = PropertyStore(self._path)
+        props = PropertyStore(
+            self._parentCollection._home.peruser_uid(),
+            self._parentCollection._home.uid(),
+            self._path
+        )
         self._transaction.addOperation(props.flush, "object properties flush")
         return props
 
@@ -846,6 +866,17 @@
             self._uid = self.xmldata
         return self._uid
 
+    def initPropertyStore(self, props):
+        # Setup peruser special properties
+        props.setSpecialProperties(
+            (
+            ),
+            (
+                PropertyName.fromElement(customxml.NotificationType),
+            ),
+        )
+
+
 class NotificationIndex(object):
     #
     # OK, here's where we get ugly.

Modified: CalendarServer/branches/new-store/txdav/propertystore/base.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/base.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/txdav/propertystore/base.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -23,13 +23,17 @@
     "PropertyName",
 ]
 
-from zope.interface import implements
-
 from twext.python.log import LoggingMixIn
+from twext.web2.dav import davxml
+from twext.web2.dav.resource import TwistedGETContentMD5,\
+    TwistedQuotaRootProperty
 
 from txdav.idav import IPropertyStore, IPropertyName
 
+from UserDict import DictMixin
 
+from zope.interface import implements
+
 class PropertyName(LoggingMixIn):
     """
     Property name.
@@ -45,6 +49,10 @@
 
         return PropertyName(sname[1:index], sname[index+1:])
 
+    @staticmethod
+    def fromElement(element):
+        return PropertyName(element.namespace, element.name)
+
     def __init__(self, namespace, name):
         self.namespace = namespace
         self.name = name
@@ -84,116 +92,103 @@
         return "{%s}%s" % (self.namespace, self.name)
 
 
-class AbstractPropertyStore(LoggingMixIn):
+class AbstractPropertyStore(LoggingMixIn, DictMixin):
     """
     Base property store.
     """
     implements(IPropertyStore)
 
-    def __init__(self, peruser):
+    _defaultShadowableKeys = set()
+    _defaultGlobalKeys = set((
+        PropertyName.fromElement(davxml.ACL),
+        PropertyName.fromElement(davxml.ResourceID),
+        PropertyName.fromElement(davxml.ResourceType),
+        PropertyName.fromElement(davxml.GETContentType),
+        PropertyName.fromElement(TwistedGETContentMD5),
+        PropertyName.fromElement(TwistedQuotaRootProperty),
+    ))
+
+    def __init__(self, peruser, defaultuser):
         """
-        Instantiate the property store for a user.
+        Instantiate the property store for a user. The default is the default user
+        (owner) property to read in the case of global or shadowable properties.
 
         @param peruser: the user uid
         @type peruser: C{str}
+
+        @param defaultuser: the default user uid
+        @type defaultuser: C{str}
         """
         
         self._peruser = peruser
+        self._defaultuser = defaultuser
+        self._shadowableKeys = set(AbstractPropertyStore._defaultShadowableKeys)
+        self._globalKeys = set(AbstractPropertyStore._defaultGlobalKeys)
 
+    def setPerUserUID(self, uid):
+        self._peruser = uid
+
+    def setSpecialProperties(self, shadowableKeys, globalKeys):
+        self._shadowableKeys.update(shadowableKeys)
+        self._globalKeys.update(globalKeys)
+
     #
     # Subclasses must override these
     #
 
-    def __delitem__(self, key):
+    def _getitem_uid(self, key, uid):
         raise NotImplementedError()
 
-    def __getitem__(self, key):
+    def _setitem_uid(self, key, value, uid):
         raise NotImplementedError()
 
-    def __contains__(self, key):
+    def _delitem_uid(self, key, uid):
         raise NotImplementedError()
 
-    def __setitem__(self, key, value):
+    def _keys_uid(self, uid):
         raise NotImplementedError()
-
-    def __iter__(self):
-        raise NotImplementedError()
-
-    def __len__(self):
-        raise NotImplementedError()
-
-    def flush(self):
-        raise NotImplementedError()
-
-    def abort(self):
-        raise NotImplementedError()
-
+        
     #
-    # Subclasses may override these
+    # Required UserDict implementations
     #
 
-    def len(self, key):
-        return self.__len__(key)
+    def __getitem__(self, key):
+        # Handle per-user behavior 
+        if self.isShadowableProperty(key):
+            try:
+                result = self._getitem_uid(key, self._peruser)
+            except KeyError:
+                result = self._getitem_uid(key, self._defaultuser)
+            return result
+        elif self.isGlobalProperty(key):
+            return self._getitem_uid(key, self._defaultuser)
+        else:
+            return self._getitem_uid(key, self._peruser)
 
-    def clear(self):
-        for key in self.__iter__():
-            self.__delitem__(key)
+    def __setitem__(self, key, value):
+        # Handle per-user behavior 
+        if self.isGlobalProperty(key):
+            return self._setitem_uid(key, value, self._defaultuser)
+        else:
+            return self._setitem_uid(key, value, self._peruser)
 
-    def get(self, key, default=None):
-        if self.__contains__(key):
-            return self.__getitem__(key)
+    def __delitem__(self, key):
+        # Handle per-user behavior 
+        if self.isGlobalProperty(key):
+            self._delitem_uid(key, self._defaultuser)
         else:
-            return default
+            self._delitem_uid(key, self._peruser)
 
-    def iteritems(self):
-        return (
-            (key, self.get(key))
-            for key in self.__iter__()
-        )
-
-    def items(self):
-        return list(self.iteritems())
-
-    iterkeys = iter
-    __iterkeys__ = iter
-
     def keys(self):
-        return tuple(self.__iter__())
+        
+        userkeys = self._keys_uid(self._peruser)
+        if self._defaultuser != self._peruser:
+            defaultkeys = self._keys_uid(self._defaultuser)
+            for key in defaultkeys:
+                if self.isShadowableProperty(key) and key not in userkeys:
+                    userkeys.append(key)
+        return tuple(userkeys)
 
-    def itervalues(self):
-        return (
-            self.get(key)
-            for key in self.__iter__()
-        )
-
-    def values(self):
-        return list(self.itervalues())
-
-    def pop(self, key, default=None):
-        try:
-            value = self.__getitem__(key)
-        except KeyError:
-            if default is None:
-                raise
-            return default
-
-        self.__delitem__(key)
-
-        return value
-
-    def popitem(self):
-        for key in self.__iter__():
-            self.__delitem__(key)
-            break
-
-    def setdefault(self, key, default=None):
-        if self.__contains__(key):
-            return key
-
-        self.__setitem__(key, default)
-
-        return default
-
     def update(self, other):
         # FIXME: direct tests.
         # FIXME: support positional signature (although since strings aren't
@@ -202,6 +197,13 @@
             self[key] = other[key]
 
 
+    # Per-user property handling
+    def isShadowableProperty(self, key):
+        return key in self._shadowableKeys
+    
+    def isGlobalProperty(self, key):
+        return key in self._globalKeys
+
 # FIXME: Actually, we should replace this with calls to IPropertyName()
 def validKey(key):
     # Used by implementations to verify that keys are valid

Modified: CalendarServer/branches/new-store/txdav/propertystore/none.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/none.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/txdav/propertystore/none.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -18,72 +18,99 @@
 Property store with no storage.
 """
 
-from __future__ import absolute_import
-
 __all__ = [
     "PropertyStore",
 ]
 
 from txdav.propertystore.base import AbstractPropertyStore, validKey
-from txdav.idav import PropertyChangeNotAllowedError
 
-
 class PropertyStore(AbstractPropertyStore):
     """
     Property store with no storage.
     """
-    def __init__(self):
+    
+    properties = {}
+
+    def __init__(self, peruser, defaultuser):
+        super(PropertyStore, self).__init__(peruser, defaultuser)
+
         self.modified = {}
+        self.removed = set()
 
     def __str__(self):
         return "<%s>" % (self.__class__.__name__,)
 
     #
-    # Accessors
+    # Required implementations
     #
 
-    def __delitem__(self, key):
+    def _getitem_uid(self, key, uid):
         validKey(key)
+        effectiveKey = (key, uid)
 
-        if key in self.modified:
-            del self.modified[key]
-        else:
+        if effectiveKey in self.modified:
+            return self.modified[effectiveKey]
+
+        if effectiveKey in self.removed:
             raise KeyError(key)
 
-    def __getitem__(self, key):
+        return self.properties[effectiveKey]
+
+    def _setitem_uid(self, key, value, uid):
         validKey(key)
+        effectiveKey = (key, uid)
 
-        if key in self.modified:
-            return self.modified[key]
-        else:
-            raise KeyError(key)
+        if effectiveKey in self.removed:
+            self.removed.remove(effectiveKey)
+        self.modified[effectiveKey] = value
 
-    def __contains__(self, key):
+    def _delitem_uid(self, key, uid):
         validKey(key)
+        effectiveKey = (key, uid)
 
-        return key in self.modified
+        if effectiveKey in self.modified:
+            del self.modified[effectiveKey]
+        elif effectiveKey not in self.properties:
+            raise KeyError(key)
 
-    def __setitem__(self, key, value):
-        validKey(key)
+        self.removed.add(effectiveKey)
 
-        self.modified[key] = value
+    def _keys_uid(self, uid):
+        seen = set()
 
-    def __iter__(self):
-        return (k for k in self.modified)
+        for effectivekey in self.properties:
+            if effectivekey[1] == uid and effectivekey not in self.removed:
+                seen.add(effectivekey)
+                yield effectivekey[0]
 
-    def __len__(self):
-        return len(self.modified)
+        for effectivekey in self.modified:
+            if effectivekey[1] == uid and effectivekey not in seen:
+                yield effectivekey[0]
 
     #
     # I/O
     #
 
     def flush(self):
-        if self.modified:
-            raise PropertyChangeNotAllowedError(
-                "None property store cannot flush changes.",
-                keys = self.modified.keys()
-            )
+        props    = self.properties
+        removed  = self.removed
+        modified = self.modified
 
+        for effectivekey in removed:
+            assert effectivekey not in modified
+            try:
+                del props[effectivekey]
+            except KeyError:
+                pass
+
+        for effectivekey in modified:
+            assert effectivekey not in removed
+            value = modified[effectivekey]
+            props[effectivekey] = value
+        
+        self.removed.clear()
+        self.modified.clear()        
+
     def abort(self):
+        self.removed.clear()
         self.modified.clear()

Modified: CalendarServer/branches/new-store/txdav/propertystore/test/base.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/test/base.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/txdav/propertystore/test/base.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -65,6 +65,117 @@
         self.assertEquals(store.get(name, None), None)
         self.failIf(name in store)
 
+    def test_peruser(self):
+        store1 = self.propertyStore1
+        store2 = self.propertyStore2
+
+        name = propertyName("test")
+        value1 = propertyValue("Hello, World1!")
+        value2 = propertyValue("Hello, World2!")
+
+        store1[name] = value1
+        store1.flush()
+        self.assertEquals(store1.get(name, None), value1)
+        self.assertEquals(store2.get(name, None), None)
+        self.failUnless(name in store1)
+        self.failIf(name in store2)
+        
+        store2[name] = value2
+        store2.flush()
+        self.assertEquals(store1.get(name, None), value1)
+        self.assertEquals(store2.get(name, None), value2)
+        self.failUnless(name in store1)
+        self.failUnless(name in store2)
+        
+        del store2[name]
+        store2.flush()
+        self.assertEquals(store1.get(name, None), value1)
+        self.assertEquals(store2.get(name, None), None)
+        self.failUnless(name in store1)
+        self.failIf(name in store2)
+        
+        del store1[name]
+        store1.flush()
+        self.assertEquals(store1.get(name, None), None)
+        self.assertEquals(store2.get(name, None), None)
+        self.failIf(name in store1)
+        self.failIf(name in store2)
+        
+    def test_peruser_shadow(self):
+        store1 = self.propertyStore1
+        store2 = self.propertyStore2
+
+        name = propertyName("shadow")
+
+        store1.setSpecialProperties((name,), ())
+        store2.setSpecialProperties((name,), ())
+
+        value1 = propertyValue("Hello, World1!")
+        value2 = propertyValue("Hello, World2!")
+
+        store1[name] = value1
+        store1.flush()
+        self.assertEquals(store1.get(name, None), value1)
+        self.assertEquals(store2.get(name, None), value1)
+        self.failUnless(name in store1)
+        self.failUnless(name in store2)
+        
+        store2[name] = value2
+        store2.flush()
+        self.assertEquals(store1.get(name, None), value1)
+        self.assertEquals(store2.get(name, None), value2)
+        self.failUnless(name in store1)
+        self.failUnless(name in store2)
+        
+        del store2[name]
+        store2.flush()
+        self.assertEquals(store1.get(name, None), value1)
+        self.assertEquals(store2.get(name, None), value1)
+        self.failUnless(name in store1)
+        self.failUnless(name in store2)
+        
+        del store1[name]
+        store1.flush()
+        self.assertEquals(store1.get(name, None), None)
+        self.assertEquals(store2.get(name, None), None)
+        self.failIf(name in store1)
+        self.failIf(name in store2)
+        
+
+    def test_peruser_global(self):
+        store1 = self.propertyStore1
+        store2 = self.propertyStore2
+
+        name = propertyName("global")
+
+        store1.setSpecialProperties((), (name,))
+        store2.setSpecialProperties((), (name,))
+
+        value1 = propertyValue("Hello, World1!")
+        value2 = propertyValue("Hello, World2!")
+
+        store1[name] = value1
+        store1.flush()
+        self.assertEquals(store1.get(name, None), value1)
+        self.assertEquals(store2.get(name, None), value1)
+        self.failUnless(name in store1)
+        self.failUnless(name in store2)
+        
+        store2[name] = value2
+        store2.flush()
+        self.assertEquals(store1.get(name, None), value2)
+        self.assertEquals(store2.get(name, None), value2)
+        self.failUnless(name in store1)
+        self.failUnless(name in store2)
+        
+        del store2[name]
+        store2.flush()
+        self.assertEquals(store1.get(name, None), None)
+        self.assertEquals(store2.get(name, None), None)
+        self.failIf(name in store1)
+        self.failIf(name in store2)
+        
+
     def test_iteration(self):
         store = self.propertyStore
 

Modified: CalendarServer/branches/new-store/txdav/propertystore/test/test_none.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/test/test_none.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/txdav/propertystore/test/test_none.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -18,40 +18,17 @@
 Property store tests.
 """
 
-from txdav.idav import PropertyChangeNotAllowedError
 from txdav.propertystore.none import PropertyStore
-from txdav.propertystore.test.base import propertyName, propertyValue
 
 from txdav.propertystore.test import base
 
-
 class PropertyStoreTest(base.PropertyStoreTest):
     def setUp(self):
-        self.propertyStore = PropertyStore()
+        self.propertyStore = self.propertyStore1 = PropertyStore("user01", "user01")
+        self.propertyStore2 = PropertyStore("user02", "user01")
 
-    def test_flush(self):
-        store = self.propertyStore
-
-        # Flushing no changes is ok
-        store.flush()
-
-        name = propertyName("test")
-        value = propertyValue("Hello, World!")
-
-        store[name] = value
-
-        # Flushing changes isn't allowed
-        self.assertRaises(PropertyChangeNotAllowedError, store.flush)
-
-        # Changes are still here
-        self.assertEquals(store.get(name, None), value)
-
-        # Flushing no changes is ok
-        del store[name]
-        store.flush()
-
-        self.assertEquals(store.get(name, None), None)
-
     def test_abort(self):
         super(PropertyStoreTest, self).test_abort()
-        self.assertEquals(self.propertyStore.modified, {})
+        store = self.propertyStore
+        self.assertEquals(store.removed, set())
+        self.assertEquals(store.modified, {})

Modified: CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/txdav/propertystore/test/test_xattr.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -13,6 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from twext.web2.dav.element.base import WebDAVTextElement
 
 """
 Property store tests.
@@ -38,7 +39,8 @@
         tempDir.makedirs()
         tempFile = tempDir.child("test")
         tempFile.touch()
-        self.propertyStore = PropertyStore(tempFile)
+        self.propertyStore = self.propertyStore1 = PropertyStore("user01", "user01", tempFile)
+        self.propertyStore2 = PropertyStore("user02", "user01", tempFile)
 
     def test_init(self):
         store = self.propertyStore
@@ -52,7 +54,34 @@
         self.assertEquals(store.removed, set())
         self.assertEquals(store.modified, {})
 
+    def test_compress(self):
 
+        class DummyProperty (WebDAVTextElement):
+            namespace = "http://calendarserver.org/ns/"
+            name = "dummy"
+
+        name = PropertyName.fromElement(DummyProperty)
+        compressedKey = self.propertyStore._encodeKey((name, self.propertyStore._defaultuser))
+        uncompressedKey = self.propertyStore._encodeKey((name, self.propertyStore._defaultuser), compressNamespace=False)
+
+        self.propertyStore[name] = DummyProperty.fromString("data")
+        self.propertyStore.flush()
+        self.assertEqual(self.propertyStore[name], DummyProperty.fromString("data"))
+        self.assertTrue(compressedKey in self.propertyStore.attrs)
+        self.assertFalse(uncompressedKey in self.propertyStore.attrs)
+
+    def test_compress_upgrade(self):
+
+        class DummyProperty (WebDAVTextElement):
+            namespace = "http://calendarserver.org/ns/"
+            name = "dummy"
+
+        name = PropertyName.fromElement(DummyProperty)
+        uncompressedKey = self.propertyStore._encodeKey((name, self.propertyStore._defaultuser), compressNamespace=False)
+        self.propertyStore.attrs[uncompressedKey] = DummyProperty.fromString("data").toxml()
+        self.assertEqual(self.propertyStore[name], DummyProperty.fromString("data"))
+        self.assertRaises(KeyError, lambda:self.propertyStore.attrs[uncompressedKey])
+
 if PropertyStore is None:
     PropertyStoreTest.skip = importErrorMessage
 

Modified: CalendarServer/branches/new-store/txdav/propertystore/xattr.py
===================================================================
--- CalendarServer/branches/new-store/txdav/propertystore/xattr.py	2010-07-08 01:54:00 UTC (rev 5861)
+++ CalendarServer/branches/new-store/txdav/propertystore/xattr.py	2010-07-09 02:37:58 UTC (rev 5862)
@@ -70,25 +70,28 @@
     if sys.platform == "linux2":
         deadPropertyXattrPrefix = "user."
 
-    @classmethod
-    def _encodeKey(cls, name):
-        result = urllib.quote(name.toString(), safe="{}:")
-        r = cls.deadPropertyXattrPrefix + result
-        return r
+    # There is a 127 character limit for xattr keys so we need to compress/expand
+    # overly long namespaces to help stay under that limit now that GUIDs are also
+    # encoded in the keys.
+    _namespaceCompress = {
+        "urn:ietf:params:xml:ns:caldav"                       :"CALDAV:",
+        "urn:ietf:params:xml:ns:carddav"                      :"CARDDAV:",
+        "http://calendarserver.org/ns/"                       :"CS:",
+        "http://cal.me.com/_namespace/"                       :"ME:",
+        "http://twistedmatrix.com/xml_namespace/dav/"         :"TD:",
+        "http://twistedmatrix.com/xml_namespace/dav/private/" :"TDP:",
+    }
+    _namespaceExpand = dict([ (v, k) for k, v in _namespaceCompress.iteritems( ) ])
 
-    @classmethod
-    def _decodeKey(cls, name):
-        return PropertyName.fromString(
-            urllib.unquote(name[len(cls.deadPropertyXattrPrefix):])
-        )
-
-    def __init__(self, path):
+    def __init__(self, peruser, defaultuser, path):
         """
         Initialize a L{PropertyStore}.
 
         @param path: the path to set extended attributes on.
         @type path: L{CachingFilePath}
         """
+        super(PropertyStore, self).__init__(peruser, defaultuser)
+
         self.path = path
         # self.attrs = xattr(path.path)
         self.removed = set()
@@ -102,36 +105,77 @@
     def __str__(self):
         return "<%s %s>" % (self.__class__.__name__, self.path.path)
 
-    #
-    # Accessors
-    #
+    def _encodeKey(self, effective, compressNamespace=True):
+        
+        qname, uid = effective
+        namespace = self._namespaceCompress.get(qname.namespace, qname.namespace) if compressNamespace else qname.namespace
+        result = urllib.quote("{%s}%s" % (namespace, qname.name), safe="{}:")
+        if uid:
+            result = uid + result
+        r = self.deadPropertyXattrPrefix + result
+        return r
 
-    def __delitem__(self, key):
-        validKey(key)
+    def _decodeKey(self, name):
 
-        if key in self.modified:
-            del self.modified[key]
-        elif self._encodeKey(key) not in self.attrs:
-            raise KeyError(key)
+        name = urllib.unquote(name[len(self.deadPropertyXattrPrefix):])
 
-        self.removed.add(key)
+        index1 = name.find("{")
+        index2 = name.find("}")
 
-    def __getitem__(self, key):
+        if (index1 is -1 or index2 is -1 or not len(name) > index2):
+            raise ValueError("Invalid encoded name: %r" % (name,))
+        if index1 == 0:
+            uid = None
+        else:
+            uid = name[:index1]
+        propnamespace = name[index1+1:index2]
+        propnamespace = self._namespaceExpand.get(propnamespace, propnamespace)
+        propname = name[index2+1:]
+
+        return PropertyName(propnamespace, propname), uid
+
+    #
+    # Required implementations
+    #
+
+    def _getitem_uid(self, key, uid):
         validKey(key)
+        effectiveKey = (key, uid)
 
-        if key in self.modified:
-            return self.modified[key]
+        if effectiveKey in self.modified:
+            return self.modified[effectiveKey]
 
-        if key in self.removed:
+        if effectiveKey in self.removed:
             raise KeyError(key)
 
         try:
-            data = self.attrs[self._encodeKey(key)]
-        except IOError, e:
-            if e.errno in [_ERRNO_NO_ATTR, errno.ENOENT]:
-                raise KeyError(key)
-            raise PropertyStoreError(e)
+            try:
+                data = self.attrs[self._encodeKey(effectiveKey)]
+            except IOError, e:
+                if e.errno in [_ERRNO_NO_ATTR, errno.ENOENT]:
+                    raise KeyError(key)
+                raise PropertyStoreError(e)
+        except KeyError:
+            # Check for uncompressed namespace
+            if  effectiveKey[0].namespace in self._namespaceCompress:
+                try:
+                    data = self.attrs[self._encodeKey(effectiveKey, compressNamespace=False)]
+                except IOError, e:
+                    raise KeyError(key)
 
+                try:                    
+                    # Write it back using the compressed format
+                    self.attrs[self._encodeKey(effectiveKey)] = data
+                    del self.attrs[self._encodeKey(effectiveKey, compressNamespace=False)]
+                except IOError, e:
+                    msg = "Unable to upgrade property to compressed namespace: %s" % (
+                        key.toString()
+                    )
+                    self.log_error(msg)
+                    raise PropertyStoreError(msg)
+            else:
+                raise
+
         #
         # Unserialize XML data from an xattr.  The storage format has changed
         # over time:
@@ -167,27 +211,30 @@
 
         if legacy:
             # XXX untested: CDT catches this though.
-            self[key] = doc.root_element
+            self._setitem_uid(key, doc.root_element, uid)
 
         return doc.root_element
 
-    def __contains__(self, key):
+    def _setitem_uid(self, key, value, uid):
         validKey(key)
+        effectiveKey = (key, uid)
 
-        if key in self.modified:
-            return True
-        if key in self.removed:
-            return False
-        return self._encodeKey(key) in self.attrs
+        if effectiveKey in self.removed:
+            self.removed.remove(effectiveKey)
+        self.modified[effectiveKey] = value
 
-    def __setitem__(self, key, value):
+    def _delitem_uid(self, key, uid):
         validKey(key)
+        effectiveKey = (key, uid)
 
-        if key in self.removed:
-            self.removed.remove(key)
-        self.modified[key] = value
+        if effectiveKey in self.modified:
+            del self.modified[effectiveKey]
+        elif self._encodeKey(effectiveKey) not in self.attrs:
+            raise KeyError(key)
 
-    def __iter__(self):
+        self.removed.add(effectiveKey)
+
+    def _keys_uid(self, uid):
         seen = set()
 
         try:
@@ -198,19 +245,15 @@
             iterattr = iter(())
 
         for key in iterattr:
-            key = self._decodeKey(key)
-            if key not in self.removed:
-                seen.add(key)
-                yield key
+            effectivekey = self._decodeKey(key)
+            if effectivekey[1] == uid and effectivekey not in self.removed:
+                seen.add(effectivekey)
+                yield effectivekey[0]
 
-        for key in self.modified:
-            if key not in seen:
-                yield key
-
-
-    def __len__(self):
-        return len(self.keys())
-
+        for effectivekey in self.modified:
+            if effectivekey[1] == uid and effectivekey not in seen:
+                yield effectivekey[0]
+            
     #
     # I/O
     #
@@ -244,6 +287,9 @@
             assert key not in removed
             value = modified[key]
             attrs[self._encodeKey(key)] = compress(value.toxml())
+        
+        self.removed.clear()
+        self.modified.clear()        
 
     def abort(self):
         self.removed.clear()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100708/43c9731e/attachment-0001.html>


More information about the calendarserver-changes mailing list