[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