[CalendarServer-changes] [14476] CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav

source_changes at macosforge.org source_changes at macosforge.org
Tue Feb 24 19:35:13 PST 2015


Revision: 14476
          http://trac.calendarserver.org//changeset/14476
Author:   cdaboo at apple.com
Date:     2015-02-24 19:35:13 -0800 (Tue, 24 Feb 2015)
Log Message:
-----------
Checkpoint: sharing state sync. Still have clean-up piece to do.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/base/datastore/util.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/test/test_sql_sharing.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/carddav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/sharing_invites.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_external.py
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_51_to_52.sql
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_51_to_52.sql
    CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_sharing.py

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/base/datastore/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/base/datastore/util.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/base/datastore/util.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -100,8 +100,8 @@
 
     # Home child objects by external id
 
-    def keyForObjectWithExternalID(self, homeResourceID, externalID):
-        return "objectWithExternalID:%s:%s" % (homeResourceID, externalID)
+    def keyForObjectWithBindUID(self, homeResourceID, bindUID):
+        return "objectWithBindUID:%s:%s" % (homeResourceID, bindUID)
 
 
     # Home metadata (Created/Modified)

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -28,7 +28,7 @@
     "CalendarObject",
 ]
 
-from twext.enterprise.dal.record import fromTable
+from twext.enterprise.dal.record import fromTable, SerializableRecord
 from twext.enterprise.dal.syntax import Count, ColumnSyntax, Delete, \
     Insert, Len, Max, Parameter, Select, Update, utcNowSQL
 from twext.enterprise.locking import NamedLock
@@ -401,6 +401,33 @@
 
 
 
+class CalendarHomeRecord(SerializableRecord, fromTable(schema.CALENDAR_HOME)):
+    """
+    @DynamicAttrs
+    L{Record} for L{schema.CALENDAR_HOME}.
+    """
+    pass
+
+
+
+class CalendarMetaDataRecord(SerializableRecord, fromTable(schema.CALENDAR_METADATA)):
+    """
+    @DynamicAttrs
+    L{Record} for L{schema.CALENDAR_METADATA}.
+    """
+    pass
+
+
+
+class CalendarBindRecord(SerializableRecord, fromTable(schema.CALENDAR_BIND)):
+    """
+    @DynamicAttrs
+    L{Record} for L{schema.CALENDAR_BIND}.
+    """
+    pass
+
+
+
 class CalendarHome(CommonHome):
 
     implements(ICalendarHome)
@@ -1002,6 +1029,12 @@
     _objectSchema = schema.CALENDAR_OBJECT
     _timeRangeSchema = schema.TIME_RANGE
 
