[CalendarServer-changes] [11666] CalendarServer/branches/users/gaya/sharedgroupfixes2
source_changes at macosforge.org
source_changes at macosforge.org
Wed Sep 11 09:56:57 PDT 2013
Revision: 11666
http://trac.calendarserver.org//changeset/11666
Author: gaya at apple.com
Date: 2013-09-11 09:56:57 -0700 (Wed, 11 Sep 2013)
Log Message:
-----------
Basics needed for group revisions.
Modified Paths:
--------------
CalendarServer/branches/users/gaya/sharedgroupfixes2/twistedcaldav/storebridge.py
CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/carddav/datastore/sql.py
CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/carddav/datastore/test/test_sql.py
CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/common/datastore/sql.py
Modified: CalendarServer/branches/users/gaya/sharedgroupfixes2/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroupfixes2/twistedcaldav/storebridge.py 2013-09-10 16:49:44 UTC (rev 11665)
+++ CalendarServer/branches/users/gaya/sharedgroupfixes2/twistedcaldav/storebridge.py 2013-09-11 16:56:57 UTC (rev 11666)
@@ -2607,7 +2607,7 @@
# Content-type check
content_type = request.headers.getHeader("content-type")
if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
- log.error("MIME type %s not allowed in calendar collection" % (content_type,))
+ log.error("MIME type {content_type} not allowed in calendar collection", content_type=content_type)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(caldav_namespace, "supported-calendar-data"),
@@ -3245,7 +3245,7 @@
# Content-type check
content_type = request.headers.getHeader("content-type")
if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "vcard"):
- log.error("MIME type %s not allowed in vcard collection" % (content_type,))
+ log.error("MIME type {content_type} not allowed in vcard collection", content_type=content_type)
raise HTTPError(ErrorResponse(
responsecode.FORBIDDEN,
(carddav_namespace, "supported-address-data"),
Modified: CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/carddav/datastore/sql.py 2013-09-10 16:49:44 UTC (rev 11665)
+++ CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/carddav/datastore/sql.py 2013-09-11 16:56:57 UTC (rev 11666)
@@ -398,7 +398,10 @@
def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, bindName=None):
ownerName = ownerHome.addressbook().name() if ownerHome else None
- super(AddressBook, self).__init__(home, name, resourceID, mode, status, revision=revision, message=message, ownerHome=ownerHome, ownerName=ownerName)
+ super(AddressBook, self).__init__(
+ home, name, resourceID, mode, status, revision=revision,
+ message=message, ownerHome=ownerHome, ownerName=ownerName
+ )
self._index = PostgresLegacyABIndexEmulator(self)
self._bindName = bindName
@@ -451,6 +454,201 @@
@inlineCallbacks
+ def resourceNamesSinceRevision(self, revision):
+ """
+ Return the changed and deleted resources since a particular revision. This implementation takes
+ into account sharing by making use of the bindRevision attribute to determine if the requested
+ revision is earlier than the share acceptance. If so, then we need to return all resources in
+ the results since the collection is in effect "new".
+
+ @param revision: the revision to determine changes since
+ @type revision: C{int}
+ """
+
+ print("AddressBook: resourceNamesSinceRevision:%s revision:%s" % (self, revision,))
+ if True: # self.owned() or self.fullyShared():
+ returnValue((yield super(AddressBook, self).resourceNamesSinceRevision(revision)))
+
+ # call sharedChildResourceNamesSinceRevision() and filter results
+ sharedChildChanged, sharedChildDeleted = yield self.sharedChildResourceNamesSinceRevision(revision, "infinity")
+
+ path = self.name() + '/'
+ lenpath = len(path)
+ changed = [item[lenpath:] for item in sharedChildChanged if item.startswith(path) and item != path]
+ deleted = [item[lenpath:] for item in sharedChildDeleted if item.startswith(path) and item != path]
+ returnValue((changed, deleted,))
+
+
+ @inlineCallbacks
+ def sharedChildResourceNamesSinceRevision(self, revision, depth):
+ """
+ Determine the list of child resources that have changed since the specified sync revision.
+ We do the same SQL query for both depth "1" and "infinity", but filter the results for
+ "1" to only account for a collection change.
+
+ We need to handle shared collection a little differently from owned ones. When a shared collection
+ is bound into a home we record a revision for it using the sharee home id and sharee collection name.
+ That revision is the "starting point" for changes: so if sync occurs with a revision earlier than
+ that, we return the list of all resources in the shared collection since they are all "new" as far
+ as the client is concerned since the shared collection has just appeared. For a later revision, we
+ just report the changes since that one. When a shared collection is removed from a home, we again
+ record a revision for the sharee home and sharee collection name with the "deleted" flag set. That way
+ the shared collection can be reported as removed.
+
+ @param revision: the sync revision to compare to
+ @type revision: C{str}
+ @param depth: depth for determine what changed
+ @type depth: C{str}
+ """
+
+ print("sharedChildResourceNamesSinceRevision:%s revision:%s, depth:%s" % (self, revision, depth))
+ assert not self.owned()
+
+ if True:#self.fullyShared():
+ returnValue((yield super(AddressBook, self).sharedChildResourceNamesSinceRevision(revision, depth)))
+
+ changed = set()
+ deleted = set()
+
+ groupBindRows = yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
+ self._txn, homeID=self._home._resourceID, addressbookID=self._resourceID
+ )
+ acceptedGroupIDs = [groupBindRow[2] for groupBindRow in groupBindRows]
+ minRevision = min([groupBindRow[5] for groupBindRow in groupBindRows])
+ sharerevision = 0 if revision < minRevision else revision
+ print("sharedChildResourceNamesSinceRevision:%s minRevision:%s, sharerevision:%s" % (self, minRevision, sharerevision))
+
+ # get revision table changes
+ rev = self._revisionsSchema
+ results = [(
+ name,
+ wasdeleted,
+ ) for name, wasdeleted in (
+ yield Select([rev.RESOURCE_NAME, rev.DELETED],
+ From=rev,
+ Where=(rev.REVISION > sharerevision).And(
+ rev.RESOURCE_ID == self._resourceID)).on(self._txn)
+ ) if name
+ ]
+ print("sharedChildResourceNamesSinceRevision:%s results:%s" % (self, results))
+
+ # get members table changes
+ aboMembers = schema.ABO_MEMBERS
+ memberRows = yield Select(
+ [aboMembers.GROUP_ID, aboMembers.MEMBER_ID, aboMembers.REMOVED],
+ From=aboMembers,
+ Where=(aboMembers.REVISION > sharerevision).And(
+ aboMembers.ADDRESSBOOK_ID == self._resourceID)
+ ).on(self._txn)
+ groupIDToChangedMemberIDMap = {}
+ for groupID, memberID, removed in memberRows:
+ if groupID not in groupIDToChangedMemberIDMap:
+ groupIDToChangedMemberIDMap[groupID] = set()
+ groupIDToChangedMemberIDMap[groupID].add(memberID)
+ print("sharedChildResourceNamesSinceRevision:%s memberRows:%s groupIDToChangedMemberIDMap=%s" % (self, memberRows, groupIDToChangedMemberIDMap,))
+
+ allowedObjectIDs = None
+ @inlineCallbacks
+ def allowedObjectIDs(acceptedGroupIDs):
+ if not hasattr(self, "__allowedObjectIDs"):
+ self.__allowedObjectIDs = yield self.expandGroupIDs(self._txn, acceptedGroupIDs)
+ returnValue(self.__allowedObjectIDs)
+
+ @inlineCallbacks
+ def getMissingNames(ids, idToNameMap):
+ idsForMissingNames = list(set(ids) - set(idToNameMap.keys()))
+ if idsForMissingNames:
+ abo = schema.ADDRESSBOOK_OBJECT
+ memberIDNameRows = (
+ yield AddressBookObject._columnsWithResourceIDsQuery(
+ [abo.RESOURCE_ID, abo.RESOURCE_NAME],
+ idsForMissingNames
+ ).on(self._txn, resourceIDs=idsForMissingNames)
+ )
+ idToNameMap = dict(dict(idToNameMap), **dict(memberIDNameRows))
+
+ returnValue(idToNameMap)
+
+ allIDs = set()
+ for groupID, memberID, removed in memberRows:
+ allIDs.add(groupID)
+ allIDs.add(memberID)
+
+ idToNameMap = yield getMissingNames(allIDs, {})
+ print("sharedChildResourceNamesSinceRevision:%s idToNameMap=%s" % (self, idToNameMap,))
+
+ path = self.name()
+ removedMemberIDs = set()
+ changedMemberIDs = set()
+
+ # TODO: use for query
+ allowedObjectIDs = yield self.expandGroupIDs(self._txn, acceptedGroupIDs)
+ print("sharedChildResourceNamesSinceRevision:%s acceptedGroupIDs:%s allowedObjectIDs=%s," % (self, acceptedGroupIDs, allowedObjectIDs,))
+
+ for groupID, memberID, removed in memberRows:
+
+ if memberID not in allowedObjectIDs or groupID not in allowedObjectIDs:
+ print("sharedChildResourceNamesSinceRevision:%s SKIP groupID:%s memberID=%s, wasdeleted=%s" % (self, groupID, memberID, wasdeleted,))
+ continue
+ print("sharedChildResourceNamesSinceRevision:%s groupID:%s memberID=%s, wasdeleted=%s" % (self, groupID, memberID, wasdeleted,))
+
+ if removed:
+ if sharerevision:
+ if depth == "1":
+ changed.add("%s/" % (path,))
+ else:
+ removedMemberIDs.add(memberID)
+
+ # Always report collection as changed
+ changed.add("%s/" % (path,))
+
+ # Resource changed - for depth "infinity" report resource as changed
+ if depth != "1":
+ changedMemberIDs.add(memberID)
+ changedMemberIDs.add(groupID)
+
+ print("sharedChildResourceNamesSinceRevision:%s changed:%s deleted=%s, removedMemberIDs=%s, changedMemberIDs=%s" % (self, changed, deleted, removedMemberIDs, changedMemberIDs))
+
+ '''
+ nameToIDMap = dict([(v, k) for k, v in idToNameMap.iteritems()])
+
+ for name, wasdeleted in results:
+
+ id = nameToIDMap[name]
+ if id not in allowedObjectIDs:
+ print("sharedChildResourceNamesSinceRevision:%s SKIP name:%s id=%s, wasdeleted=%s" % (self, name, id, wasdeleted,))
+ continue
+
+ print("sharedChildResourceNamesSinceRevision:%s name:%s id=%s, wasdeleted=%s" % (self, name, id, wasdeleted,))
+ if wasdeleted:
+ if sharerevision:
+ if depth == "1":
+ changed.add("%s/" % (path,))
+ else:
+ deleted.add("%s/%s" % (path, name,))
+
+ # Always report collection as changed
+ changed.add("%s/" % (path,))
+
+ # Resource changed - for depth "infinity" report resource as changed
+ if depth != "1":
+ changed.add("%s/%s" % (path, name,))
+
+ print("sharedChildResourceNamesSinceRevision:%s changed:%s deleted=%s, removedMemberIDs=%s, changedMemberIDs=%s" % (self, changed, deleted, removedMemberIDs, changedMemberIDs))
+ '''
+
+ # for changes, get object names all at once here
+ idToNameMap = yield getMissingNames(removedMemberIDs | changedMemberIDs, idToNameMap)
+ for removedMemberID in removedMemberIDs:
+ deleted.add("%s/%s" % (path, idToNameMap[removedMemberID],))
+
+ for changedMemberID in changedMemberIDs:
+ changed.add("%s/%s" % (path, idToNameMap[changedMemberID],))
+
+ returnValue((changed, deleted))
+
+
+ @inlineCallbacks
def _loadPropertyStore(self, props=None):
if props is None:
props = yield PropertyStore.load(
@@ -491,11 +689,29 @@
# raise HomeChildNameNotAllowedError
raise HTTPError(FORBIDDEN)
+ '''
+ @inlineCallbacks
+ def removedObjectResource(self, child):
+ """
+ just like CommonHomeChild.removedObjectResource() but does not call self._deleteRevision(child.name())
+ """
+ self._objects.pop(child.name(), None)
+ self._objects.pop(child.uid(), None)
+ if self._objectNames and child.name() in self._objectNames:
+ self._objectNames.remove(child.name())
+ #yield self._deleteRevision(child.name())
+ yield self.notifyChanged()
+ '''
+
@inlineCallbacks
def remove(self):
if self._resourceID == self._home._resourceID:
+
+ # Note that revision table is NOT queried for removes
+ yield self._updateRevision(self.name())
+
# Allow remove, as a way to reset the address book to an empty state
for abo in (yield self.objectResources()):
yield abo.remove()
@@ -503,9 +719,6 @@
yield self.unshare() # storebridge should already have done this
- # Note that revision table is NOT queried for removes
- yield self._updateRevision(self.name())
-
yield self.properties()._removeResource()
yield self._loadPropertyStore()
@@ -691,9 +904,7 @@
ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
if ownerHome not in ownerHomeToDataRowMap:
groupBindRow[0] = _BIND_MODE_WRITE
- groupBindRow[3] = None # bindName
- groupBindRow[4] = None # bindStatus
- groupBindRow[6] = None # bindMessage
+ groupBindRow[3:7] = 4 * [None] # bindName, bindStatus, bindRevision, bindMessage
ownerHomeToDataRowMap[ownerHome] = groupBindRow
if ownerHomeToDataRowMap:
@@ -765,7 +976,7 @@
# Retrieve data from cache
cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
cachedRows = yield queryCacher.get(cacheKey)
- if cachedRows and (cachedRows[0][4] == _BIND_STATUS_ACCEPTED) == bool(accepted):
+ if cachedRows and (cachedRows[0][8] == _BIND_STATUS_ACCEPTED) == bool(accepted): #cachedRows[0][8] == cachedBindStatus
rows = cachedRows
if not rows:
@@ -785,17 +996,23 @@
groupBindRows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
home._txn, homeID=home._resourceID, addressbookID=ownerAddressBook._resourceID
)
- for groupBindRow in groupBindRows:
- if (groupBindRow[4] == _BIND_STATUS_ACCEPTED) == bool(accepted):
- groupBindRow.insert(AddressBookObject.bindColumnCount, ownerAddressBook._resourceID)
- groupBindRow.insert(AddressBookObject.bindColumnCount + 1, groupBindRow[4])
- groupBindRow[0] = _BIND_MODE_WRITE
- groupBindRow[3] = None # bindName
- groupBindRow[4] = None # bindStatus
- groupBindRow[6] = None # bindMessage
- rows = [groupBindRow]
- break
+ # prefer a bindStatus match, otherwise get row for cache
+ matchingGroupBindRows = [groupBindRow for groupBindRow in groupBindRows if (groupBindRow[4] == _BIND_STATUS_ACCEPTED) == bool(accepted)]
+ if matchingGroupBindRows:
+ groupBindRows = matchingGroupBindRows
+ elif bindRows:
+ bindRows[0].insert(cls.bindColumnCount, ownerAddressBook._resourceID)
+ bindRows[0].insert(cls.bindColumnCount + 1, bindRows[0][4]) # cachedStatus = bindStatus
+ rows = bindRows
+ if not rows and groupBindRows:
+ groupBindRow = groupBindRows[0]
+ groupBindRow.insert(AddressBookObject.bindColumnCount, ownerAddressBook._resourceID)
+ groupBindRow.insert(AddressBookObject.bindColumnCount + 1, groupBindRow[4])
+ groupBindRow[0] = _BIND_MODE_WRITE
+ groupBindRow[3:7] = 4 * [None] # bindName, bindStatus, bindRevision, bindMessage
+ rows = [groupBindRow]
+
if rows and queryCacher:
# Cache the result
queryCacher.setAfterCommit(home._txn, cacheKey, rows)
@@ -839,8 +1056,7 @@
"""
bindRows = yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)
if bindRows and (bindRows[0][4] == _BIND_STATUS_ACCEPTED) == bool(accepted):
- bindRow = bindRows[0]
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = bindRow[:cls.bindColumnCount] #@UnusedVariable
+ resourceID = bindRows[0][2]
# alt:
# returnValue((yield cls.objectWithID(home, resourceID)))
@@ -854,8 +1070,7 @@
home._txn, name=name, homeID=home._resourceID
)
if groupBindRows and (groupBindRows[0][4] == _BIND_STATUS_ACCEPTED) == bool(accepted):
- groupBindRow = groupBindRows[0]
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ resourceID = groupBindRows[0][2]
ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
# alt:
@@ -893,8 +1108,7 @@
home._txn, resourceID=resourceID, homeID=home._resourceID
)
if bindRows and (bindRows[0][4] == _BIND_STATUS_ACCEPTED) == bool(accepted):
- bindRow = bindRows[0]
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = bindRow[:cls.bindColumnCount] #@UnusedVariable
+ resourceID = bindRows[0][2]
ownerHome = yield home.ownerHomeWithChildID(resourceID)
if accepted:
@@ -906,8 +1120,7 @@
home._txn, homeID=home._resourceID, addressbookID=resourceID
)
if groupBindRows and (groupBindRows[0][4] == _BIND_STATUS_ACCEPTED) == bool(accepted):
- groupBindRow = groupBindRows[0]
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ resourceID = groupBindRows[0][2]
ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
@@ -944,18 +1157,18 @@
home._txn, homeID=home._resourceID
)
for row in rows:
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
+ resourceID = row[2]
ownerHome = yield home._txn.homeWithResourceID(home._homeType, resourceID, create=True)
- names |= set([ownerHome.shareeAddressBookName()])
+ names.add(ownerHome.shareeAddressBookName())
groupRows = yield AddressBookObject._acceptedBindForHomeID.on(
home._txn, homeID=home._resourceID
)
for groupRow in groupRows:
- bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ resourceID = groupRow[2]
ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerAddressBookID, create=True)
- names |= set([ownerHome.shareeAddressBookName()])
+ names.add(ownerHome.shareeAddressBookName())
returnValue(tuple(names))
@@ -1060,7 +1273,7 @@
rows = yield self._allColumnsWithParent(self)
ids = set([row[1] for row in rows])
if self.fullyShared():
- ids |= set([self._resourceID, ])
+ ids.add(self._resourceID)
if self.owned() or self._bindMode == _BIND_MODE_WRITE:
returnValue(tuple(readOnlyIDs), tuple(readWriteIDs))
readOnlyIDs = set(ids)
@@ -1134,15 +1347,18 @@
# remove None parameters, and substitute None for empty string
bind = self._bindSchema
- columnMap = dict([(k, v if v != "" else None)
- for k, v in {bind.BIND_MODE:mode,
- bind.BIND_STATUS:status,
- bind.MESSAGE:message}.iteritems() if v is not None])
+ columnMap = {}
+ if mode != None and mode != shareeView._bindMode:
+ columnMap[bind.BIND_MODE] = mode
+ if status != None and status != shareeView._bindStatus:
+ columnMap[bind.BIND_STATUS] = status
+ if message != None and message != shareeView._bindMessage:
+ columnMap[bind.MESSAGE] = message
- if len(columnMap):
+ if columnMap:
# count accepted
- if status is not None:
+ if bind.BIND_STATUS in columnMap:
previouslyAcceptedBindCount = 1 if shareeView.fullyShared() else 0
previouslyAcceptedBindCount += len((yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
self._txn, homeID=shareeView.viewerHome()._resourceID, addressbookID=shareeView._resourceID
@@ -1150,28 +1366,29 @@
bindNameRows = yield self._updateBindColumnsQuery(columnMap).on(
self._txn,
- resourceID=self._resourceID, homeID=shareeView.viewerHome()._resourceID
+ resourceID=self._resourceID,
+ homeID=shareeView.viewerHome()._resourceID
)
# update affected attributes
- if mode is not None:
+ if bind.BIND_MODE in columnMap:
shareeView._bindMode = columnMap[bind.BIND_MODE]
- if status is not None:
+ if bind.BIND_STATUS in columnMap:
shareeView._bindStatus = columnMap[bind.BIND_STATUS]
if shareeView._bindStatus == _BIND_STATUS_ACCEPTED:
if 0 == previouslyAcceptedBindCount:
yield shareeView._initSyncToken()
- yield shareeView._initBindRevision()
shareeView.viewerHome()._children[self.shareeName()] = shareeView
shareeView.viewerHome()._children[shareeView._resourceID] = shareeView
+ yield shareeView._initBindRevision()
elif shareeView._bindStatus == _BIND_STATUS_DECLINED:
if 1 == previouslyAcceptedBindCount:
yield shareeView._deletedSyncToken(sharedRemoval=True)
shareeView.viewerHome()._children.pop(self.shareeName(), None)
shareeView.viewerHome()._children.pop(shareeView._resourceID, None)
- if message is not None:
+ if bind.MESSAGE in columnMap:
shareeView._bindMessage = columnMap[bind.MESSAGE]
queryCacher = self._txn._queryCacher
@@ -1234,7 +1451,7 @@
# Must send notification to ensure cache invalidation occurs
yield self.notifyPropertyChanged()
- # delete binds including invites
+ # delete bind table rows for this share
deletedBindNameRows = yield self._deleteBindForResourceIDAndHomeID.on(self._txn, resourceID=self._resourceID,
homeID=shareeHome._resourceID
)
@@ -1277,6 +1494,7 @@
self._bindStatus = None
self._bindMessage = None
self._bindName = None
+ self._bindRevision = None
super(AddressBookObject, self).__init__(addressbook, name, uid, resourceID, options)
self._options = {} if options is None else options
@@ -1322,12 +1540,12 @@
raise HTTPError(FORBIDDEN)
if not self.owned() and not self.addressbook().fullyShared():
- readWriteObjectIDs = []
readWriteGroupIDs = yield self.addressbook().readWriteGroupIDs()
- if readWriteGroupIDs:
- readWriteObjectIDs = yield self.addressbook().expandGroupIDs(self._txn, readWriteGroupIDs)
+ readWriteObjectIDs = (
+ yield self.addressbook().expandGroupIDs(self._txn, readWriteGroupIDs)
+ ) if readWriteGroupIDs else []
- # can't delete item in shared group, even if user has addressbook unbind
+ # can't delete item in read-only shared group, even if user has addressbook unbind
if self._resourceID not in readWriteObjectIDs:
raise HTTPError(FORBIDDEN)
@@ -1467,10 +1685,8 @@
resourceIDs=allowedObjectIDs,
))
elif self._resourceID:
- if self._resourceID not in allowedObjectIDs:
- # allow invited groups
- allowedObjectIDs = yield self.addressbook().unacceptedGroupIDs()
- if self._resourceID in allowedObjectIDs:
+ if (self._resourceID in allowedObjectIDs or
+ self._resourceID in (yield self.addressbook().unacceptedGroupIDs())): # allow invited groups
rows = (yield self._allColumnsWithResourceID.on(
self._txn, resourceID=self._resourceID,
))
@@ -1491,6 +1707,7 @@
self._bindStatus = bindStatus
self._bindMessage = bindMessage
self._bindName = bindName
+ self._bindRevision = bindRevision
yield self._initIsShared()
@@ -1588,16 +1805,6 @@
returnValue(rows)
- @inlineCallbacks
- def _changeAddressBookRevision(self, addressbook, inserting=False):
- if inserting:
- yield addressbook._insertRevision(self._name)
- else:
- yield addressbook._updateRevision(self._name)
-
- yield addressbook.notifyChanged()
-
-
# Stuff from put_addressbook_common
def fullValidation(self, component, inserting):
"""
@@ -1608,7 +1815,14 @@
# Valid data sizes
if config.MaxResourceSize:
- vcardsize = len(str(component))
+ if self._componentResourceKindToKind(component) == _ABO_KIND_GROUP:
+ thinGroup = deepcopy(component)
+ thinGroup.removeProperties("X-ADDRESSBOOKSERVER-MEMBER")
+ thinGroup.removeProperties("X-ADDRESSBOOKSERVER-KIND")
+ thinGroup.removeProperties("UID")
+ vcardsize = len(str(thinGroup))
+ else:
+ vcardsize = len(str(component))
if vcardsize > config.MaxResourceSize:
raise ObjectResourceTooBigError()
@@ -1687,30 +1901,22 @@
self._componentChanged = False
- # Handle all validation operations here.
- self.fullValidation(component, inserting)
+ if "coaddedUIDs" not in self._options:
+ # Handle all validation operations here.
+ self.fullValidation(component, inserting)
- # UID lock - this will remain active until the end of the current txn
- if not inserting or self._options.get("coaddedUIDs") is None:
+ # UID lock - this will remain active until the end of the current txn
yield self._lockUID(component, inserting)
+ if inserting:
+ yield self.addressbook()._insertRevision(self._name)
+ else:
+ yield self.addressbook()._updateRevision(self._name)
+
+ yield self.addressbook().notifyChanged()
+
yield self.updateDatabase(component, inserting=inserting)
- yield self._changeAddressBookRevision(self._addressbook, inserting)
- if self.owned():
- # update revision table of the sharee group address book
- if self._kind == _ABO_KIND_GROUP: # optimization
- invites = yield self.sharingInvites()
- for invite in invites:
- shareeHome = (yield self._txn.homeWithResourceID(self.addressbook()._home._homeType, invite.shareeHomeID()))
- yield self._changeAddressBookRevision(shareeHome.addressbook(), inserting)
- # one is enough because all have the same resourceID
- break
- else:
- if self.addressbook()._resourceID != self._ownerAddressBookResourceID:
- # update revisions table of shared group's containing address book
- yield self._changeAddressBookRevision(self.ownerHome().addressbook(), inserting)
-
returnValue(self._componentChanged)
@@ -1744,7 +1950,7 @@
@classproperty
- def _insertABObject(cls): #@NoSelf
+ def _insertABObjectQuery(cls): #@NoSelf
"""
DAL statement to create an addressbook object with all default values.
"""
@@ -1809,7 +2015,7 @@
if not self.owned() and not self.addressbook().fullyShared():
# in partially shared addressbook, all members UIDs must be inside the shared groups
# except during bulk operations, when other UIDs added are OK
- coaddedUIDs = set() if self._options.get("coaddedUIDs") is None else self._options["coaddedUIDs"]
+ coaddedUIDs = self._options.get("coaddedUIDs", set())
if missingUIDs - coaddedUIDs:
raise GroupWithUnsharedAddressNotAllowedError(missingUIDs)
@@ -1824,16 +2030,16 @@
# sort unique members
component.removeProperties("X-ADDRESSBOOKSERVER-MEMBER")
- for memberAddress in sorted(list(memberAddresses)): # sort unique
+ for memberAddress in sorted(memberAddresses):
component.addProperty(Property("X-ADDRESSBOOKSERVER-MEMBER", memberAddress))
componentText = str(component)
# remove unneeded fields to get stored _objectText
- thinComponent = deepcopy(component)
- thinComponent.removeProperties("X-ADDRESSBOOKSERVER-MEMBER")
- thinComponent.removeProperties("X-ADDRESSBOOKSERVER-KIND")
- thinComponent.removeProperties("UID")
- self._objectText = str(thinComponent)
+ thinGroup = deepcopy(component)
+ thinGroup.removeProperties("X-ADDRESSBOOKSERVER-MEMBER")
+ thinGroup.removeProperties("X-ADDRESSBOOKSERVER-KIND")
+ thinGroup.removeProperties("UID")
+ self._objectText = str(thinGroup)
else:
componentText = str(component)
self._objectText = componentText
@@ -1853,7 +2059,7 @@
if inserting:
self._resourceID, self._created, self._modified = (
- yield self._insertABObject.on(
+ yield self._insertABObjectQuery.on(
self._txn,
addressbookResourceID=self._ownerAddressBookResourceID,
name=self._name,
@@ -1864,10 +2070,9 @@
)
)[0]
- # delete foreign members table row for this object
+ # delete foreign members table rows for this object, keeping revision
groupIDRows = yield Delete(
aboForeignMembers,
- # should this be scoped to the owner address book?
Where=aboForeignMembers.MEMBER_ADDRESS == "urn:uuid:" + self._uid,
Return=aboForeignMembers.GROUP_ID
).on(self._txn)
@@ -2288,21 +2493,16 @@
returnValue(bindName)
+ @property
+ def _syncTokenRevision(self):
+ return self.addressbook()._syncTokenRevision
+
+
@inlineCallbacks
- def _initBindRevision(self):
- yield self.addressbook()._initBindRevision()
+ def syncToken(self):
+ returnValue((yield self.addressbook().syncToken()))
- bind = self._bindSchema
- yield self._updateBindColumnsQuery(
- {bind.BIND_REVISION : Parameter("revision"), }).on(
- self._txn,
- revision=self.addressbook()._bindRevision,
- resourceID=self._resourceID,
- homeID=self.viewerHome()._resourceID,
- )
- yield self.invalidateQueryCache()
-
@inlineCallbacks
def updateShare(self, shareeView, mode=None, status=None, message=None):
"""
@@ -2333,15 +2533,18 @@
# remove None parameters, and substitute None for empty string
bind = self._bindSchema
- columnMap = dict([(k, v if v != "" else None)
- for k, v in {bind.BIND_MODE:mode,
- bind.BIND_STATUS:status,
- bind.MESSAGE:message}.iteritems() if v is not None])
+ columnMap = {}
+ if mode != None and mode != shareeView._bindMode:
+ columnMap[bind.BIND_MODE] = mode
+ if status != None and status != shareeView._bindStatus:
+ columnMap[bind.BIND_STATUS] = status
+ if message != None and message != shareeView._bindMessage:
+ columnMap[bind.MESSAGE] = message
- if len(columnMap):
+ if columnMap:
# count accepted
- if status is not None:
+ if bind.BIND_STATUS in columnMap:
previouslyAcceptedBindCount = 1 if self.addressbook().fullyShared() else 0
previouslyAcceptedBindCount += len((
yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
@@ -2351,28 +2554,29 @@
bindNameRows = yield self._updateBindColumnsQuery(columnMap).on(
self._txn,
- resourceID=self._resourceID, homeID=shareeView.viewerHome()._resourceID
+ resourceID=self._resourceID,
+ homeID=shareeView.viewerHome()._resourceID
)
# update affected attributes
- if mode is not None:
+ if bind.BIND_MODE in columnMap:
shareeView._bindMode = columnMap[bind.BIND_MODE]
- if status is not None:
+ if bind.BIND_STATUS in columnMap:
shareeView._bindStatus = columnMap[bind.BIND_STATUS]
if shareeView._bindStatus == _BIND_STATUS_ACCEPTED:
if 0 == previouslyAcceptedBindCount:
yield shareeView.addressbook()._initSyncToken()
- yield shareeView._initBindRevision()
shareeView.viewerHome()._children[self.addressbook().shareeName()] = shareeView.addressbook()
shareeView.viewerHome()._children[shareeView._resourceID] = shareeView.addressbook()
+ yield shareeView._initBindRevision()
elif shareeView._bindStatus != _BIND_STATUS_INVITED:
if 1 == previouslyAcceptedBindCount:
yield shareeView.addressbook()._deletedSyncToken(sharedRemoval=True)
shareeView.viewerHome()._children.pop(self.addressbook().shareeName(), None)
shareeView.viewerHome()._children.pop(shareeView._resourceID, None)
- if message is not None:
+ if bind.MESSAGE in columnMap:
shareeView._bindMessage = columnMap[bind.MESSAGE]
# safer to just invalidate in all cases rather than calculate when to invalidate
Modified: CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/carddav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/carddav/datastore/test/test_sql.py 2013-09-10 16:49:44 UTC (rev 11665)
+++ CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/carddav/datastore/test/test_sql.py 2013-09-11 16:56:57 UTC (rev 11666)
@@ -574,11 +574,11 @@
)
groupObject = yield adbk.createAddressBookObjectWithName("g.vcf", group)
- aboForeignMembers = schema.ABO_FOREIGN_MEMBERS
aboMembers = schema.ABO_MEMBERS
- memberRows = yield Select([aboMembers.GROUP_ID, aboMembers.MEMBER_ID], From=aboMembers,).on(txn)
+ memberRows = yield Select([aboMembers.GROUP_ID, aboMembers.MEMBER_ID], From=aboMembers).on(txn)
self.assertEqual(memberRows, [])
+ aboForeignMembers = schema.ABO_FOREIGN_MEMBERS
foreignMemberRows = yield Select([aboForeignMembers.GROUP_ID, aboForeignMembers.MEMBER_ADDRESS], From=aboForeignMembers).on(txn)
self.assertEqual(foreignMemberRows, [[groupObject._resourceID, "urn:uuid:uid3"]])
@@ -597,7 +597,7 @@
)
subgroupObject = yield adbk.createAddressBookObjectWithName("sg.vcf", subgroup)
- memberRows = yield Select([aboMembers.GROUP_ID, aboMembers.MEMBER_ID], From=aboMembers,).on(txn)
+ memberRows = yield Select([aboMembers.GROUP_ID, aboMembers.MEMBER_ID], From=aboMembers).on(txn)
self.assertEqual(sorted(memberRows), sorted([
[groupObject._resourceID, subgroupObject._resourceID],
[subgroupObject._resourceID, personObject._resourceID],
@@ -607,7 +607,7 @@
self.assertEqual(foreignMemberRows, [])
yield subgroupObject.remove()
- memberRows = yield Select([aboMembers.GROUP_ID, aboMembers.MEMBER_ID], From=aboMembers,).on(txn)
+ memberRows = yield Select([aboMembers.GROUP_ID, aboMembers.MEMBER_ID], From=aboMembers).on(txn)
self.assertEqual(memberRows, [])
foreignMemberRows = yield Select([aboForeignMembers.GROUP_ID, aboForeignMembers.MEMBER_ADDRESS], From=aboForeignMembers,
@@ -901,8 +901,9 @@
self.assertEqual(normalAB._bindRevision, 0)
otherHome = yield self.homeUnderTest(name="home2")
otherGroup = yield otherHome.objectWithShareUID(newGroupShareUID)
+ self.assertNotEqual(otherGroup._bindRevision, 0)
otherAB = otherGroup.addressbook()
- self.assertNotEqual(otherAB._bindRevision, 0)
+ self.assertEqual(otherAB._bindRevision, None)
@inlineCallbacks
@@ -952,8 +953,9 @@
self.assertEqual(normalAB._bindRevision, 0)
otherHome = yield self.homeUnderTest(name="home2")
otherGroup = yield otherHome.invitedObjectWithShareUID(newGroupShareUID)
+ self.assertEqual(otherGroup._bindRevision, 0)
otherAB = otherGroup.addressbook()
- self.assertEqual(otherAB._bindRevision, 0)
+ self.assertEqual(otherAB._bindRevision, None)
yield self.commit()
normalAB = yield self.addressbookUnderTest(home="home3")
@@ -967,8 +969,9 @@
self.assertEqual(normalAB._bindRevision, 0)
otherHome = yield self.homeUnderTest(name="home2")
otherGroup = yield otherHome.objectWithShareUID(newGroupShareUID)
+ self.assertNotEqual(otherGroup._bindRevision, 0)
otherAB = otherGroup.addressbook()
- self.assertNotEqual(otherAB._bindRevision, 0)
+ self.assertEqual(otherAB._bindRevision, None)
@inlineCallbacks
@@ -1022,23 +1025,28 @@
self.assertEqual(normalAB._bindRevision, 0)
otherHome = yield self.homeUnderTest(name="home2")
otherGroup = yield otherHome.objectWithShareUID(newGroupShareUID)
+ self.assertNotEqual(otherGroup._bindRevision, 0)
otherAB = otherGroup.addressbook()
- self.assertNotEqual(otherAB._bindRevision, 0)
+ self.assertEqual(otherAB._bindRevision, None)
- changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision - 1)
- self.assertNotEqual(len(changed), 0)
+ changed, deleted = yield otherAB.resourceNamesSinceRevision(otherGroup._bindRevision - 1)
+ print("revision=%s, changed=%s, deleted=%s" % (otherGroup._bindRevision - 1, changed, deleted,))
+ self.assertEqual(set(changed), set(['1.vcf', '4.vcf', '2.vcf', ]))
self.assertEqual(len(deleted), 0)
- changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
+ changed, deleted = yield otherAB.resourceNamesSinceRevision(otherGroup._bindRevision)
+ print("revision=%s, changed=%s, deleted=%s" % (otherGroup._bindRevision, changed, deleted,))
self.assertEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
- for depth in ("1", "infinity",):
- changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision - 1, depth)
- self.assertNotEqual(len(changed), 0)
+ for depth, result in (("1", ['home3/']), ("infinity", ['home3/1.vcf', 'home3/4.vcf', 'home3/2.vcf', 'home3/'])):
+ changed, deleted = yield otherHome.resourceNamesSinceRevision(otherGroup._bindRevision - 1, depth)
+ print("revision=%s, depth=%s, changed=%s, deleted=%s" % (otherGroup._bindRevision - 1, depth, changed, deleted,))
+ self.assertEqual(set(changed), set(result))
self.assertEqual(len(deleted), 0)
- changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
+ changed, deleted = yield otherHome.resourceNamesSinceRevision(otherGroup._bindRevision, depth)
+ print("revision=%s, depth=%s, changed=%s, deleted=%s" % (otherGroup._bindRevision, depth, changed, deleted,))
self.assertEqual(len(changed), 0)
self.assertEqual(len(deleted), 0)
Modified: CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/common/datastore/sql.py 2013-09-10 16:49:44 UTC (rev 11665)
+++ CalendarServer/branches/users/gaya/sharedgroupfixes2/txdav/common/datastore/sql.py 2013-09-11 16:56:57 UTC (rev 11666)
@@ -1918,7 +1918,6 @@
changed = set()
deleted = set()
deleted_collections = set()
- changed_collections = set()
for path, name, wasdeleted in results:
if wasdeleted:
if revision:
@@ -1934,7 +1933,6 @@
deleted.add("%s/" % (path,))
deleted_collections.add(path)
- for path, name, wasdeleted in results:
if path not in deleted_collections:
# Always report collection as changed
changed.add("%s/" % (path,))
@@ -1942,47 +1940,16 @@
# Resource changed - for depth "infinity" report resource as changed
if depth != "1":
changed.add("%s/%s" % (path, name,))
- else:
- # Collection was changed
- changed_collections.add(path)
# Now deal with shared collections
# TODO: think about whether this can be done in one query rather than looping over each share
- rev = self._revisionsSchema
shares = yield self.children()
for share in shares:
if not share.owned():
- sharerevision = 0 if revision < share._bindRevision else revision
- results = [
- (
- share.name(),
- name if name else "",
- wasdeleted
- )
- for name, wasdeleted in
- (yield Select([rev.RESOURCE_NAME, rev.DELETED],
- From=rev,
- Where=(rev.REVISION > sharerevision).And(
- rev.RESOURCE_ID == share._resourceID)).on(self._txn))
- if name
- ]
+ sharedChanged, sharedDeleted = yield share.sharedChildResourceNamesSinceRevision(revision, depth)
+ changed |= sharedChanged
+ deleted |= sharedDeleted
- for path, name, wasdeleted in results:
- if wasdeleted:
- if sharerevision:
- if depth == "1":
- changed.add("%s/" % (path,))
- else:
- deleted.add("%s/%s" % (path, name,))
-
- for path, name, wasdeleted in results:
- # Always report collection as changed
- changed.add("%s/" % (path,))
- if name:
- # Resource changed - for depth "infinity" report resource as changed
- if depth != "1":
- changed.add("%s/%s" % (path, name,))
-
changed = sorted(changed)
deleted = sorted(deleted)
returnValue((changed, deleted))
@@ -2940,9 +2907,10 @@
# Look up the shared child - might be accepted or not. If accepted use the resource name
# to look it up, else use the invitation uid (bind name)
shareeHome = yield self._txn.homeWithUID(self._home._homeType, invitation.shareeUID())
- shareeView = yield shareeHome.childWithName(invitation.resourceName())
+ shareeView = yield shareeHome.objectWithShareUID(invitation.uid())
if shareeView is None:
shareeView = yield shareeHome.invitedObjectWithShareUID(invitation.uid())
+ assert shareeView
result = yield self.updateShare(shareeView, mode, status, message)
returnValue(result)
@@ -2978,12 +2946,15 @@
#remove None parameters, and substitute None for empty string
bind = self._bindSchema
- columnMap = dict([(k, v if v != "" else None)
- for k, v in {bind.BIND_MODE:mode,
- bind.BIND_STATUS:status,
- bind.MESSAGE:message}.iteritems() if v is not None])
+ columnMap = {}
+ if mode != None and mode != shareeView._bindMode:
+ columnMap[bind.BIND_MODE] = mode
+ if status != None and status != shareeView._bindStatus:
+ columnMap[bind.BIND_STATUS] = status
+ if message != None and message != shareeView._bindMessage:
+ columnMap[bind.MESSAGE] = message
- if len(columnMap):
+ if columnMap:
sharedname = yield self._updateBindColumnsQuery(columnMap).on(
self._txn,
@@ -2991,10 +2962,10 @@
)
#update affected attributes
- if mode is not None:
+ if bind.BIND_MODE in columnMap:
shareeView._bindMode = columnMap[bind.BIND_MODE]
- if status is not None:
+ if bind.BIND_STATUS in columnMap:
shareeView._bindStatus = columnMap[bind.BIND_STATUS]
if shareeView._bindStatus == _BIND_STATUS_ACCEPTED:
yield shareeView._initSyncToken()
@@ -3006,7 +2977,7 @@
shareeView._home._children.pop(shareeView._name, None)
shareeView._home._children.pop(shareeView._resourceID, None)
- if message is not None:
+ if bind.MESSAGE in columnMap:
shareeView._bindMessage = columnMap[bind.MESSAGE]
queryCacher = self._txn._queryCacher
@@ -3130,6 +3101,7 @@
@inlineCallbacks
def _initBindRevision(self):
+ yield self.syncToken()
self._bindRevision = self._syncTokenRevision
bind = self._bindSchema
@@ -4146,6 +4118,65 @@
@inlineCallbacks
+ def sharedChildResourceNamesSinceRevision(self, revision, depth):
+ """
+ Determine the list of child resources that have changed since the specified sync revision.
+ We do the same SQL query for both depth "1" and "infinity", but filter the results for
+ "1" to only account for a collection change.
+
+ We need to handle shared collection a little differently from owned ones. When a shared collection
+ is bound into a home we record a revision for it using the sharee home id and sharee collection name.
+ That revision is the "starting point" for changes: so if sync occurs with a revision earlier than
+ that, we return the list of all resources in the shared collection since they are all "new" as far
+ as the client is concerned since the shared collection has just appeared. For a later revision, we
+ just report the changes since that one. When a shared collection is removed from a home, we again
+ record a revision for the sharee home and sharee collection name with the "deleted" flag set. That way
+ the shared collection can be reported as removed.
+
+ @param revision: the sync revision to compare to
+ @type revision: C{str}
+ @param depth: depth for determine what changed
+ @type depth: C{str}
+ """
+ assert not self.owned()
+
+ changed = set()
+ deleted = set()
+ rev = self._revisionsSchema
+ sharerevision = 0 if revision < self._bindRevision else revision
+ results = [
+ (
+ self.name(),
+ name if name else "",
+ wasdeleted
+ )
+ for name, wasdeleted in
+ (yield Select([rev.RESOURCE_NAME, rev.DELETED],
+ From=rev,
+ Where=(rev.REVISION > sharerevision).And(
+ rev.RESOURCE_ID == self._resourceID)).on(self._txn))
+ if name
+ ]
+
+ for path, name, wasdeleted in results:
+ if wasdeleted:
+ if sharerevision:
+ if depth == "1":
+ changed.add("%s/" % (path,))
+ else:
+ deleted.add("%s/%s" % (path, name,))
+
+ # Always report collection as changed
+ changed.add("%s/" % (path,))
+
+ # Resource changed - for depth "infinity" report resource as changed
+ if depth != "1":
+ changed.add("%s/%s" % (path, name,))
+
+ returnValue((changed, deleted))
+
+
+ @inlineCallbacks
def _loadPropertyStore(self, props=None):
if props is None:
props = yield PropertyStore.load(
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130911/fcf42b9f/attachment-0001.html>
More information about the calendarserver-changes
mailing list