<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[14412] CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/14412">14412</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2015-02-13 13:21:26 -0800 (Fri, 13 Feb 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Checkpoint: migration final sync attachment links.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcaldavdatastoresqlpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcaldavdatastoresql_externalpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql_external.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcaldavdatastoreutilpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/util.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingattachmentspy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/attachments.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingconduitpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/conduit.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationhome_syncpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationtesttest_home_syncpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingrequestpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/request.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingresourcepy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/resource.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingstore_apipy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingtesttest_conduitpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/test_conduit.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingtestutilpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/util.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingutilpy">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/util.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcaldavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -143,7 +143,7 @@
</span><span class="cx">         @type txn: L{txdav.common.datastore.sql.CommonStoreTransaction}
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        at = schema.ATTACHMENT
</del><ins>+        at = Attachment._attachmentSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             (at.DROPBOX_ID,),
</span><span class="cx">             From=at,
</span><span class="lines">@@ -175,8 +175,8 @@
</span><span class="cx">         txn = self._store.newTransaction(&quot;CalendarStoreFeatures.upgradeToManagedAttachments - preliminary work&quot;)
</span><span class="cx">         try:
</span><span class="cx">             # Clear out unused CALENDAR_OBJECT.DROPBOX_IDs
</span><del>-            co = schema.CALENDAR_OBJECT
-            at = schema.ATTACHMENT
</del><ins>+            co = CalendarObject._objectSchema
+            at = Attachment._attachmentSchema
</ins><span class="cx">             yield Update(
</span><span class="cx">                 {co.DROPBOX_ID: None},
</span><span class="cx">                 Where=co.RESOURCE_ID.In(Select(
</span><span class="lines">@@ -249,7 +249,7 @@
</span><span class="cx">         log.debug(&quot;  {0} affected calendar objects&quot;.format(len(cobjs),))
</span><span class="cx"> 
</span><span class="cx">         # Get names of each matching attachment
</span><del>-        at = schema.ATTACHMENT
</del><ins>+        at = Attachment._attachmentSchema
</ins><span class="cx">         names = (yield Select(
</span><span class="cx">             (at.PATH,),
</span><span class="cx">             From=at,
</span><span class="lines">@@ -318,8 +318,8 @@
</span><span class="cx">         @type dropbox_id: C{str}
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        co = schema.CALENDAR_OBJECT
-        cb = schema.CALENDAR_BIND
</del><ins>+        co = CalendarObject._objectSchema
+        cb = Calendar._bindSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             (cb.CALENDAR_HOME_RESOURCE_ID, co.CALENDAR_RESOURCE_ID, co.RESOURCE_ID,),
</span><span class="cx">             From=co.join(cb, co.CALENDAR_RESOURCE_ID == cb.CALENDAR_RESOURCE_ID),
</span><span class="lines">@@ -511,7 +511,7 @@
</span><span class="cx">         assert self._txn._migrating
</span><span class="cx"> 
</span><span class="cx">         # Simple attributes that can be copied over as-is
</span><del>-        chm = schema.CALENDAR_HOME_METADATA
</del><ins>+        chm = self._homeMetaDataSchema
</ins><span class="cx">         values = {
</span><span class="cx">             chm.ALARM_VEVENT_TIMED : other._alarm_vevent_timed,
</span><span class="cx">             chm.ALARM_VEVENT_ALLDAY : other._alarm_vevent_allday,
</span><span class="lines">@@ -617,8 +617,8 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Implement lookup via queries.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        co = schema.CALENDAR_OBJECT
-        cb = schema.CALENDAR_BIND
</del><ins>+        co = self._objectSchema
+        cb = self._bindSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [co.PARENT_RESOURCE_ID,
</span><span class="cx">              co.RESOURCE_ID],
</span><span class="lines">@@ -637,16 +637,22 @@
</span><span class="cx">         returnValue(None)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    @inlineCallbacks
</del><span class="cx">     def getAllAttachments(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Return all the L{Attachment} objects associated with this calendar home.
</span><span class="cx">         Needed during migration.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        attachments = yield Attachment.loadAllAttachments(self)
-        returnValue(attachments)
</del><ins>+        return Attachment.loadAllAttachments(self)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def getAttachmentLinks(self):
+        &quot;&quot;&quot;
+        Read the attachment&lt;-&gt;calendar object mapping data associated with this calendar home.
+        Needed during migration only.
+        &quot;&quot;&quot;
+        return AttachmentLink.linksForHome(self)
+
+
</ins><span class="cx">     def getAttachmentByID(self, id):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Return a specific attachment associated with this calendar home.
</span><span class="lines">@@ -657,8 +663,8 @@
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def getAllDropboxIDs(self):
</span><del>-        co = schema.CALENDAR_OBJECT
-        cb = schema.CALENDAR_BIND
</del><ins>+        co = self._objectSchema
+        cb = self._bindSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [co.DROPBOX_ID],
</span><span class="cx">             From=co.join(cb, co.PARENT_RESOURCE_ID == cb.RESOURCE_ID),
</span><span class="lines">@@ -671,7 +677,7 @@
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def getAllAttachmentNames(self):
</span><del>-        att = schema.ATTACHMENT
</del><ins>+        att = Attachment._attachmentSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [att.DROPBOX_ID],
</span><span class="cx">             From=att,
</span><span class="lines">@@ -683,8 +689,8 @@
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def getAllManagedIDs(self):
</span><del>-        at = schema.ATTACHMENT
-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        at = Attachment._attachmentSchema
+        attco = Attachment._attachmentLinkSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [attco.MANAGED_ID, ],
</span><span class="cx">             From=attco.join(at, attco.ATTACHMENT_ID == at.ATTACHMENT_ID),
</span><span class="lines">@@ -1565,7 +1571,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Query to find resources that need to be re-expanded
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        co = schema.CALENDAR_OBJECT
</del><ins>+        co = cls._objectSchema
</ins><span class="cx">         return Select(
</span><span class="cx">             [co.RESOURCE_NAME],
</span><span class="cx">             From=co,
</span><span class="lines">@@ -3598,7 +3604,7 @@
</span><span class="cx">                 recurrenceLowerLimit = None
</span><span class="cx">                 recurrenceLimit = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
</span><span class="cx"> 
</span><del>-        co = schema.CALENDAR_OBJECT
</del><ins>+        co = self._objectSchema
</ins><span class="cx">         tr = schema.TIME_RANGE
</span><span class="cx"> 
</span><span class="cx">         # Do not update if reCreate (re-indexing - we don't want to re-write data
</span><span class="lines">@@ -3809,7 +3815,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         assert self._txn._migrating
</span><span class="cx"> 
</span><del>-        co = schema.CALENDAR_OBJECT
</del><ins>+        co = self._objectSchema
</ins><span class="cx">         values = {
</span><span class="cx">             co.ATTACHMENTS_MODE                : other._attachment,
</span><span class="cx">             co.DROPBOX_ID                      : other._dropboxID,
</span><span class="lines">@@ -4058,7 +4064,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         DAL query to load RECURRANCE_MIN, RECURRANCE_MAX via an object's resource ID.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        co = schema.CALENDAR_OBJECT
</del><ins>+        co = cls._objectSchema
</ins><span class="cx">         return Select(
</span><span class="cx">             [co.RECURRANCE_MIN, co.RECURRANCE_MAX, ],
</span><span class="cx">             From=co,
</span><span class="lines">@@ -4593,8 +4599,8 @@
</span><span class="cx">         Get a list of managed attachments where the names returned are for the last path segment
</span><span class="cx">         of the attachment URI.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        at = schema.ATTACHMENT
-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        at = Attachment._attachmentSchema
+        attco = Attachment._attachmentLinkSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [attco.MANAGED_ID, at.PATH, ],
</span><span class="cx">             From=attco.join(at, attco.ATTACHMENT_ID == at.ATTACHMENT_ID),
</span><span class="lines">@@ -4610,8 +4616,8 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Scan all the associated attachments for the one that matches
</span><del>-        at = schema.ATTACHMENT
-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        at = Attachment._attachmentSchema
+        attco = Attachment._attachmentLinkSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [attco.MANAGED_ID, at.PATH, ],
</span><span class="cx">             From=attco.join(at, attco.ATTACHMENT_ID == at.ATTACHMENT_ID),
</span><span class="lines">@@ -5050,32 +5056,136 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+class AttachmentLink(object):
+    &quot;&quot;&quot;
+    A binding between an L{Attachment} and an L{CalendarObject}.
+    &quot;&quot;&quot;
+
+    _attachmentSchema = schema.ATTACHMENT
+    _attachmentLinkSchema = schema.ATTACHMENT_CALENDAR_OBJECT
+
+    @classmethod
+    def makeClass(cls, txn, linkData):
+        &quot;&quot;&quot;
+        Given the various database rows, build the actual class.
+
+        @param objectData: the standard set of object columns
+        @type objectData: C{list}
+
+        @return: the constructed child class
+        @rtype: L{CommonHomeChild}
+        &quot;&quot;&quot;
+
+        child = cls(txn)
+        for attr, value in zip(child._rowAttributes(), linkData):
+            setattr(child, attr, value)
+        return child
+
+
+    @classmethod
+    def _allColumns(cls):
+        &quot;&quot;&quot;
+        Full set of columns in the object table that need to be loaded to
+        initialize the object resource state.
+        &quot;&quot;&quot;
+        aco = cls._attachmentLinkSchema
+        return [
+            aco.ATTACHMENT_ID,
+            aco.MANAGED_ID,
+            aco.CALENDAR_OBJECT_RESOURCE_ID,
+        ]
+
+
+    @classmethod
+    def _rowAttributes(cls):
+        &quot;&quot;&quot;
+        Object attributes used to store the column values from L{_allColumns}. This used to create
+        a mapping when serializing the object for cross-pod requests.
+        &quot;&quot;&quot;
+        return (
+            &quot;_attachmentID&quot;,
+            &quot;_managedID&quot;,
+            &quot;_calendarObjectID&quot;,
+        )
+
+
+    @classmethod
+    @inlineCallbacks
+    def linksForHome(cls, home):
+        &quot;&quot;&quot;
+        Load all attachment&lt;-&gt;calendar object mappings for the specified home collection.
+        &quot;&quot;&quot;
+
+        # Load from the main table first
+        att = cls._attachmentSchema
+        attco = cls._attachmentLinkSchema
+        dataRows = yield Select(
+            cls._allColumns(),
+            From=attco.join(att, on=(attco.ATTACHMENT_ID == att.ATTACHMENT_ID)),
+            Where=att.CALENDAR_HOME_RESOURCE_ID == home.id(),
+        ).on(home._txn)
+
+        # Create the actual objects
+        returnValue([cls.makeClass(home._txn, row) for row in dataRows])
+
+
+    def __init__(self, txn):
+        self._txn = txn
+        for attr in self._rowAttributes():
+            setattr(self, attr, None)
+
+
+    def externalize(self):
+        &quot;&quot;&quot;
+        Create a dictionary mapping key attributes so this object can be sent over a cross-pod call
+        and reconstituted at the other end. Note that the other end may have a different schema so
+        the attributes may not match exactly and will need to be processed accordingly.
+        &quot;&quot;&quot;
+        return dict([(attr[1:], getattr(self, attr, None)) for attr in self._rowAttributes()])
+
+
+    @classmethod
+    def internalize(cls, txn, mapping):
+        &quot;&quot;&quot;
+        Given a mapping generated by L{externalize}, convert the values into an array of database
+        like items that conforms to the ordering of L{_allColumns} so it can be fed into L{makeClass}.
+        Note that there may be a schema mismatch with the external data, so treat missing items as
+        C{None} and ignore extra items.
+        &quot;&quot;&quot;
+
+        return cls.makeClass(txn, [mapping.get(row[1:]) for row in cls._rowAttributes()])
+
+
+    def insert(self):
+        &quot;&quot;&quot;
+        Insert the object.
+        &quot;&quot;&quot;
+
+        row = dict([(column, getattr(self, attr)) for column, attr in itertools.izip(self._allColumns(), self._rowAttributes())])
+        return Insert(row).on(self._txn)
+
+
+
</ins><span class="cx"> class Attachment(object):
</span><span class="cx"> 
</span><span class="cx">     implements(IAttachment)
</span><span class="cx"> 
</span><span class="cx">     _attachmentSchema = schema.ATTACHMENT
</span><ins>+    _attachmentLinkSchema = schema.ATTACHMENT_CALENDAR_OBJECT
</ins><span class="cx"> 
</span><span class="cx">     @classmethod
</span><span class="cx">     def makeClass(cls, txn, attachmentData):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Given the various database rows, build the actual class.
</span><span class="cx"> 
</span><del>-        @param parent: the parent collection object
-        @type parent: L{CommonHomeChild}
</del><ins>+        @param attachmentData: the standard set of attachment columns
+        @type attachmentData: C{list}
</ins><span class="cx"> 
</span><del>-        @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}
-
</del><span class="cx">         @return: the constructed child class
</span><del>-        @rtype: L{CommonHomeChild}
</del><ins>+        @rtype: L{Attachment}
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        att = schema.ATTACHMENT
</del><ins>+        att = cls._attachmentSchema
</ins><span class="cx">         dropbox_id = attachmentData[cls._allColumns().index(att.DROPBOX_ID)]
</span><span class="cx">         c = ManagedAttachment if dropbox_id == &quot;.&quot; else DropBoxAttachment
</span><span class="cx">         child = c(
</span><span class="lines">@@ -5233,7 +5343,7 @@
</span><span class="cx"> 
</span><span class="cx">         @return: C{True} if this attachment exists, C{False} otherwise.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        att = schema.ATTACHMENT
</del><ins>+        att = self._attachmentSchema
</ins><span class="cx">         if self._dropboxID and self._dropboxID != &quot;.&quot;:
</span><span class="cx">             where = (att.DROPBOX_ID == self._dropboxID).And(
</span><span class="cx">                 att.PATH == self._name)
</span><span class="lines">@@ -5255,7 +5365,6 @@
</span><span class="cx">         returnValue(self)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    @inlineCallbacks
</del><span class="cx">     def copyRemote(self, remote):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Copy properties from a remote (external) attachment that is being migrated.
</span><span class="lines">@@ -5263,7 +5372,7 @@
</span><span class="cx">         @param remote: the external attachment
</span><span class="cx">         @type remote: L{Attachment}
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        yield self.changed(remote.contentType(), remote.name(), remote.md5(), remote.size())
</del><ins>+        return self.changed(remote.contentType(), remote.name(), remote.md5(), remote.size())
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def id(self):
</span><span class="lines">@@ -5361,8 +5470,8 @@
</span><span class="cx"> 
</span><span class="cx">         TODO: this needs to be transactional wrt the actual file deletes.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        att = schema.ATTACHMENT
-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        att = cls._attachmentSchema
+        attco = cls._attachmentLinkSchema
</ins><span class="cx"> 
</span><span class="cx">         rows = (yield Select(
</span><span class="cx">             [att.ATTACHMENT_ID, att.DROPBOX_ID, ],
</span><span class="lines">@@ -5448,7 +5557,7 @@
</span><span class="cx">             raise AttachmentDropboxNotAllowed
</span><span class="cx"> 
</span><span class="cx">         # Now create the DB entry
</span><del>-        att = schema.ATTACHMENT
</del><ins>+        att = cls._attachmentSchema
</ins><span class="cx">         rows = (yield Insert({
</span><span class="cx">             att.CALENDAR_HOME_RESOURCE_ID : ownerHomeID,
</span><span class="cx">             att.DROPBOX_ID                : dropboxID,
</span><span class="lines">@@ -5500,7 +5609,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # See if any other resources still reference this dropbox ID
</span><del>-        co = schema.CALENDAR_OBJECT
</del><ins>+        co = CalendarObject._objectSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [co.RESOURCE_ID, ],
</span><span class="cx">             From=co,
</span><span class="lines">@@ -5510,7 +5619,7 @@
</span><span class="cx"> 
</span><span class="cx">         if not rows:
</span><span class="cx">             # Find each attachment with matching dropbox ID
</span><del>-            att = schema.ATTACHMENT
</del><ins>+            att = cls._attachmentSchema
</ins><span class="cx">             rows = (yield Select(
</span><span class="cx">                 [att.PATH],
</span><span class="cx">                 From=att,
</span><span class="lines">@@ -5532,7 +5641,7 @@
</span><span class="cx">         self._md5 = md5
</span><span class="cx">         self._size = size
</span><span class="cx"> 
</span><del>-        att = schema.ATTACHMENT
</del><ins>+        att = self._attachmentSchema
</ins><span class="cx">         self._created, self._modified = map(
</span><span class="cx">             sqltime,
</span><span class="cx">             (yield Update(
</span><span class="lines">@@ -5559,7 +5668,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Change the DROPBOX_ID to a single &quot;.&quot; to indicate a managed attachment.
</span><del>-        att = schema.ATTACHMENT
</del><ins>+        att = self._attachmentSchema
</ins><span class="cx">         (yield Update(
</span><span class="cx">             {att.DROPBOX_ID    : &quot;.&quot;, },
</span><span class="cx">             Where=(att.ATTACHMENT_ID == self._attachmentID),
</span><span class="lines">@@ -5613,7 +5722,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Now create the DB entry
</span><del>-        att = schema.ATTACHMENT
</del><ins>+        att = cls._attachmentSchema
</ins><span class="cx">         rows = (yield Insert({
</span><span class="cx">             att.CALENDAR_HOME_RESOURCE_ID : ownerHomeID,
</span><span class="cx">             att.DROPBOX_ID                : &quot;.&quot;,
</span><span class="lines">@@ -5663,7 +5772,7 @@
</span><span class="cx">         attachment._objectResourceID = referencedBy
</span><span class="cx"> 
</span><span class="cx">         # Create the attachment&lt;-&gt;calendar object relationship for managed attachments
</span><del>-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        attco = cls._attachmentLinkSchema
</ins><span class="cx">         yield Insert({
</span><span class="cx">             attco.ATTACHMENT_ID               : attachment._attachmentID,
</span><span class="cx">             attco.MANAGED_ID                  : attachment._managedID,
</span><span class="lines">@@ -5698,7 +5807,7 @@
</span><span class="cx">         attachment._objectResourceID = referencedBy
</span><span class="cx"> 
</span><span class="cx">         # Update the attachment&lt;-&gt;calendar object relationship for managed attachments
</span><del>-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        attco = cls._attachmentLinkSchema
</ins><span class="cx">         yield Update(
</span><span class="cx">             {
</span><span class="cx">                 attco.ATTACHMENT_ID    : attachment._attachmentID,
</span><span class="lines">@@ -5732,7 +5841,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         if managedID:
</span><del>-            attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+            attco = cls._attachmentLinkSchema
</ins><span class="cx">             where = (attco.MANAGED_ID == managedID)
</span><span class="cx">             if referencedID is not None:
</span><span class="cx">                 where = where.And(attco.CALENDAR_OBJECT_RESOURCE_ID == referencedID)
</span><span class="lines">@@ -5760,7 +5869,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Find all the calendar object resourceIds referenced by this supplied managed-id.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        attco = cls._attachmentLinkSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [attco.CALENDAR_OBJECT_RESOURCE_ID, ],
</span><span class="cx">             From=attco,
</span><span class="lines">@@ -5776,9 +5885,9 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Return the &quot;owner&quot; home and referencing resource is, and UID for a managed-id.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        att = schema.ATTACHMENT
-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
-        co = schema.CALENDAR_OBJECT
</del><ins>+        att = cls._attachmentSchema
+        attco = cls._attachmentLinkSchema
+        co = CalendarObject._objectSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [
</span><span class="cx">                 att.CALENDAR_HOME_RESOURCE_ID,
</span><span class="lines">@@ -5801,7 +5910,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Find all reference attachment-ids and dereference
</span><del>-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        attco = cls._attachmentLinkSchema
</ins><span class="cx">         rows = (yield Select(
</span><span class="cx">             [attco.MANAGED_ID, ],
</span><span class="cx">             From=attco,
</span><span class="lines">@@ -5821,7 +5930,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Find the associated attachment-id and insert new reference
</span><del>-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        attco = cls._attachmentLinkSchema
</ins><span class="cx">         aid = (yield Select(
</span><span class="cx">             [attco.ATTACHMENT_ID, ],
</span><span class="cx">             From=attco,
</span><span class="lines">@@ -5897,7 +6006,7 @@
</span><span class="cx">         self._name = dispositionName
</span><span class="cx">         self._md5 = md5
</span><span class="cx">         self._size = size
</span><del>-        att = schema.ATTACHMENT
</del><ins>+        att = self._attachmentSchema
</ins><span class="cx">         self._created, self._modified = map(
</span><span class="cx">             sqltime,
</span><span class="cx">             (yield Update(
</span><span class="lines">@@ -5926,7 +6035,7 @@
</span><span class="cx">         @rtype: L{ManagedAttachment}
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        attco = self._attachmentLinkSchema
</ins><span class="cx">         yield Insert({
</span><span class="cx">             attco.ATTACHMENT_ID               : self._attachmentID,
</span><span class="cx">             attco.MANAGED_ID                  : self._managedID,
</span><span class="lines">@@ -5941,7 +6050,7 @@
</span><span class="cx">     def removeFromResource(self, resourceID):
</span><span class="cx"> 
</span><span class="cx">         # Delete the reference
</span><del>-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
</del><ins>+        attco = self._attachmentLinkSchema
</ins><span class="cx">         yield Delete(
</span><span class="cx">             From=attco,
</span><span class="cx">             Where=(attco.ATTACHMENT_ID == self._attachmentID).And(
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcaldavdatastoresql_externalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql_external.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql_external.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql_external.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -23,7 +23,7 @@
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> 
</span><span class="cx"> from txdav.caldav.datastore.sql import CalendarHome, Calendar, CalendarObject, \
</span><del>-    Attachment
</del><ins>+    Attachment, AttachmentLink
</ins><span class="cx"> from txdav.caldav.icalendarstore import ComponentUpdateState, ComponentRemoveState
</span><span class="cx"> from txdav.common.datastore.sql_external import CommonHomeExternal, CommonHomeChildExternal, \
</span><span class="cx">     CommonObjectResourceExternal
</span><span class="lines">@@ -68,14 +68,10 @@
</span><span class="cx">         Return all the L{Attachment} objects associated with this calendar home.
</span><span class="cx">         Needed during migration.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        raw_results = yield self._txn.store().conduit.send_get_all_attachments(self)
</del><ins>+        raw_results = yield self._txn.store().conduit.send_home_get_all_attachments(self)
+        returnValue([Attachment.internalize(self._txn, attachment) for attachment in raw_results])
</ins><span class="cx"> 
</span><del>-        results = []
-        for attachment in raw_results:
-            results.append(Attachment.internalize(self._txn, attachment))
-        returnValue(results)
</del><span class="cx"> 
</span><del>-
</del><span class="cx">     @inlineCallbacks
</span><span class="cx">     def readAttachmentData(self, remote_id, attachment):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -86,6 +82,16 @@
</span><span class="cx">         yield self._txn.store().conduit.send_get_attachment_data(self, remote_id, stream)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def getAttachmentLinks(self):
+        &quot;&quot;&quot;
+        Read the attachment&lt;-&gt;calendar object mapping data associated with this calendar home.
+        Needed during migration only.
+        &quot;&quot;&quot;
+        raw_results = yield self._txn.store().conduit.send_home_get_attachment_links(self)
+        returnValue([AttachmentLink.internalize(self._txn, attachment) for attachment in raw_results])
+
+
</ins><span class="cx">     def getAllDropboxIDs(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         No children.
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcaldavdatastoreutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/util.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/util.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/util.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -522,6 +522,11 @@
</span><span class="cx">             self._contentType = http_headers.MimeType.fromString(getType(self._attachment.name(), self.contentTypes))
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def resetDetails(self, contentType, dispositionName):
+        self._contentType = contentType
+        self._dispositionName = dispositionName
+
+
</ins><span class="cx">     def write(self, data):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Children must override this to actually write the data, but should
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingattachmentspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/attachments.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/attachments.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/attachments.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -16,6 +16,7 @@
</span><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> from txdav.caldav.icalendarstore import InvalidAttachmentOperation
</span><ins>+from txdav.common.datastore.podding.util import UtilityConduitMixin
</ins><span class="cx"> from txweb2.http_headers import generateContentType
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -155,36 +156,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def send_get_all_attachments(self, home):
-        &quot;&quot;&quot;
-        Managed attachment removeAttachment call.
-
-        @param home: the home whose attachments are being requested
-        @type home: L{CalendarHome}
-        &quot;&quot;&quot;
-
-        actionName = &quot;get-all-attachments&quot;
-        txn, request, server = yield self._getRequestForStoreObject(actionName, home, False)
-
-        response = yield self.sendRequestToServer(txn, server, request)
-        returnValue(response)
-
-
-    @inlineCallbacks
-    def recv_get_all_attachments(self, txn, request):
-        &quot;&quot;&quot;
-        Process an getAllAttachments cross-pod request. Request arguments as per L{send_get_all_attachments}.
-
-        @param request: request arguments
-        @type request: C{dict}
-        &quot;&quot;&quot;
-
-        home, _ignore = yield self._getStoreObjectForRequest(txn, request)
-        attachments = yield home.getAllAttachments()
-        returnValue([attachment.externalize() for attachment in attachments])
-
-
-    @inlineCallbacks
</del><span class="cx">     def send_get_attachment_data(self, home, attachment_id, stream):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Managed attachment readAttachmentData call. We are using streams on the sender and the receiver
</span><span class="lines">@@ -222,3 +193,8 @@
</span><span class="cx"> 
</span><span class="cx">         attachment.retrieve(stream)
</span><span class="cx">         returnValue((generateContentType(attachment.contentType()), attachment.name(),))
</span><ins>+
+
+# Calls on L{CommonHome} objects
+UtilityConduitMixin._make_simple_action(AttachmentsConduitMixin, &quot;home_get_all_attachments&quot;, &quot;getAllAttachments&quot;, classMethod=False, transform_recv_result=UtilityConduitMixin._to_externalize_list)
+UtilityConduitMixin._make_simple_action(AttachmentsConduitMixin, &quot;home_get_attachment_links&quot;, &quot;getAttachmentLinks&quot;, classMethod=False, transform_recv_result=UtilityConduitMixin._to_externalize_list)
</ins></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingconduitpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/conduit.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/conduit.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/conduit.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -20,9 +20,10 @@
</span><span class="cx"> from txdav.common.datastore.podding.attachments import AttachmentsConduitMixin
</span><span class="cx"> from txdav.common.datastore.podding.base import FailedCrossPodRequestError
</span><span class="cx"> from txdav.common.datastore.podding.directory import DirectoryPoddingConduitMixin
</span><del>-from txdav.common.datastore.podding.store_api import StoreAPIConduitMixin
</del><span class="cx"> from txdav.common.datastore.podding.request import ConduitRequest
</span><span class="cx"> from txdav.common.datastore.podding.sharing_invites import SharingInvitesConduitMixin
</span><ins>+from txdav.common.datastore.podding.store_api import StoreAPIConduitMixin
+from txdav.common.datastore.podding.util import UtilityConduitMixin
</ins><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> from twisted.python.reflect import namedClass
</span><span class="lines">@@ -32,6 +33,7 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class PoddingConduit(
</span><ins>+    UtilityConduitMixin,
</ins><span class="cx">     StoreAPIConduitMixin,
</span><span class="cx">     AttachmentsConduitMixin,
</span><span class="cx">     SharingInvitesConduitMixin,
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationhome_syncpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -162,10 +162,7 @@
</span><span class="cx">         # TODO: sync attachments
</span><span class="cx">         yield self.syncAttachments()
</span><span class="cx"> 
</span><del>-        # TODO: group attendee/sharee reconcile
-        pass
</del><span class="cx"> 
</span><del>-
</del><span class="cx">     @inlineCallbacks
</span><span class="cx">     def finalSync(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -174,14 +171,20 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # TODO: link attachments to resources: ATTACHMENT_CALENDAR_OBJECT table
</span><del>-        pass
</del><ins>+        yield self.linkAttachments()
</ins><span class="cx"> 
</span><span class="cx">         # TODO: Re-write attachment URIs - not sure if we need this as reverse proxy may take care of it
</span><span class="cx">         pass
</span><span class="cx"> 
</span><ins>+        # TODO: group attendee reconcile
+        pass
+
</ins><span class="cx">         # TODO: shared collections reconcile
</span><span class="cx">         pass
</span><span class="cx"> 
</span><ins>+        # TODO: group sharee reconcile
+        pass
+
</ins><span class="cx">         # TODO: delegates reconcile
</span><span class="cx">         pass
</span><span class="cx"> 
</span><span class="lines">@@ -273,7 +276,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         from txdav.caldav.datastore.sql_external import CalendarHomeExternal
</span><del>-        resourceID = yield txn.store().conduit.send_home_resource_id(self, self.record)
</del><ins>+        resourceID = yield txn.store().conduit.send_home_resource_id(txn, self.record)
</ins><span class="cx">         home = CalendarHomeExternal(txn, self.record.uid, resourceID) if resourceID is not None else None
</span><span class="cx">         if home:
</span><span class="cx">             home._childClass = home._childClass._externalClass
</span><span class="lines">@@ -754,3 +757,82 @@
</span><span class="cx"> 
</span><span class="cx">         # Read the data from the conduit
</span><span class="cx">         yield remote_home.readAttachmentData(remote_id, attachment)
</span><ins>+
+
+    @inlineCallbacks
+    def linkAttachments(self):
+        &quot;&quot;&quot;
+        Link attachments to the calendar objects they belong to.
+        &quot;&quot;&quot;
+
+        # Get the map of links for the remote home
+        links = yield self.getAttachmentLinks()
+
+        # Get remote-&gt;local ID mappings
+        attachmentIDMap, objectIDMap = yield self.getAttachmentMappings()
+
+        # Batch setting links for the local home
+        len_links = len(links)
+        while len(links):
+            yield self.makeAttachmentLinks(links[:50], attachmentIDMap, objectIDMap)
+            links = links[50:]
+
+        returnValue(len_links)
+
+
+    @inTransactionWrapper
+    @inlineCallbacks
+    def getAttachmentLinks(self, txn):
+        &quot;&quot;&quot;
+        Get the remote link information.
+        &quot;&quot;&quot;
+
+        # Get the map of links for the remote home
+        remote_home = yield self._remoteHome(txn)
+        links = yield remote_home.getAttachmentLinks()
+        returnValue(links)
+
+
+    @inTransactionWrapper
+    @inlineCallbacks
+    def getAttachmentMappings(self, txn):
+        &quot;&quot;&quot;
+        Get the remote link information.
+        &quot;&quot;&quot;
+
+        # Get migration mappings
+        am = schema.ATTACHMENT_MIGRATION
+        rows = yield Select(
+            [am.REMOTE_RESOURCE_ID, am.LOCAL_RESOURCE_ID],
+            From=am,
+            Where=(am.CALENDAR_HOME_RESOURCE_ID == self.homeId),
+        ).on(txn)
+        attachmentIDMap = dict(rows)
+
+        com = schema.CALENDAR_OBJECT_MIGRATION
+        rows = yield Select(
+            [com.REMOTE_RESOURCE_ID, com.LOCAL_RESOURCE_ID],
+            From=com,
+            Where=(com.CALENDAR_HOME_RESOURCE_ID == self.homeId),
+        ).on(txn)
+        objectIDMap = dict(rows)
+
+        returnValue((attachmentIDMap, objectIDMap,))
+
+
+    @inTransactionWrapper
+    @inlineCallbacks
+    def makeAttachmentLinks(self, txn, links, attachmentIDMap, objectIDMap):
+        &quot;&quot;&quot;
+        Map remote links to local links.
+        &quot;&quot;&quot;
+
+        for link in links:
+            # Remote link has an invalid txn at this point so replace that first
+            link._txn = txn
+
+            # Now re-map the attachment ID and calendar_object_id to the local ones
+            link._attachmentID = attachmentIDMap[link._attachmentID]
+            link._calendarObjectID = objectIDMap[link._calendarObjectID]
+
+            yield link.insert()
</ins></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingmigrationtesttest_home_syncpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -18,6 +18,7 @@
</span><span class="cx"> from twext.enterprise.dal.syntax import Select
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks
</span><span class="cx"> from twistedcaldav.ical import Component, normalize_iCalStr
</span><ins>+from txdav.caldav.datastore.sql import ManagedAttachment
</ins><span class="cx"> from txdav.common.datastore.podding.migration.home_sync import CrossPodHomeSync
</span><span class="cx"> from txdav.common.datastore.podding.test.util import MultiStoreConduitTest
</span><span class="cx"> from txdav.common.datastore.sql_tables import schema
</span><span class="lines">@@ -588,3 +589,100 @@
</span><span class="cx">         attachments = yield home1.getAllAttachments()
</span><span class="cx">         mapping1 = dict([(o.md5(), o.id()) for o in attachments])
</span><span class="cx">         yield _checkAttachmentObjectMigrationState(home1, mapping1)
</span><ins>+
+
+    @inlineCallbacks
+    def test_link_attachments(self):
+        &quot;&quot;&quot;
+        Test that L{linkAttachments} links attachment data to the associated calendar object.
+        &quot;&quot;&quot;
+
+        home0 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name=&quot;user01&quot;, create=True)
+        calendar0 = yield home0.childWithName(&quot;calendar&quot;)
+        object0_1 = yield calendar0.createCalendarObjectWithName(&quot;1.ics&quot;, Component.fromString(self.caldata1))
+        object0_2 = yield calendar0.createCalendarObjectWithName(&quot;2.ics&quot;, Component.fromString(self.caldata2))
+        yield calendar0.createCalendarObjectWithName(&quot;3.ics&quot;, Component.fromString(self.caldata3))
+        remote_id = calendar0.id()
+
+        attachment, _ignore_location = yield object0_1.addAttachment(None, MimeType.fromString(&quot;text/plain&quot;), &quot;test.txt&quot;, MemoryStream(&quot;Here is some text #1.&quot;))
+        id0_1 = attachment.id()
+        md50_1 = attachment.md5()
+        managedid0_1 = attachment.managedID()
+        pathID0_1 = ManagedAttachment.lastSegmentOfUriPath(managedid0_1, attachment.name())
+
+        attachment, _ignore_location = yield object0_2.addAttachment(None, MimeType.fromString(&quot;text/plain&quot;), &quot;test2.txt&quot;, MemoryStream(&quot;Here is some text #2.&quot;))
+        id0_2 = attachment.id()
+        md50_2 = attachment.md5()
+        managedid0_2 = attachment.managedID()
+        pathID0_2 = ManagedAttachment.lastSegmentOfUriPath(managedid0_2, attachment.name())
+
+        yield self.commitTransaction(0)
+
+        # Add original to a different resource
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home=&quot;user01&quot;, calendar_name=&quot;calendar&quot;, name=&quot;1.ics&quot;)
+        component = yield object1.componentForUser()
+        attach = component.mainComponent().getProperty(&quot;ATTACH&quot;)
+
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home=&quot;user01&quot;, calendar_name=&quot;calendar&quot;, name=&quot;3.ics&quot;)
+        component = yield object1.componentForUser()
+        attach = component.mainComponent().addProperty(attach)
+        yield object1.setComponent(component)
+        yield self.commitTransaction(0)
+
+        syncer = CrossPodHomeSync(self.theStoreUnderTest(1), &quot;user01&quot;)
+        yield syncer.loadRecord()
+        syncer.homeId = yield syncer.prepareCalendarHome()
+
+        # Trigger sync of the one calendar
+        local_sync_state = {}
+        remote_sync_state = yield syncer.getCalendarSyncList()
+        yield syncer.syncCalendar(
+            remote_id,
+            local_sync_state,
+            remote_sync_state,
+        )
+        self.assertEqual(len(local_sync_state), 1)
+        self.assertEqual(local_sync_state[remote_id].lastSyncToken, remote_sync_state[remote_id].lastSyncToken)
+
+        # Sync attachments
+        changed, removed = yield syncer.syncAttachments()
+        self.assertEqual(changed, set((id0_1, id0_2,)))
+        self.assertEqual(removed, set())
+
+        # Link attachments
+        len_links = yield syncer.linkAttachments()
+        self.assertEqual(len_links, 3)
+
+        # Local calendar exists
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+        calendar1 = yield home1.childWithName(&quot;calendar&quot;)
+        self.assertTrue(calendar1 is not None)
+        children = yield calendar1.objectResources()
+        self.assertEqual(set([child.name() for child in children]), set((&quot;1.ics&quot;, &quot;2.ics&quot;, &quot;3.ics&quot;,)))
+
+        # Make sure calendar object is associated with attachment
+        object1 = yield calendar1.objectResourceWithName(&quot;1.ics&quot;)
+        attachments = yield object1.managedAttachmentList()
+        self.assertEqual(attachments, [pathID0_1, ])
+
+        attachment = yield object1.attachmentWithManagedID(managedid0_1)
+        self.assertTrue(attachment is not None)
+        self.assertEqual(attachment.md5(), md50_1)
+
+        # Make sure calendar object is associated with attachment
+        object1 = yield calendar1.objectResourceWithName(&quot;2.ics&quot;)
+        attachments = yield object1.managedAttachmentList()
+        self.assertEqual(attachments, [pathID0_2, ])
+
+        attachment = yield object1.attachmentWithManagedID(managedid0_2)
+        self.assertTrue(attachment is not None)
+        self.assertEqual(attachment.md5(), md50_2)
+
+        # Make sure calendar object is associated with attachment
+        object1 = yield calendar1.objectResourceWithName(&quot;3.ics&quot;)
+        attachments = yield object1.managedAttachmentList()
+        self.assertEqual(attachments, [pathID0_1, ])
+
+        attachment = yield object1.attachmentWithManagedID(managedid0_1)
+        self.assertTrue(attachment is not None)
+        self.assertEqual(attachment.md5(), md50_1)
</ins></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingrequestpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/request.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/request.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/request.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -79,7 +79,6 @@
</span><span class="cx">                     data = json.loads(data)
</span><span class="cx">                 else:
</span><span class="cx">                     yield readStream(response.stream, self.writeStream.write)
</span><del>-                    yield self.writeStream.loseConnection()
</del><span class="cx">                     content_type = response.headers.getHeader(&quot;content-type&quot;)
</span><span class="cx">                     if content_type is None:
</span><span class="cx">                         content_type = MimeType(&quot;application&quot;, &quot;octet-stream&quot;)
</span><span class="lines">@@ -88,7 +87,9 @@
</span><span class="cx">                         filename = &quot;&quot;
</span><span class="cx">                     else:
</span><span class="cx">                         filename = content_disposition.params[&quot;filename&quot;]
</span><del>-                    response = {
</del><ins>+                    self.writeStream.resetDetails(content_type, filename)
+                    yield self.writeStream.loseConnection()
+                    data = {
</ins><span class="cx">                         &quot;result&quot;: &quot;ok&quot;,
</span><span class="cx">                         &quot;content-type&quot;: content_type,
</span><span class="cx">                         &quot;name&quot;: filename,
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingresourcepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/resource.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/resource.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/resource.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -177,7 +177,7 @@
</span><span class="cx">                 except ValueError:
</span><span class="cx">                     code = responsecode.BAD_REQUEST
</span><span class="cx">                 else:
</span><del>-                    headers = {&quot;content-type&quot;: ct}
</del><ins>+                    headers = {&quot;content-type&quot;: MimeType.fromString(ct)}
</ins><span class="cx">                     headers[&quot;content-disposition&quot;] = MimeDisposition(&quot;attachment&quot;, params={&quot;filename&quot;: name})
</span><span class="cx">                     returnValue(Response(responsecode.OK, headers, stream))
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingstore_apipy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/store_api.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -16,8 +16,8 @@
</span><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> 
</span><del>-from txdav.common.datastore.podding.base import FailedCrossPodRequestError
</del><span class="cx"> from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
</span><ins>+from txdav.common.datastore.podding.util import UtilityConduitMixin
</ins><span class="cx"> 
</span><span class="cx"> from twistedcaldav.caldavxml import TimeRange
</span><span class="cx"> 
</span><span class="lines">@@ -27,115 +27,7 @@
</span><span class="cx">     Defines common cross-pod API for generic access to remote resources.
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-    #
-    # Utility methods to map from store objects to/from JSON
-    #
-
</del><span class="cx">     @inlineCallbacks
</span><del>-    def _getRequestForStoreObject(self, action, storeObject, classMethod):
-        &quot;&quot;&quot;
-        Create the JSON data needed to identify the remote resource by type and ids, along with any parent resources.
-
-        @param action: the conduit action name
-        @type action: L{str}
-        @param storeObject: the store object that is being operated on
-        @type storeObject: L{object}
-        @param classMethod: indicates whether the method being called is a classmethod
-        @type classMethod: L{bool}
-
-        @return: the transaction in use, the JSON dict to send in the request,
-            the server where the request should be sent
-        @rtype: L{tuple} of (L{CommonStoreTransaction}, L{dict}, L{str})
-        &quot;&quot;&quot;
-
-        from txdav.common.datastore.sql import CommonObjectResource, CommonHomeChild, CommonHome
-        result = {
-            &quot;action&quot;: action,
-        }
-
-        # Extract the relevant store objects
-        txn = storeObject._txn
-        owner_home = None
-        viewer_home = None
-        home_child = None
-        object_resource = None
-        if isinstance(storeObject, CommonObjectResource):
-            owner_home = storeObject.ownerHome()
-            viewer_home = storeObject.viewerHome()
-            home_child = storeObject.parentCollection()
-            object_resource = storeObject
-        elif isinstance(storeObject, CommonHomeChild):
-            owner_home = storeObject.ownerHome()
-            viewer_home = storeObject.viewerHome()
-            home_child = storeObject
-            result[&quot;classMethod&quot;] = classMethod
-        elif isinstance(storeObject, CommonHome):
-            owner_home = storeObject
-            viewer_home = storeObject
-            txn = storeObject._txn
-            result[&quot;classMethod&quot;] = classMethod
-
-        # Add store object identities to JSON request
-        result[&quot;homeType&quot;] = viewer_home._homeType
-        result[&quot;homeUID&quot;] = viewer_home.uid()
-        if home_child:
-            if home_child.owned():
-                result[&quot;homeChildID&quot;] = home_child.id()
-            else:
-                result[&quot;homeChildSharedID&quot;] = home_child.name()
-        if object_resource:
-            result[&quot;objectResourceID&quot;] = object_resource.id()
-
-        # Note that the owner_home is always the ownerHome() because in the sharing case
-        # a viewer is accessing the owner's data on another pod.
-        recipient = yield self.store.directoryService().recordWithUID(owner_home.uid())
-
-        returnValue((txn, result, recipient.server(),))
-
-
-    @inlineCallbacks
-    def _getStoreObjectForRequest(self, txn, request):
-        &quot;&quot;&quot;
-        Resolve the supplied JSON data to get a store object to operate on.
-        &quot;&quot;&quot;
-
-        returnObject = txn
-        classObject = None
-
-        if &quot;homeUID&quot; in request:
-            home = yield txn.homeWithUID(request[&quot;homeType&quot;], request[&quot;homeUID&quot;])
-            if home is None:
-                raise FailedCrossPodRequestError(&quot;Invalid owner UID specified&quot;)
-            home._internalRequest = False
-            returnObject = home
-            if request.get(&quot;classMethod&quot;, False):
-                classObject = home._childClass
-
-        if &quot;homeChildID&quot; in request:
-            homeChild = yield home.childWithID(request[&quot;homeChildID&quot;])
-            if homeChild is None:
-                raise FailedCrossPodRequestError(&quot;Invalid home child specified&quot;)
-            returnObject = homeChild
-            if request.get(&quot;classMethod&quot;, False):
-                classObject = homeChild._objectResourceClass
-        elif &quot;homeChildSharedID&quot; in request:
-            homeChild = yield home.childWithName(request[&quot;homeChildSharedID&quot;])
-            if homeChild is None:
-                raise FailedCrossPodRequestError(&quot;Invalid home child specified&quot;)
-            returnObject = homeChild
-            if request.get(&quot;classMethod&quot;, False):
-                classObject = homeChild._objectResourceClass
-
-        if &quot;objectResourceID&quot; in request:
-            objectResource = yield homeChild.objectResourceWithID(request[&quot;objectResourceID&quot;])
-            if objectResource is None:
-                raise FailedCrossPodRequestError(&quot;Invalid object resource specified&quot;)
-            returnObject = objectResource
-
-        returnValue((returnObject, classObject,))
-
-
-    @inlineCallbacks
</del><span class="cx">     def send_home_resource_id(self, txn, recipient):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Lookup the remote resourceID matching the specified directory uid.
</span><span class="lines">@@ -235,134 +127,29 @@
</span><span class="cx">             &quot;matchtotal&quot;: matchtotal,
</span><span class="cx">         })
</span><span class="cx"> 
</span><del>-
-    #
-    # We can simplify code generation for simple calls by dynamically generating the appropriate class methods.
-    #
-
-    @inlineCallbacks
-    def _simple_object_send(self, actionName, storeObject, classMethod=False, transform=None, args=None, kwargs=None):
-        &quot;&quot;&quot;
-        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 objectResource: the resource being operated on, or C{None} for classmethod.
-        @type objectResource: L{CommonObjectResourceExternal}
-        @param transform: a function used to convert the JSON response into return values.
-        @type transform: C{callable}
-        @param args: list of optional arguments.
-        @type args: C{list}
-        @param kwargs: optional keyword arguments.
-        @type kwargs: C{dict}
-        &quot;&quot;&quot;
-
-        txn, request, server = yield self._getRequestForStoreObject(actionName, storeObject, classMethod)
-        if args is not None:
-            request[&quot;arguments&quot;] = args
-        if kwargs is not None:
-            request[&quot;keywords&quot;] = kwargs
-        response = yield self.sendRequestToServer(txn, server, request)
-        returnValue(transform(response) if transform is not None else response)
-
-
-    @inlineCallbacks
-    def _simple_object_recv(self, txn, actionName, request, method, transform=None):
-        &quot;&quot;&quot;
-        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 request: request arguments
-        @type request: C{dict}
-        @param method: name of the method to execute on the shared resource to get the result.
-        @type method: C{str}
-        @param transform: method to call on returned JSON value to convert it to something useful.
-        @type transform: C{callable}
-        &quot;&quot;&quot;
-
-        storeObject, classObject = yield self._getStoreObjectForRequest(txn, request)
-        if classObject is not None:
-            value = yield getattr(classObject, method)(storeObject, *request.get(&quot;arguments&quot;, ()), **request.get(&quot;keywords&quot;, {}))
-        else:
-            value = yield getattr(storeObject, method)(*request.get(&quot;arguments&quot;, ()), **request.get(&quot;keywords&quot;, {}))
-
-        returnValue(transform(value) if transform is not None else value)
-
-
-    #
-    # Factory methods for binding actions to the conduit class
-    #
-    @classmethod
-    def _make_simple_action(cls, action, method, classMethod=False, transform_recv_result=None, transform_send_result=None):
-        setattr(
-            cls,
-            &quot;send_{}&quot;.format(action),
-            lambda self, storeObject, *args, **kwargs:
-                self._simple_object_send(action, storeObject, classMethod=classMethod, transform=transform_send_result, args=args, kwargs=kwargs)
-        )
-        setattr(
-            cls,
-            &quot;recv_{}&quot;.format(action),
-            lambda self, txn, message:
-                self._simple_object_recv(txn, action, message, method, transform=transform_recv_result)
-        )
-
-
-    #
-    # Transforms for returned data
-    #
-    @staticmethod
-    def _to_externalize(value):
-        &quot;&quot;&quot;
-        Convert the value to the external (JSON-based) representation.
-        &quot;&quot;&quot;
-        return value.externalize() if value is not None else None
-
-
-    @staticmethod
-    def _to_externalize_list(value):
-        &quot;&quot;&quot;
-        Convert the value to the external (JSON-based) representation.
-        &quot;&quot;&quot;
-        return [v.externalize() for v in value]
-
-
-    @staticmethod
-    def _to_string(value):
-        return str(value)
-
-
-    @staticmethod
-    def _to_tuple(value):
-        return tuple(value)
-
</del><span class="cx"> # These are the actions on store objects we need to expose via the conduit api
</span><span class="cx"> 
</span><span class="cx"> # Calls on L{CommonHome} objects
</span><span class="cx"> 
</span><span class="cx"> # Calls on L{CommonHomeChild} objects
</span><del>-StoreAPIConduitMixin._make_simple_action(&quot;homechild_listobjects&quot;, &quot;listObjects&quot;, classMethod=True)
-StoreAPIConduitMixin._make_simple_action(&quot;homechild_loadallobjects&quot;, &quot;loadAllObjects&quot;, classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize_list)
-StoreAPIConduitMixin._make_simple_action(&quot;homechild_objectwith&quot;, &quot;objectWith&quot;, classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize)
-StoreAPIConduitMixin._make_simple_action(&quot;homechild_movehere&quot;, &quot;moveObjectResourceHere&quot;)
-StoreAPIConduitMixin._make_simple_action(&quot;homechild_moveaway&quot;, &quot;moveObjectResourceAway&quot;)
-StoreAPIConduitMixin._make_simple_action(&quot;homechild_synctokenrevision&quot;, &quot;syncTokenRevision&quot;)
-StoreAPIConduitMixin._make_simple_action(&quot;homechild_resourcenamessincerevision&quot;, &quot;resourceNamesSinceRevision&quot;, transform_send_result=StoreAPIConduitMixin._to_tuple)
-StoreAPIConduitMixin._make_simple_action(&quot;homechild_search&quot;, &quot;search&quot;)
</del><ins>+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;homechild_listobjects&quot;, &quot;listObjects&quot;, classMethod=True)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;homechild_loadallobjects&quot;, &quot;loadAllObjects&quot;, classMethod=True, transform_recv_result=UtilityConduitMixin._to_externalize_list)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;homechild_objectwith&quot;, &quot;objectWith&quot;, classMethod=True, transform_recv_result=UtilityConduitMixin._to_externalize)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;homechild_movehere&quot;, &quot;moveObjectResourceHere&quot;)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;homechild_moveaway&quot;, &quot;moveObjectResourceAway&quot;)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;homechild_synctokenrevision&quot;, &quot;syncTokenRevision&quot;)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;homechild_resourcenamessincerevision&quot;, &quot;resourceNamesSinceRevision&quot;, transform_send_result=UtilityConduitMixin._to_tuple)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;homechild_search&quot;, &quot;search&quot;)
</ins><span class="cx"> 
</span><span class="cx"> # Calls on L{CommonObjectResource} objects
</span><del>-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_loadallobjects&quot;, &quot;loadAllObjects&quot;, classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize_list)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_loadallobjectswithnames&quot;, &quot;loadAllObjectsWithNames&quot;, classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize_list)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_listobjects&quot;, &quot;listObjects&quot;, classMethod=True)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_countobjects&quot;, &quot;countObjects&quot;, classMethod=True)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_objectwith&quot;, &quot;objectWith&quot;, classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_resourcenameforuid&quot;, &quot;resourceNameForUID&quot;, classMethod=True)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_resourceuidforname&quot;, &quot;resourceUIDForName&quot;, classMethod=True)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_create&quot;, &quot;create&quot;, classMethod=True, transform_recv_result=StoreAPIConduitMixin._to_externalize)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_setcomponent&quot;, &quot;setComponent&quot;)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_component&quot;, &quot;component&quot;, transform_recv_result=StoreAPIConduitMixin._to_string)
-StoreAPIConduitMixin._make_simple_action(&quot;objectresource_remove&quot;, &quot;remove&quot;)
</del><ins>+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_loadallobjects&quot;, &quot;loadAllObjects&quot;, classMethod=True, transform_recv_result=UtilityConduitMixin._to_externalize_list)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_loadallobjectswithnames&quot;, &quot;loadAllObjectsWithNames&quot;, classMethod=True, transform_recv_result=UtilityConduitMixin._to_externalize_list)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_listobjects&quot;, &quot;listObjects&quot;, classMethod=True)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_countobjects&quot;, &quot;countObjects&quot;, classMethod=True)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_objectwith&quot;, &quot;objectWith&quot;, classMethod=True, transform_recv_result=UtilityConduitMixin._to_externalize)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_resourcenameforuid&quot;, &quot;resourceNameForUID&quot;, classMethod=True)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_resourceuidforname&quot;, &quot;resourceUIDForName&quot;, classMethod=True)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_create&quot;, &quot;create&quot;, classMethod=True, transform_recv_result=UtilityConduitMixin._to_externalize)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_setcomponent&quot;, &quot;setComponent&quot;)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_component&quot;, &quot;component&quot;, transform_recv_result=UtilityConduitMixin._to_string)
+UtilityConduitMixin._make_simple_action(StoreAPIConduitMixin, &quot;objectresource_remove&quot;, &quot;remove&quot;)
</ins></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingtesttest_conduitpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/test_conduit.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/test_conduit.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/test_conduit.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -32,7 +32,7 @@
</span><span class="cx"> from txdav.caldav.datastore.query.filter import Filter
</span><span class="cx"> from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
</span><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.localservers import ServersDB, Server
</span><del>-from txdav.caldav.datastore.sql import ManagedAttachment
</del><ins>+from txdav.caldav.datastore.sql import ManagedAttachment, AttachmentLink
</ins><span class="cx"> from txdav.caldav.datastore.test.common import CaptureProtocol
</span><span class="cx"> from txdav.common.datastore.podding.conduit import PoddingConduit, \
</span><span class="cx">     FailedCrossPodRequestError
</span><span class="lines">@@ -1107,3 +1107,32 @@
</span><span class="cx">         attachment._name = &quot;test.txt&quot;
</span><span class="cx">         yield shared_object.ownerHome().readAttachmentData(remote_id, attachment)
</span><span class="cx">         yield self.commitTransaction(1)
</span><ins>+
+
+    @inlineCallbacks
+    def test_get_attachment_links(self):
+        &quot;&quot;&quot;
+        Test that action=get-attachment-links works.
+        &quot;&quot;&quot;
+
+        yield self.createShare(&quot;user01&quot;, &quot;puser01&quot;)
+
+        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        cobj1 = yield calendar1.createCalendarObjectWithName(&quot;1.ics&quot;, Component.fromString(self.caldata1))
+        calobjID = cobj1.id()
+        yield self.commitTransaction(0)
+
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home=&quot;user01&quot;, calendar_name=&quot;calendar&quot;, name=&quot;1.ics&quot;)
+        attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString(&quot;text/plain&quot;), &quot;test.txt&quot;, MemoryStream(&quot;Here is some text.&quot;))
+        attID = attachment.id()
+        managedID = attachment.managedID()
+        yield self.commitTransaction(0)
+
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home=&quot;puser01&quot;, calendar_name=&quot;shared-calendar&quot;, name=&quot;1.ics&quot;)
+        links = yield shared_object.ownerHome().getAttachmentLinks()
+        self.assertEqual(len(links), 1)
+        self.assertTrue(isinstance(links[0], AttachmentLink))
+        self.assertEqual(links[0]._attachmentID, attID)
+        self.assertEqual(links[0]._managedID, managedID)
+        self.assertEqual(links[0]._calendarObjectID, calobjID)
+        yield self.commitTransaction(1)
</ins></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingtestutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/util.py (14411 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/util.py        2015-02-13 21:16:52 UTC (rev 14411)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/util.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -21,20 +21,24 @@
</span><span class="cx">     Server, ServersDB
</span><span class="cx"> )
</span><span class="cx"> from txdav.common.datastore.podding.conduit import PoddingConduit
</span><ins>+from txdav.common.datastore.podding.request import ConduitRequest
</ins><span class="cx"> from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
</span><span class="cx"> from txdav.common.datastore.test.util import (
</span><span class="cx">     CommonCommonTests, SQLStoreBuilder, buildTestDirectory
</span><span class="cx"> )
</span><span class="cx"> 
</span><span class="cx"> import txweb2.dav.test.util
</span><del>-from txweb2.stream import ProducerStream, readStream
</del><ins>+from txweb2 import responsecode
+from txweb2.http import Response, JSONResponse
+from txweb2.http_headers import MimeDisposition, MimeType
+from txweb2.stream import ProducerStream
</ins><span class="cx"> 
</span><span class="cx"> from twext.enterprise.ienterprise import AlreadyFinishedError
</span><span class="cx"> 
</span><span class="cx"> import json
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-class FakeConduitRequest(object):
</del><ins>+class FakeConduitRequest(ConduitRequest):
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     A conduit request that sends messages internally rather than using HTTP
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="lines">@@ -66,32 +70,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def doRequest(self, txn):
-
-        # Generate an HTTP client request
-        try:
-            response = (yield self._processRequest())
-            if self.writeStream is None:
-                response = json.loads(response)
-            else:
-                try:
-                    ct, name, stream = response
-                    response = {
-                        &quot;result&quot;: &quot;ok&quot;,
-                        &quot;content-type&quot;: ct,
-                        &quot;name&quot;: name,
-                    }
-                    yield readStream(stream, self.writeStream.write)
-                    yield self.writeStream.loseConnection()
-                except ValueError:
-                    pass
-        except Exception as e:
-            raise ValueError(&quot;Failed cross-pod request: {}&quot;.format(e))
-
-        returnValue(response)
-
-
-    @inlineCallbacks
</del><span class="cx">     def _processRequest(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Process the request by sending it to the relevant server.
</span><span class="lines">@@ -121,11 +99,14 @@
</span><span class="cx">                 try:
</span><span class="cx">                     ct, name = result
</span><span class="cx">                 except ValueError:
</span><del>-                    pass
</del><ins>+                    code = responsecode.BAD_REQUEST
</ins><span class="cx">                 else:
</span><del>-                    returnValue((ct, name, stream,))
</del><ins>+                    headers = {&quot;content-type&quot;: MimeType.fromString(ct)}
+                    headers[&quot;content-disposition&quot;] = MimeDisposition(&quot;attachment&quot;, params={&quot;filename&quot;: name})
+                    returnValue(Response(responsecode.OK, headers, stream))
</ins><span class="cx">             else:
</span><span class="cx">                 result = yield store.conduit.processRequest(j)
</span><ins>+                code = responsecode.OK
</ins><span class="cx">         except Exception as e:
</span><span class="cx">             # Send the exception over to the other side
</span><span class="cx">             result = {
</span><span class="lines">@@ -133,11 +114,13 @@
</span><span class="cx">                 &quot;class&quot;: &quot;.&quot;.join((e.__class__.__module__, e.__class__.__name__,)),
</span><span class="cx">                 &quot;details&quot;: str(e),
</span><span class="cx">             }
</span><del>-        result = json.dumps(result)
-        returnValue(result)
</del><ins>+            code = responsecode.BAD_REQUEST
</ins><span class="cx"> 
</span><ins>+        response = JSONResponse(code, result)
+        returnValue(response)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> class MultiStoreConduitTest(CommonCommonTests, txweb2.dav.test.util.TestCase):
</span><span class="cx"> 
</span><span class="cx">     numberOfStores = 2
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingutilpy"></a>
<div class="addfile"><h4>Added: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/util.py (0 => 14412)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/util.py                                (rev 0)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/util.py        2015-02-13 21:21:26 UTC (rev 14412)
</span><span class="lines">@@ -0,0 +1,237 @@
</span><ins>+##
+# Copyright (c) 2013-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# 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 &quot;AS IS&quot; 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.common.datastore.podding.base import FailedCrossPodRequestError
+
+
+class UtilityConduitMixin(object):
+    &quot;&quot;&quot;
+    Defines utility methods for cross-pod API and mix-ins.
+    &quot;&quot;&quot;
+
+    #
+    # Utility methods to map from store objects to/from JSON
+    #
+
+    @inlineCallbacks
+    def _getRequestForStoreObject(self, action, storeObject, classMethod):
+        &quot;&quot;&quot;
+        Create the JSON data needed to identify the remote resource by type and ids, along with any parent resources.
+
+        @param action: the conduit action name
+        @type action: L{str}
+        @param storeObject: the store object that is being operated on
+        @type storeObject: L{object}
+        @param classMethod: indicates whether the method being called is a classmethod
+        @type classMethod: L{bool}
+
+        @return: the transaction in use, the JSON dict to send in the request,
+            the server where the request should be sent
+        @rtype: L{tuple} of (L{CommonStoreTransaction}, L{dict}, L{str})
+        &quot;&quot;&quot;
+
+        from txdav.common.datastore.sql import CommonObjectResource, CommonHomeChild, CommonHome
+        result = {
+            &quot;action&quot;: action,
+        }
+
+        # Extract the relevant store objects
+        txn = storeObject._txn
+        owner_home = None
+        viewer_home = None
+        home_child = None
+        object_resource = None
+        if isinstance(storeObject, CommonObjectResource):
+            owner_home = storeObject.ownerHome()
+            viewer_home = storeObject.viewerHome()
+            home_child = storeObject.parentCollection()
+            object_resource = storeObject
+        elif isinstance(storeObject, CommonHomeChild):
+            owner_home = storeObject.ownerHome()
+            viewer_home = storeObject.viewerHome()
+            home_child = storeObject
+            result[&quot;classMethod&quot;] = classMethod
+        elif isinstance(storeObject, CommonHome):
+            owner_home = storeObject
+            viewer_home = storeObject
+            txn = storeObject._txn
+            result[&quot;classMethod&quot;] = classMethod
+
+        # Add store object identities to JSON request
+        result[&quot;homeType&quot;] = viewer_home._homeType
+        result[&quot;homeUID&quot;] = viewer_home.uid()
+        if home_child:
+            if home_child.owned():
+                result[&quot;homeChildID&quot;] = home_child.id()
+            else:
+                result[&quot;homeChildSharedID&quot;] = home_child.name()
+        if object_resource:
+            result[&quot;objectResourceID&quot;] = object_resource.id()
+
+        # Note that the owner_home is always the ownerHome() because in the sharing case
+        # a viewer is accessing the owner's data on another pod.
+        recipient = yield self.store.directoryService().recordWithUID(owner_home.uid())
+
+        returnValue((txn, result, recipient.server(),))
+
+
+    @inlineCallbacks
+    def _getStoreObjectForRequest(self, txn, request):
+        &quot;&quot;&quot;
+        Resolve the supplied JSON data to get a store object to operate on.
+        &quot;&quot;&quot;
+
+        returnObject = txn
+        classObject = None
+
+        if &quot;homeUID&quot; in request:
+            home = yield txn.homeWithUID(request[&quot;homeType&quot;], request[&quot;homeUID&quot;])
+            if home is None:
+                raise FailedCrossPodRequestError(&quot;Invalid owner UID specified&quot;)
+            home._internalRequest = False
+            returnObject = home
+            if request.get(&quot;classMethod&quot;, False):
+                classObject = home._childClass
+
+        if &quot;homeChildID&quot; in request:
+            homeChild = yield home.childWithID(request[&quot;homeChildID&quot;])
+            if homeChild is None:
+                raise FailedCrossPodRequestError(&quot;Invalid home child specified&quot;)
+            returnObject = homeChild
+            if request.get(&quot;classMethod&quot;, False):
+                classObject = homeChild._objectResourceClass
+        elif &quot;homeChildSharedID&quot; in request:
+            homeChild = yield home.childWithName(request[&quot;homeChildSharedID&quot;])
+            if homeChild is None:
+                raise FailedCrossPodRequestError(&quot;Invalid home child specified&quot;)
+            returnObject = homeChild
+            if request.get(&quot;classMethod&quot;, False):
+                classObject = homeChild._objectResourceClass
+
+        if &quot;objectResourceID&quot; in request:
+            objectResource = yield homeChild.objectResourceWithID(request[&quot;objectResourceID&quot;])
+            if objectResource is None:
+                raise FailedCrossPodRequestError(&quot;Invalid object resource specified&quot;)
+            returnObject = objectResource
+
+        returnValue((returnObject, classObject,))
+
+
+    #
+    # We can simplify code generation for simple calls by dynamically generating the appropriate class methods.
+    #
+
+    @inlineCallbacks
+    def _simple_object_send(self, actionName, storeObject, classMethod=False, transform=None, args=None, kwargs=None):
+        &quot;&quot;&quot;
+        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 objectResource: the resource being operated on, or C{None} for classmethod.
+        @type objectResource: L{CommonObjectResourceExternal}
+        @param transform: a function used to convert the JSON response into return values.
+        @type transform: C{callable}
+        @param args: list of optional arguments.
+        @type args: C{list}
+        @param kwargs: optional keyword arguments.
+        @type kwargs: C{dict}
+        &quot;&quot;&quot;
+
+        txn, request, server = yield self._getRequestForStoreObject(actionName, storeObject, classMethod)
+        if args is not None:
+            request[&quot;arguments&quot;] = args
+        if kwargs is not None:
+            request[&quot;keywords&quot;] = kwargs
+        response = yield self.sendRequestToServer(txn, server, request)
+        returnValue(transform(response) if transform is not None else response)
+
+
+    @inlineCallbacks
+    def _simple_object_recv(self, txn, actionName, request, method, transform=None):
+        &quot;&quot;&quot;
+        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 request: request arguments
+        @type request: C{dict}
+        @param method: name of the method to execute on the shared resource to get the result.
+        @type method: C{str}
+        @param transform: method to call on returned JSON value to convert it to something useful.
+        @type transform: C{callable}
+        &quot;&quot;&quot;
+
+        storeObject, classObject = yield self._getStoreObjectForRequest(txn, request)
+        if classObject is not None:
+            value = yield getattr(classObject, method)(storeObject, *request.get(&quot;arguments&quot;, ()), **request.get(&quot;keywords&quot;, {}))
+        else:
+            value = yield getattr(storeObject, method)(*request.get(&quot;arguments&quot;, ()), **request.get(&quot;keywords&quot;, {}))
+
+        returnValue(transform(value) if transform is not None else value)
+
+
+    #
+    # Factory methods for binding actions to the conduit class
+    #
+    @staticmethod
+    def _make_simple_action(bindcls, action, method, classMethod=False, transform_recv_result=None, transform_send_result=None):
+        setattr(
+            bindcls,
+            &quot;send_{}&quot;.format(action),
+            lambda self, storeObject, *args, **kwargs:
+                self._simple_object_send(action, storeObject, classMethod=classMethod, transform=transform_send_result, args=args, kwargs=kwargs)
+        )
+        setattr(
+            bindcls,
+            &quot;recv_{}&quot;.format(action),
+            lambda self, txn, message:
+                self._simple_object_recv(txn, action, message, method, transform=transform_recv_result)
+        )
+
+
+    #
+    # Transforms for returned data
+    #
+    @staticmethod
+    def _to_externalize(value):
+        &quot;&quot;&quot;
+        Convert the value to the external (JSON-based) representation.
+        &quot;&quot;&quot;
+        return value.externalize() if value is not None else None
+
+
+    @staticmethod
+    def _to_externalize_list(value):
+        &quot;&quot;&quot;
+        Convert the value to the external (JSON-based) representation.
+        &quot;&quot;&quot;
+        return [v.externalize() for v in value]
+
+
+    @staticmethod
+    def _to_string(value):
+        return str(value)
+
+
+    @staticmethod
+    def _to_tuple(value):
+        return tuple(value)
</ins></span></pre>
</div>
</div>

</body>
</html>