+    _homeRecordClass = CalendarHomeRecord
+    _metadataRecordClass = CalendarMetaDataRecord
+    _bindRecordClass = CalendarBindRecord
+    _bindHomeIDAttributeName = "calendarHomeResourceID"
+    _bindResourceIDAttributeName = "calendarResourceID"
+
     # Mapping of iCalendar property name to DB column name
     _queryFields = {
         "UID": _objectSchema.UID,

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/test/test_sql_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/test/test_sql_sharing.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/test/test_sql_sharing.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -587,7 +587,42 @@
         yield self.commit()
 
 
+    @inlineCallbacks
+    def test_sharingBindRecords(self):
 
+        yield self.calendarUnderTest(home="user01", name="calendar")
+        yield self.commit()
+
+        shared_name = yield self._createShare()
+
+        shared = yield self.calendarUnderTest(home="user01", name="calendar")
+        results = yield shared.sharingBindRecords()
+        self.assertEqual(len(results), 1)
+        self.assertEqual(results.keys(), ["user02"])
+        self.assertEqual(results["user02"].calendarResourceName, shared_name)
+
+
+    @inlineCallbacks
+    def test_sharedToBindRecords(self):
+
+        yield self.calendarUnderTest(home="user01", name="calendar")
+        yield self.commit()
+
+        shared_name = yield self._createShare()
+
+        home = yield self.homeUnderTest(name="user02")
+        results = yield home.sharedToBindRecords()
+        self.assertEqual(len(results), 1)
+        self.assertEqual(results.keys(), ["user01"])
+        sharedRecord = results["user01"][0]
+        ownerRecord = results["user01"][1]
+        metadataRecord = results["user01"][2]
+        self.assertEqual(ownerRecord.calendarResourceName, "calendar")
+        self.assertEqual(sharedRecord.calendarResourceName, shared_name)
+        self.assertEqual(metadataRecord.supportedComponents, None)
+
+
+
 class GroupSharingTests(BaseSharingTests):
     """
     Test store-based group sharing.

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/carddav/datastore/sql.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/carddav/datastore/sql.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -462,7 +462,7 @@
 
     @classmethod
     @inlineCallbacks
-    def _getDBDataIndirect(cls, home, name, resourceID, externalID):
+    def _getDBDataIndirect(cls, home, name, resourceID, bindUID):
 
         # Get the bind row data
         row = None
@@ -492,7 +492,7 @@
         overallBindStatus = _BIND_STATUS_INVITED
         minBindRevision = None
         for row in rows:
-            bindMode, homeID, resourceGroupID, externalID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
+            homeID, resourceGroupID, name, bindMode, bindStatus, bindRevision, bindUID, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
             if groupID is None:
                 groupID = resourceGroupID
             minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
@@ -532,9 +532,9 @@
         returnValue((bindData, additionalBindData, metadataData, ownerHome,))
 
 
-    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=None):
+    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, bindUID=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, externalID=externalID)
+        super(AddressBook, self).__init__(home, name, resourceID, mode, status, revision=revision, message=message, ownerHome=ownerHome, ownerName=ownerName, bindUID=bindUID)
 
 
     def __repr__(self):
@@ -851,7 +851,7 @@
 
 
     @classmethod
-    def create(cls, home, name, externalID=None):
+    def create(cls, home, name, bindUID=None):
         if name == home.addressbook().name():
             # raise HomeChildNameAlreadyExistsError
             pass
@@ -1117,7 +1117,7 @@
             home._txn, homeID=home._resourceID
         )
         for groupRow in groupRows:
-            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            homeID, resourceID, bindName, bindMode, bindStatus, bindRevision, bindUID, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
             ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
             ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerAddressBookID)
             names |= set([ownerHome.uid()])
@@ -1145,7 +1145,7 @@
         )
         # get ownerHomeIDs
         for dataRow in dataRows:
-            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
+            homeID, resourceID, bindName, bindMode, bindStatus, bindRevision, bindUID, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
             ownerHome = yield home.ownerHomeWithChildID(resourceID)
             ownerHomeToDataRowMap[ownerHome] = dataRow
 
@@ -1154,12 +1154,16 @@
             home._txn, homeID=home._resourceID
         )
         for groupBindRow in groupBindRows:
-            bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            homeID, resourceID, name, bindMode, bindStatus, bindRevision, bindUID, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
             ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
             ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
             if ownerHome not in ownerHomeToDataRowMap:
-                groupBindRow[0] = _BIND_MODE_INDIRECT
-                groupBindRow[3:7] = 4 * [None]  # bindName, bindStatus, bindRevision, bindMessage
+                groupBindRow[cls.bindColumns().index(cls._bindSchema.BIND_MODE)] = _BIND_MODE_INDIRECT
+                groupBindRow[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] = None
+                groupBindRow[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)] = None
+                groupBindRow[cls.bindColumns().index(cls._bindSchema.BIND_REVISION)] = None
+                groupBindRow[cls.bindColumns().index(cls._bindSchema.BIND_UID)] = None
+                groupBindRow[cls.bindColumns().index(cls._bindSchema.MESSAGE)] = None
                 ownerHomeToDataRowMap[ownerHome] = groupBindRow
 
         if ownerHomeToDataRowMap:
@@ -1248,7 +1252,7 @@
 
     @classmethod
     @inlineCallbacks
-    def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
+    def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, bindUID=None, accepted=True):
         # replaces objectWithName()
         """
         Synthesize and indirect child for matching name or id based on whether shared groups exist.
@@ -1261,7 +1265,7 @@
             exists.
         """
 
-        dbData = yield cls._getDBDataIndirect(home, name, resourceID, externalID)
+        dbData = yield cls._getDBDataIndirect(home, name, resourceID, bindUID)
         if dbData is None:
             returnValue(None)
         bindData, additionalBindData, metadataData, ownerHome = dbData
@@ -1399,7 +1403,7 @@
             readWriteGroupIDs = set()
             readOnlyGroupIDs = set()
             for groupBindRow in groupBindRows:
-                bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+                homeID, resourceID, name, bindMode, bindStatus, bindRevision, bindUID, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
                 if bindMode == _BIND_MODE_WRITE:
                     readWriteGroupIDs.add(resourceID)
                 else:
@@ -1460,7 +1464,7 @@
         readWriteGroupIDs = []
         readOnlyGroupIDs = []
         for groupBindRow in groupBindRows:
-            bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            homeID, resourceID, name, bindMode, bindStatus, bindRevision, bindUID, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
             if bindMode == _BIND_MODE_WRITE:
                 readWriteGroupIDs.append(resourceID)
             else:
@@ -1578,10 +1582,10 @@
                 subt,
                 homeID=shareeHome._resourceID,
                 resourceID=self._resourceID,
-                externalID=None,
                 name=newName,
                 mode=mode,
                 bindStatus=status,
+                bindUID=None,
                 message=summary
             )
             returnValue(newName)
@@ -1896,7 +1900,7 @@
         yield child._loadPropertyStore(propstore)
 
         if groupBindData:
-            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindData[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            homeID, resourceID, bindName, bindMode, bindStatus, bindRevision, bindUID, bindMessage = groupBindData[:AddressBookObject.bindColumnCount] #@UnusedVariable
             child._bindMode = bindMode
             child._bindStatus = bindStatus
             child._bindMessage = bindMessage
@@ -1997,7 +2001,7 @@
         self._bindName = None
         self._bindRevision = None
         super(AddressBookObject, self).__init__(addressbook, name, uid, resourceID, options)
-        self._externalID = None
+        self._bindUID = None
         self._options = {} if options is None else options
 
 
@@ -2206,7 +2210,7 @@
         )
         if groupBindRows:
             groupBindRow = groupBindRows[0]
-            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            homeID, resourceID, bindName, bindMode, bindStatus, bindRevision, bindUID, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
 
             if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
                 returnValue(None)

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -22,7 +22,7 @@
 from txdav.caldav.icalendarstore import ComponentUpdateState
 from txdav.common.datastore.podding.migration.sync_metadata import CalendarMigrationRecord, \
     CalendarObjectMigrationRecord, AttachmentMigrationRecord
-from txdav.caldav.datastore.sql import ManagedAttachment
+from txdav.caldav.datastore.sql import ManagedAttachment, CalendarBindRecord
 from txdav.common.datastore.sql_external import NotificationCollectionExternal
 from txdav.common.datastore.sql_notification import NotificationCollection
 from txdav.common.datastore.sql_tables import _HOME_STATUS_EXTERNAL
@@ -176,19 +176,20 @@
         # TODO: Re-write attachment URIs - not sure if we need this as reverse proxy may take care of it
         pass
 
-        # TODO: group attendee reconcile
+        # Group attendee reconcile
         yield self.groupAttendeeReconcile()
 
-        # TODO: delegates reconcile
+        # Delegates reconcile
         yield self.delegateReconcile()
 
         # TODO: shared collections reconcile
-        pass
+        yield self.sharedByCollectionsReconcile()
+        yield self.sharedToCollectionsReconcile()
 
         # TODO: group sharee reconcile
         pass
 
-        # TODO: notifications
+        # Notifications
         yield self.notificationsReconcile()
 
         # TODO: work items
@@ -943,3 +944,160 @@
             # Do this via the "write" API so that sync revisions are updated properly, rather than just
             # inserting the records directly.
             yield notifications.writeNotificationObject(record.notificationUID, record.notificationType, record.notificationData)
+
+
+    @inlineCallbacks
+    def sharedByCollectionsReconcile(self):
+        """
+        Sync all the collections shared by the migrating user from the remote store. We will do this one calendar at a time since
+        there could be a large number of sharees per calendar.
+        """
+
+        calendars = yield self.getSyncState()
+
+        len_records = 0
+        for calendar in calendars.values():
+            records = yield self.sharedByCollectionRecords(calendar.remoteResourceID)
+            records = records.items()
+
+            # Batch setting resources for the local home
+            len_records += len(records)
+            while records:
+                yield self.makeSharedByCollections(records[:50], calendar.localResourceID)
+                records = records[50:]
+
+        returnValue(len_records)
+
+
+    @inTransactionWrapper
+    @inlineCallbacks
+    def sharedByCollectionRecords(self, txn, remote_id):
+        """
+        Get all the existing L{CalendarBindRecord}'s from the remote store.
+        """
+
+        remote_home = yield self._remoteHome(txn)
+        remote_calendar = yield remote_home.childWithID(remote_id)
+        records = yield remote_calendar.sharingBindRecords()
+        returnValue(records)
+
+
+    @inTransactionWrapper
+    @inlineCallbacks
+    def makeSharedByCollections(self, txn, records, calendar_id):
+        """
+        Create L{CalendarBindRecord} records in the local store.
+        """
+
+        for shareeUID, record in records:
+            shareeHome = yield txn.calendarHomeWithUID(shareeUID, create=True)
+
+            # First look for an existing record that could be present if the migrating user had
+            # previously shared with this sharee as a cross-pod share
+            oldrecord = yield CalendarBindRecord.querysimple(
+                txn,
+                calendarHomeResourceID=shareeHome.id(),
+                calendarResourceName=record.calendarResourceName,
+            )
+
+            # FIXME: need to figure out sync-token and bind revision changes
+
+            if oldrecord:
+                # Point old record to the new local calendar being shared
+                yield oldrecord[0].update(
+                    calendarResourceID=calendar_id,
+                    bindRevision=0,
+                )
+            else:
+                # Map the record resource ids and insert a new record
+                record.calendarHomeResourceID = shareeHome.id()
+                record.calendarResourceID = calendar_id
+                record.bindRevision = 0
+                yield record.insert(txn)
+
+
+    @inlineCallbacks
+    def sharedToCollectionsReconcile(self):
+        """
+        Sync all the collections shared to the migrating user from the remote store.
+        """
+        records = yield self.sharedToCollectionRecords()
+        records = records.items()
+        len_records = len(records)
+
+        while records:
+            yield self.makeSharedToCollections(records[:50])
+            records = records[50:]
+
+        returnValue(len_records)
+
+
+    @inTransactionWrapper
+    @inlineCallbacks
+    def sharedToCollectionRecords(self, txn):
+        """
+        Get the names and sharer UIDs for remote shared calendars.
+        """
+
+        # List of calendars from the remote side
+        home = yield self._remoteHome(txn)
+        if home is None:
+            returnValue(None)
+        results = yield home.sharedToBindRecords()
+        returnValue(results)
+
+
+    @inTransactionWrapper
+    @inlineCallbacks
+    def makeSharedToCollections(self, txn, records):
+        """
+        Create L{CalendarBindRecord} records in the local store.
+        """
+
+        for sharerUID, (shareeRecord, ownerRecord, metadataRecord) in records:
+            sharerHome = yield txn.calendarHomeWithUID(sharerUID, create=True)
+
+            # We need to figure out the right thing to do based on whether the sharer is local to this pod
+            # (the one where the migrated user will be hosted) vs located on another pod
+
+            if sharerHome.normal():
+                # First look for an existing record that must be present if the migrating user had
+                # previously been shared with by this sharee
+                oldrecord = yield CalendarBindRecord.querysimple(
+                    txn,
+                    calendarResourceName=shareeRecord.calendarResourceName,
+                )
+                if len(oldrecord) == 1:
+                    # Point old record to the new local calendar home
+                    yield oldrecord[0].update(
+                        calendarHomeResourceID=self.homeId,
+                    )
+                else:
+                    raise AssertionError("An existing share must be present")
+            else:
+                # We have an external user. That sharer may have already shared the calendar with some other user
+                # on this pod, in which case there is already a CALENDAR table entry for it, and we need the
+                # resource ID from that to use in the new CALENDAR_BIND record we create. If a pre-existing share
+                # is not present, then we have to create the CALENDAR table entry and associated pieces
+
+                # Look for pre-existing share with the same external ID
+                oldrecord = yield CalendarBindRecord.querysimple(
+                    txn,
+                    calendarHomeResourceID=sharerHome.id(),
+                    bindUID=ownerRecord.bindUID,
+                )
+                if oldrecord:
+                    # Map the record resource ids and insert a new record
+                    calendar_id = oldrecord.calendarResourceID
+                else:
+                    sharerView = yield sharerHome.createCollectionForExternalShare(
+                        ownerRecord.calendarResourceName,
+                        ownerRecord.bindUID,
+                        metadataRecord.supportedComponents,
+                    )
+                    calendar_id = sharerView.id()
+
+                shareeRecord.calendarHomeResourceID = self.homeId
+                shareeRecord.calendarResourceID = calendar_id
+                shareeRecord.bindRevision = 0
+                yield shareeRecord.insert(txn)

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -30,7 +30,8 @@
 from txdav.common.datastore.sql_directory import DelegateRecord, \
     ExternalDelegateGroupsRecord, DelegateGroupsRecord
 from txdav.common.datastore.sql_notification import NotificationCollection
-from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_EXTERNAL
+from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_EXTERNAL, \
+    _BIND_MODE_READ
 from txdav.common.datastore.test.util import populateCalendarsFrom
 from txdav.who.delegates import Delegates
 from txweb2.http_headers import MimeType
@@ -976,6 +977,81 @@
 
 
 
+class TestSharingSync(MultiStoreConduitTest):
+    """
+    Test that L{CrossPodHomeSync} sharing sync works.
+    """
+
+    @inlineCallbacks
+    def _createShare(self, shareFrom, shareTo):
+        # Invite
+        txnindex = 1 if shareFrom[0] == "p" else 0
+        home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(txnindex), name=shareFrom, create=True)
+        calendar = yield home.childWithName("calendar")
+        shareeView = yield calendar.inviteUIDToShare(shareTo, _BIND_MODE_READ, "summary")
+        inviteUID = shareeView.shareUID()
+        yield self.commitTransaction(txnindex)
+
+        # Accept
+        txnindex = 1 if shareTo[0] == "p" else 0
+        shareeHome = yield self.homeUnderTest(txn=self.theTransactionUnderTest(txnindex), name=shareTo)
+        shareeView = yield shareeHome.acceptShare(inviteUID)
+        sharedName = shareeView.name()
+        yield self.commitTransaction(txnindex)
+
+        returnValue(sharedName)
+
+
+    @inlineCallbacks
+    def test_shared_collections_reconcile(self):
+        """
+        Test that L{sharedCollectionsReconcile} copies over the full set of delegates and caches associated groups..
+        """
+
+        # Create home
+        yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True)
+        yield self.commitTransaction(0)
+
+        # Shared by migrating user
+        shared_name_02 = yield self._createShare("user01", "user02")
+        shared_name_03 = yield self._createShare("user01", "puser03")
+
+        # Shared to migrating user
+        shared_name_04 = yield self._createShare("user04", "user01")
+        shared_name_05 = yield self._createShare("puser05", "user01")
+
+        # Sync from remote side
+        syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01")
+        yield syncer.loadRecord()
+        yield syncer.sync()
+        changes = yield syncer.sharedByCollectionsReconcile()
+        self.assertEqual(changes, 2)
+        changes = yield syncer.sharedToCollectionsReconcile()
+        self.assertEqual(changes, 2)
+
+        # Local calendar exists with shares
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+        calendar1 = yield home1.childWithName("calendar")
+        invites1 = yield calendar1.sharingInvites()
+        self.assertEqual(len(invites1), 2)
+        self.assertEqual(set([invite.uid for invite in invites1]), set((shared_name_02, shared_name_03,)))
+        yield self.commitTransaction(1)
+
+        # Local sharee can access it
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name="puser03")
+        calendar1 = yield home1.childWithName(shared_name_03)
+        self.assertTrue(calendar1 is not None)
+
+        # Local shared calendars exist
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+        calendar1 = yield home1.childWithName(shared_name_04)
+        self.assertTrue(calendar1 is not None)
+        calendar1 = yield home1.childWithName(shared_name_05)
+        self.assertTrue(calendar1 is not None)
+        yield self.commitTransaction(1)
+
+
+
 class TestGroupAttendeeSync(MultiStoreConduitTest):
     """
     GroupAttendeeReconciliation tests

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/sharing_invites.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/sharing_invites.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/sharing_invites.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -26,7 +26,7 @@
     """
 
     @inlineCallbacks
-    def send_shareinvite(self, txn, homeType, ownerUID, ownerID, ownerName, shareeUID, shareUID, bindMode, summary, copy_properties, supported_components):
+    def send_shareinvite(self, txn, homeType, ownerUID, ownerName, shareeUID, shareUID, bindMode, bindUID, summary, copy_properties, supported_components):
         """
         Send a sharing invite cross-pod message.
 
@@ -34,8 +34,6 @@
         @type homeType: C{int}
         @param ownerUID: UID of the sharer.
         @type ownerUID: C{str}
-        @param ownerID: resource ID of the sharer calendar
-        @type ownerID: C{int}
         @param ownerName: owner's name of the sharer calendar
         @type ownerName: C{str}
         @param shareeUID: UID of the sharee
@@ -44,6 +42,8 @@
         @type shareUID: C{str}
         @param bindMode: bind mode for the share
         @type bindMode: C{str}
+        @param bindUID: bind UID of the sharer calendar
+        @type bindUID: C{str}
         @param summary: sharing message
         @type summary: C{str}
         @param copy_properties: C{str} name/value for properties to be copied
@@ -58,11 +58,11 @@
             "action": "shareinvite",
             "type": homeType,
             "owner": ownerUID,
-            "owner_id": ownerID,
             "owner_name": ownerName,
             "sharee": shareeUID,
             "share_id": shareUID,
             "mode": bindMode,
+            "bind_uid": bindUID,
             "summary": summary,
             "properties": copy_properties,
         }
