[CalendarServer-changes] [12051] CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav
source_changes at macosforge.org
source_changes at macosforge.org
Wed Mar 12 11:16:52 PDT 2014
Revision: 12051
http://trac.calendarserver.org//changeset/12051
Author: cdaboo at apple.com
Date: 2013-12-11 14:16:01 -0800 (Wed, 11 Dec 2013)
Log Message:
-----------
Check point. Implemented "external" classes that can proxy various apis through the conduit to get results
from another pod.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_util.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/util.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/test_sql.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/util.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/upgrade/test/test_migrate.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/icommondatastore.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -75,7 +75,7 @@
InvalidAttachmentOperation, DuplicatePrivateCommentsError
from txdav.caldav.icalendarstore import QuotaExceeded
from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
- CommonObjectResource, ECALENDARTYPE, CommonHomeExternal
+ CommonObjectResource, ECALENDARTYPE
from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator, \
PostgresLegacyInboxIndexEmulator
from txdav.common.datastore.sql_tables import _ATTACHMENTS_MODE_NONE, \
@@ -431,13 +431,6 @@
"VPOLL": "_default_polls",
}
- def __init__(self, transaction, ownerUID):
-
- self._externalClass = CalendarHomeExternal
- self._childClass = Calendar
- super(CalendarHome, self).__init__(transaction, ownerUID)
-
-
@classmethod
def metadataColumns(cls):
"""
@@ -941,127 +934,6 @@
-class CalendarHomeExternal(CommonHomeExternal, CalendarHome):
-
- def __init__(self, transaction, ownerUID, resourceID):
-
- CalendarHome.__init__(self, transaction, ownerUID)
- CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
-
-
- def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, mode):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def getCalendarResourcesForUID(self, uid):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def calendarObjectWithDropboxID(self, dropboxID):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def getAllDropboxIDs(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def getAllAttachmentNames(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def getAllManagedIDs(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def createdHome(self):
- """
- No children - make this a no-op.
- """
- return succeed(None)
-
-
- def splitCalendars(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def ensureDefaultCalendarsExist(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def setDefaultCalendar(self, calendar, componentType):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def defaultCalendar(self, componentType, create=True):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def isDefaultCalendar(self, calendar):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def getDefaultAlarm(self, vevent, timed):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def setDefaultAlarm(self, alarm, vevent, timed):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def getAvailability(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def setAvailability(self, availability):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
-
class Calendar(CommonHomeChild):
"""
SQL-based implementation of L{ICalendar}.
@@ -1662,24 +1534,7 @@
self._cachedComponent = None
self._cachedCommponentPerUser = {}
- _allColumns = [
- _objectSchema.RESOURCE_ID,
- _objectSchema.RESOURCE_NAME,
- _objectSchema.UID,
- _objectSchema.MD5,
- Len(_objectSchema.TEXT),
- _objectSchema.ATTACHMENTS_MODE,
- _objectSchema.DROPBOX_ID,
- _objectSchema.ACCESS,
- _objectSchema.SCHEDULE_OBJECT,
- _objectSchema.SCHEDULE_TAG,
- _objectSchema.SCHEDULE_ETAGS,
- _objectSchema.PRIVATE_COMMENTS,
- _objectSchema.CREATED,
- _objectSchema.MODIFIED
- ]
-
@classmethod
@inlineCallbacks
def _createInternal(cls, parent, name, component, internal_state, options=None, split_details=None):
@@ -1691,7 +1546,8 @@
if name.startswith("."):
raise ObjectResourceNameNotAllowedError(name)
- objectResource = cls(parent, name, None, None, options=options)
+ c = cls._externalClass if parent.external() else cls
+ objectResource = c(parent, name, None, None, options=options)
yield objectResource._setComponentInternal(component, inserting=True, internal_state=internal_state, split_details=split_details)
yield objectResource._loadPropertyStore(created=True)
@@ -1701,27 +1557,51 @@
returnValue(objectResource)
- def _initFromRow(self, row):
+ @classmethod
+ def _allColumns(cls): #@NoSelf
"""
- Given a select result using the columns from L{_allColumns}, initialize
- the calendar object resource state.
+ Full set of columns in the object table that need to be loaded to
+ initialize the object resource state.
"""
- (self._resourceID,
- self._name,
- self._uid,
- self._md5,
- self._size,
- self._attachment,
- self._dropboxID,
- self._access,
- self._schedule_object,
- self._schedule_tag,
- self._schedule_etags,
- self._private_comments,
- self._created,
- self._modified,) = tuple(row)
+ obj = cls._objectSchema
+ return [
+ obj.RESOURCE_ID,
+ obj.RESOURCE_NAME,
+ obj.UID,
+ obj.MD5,
+ Len(obj.TEXT),
+ obj.ATTACHMENTS_MODE,
+ obj.DROPBOX_ID,
+ obj.ACCESS,
+ obj.SCHEDULE_OBJECT,
+ obj.SCHEDULE_TAG,
+ obj.SCHEDULE_ETAGS,
+ obj.PRIVATE_COMMENTS,
+ obj.CREATED,
+ obj.MODIFIED
+ ]
+ @classmethod
+ def _rowAttributes(cls): #@NoSelf
+ return (
+ "_resourceID",
+ "_name",
+ "_uid",
+ "_md5",
+ "_size",
+ "_attachment",
+ "_dropboxID",
+ "_access",
+ "_schedule_object",
+ "_schedule_tag",
+ "_schedule_etags",
+ "_private_comments",
+ "_created",
+ "_modified",
+ )
+
+
@property
def _calendar(self):
return self._parentCollection
@@ -4499,4 +4379,10 @@
returnValue(location)
+# Hook-up class relationships at the end after they have all been defined
+from txdav.caldav.datastore.sql_external import CalendarHomeExternal, CalendarExternal, CalendarObjectExternal
+CalendarHome._externalClass = CalendarHomeExternal
+CalendarHome._childClass = Calendar
+Calendar._externalClass = CalendarExternal
Calendar._objectResourceClass = CalendarObject
+CalendarObject._externalClass = CalendarObjectExternal
Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -0,0 +1,167 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+"""
+SQL backend for CalDAV storage when resources are external.
+"""
+
+from twisted.internet.defer import succeed
+
+from twext.python.log import Logger
+
+from txdav.caldav.datastore.sql import CalendarHome, Calendar, CalendarObject
+from txdav.common.datastore.sql_external import CommonHomeExternal, CommonHomeChildExternal, \
+ CommonObjectResourceExternal
+
+log = Logger()
+
+class CalendarHomeExternal(CommonHomeExternal, CalendarHome):
+ """
+ Wrapper for a CalendarHome that is external and only supports a limited set of operations.
+ """
+
+ def __init__(self, transaction, ownerUID, resourceID):
+
+ CalendarHome.__init__(self, transaction, ownerUID)
+ CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
+
+
+ def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, mode):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getCalendarResourcesForUID(self, uid):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def calendarObjectWithDropboxID(self, dropboxID):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAllDropboxIDs(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAllAttachmentNames(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAllManagedIDs(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def createdHome(self):
+ """
+ No children - make this a no-op.
+ """
+ return succeed(None)
+
+
+ def splitCalendars(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def ensureDefaultCalendarsExist(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def setDefaultCalendar(self, calendar, componentType):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def defaultCalendar(self, componentType, create=True):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def isDefaultCalendar(self, calendar):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getDefaultAlarm(self, vevent, timed):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def setDefaultAlarm(self, alarm, vevent, timed):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAvailability(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def setAvailability(self, availability):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class CalendarExternal(CommonHomeChildExternal, Calendar):
+ """
+ SQL-based implementation of L{ICalendar}.
+ """
+ pass
+
+
+
+class CalendarObjectExternal(CommonObjectResourceExternal, CalendarObject):
+ """
+ SQL-based implementation of L{ICalendar}.
+ """
+ pass
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -1703,7 +1703,7 @@
L{ICalendarStore.withEachCalendarHomeDo} executes its C{action}
argument repeatedly with all homes that have been created.
"""
- additionalUIDs = set('alpha-uid home2 home3 beta-uid'.split())
+ additionalUIDs = set('user01 home2 home3 uid1'.split())
txn = self.transactionUnderTest()
for name in additionalUIDs:
yield txn.calendarHomeWithUID(name, create=True)
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -1542,8 +1542,8 @@
# Tests on inbox - resources with properties
txn = self.transactionUnderTest()
- yield txn.homeWithUID(ECALENDARTYPE, "byNameTest", create=True)
- inbox = yield self.calendarUnderTest(txn=txn, name="inbox", home="byNameTest")
+ yield txn.homeWithUID(ECALENDARTYPE, "user01", create=True)
+ inbox = yield self.calendarUnderTest(txn=txn, name="inbox", home="user01")
caldata = """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
@@ -1574,7 +1574,7 @@
yield _createInboxItem("4.ics", "p4")
yield self.commit()
- inbox = yield self.calendarUnderTest(name="inbox", home="byNameTest")
+ inbox = yield self.calendarUnderTest(name="inbox", home="user01")
yield _tests(inbox)
resources = yield inbox.objectResourcesWithNames(("1.ics",))
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_util.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_util.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -323,7 +323,12 @@
@inlineCallbacks
def setUp(self):
yield super(HomeMigrationTests, self).setUp()
- self.theStore = yield buildCalendarStore(self, self.notifierFactory, homes=("conflict1", "conflict2",))
+ self.theStore = yield buildCalendarStore(self, self.notifierFactory, homes=(
+ "conflict1",
+ "conflict2",
+ "empty_home",
+ "non_empty_home",
+ ))
def storeUnderTest(self):
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/util.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/util.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -62,16 +62,12 @@
calendarUserAddresses,
cutype="INDIVIDUAL",
thisServer=True,
+ server=None,
):
- super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames, fullName)
- self.uid = uid
- self.shortNames = shortNames
- self.fullName = fullName
- self.displayName = self.fullName if self.fullName else self.shortNames[0]
+ super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames, fullName, thisServer, server)
self.calendarUserAddresses = calendarUserAddresses
self.cutype = cutype
- self._thisServer = thisServer
def canonicalCalendarUserAddress(self):
@@ -90,10 +86,6 @@
return cua
- def thisServer(self):
- return self._thisServer
-
-
def calendarsEnabled(self):
return True
@@ -148,12 +140,19 @@
homes.update((
"home1",
"home2",
- "Home_attachments",
+ "home3",
+ "home_attachments",
"home_bad",
"home_defaults",
"home_no_splits",
+ "home_provision1",
+ "home_provision2",
"home_splits",
"home_splits_shared",
+ "uid1",
+ "uid2",
+ "new-home",
+ "xyzzy",
))
for uid in homes:
directory.addRecord(buildDirectoryRecord(uid))
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -14,7 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# #
-from txdav.xml import element
"""
SQL backend for CardDAV storage.
@@ -62,6 +61,7 @@
InvalidUIDError, UIDExistsError, ObjectResourceTooBigError, \
InvalidObjectResourceError, InvalidComponentForStoreError, \
AllRetriesFailed, ObjectResourceNameAlreadyExistsError
+from txdav.xml import element
from zope.interface.declarations import implements
@@ -86,7 +86,6 @@
def __init__(self, transaction, ownerUID):
- self._childClass = AddressBook
super(AddressBookHome, self).__init__(transaction, ownerUID)
self._addressbookPropertyStoreID = None
self._addressbook = None
@@ -441,9 +440,81 @@
_objectSchema = schema.ADDRESSBOOK_OBJECT
- def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None):
+ @classmethod
+ @inlineCallbacks
+ def _getDBDataIndirect(cls, home, name, resourceID, externalID):
+
+ # Get the bind row data
+ row = None
+
+ # TODO: query cacher
+
+ rows = None
+ ownerHome = None
+
+ # TODO: add queryCacher support
+
+ if rows is None:
+ # No cached copy
+ if name:
+ ownerHome = yield home._txn.addressbookHomeWithUID(name)
+ if ownerHome is None:
+ returnValue(None)
+ resourceID = ownerHome.addressbook()._resourceID
+ rows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
+ home._txn, homeID=home._resourceID, addressbookID=resourceID
+ )
+
+ if not rows:
+ returnValue(None)
+
+ groupID = None
+ overallBindStatus = _BIND_STATUS_INVITED
+ minBindRevision = None
+ for row in rows:
+ bindMode, homeID, resourceGroupID, externalID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
+ if groupID is None:
+ groupID = resourceGroupID
+ minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
+ if bindStatus == _BIND_STATUS_ACCEPTED:
+ overallBindStatus = _BIND_STATUS_ACCEPTED
+
+ if ownerHome is None:
+ ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, groupID)
+ ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
+
+ bindData = row[:cls.bindColumnCount]
+ additionalBindData = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+
+ # Adjust for aggregate values
+ bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)] = resourceID
+ bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] = ownerHome.uid()
+ bindData[cls.bindColumns().index(cls._bindSchema.BIND_MODE)] = _BIND_MODE_INDIRECT
+ bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)] = overallBindStatus
+ bindData[cls.bindColumns().index(cls._bindSchema.BIND_REVISION)] = minBindRevision
+ bindData[cls.bindColumns().index(cls._bindSchema.MESSAGE)] = ""
+
+ # Get the matching metadata data
+ metadataData = None
+ queryCacher = home._txn._queryCacher
+ if queryCacher:
+ # Retrieve from cache
+ cacheKey = queryCacher.keyForHomeChildMetaData(resourceID)
+ metadataData = yield queryCacher.get(cacheKey)
+
+ if metadataData is None:
+ # No cached copy
+ metadataData = (yield cls._metadataByIDQuery.on(home._txn, resourceID=resourceID))[0]
+ if queryCacher:
+ # Cache the results
+ yield queryCacher.setAfterCommit(home._txn, cacheKey, metadataData)
+
+ returnValue((bindData, additionalBindData, metadataData, ownerHome,))
+
+
+ def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=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, externalID=externalID)
self._index = PostgresLegacyABIndexEmulator(self)
@@ -505,7 +576,7 @@
@classmethod
- def create(cls, home, name):
+ def create(cls, home, name, externalID=None):
if name == home.addressbook().name():
# raise HomeChildNameAlreadyExistsError
pass
@@ -767,32 +838,17 @@
# Create the actual objects merging in properties
for ownerHome, dataRow in ownerHomeToDataRowMap.iteritems():
- bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
- additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
- metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+ bindData = dataRow[:cls.bindColumnCount]
+ additionalBindData = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+ metadataData = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+ propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
- child = cls(
- home=home,
- name=ownerHome.uid(),
- resourceID=ownerHome._resourceID,
- mode=bindMode,
- status=bindStatus,
- revision=bindRevision,
- message=bindMessage,
- ownerHome=ownerHome,
- )
+ # Some adjustments for addressbook share model
+ bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)] = ownerHome._resourceID
+ bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] = ownerHome.uid()
- for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
- setattr(child, attr, value)
- for attr, value in zip(cls.metadataAttributes(), metadata):
- setattr(child, attr, value)
+ child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore, ownerHome)
child._syncTokenRevision = revisions[child._resourceID]
- propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
- # We have to re-adjust the property store object to account for possible shared
- # collections as previously we loaded them all as if they were owned
- if propstore:
- propstore._setDefaultUserUID(ownerHome.uid())
- yield child._loadPropertyStore(propstore)
results.append(child)
returnValue(results)
@@ -855,7 +911,7 @@
@classmethod
@inlineCallbacks
- def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, accepted=True):
+ def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
# replaces objectWithName()
"""
Synthesize and indirect child for matching name or id based on whether shared groups exist.
@@ -867,56 +923,17 @@
@return: an L{CommonHomeChild} or C{None} if no such child
exists.
"""
- rows = None
- ownerHome = None
- # TODO: add queryCacher support
-
- if rows is None:
- # No cached copy
- if name:
- ownerHome = yield home._txn.addressbookHomeWithUID(name)
- if ownerHome is None:
- returnValue(None)
- resourceID = ownerHome.addressbook()._resourceID
- rows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
- home._txn, homeID=home._resourceID, addressbookID=resourceID
- )
-
- if not rows:
+ dbData = yield cls._getDBDataIndirect(home, name, resourceID, externalID)
+ if dbData is None:
returnValue(None)
+ bindData, additionalBindData, metadataData, ownerHome = dbData
- groupID = None
- overallBindStatus = _BIND_STATUS_INVITED
- minBindRevision = None
- for row in rows:
- bindMode, homeID, resourceGroupID, externalID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
- if groupID is None:
- groupID = resourceGroupID
- minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
- if bindStatus == _BIND_STATUS_ACCEPTED:
- overallBindStatus = _BIND_STATUS_ACCEPTED
-
- if accepted is not None and (overallBindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
+ bindStatus = bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)]
+ if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
returnValue(None)
- additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
- if ownerHome is None:
- ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, groupID)
- ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
-
- child = cls(
- home=home,
- name=ownerHome.uid(),
- resourceID=resourceID,
- mode=_BIND_MODE_INDIRECT,
- status=overallBindStatus,
- revision=minBindRevision,
- message="",
- ownerHome=ownerHome,
- ownerName=ownerHome.uid()
- )
- yield child.initFromStore(additionalBind)
+ child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, None, ownerHome)
returnValue(child)
@@ -1142,7 +1159,7 @@
@inlineCallbacks
- def shareWith(self, shareeHome, mode, status=None, summary=None):
+ def shareWith(self, shareeHome, mode, status=None, summary=None, shareName=None):
"""
Share this (owned) L{AddressBookObject} with another home.
@@ -1170,11 +1187,12 @@
@inlineCallbacks
def doInsert(subt):
- newName = self.newShareName()
+ newName = shareName if shareName is not None else self.newShareName()
yield self._bindInsertQuery.on(
subt,
homeID=shareeHome._resourceID,
resourceID=self._resourceID,
+ externalID=None,
name=newName,
mode=mode,
bindStatus=status,
@@ -1210,14 +1228,14 @@
@inlineCallbacks
- def createShare(self, shareeUID, mode, summary=None):
+ def createShare(self, shareeUID, mode, summary=None, shareName=None):
"""
Create a new shared resource. If the mode is direct, the share is created in accepted state,
otherwise the share is created in invited state.
"""
if self._kind == _ABO_KIND_GROUP:
- shareeView = yield super(AddressBookObjectSharingMixIn, self).createShare(shareeUID, mode, summary)
+ shareeView = yield super(AddressBookObjectSharingMixIn, self).createShare(shareeUID, mode, summary, shareName)
returnValue(shareeView)
else:
returnValue(None)
@@ -1430,6 +1448,130 @@
# _homeChildMetaDataSchema = schema.ADDRESSBOOK_OBJECT
+ @classmethod
+ @inlineCallbacks
+ def makeClass(cls, parent, objectData, groupBindData=None, propstore=None):
+ """
+ Given the various database rows, build the actual class.
+
+ @param parent: the parent collection object
+ @type parent: L{AddressBook}
+ @param objectData: the standard set of object columns
+ @type objectData: C{list}
+ @param groupBindData: additional group bind data
+ @type groupBindData: C{list}
+ @param propstore: a property store to use, or C{None} to load it automatically
+ @type propstore: L{PropertyStore}
+
+ @return: the constructed child class
+ @rtype: L{CommonHomeChild}
+ """
+
+ c = cls._externalClass if parent.external() else cls
+ child = c(
+ parent,
+ objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
+ objectData[cls._allColumns().index(cls._objectSchema.UID)],
+ )
+
+ for attr, value in zip(child._rowAttributes(), objectData):
+ setattr(child, attr, value)
+
+ yield child._loadPropertyStore(propstore)
+
+ if groupBindData:
+ bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindData[:AddressBookObject.bindColumnCount] #@UnusedVariable
+ child._bindMode = bindMode
+ child._bindStatus = bindStatus
+ child._bindMessage = bindMessage
+ child._bindName = bindName
+ else:
+ invites = yield child.sharingInvites()
+ if len(invites):
+ child._bindMessage = "shared"
+
+ returnValue(child)
+
+
+ @classmethod
+ @inlineCallbacks
+ def _getDBData(cls, parent, name, uid, resourceID):
+ """
+ Given a set of identifying information, load the data rows for the object. Only one of
+ L{name}, L{uid} or L{resourceID} is specified - others are C{None}.
+
+ @param parent: the parent collection object
+ @type parent: L{AddressBook}
+ @param name: the resource name
+ @type name: C{str}
+ @param uid: the UID of the data
+ @type uid: C{str}
+ @param resourceID: the resource ID
+ @type resourceID: C{int}
+ """
+
+ row = None
+ groupBindRow = None
+
+ if parent.owned() or parent.fullyShared(): # owned or fully shared
+ row = yield super(AddressBookObject, cls)._getDBData(parent, name, uid, resourceID)
+
+ # Might be special group
+ if row is None and parent.fullyShared():
+ if name:
+ if name == parent._groupForSharedAddressBookName():
+ row = parent._groupForSharedAddressBookRow()
+ elif uid:
+ if uid == (yield parent._groupForSharedAddressBookUID()):
+ row = parent._groupForSharedAddressBookRow()
+ elif resourceID:
+ if resourceID == parent.id():
+ rows = parent._groupForSharedAddressBookRow()
+
+ else:
+ acceptedGroupIDs = yield parent.acceptedGroupIDs()
+ allowedObjectIDs = yield parent.expandGroupIDs(parent._txn, acceptedGroupIDs)
+ rows = None
+ if name:
+ if allowedObjectIDs:
+ rows = (yield cls._allColumnsWithResourceIDsAndName(allowedObjectIDs).on(
+ parent._txn,
+ name=name,
+ resourceIDs=allowedObjectIDs,
+ ))
+ elif uid:
+ if allowedObjectIDs:
+ rows = (yield cls._allColumnsWithResourceIDsAndUID(allowedObjectIDs).on(
+ parent._txn,
+ uid=uid,
+ resourceIDs=allowedObjectIDs,
+ ))
+ elif resourceID:
+ if resourceID not in allowedObjectIDs:
+ # allow invited groups
+ allowedObjectIDs = yield parent.unacceptedGroupIDs()
+ if resourceID in allowedObjectIDs:
+ rows = (yield cls._allColumnsWithResourceID.on(
+ parent._txn,
+ resourceID=resourceID,
+ ))
+ if rows:
+ row = rows[0]
+
+ if row is not None:
+ if row[cls._allColumns().index(cls._objectSchema.KIND)] == _ABO_KIND_GROUP:
+
+ resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+ groupBindRows = yield AddressBookObject._bindForResourceIDAndHomeID.on(
+ parent._txn, resourceID=resourceID, homeID=parent._home._resourceID
+ )
+
+ if groupBindRows:
+ groupBindRow = groupBindRows[0]
+
+ returnValue((row, groupBindRow,))
+
+
def __init__(self, addressbook, name, uid, resourceID=None, options=None):
self._kind = None
@@ -1443,6 +1585,7 @@
self._bindMessage = None
self._bindName = None
super(AddressBookObject, self).__init__(addressbook, name, uid, resourceID, options)
+ self._externalID = None
self._options = {} if options is None else options
@@ -1467,6 +1610,15 @@
return self._resourceID == self.addressbook()._resourceID
+ def external(self):
+ """
+ Is this an external object.
+
+ @return: a string.
+ """
+ return self.addressbook().external()
+
+
@classmethod
def _deleteMembersWithMemberIDAndGroupIDsQuery(cls, memberID, groupIDs):
aboMembers = schema.ABO_MEMBERS
@@ -1553,7 +1705,7 @@
"""
obj = cls._objectSchema
return Select(
- cls._allColumns, From=obj,
+ cls._allColumns(), From=obj,
Where=(column == Parameter(paramName)).And(
obj.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs)))),
)
@@ -1573,7 +1725,7 @@
def _allColumnsWithResourceID(cls): #@NoSelf
obj = cls._objectSchema
return Select(
- cls._allColumns, From=obj,
+ cls._allColumns(), From=obj,
Where=obj.RESOURCE_ID == Parameter("resourceID"),)
@@ -1609,92 +1761,20 @@
returnValue(None)
+ @classmethod
@inlineCallbacks
- def initFromStore(self):
- """
- Initialise this object from the store. We read in and cache all the
- extra metadata from the DB to avoid having to do DB queries for those
- individually later. Either the name or uid is present, so we have to
- tweak the query accordingly.
+ def _objectWithNameOrID(cls, parent, name, uid, resourceID):
- @return: L{self} if object exists in the DB, else C{None}
- """
- abo = None
- if self.owned() or self.addressbook().fullyShared(): # owned or fully shared
- abo = yield super(AddressBookObject, self).initFromStore()
+ row, groupBindRow = yield cls._getDBData(parent, name, uid, resourceID)
- # Might be special group
- if abo is None and self.addressbook().fullyShared():
- rows = None
- if self._name:
- if self._name == self.addressbook()._groupForSharedAddressBookName():
- rows = [self.addressbook()._groupForSharedAddressBookRow()]
- elif self._uid:
- if self._uid == (yield self.addressbook()._groupForSharedAddressBookUID()):
- rows = [self.addressbook()._groupForSharedAddressBookRow()]
- elif self._resourceID:
- if self.isGroupForSharedAddressBook():
- rows = [self.addressbook()._groupForSharedAddressBookRow()]
-
- if rows:
- self._initFromRow(tuple(rows[0]))
- yield self._loadPropertyStore()
- abo = self
-
+ if row:
+ child = yield cls.makeClass(parent, row, groupBindRow)
+ returnValue(child)
else:
- acceptedGroupIDs = yield self.addressbook().acceptedGroupIDs()
- allowedObjectIDs = yield self.addressbook().expandGroupIDs(self._txn, acceptedGroupIDs)
- rows = None
- if self._name:
- if allowedObjectIDs:
- rows = (yield self._allColumnsWithResourceIDsAndName(allowedObjectIDs).on(
- self._txn, name=self._name,
- resourceIDs=allowedObjectIDs,
- ))
- elif self._uid:
- if allowedObjectIDs:
- rows = (yield self._allColumnsWithResourceIDsAndUID(allowedObjectIDs).on(
- self._txn, uid=self._uid,
- resourceIDs=allowedObjectIDs,
- ))
- elif self._resourceID:
- if self._resourceID not in allowedObjectIDs:
- # allow invited groups
- allowedObjectIDs = yield self.addressbook().unacceptedGroupIDs()
- if self._resourceID in allowedObjectIDs:
- rows = (yield self._allColumnsWithResourceID.on(
- self._txn, resourceID=self._resourceID,
- ))
- if rows:
- self._initFromRow(tuple(rows[0]))
- yield self._loadPropertyStore()
- abo = self
-
- if abo is not None:
- if self._kind == _ABO_KIND_GROUP:
-
- groupBindRows = yield AddressBookObject._bindForResourceIDAndHomeID.on(
- self._txn, resourceID=self._resourceID, homeID=self._home._resourceID
- )
-
- if groupBindRows:
- groupBindRow = groupBindRows[0]
- bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
- self._bindMode = bindMode
- self._bindStatus = bindStatus
- self._bindMessage = bindMessage
- self._bindName = bindName
- else:
- invites = yield self.sharingInvites()
- if len(invites):
- self._bindMessage = "shared"
-
- returnValue(self)
- else:
returnValue(None)
- @classproperty
+ @classmethod
def _allColumns(cls): #@NoSelf
"""
Full set of columns in the object table that need to be loaded to
@@ -1710,24 +1790,23 @@
obj.MD5,
Len(obj.TEXT),
obj.CREATED,
- obj.MODIFIED,
+ obj.MODIFIED
]
- def _initFromRow(self, row):
- """
- Given a select result using the columns from L{_allColumns}, initialize
- the object resource state.
- """
- (self._ownerAddressBookResourceID,
- self._resourceID,
- self._name,
- self._uid,
- self._kind,
- self._md5,
- self._size,
- self._created,
- self._modified,) = tuple(row)
+ @classmethod
+ def _rowAttributes(cls): #@NoSelf
+ return (
+ "_ownerAddressBookResourceID",
+ "_resourceID",
+ "_name",
+ "_uid",
+ "_kind",
+ "_md5",
+ "_size",
+ "_created",
+ "_modified",
+ )
@classmethod
@@ -1750,7 +1829,7 @@
else:
acceptedGroupIDs = yield addressbook.acceptedGroupIDs()
allowedObjectIDs = yield addressbook.expandGroupIDs(addressbook._txn, acceptedGroupIDs)
- rows = yield cls._columnsWithResourceIDsQuery(cls._allColumns, allowedObjectIDs).on(
+ rows = yield cls._columnsWithResourceIDsQuery(cls._allColumns(), allowedObjectIDs).on(
addressbook._txn, resourceIDs=allowedObjectIDs
)
returnValue(rows)
@@ -1759,7 +1838,7 @@
@classmethod
def _allColumnsWithResourceIDsAndNamesQuery(cls, resourceIDs, names):
obj = cls._objectSchema
- return Select(cls._allColumns, From=obj,
+ return Select(cls._allColumns(), From=obj,
Where=(obj.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs))).And(
obj.RESOURCE_NAME.In(Parameter("names", len(names))))),)
@@ -2336,4 +2415,10 @@
)
+# Hook-up class relationships at the end after they have all been defined
+from txdav.carddav.datastore.sql_external import AddressBookHomeExternal, AddressBookExternal, AddressBookObjectExternal
+AddressBookHome._externalClass = AddressBookHomeExternal
+AddressBookHome._childClass = AddressBook
+AddressBook._externalClass = AddressBookExternal
AddressBook._objectResourceClass = AddressBookObject
+AddressBookObject._externalClass = AddressBookObjectExternal
Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -0,0 +1,81 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+"""
+SQL backend for CardDAV storage when resources are external.
+"""
+
+from twisted.internet.defer import succeed
+
+from twext.python.log import Logger
+
+from txdav.carddav.datastore.sql import AddressBookHome, AddressBook, \
+ AddressBookObject
+from txdav.common.datastore.sql_external import CommonHomeExternal, CommonHomeChildExternal, \
+ CommonObjectResourceExternal
+
+log = Logger()
+
+class AddressBookHomeExternal(CommonHomeExternal, AddressBookHome):
+
+ def __init__(self, transaction, ownerUID, resourceID):
+
+ AddressBookHome.__init__(self, transaction, ownerUID)
+ CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
+
+
+ def hasAddressBookResourceUIDSomewhereElse(self, uid, ok_object, mode):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def getAddressBookResourcesForUID(self, uid):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def createdHome(self):
+ """
+ No children - make this a no-op.
+ """
+ return succeed(None)
+
+
+ def addressbook(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class AddressBookExternal(CommonHomeChildExternal, AddressBook):
+ """
+ SQL-based implementation of L{IAddressBook}.
+ """
+ pass
+
+
+
+class AddressBookObjectExternal(CommonObjectResourceExternal, AddressBookObject):
+ """
+ SQL-based implementation of L{ICalendar}.
+ """
+ pass
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -41,7 +41,7 @@
from txdav.common.icommondatastore import NoSuchObjectResourceError
from txdav.common.datastore.sql import EADDRESSBOOKTYPE, CommonObjectResource
from txdav.common.datastore.sql_tables import _ABO_KIND_PERSON, _ABO_KIND_GROUP, schema
-from txdav.common.datastore.test.util import buildStore
+from txdav.common.datastore.test.util import buildStore, cleanStore
from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
@@ -55,7 +55,22 @@
@inlineCallbacks
def setUp(self):
yield super(AddressBookSQLStorageTests, self).setUp()
- self._sqlStore = yield buildStore(self, self.notifierFactory)
+ self._sqlStore = yield buildStore(
+ self,
+ self.notifierFactory,
+ homes=(
+ "home1",
+ "home2",
+ "home3",
+ "home_bad",
+ "home_empty",
+ "homeNew",
+ "new-home",
+ "uid1",
+ "uid2",
+ "xyzzy",
+ )
+ )
yield self.populate()
@@ -288,7 +303,7 @@
Test that two concurrent attempts to PUT different address book object resources to the
same address book home does not cause a deadlock.
"""
- addressbookStore = yield buildStore(self, self.notifierFactory)
+ addressbookStore = self._sqlStore
# Provision the home and addressbook now
txn = addressbookStore.newTransaction()
@@ -394,7 +409,7 @@
"""
Test that kind property UID is stored correctly in database
"""
- addressbookStore = yield buildStore(self, self.notifierFactory)
+ addressbookStore = self._sqlStore
# Provision the home and addressbook, one user and one group
txn = addressbookStore.newTransaction()
@@ -440,7 +455,7 @@
"""
Test that kind property vCard is stored correctly in database
"""
- addressbookStore = yield buildStore(self, self.notifierFactory)
+ addressbookStore = self._sqlStore
# Provision the home and addressbook, one user and one group
txn = addressbookStore.newTransaction()
@@ -531,7 +546,8 @@
"""
Test that kind property vCard is stored correctly in database
"""
- addressbookStore = yield buildStore(self, self.notifierFactory)
+ addressbookStore = self._sqlStore
+ cleanStore(self, addressbookStore)
# Provision the home and addressbook, one user and one group
txn = addressbookStore.newTransaction()
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -14,6 +14,8 @@
# limitations under the License.
##
+from twext.python.log import Logger
+
from twisted.internet.defer import inlineCallbacks, returnValue
from txdav.common.datastore.podding.request import ConduitRequest
@@ -25,21 +27,17 @@
"PoddingConduitResource",
]
-class BadMessageError(Exception):
- pass
+log = Logger()
-
-class InvalidCrossPodRequestError(Exception):
+class FailedCrossPodRequestError(RuntimeError):
+ """
+ Request returned an error.
+ """
pass
-class FailedCrossPodRequestError(Exception):
- pass
-
-
-
class PoddingConduit(object):
"""
This class is the API/RPC bridge between cross-pod requests and the store.
@@ -51,7 +49,7 @@
the other keys are arguments to that call.
Each response C{dict} has a "result" key that indicates the call result, and other
- optional keys for any parameters returned by the call.
+ optional keys for any values returned by the call.
The conduit provides two methods for each action: one for the sending side and one for
the receiving side, called "send_{action}" and "recv_{action}", respectively, where
@@ -63,10 +61,14 @@
The "recv_{action}" calls take a single C{dict} argument that is the deserialized JSON
data from the incoming request. The return value is a C{dict} with the result.
+ Some simple forms of send_/recv_ methods can be auto-generated to simplify coding.
+
Right now this conduit is used for cross-pod sharing operations. In the future we will
likely use it for cross-pod migration.
"""
+ conduitRequestClass = ConduitRequest
+
def __init__(self, store):
"""
@param store: the L{CommonDataStore} in use.
@@ -91,18 +93,73 @@
if source is None:
raise DirectoryRecordNotFoundError("Cross-pod source: {}".format(source_guid))
if not source.thisServer():
- raise InvalidCrossPodRequestError("Cross-pod source not on this server: {}".format(source_guid))
+ raise FailedCrossPodRequestError("Cross-pod source not on this server: {}".format(source_guid))
destination = self.store.directoryService().recordWithUID(destination_guid)
if destination is None:
raise DirectoryRecordNotFoundError("Cross-pod destination: {}".format(destination_guid))
if destination.thisServer():
- raise InvalidCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_guid))
+ raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_guid))
return (source, destination,)
@inlineCallbacks
+ def sendRequest(self, txn, recipient, data):
+
+ request = self.conduitRequestClass(recipient.server(), data)
+ try:
+ response = (yield request.doRequest(txn))
+ except Exception as e:
+ raise FailedCrossPodRequestError("Failed cross-pod request: {}".format(e))
+ returnValue(response)
+
+
+ @inlineCallbacks
+ def processRequest(self, data):
+ """
+ Process the request.
+
+ @param data: the JSON data to process
+ @type data: C{dict}
+ """
+ # Must have a dict with an "action" key
+ try:
+ action = data["action"]
+ except (KeyError, TypeError) as e:
+ log.error("JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}", ex=e, json=data)
+ raise FailedCrossPodRequestError("JSON data must have an object as its root with an 'action' attribute: {}\n{}".format(e, data,))
+
+ if action == "ping":
+ result = {"result": "ok"}
+ returnValue(result)
+
+ method = "recv_{}".format(action)
+ if not hasattr(self, method):
+ log.error("Unsupported action: {action}", action=action)
+ raise FailedCrossPodRequestError("Unsupported action: {}".format(action))
+
+ # Need a transaction to work with
+ txn = self.store.newTransaction(repr("Conduit request"))
+
+ # Do the actual request processing
+ try:
+ result = (yield getattr(self, method)(txn, data))
+ except Exception as e:
+ yield txn.abort()
+ log.error("Failed action: {action}, {ex}", action=action, ex=e)
+ raise FailedCrossPodRequestError("Failed action: {}, {}".format(action, e))
+
+ yield txn.commit()
+
+ returnValue(result)
+
+
+ #
+ # Invite related apis
+ #
+
+ @inlineCallbacks
def send_shareinvite(self, txn, homeType, ownerUID, ownerID, ownerName, shareeUID, shareUID, bindMode, summary, supported_components):
"""
Send a sharing invite cross-pod message.
@@ -127,7 +184,7 @@
@type supported_components: C{str}
"""
- _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
+ _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
action = {
"action": "shareinvite",
@@ -143,10 +200,8 @@
if supported_components is not None:
action["supported-components"] = supported_components
- request = ConduitRequest(sharee.server(), action)
- response = (yield request.doRequest(txn))
- if response["result"] != "ok":
- raise FailedCrossPodRequestError(response["description"])
+ result = yield self.sendRequest(txn, recipient, action)
+ returnValue(result)
@inlineCallbacks
@@ -159,15 +214,12 @@
"""
if message["action"] != "shareinvite":
- raise BadMessageError("Wrong action '{}' for recv_shareinvite".format(message["action"]))
+ raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareinvite".format(message["action"]))
# Create a share
shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
if shareeHome is None or shareeHome.external():
- returnValue({
- "result": "bad",
- "description": "Invalid sharee UID specified",
- })
+ raise FailedCrossPodRequestError("Invalid sharee UID specified")
try:
yield shareeHome.processExternalInvite(
@@ -180,14 +232,10 @@
supported_components=message.get("supported-components")
)
except ExternalShareFailed as e:
- returnValue({
- "result": "bad",
- "description": str(e),
- })
+ raise FailedCrossPodRequestError(str(e))
returnValue({
"result": "ok",
- "description": "Success"
})
@@ -208,7 +256,7 @@
@type shareUID: C{str}
"""
- _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
+ _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
action = {
"action": "shareuninvite",
@@ -219,10 +267,8 @@
"share_id": shareUID,
}
- request = ConduitRequest(sharee.server(), action)
- response = (yield request.doRequest(txn))
- if response["result"] != "ok":
- raise FailedCrossPodRequestError(response["description"])
+ result = yield self.sendRequest(txn, recipient, action)
+ returnValue(result)
@inlineCallbacks
@@ -235,15 +281,12 @@
"""
if message["action"] != "shareuninvite":
- raise BadMessageError("Wrong action '{}' for recv_shareuninvite".format(message["action"]))
+ raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareuninvite".format(message["action"]))
# Create a share
shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
if shareeHome is None or shareeHome.external():
- returnValue({
- "result": "bad",
- "description": "Invalid sharee UID specified",
- })
+ FailedCrossPodRequestError("Invalid sharee UID specified")
try:
yield shareeHome.processExternalUninvite(
@@ -252,14 +295,10 @@
message["share_id"],
)
except ExternalShareFailed as e:
- returnValue({
- "result": "bad",
- "description": str(e),
- })
+ FailedCrossPodRequestError(str(e))
returnValue({
"result": "ok",
- "description": "Success"
})
@@ -272,9 +311,9 @@
@type homeType: C{int}
@param ownerUID: GUID of the sharer.
@type ownerUID: C{str}
- @param shareeUID: GUID of the sharee
+ @param shareeUID: GUID of the recipient
@type shareeUID: C{str}
- @param shareUID: Resource/invite ID for sharee
+ @param shareUID: Resource/invite ID for recipient
@type shareUID: C{str}
@param bindStatus: bind mode for the share
@type bindStatus: C{str}
@@ -282,7 +321,7 @@
@type summary: C{str}
"""
- _ignore_owner, sharee = self.validRequst(shareeUID, ownerUID)
+ _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
action = {
"action": "sharereply",
@@ -295,10 +334,8 @@
if summary is not None:
action["summary"] = summary
- request = ConduitRequest(sharee.server(), action)
- response = (yield request.doRequest(txn))
- if response["result"] != "ok":
- raise FailedCrossPodRequestError(response["description"])
+ result = yield self.sendRequest(txn, recipient, action)
+ returnValue(result)
@inlineCallbacks
@@ -311,15 +348,12 @@
"""
if message["action"] != "sharereply":
- raise BadMessageError("Wrong action '{}' for recv_sharereply".format(message["action"]))
+ raise FailedCrossPodRequestError("Wrong action '{}' for recv_sharereply".format(message["action"]))
# Create a share
ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
if ownerHome is None or ownerHome.external():
- returnValue({
- "result": "bad",
- "description": "Invalid owner UID specified",
- })
+ FailedCrossPodRequestError("Invalid owner UID specified")
try:
yield ownerHome.processExternalReply(
@@ -330,12 +364,126 @@
summary=message.get("summary")
)
except ExternalShareFailed as e:
- returnValue({
- "result": "bad",
- "description": str(e),
- })
+ FailedCrossPodRequestError(str(e))
returnValue({
"result": "ok",
- "description": "Success"
})
+
+
+ #
+ # Sharer data access related apis
+ #
+
+ def _send(self, action, shareeView):
+ """
+ Base behavior for an operation on a sharee resource.
+
+ @param shareeView: sharee resource being operated on.
+ @type shareeView: L{CommonHomeChildExternal}
+ """
+
+ homeType = shareeView.ownerHome()._homeType
+ ownerUID = shareeView.ownerHome().uid()
+ ownerID = shareeView.external_id()
+ shareeUID = shareeView.viewerHome().uid()
+
+ _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+
+ result = {
+ "action": action,
+ "type": homeType,
+ "owner": ownerUID,
+ "owner_id": ownerID,
+ "sharee": shareeUID,
+ }
+ return result, recipient
+
+
+ @inlineCallbacks
+ def _recv(self, txn, message, expected_action):
+ """
+ Base behavior for sharer data access.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ if message["action"] != expected_action:
+ raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(message["action"], expected_action))
+
+ # Create a share
+ ownerHome = yield txn.homeWithUID(message["type"], message["owner"], create=True)
+ if ownerHome is None or ownerHome.external():
+ FailedCrossPodRequestError("Invalid owner UID specified")
+
+ ownerHomeChild = yield ownerHome.childWithID(message["owner_id"])
+ if ownerHomeChild is None:
+ FailedCrossPodRequestError("Invalid owner shared resource specified")
+
+ returnValue((ownerHome, ownerHomeChild))
+
+
+ #
+ # Simple calls are ones where there is no argument and a single return value. We can simplify
+ # code generation for these by dynamically generating the appropriate class methods.
+ #
+
+ @inlineCallbacks
+ def _simple_send(self, actionName, shareeView, args=None, kwargs=None):
+ """
+ A simple send operation that returns a value.
+
+ @param actionName: name of the action.
+ @type actionName: C{str}
+ @param shareeView: sharee resource being operated on.
+ @type shareeView: L{CommonHomeChildExternal}
+ @param args: list of optional arguments.
+ @type args: C{list}
+ @param kwargs: optional keyword arguments.
+ @type kwargs: C{dict}
+ """
+
+ action, recipient = self._send(actionName, shareeView)
+ if args is not None:
+ action["arguments"] = args
+ if kwargs is not None:
+ action["keywords"] = kwargs
+ result = yield self.sendRequest(shareeView._txn, recipient, action)
+ returnValue(result["value"])
+
+
+ @inlineCallbacks
+ def _simple_recv(self, txn, actionName, message, method):
+ """
+ A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
+ and include those only if present.
+
+ @param actionName: name of the action.
+ @type actionName: C{str}
+ @param message: message arguments
+ @type message: C{dict}
+ @param method: name of the method to execute on the shared resource to get the result.
+ @type method: C{str}
+ """
+
+ _ignore_ownerHome, ownerHomeChild = yield self._recv(txn, message, actionName)
+ value = yield getattr(ownerHomeChild, method)(*message.get("arguments", ()), **message.get("keywords", {}))
+ returnValue({
+ "result": "ok",
+ "value": value,
+ })
+
+
+ @classmethod
+ def _make_simple_action(cls, action, method):
+ setattr(cls, "send_{}".format(action), lambda self, shareeView, *args, **kwargs: self._simple_send(action, shareeView, args, kwargs))
+ setattr(cls, "recv_{}".format(action), lambda self, txn, message: self._simple_recv(txn, action, message, method))
+
+
+PoddingConduit._make_simple_action("countobjects", "countObjectResources")
+PoddingConduit._make_simple_action("listobjects", "listObjectResources")
+PoddingConduit._make_simple_action("synctoken", "syncToken")
+PoddingConduit._make_simple_action("resourcenamessincerevision", "resourceNamesSinceRevision")
+PoddingConduit._make_simple_action("resourceuidforname", "resourceUIDForName")
+PoddingConduit._make_simple_action("resourcenameforuid", "resourceNameForUID")
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -73,7 +73,7 @@
except Exception as e:
# Request failed
log.error("Could not do cross-pod request : {request} {ex}", request=self, ex=e)
- raise ValueError("Failed cross-pod request: {}".format(response.code))
+ raise ValueError("Failed cross-pod request: {}".format(e))
returnValue(data)
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -32,6 +32,7 @@
from txdav.xml import element as davxml
from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
+from txdav.common.datastore.podding.conduit import FailedCrossPodRequestError
__all__ = [
"ConduitResource",
@@ -138,36 +139,14 @@
self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body)
raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body)))
- # Must have a dict with an "action" key
+ # Get the conduit to process the data
try:
- action = j["action"]
- except (KeyError, TypeError) as e:
- self.log.error("JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}", ex=e, json=j)
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "JSON data must have an object as its root with an 'action' attribute: {}\n{}".format(e, j,)))
-
- if action == "ping":
- result = {"result": "ok"}
- response = JSONResponse(responsecode.OK, result)
- returnValue(response)
-
- method = "recv_{}".format(action)
- if not hasattr(self.store.conduit, method):
- self.log.error("Unsupported action: {action}", action=action)
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Unsupported action: {}".format(action)))
-
- # Need a transaction to work with
- txn = self.store.newTransaction(repr(request))
-
- # Do the POST processing treating this as a non-local schedule
- try:
- result = (yield getattr(self.store.conduit, method)(txn, j))
+ result = yield self.store.conduit.processRequest(j)
+ except FailedCrossPodRequestError as e:
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
except Exception as e:
- yield txn.abort()
- self.log.error("Failed action: {action}, {ex}", action=action, ex=e)
- raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "Failed action: {}, {}".format(action, e)))
+ raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, str(e)))
- yield txn.commit()
-
response = JSONResponse(responsecode.OK, result)
returnValue(response)
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -16,17 +16,22 @@
from twext.python.clsprop import classproperty
import twext.web2.dav.test.util
-from twisted.internet.defer import inlineCallbacks, succeed
+from twisted.internet.defer import inlineCallbacks, succeed, returnValue
from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
from txdav.caldav.datastore.test.util import buildCalendarStore, \
TestCalendarStoreDirectoryRecord
from txdav.common.datastore.podding.resource import ConduitResource
from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
from txdav.common.datastore.podding.conduit import PoddingConduit, \
- InvalidCrossPodRequestError
+ FailedCrossPodRequestError
from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest, \
+ FakeConduitRequest
+from txdav.common.datastore.sql_tables import _BIND_STATUS_ACCEPTED
+from pycalendar.datetime import DateTime
+from twistedcaldav.ical import Component
-class Conduit (CommonCommonTests, twext.web2.dav.test.util.TestCase):
+class TestConduit (CommonCommonTests, twext.web2.dav.test.util.TestCase):
class FakeConduit(object):
@@ -40,7 +45,7 @@
@inlineCallbacks
def setUp(self):
- yield super(Conduit, self).setUp()
+ yield super(TestConduit, self).setUp()
self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
self.directory = self._sqlCalendarStore.directoryService()
@@ -113,4 +118,378 @@
self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "bogus01", "user02")
self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "user01", "bogus02")
- self.assertRaises(InvalidCrossPodRequestError, conduit.validRequst, "user01", "user02")
+ self.assertRaises(FailedCrossPodRequestError, conduit.validRequst, "user01", "user02")
+
+
+
+class TestConduitToConduit(MultiStoreConduitTest):
+
+ class FakeConduit(PoddingConduit):
+
+ @inlineCallbacks
+ def send_fake(self, txn, ownerUID, shareeUID):
+ _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
+ action = {
+ "action": "fake",
+ "echo": "bravo"
+ }
+
+ result = yield self.sendRequest(txn, sharee, action)
+ returnValue(result)
+
+
+ def recv_fake(self, txn, j):
+ return succeed({
+ "result": "ok",
+ "back2u": j["echo"],
+ "more": "bits",
+ })
+
+
+ def makeConduit(self, store):
+ """
+ Use our own variant.
+ """
+ conduit = self.FakeConduit(store)
+ conduit.conduitRequestClass = FakeConduitRequest
+ return conduit
+
+
+ @inlineCallbacks
+ def test_fake_action(self):
+ """
+ Cross-pod request works when conduit does support the action.
+ """
+
+ txn = self.transactionUnderTest()
+ store1 = self.storeUnderTest()
+ response = yield store1.conduit.send_fake(txn, "user01", "puser01")
+ self.assertTrue("result" in response)
+ self.assertEqual(response["result"], "ok")
+ self.assertTrue("back2u" in response)
+ self.assertEqual(response["back2u"], "bravo")
+ self.assertTrue("more" in response)
+ self.assertEqual(response["more"], "bits")
+ yield txn.commit()
+
+ store2 = self.otherStoreUnderTest()
+ txn = store2.newTransaction()
+ response = yield store2.conduit.send_fake(txn, "puser01", "user01")
+ self.assertTrue("result" in response)
+ self.assertEqual(response["result"], "ok")
+ self.assertTrue("back2u" in response)
+ self.assertEqual(response["back2u"], "bravo")
+ self.assertTrue("more" in response)
+ self.assertEqual(response["more"], "bits")
+ yield txn.commit()
+
+
+
+class TestConduitAPI(MultiStoreConduitTest):
+ """
+ Test that the conduit api works.
+ """
+
+ nowYear = {"now": DateTime.getToday().getYear()}
+
+ caldata1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ caldata2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:ui2
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+ @inlineCallbacks
+ def test_basic_share(self):
+ """
+ Test that basic invite/uninvite works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ shared = yield calendar1.shareeView("puser01")
+ self.assertEqual(shared.shareStatus(), _BIND_STATUS_ACCEPTED)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ self.assertTrue(shared is not None)
+ self.assertTrue(shared.external())
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.uninviteUserFromShare("puser01")
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_countobjects(self):
+ """
+ Test that action=countobjects works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ count = yield shared.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ count = yield calendar1.countObjectResources()
+ self.assertEqual(count, 1)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ count = yield shared.countObjectResources()
+ self.assertEqual(count, 1)
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ count = yield calendar1.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ count = yield shared.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_listobjects(self):
+ """
+ Test that action=listobjects works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ objects = yield shared.listObjectResources()
+ self.assertEqual(set(objects), set())
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+ objects = yield calendar1.listObjectResources()
+ self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ objects = yield shared.listObjectResources()
+ self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ objects = yield calendar1.listObjectResources()
+ self.assertEqual(set(objects), set(("2.ics",)))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ objects = yield shared.listObjectResources()
+ self.assertEqual(set(objects), set(("2.ics",)))
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_synctoken(self):
+ """
+ Test that action=synctoken works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_1 = yield calendar1.syncToken()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_1 = yield shared.syncToken()
+ yield self.otherCommit()
+
+ self.assertEqual(token1_1, token2_1)
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_2 = yield calendar1.syncToken()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_2 = yield shared.syncToken()
+ yield self.otherCommit()
+
+ self.assertNotEqual(token1_1, token1_2)
+ self.assertEqual(token1_2, token2_2)
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ count = yield calendar1.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_3 = yield calendar1.syncToken()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_3 = yield shared.syncToken()
+ yield self.otherCommit()
+
+ self.assertNotEqual(token1_1, token1_3)
+ self.assertNotEqual(token1_2, token1_3)
+ self.assertEqual(token1_3, token2_3)
+
+
+ @inlineCallbacks
+ def test_resourcenamessincerevision(self):
+ """
+ Test that action=synctoken works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_1 = yield calendar1.syncToken()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_1 = yield shared.syncToken()
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_2 = yield calendar1.syncToken()
+ names1 = yield calendar1.resourceNamesSinceToken(token1_1)
+ self.assertEqual(names1, (["1.ics"], [],))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_2 = yield shared.syncToken()
+ names2 = yield shared.resourceNamesSinceToken(token2_1)
+ self.assertEqual(names2, (["1.ics"], [],))
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ count = yield calendar1.countObjectResources()
+ self.assertEqual(count, 0)
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ token1_3 = yield calendar1.syncToken()
+ names1 = yield calendar1.resourceNamesSinceToken(token1_2)
+ self.assertEqual(names1, ([], ["1.ics"],))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ token2_3 = yield shared.syncToken()
+ names2 = yield shared.resourceNamesSinceToken(token2_2)
+ self.assertEqual(names2, ([], ["1.ics"],))
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ names1 = yield calendar1.resourceNamesSinceToken(token1_3)
+ self.assertEqual(names1, ([], [],))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ names2 = yield shared.resourceNamesSinceToken(token2_3)
+ self.assertEqual(names2, ([], [],))
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_resourceuidforname(self):
+ """
+ Test that action=resourceuidforname works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ uid = yield calendar1.resourceUIDForName("1.ics")
+ self.assertEqual(uid, "uid1")
+ uid = yield calendar1.resourceUIDForName("2.ics")
+ self.assertTrue(uid is None)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ uid = yield shared.resourceUIDForName("1.ics")
+ self.assertEqual(uid, "uid1")
+ uid = yield shared.resourceUIDForName("2.ics")
+ self.assertTrue(uid is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_resourcenameforuid(self):
+ """
+ Test that action=resourcenameforuid works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ uid = yield calendar1.resourceNameForUID("uid1")
+ self.assertEqual(uid, "1.ics")
+ uid = yield calendar1.resourceNameForUID("uid2")
+ self.assertTrue(uid is None)
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ uid = yield shared.resourceNameForUID("uid1")
+ self.assertEqual(uid, "1.ics")
+ uid = yield shared.resourceNameForUID("uid2")
+ self.assertTrue(uid is None)
+ yield self.otherCommit()
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -86,7 +86,8 @@
home = yield self.transactionUnderTest().calendarHomeWithUID("puser{:02d}".format(i), create=True)
self.assertTrue(home is not None)
self.assertEqual(home._status, _HOME_STATUS_EXTERNAL)
- self.assertRaises(AssertionError, home.childWithName, "calendar")
+ calendar = yield home.childWithName("calendar")
+ self.assertTrue(calendar is None)
@inlineCallbacks
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -25,12 +25,13 @@
from txdav.common.datastore.podding.resource import ConduitResource
from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
import json
+from txdav.common.datastore.podding.conduit import PoddingConduit
class ConduitPOST (CommonCommonTests, twext.web2.dav.test.util.TestCase):
- class FakeConduit(object):
+ class FakeConduit(PoddingConduit):
- def recv_fake(self, j):
+ def recv_fake(self, txn, j):
return succeed({
"result": "ok",
"back2u": j["echo"],
@@ -214,7 +215,8 @@
Cross-pod request fails when conduit does not support the action.
"""
- self.patch(self.storeUnderTest(), "conduit", self.FakeConduit())
+ store = self.storeUnderTest()
+ self.patch(store, "conduit", self.FakeConduit(store))
request = SimpleRequest(
self.site,
@@ -242,7 +244,8 @@
Cross-pod request works when conduit does support the action.
"""
- self.patch(self.storeUnderTest(), "conduit", self.FakeConduit())
+ store = self.storeUnderTest()
+ self.patch(store, "conduit", self.FakeConduit(store))
request = SimpleRequest(
self.site,
Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -0,0 +1,202 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Server, \
+ Servers
+from txdav.caldav.datastore.test.util import \
+ TestCalendarStoreDirectoryRecord, TestCalendarStoreDirectoryService
+from txdav.common.datastore.podding.conduit import PoddingConduit
+from txdav.common.datastore.test.util import CommonCommonTests, SQLStoreBuilder,\
+ theStoreBuilder
+
+import twext.web2.dav.test.util
+from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
+
+class FakeConduitRequest(object):
+ """
+ A conduit request that sends messages internally rather than using HTTP
+ """
+
+ storeMap = {}
+
+ @classmethod
+ def addServerStore(cls, server, store):
+ """
+ Add a store mapped to a server. These mappings are used to "deliver" conduit
+ requests to the appropriate store.
+
+ @param uri: the server
+ @type uri: L{Server}
+ @param store: the store
+ @type store: L{ICommonDataStore}
+ """
+
+ cls.storeMap[server.details()] = store
+
+
+ def __init__(self, server, data):
+
+ self.server = server
+ self.data = data
+
+
+ @inlineCallbacks
+ def doRequest(self, txn):
+
+ # Generate an HTTP client request
+ try:
+ response = (yield self._processRequest())
+ except Exception as e:
+ raise ValueError("Failed cross-pod request: {}".format(e))
+
+ returnValue(response)
+
+
+ @inlineCallbacks
+ def _processRequest(self):
+ """
+ Process the request by sending it to the relevant server.
+
+ @return: the HTTP response.
+ @rtype: L{Response}
+ """
+
+ store = self.storeMap[self.server.details()]
+ result = yield store.conduit.processRequest(self.data)
+ returnValue(result)
+
+
+
+class MultiStoreConduitTest(CommonCommonTests, twext.web2.dav.test.util.TestCase):
+
+ theStoreBuilder2 = SQLStoreBuilder(secondary=True)
+ otherTransaction = None
+
+ @inlineCallbacks
+ def setUp(self):
+ yield super(MultiStoreConduitTest, self).setUp()
+
+ server1 = Server("A", "http://127.0.0.1:8008", "A", True)
+ Servers.addServer(server1)
+
+ server2 = Server("B", "http://127.0.0.1:8108", "B", False)
+ Servers.addServer(server2)
+
+ self._sqlCalendarStore1 = yield self.makeStore(theStoreBuilder, True, server1, server2)
+ self._sqlCalendarStore2 = yield self.makeStore(self.theStoreBuilder2, False, server1, server2)
+
+ FakeConduitRequest.addServerStore(server1, self._sqlCalendarStore1)
+ FakeConduitRequest.addServerStore(server2, self._sqlCalendarStore2)
+
+
+ def storeUnderTest(self):
+ """
+ Return a store for testing.
+ """
+ return self._sqlCalendarStore1
+
+
+ def otherStoreUnderTest(self):
+ """
+ Return a store for testing.
+ """
+ return self._sqlCalendarStore2
+
+
+ def newOtherTransaction(self):
+ assert self.otherTransaction is None
+ store2 = self.otherStoreUnderTest()
+ self.otherTransaction = store2.newTransaction()
+ return self.otherTransaction
+
+
+ @inlineCallbacks
+ def otherCommit(self):
+ assert self.otherTransaction is not None
+ yield self.otherTransaction.commit()
+ self.otherTransaction = None
+
+
+ @inlineCallbacks
+ def otherAbort(self):
+ assert self.otherTransaction is not None
+ yield self.otherTransaction.abort()
+ self.otherTransaction = None
+
+
+ @inlineCallbacks
+ def makeStore(self, builder, internal, server1, server2):
+
+ directory = self.makeDirectory(internal, server1, server2)
+ store = yield builder.buildStore(self, self.notifierFactory, directory)
+ store.queryCacher = None # Cannot use query caching
+ store.conduit = self.makeConduit(store)
+ returnValue(store)
+
+
+ def makeDirectory(self, internal, server1, server2):
+
+ directory = TestCalendarStoreDirectoryService()
+
+ # User accounts
+ for ctr in range(1, 100):
+ directory.addRecord(TestCalendarStoreDirectoryRecord(
+ "user%02d" % (ctr,),
+ ("user%02d" % (ctr,),),
+ "User %02d" % (ctr,),
+ frozenset((
+ "urn:uuid:user%02d" % (ctr,),
+ "mailto:user%02d at example.com" % (ctr,),
+ )),
+ thisServer=internal,
+ server=server1
+ ))
+
+ for ctr in range(1, 100):
+ directory.addRecord(TestCalendarStoreDirectoryRecord(
+ "puser{:02d}".format(ctr),
+ ("puser{:02d}".format(ctr),),
+ "Puser {:02d}".format(ctr),
+ frozenset((
+ "urn:uuid:puser{:02d}".format(ctr),
+ "mailto:puser{:02d}@example.com".format(ctr),
+ )),
+ thisServer=not internal,
+ server=server2
+ ))
+
+ return directory
+
+
+ def makeConduit(self, store):
+ conduit = PoddingConduit(store)
+ conduit.conduitRequestClass = FakeConduitRequest
+ return conduit
+
+
+ @inlineCallbacks
+ def createShare(self, ownerGUID, shareeGUID, name="calendar"):
+
+ home = yield self.homeUnderTest(name=ownerGUID, create=True)
+ calendar = yield home.calendarWithName("calendar")
+ yield calendar.inviteUserToShare(shareeGUID, _BIND_MODE_WRITE, "shared", shareName="shared-calendar")
+ yield self.commit()
+
+ home2 = yield self.homeUnderTest(txn=self.newOtherTransaction(), name=shareeGUID)
+ yield home2.acceptShare("shared-calendar")
+ yield self.otherCommit()
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -67,7 +67,7 @@
_BIND_MODE_INDIRECT, _HOME_STATUS_NORMAL, _HOME_STATUS_EXTERNAL
from txdav.common.datastore.sql_tables import schema, splitSQLString
from txdav.common.icommondatastore import ConcurrentModification, \
- RecordNotAllowedError, ExternalShareFailed
+ RecordNotAllowedError, ExternalShareFailed, ShareNotAllowed
from txdav.common.icommondatastore import HomeChildNameNotAllowedError, \
HomeChildNameAlreadyExistsError, NoSuchHomeChildError, \
ObjectResourceNameNotAllowedError, ObjectResourceNameAlreadyExistsError, \
@@ -477,7 +477,7 @@
self._label = label
self._migrating = migrating
self._primaryHomeType = None
- self._disableCache = disableCache
+ self._disableCache = disableCache or not store.queryCachingEnabled()
if disableCache:
self._queryCacher = None
else:
@@ -1562,6 +1562,25 @@
_cacher = None # Initialize in derived classes
+ @classmethod
+ @inlineCallbacks
+ def makeClass(cls, transaction, ownerUID, no_cache=False):
+ """
+ Build the actual home class taking into account the possibility that we might need to
+ switch in the external version of the class.
+
+ @param transaction: transaction
+ @type transaction: L{CommonStoreTransaction}
+ @param ownerUID: owner UID of home to load
+ @type ownerUID: C{str}
+ @param no_cache: should cached query be used
+ @type no_cache: C{bool}
+ """
+ home = cls(transaction, ownerUID)
+ actualHome = yield home.initFromStore(no_cache)
+ returnValue(actualHome)
+
+
def __init__(self, transaction, ownerUID):
self._txn = transaction
self._ownerUID = ownerUID
@@ -1708,6 +1727,10 @@
actualHome = self
yield actualHome.initMetaDataFromStore()
yield actualHome._loadPropertyStore()
+
+ for factory_type, factory in self._txn._notifierFactories.items():
+ actualHome.addNotifier(factory_type, factory.newNotifier(actualHome))
+
returnValue(actualHome)
else:
returnValue(None)
@@ -1756,10 +1779,7 @@
@classmethod
@inlineCallbacks
def homeWithUID(cls, txn, uid, create=False):
- homeObject = cls(txn, uid)
- for factory_type, factory in txn._notifierFactories.items():
- homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
- homeObject = (yield homeObject.initFromStore())
+ homeObject = yield cls.makeClass(txn, uid)
if homeObject is not None:
returnValue(homeObject)
else:
@@ -1769,7 +1789,7 @@
# Determine if the user is local or external
record = txn.directoryService().recordWithUID(uid)
if record is None:
- raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {uid}".format(uid=uid))
+ raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
@@ -1794,10 +1814,7 @@
yield savepoint.rollback(txn)
# Retry the query - row may exist now, if not re-raise
- homeObject = cls(txn, uid)
- for factory_type, factory in txn._notifierFactories.items():
- homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
- homeObject = (yield homeObject.initFromStore())
+ homeObject = yield cls.makeClass(txn, uid)
if homeObject:
returnValue(homeObject)
else:
@@ -1808,10 +1825,7 @@
# Note that we must not cache the owner_uid->resource_id
# mapping in _cacher when creating as we don't want that to appear
# until AFTER the commit
- home = cls(txn, uid)
- for factory_type, factory in txn._notifierFactories.items():
- home.addNotifier(factory_type, factory.newNotifier(home))
- home = (yield home.initFromStore(no_cache=True))
+ home = yield cls.makeClass(txn, uid, no_cache=True)
yield home.createdHome()
returnValue(home)
@@ -2533,165 +2547,6 @@
-class CommonHomeExternal(CommonHome):
- """
- A CommonHome for a user not hosted on this system, but on another pod. This is needed to provide a
- "reference" to the external user so we can share with them. Actual operations to list child resources, etc
- are all stubbed out since no data for the user is actually hosted in this store.
- """
-
- def __init__(self, transaction, ownerUID, resourceID):
- super(CommonHomeExternal, self).__init__(transaction, ownerUID)
- self._resourceID = resourceID
- self._status = _HOME_STATUS_EXTERNAL
-
-
- def initFromStore(self, no_cache=False):
- """
- Never called - this should be done by CommonHome.initFromStore only.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def external(self):
- """
- Is this an external home.
-
- @return: a string.
- """
- return True
-
-
- def children(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def loadChildren(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def listChildren(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def objectWithShareUID(self, shareUID):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def invitedObjectWithShareUID(self, shareUID):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- @memoizedKey("name", "_children")
- @inlineCallbacks
- def createChildWithName(self, name, externalID=None):
- """
- No real children - only external ones.
- """
- if externalID is None:
- raise AssertionError("CommonHomeExternal: not supported")
- child = yield super(CommonHomeExternal, self).createChildWithName(name, externalID)
- returnValue(child)
-
-
- def removeChildWithName(self, name):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def syncToken(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def resourceNamesSinceRevision(self, revision, depth):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- @inlineCallbacks
- def _loadPropertyStore(self):
- """
- No property store - stub to a NonePropertyStore.
- """
- props = yield PropertyStore.load(
- self.uid(),
- self.uid(),
- self._txn,
- self._resourceID,
- notifyCallback=self.notifyChanged
- )
- self._propertyStore = props
-
-
- def properties(self):
- return self._propertyStore
-
-
- def objectResourcesWithUID(self, uid, ignore_children=[], allowShared=True):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def objectResourceWithID(self, rid):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
- def notifyChanged(self):
- """
- Notifications are not handled for external homes - make this a no-op.
- """
- return succeed(None)
-
-
- def bumpModified(self):
- """
- No changes recorded for external homes - make this a no-op.
- """
- return succeed(None)
-
-
- def removeUnacceptedShares(self):
- """
- No children.
- """
- raise AssertionError("CommonHomeExternal: not supported")
-
-
-# def ownerHomeAndChildNameForChildID(self, resourceID):
-# """
-# No children.
-# """
-# raise AssertionError("CommonHomeExternal: not supported")
-
-
-
class _SharedSyncLogic(object):
"""
Logic for maintaining sync-token shared between notification collections and
@@ -3507,7 +3362,7 @@
subt,
homeID=shareeHome._resourceID,
resourceID=self._resourceID,
- externalID=None,
+ externalID=self._externalID,
name=newName,
mode=mode,
bindStatus=status,
@@ -3997,6 +3852,7 @@
"_resourceID",
)
+ _externalClass = None
_objectResourceClass = None
_bindSchema = None
@@ -4007,6 +3863,142 @@
_objectSchema = None
+ @classmethod
+ @inlineCallbacks
+ def makeClass(cls, home, bindData, additionalBindData, metadataData, propstore=None, ownerHome=None):
+ """
+ Given the various database rows, build the actual class.
+
+ @param home: the parent home object
+ @type home: L{CommonHome}
+ @param bindData: the standard set of bind columns
+ @type bindData: C{list}
+ @param additionalBindData: additional bind data specific to sub-classes
+ @type additionalBindData: C{list}
+ @param metadataData: metadata data
+ @type metadataData: C{list}
+ @param propstore: a property store to use, or C{None} to load it automatically
+ @type propstore: L{PropertyStore}
+ @param ownerHome: the home of the owner, or C{None} to figure it out automatically
+ @type ownerHome: L{CommonHome}
+
+ @return: the constructed child class
+ @rtype: L{CommonHomeChild}
+ """
+
+ bindMode, _ignore_homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = bindData
+
+ if ownerHome is None:
+ if bindMode == _BIND_MODE_OWN:
+ ownerHome = home
+ ownerName = name
+ else:
+ ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
+ else:
+ ownerName = None
+
+ c = cls._externalClass if ownerHome.external() else cls
+ child = c(
+ home=home,
+ name=name,
+ resourceID=resourceID,
+ mode=bindMode,
+ status=bindStatus,
+ revision=bindRevision,
+ message=bindMessage,
+ ownerHome=ownerHome,
+ ownerName=ownerName,
+ externalID=externalID,
+ )
+
+ if additionalBindData:
+ for attr, value in zip(child.additionalBindAttributes(), additionalBindData):
+ setattr(child, attr, value)
+
+ if metadataData:
+ for attr, value in zip(child.metadataAttributes(), metadataData):
+ setattr(child, attr, value)
+
+ # We have to re-adjust the property store object to account for possible shared
+ # collections as previously we loaded them all as if they were owned
+ if propstore and bindMode != _BIND_MODE_OWN:
+ propstore._setDefaultUserUID(ownerHome.uid())
+ yield child._loadPropertyStore(propstore)
+
+ returnValue(child)
+
+
+ @classmethod
+ @inlineCallbacks
+ def _getDBData(cls, home, name, resourceID, externalID):
+ """
+ Given a set of identifying information, load the data rows for the object. Only one of
+ L{name}, L{resourceID} or L{externalID} is specified - others are C{None}.
+
+ @param home: the parent home object
+ @type home: L{CommonHome}
+ @param name: the resource name
+ @type name: C{str}
+ @param resourceID: the resource ID
+ @type resourceID: C{int}
+ @param externalID: the resource ID of the external (cross-pod) referenced item
+ @type externalID: C{int}
+ """
+
+ # Get the bind row data
+ row = None
+ queryCacher = home._txn._queryCacher
+
+ if queryCacher:
+ # Retrieve data from cache
+ if name:
+ cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
+ elif resourceID:
+ cacheKey = queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID)
+ elif externalID:
+ cacheKey = queryCacher.keyForObjectWithExternalID(home._resourceID, externalID)
+ row = yield queryCacher.get(cacheKey)
+
+ if row is None:
+ # No cached copy
+ if name:
+ rows = yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)
+ elif resourceID:
+ rows = yield cls._bindForResourceIDAndHomeID.on(home._txn, resourceID=resourceID, homeID=home._resourceID)
+ elif externalID:
+ rows = yield cls._bindForExternalIDAndHomeID.on(home._txn, externalID=externalID, homeID=home._resourceID)
+ row = rows[0] if rows else None
+
+ if not row:
+ returnValue(None)
+
+ if queryCacher:
+ # Cache the result
+ queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithName(home._resourceID, name), row)
+ queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID), row)
+ queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithExternalID(home._resourceID, externalID), row)
+
+ bindData = row[:cls.bindColumnCount]
+ additionalBindData = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+ resourceID = bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)]
+
+ # Get the matching metadata data
+ metadataData = None
+ if queryCacher:
+ # Retrieve from cache
+ cacheKey = queryCacher.keyForHomeChildMetaData(resourceID)
+ metadataData = yield queryCacher.get(cacheKey)
+
+ if metadataData is None:
+ # No cached copy
+ metadataData = (yield cls._metadataByIDQuery.on(home._txn, resourceID=resourceID))[0]
+ if queryCacher:
+ # Cache the results
+ yield queryCacher.setAfterCommit(home._txn, cacheKey, metadataData)
+
+ returnValue((bindData, additionalBindData, metadataData,))
+
+
def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=None):
self._home = home
@@ -4062,7 +4054,7 @@
rows = yield cls._acceptedBindForHomeID.on(
home._txn, homeID=home._resourceID
)
- names = [row[3] for row in rows]
+ names = [row[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] for row in rows]
returnValue(names)
@@ -4095,41 +4087,16 @@
# Create the actual objects merging in properties
for dataRow in dataRows:
- bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
- additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
- metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+ bindData = dataRow[:cls.bindColumnCount] #@UnusedVariable
+ resourceID = bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)]
+ additionalBindData = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+ metadataData = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+ propstore = propertyStores.get(resourceID, None)
- if bindMode == _BIND_MODE_OWN:
- ownerHome = home
- ownerName = bindName
- else:
- #TODO: get all ownerHomeIDs at once
- ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
-
- child = cls(
- home=home,
- name=bindName,
- resourceID=resourceID,
- mode=bindMode,
- status=bindStatus,
- revision=bindRevision,
- message=bindMessage,
- ownerHome=ownerHome,
- ownerName=ownerName,
- externalID=externalID,
- )
- for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
- setattr(child, attr, value)
- for attr, value in zip(cls.metadataAttributes(), metadata):
- setattr(child, attr, value)
+ child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore)
child._syncTokenRevision = revisions[resourceID]
- propstore = propertyStores.get(resourceID, None)
- # We have to re-adjust the property store object to account for possible shared
- # collections as previously we loaded them all as if they were owned
- if propstore and bindMode != _BIND_MODE_OWN:
- propstore._setDefaultUserUID(ownerHome.uid())
- yield child._loadPropertyStore(propstore)
results.append(child)
+
returnValue(results)
@@ -4163,63 +4130,17 @@
@return: an L{CommonHomeChild} or C{None} if no such child
exists.
"""
- rows = None
- queryCacher = home._txn._queryCacher
- if queryCacher:
- # Retrieve data from cache
- if name:
- cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
- elif resourceID:
- cacheKey = queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID)
- elif externalID:
- cacheKey = queryCacher.keyForObjectWithExternalID(home._resourceID, externalID)
- rows = yield queryCacher.get(cacheKey)
-
- if rows is None:
- # No cached copy
- if name:
- rows = yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)
- elif resourceID:
- rows = yield cls._bindForResourceIDAndHomeID.on(home._txn, resourceID=resourceID, homeID=home._resourceID)
- elif resourceID:
- rows = yield cls._bindForExternalIDAndHomeID.on(home._txn, externalID=externalID, homeID=home._resourceID)
-
- if not rows:
+ dbData = yield cls._getDBData(home, name, resourceID, externalID)
+ if dbData is None:
returnValue(None)
+ bindData, additionalBindData, metadataData = dbData
- row = rows[0]
- bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
-
- if queryCacher:
- # Cache the result
- queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithName(home._resourceID, name), rows)
- queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID), rows)
- queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithExternalID(home._resourceID, externalID), rows)
-
+ bindStatus = bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)]
if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
returnValue(None)
- additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
- if bindMode == _BIND_MODE_OWN:
- ownerHome = home
- ownerName = name
- else:
- ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
-
- child = cls(
- home=home,
- name=name,
- resourceID=resourceID,
- mode=bindMode,
- status=bindStatus,
- revision=bindRevision,
- message=bindMessage,
- ownerHome=ownerHome,
- ownerName=ownerName,
- externalID=externalID,
- )
- yield child.initFromStore(additionalBind)
+ child = yield cls.makeClass(home, bindData, additionalBindData, metadataData)
returnValue(child)
@@ -4289,36 +4210,6 @@
Where=child.RESOURCE_ID == Parameter("resourceID"))
- @inlineCallbacks
- def initFromStore(self, additionalBind=None):
- """
- Initialise this object from the store, based on its already-populated
- resource ID. We read in and cache all the extra metadata from the DB to
- avoid having to do DB queries for those individually later.
- """
- queryCacher = self._txn._queryCacher
- if queryCacher:
- # Retrieve from cache
- cacheKey = queryCacher.keyForHomeChildMetaData(self._resourceID)
- dataRows = yield queryCacher.get(cacheKey)
- else:
- dataRows = None
- if dataRows is None:
- # No cached copy
- dataRows = (yield self._metadataByIDQuery.on(self._txn, resourceID=self._resourceID))[0]
- if queryCacher:
- # Cache the results
- yield queryCacher.setAfterCommit(self._txn, cacheKey, dataRows)
-
- if additionalBind:
- for attr, value in zip(self.additionalBindAttributes(), additionalBind):
- setattr(self, attr, value)
-
- for attr, value in zip(self.metadataAttributes(), dataRows):
- setattr(self, attr, value)
- yield self._loadPropertyStore()
-
-
def id(self):
"""
Retrieve the store identifier for this collection.
@@ -4329,6 +4220,15 @@
return self._resourceID
+ def external_id(self):
+ """
+ Retrieve the external store identifier for this collection.
+
+ @return: a string.
+ """
+ return self._externalID
+
+
def external(self):
"""
Is this an external home.
@@ -4385,6 +4285,10 @@
@return: a L{Deferred} which fires when the modification is complete.
"""
+
+ if self.isShared() or self.external():
+ raise ShareNotAllowed("Cannot rename a shared collection")
+
oldName = self._name
yield self.invalidateQueryCache()
@@ -4521,8 +4425,7 @@
@inlineCallbacks
def countObjectResources(self):
if self._objectNames is None:
- rows = yield self._objectCountQuery.on(
- self._txn, resourceID=self._resourceID)
+ rows = yield self._objectCountQuery.on(self._txn, resourceID=self._resourceID)
returnValue(rows[0][0])
returnValue(len(self._objectNames))
@@ -4612,7 +4515,7 @@
obj = cls._objectSchema
return Select(
[obj.UID], From=obj,
- Where=(obj.UID == Parameter("name")
+ Where=(obj.RESOURCE_NAME == Parameter("name")
).And(obj.PARENT_RESOURCE_ID == Parameter("resourceID")))
@@ -4971,10 +4874,84 @@
"_parentCollection",
)
+ _externalClass = None
_objectSchema = None
BATCH_LOAD_SIZE = 50
+
+ @classmethod
+ @inlineCallbacks
+ def makeClass(cls, parent, objectData, propstore=None):
+ """
+ Given the various database rows, build the actual class.
+
+ @param parent: the parent collection object
+ @type parent: L{CommonHomeChild}
+ @param objectData: the standard set of object columns
+ @type objectData: C{list}
+ @param propstore: a property store to use, or C{None} to load it automatically
+ @type propstore: L{PropertyStore}
+
+ @return: the constructed child class
+ @rtype: L{CommonHomeChild}
+ """
+
+ c = cls._externalClass if parent.external() else cls
+ child = c(
+ parent,
+ objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
+ objectData[cls._allColumns().index(cls._objectSchema.UID)],
+ )
+
+ for attr, value in zip(child._rowAttributes(), objectData):
+ setattr(child, attr, value)
+
+ yield child._loadPropertyStore(propstore)
+
+ returnValue(child)
+
+
+ @classmethod
+ @inlineCallbacks
+ def _getDBData(cls, parent, name, uid, resourceID):
+ """
+ Given a set of identifying information, load the data rows for the object. Only one of
+ L{name}, L{uid} or L{resourceID} is specified - others are C{None}.
+
+ @param parent: the parent collection object
+ @type parent: L{CommonHomeChild}
+ @param name: the resource name
+ @type name: C{str}
+ @param uid: the UID of the data
+ @type uid: C{str}
+ @param resourceID: the resource ID
+ @type resourceID: C{int}
+ """
+
+ rows = None
+ if name:
+ rows = yield cls._allColumnsWithParentAndName.on(
+ parent._txn,
+ name=name,
+ parentID=parent._resourceID
+ )
+ elif uid:
+ rows = yield cls._allColumnsWithParentAndUID.on(
+ parent._txn,
+ uid=uid,
+ parentID=parent._resourceID
+ )
+ elif resourceID:
+ rows = yield cls._allColumnsWithParentAndID.on(
+ parent._txn,
+ resourceID=resourceID,
+ parentID=parent._resourceID
+ )
+
+ returnValue(rows[0] if rows else None)
+
+
def __init__(self, parent, name, uid, resourceID=None, options=None): #@UnusedVariable
self._parentCollection = parent
self._resourceID = resourceID
@@ -4992,7 +4969,7 @@
@classproperty
def _allColumnsWithParentQuery(cls): #@NoSelf
obj = cls._objectSchema
- return Select(cls._allColumns, From=obj,
+ return Select(cls._allColumns(), From=obj,
Where=obj.PARENT_RESOURCE_ID == Parameter("parentID"))
@@ -5033,11 +5010,10 @@
# Create the actual objects merging in properties
for row in dataRows:
- child = cls(parent, "", None)
- child._initFromRow(tuple(row))
- yield child._loadPropertyStore(
- props=propertyStores.get(child._resourceID, None)
- )
+ resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+ propstore = propertyStores.get(resourceID, None)
+
+ child = yield cls.makeClass(parent, row, propstore=propstore)
results.append(child)
returnValue(results)
@@ -5062,7 +5038,7 @@
@classmethod
def _allColumnsWithParentAndNamesQuery(cls, names):
obj = cls._objectSchema
- return Select(cls._allColumns, From=obj,
+ return Select(cls._allColumns(), From=obj,
Where=(obj.PARENT_RESOURCE_ID == Parameter("parentID")).And(
obj.RESOURCE_NAME.In(Parameter("names", len(names)))))
@@ -5107,11 +5083,10 @@
# Create the actual objects merging in properties
for row in dataRows:
- child = cls(parent, "", None)
- child._initFromRow(tuple(row))
- yield child._loadPropertyStore(
- props=propertyStores.get(child._resourceID, None)
- )
+ resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+ propstore = propertyStores.get(resourceID, None)
+
+ child = yield cls.makeClass(parent, row, propstore=propstore)
results.append(child)
returnValue(results)
@@ -5119,18 +5094,29 @@
@classmethod
def objectWithName(cls, parent, name, uid):
- objectResource = cls(parent, name, uid, None)
- return objectResource.initFromStore()
+ return cls._objectWithNameOrID(parent, name, uid, None)
@classmethod
def objectWithID(cls, parent, resourceID):
- objectResource = cls(parent, None, None, resourceID)
- return objectResource.initFromStore()
+ return cls._objectWithNameOrID(parent, None, None, resourceID)
@classmethod
@inlineCallbacks
+ def _objectWithNameOrID(cls, parent, name, uid, resourceID):
+
+ row = yield cls._getDBData(parent, name, uid, resourceID)
+
+ if row:
+ child = yield cls.makeClass(parent, row)
+ returnValue(child)
+ else:
+ returnValue(None)
+
+
+ @classmethod
+ @inlineCallbacks
def create(cls, parent, name, component, options=None):
child = (yield parent.objectResourceWithName(name))
@@ -5140,13 +5126,13 @@
if name.startswith("."):
raise ObjectResourceNameNotAllowedError(name)
- objectResource = cls(parent, name, None, None, options=options)
+ c = cls._externalClass if parent.external() else cls
+ objectResource = c(parent, name, None, None, options=options)
yield objectResource.setComponent(component, inserting=True)
yield objectResource._loadPropertyStore(created=True)
# Note: setComponent triggers a notification, so we don't need to
# call notify( ) here like we do for object removal.
-
returnValue(objectResource)
@@ -5157,7 +5143,7 @@
parameter and a given instance column matches a given parameter name.
"""
return Select(
- cls._allColumns, From=cls._objectSchema,
+ cls._allColumns(), From=cls._objectSchema,
Where=(column == Parameter(paramName)).And(
cls._objectSchema.PARENT_RESOURCE_ID == Parameter("parentID"))
)
@@ -5178,38 +5164,7 @@
return cls._allColumnsWithParentAnd(cls._objectSchema.RESOURCE_ID, "resourceID")
- @inlineCallbacks
- def initFromStore(self):
- """
- Initialise this object from the store. We read in and cache all the
- extra metadata from the DB to avoid having to do DB queries for those
- individually later. Either the name or uid is present, so we have to
- tweak the query accordingly.
-
- @return: L{self} if object exists in the DB, else C{None}
- """
-
- if self._name:
- rows = yield self._allColumnsWithParentAndName.on(
- self._txn, name=self._name,
- parentID=self._parentCollection._resourceID)
- elif self._uid:
- rows = yield self._allColumnsWithParentAndUID.on(
- self._txn, uid=self._uid,
- parentID=self._parentCollection._resourceID)
- elif self._resourceID:
- rows = yield self._allColumnsWithParentAndID.on(
- self._txn, resourceID=self._resourceID,
- parentID=self._parentCollection._resourceID)
- if rows:
- self._initFromRow(tuple(rows[0]))
- yield self._loadPropertyStore()
- returnValue(self)
- else:
- returnValue(None)
-
-
- @classproperty
+ @classmethod
def _allColumns(cls): #@NoSelf
"""
Full set of columns in the object table that need to be loaded to
@@ -5227,18 +5182,17 @@
]
- def _initFromRow(self, row):
- """
- Given a select result using the columns from L{_allColumns}, initialize
- the object resource state.
- """
- (self._resourceID,
- self._name,
- self._uid,
- self._md5,
- self._size,
- self._created,
- self._modified,) = tuple(row)
+ @classmethod
+ def _rowAttributes(cls): #@NoSelf
+ return (
+ "_resourceID",
+ "_name",
+ "_uid",
+ "_md5",
+ "_size",
+ "_created",
+ "_modified",
+ )
@inlineCallbacks
@@ -5542,11 +5496,11 @@
# Determine if the user is local or external
record = txn.directoryService().recordWithUID(uid)
if record is None:
- raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {uid}".format(uid=uid))
+ raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
if state == _HOME_STATUS_EXTERNAL:
- raise RecordNotAllowedError("Cannot store notifications for external user: {uid}".format(uid=uid))
+ raise RecordNotAllowedError("Cannot store notifications for external user: {}".format(uid))
# Use savepoint so we can do a partial rollback if there is a race
# condition where this row has already been inserted
Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -0,0 +1,354 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from txdav.common.icommondatastore import NonExistentExternalShare, \
+ ExternalShareFailed
+"""
+SQL data store.
+"""
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
+from twext.internet.decorate import memoizedKey
+from twext.python.log import Logger
+
+from txdav.base.propertystore.sql import PropertyStore
+from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
+ CommonObjectResource
+from txdav.common.datastore.sql_tables import _HOME_STATUS_EXTERNAL
+
+
+log = Logger()
+
+class CommonHomeExternal(CommonHome):
+ """
+ A CommonHome for a user not hosted on this system, but on another pod. This is needed to provide a
+ "reference" to the external user so we can share with them. Actual operations to list child resources, etc
+ are all stubbed out since no data for the user is actually hosted in this store.
+ """
+
+ def __init__(self, transaction, ownerUID, resourceID):
+ super(CommonHomeExternal, self).__init__(transaction, ownerUID)
+ self._resourceID = resourceID
+ self._status = _HOME_STATUS_EXTERNAL
+
+
+ def initFromStore(self, no_cache=False):
+ """
+ Never called - this should be done by CommonHome.initFromStore only.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def external(self):
+ """
+ Is this an external home.
+
+ @return: a string.
+ """
+ return True
+
+
+ def children(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def loadChildren(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def listChildren(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def objectWithShareUID(self, shareUID):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def invitedObjectWithShareUID(self, shareUID):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ @memoizedKey("name", "_children")
+ @inlineCallbacks
+ def createChildWithName(self, name, externalID=None):
+ """
+ No real children - only external ones.
+ """
+ if externalID is None:
+ raise AssertionError("CommonHomeExternal: not supported")
+ child = yield super(CommonHomeExternal, self).createChildWithName(name, externalID)
+ returnValue(child)
+
+
+ def removeChildWithName(self, name):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def syncToken(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def resourceNamesSinceRevision(self, revision, depth):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ @inlineCallbacks
+ def _loadPropertyStore(self):
+ """
+ No property store - stub to a NonePropertyStore.
+ """
+ props = yield PropertyStore.load(
+ self.uid(),
+ self.uid(),
+ self._txn,
+ self._resourceID,
+ notifyCallback=self.notifyChanged
+ )
+ self._propertyStore = props
+
+
+ def properties(self):
+ return self._propertyStore
+
+
+ def objectResourcesWithUID(self, uid, ignore_children=[], allowShared=True):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def objectResourceWithID(self, rid):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+ def notifyChanged(self):
+ """
+ Notifications are not handled for external homes - make this a no-op.
+ """
+ return succeed(None)
+
+
+ def bumpModified(self):
+ """
+ No changes recorded for external homes - make this a no-op.
+ """
+ return succeed(None)
+
+
+ def removeUnacceptedShares(self):
+ """
+ No children.
+ """
+ raise AssertionError("CommonHomeExternal: not supported")
+
+
+# def ownerHomeAndChildNameForChildID(self, resourceID):
+# """
+# No children.
+# """
+# raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class CommonHomeChildExternal(CommonHomeChild):
+ """
+ A CommonHomeChild for a collection not hosted on this system, but on another pod. This will forward
+ specific apis to the other pod using cross-pod requests.
+ """
+
+ def external(self):
+ """
+ Is this an external home.
+
+ @return: a string.
+ """
+ return True
+
+
+ def fixNonExistentExternalShare(self):
+ """
+ An external request has returned and indicates the external share no longer exists. That
+ means this shared resource is an "orphan" and needs to be remove (uninvited) to clean things up.
+ """
+ log.error("Non-existent share detected and removed for {share}", share=self)
+ ownerView = yield self.ownerView()
+ yield ownerView.removeShare(self)
+
+
+ def remove(self, rid):
+ """
+ External shares are never removed directly - instead they must be "uninvited".
+ """
+ raise AssertionError("CommonHomeChildExternal: not supported")
+
+
+ @inlineCallbacks
+ def objectResources(self):
+ raise NotImplementedError("TODO: external resource")
+
+
+ @inlineCallbacks
+ def objectResourcesWithNames(self, names):
+ raise NotImplementedError("TODO: external resource")
+
+
+ @inlineCallbacks
+ def listObjectResources(self):
+ if self._objectNames is None:
+ try:
+ self._objectNames = yield self._txn.store().conduit.send_listobjects(self)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+
+ returnValue(self._objectNames)
+
+
+ @inlineCallbacks
+ def countObjectResources(self):
+ if self._objectNames is None:
+ try:
+ count = yield self._txn.store().conduit.send_countobjects(self)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+ returnValue(count)
+ returnValue(len(self._objectNames))
+
+
+ def objectResourceWithName(self, name):
+ raise NotImplementedError("TODO: external resource")
+
+
+ def objectResourceWithUID(self, uid):
+ raise NotImplementedError("TODO: external resource")
+
+
+ def objectResourceWithID(self, resourceID):
+ raise NotImplementedError("TODO: external resource")
+
+
+ @inlineCallbacks
+ def resourceNameForUID(self, uid):
+ try:
+ resource = self._objects[uid]
+ returnValue(resource.name() if resource else None)
+ except KeyError:
+ pass
+
+ try:
+ name = yield self._txn.store().conduit.send_resourcenameforuid(self, uid)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+
+ if name:
+ returnValue(name)
+ else:
+ self._objects[uid] = None
+ returnValue(None)
+
+
+ @inlineCallbacks
+ def resourceUIDForName(self, name):
+ try:
+ resource = self._objects[name]
+ returnValue(resource.uid() if resource else None)
+ except KeyError:
+ pass
+
+ try:
+ uid = yield self._txn.store().conduit.send_resourceuidforname(self, name)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+
+ if uid:
+ returnValue(uid)
+ else:
+ self._objects[name] = None
+ returnValue(None)
+
+
+ @inlineCallbacks
+ def createObjectResourceWithName(self, name, component, options=None):
+ raise NotImplementedError("TODO: external resource")
+
+
+ @inlineCallbacks
+ def moveObjectResource(self, child, newparent, newname=None):
+ raise NotImplementedError("TODO: external resource")
+
+
+ @inlineCallbacks
+ def syncToken(self):
+ if self._syncTokenRevision is None:
+ try:
+ token = yield self._txn.store().conduit.send_synctoken(self)
+ self._syncTokenRevision = self.revisionFromToken(token)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+ returnValue(("%s_%s" % (self._externalID, self._syncTokenRevision,)))
+
+
+ @inlineCallbacks
+ def resourceNamesSinceRevision(self, revision):
+ try:
+ names = yield self._txn.store().conduit.send_resourcenamessincerevision(self, revision)
+ except NonExistentExternalShare:
+ yield self.fixNonExistentExternalShare()
+ raise ExternalShareFailed("External share does not exist")
+
+ returnValue(names)
+
+
+
+class CommonObjectResourceExternal(CommonObjectResource):
+ """
+ A CommonObjectResource for a resource not hosted on this system, but on another pod. This will forward
+ specific apis to the other pod using cross-pod requests.
+ """
+ pass
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/test_sql.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/test_sql.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
+from txdav.caldav.datastore.test.util import buildDirectoryRecord
"""
Tests for L{txdav.common.datastore.sql}.
@@ -34,6 +35,11 @@
from twext.enterprise.dal.syntax import Insert
from txdav.common.datastore.sql import fixUUIDNormalization
+from uuid import UUID
+exampleUID = UUID("a" * 32)
+denormalizedUID = str(exampleUID)
+normalizedUID = denormalizedUID.upper()
+
class CommonSQLStoreTests(CommonCommonTests, TestCase):
"""
Tests for shared functionality in L{txdav.common.datastore.sql}.
@@ -46,6 +52,9 @@
"""
yield super(CommonSQLStoreTests, self).setUp()
self._sqlStore = yield buildStore(self, self.notifierFactory)
+ self._sqlStore.directoryService().addRecord(buildDirectoryRecord(denormalizedUID))
+ self._sqlStore.directoryService().addRecord(buildDirectoryRecord(normalizedUID))
+ self._sqlStore.directoryService().addRecord(buildDirectoryRecord("uid"))
def storeUnderTest(self):
@@ -421,10 +430,3 @@
yield fixUUIDNormalization(self.storeUnderTest())
self.assertEqual((yield self.allHomeUIDs(schema.ADDRESSBOOK_HOME)),
[[normalizedUID]])
-
-
-
-from uuid import UUID
-exampleUID = UUID("a" * 32)
-denormalizedUID = str(exampleUID)
-normalizedUID = denormalizedUID.upper()
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/util.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/util.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -122,30 +122,68 @@
implements(IStoreDirectoryRecord)
- def __init__(self, uid, shortNames, fullName):
+ def __init__(self, uid, shortNames, fullName, thisServer=True, server=None):
self.uid = uid
self.shortNames = shortNames
self.fullName = fullName
self.displayName = self.fullName if self.fullName else self.shortNames[0]
+ self._thisServer = thisServer
+ self._server = server
+ def thisServer(self):
+ return self._thisServer
+
+ def server(self):
+ return self._server
+
+
+
+def buildDirectory(homes=None):
+
+ directory = TestStoreDirectoryService()
+
+ # User accounts
+ for ctr in range(1, 100):
+ directory.addRecord(TestStoreDirectoryRecord(
+ "user%02d" % (ctr,),
+ ("user%02d" % (ctr,),),
+ "User %02d" % (ctr,),
+ ))
+
+ homes = set(homes) if homes is not None else set()
+ for uid in homes:
+ directory.addRecord(buildDirectoryRecord(uid))
+
+ return directory
+
+
+
+def buildDirectoryRecord(uid):
+ return TestStoreDirectoryRecord(
+ uid,
+ (uid,),
+ uid.capitalize(),
+ )
+
+
+
class SQLStoreBuilder(object):
"""
Test-fixture-builder which can construct a PostgresStore.
"""
- sharedService = None
- currentTestID = None
+ def __init__(self, secondary=False):
+ self.sharedService = None
+ self.currentTestID = None
+ self.sharedDBPath = "_test_sql_db" + str(os.getpid()) + ("-2" if secondary else "")
- SHARED_DB_PATH = "_test_sql_db" + str(os.getpid())
-
- @classmethod
- def createService(cls, serviceFactory):
+ def createService(self, serviceFactory):
"""
Create a L{PostgresService} to use for building a store.
"""
- dbRoot = CachingFilePath(cls.SHARED_DB_PATH)
+ dbRoot = CachingFilePath(self.sharedDBPath)
return PostgresService(
dbRoot, serviceFactory, current_sql_schema, resetSchema=True,
databaseName="caldav",
@@ -161,17 +199,15 @@
)
- @classmethod
- def childStore(cls):
+ def childStore(self):
"""
Create a store suitable for use in a child process, that is hooked up
to the store that a parent test process is managing.
"""
disableMemcacheForTest(TestCase())
staticQuota = 3000
- attachmentRoot = (CachingFilePath(cls.SHARED_DB_PATH)
- .child("attachments"))
- stubsvc = cls.createService(lambda cf: Service())
+ attachmentRoot = (CachingFilePath(self.sharedDBPath).child("attachments"))
+ stubsvc = self.createService(lambda cf: Service())
cp = ConnectionPool(stubsvc.produceConnection, maxConnections=1)
# Attach the service to the running reactor.
@@ -187,17 +223,17 @@
return cds
- def buildStore(self, testCase, notifierFactory, directoryService=None):
+ def buildStore(self, testCase, notifierFactory, directoryService=None, homes=None):
"""
Do the necessary work to build a store for a particular test case.
@return: a L{Deferred} which fires with an L{IDataStore}.
"""
disableMemcacheForTest(testCase)
- dbRoot = CachingFilePath(self.SHARED_DB_PATH)
+ dbRoot = CachingFilePath(self.sharedDBPath)
attachmentRoot = dbRoot.child("attachments")
if directoryService is None:
- directoryService = TestStoreDirectoryService()
+ directoryService = buildDirectory(homes=homes)
if self.sharedService is None:
ready = Deferred()
def getReady(connectionFactory, storageService):
@@ -244,8 +280,7 @@
attachmentRoot.createDirectory()
currentTestID = testCase.id()
- cp = ConnectionPool(self.sharedService.produceConnection,
- maxConnections=5)
+ cp = ConnectionPool(self.sharedService.produceConnection, maxConnections=5)
quota = deriveQuota(testCase)
store = CommonDataStore(
cp.connection,
@@ -307,6 +342,7 @@
theStoreBuilder = SQLStoreBuilder()
buildStore = theStoreBuilder.buildStore
+cleanStore = theStoreBuilder.cleanStore
_notSet = object()
@@ -678,13 +714,13 @@
@inlineCallbacks
- def homeUnderTest(self, txn=None, name="home1"):
+ def homeUnderTest(self, txn=None, name="home1", create=False):
"""
Get the calendar home detailed by C{requirements['home1']}.
"""
if txn is None:
txn = self.transactionUnderTest()
- returnValue((yield txn.calendarHomeWithUID(name)))
+ returnValue((yield txn.calendarHomeWithUID(name, create=create)))
@inlineCallbacks
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/upgrade/test/test_migrate.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/upgrade/test/test_migrate.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/upgrade/test/test_migrate.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -143,7 +143,17 @@
self.filesPath, {"push": StubNotifierFactory()}, TestStoreDirectoryService(), True, True
)
self.sqlStore = yield theStoreBuilder.buildStore(
- self, StubNotifierFactory()
+ self,
+ StubNotifierFactory(),
+ homes=(
+ "home1",
+ "home2",
+ "home3",
+ "home_defaults",
+ "home_no_splits",
+ "home_splits",
+ "home_splits_shared",
+ )
)
self.upgrader = UpgradeToDatabaseStep(self.fileStore, self.sqlStore)
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/icommondatastore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/icommondatastore.py 2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/icommondatastore.py 2013-12-11 22:16:01 UTC (rev 12051)
@@ -213,12 +213,28 @@
+class ShareNotAllowed(CommonStoreError):
+ """
+ An operation on a shared resource is not allowed.
+ """
+
+
+
class ExternalShareFailed(CommonStoreError):
"""
An external sharee operation failed.
"""
+
+class NonExistentExternalShare(CommonStoreError):
+ """
+ An external sharee operation failed because the share does not exist on the
+ other pod. The caller of the external request receiving this exception should
+ remove the local external share to "heal" this mismatch.
+ """
+
+
# Indexing / sync tokens
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/088bfe01/attachment.html>
More information about the calendarserver-changes
mailing list