@@ -89,10 +89,10 @@
         # Create a share
         yield shareeHome.processExternalInvite(
             request["owner"],
-            request["owner_id"],
             request["owner_name"],
             request["share_id"],
             request["mode"],
+            request["bind_uid"],
             request["summary"],
             request["properties"],
             supported_components=request.get("supported-components")
@@ -100,7 +100,7 @@
 
 
     @inlineCallbacks
-    def send_shareuninvite(self, txn, homeType, ownerUID, ownerID, shareeUID, shareUID):
+    def send_shareuninvite(self, txn, homeType, ownerUID, bindUID, shareeUID, shareUID):
         """
         Send a sharing uninvite cross-pod message.
 
@@ -108,8 +108,8 @@
         @type homeType: C{int}
         @param ownerUID: UID of the sharer.
         @type ownerUID: C{str}
-        @param ownerID: resource ID of the sharer calendar
-        @type ownerID: C{int}
+        @param bindUID: bind UID of the sharer calendar
+        @type bindUID: C{str}
         @param shareeUID: UID of the sharee
         @type shareeUID: C{str}
         @param shareUID: Resource/invite ID for sharee
@@ -122,7 +122,7 @@
             "action": "shareuninvite",
             "type": homeType,
             "owner": ownerUID,
-            "owner_id": ownerID,
+            "bind_uid": bindUID,
             "sharee": shareeUID,
             "share_id": shareUID,
         }
@@ -147,7 +147,7 @@
         # Remove a share
         yield shareeHome.processExternalUninvite(
             request["owner"],
-            request["owner_id"],
+            request["bind_uid"],
             request["share_id"],
         )
 

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -135,11 +135,28 @@
         """
         return [[a.serialize(), b.serialize(), ] for a, b in value]
 
+
+    @staticmethod
+    def _to_serialize_dict_value(value):
+        """
+        Convert the value to the external (JSON-based) representation.
+        """
+        return dict([(k, v.serialize(),) for k, v in value.items()])
+
+
+    @staticmethod
+    def _to_serialize_dict_list_serialized_value(value):
+        """
+        Convert the value to the external (JSON-based) representation.
+        """
+        return dict([(k, UtilityConduitMixin._to_serialize_list(v),) for k, v in value.items()])
+
 # These are the actions on store objects we need to expose via the conduit api
 
 # Calls on L{CommonHome} objects
 UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, "home_metadata", "serialize", classMethod=False)
 UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, "home_get_all_group_attendees", "getAllGroupAttendees", classMethod=False, transform_recv_result=StoreAPIConduitMixin._to_serialize_pair_list)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, "home_shared_to_records", "sharedToBindRecords", transform_recv_result=StoreAPIConduitMixin._to_serialize_dict_list_serialized_value)
 
 # Calls on L{CommonHomeChild} objects
 UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, "homechild_listobjects", "listObjects", classMethod=True)
@@ -150,6 +167,7 @@
 UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, "homechild_synctokenrevision", "syncTokenRevision")
 UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, "homechild_resourcenamessincerevision", "resourceNamesSinceRevision", transform_send_result=UtilityConduitMixin._to_tuple)
 UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, "homechild_search", "search")
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, "homechild_sharing_records", "sharingBindRecords", transform_recv_result=StoreAPIConduitMixin._to_serialize_dict_value)
 
 # Calls on L{CommonObjectResource} objects
 UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, "objectresource_loadallobjects", "loadAllObjects", classMethod=True, transform_recv_result=UtilityConduitMixin._to_serialize_list)

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -1885,6 +1885,15 @@
         return self._authzUID
 
 
+    def normal(self):
+        """
+        Is this an normal (internal) home.
+
+        @return: a L{bool}.
+        """
+        return self._status == _HOME_STATUS_NORMAL
+
+
     def external(self):
         """
         Is this an external home.
@@ -2106,15 +2115,15 @@
         return self._childClass.objectWithID(self, resourceID)
 
 
-    def childWithExternalID(self, externalID):
+    def childWithBindUID(self, bindUID):
         """
-        Retrieve the child with the given C{externalID} contained in this
+        Retrieve the child with the given C{bindUID} contained in this
         home.
 
         @param name: a string.
         @return: an L{ICalendar} or C{None} if no such child exists.
         """
-        return self._childClass.objectWithExternalID(self, externalID)
+        return self._childClass.objectWithBindUID(self, bindUID)
 
 
     def allChildWithID(self, resourceID):
@@ -2129,11 +2138,11 @@
 
 
     @inlineCallbacks
-    def createChildWithName(self, name, externalID=None):
+    def createChildWithName(self, name, bindUID=None):
         if name.startswith("."):
             raise HomeChildNameNotAllowedError(name)
 
-        child = yield self._childClass.create(self, name, externalID=externalID)
+        child = yield self._childClass.create(self, name, bindUID=bindUID)
         returnValue(child)
 
 
@@ -2706,9 +2715,13 @@
         Get the owner home for a shared child ID and the owner's name for that bound child.
         Subclasses may override.
         """
-        ownerHomeID, ownerName = (yield self._childClass._ownerHomeWithResourceID.on(self._txn, resourceID=resourceID))[0]
-        ownerHome = yield self._txn.homeWithResourceID(self._homeType, ownerHomeID)
-        returnValue((ownerHome, ownerName))
+        rows = yield self._childClass._ownerHomeWithResourceID.on(self._txn, resourceID=resourceID)
+        if rows:
+            ownerHomeID, ownerName = rows[0]
+            ownerHome = yield self._txn.homeWithResourceID(self._homeType, ownerHomeID)
+            returnValue((ownerHome, ownerName))
+        else:
+            returnValue((None, None))
 
 
 
@@ -2725,6 +2738,11 @@
     )
 
     _externalClass = None
+    _homeRecordClass = None
+    _metadataRecordClass = None
+    _bindRecordClass = None
+    _bindHomeIDAttributeName = None
+    _bindResourceIDAttributeName = None
     _objectResourceClass = None
 
     _bindSchema = None
@@ -2758,7 +2776,7 @@
         @rtype: L{CommonHomeChild}
         """
 
-        bindMode, _ignore_homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = bindData
+        _ignore_homeID, resourceID, name, bindMode, bindStatus, bindRevision, bindUID, bindMessage = bindData
 
         if ownerHome is None:
             if bindMode == _BIND_MODE_OWN:
@@ -2769,7 +2787,7 @@
         else:
             ownerName = None
 
-        c = cls._externalClass if ownerHome.externalClass() else cls
+        c = cls._externalClass if ownerHome and ownerHome.externalClass() else cls
         child = c(
             home=home,
             name=name,
@@ -2780,7 +2798,7 @@
             message=bindMessage,
             ownerHome=ownerHome,
             ownerName=ownerName,
-            externalID=externalID,
+            bindUID=bindUID,
         )
 
         if additionalBindData:
@@ -2793,7 +2811,7 @@
 
         # 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:
+        if ownerHome and propstore and bindMode != _BIND_MODE_OWN:
             propstore._setDefaultUserUID(ownerHome.uid())
         yield child._loadPropertyStore(propstore)
 
@@ -2802,10 +2820,10 @@
 
     @classmethod
     @inlineCallbacks
-    def _getDBData(cls, home, name, resourceID, externalID):
+    def _getDBData(cls, home, name, resourceID, bindUID):
         """
         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}.
+        L{name}, L{resourceID} or L{bindUID} is specified - others are C{None}.
 
         @param home: the parent home object
         @type home: L{CommonHome}
@@ -2813,8 +2831,8 @@
         @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}
+        @param bindUID: the unique ID of the external (cross-pod) referenced item
+        @type bindUID: C{int}
         """
 
         # Get the bind row data
@@ -2827,8 +2845,8 @@
                 cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
             elif resourceID:
                 cacheKey = queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID)
-            elif externalID:
-                cacheKey = queryCacher.keyForObjectWithExternalID(home._resourceID, externalID)
+            elif bindUID:
+                cacheKey = queryCacher.keyForObjectWithBindUID(home._resourceID, bindUID)
             row = yield queryCacher.get(cacheKey)
 
         if row is None:
@@ -2837,8 +2855,8 @@
                 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)
+            elif bindUID:
+                rows = yield cls._bindForBindUIDAndHomeID.on(home._txn, bindUID=bindUID, homeID=home._resourceID)
             row = rows[0] if rows else None
 
         if not row:
@@ -2848,7 +2866,7 @@
             # 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)
+            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithBindUID(home._resourceID, bindUID), row)
 
         bindData = row[:cls.bindColumnCount]
         additionalBindData = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
@@ -2871,15 +2889,15 @@
         returnValue((bindData, additionalBindData, metadataData,))
 
 
-    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=None):
+    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, bindUID=None):
 
         self._home = home
         self._name = name
         self._resourceID = resourceID
-        self._externalID = externalID
         self._bindMode = mode
         self._bindStatus = status
         self._bindRevision = revision
+        self._bindUID = bindUID
         self._bindMessage = message
         self._ownerHome = home if ownerHome is None else ownerHome
         self._ownerName = name if ownerName is None else ownerName
@@ -2943,9 +2961,10 @@
         # Load from the main table first
         dataRows = (yield cls._childrenAndMetadataForHomeID.on(home._txn, homeID=home._resourceID))
 
+        resourceID_index = cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)
         if dataRows:
             # Get property stores
-            childResourceIDs = [dataRow[2] for dataRow in dataRows]
+            childResourceIDs = [dataRow[resourceID_index] for dataRow in dataRows]
 
             propertyStores = yield PropertyStore.forMultipleResourcesWithResourceIDs(
                 home.uid(), None, None, home._txn, childResourceIDs
@@ -2958,7 +2977,7 @@
         # Create the actual objects merging in properties
         for dataRow in dataRows:
             bindData = dataRow[:cls.bindColumnCount]
-            resourceID = bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)]
+            resourceID = bindData[resourceID_index]
             additionalBindData = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
             metadataData = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
             propstore = propertyStores.get(resourceID, None)
@@ -2981,13 +3000,13 @@
 
 
     @classmethod
-    def objectWithExternalID(cls, home, externalID, accepted=True):
-        return cls.objectWith(home, externalID=externalID, accepted=accepted)
+    def objectWithBindUID(cls, home, bindUID, accepted=True):
+        return cls.objectWith(home, bindUID=bindUID, accepted=accepted)
 
 
     @classmethod
     @inlineCallbacks
-    def objectWith(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
+    def objectWith(cls, home, name=None, resourceID=None, bindUID=None, accepted=True):
         """
         Create the object using one of the specified arguments as the key to load it. One
         and only one of the keyword arguments must be set.
@@ -3007,7 +3026,7 @@
         @rtype: C{CommonHomeChild}
         """
 
-        dbData = yield cls._getDBData(home, name, resourceID, externalID)
+        dbData = yield cls._getDBData(home, name, resourceID, bindUID)
         if dbData is None:
             returnValue(None)
         bindData, additionalBindData, metadataData = dbData
@@ -3044,7 +3063,7 @@
 
     @classmethod
     @inlineCallbacks
-    def create(cls, home, name, externalID=None):
+    def create(cls, home, name, bindUID=None):
 
         if (yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)):
             raise HomeChildNameAlreadyExistsError(name)
@@ -3059,7 +3078,7 @@
         _created, _modified = (yield cls._insertHomeChildMetaData.on(home._txn, resourceID=resourceID))[0]
         # Bind table needs entry
         yield cls._bindInsertQuery.on(
-            home._txn, homeID=home._resourceID, resourceID=resourceID, externalID=externalID,
+            home._txn, homeID=home._resourceID, resourceID=resourceID, bindUID=bindUID,
             name=name, mode=_BIND_MODE_OWN, bindStatus=_BIND_STATUS_ACCEPTED,
             message=None,
         )
@@ -3096,15 +3115,6 @@
         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.

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_external.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_external.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -89,13 +89,13 @@
 
     @memoizedKey("name", "_children")
     @inlineCallbacks
-    def createChildWithName(self, name, externalID=None):
+    def createChildWithName(self, name, bindUID=None):
         """
         No real children - only external ones.
         """
-        if externalID is None:
+        if bindUID is None:
             raise AssertionError("CommonHomeExternal: not supported")
-        child = yield super(CommonHomeExternal, self).createChildWithName(name, externalID)
+        child = yield super(CommonHomeExternal, self).createChildWithName(name, bindUID)
         returnValue(child)
 
 
@@ -112,7 +112,7 @@
         Remove an external child. Check that it is invalid or unused before calling this because if there
         are valid references to it, removing will break things.
         """
-        if child._externalID is None:
+        if child._bindUID is None:
             raise AssertionError("CommonHomeExternal: not supported")
         yield super(CommonHomeExternal, self).removeChildWithName(child.name())
 
@@ -186,11 +186,17 @@
         raise AssertionError("CommonHomeExternal: not supported")
 
 
-#    def ownerHomeAndChildNameForChildID(self, resourceID):
-#        """
-#        No children.
-#        """
-#        raise AssertionError("CommonHomeExternal: not supported")
+    @inlineCallbacks
+    def sharedToBindRecords(self):
+        results = yield self._txn.store().conduit.send_home_shared_to_records(self)
+        returnValue(dict([(
+            k,
+            (
+                self._childClass._bindRecordClass.deserialize(v[0]),
+                self._childClass._bindRecordClass.deserialize(v[1]),
+                self._childClass._metadataRecordClass.deserialize(v[2]),
+            ),
+        ) for k, v in results.items()]))
 
 
 
@@ -225,8 +231,8 @@
 
     @classmethod
     @inlineCallbacks
-    def objectWith(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
-        mapping = yield home._txn.store().conduit.send_homechild_objectwith(home, name, resourceID, externalID, accepted)
+    def objectWith(cls, home, name=None, resourceID=None, bindUID=None, accepted=True):
+        mapping = yield home._txn.store().conduit.send_homechild_objectwith(home, name, resourceID, bindUID, accepted)
 
         if mapping:
             child = yield cls.deserialize(home, mapping)
@@ -351,7 +357,13 @@
         returnValue(results)
 
 
+    @inlineCallbacks
+    def sharingBindRecords(self):
+        results = yield self._txn.store().conduit.send_homechild_sharing_records(self)
+        returnValue(dict([(k, self._bindRecordClass.deserialize(v),) for k, v in results.items()]))
 
+
+
 class CommonObjectResourceExternal(CommonObjectResource):
     """
     A CommonObjectResource for a resource not hosted on this system, but on another pod. This will forward

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2015-02-25 03:35:13 UTC (rev 14476)
@@ -99,11 +99,11 @@
 create table CALENDAR_BIND (
     "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
     "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
-    "EXTERNAL_ID" integer default null,
     "CALENDAR_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
     "BIND_REVISION" integer default 0 not null,
+    "BIND_UID" nvarchar2(36) default null,
     "MESSAGE" nclob,
     "TRANSP" integer default 0 not null,
     "ALARM_VEVENT_TIMED" nclob default null,
@@ -277,11 +277,11 @@
 create table SHARED_ADDRESSBOOK_BIND (
     "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
     "OWNER_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
-    "EXTERNAL_ID" integer default null,
     "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
     "BIND_REVISION" integer default 0 not null,
+    "BIND_UID" nvarchar2(36) default null,
     "MESSAGE" nclob, 
     primary key ("ADDRESSBOOK_HOME_RESOURCE_ID", "OWNER_HOME_RESOURCE_ID"), 
     unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")
@@ -331,11 +331,11 @@
 create table SHARED_GROUP_BIND (
     "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
     "GROUP_RESOURCE_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
-    "EXTERNAL_ID" integer default null,
     "GROUP_ADDRESSBOOK_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
     "BIND_REVISION" integer default 0 not null,
+    "BIND_UID" nvarchar2(36) default null,
     "MESSAGE" nclob, 
     primary key ("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_RESOURCE_ID"), 
     unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_ADDRESSBOOK_NAME")

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql	2015-02-25 03:35:13 UTC (rev 14476)
@@ -190,11 +190,11 @@
 create table CALENDAR_BIND (
   CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
   CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
-  EXTERNAL_ID               integer      default null,
   CALENDAR_RESOURCE_NAME    varchar(255) not null,
   BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
   BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
   BIND_REVISION             integer      default 0 not null,
+  BIND_UID                  varchar(36)  default null,
   MESSAGE                   text,
   TRANSP                    integer      default 0 not null, -- enum CALENDAR_TRANSP
   ALARM_VEVENT_TIMED        text         default null,
@@ -502,11 +502,11 @@
 create table SHARED_ADDRESSBOOK_BIND (
   ADDRESSBOOK_HOME_RESOURCE_ID          integer         not null references ADDRESSBOOK_HOME,
   OWNER_HOME_RESOURCE_ID                integer         not null references ADDRESSBOOK_HOME on delete cascade,
-  EXTERNAL_ID                           integer         default null,
   ADDRESSBOOK_RESOURCE_NAME             varchar(255)    not null,
   BIND_MODE                             integer         not null, -- enum CALENDAR_BIND_MODE
   BIND_STATUS                           integer         not null, -- enum CALENDAR_BIND_STATUS
   BIND_REVISION                         integer         default 0 not null,
+  BIND_UID                              varchar(36)     default null,
   MESSAGE                               text,                     -- FIXME: xml?
 
   primary key (ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID), -- implicit index
@@ -603,11 +603,11 @@
 create table SHARED_GROUP_BIND (
   ADDRESSBOOK_HOME_RESOURCE_ID      integer      not null references ADDRESSBOOK_HOME,
   GROUP_RESOURCE_ID                 integer      not null references ADDRESSBOOK_OBJECT on delete cascade,
-  EXTERNAL_ID                       integer      default null,
   GROUP_ADDRESSBOOK_NAME            varchar(255) not null,
   BIND_MODE                         integer      not null, -- enum CALENDAR_BIND_MODE
   BIND_STATUS                       integer      not null, -- enum CALENDAR_BIND_STATUS
   BIND_REVISION                     integer      default 0 not null,
+  BIND_UID                          varchar(36)  default null,
   MESSAGE                           text,                  -- FIXME: xml?
 
   primary key (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_RESOURCE_ID), -- implicit index

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_51_to_52.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_51_to_52.sql	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_51_to_52.sql	2015-02-25 03:35:13 UTC (rev 14476)
@@ -21,6 +21,20 @@
 -- New status value
 insert into HOME_STATUS (DESCRIPTION, ID) values ('migrating', 3);
 
+-- Change columns
+alter table CALENDAR_BIND
+	drop column EXTERNAL_ID
+	add ("BIND_UID" nvarchar2(36) default null);
+
+alter table SHARED_ADDRESSBOOK_BIND
+	drop column EXTERNAL_ID
+	add ("BIND_UID" nvarchar2(36) default null);
+
+alter table SHARED_GROUP_BIND
+	drop column EXTERNAL_ID
+	add ("BIND_UID" nvarchar2(36) default null);
+
+
 -- New table
 create table CALENDAR_MIGRATION (
     "CALENDAR_HOME_RESOURCE_ID" integer references CALENDAR_HOME on delete cascade,

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_51_to_52.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_51_to_52.sql	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_51_to_52.sql	2015-02-25 03:35:13 UTC (rev 14476)
@@ -21,6 +21,20 @@
 -- New status value
 insert into HOME_STATUS values (3, 'migrating');
 
+-- Change columns
+alter table CALENDAR_BIND
+	drop column EXTERNAL_ID,
+	add column BIND_UID varchar(36) default null;
+
+alter table SHARED_ADDRESSBOOK_BIND
+	drop column EXTERNAL_ID,
+	add column BIND_UID varchar(36) default null;
+
+alter table SHARED_GROUP_BIND
+	drop column EXTERNAL_ID,
+	add column BIND_UID varchar(36) default null;
+
+
 -- New table
 create table CALENDAR_MIGRATION (
   CALENDAR_HOME_RESOURCE_ID		integer references CALENDAR_HOME on delete cascade,

Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_sharing.py	2015-02-24 23:14:43 UTC (rev 14475)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_sharing.py	2015-02-25 03:35:13 UTC (rev 14476)
@@ -78,7 +78,7 @@
     #
     @inlineCallbacks
     def processExternalInvite(
-        self, ownerUID, ownerRID, ownerName, shareUID, bindMode, summary,
+        self, ownerUID, ownerName, shareUID, bindMode, bindUID, summary,
         copy_invite_properties, supported_components=None
     ):
         """
@@ -93,42 +93,10 @@
             raise ExternalShareFailed("Invalid owner UID: {}".format(ownerUID))
 
         # Try to find owner calendar via its external id
-        ownerView = yield ownerHome.childWithExternalID(ownerRID)
+        ownerView = yield ownerHome.childWithBindUID(bindUID)
         if ownerView is None:
-            try:
-                ownerView = yield ownerHome.createChildWithName(
-                    ownerName, externalID=ownerRID
-                )
-            except HomeChildNameAlreadyExistsError:
-                # This is odd - it means we possibly have a left over sharer
-                # collection which the sharer likely removed and re-created
-                # with the same name but now it has a different externalID and
-                # is not found by the initial query. What we do is check to see
-                # whether any shares still reference the old ID - if they do we
-                # are hosed. If not, we can remove the old item and create a new one.
-                oldOwnerView = yield ownerHome.childWithName(ownerName)
-                invites = yield oldOwnerView.sharingInvites()
-                if len(invites) != 0:
-                    log.error(
-                        "External invite collection name is present with a "
-                        "different externalID and still has shares"
-                    )
-                    raise
-                log.error(
-                    "External invite collection name is present with a "
-                    "different externalID - trying to fix"
-                )
-                yield ownerHome.removeExternalChild(oldOwnerView)
-                ownerView = yield ownerHome.createChildWithName(
-                    ownerName, externalID=ownerRID
-                )
+            ownerView = yield ownerHome.createCollectionForExternalShare(ownerName, bindUID, supported_components)
 
-            if (
-                supported_components is not None and
-                hasattr(ownerView, "setSupportedComponents")
-            ):
-                yield ownerView.setSupportedComponents(supported_components)
-
         # Now carry out the share operation
         if bindMode == _BIND_MODE_DIRECT:
             shareeView = yield ownerView.directShareWithUser(
@@ -143,7 +111,7 @@
 
 
     @inlineCallbacks
-    def processExternalUninvite(self, ownerUID, ownerRID, shareUID):
+    def processExternalUninvite(self, ownerUID, bindUID, shareUID):
         """
         External invite received.
         """
@@ -154,7 +122,7 @@
             raise ExternalShareFailed("Invalid owner UID: {}".format(ownerUID))
 
         # Try to find owner calendar via its external id
-        ownerView = yield ownerHome.childWithExternalID(ownerRID)
+        ownerView = yield ownerHome.childWithBindUID(bindUID)
         if ownerView is None:
             raise ExternalShareFailed("Invalid share ID: {}".format(shareUID))
 
@@ -200,7 +168,111 @@
                 yield shareeHome.declineShare(shareUID)
 
 
+    @inlineCallbacks
+    def createCollectionForExternalShare(self, name, bindUID, supported_components):
+        """
+        Create the L{CommonHomeChild} object that used as a "stub" to represent the external
+        object on the other pod for the sharer.
 
+        @param name: name of the collection
+        @type name: L{str}
+        @param bindUID: id on other pod
+        @type bindUID: L{str}
+        @param supported_components: optional set of support components
+        @type supported_components: L{str}
+        """
+        try:
+            ownerView = yield self.createChildWithName(
+                name, bindUID=bindUID
+            )
+        except HomeChildNameAlreadyExistsError:
+            # This is odd - it means we possibly have a left over sharer
+            # collection which the sharer likely removed and re-created
+            # with the same name but now it has a different bindUID and
+            # is not found by the initial query. What we do is check to see
+            # whether any shares still reference the old ID - if they do we
+            # are hosed. If not, we can remove the old item and create a new one.
+            oldOwnerView = yield self.childWithName(name)
+            invites = yield oldOwnerView.sharingInvites()
+            if len(invites) != 0:
+                log.error(
+                    "External invite collection name is present with a "
+                    "different bindUID and still has shares"
+                )
+                raise
+            log.error(
+                "External invite collection name is present with a "
+                "different bindUID - trying to fix"
+            )
+            yield self.removeExternalChild(oldOwnerView)
+            ownerView = yield self.createChildWithName(
+                name, bindUID=bindUID
+            )
+
+        if (
+            supported_components is not None and
+            hasattr(ownerView, "setSupportedComponents")
+        ):
+            yield ownerView.setSupportedComponents(supported_components)
+
+        returnValue(ownerView)
+
+
+    @inlineCallbacks
+    def sharedToBindRecords(self):
+        """
+        Return an L{dict} that maps home/directory uid to a sharing bind record for collections shared to this user.
+        """
+
+        # Get shared to bind records
+        records = yield self._childClass._bindRecordClass.query(
+            self._txn,
+            (getattr(self._childClass._bindRecordClass, self._childClass._bindHomeIDAttributeName) == self.id()).And(
+                self._childClass._bindRecordClass.bindMode != _BIND_MODE_OWN
+            )
+        )
+        records = dict([(getattr(record, self._childClass._bindResourceIDAttributeName), record) for record in records])
+        if not records:
+            returnValue({})
+
+        # Look up the owner records for each of the shared to records
+        ownerRecords = yield self._childClass._bindRecordClass.query(
+            self._txn,
+            (getattr(self._childClass._bindRecordClass, self._childClass._bindResourceIDAttributeName).In(records.keys())).And(
+                self._childClass._bindRecordClass.bindMode == _BIND_MODE_OWN
+            )
+        )
+
+        # Important - this method is called when migrating shared-to records to some other pod. For that to work all the
+        # owner records must have a bindUID assigned to them. Normally bindUIDs are assigned the first time an external
+        # share is created, but migration will implicitly create the external share
+        for ownerRecord in ownerRecords:
+            if not ownerRecord.bindUID:
+                yield ownerRecord.update(bindUID=str(uuid4()))
+
+        ownerRecords = dict([(getattr(record, self._childClass._bindResourceIDAttributeName), record) for record in ownerRecords])
+
+        # Look up the metadata records for each of the shared to records
+        metadataRecords = yield self._childClass._metadataRecordClass.query(
+            self._txn,
+            self._childClass._metadataRecordClass.resourceID.In(records.keys()),
+        )
+        metadataRecords = dict([(record.resourceID, record) for record in metadataRecords])
+
+        # Map the owner records to home ownerUIDs
+        homeIDs = dict([(
+            getattr(record, self._childClass._bindHomeIDAttributeName), getattr(record, self._childClass._bindResourceIDAttributeName)
+        ) for record in ownerRecords.values()])
+        homes = yield self._childClass._homeRecordClass.query(
+            self._txn,
+            self._childClass._homeRecordClass.resourceID.In(homeIDs.keys()),
+        )
+        homeMap = dict((homeIDs[home.resourceID], home.ownerUID,) for home in homes)
+
+        returnValue(dict([(homeMap[calendarID], (records[calendarID], ownerRecords[calendarID], metadataRecords[calendarID],),) for calendarID in records]))
+
+
+
 SharingInvitation = namedtuple(
     "SharingInvitation",
     ["uid", "ownerUID", "ownerHomeID", "shareeUID", "shareeHomeID", "mode", "status", "summary"]
@@ -223,10 +295,10 @@
         return Insert({
             bind.HOME_RESOURCE_ID: Parameter("homeID"),
             bind.RESOURCE_ID: Parameter("resourceID"),
-            bind.EXTERNAL_ID: Parameter("externalID"),
             bind.RESOURCE_NAME: Parameter("name"),
             bind.BIND_MODE: Parameter("mode"),
             bind.BIND_STATUS: Parameter("bindStatus"),
+            bind.BIND_UID: Parameter("bindUID"),
             bind.MESSAGE: Parameter("message"),
         })
 
@@ -309,13 +381,13 @@
 
 
     @classproperty
-    def _bindForExternalIDAndHomeID(cls):
+    def _bindForBindUIDAndHomeID(cls):
         """
         DAL query that looks up home bind rows by home child
         resource ID and home resource ID.
         """
         bind = cls._bindSchema
-        return cls._bindFor((bind.EXTERNAL_ID == Parameter("externalID"))
+        return cls._bindFor((bind.BIND_UID == Parameter("bindUID"))
                             .And(bind.HOME_RESOURCE_ID == Parameter("homeID")))
 
 
@@ -580,15 +652,24 @@
     @inlineCallbacks
     def _sendExternalInvite(self, shareeView):
 
+        # Must make sure this collection has a BIND_UID assigned
+        if not self._bindUID:
+            self._bindUID = str(uuid4())
+            yield self._updateBindColumnsQuery({self._bindSchema.BIND_UID: self._bindUID}).on(
+                self._txn,
+                resourceID=self.id(), homeID=self.ownerHome().id()
+            )
+
+        # Now send the invite
         yield self._txn.store().conduit.send_shareinvite(
             self._txn,
             shareeView.ownerHome()._homeType,
             shareeView.ownerHome().uid(),
-            self.id(),
             self.shareName(),
             shareeView.viewerHome().uid(),
             shareeView.shareUID(),
             shareeView.shareMode(),
+            self.bindUID(),
             shareeView.shareMessage(),
             self.getInviteCopyProperties(),
             supported_components=self.getSupportedComponents() if hasattr(self, "getSupportedComponents") else None,
@@ -602,7 +683,7 @@
             self._txn,
             shareeView.ownerHome()._homeType,
             shareeView.ownerHome().uid(),
-            self.id(),
+            self.bindUID(),
             shareeView.viewerHome().uid(),
             shareeView.shareUID(),
         )
@@ -723,10 +804,10 @@
                 subt,
                 homeID=shareeHome._resourceID,
                 resourceID=self._resourceID,
-                externalID=self._externalID,
                 name=newName,
                 mode=mode,
                 bindStatus=status,
+                bindUID=None,
                 message=summary
             )
             returnValue(newName)
@@ -939,6 +1020,28 @@
 
 
     @inlineCallbacks
+    def sharingBindRecords(self):
+        """
+        Return an L{dict} that maps home/directory uid to a sharing bind record.
+        """
+        if not self.owned():
+            returnValue({})
+
+        records = yield self._bindRecordClass.querysimple(
+            self._txn,
+            **{self._bindResourceIDAttributeName: self.id()}
+        )
+        homeIDs = [getattr(record, self._bindHomeIDAttributeName) for record in records]
+        homes = yield self._homeRecordClass.query(
+            self._txn,
+            self._homeRecordClass.resourceID.In(homeIDs),
+        )
+        homeMap = dict((home.resourceID, home.ownerUID,) for home in homes)
+
+        returnValue(dict([(homeMap[getattr(record, self._bindHomeIDAttributeName)], record,) for record in records if record.bindMode != _BIND_MODE_OWN]))
+
+
+    @inlineCallbacks
     def _initBindRevision(self):
         yield self.syncToken() # init self._syncTokenRevision if None
         self._bindRevision = self._syncTokenRevision
@@ -1086,6 +1189,13 @@
         return self._bindStatus
 
 
+    def bindUID(self):
+        """
+        @see: L{ICalendar.bindUID}
+        """
+        return self._bindUID
+
+
     def accepted(self):
         """
         @see: L{ICalendar.shareStatus}
@@ -1159,13 +1269,13 @@
         """
 
         return (
-            cls._bindSchema.BIND_MODE,
             cls._bindSchema.HOME_RESOURCE_ID,
             cls._bindSchema.RESOURCE_ID,
-            cls._bindSchema.EXTERNAL_ID,
             cls._bindSchema.RESOURCE_NAME,
+            cls._bindSchema.BIND_MODE,
             cls._bindSchema.BIND_STATUS,
             cls._bindSchema.BIND_REVISION,
+            cls._bindSchema.BIND_UID,
             cls._bindSchema.MESSAGE
         )
 
@@ -1179,13 +1289,13 @@
         """
 
         return (
-            "_bindMode",
             "_homeResourceID",
             "_resourceID",
-            "_externalID",
             "_name",
+            "_bindMode",
             "_bindStatus",
             "_bindRevision",
+            "_bindUID",
             "_bindMessage",
         )
 
@@ -1251,4 +1361,4 @@
             yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForHomeChildMetaData(self._resourceID))
             yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithName(self._home._resourceID, self._name))
             yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithResourceID(self._home._resourceID, self._resourceID))
-            yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithExternalID(self._home._resourceID, self._externalID))
+            yield queryCacher.invalidateAfterCommit(self._txn, queryCacher.keyForObjectWithBindUID(self._home._resourceID, self._bindUID))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150224/050a09cc/attachment-0001.html>


More information about the calendarserver-changes mailing list