<!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>[14403] 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/14403">14403</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2015-02-11 12:34:55 -0800 (Wed, 11 Feb 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Checkpoint: attachment sync'ing during migration.</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="#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="#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>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastoresql_schemacurrentoracledialectsql">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current-oracle-dialect.sql</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastoresql_schemacurrentsql">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastoresql_schemaupgradesoracledialectupgrade_from_51_to_52sql">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_51_to_52.sql</a></li>
<li><a href="#CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastoresql_schemaupgradespostgresdialectupgrade_from_51_to_52sql">CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_51_to_52.sql</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 (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -638,6 +638,24 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><ins>+    def getAllAttachments(self):
+        &quot;&quot;&quot;
+        Return all the L{Attachment} objects associated with this calendar home.
+        Needed during migration.
+        &quot;&quot;&quot;
+        attachments = yield Attachment.loadAllAttachments(self)
+        returnValue(attachments)
+
+
+    def getAttachmentByID(self, id):
+        &quot;&quot;&quot;
+        Return a specific attachment associated with this calendar home.
+        Needed during migration only.
+        &quot;&quot;&quot;
+        return Attachment.loadAttachmentByID(self, id)
+
+
+    @inlineCallbacks
</ins><span class="cx">     def getAllDropboxIDs(self):
</span><span class="cx">         co = schema.CALENDAR_OBJECT
</span><span class="cx">         cb = schema.CALENDAR_BIND
</span><span class="lines">@@ -4646,8 +4664,10 @@
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def attachments(self):
</span><span class="cx">         if self._dropboxID:
</span><del>-            rows = yield self._attachmentsQuery.on(self._txn,
-                                                   dropboxID=self._dropboxID)
</del><ins>+            rows = yield self._attachmentsQuery.on(
+                self._txn,
+                dropboxID=self._dropboxID,
+            )
</ins><span class="cx">             result = []
</span><span class="cx">             for row in rows:
</span><span class="cx">                 result.append((yield self.attachmentWithName(row[0])))
</span><span class="lines">@@ -4920,7 +4940,7 @@
</span><span class="cx"> 
</span><span class="cx">     _TEMPORARY_UPLOADS_DIRECTORY = &quot;Temporary&quot;
</span><span class="cx"> 
</span><del>-    def __init__(self, attachment, contentType, dispositionName, creating=False):
</del><ins>+    def __init__(self, attachment, contentType, dispositionName, creating=False, migrating=False):
</ins><span class="cx">         super(AttachmentStorageTransport, self).__init__(
</span><span class="cx">             attachment, contentType, dispositionName)
</span><span class="cx"> 
</span><span class="lines">@@ -4930,6 +4950,7 @@
</span><span class="cx">         self._path = CachingFilePath(fileName)
</span><span class="cx">         self._hash = hashlib.md5()
</span><span class="cx">         self._creating = creating
</span><ins>+        self._migrating = migrating
</ins><span class="cx"> 
</span><span class="cx">         self._txn.postAbort(self.aborted)
</span><span class="cx"> 
</span><span class="lines">@@ -4970,6 +4991,10 @@
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def loseConnection(self):
</span><ins>+        &quot;&quot;&quot;
+        Note that when self._migrating is set we only care about the data and don't need to
+        do any quota checks/adjustments.
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><span class="cx">         # FIXME: this should be synchronously accessible; IAttachment should
</span><span class="cx">         # have a method for getting its parent just as CalendarObject/Calendar
</span><span class="lines">@@ -4986,20 +5011,21 @@
</span><span class="cx">         self._file.close()
</span><span class="cx"> 
</span><span class="cx">         # Check max size for attachment
</span><del>-        if newSize &gt; config.MaximumAttachmentSize:
</del><ins>+        if not self._migrating and newSize &gt; config.MaximumAttachmentSize:
</ins><span class="cx">             self._path.remove()
</span><span class="cx">             if self._creating:
</span><span class="cx">                 yield self._attachment._internalRemove()
</span><span class="cx">             raise AttachmentSizeTooLarge()
</span><span class="cx"> 
</span><span class="cx">         # Check overall user quota
</span><del>-        allowed = home.quotaAllowedBytes()
-        if allowed is not None and allowed &lt; ((yield home.quotaUsedBytes())
-                                              + (newSize - oldSize)):
-            self._path.remove()
-            if self._creating:
-                yield self._attachment._internalRemove()
-            raise QuotaExceeded()
</del><ins>+        if not self._migrating:
+            allowed = home.quotaAllowedBytes()
+            if allowed is not None and allowed &lt; ((yield home.quotaUsedBytes())
+                                                  + (newSize - oldSize)):
+                self._path.remove()
+                if self._creating:
+                    yield self._attachment._internalRemove()
+                raise QuotaExceeded()
</ins><span class="cx"> 
</span><span class="cx">         self._path.moveTo(self._attachment._path)
</span><span class="cx"> 
</span><span class="lines">@@ -5010,7 +5036,7 @@
</span><span class="cx">             newSize
</span><span class="cx">         )
</span><span class="cx"> 
</span><del>-        if home:
</del><ins>+        if not self._migrating and home:
</ins><span class="cx">             # Adjust quota
</span><span class="cx">             yield home.adjustQuotaUsedBytes(self._attachment.size() - oldSize)
</span><span class="cx"> 
</span><span class="lines">@@ -5028,6 +5054,153 @@
</span><span class="cx"> 
</span><span class="cx">     implements(IAttachment)
</span><span class="cx"> 
</span><ins>+    _attachmentSchema = schema.ATTACHMENT
+
+    @classmethod
+    def makeClass(cls, txn, attachmentData):
+        &quot;&quot;&quot;
+        Given the various database rows, build the actual class.
+
+        @param parent: the parent collection object
+        @type parent: L{CommonHomeChild}
+
+        @param objectData: the standard set of object columns
+        @type objectData: C{list}
+
+        @param propstore: a property store to use, or C{None} to load it
+            automatically
+        @type propstore: L{PropertyStore}
+
+        @return: the constructed child class
+        @rtype: L{CommonHomeChild}
+        &quot;&quot;&quot;
+
+        att = schema.ATTACHMENT
+        dropbox_id = attachmentData[cls._allColumns().index(att.DROPBOX_ID)]
+        c = ManagedAttachment if dropbox_id == &quot;.&quot; else DropBoxAttachment
+        child = c(
+            txn,
+            attachmentData[cls._allColumns().index(att.ATTACHMENT_ID)],
+            attachmentData[cls._allColumns().index(att.DROPBOX_ID)],
+            attachmentData[cls._allColumns().index(att.PATH)],
+        )
+
+        for attr, value in zip(child._rowAttributes(), attachmentData):
+            setattr(child, attr, value)
+        child._contentType = MimeType.fromString(child._contentType)
+
+        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;
+        att = cls._attachmentSchema
+        return [
+            att.ATTACHMENT_ID,
+            att.DROPBOX_ID,
+            att.CALENDAR_HOME_RESOURCE_ID,
+            att.CONTENT_TYPE,
+            att.SIZE,
+            att.MD5,
+            att.CREATED,
+            att.MODIFIED,
+            att.PATH,
+        ]
+
+
+    @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;_dropboxID&quot;,
+            &quot;_ownerHomeID&quot;,
+            &quot;_contentType&quot;,
+            &quot;_size&quot;,
+            &quot;_md5&quot;,
+            &quot;_created&quot;,
+            &quot;_modified&quot;,
+            &quot;_name&quot;,
+        )
+
+
+    @classmethod
+    @inlineCallbacks
+    def loadAllAttachments(cls, home):
+        &quot;&quot;&quot;
+        Load all attachments assigned to the specified home collection. This should only be
+        used when sync'ing an entire home's set of attachments.
+        &quot;&quot;&quot;
+
+        results = []
+
+        # Load from the main table first
+        att = cls._attachmentSchema
+        dataRows = yield Select(
+            cls._allColumns(),
+            From=att,
+            Where=att.CALENDAR_HOME_RESOURCE_ID == home.id(),
+        ).on(home._txn)
+
+        # Create the actual objects
+        for row in dataRows:
+            child = cls.makeClass(home._txn, row)
+            results.append(child)
+
+        returnValue(results)
+
+
+    @classmethod
+    @inlineCallbacks
+    def loadAttachmentByID(cls, home, id):
+        &quot;&quot;&quot;
+        Load one attachments assigned to the specified home collection. This should only be
+        used when sync'ing an entire home's set of attachments.
+        &quot;&quot;&quot;
+
+        # Load from the main table first
+        att = cls._attachmentSchema
+        rows = yield Select(
+            cls._allColumns(),
+            From=att,
+            Where=(att.CALENDAR_HOME_RESOURCE_ID == home.id()).And(
+                att.ATTACHMENT_ID == id),
+        ).on(home._txn)
+
+        # Create the actual object
+        returnValue(cls.makeClass(home._txn, rows[0]) if len(rows) == 1 else 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;
+        result = dict([(attr[1:], getattr(self, attr, None)) for attr in self._rowAttributes()])
+        result[&quot;contentType&quot;] = generateContentType(result[&quot;contentType&quot;])
+        return result
+
+
+    @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()])
+
+
</ins><span class="cx">     def __init__(self, txn, a_id, dropboxID, name, ownerHomeID=None, justCreated=False):
</span><span class="cx">         self._txn = txn
</span><span class="cx">         self._attachmentID = a_id
</span><span class="lines">@@ -5061,23 +5234,13 @@
</span><span class="cx">         @return: C{True} if this attachment exists, C{False} otherwise.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         att = schema.ATTACHMENT
</span><del>-        if self._dropboxID:
</del><ins>+        if self._dropboxID and self._dropboxID != &quot;.&quot;:
</ins><span class="cx">             where = (att.DROPBOX_ID == self._dropboxID).And(
</span><span class="cx">                 att.PATH == self._name)
</span><span class="cx">         else:
</span><span class="cx">             where = (att.ATTACHMENT_ID == self._attachmentID)
</span><span class="cx">         rows = (yield Select(
</span><del>-            [
-                att.ATTACHMENT_ID,
-                att.DROPBOX_ID,
-                att.CALENDAR_HOME_RESOURCE_ID,
-                att.CONTENT_TYPE,
-                att.SIZE,
-                att.MD5,
-                att.CREATED,
-                att.MODIFIED,
-                att.PATH,
-            ],
</del><ins>+            self._allColumns(),
</ins><span class="cx">             From=att,
</span><span class="cx">             Where=where
</span><span class="cx">         ).on(self._txn))
</span><span class="lines">@@ -5085,20 +5248,28 @@
</span><span class="cx">         if not rows:
</span><span class="cx">             returnValue(None)
</span><span class="cx"> 
</span><del>-        row_iter = iter(rows[0])
-        self._attachmentID = row_iter.next()
-        self._dropboxID = row_iter.next()
-        self._ownerHomeID = row_iter.next()
-        self._contentType = MimeType.fromString(row_iter.next())
-        self._size = row_iter.next()
-        self._md5 = row_iter.next()
-        self._created = sqltime(row_iter.next())
-        self._modified = sqltime(row_iter.next())
-        self._name = row_iter.next()
</del><ins>+        for attr, value in zip(self._rowAttributes(), rows[0]):
+            setattr(self, attr, value)
+        self._contentType = MimeType.fromString(self._contentType)
</ins><span class="cx"> 
</span><span class="cx">         returnValue(self)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def copyRemote(self, remote):
+        &quot;&quot;&quot;
+        Copy properties from a remote (external) attachment that is being migrated.
+
+        @param remote: the external attachment
+        @type remote: L{Attachment}
+        &quot;&quot;&quot;
+        yield self.changed(remote.contentType(), remote.name(), remote.md5(), remote.size())
+
+
+    def id(self):
+        return self._attachmentID
+
+
</ins><span class="cx">     def dropboxID(self):
</span><span class="cx">         return self._dropboxID
</span><span class="cx"> 
</span><span class="lines">@@ -5115,10 +5286,10 @@
</span><span class="cx">         pass  # stub
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def store(self, contentType, dispositionName=None):
</del><ins>+    def store(self, contentType, dispositionName=None, migrating=False):
</ins><span class="cx">         if not self._name:
</span><span class="cx">             self._name = dispositionName
</span><del>-        return AttachmentStorageTransport(self, contentType, dispositionName, self._justCreated)
</del><ins>+        return AttachmentStorageTransport(self, contentType, dispositionName, self._justCreated, migrating=migrating)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def retrieve(self, protocol):
</span><span class="lines">@@ -5135,17 +5306,19 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def remove(self):
</del><ins>+    def remove(self, adjustQuota=True):
</ins><span class="cx">         oldSize = self._size
</span><span class="cx">         self._txn.postCommit(self.removePaths)
</span><span class="cx">         yield self._internalRemove()
</span><ins>+
</ins><span class="cx">         # Adjust quota
</span><del>-        home = (yield self._txn.calendarHomeWithResourceID(self._ownerHomeID))
-        if home:
-            yield home.adjustQuotaUsedBytes(-oldSize)
</del><ins>+        if adjustQuota:
+            home = (yield self._txn.calendarHomeWithResourceID(self._ownerHomeID))
+            if home:
+                yield home.adjustQuotaUsedBytes(-oldSize)
</ins><span class="cx"> 
</span><del>-            # Send change notification to home
-            yield home.notifyChanged()
</del><ins>+                # Send change notification to home
+                yield home.notifyChanged()
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def removePaths(self):
</span><span class="lines">@@ -5200,7 +5373,7 @@
</span><span class="cx">         ).on(txn))
</span><span class="cx"> 
</span><span class="cx">         for attachmentID, dropboxID in rows:
</span><del>-            if dropboxID:
</del><ins>+            if dropboxID != &quot;.&quot;:
</ins><span class="cx">                 attachment = DropBoxAttachment(txn, attachmentID, None, None)
</span><span class="cx">             else:
</span><span class="cx">                 attachment = ManagedAttachment(txn, attachmentID, None, None)
</span><span class="lines">@@ -5473,7 +5646,7 @@
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def create(cls, txn, managedID, ownerHomeID, referencedBy):
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        Create a new Attachment object.
</del><ins>+        Create a new Attachment object and reference it.
</ins><span class="cx"> 
</span><span class="cx">         @param txn: The transaction to use
</span><span class="cx">         @type txn: L{CommonStoreTransaction}
</span><span class="lines">@@ -5504,7 +5677,8 @@
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def update(cls, txn, oldManagedID, ownerHomeID, referencedBy, oldAttachmentID):
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        Create a new Attachment object.
</del><ins>+        Update an Attachment object. This creates a new one and adjusts the reference to the old
+        one to point to the new one. If the old one is no longer referenced at all, it is deleted.
</ins><span class="cx"> 
</span><span class="cx">         @param txn: The transaction to use
</span><span class="cx">         @type txn: L{CommonStoreTransaction}
</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 (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql_external.py        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/caldav/datastore/sql_external.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -22,7 +22,8 @@
</span><span class="cx"> 
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> 
</span><del>-from txdav.caldav.datastore.sql import CalendarHome, Calendar, CalendarObject
</del><ins>+from txdav.caldav.datastore.sql import CalendarHome, Calendar, CalendarObject, \
+    Attachment
</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">@@ -61,6 +62,30 @@
</span><span class="cx">         raise AssertionError(&quot;CommonHomeExternal: not supported&quot;)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def getAllAttachments(self):
+        &quot;&quot;&quot;
+        Return all the L{Attachment} objects associated with this calendar home.
+        Needed during migration.
+        &quot;&quot;&quot;
+        raw_results = yield self._txn.store().conduit.send_get_all_attachments(self)
+
+        results = []
+        for attachment in raw_results:
+            results.append(Attachment.internalize(self._txn, attachment))
+        returnValue(results)
+
+
+    @inlineCallbacks
+    def readAttachmentData(self, remote_id, attachment):
+        &quot;&quot;&quot;
+        Read the data associated with an attachment associated with this calendar home.
+        Needed during migration only.
+        &quot;&quot;&quot;
+        stream = attachment.store(attachment.contentType(), attachment.name(), migrating=True)
+        yield self._txn.store().conduit.send_get_attachment_data(self, remote_id, stream)
+
+
</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="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastorepoddingattachmentspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/attachments.py (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/attachments.py        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/attachments.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -15,6 +15,8 @@
</span><span class="cx"> ##
</span><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><ins>+from txdav.caldav.icalendarstore import InvalidAttachmentOperation
+from txweb2.http_headers import generateContentType
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class AttachmentsConduitMixin(object):
</span><span class="lines">@@ -150,3 +152,73 @@
</span><span class="cx">             request[&quot;rids&quot;],
</span><span class="cx">             request[&quot;managedID&quot;],
</span><span class="cx">         )
</span><ins>+
+
+    @inlineCallbacks
+    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
+    def send_get_attachment_data(self, home, attachment_id, stream):
+        &quot;&quot;&quot;
+        Managed attachment readAttachmentData call. We are using streams on the sender and the receiver
+        side to avoid reading the whole attachment into memory.
+
+        @param home: the home whose attachment is being read
+        @type home: L{CalendarHome}
+        @param attachment_id: attachment-id to get
+        @type attachment_id: C{str}
+        @param stream: attachment data stream to write to
+        @type stream: L{IStream}
+        &quot;&quot;&quot;
+
+        actionName = &quot;get-attachment-data&quot;
+        txn, request, server = yield self._getRequestForStoreObject(actionName, home, False)
+        request[&quot;attachmentID&quot;] = attachment_id
+
+        response = yield self.sendRequestToServer(txn, server, request, writeStream=stream)
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def recv_get_attachment_data(self, txn, request, stream):
+        &quot;&quot;&quot;
+        Process an getAttachmentData cross-pod request. Request arguments as per L{send_get_attachment_data}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        &quot;&quot;&quot;
+
+        home, _ignore = yield self._getStoreObjectForRequest(txn, request)
+        attachment = yield home.getAttachmentByID(request[&quot;attachmentID&quot;])
+        if attachment is None:
+            raise InvalidAttachmentOperation(&quot;Attachment is missing: {}&quot;.format(request[&quot;attachmentID&quot;]))
+
+        attachment.retrieve(stream)
+        returnValue((generateContentType(attachment.contentType()), attachment.name(),))
</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 (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/conduit.py        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/conduit.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -72,6 +72,7 @@
</span><span class="cx">         @param store: the L{CommonDataStore} in use.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         self.store = store
</span><ins>+        self.streamingActions = (&quot;get-attachment-data&quot;,)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -108,9 +109,9 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def sendRequestToServer(self, txn, server, data, stream=None, streamType=None):
</del><ins>+    def sendRequestToServer(self, txn, server, data, stream=None, streamType=None, writeStream=None):
</ins><span class="cx"> 
</span><del>-        request = self.conduitRequestClass(server, data, stream, streamType)
</del><ins>+        request = self.conduitRequestClass(server, data, stream, streamType, writeStream)
</ins><span class="cx">         try:
</span><span class="cx">             response = (yield request.doRequest(txn))
</span><span class="cx">         except Exception as e:
</span><span class="lines">@@ -123,6 +124,24 @@
</span><span class="cx">             returnValue(response.get(&quot;value&quot;))
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def isStreamAction(self, data):
+        &quot;&quot;&quot;
+        Check to see if this is a request that will return a data stream rather than a JSON response.
+        e.g., this is used to retrieve attachment data on another pod.
+
+        @param data: the JSON data to process
+        @type data: C{dict}
+        &quot;&quot;&quot;
+        # Must have a dict with an &quot;action&quot; key
+        try:
+            action = data[&quot;action&quot;]
+        except (KeyError, TypeError) as e:
+            log.error(&quot;JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}&quot;, ex=e, json=data)
+            return False
+
+        return action in self.streamingActions
+
+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def processRequest(self, data):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -171,3 +190,48 @@
</span><span class="cx">             yield txn.commit()
</span><span class="cx"> 
</span><span class="cx">         returnValue(result)
</span><ins>+
+
+    @inlineCallbacks
+    def processRequestStream(self, data, stream):
+        &quot;&quot;&quot;
+        Process the request.
+
+        @param data: the JSON data to process
+        @type data: C{dict}
+
+        @return: a L{tuple} of content-type and name, if successful, else a L{dict} for a JSON result
+        @rtype: L{tuple} of (L{str}, L{str}), or L{dict}
+        &quot;&quot;&quot;
+        # Must have a dict with an &quot;action&quot; key
+        try:
+            action = data[&quot;action&quot;]
+        except (KeyError, TypeError) as e:
+            log.error(&quot;JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}&quot;, ex=e, json=data)
+            raise FailedCrossPodRequestError(&quot;JSON data must have an object as its root with an 'action' attribute: {}\n{}&quot;.format(e, data,))
+
+        method = &quot;recv_{}&quot;.format(action.replace(&quot;-&quot;, &quot;_&quot;))
+        if not hasattr(self, method):
+            log.error(&quot;Unsupported action: {action}&quot;, action=action)
+            raise FailedCrossPodRequestError(&quot;Unsupported action: {}&quot;.format(action))
+
+        # Need a transaction to work with
+        txn = self.store.newTransaction(repr(&quot;Conduit request&quot;))
+
+        # Do the actual request processing
+        try:
+            result = (yield getattr(self, method)(txn, data, stream))
+        except Exception as e:
+            # Send the exception over to the other side
+            yield txn.abort()
+            log.error(&quot;Failed action: {action}, {ex}&quot;, action=action, ex=e)
+            result = {
+                &quot;result&quot;: &quot;exception&quot;,
+                &quot;class&quot;: &quot;.&quot;.join((e.__class__.__module__, e.__class__.__name__,)),
+                &quot;details&quot;: str(e),
+            }
+
+        else:
+            yield txn.commit()
+
+        returnValue(result)
</ins></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 (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/home_sync.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -23,6 +23,7 @@
</span><span class="cx"> from twisted.internet.defer import returnValue, inlineCallbacks
</span><span class="cx"> from twisted.python.failure import Failure
</span><span class="cx"> from txdav.caldav.icalendarstore import ComponentUpdateState
</span><ins>+from txdav.caldav.datastore.sql import ManagedAttachment
</ins><span class="cx"> from txdav.common.datastore.sql_tables import schema
</span><span class="cx"> from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
</span><span class="cx"> 
</span><span class="lines">@@ -159,7 +160,7 @@
</span><span class="cx">         yield self.syncCalendarHomeMetaData()
</span><span class="cx"> 
</span><span class="cx">         # TODO: sync attachments
</span><del>-        pass
</del><ins>+        yield self.syncAttachments()
</ins><span class="cx"> 
</span><span class="cx">         # TODO: group attendee/sharee reconcile
</span><span class="cx">         pass
</span><span class="lines">@@ -172,6 +173,9 @@
</span><span class="cx">         rows, recalculate quota etc.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><ins>+        # TODO: link attachments to resources: ATTACHMENT_CALENDAR_OBJECT table
+        pass
+
</ins><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><span class="lines">@@ -184,7 +188,10 @@
</span><span class="cx">         # TODO: notifications
</span><span class="cx">         pass
</span><span class="cx"> 
</span><ins>+        # TODO: work items
+        pass
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def disableRemoteHome(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -320,11 +327,11 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Get local synchronization state for the home being migrated.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        cms = schema.CALENDAR_MIGRATION_STATE
</del><ins>+        cm = schema.CALENDAR_MIGRATION
</ins><span class="cx">         rows = yield Select(
</span><del>-            columns=(cms.REMOTE_RESOURCE_ID, cms.CALENDAR_RESOURCE_ID, cms.LAST_SYNC_TOKEN,),
-            From=cms,
-            Where=(cms.CALENDAR_HOME_RESOURCE_ID == self.homeId)
</del><ins>+            columns=(cm.REMOTE_RESOURCE_ID, cm.CALENDAR_RESOURCE_ID, cm.LAST_SYNC_TOKEN,),
+            From=cm,
+            Where=(cm.CALENDAR_HOME_RESOURCE_ID == self.homeId)
</ins><span class="cx">         ).on(txn)
</span><span class="cx">         returnValue(dict([(remote_id, self.CalendarSyncState(local_id, sync,)) for remote_id, local_id, sync in rows]))
</span><span class="cx"> 
</span><span class="lines">@@ -335,7 +342,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Get local synchronization state for the home being migrated.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        cms = schema.CALENDAR_MIGRATION_STATE
</del><ins>+        cm = schema.CALENDAR_MIGRATION
</ins><span class="cx"> 
</span><span class="cx">         old_details = yield self.getSyncState(txn=txn)
</span><span class="cx"> 
</span><span class="lines">@@ -343,9 +350,9 @@
</span><span class="cx">         missing = set(old_details.keys()) - set(details.keys())
</span><span class="cx">         if missing:
</span><span class="cx">             yield Delete(
</span><del>-                From=cms,
-                Where=(cms.CALENDAR_HOME_RESOURCE_ID == self.homeId).And(
-                    cms.REMOTE_RESOURCE_ID.In(Parameter(&quot;missing&quot;, len(missing)))
</del><ins>+                From=cm,
+                Where=(cm.CALENDAR_HOME_RESOURCE_ID == self.homeId).And(
+                    cm.REMOTE_RESOURCE_ID.In(Parameter(&quot;missing&quot;, len(missing)))
</ins><span class="cx">                 )
</span><span class="cx">             ).on(txn, missing=missing)
</span><span class="cx"> 
</span><span class="lines">@@ -353,10 +360,10 @@
</span><span class="cx">         insert = set(details.keys()) - set(old_details.keys())
</span><span class="cx">         for key in insert:
</span><span class="cx">             yield Insert({
</span><del>-                cms.CALENDAR_HOME_RESOURCE_ID: self.homeId,
-                cms.REMOTE_RESOURCE_ID: key,
-                cms.CALENDAR_RESOURCE_ID: details[key].localID,
-                cms.LAST_SYNC_TOKEN: details[key].lastSyncToken,
</del><ins>+                cm.CALENDAR_HOME_RESOURCE_ID: self.homeId,
+                cm.REMOTE_RESOURCE_ID: key,
+                cm.CALENDAR_RESOURCE_ID: details[key].localID,
+                cm.LAST_SYNC_TOKEN: details[key].lastSyncToken,
</ins><span class="cx">             }).on(txn)
</span><span class="cx"> 
</span><span class="cx">         # Update existing ones
</span><span class="lines">@@ -364,11 +371,11 @@
</span><span class="cx">         for key in updates:
</span><span class="cx">             yield Update(
</span><span class="cx">                 {
</span><del>-                    cms.CALENDAR_RESOURCE_ID: details[key].localID,
-                    cms.LAST_SYNC_TOKEN: details[key].lastSyncToken,
</del><ins>+                    cm.CALENDAR_RESOURCE_ID: details[key].localID,
+                    cm.LAST_SYNC_TOKEN: details[key].lastSyncToken,
</ins><span class="cx">                 },
</span><del>-                Where=(cms.CALENDAR_HOME_RESOURCE_ID == self.homeId).And(
-                    cms.REMOTE_RESOURCE_ID == key
</del><ins>+                Where=(cm.CALENDAR_HOME_RESOURCE_ID == self.homeId).And(
+                    cm.REMOTE_RESOURCE_ID == key
</ins><span class="cx">                 )
</span><span class="cx">             ).on(txn)
</span><span class="cx"> 
</span><span class="lines">@@ -612,6 +619,7 @@
</span><span class="cx">         # matches the remote one (which should help reduce the need for a client to resync
</span><span class="cx">         # the data when moved from one pod to the other).
</span><span class="cx">         txn._migrating = True
</span><ins>+        com = schema.CALENDAR_OBJECT_MIGRATION
</ins><span class="cx">         for obj_name in remote_objects.keys():
</span><span class="cx">             remote_object = remote_objects[obj_name]
</span><span class="cx">             remote_data = yield remote_object.component()
</span><span class="lines">@@ -623,9 +631,126 @@
</span><span class="cx">             else:
</span><span class="cx">                 local_object = yield local_calendar._createCalendarObjectWithNameInternal(obj_name, remote_data, internal_state=ComponentUpdateState.RAW)
</span><span class="cx"> 
</span><ins>+                # Maintain the mapping from the remote to local id. Note that this mapping never changes as the ids on both
+                # sides are immutable - though it may get deleted if the local object is removed during sync (via a cascade).
+                yield Insert(
+                    {
+                        com.CALENDAR_HOME_RESOURCE_ID: self.homeId,
+                        com.REMOTE_RESOURCE_ID: remote_object.id(),
+                        com.LOCAL_RESOURCE_ID: local_object.id()
+                    }
+                ).on(txn)
+
</ins><span class="cx">             # Sync meta-data such as schedule object, schedule tags, access mode etc
</span><span class="cx">             yield local_object.copyMetadata(remote_object)
</span><span class="cx"> 
</span><span class="cx">         # Purge the ones that remain
</span><span class="cx">         for local_object in local_objects.values():
</span><span class="cx">             yield local_object.purge()
</span><ins>+
+
+    @inlineCallbacks
+    def syncAttachments(self):
+        &quot;&quot;&quot;
+        Sync attachments (both metadata and actual attachment data) for the home being migrated.
+        &quot;&quot;&quot;
+
+        # Two steps - sync the table first in one txn, then sync each attachment's data
+        changed_ids, removed_ids = yield self.syncAttachmentTable()
+
+        for local_id in changed_ids:
+            yield self.syncAttachmentData(local_id)
+
+        returnValue((changed_ids, removed_ids,))
+
+
+    @inTransactionWrapper
+    @inlineCallbacks
+    def syncAttachmentTable(self, txn):
+        &quot;&quot;&quot;
+        Sync the ATTACHMENT table data for the home being migrated. Return the list of local attachment ids that
+        now need there attachment data sync'd from the server.
+        &quot;&quot;&quot;
+
+        remote_home = yield self._remoteHome(txn)
+        rattachments = yield remote_home.getAllAttachments()
+        rmap = dict([(attachment.id(), attachment) for attachment in rattachments])
+
+        local_home = yield txn.calendarHomeWithUID(self.migratingUid())
+        lattachments = yield local_home.getAllAttachments()
+        lmap = dict([(attachment.id(), attachment) for attachment in lattachments])
+
+        # Figure out the differences
+        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)
+        mapping = dict(rows)
+
+        # Removed - remove attachment and migration state
+        removed = set(mapping.keys()) - set(rmap.keys())
+        for remove_id in removed:
+            local_id = mapping[remove_id]
+            att = yield ManagedAttachment.load(txn, None, None, attachmentID=local_id)
+            if att:
+                yield att.remove(adjustQuota=False)
+            yield Delete(
+                From=am,
+                Where=(am.LOCAL_RESOURCE_ID == local_id),
+            ).on(txn)
+
+        # Track which ones need attachment data sync'd over
+        data_ids = set()
+
+        # Added - add new attachment and migration state
+        added = set(rmap.keys()) - set(mapping.keys())
+        for added_id in added:
+            attachment = yield ManagedAttachment._create(txn, None, self.homeId)
+            yield Insert(
+                {
+                    am.CALENDAR_HOME_RESOURCE_ID: self.homeId,
+                    am.REMOTE_RESOURCE_ID: added_id,
+                    am.LOCAL_RESOURCE_ID: attachment.id(),
+                }
+            ).on(txn)
+            data_ids.add(attachment.id())
+
+        # Possible updates - check for md5 change and sync
+        updates = set(mapping.keys()) &amp; set(rmap.keys())
+        for updated_id in updates:
+            local_id = mapping[updated_id]
+            if rmap[updated_id].md5() != lmap[local_id].md5():
+                yield lmap[local_id].copyRemote(rmap[updated_id])
+                data_ids.add(local_id)
+
+        returnValue((data_ids, removed,))
+
+
+    @inTransactionWrapper
+    @inlineCallbacks
+    def syncAttachmentData(self, txn, local_id):
+        &quot;&quot;&quot;
+        Sync the attachment data for the home being migrated.
+        &quot;&quot;&quot;
+
+        remote_home = yield self._remoteHome(txn)
+        local_home = yield txn.calendarHomeWithUID(self.migratingUid())
+        attachment = yield local_home.getAttachmentByID(local_id)
+        if attachment is None:
+            returnValue(None)
+
+        am = schema.ATTACHMENT_MIGRATION
+        rows = yield Select(
+            [am.LOCAL_RESOURCE_ID, am.REMOTE_RESOURCE_ID],
+            From=am,
+            Where=(am.CALENDAR_HOME_RESOURCE_ID == self.homeId),
+        ).on(txn)
+        mapping = dict(rows)
+        remote_id = mapping.get(local_id)
+        if remote_id is None:
+            returnValue(None)
+
+        # Read the data from the conduit
+        yield remote_home.readAttachmentData(remote_id, attachment)
</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 (14402 => 14403)</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-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/migration/test/test_home_sync.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -14,11 +14,15 @@
</span><span class="cx"> # limitations under the License.
</span><span class="cx"> ##
</span><span class="cx"> 
</span><ins>+from pycalendar.datetime import DateTime
+from twext.enterprise.dal.syntax import Select
</ins><span class="cx"> from twisted.internet.defer import inlineCallbacks
</span><ins>+from twistedcaldav.ical import Component, normalize_iCalStr
+from txdav.common.datastore.podding.migration.home_sync import CrossPodHomeSync
</ins><span class="cx"> from txdav.common.datastore.podding.test.util import MultiStoreConduitTest
</span><del>-from txdav.common.datastore.podding.migration.home_sync import CrossPodHomeSync
-from pycalendar.datetime import DateTime
-from twistedcaldav.ical import Component, normalize_iCalStr
</del><ins>+from txdav.common.datastore.sql_tables import schema
+from txweb2.http_headers import MimeType
+from txweb2.stream import MemoryStream
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class TestConduitAPI(MultiStoreConduitTest):
</span><span class="lines">@@ -246,10 +250,11 @@
</span><span class="cx"> 
</span><span class="cx">         home0 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name=&quot;user01&quot;, create=True)
</span><span class="cx">         calendar0 = yield home0.childWithName(&quot;calendar&quot;)
</span><del>-        yield calendar0.createCalendarObjectWithName(&quot;1.ics&quot;, Component.fromString(self.caldata1))
-        yield calendar0.createCalendarObjectWithName(&quot;2.ics&quot;, Component.fromString(self.caldata2))
-        yield calendar0.createCalendarObjectWithName(&quot;3.ics&quot;, Component.fromString(self.caldata3))
</del><ins>+        o1 = yield calendar0.createCalendarObjectWithName(&quot;1.ics&quot;, Component.fromString(self.caldata1))
+        o2 = yield calendar0.createCalendarObjectWithName(&quot;2.ics&quot;, Component.fromString(self.caldata2))
+        o3 = yield calendar0.createCalendarObjectWithName(&quot;3.ics&quot;, Component.fromString(self.caldata3))
</ins><span class="cx">         remote_id = calendar0.id()
</span><ins>+        mapping0 = dict([(o.name(), o.id()) for o in (o1, o2, o3)])
</ins><span class="cx">         yield self.commitTransaction(0)
</span><span class="cx"> 
</span><span class="cx">         syncer = CrossPodHomeSync(self.theStoreUnderTest(1), &quot;user01&quot;)
</span><span class="lines">@@ -273,12 +278,26 @@
</span><span class="cx">         self.assertEqual(len(local_sync_state), 1)
</span><span class="cx">         self.assertEqual(local_sync_state[remote_id].lastSyncToken, remote_sync_state[remote_id].lastSyncToken)
</span><span class="cx"> 
</span><ins>+        @inlineCallbacks
+        def _checkCalendarObjectMigrationState(home, mapping1):
+            com = schema.CALENDAR_OBJECT_MIGRATION
+            mappings = yield Select(
+                columns=[com.REMOTE_RESOURCE_ID, com.LOCAL_RESOURCE_ID],
+                From=com,
+                Where=(com.CALENDAR_HOME_RESOURCE_ID == home.id())
+            ).on(self.theTransactionUnderTest(1))
+            expected_mappings = dict([(mapping0[name], mapping1[name]) for name in mapping0.keys()])
+            self.assertEqual(dict(mappings), expected_mappings)
+
+
</ins><span class="cx">         # Local calendar exists
</span><span class="cx">         home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
</span><span class="cx">         calendar1 = yield home1.childWithName(&quot;calendar&quot;)
</span><span class="cx">         self.assertTrue(calendar1 is not None)
</span><del>-        children = yield calendar1.listObjectResources()
-        self.assertEqual(set(children), set((&quot;1.ics&quot;, &quot;2.ics&quot;, &quot;3.ics&quot;,)))
</del><ins>+        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;,)))
+        mapping1 = dict([(o.name(), o.id()) for o in children])
+        yield _checkCalendarObjectMigrationState(home1, mapping1)
</ins><span class="cx">         yield self.commitTransaction(1)
</span><span class="cx"> 
</span><span class="cx">         # Change one resource
</span><span class="lines">@@ -307,6 +326,7 @@
</span><span class="cx">             txn=self.theTransactionUnderTest(0), home=&quot;user01&quot;, calendar_name=&quot;calendar&quot;, name=&quot;2.ics&quot;
</span><span class="cx">         )
</span><span class="cx">         yield object0.remove()
</span><ins>+        del mapping0[&quot;2.ics&quot;]
</ins><span class="cx">         yield self.commitTransaction(0)
</span><span class="cx"> 
</span><span class="cx">         remote_sync_state = yield syncer.getCalendarSyncList()
</span><span class="lines">@@ -317,13 +337,16 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home=syncer.migratingUid(), name=&quot;calendar&quot;)
</span><del>-        children = yield calendar1.listObjectResources()
-        self.assertEqual(set(children), set((&quot;1.ics&quot;, &quot;3.ics&quot;,)))
</del><ins>+        children = yield calendar1.objectResources()
+        self.assertEqual(set([child.name() for child in children]), set((&quot;1.ics&quot;, &quot;3.ics&quot;,)))
+        mapping1 = dict([(o.name(), o.id()) for o in children])
+        yield _checkCalendarObjectMigrationState(home1, mapping1)
</ins><span class="cx">         yield self.commitTransaction(1)
</span><span class="cx"> 
</span><span class="cx">         # Add one resource
</span><span class="cx">         calendar0 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home=&quot;user01&quot;, name=&quot;calendar&quot;)
</span><del>-        yield calendar0.createCalendarObjectWithName(&quot;4.ics&quot;, Component.fromString(self.caldata4))
</del><ins>+        o4 = yield calendar0.createCalendarObjectWithName(&quot;4.ics&quot;, Component.fromString(self.caldata4))
+        mapping0[o4.name()] = o4.id()
</ins><span class="cx">         yield self.commitTransaction(0)
</span><span class="cx"> 
</span><span class="cx">         remote_sync_state = yield syncer.getCalendarSyncList()
</span><span class="lines">@@ -334,8 +357,10 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home=syncer.migratingUid(), name=&quot;calendar&quot;)
</span><del>-        children = yield calendar1.listObjectResources()
-        self.assertEqual(set(children), set((&quot;1.ics&quot;, &quot;3.ics&quot;, &quot;4.ics&quot;,)))
</del><ins>+        children = yield calendar1.objectResources()
+        self.assertEqual(set([child.name() for child in children]), set((&quot;1.ics&quot;, &quot;3.ics&quot;, &quot;4.ics&quot;)))
+        mapping1 = dict([(o.name(), o.id()) for o in children])
+        yield _checkCalendarObjectMigrationState(home1, mapping1)
</ins><span class="cx">         yield self.commitTransaction(1)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -400,3 +425,166 @@
</span><span class="cx">         self.assertTrue(&quot;new-calendar&quot; not in details1.values())
</span><span class="cx">         self.assertEqual(set(details1.values()), set(details0.values()))
</span><span class="cx">         yield self.commitTransaction(1)
</span><ins>+
+
+    @inlineCallbacks
+    def test_sync_attachments_add_remove(self):
+        &quot;&quot;&quot;
+        Test that L{syncAttachments} syncs attachment data, then an update to the data,
+        and finally a removal of the data.
+        &quot;&quot;&quot;
+
+
+        home0 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name=&quot;user01&quot;, create=True)
+        calendar0 = yield home0.childWithName(&quot;calendar&quot;)
+        yield calendar0.createCalendarObjectWithName(&quot;1.ics&quot;, Component.fromString(self.caldata1))
+        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()
+        mapping0 = dict()
+        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())
+        self.assertEqual(removed, set())
+
+        @inlineCallbacks
+        def _checkAttachmentObjectMigrationState(home, mapping1):
+            am = schema.ATTACHMENT_MIGRATION
+            mappings = yield Select(
+                columns=[am.REMOTE_RESOURCE_ID, am.LOCAL_RESOURCE_ID],
+                From=am,
+                Where=(am.CALENDAR_HOME_RESOURCE_ID == home.id())
+            ).on(self.theTransactionUnderTest(1))
+            expected_mappings = dict([(mapping0[name], mapping1[name]) for name in mapping0.keys()])
+            self.assertEqual(dict(mappings), expected_mappings)
+
+
+        # 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;,)))
+
+        attachments = yield home1.getAllAttachments()
+        mapping1 = dict([(o.md5(), o.id()) for o in attachments])
+        yield _checkAttachmentObjectMigrationState(home1, mapping1)
+        yield self.commitTransaction(1)
+
+        # Add one attachment
+        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 #1.&quot;))
+        id0_1 = attachment.id()
+        md50_1 = attachment.md5()
+        managedid0_1 = attachment.managedID()
+        mapping0[md50_1] = id0_1
+        yield self.commitTransaction(0)
+
+        # Sync attachments
+        changed, removed = yield syncer.syncAttachments()
+        self.assertEqual(changed, set((id0_1,)))
+        self.assertEqual(removed, set())
+
+        # Validate changes
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+        attachments = yield home1.getAllAttachments()
+        mapping1 = dict([(o.md5(), o.id()) for o in attachments])
+        yield _checkAttachmentObjectMigrationState(home1, mapping1)
+
+        # Add another attachment
+        object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home=&quot;user01&quot;, calendar_name=&quot;calendar&quot;, name=&quot;2.ics&quot;)
+        attachment, _ignore_location = yield object1.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()
+        mapping0[md50_2] = id0_2
+        yield self.commitTransaction(0)
+
+        # Sync attachments
+        changed, removed = yield syncer.syncAttachments()
+        self.assertEqual(changed, set((id0_2,)))
+        self.assertEqual(removed, set())
+
+        # Validate changes
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+        attachments = yield home1.getAllAttachments()
+        mapping1 = dict([(o.md5(), o.id()) for o in attachments])
+        yield _checkAttachmentObjectMigrationState(home1, mapping1)
+
+        # Change original attachment (this is actually a remove and a create all in one)
+        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.updateAttachment(managedid0_1, MimeType.fromString(&quot;text/plain&quot;), &quot;test.txt&quot;, MemoryStream(&quot;Here is some text #1 - changed.&quot;))
+        del mapping0[md50_1]
+        id0_1_changed = attachment.id()
+        md50_1_changed = attachment.md5()
+        managedid0_1_changed = attachment.managedID()
+        mapping0[md50_1_changed] = id0_1_changed
+        yield self.commitTransaction(0)
+
+        # Sync attachments
+        changed, removed = yield syncer.syncAttachments()
+        self.assertEqual(changed, set((id0_1_changed,)))
+        self.assertEqual(removed, set((id0_1,)))
+
+        # Validate changes
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+        attachments = yield home1.getAllAttachments()
+        mapping1 = dict([(o.md5(), o.id()) for o in attachments])
+        yield _checkAttachmentObjectMigrationState(home1, mapping1)
+
+        # 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)
+
+        # Sync attachments
+        changed, removed = yield syncer.syncAttachments()
+        self.assertEqual(changed, set())
+        self.assertEqual(removed, set())
+
+        # Validate changes
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+        attachments = yield home1.getAllAttachments()
+        mapping1 = dict([(o.md5(), o.id()) for o in attachments])
+        yield _checkAttachmentObjectMigrationState(home1, mapping1)
+
+        # Change original attachment in original resource (this creates a new one and does not remove the old)
+        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.updateAttachment(managedid0_1_changed, MimeType.fromString(&quot;text/plain&quot;), &quot;test.txt&quot;, MemoryStream(&quot;Here is some text #1 - changed again.&quot;))
+        id0_1_changed_again = attachment.id()
+        md50_1_changed_again = attachment.md5()
+        mapping0[md50_1_changed_again] = id0_1_changed_again
+        yield self.commitTransaction(0)
+
+        # Sync attachments
+        changed, removed = yield syncer.syncAttachments()
+        self.assertEqual(changed, set((id0_1_changed_again,)))
+        self.assertEqual(removed, set())
+
+        # Validate changes
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=syncer.migratingUid())
+        attachments = yield home1.getAllAttachments()
+        mapping1 = dict([(o.md5(), o.id()) for o in attachments])
+        yield _checkAttachmentObjectMigrationState(home1, mapping1)
</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 (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/request.py        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/request.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -23,7 +23,7 @@
</span><span class="cx"> from txweb2.client.http import HTTPClientProtocol, ClientRequest
</span><span class="cx"> from txweb2.dav.util import allDataFromStream
</span><span class="cx"> from txweb2.http_headers import Headers, MimeType
</span><del>-from txweb2.stream import MemoryStream
</del><ins>+from txweb2.stream import MemoryStream, readStream
</ins><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> from twisted.internet.protocol import Factory
</span><span class="lines">@@ -50,11 +50,12 @@
</span><span class="cx">     case the JSON data is sent in an HTTP header.
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-    def __init__(self, server, data, stream=None, stream_type=None):
</del><ins>+    def __init__(self, server, data, stream=None, stream_type=None, writeStream=None):
</ins><span class="cx">         self.server = server
</span><span class="cx">         self.data = json.dumps(data)
</span><span class="cx">         self.stream = stream
</span><span class="cx">         self.streamType = stream_type
</span><ins>+        self.writeStream = writeStream
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -72,7 +73,27 @@
</span><span class="cx">                 self.loggedResponse = yield self.logResponse(response)
</span><span class="cx">                 emitAccounting(&quot;xPod&quot;, &quot;&quot;, self.loggedRequest + &quot;\n&quot; + self.loggedResponse, &quot;POST&quot;)
</span><span class="cx"> 
</span><del>-            if response.code in (responsecode.OK, responsecode.BAD_REQUEST,):
</del><ins>+            if response.code == responsecode.OK:
+                if self.writeStream is None:
+                    data = (yield allDataFromStream(response.stream))
+                    data = json.loads(data)
+                else:
+                    yield readStream(response.stream, self.writeStream.write)
+                    yield self.writeStream.loseConnection()
+                    content_type = response.headers.getHeader(&quot;content-type&quot;)
+                    if content_type is None:
+                        content_type = MimeType(&quot;application&quot;, &quot;octet-stream&quot;)
+                    content_disposition = response.headers.getHeader(&quot;content-disposition&quot;)
+                    if content_disposition is None or &quot;filename&quot; not in content_disposition.params:
+                        filename = &quot;&quot;
+                    else:
+                        filename = content_disposition.params[&quot;filename&quot;]
+                    response = {
+                        &quot;result&quot;: &quot;ok&quot;,
+                        &quot;content-type&quot;: content_type,
+                        &quot;name&quot;: filename,
+                    }
+            elif response.code == responsecode.BAD_REQUEST:
</ins><span class="cx">                 data = (yield allDataFromStream(response.stream))
</span><span class="cx">                 data = json.loads(data)
</span><span class="cx">             else:
</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 (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/resource.py        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/resource.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -18,9 +18,11 @@
</span><span class="cx"> from txweb2.dav.noneprops import NonePropertyStore
</span><span class="cx"> from txweb2.dav.util import allDataFromStream
</span><span class="cx"> from txweb2.http import Response, HTTPError, StatusResponse, JSONResponse
</span><del>-from txweb2.http_headers import MimeType
</del><ins>+from txweb2.http_headers import MimeType, MimeDisposition
+from txweb2.stream import ProducerStream
</ins><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import succeed, returnValue, inlineCallbacks
</span><ins>+from twisted.internet.protocol import Protocol
</ins><span class="cx"> 
</span><span class="cx"> from twistedcaldav.extensions import DAVResource, \
</span><span class="cx">     DAVResourceWithoutChildrenMixin
</span><span class="lines">@@ -154,19 +156,54 @@
</span><span class="cx">             request.extendedLogItems = {}
</span><span class="cx">         request.extendedLogItems[&quot;xpod&quot;] = j[&quot;action&quot;] if &quot;action&quot; in j else &quot;unknown&quot;
</span><span class="cx"> 
</span><del>-        # Get the conduit to process the data
-        try:
-            result = yield self.store.conduit.processRequest(j)
-            code = responsecode.OK if result[&quot;result&quot;] == &quot;ok&quot; else responsecode.BAD_REQUEST
-        except Exception as e:
-            # Send the exception over to the other side
-            result = {
-                &quot;result&quot;: &quot;exception&quot;,
-                &quot;class&quot;: &quot;.&quot;.join((e.__class__.__module__, e.__class__.__name__,)),
-                &quot;request&quot;: str(e),
-            }
-            code = responsecode.BAD_REQUEST
</del><ins>+        # Look for a streaming action which needs special handling
+        if self.store.conduit.isStreamAction(j):
+            # Get the conduit to process the data stream
+            try:
</ins><span class="cx"> 
</span><ins>+                stream = ProducerStream()
+                class StreamProtocol(Protocol):
+                    def connectionMade(self):
+                        stream.registerProducer(self.transport, False)
+                    def dataReceived(self, data):
+                        stream.write(data)
+                    def connectionLost(self, reason):
+                        stream.finish()
+
+                result = yield self.store.conduit.processRequestStream(j, StreamProtocol())
+
+                try:
+                    ct, name = result
+                except ValueError:
+                    code = responsecode.BAD_REQUEST
+                else:
+                    headers = {&quot;content-type&quot;: ct}
+                    headers[&quot;content-disposition&quot;] = MimeDisposition(&quot;attachment&quot;, params={&quot;filename&quot;: name})
+                    returnValue(Response(responsecode.OK, headers, stream))
+
+            except Exception as e:
+                # Send the exception over to the other side
+                result = {
+                    &quot;result&quot;: &quot;exception&quot;,
+                    &quot;class&quot;: &quot;.&quot;.join((e.__class__.__module__, e.__class__.__name__,)),
+                    &quot;details&quot;: str(e),
+                }
+                code = responsecode.BAD_REQUEST
+
+        else:
+            # Get the conduit to process the data
+            try:
+                result = yield self.store.conduit.processRequest(j)
+                code = responsecode.OK if result[&quot;result&quot;] == &quot;ok&quot; else responsecode.BAD_REQUEST
+            except Exception as e:
+                # Send the exception over to the other side
+                result = {
+                    &quot;result&quot;: &quot;exception&quot;,
+                    &quot;class&quot;: &quot;.&quot;.join((e.__class__.__module__, e.__class__.__name__,)),
+                    &quot;details&quot;: str(e),
+                }
+                code = responsecode.BAD_REQUEST
+
</ins><span class="cx">         response = JSONResponse(code, result)
</span><span class="cx">         returnValue(response)
</span><span class="cx"> 
</span></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 (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/test_conduit.py        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/test_conduit.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -1056,3 +1056,54 @@
</span><span class="cx">         attachment = yield ManagedAttachment.load(self.theTransactionUnderTest(0), resourceID, managedID)
</span><span class="cx">         self.assertTrue(attachment is None)
</span><span class="cx">         yield self.commitTransaction(0)
</span><ins>+
+
+    @inlineCallbacks
+    def test_get_all_attachments(self):
+        &quot;&quot;&quot;
+        Test that action=get-all-attachments 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;)
+        yield calendar1.createCalendarObjectWithName(&quot;1.ics&quot;, Component.fromString(self.caldata1))
+        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;)
+        yield object1.addAttachment(None, MimeType.fromString(&quot;text/plain&quot;), &quot;test.txt&quot;, MemoryStream(&quot;Here is some text.&quot;))
+        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;)
+        attachments = yield shared_object.ownerHome().getAllAttachments()
+        self.assertEqual(len(attachments), 1)
+        self.assertTrue(isinstance(attachments[0], ManagedAttachment))
+        self.assertEqual(attachments[0].contentType(), MimeType.fromString(&quot;text/plain&quot;))
+        self.assertEqual(attachments[0].name(), &quot;test.txt&quot;)
+        yield self.commitTransaction(1)
+
+
+    @inlineCallbacks
+    def test_get_attachment_data(self):
+        &quot;&quot;&quot;
+        Test that action=get-all-attachments 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;)
+        yield calendar1.createCalendarObjectWithName(&quot;1.ics&quot;, Component.fromString(self.caldata1))
+        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;))
+        remote_id = attachment.id()
+        yield self.commitTransaction(0)
+
+        home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name=&quot;puser01&quot;)
+        shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home=&quot;puser01&quot;, calendar_name=&quot;shared-calendar&quot;, name=&quot;1.ics&quot;)
+        attachment = yield ManagedAttachment._create(self.theTransactionUnderTest(1), None, home1.id())
+        attachment._contentType = MimeType.fromString(&quot;text/plain&quot;)
+        attachment._name = &quot;test.txt&quot;
+        yield shared_object.ownerHome().readAttachmentData(remote_id, attachment)
+        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 (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/util.py        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/podding/test/util.py        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -15,6 +15,7 @@
</span><span class="cx"> ##
</span><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><ins>+from twisted.internet.protocol import Protocol
</ins><span class="cx"> 
</span><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.localservers import (
</span><span class="cx">     Server, ServersDB
</span><span class="lines">@@ -26,6 +27,7 @@
</span><span class="cx"> )
</span><span class="cx"> 
</span><span class="cx"> import txweb2.dav.test.util
</span><ins>+from txweb2.stream import ProducerStream, readStream
</ins><span class="cx"> 
</span><span class="cx"> from twext.enterprise.ienterprise import AlreadyFinishedError
</span><span class="cx"> 
</span><span class="lines">@@ -54,12 +56,13 @@
</span><span class="cx">         cls.storeMap[server.details()] = store
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def __init__(self, server, data, stream=None, stream_type=None):
</del><ins>+    def __init__(self, server, data, stream=None, stream_type=None, writeStream=None):
</ins><span class="cx"> 
</span><span class="cx">         self.server = server
</span><span class="cx">         self.data = json.dumps(data)
</span><span class="cx">         self.stream = stream
</span><span class="cx">         self.streamType = stream_type
</span><ins>+        self.writeStream = writeStream
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -68,7 +71,20 @@
</span><span class="cx">         # Generate an HTTP client request
</span><span class="cx">         try:
</span><span class="cx">             response = (yield self._processRequest())
</span><del>-            response = json.loads(response)
</del><ins>+            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
</ins><span class="cx">         except Exception as e:
</span><span class="cx">             raise ValueError(&quot;Failed cross-pod request: {}&quot;.format(e))
</span><span class="cx"> 
</span><span class="lines">@@ -90,13 +106,32 @@
</span><span class="cx">             j[&quot;stream&quot;] = self.stream
</span><span class="cx">             j[&quot;streamType&quot;] = self.streamType
</span><span class="cx">         try:
</span><del>-            result = yield store.conduit.processRequest(j)
</del><ins>+            if store.conduit.isStreamAction(j):
+                stream = ProducerStream()
+                class StreamProtocol(Protocol):
+                    def connectionMade(self):
+                        stream.registerProducer(self.transport, False)
+                    def dataReceived(self, data):
+                        stream.write(data)
+                    def connectionLost(self, reason):
+                        stream.finish()
+
+                result = yield store.conduit.processRequestStream(j, StreamProtocol())
+
+                try:
+                    ct, name = result
+                except ValueError:
+                    pass
+                else:
+                    returnValue((ct, name, stream,))
+            else:
+                result = yield store.conduit.processRequest(j)
</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="cx">                 &quot;result&quot;: &quot;exception&quot;,
</span><span class="cx">                 &quot;class&quot;: &quot;.&quot;.join((e.__class__.__module__, e.__class__.__name__,)),
</span><del>-                &quot;request&quot;: str(e),
</del><ins>+                &quot;details&quot;: str(e),
</ins><span class="cx">             }
</span><span class="cx">         result = json.dumps(result)
</span><span class="cx">         returnValue(result)
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastoresql_schemacurrentoracledialectsql"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current-oracle-dialect.sql (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current-oracle-dialect.sql        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current-oracle-dialect.sql        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -69,7 +69,7 @@
</span><span class="cx">     &quot;MODIFIED&quot; timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
</span><span class="cx"> );
</span><span class="cx"> 
</span><del>-create table CALENDAR_MIGRATION_STATE (
</del><ins>+create table CALENDAR_MIGRATION (
</ins><span class="cx">     &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer references CALENDAR_HOME on delete cascade,
</span><span class="cx">     &quot;REMOTE_RESOURCE_ID&quot; integer not null,
</span><span class="cx">     &quot;CALENDAR_RESOURCE_ID&quot; integer references CALENDAR on delete cascade,
</span><span class="lines">@@ -217,6 +217,13 @@
</span><span class="cx">     primary key (&quot;TIME_RANGE_INSTANCE_ID&quot;, &quot;USER_ID&quot;)
</span><span class="cx"> );
</span><span class="cx"> 
</span><ins>+create table CALENDAR_OBJECT_MIGRATION (
+    &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer references CALENDAR_HOME on delete cascade,
+    &quot;REMOTE_RESOURCE_ID&quot; integer not null,
+    &quot;LOCAL_RESOURCE_ID&quot; integer references CALENDAR_OBJECT on delete cascade, 
+    primary key (&quot;CALENDAR_HOME_RESOURCE_ID&quot;, &quot;REMOTE_RESOURCE_ID&quot;)
+);
+
</ins><span class="cx"> create table ATTACHMENT (
</span><span class="cx">     &quot;ATTACHMENT_ID&quot; integer primary key,
</span><span class="cx">     &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer not null references CALENDAR_HOME,
</span><span class="lines">@@ -237,6 +244,13 @@
</span><span class="cx">     unique (&quot;MANAGED_ID&quot;, &quot;CALENDAR_OBJECT_RESOURCE_ID&quot;)
</span><span class="cx"> );
</span><span class="cx"> 
</span><ins>+create table ATTACHMENT_MIGRATION (
+    &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer references CALENDAR_HOME on delete cascade,
+    &quot;REMOTE_RESOURCE_ID&quot; integer not null,
+    &quot;LOCAL_RESOURCE_ID&quot; integer references ATTACHMENT on delete cascade, 
+    primary key (&quot;CALENDAR_HOME_RESOURCE_ID&quot;, &quot;REMOTE_RESOURCE_ID&quot;)
+);
+
</ins><span class="cx"> create table RESOURCE_PROPERTY (
</span><span class="cx">     &quot;RESOURCE_ID&quot; integer not null,
</span><span class="cx">     &quot;NAME&quot; nvarchar2(255),
</span><span class="lines">@@ -633,7 +647,7 @@
</span><span class="cx">     DEFAULT_POLLS
</span><span class="cx"> );
</span><span class="cx"> 
</span><del>-create index CALENDAR_MIGRATION_ST_57f40e9a on CALENDAR_MIGRATION_STATE (
</del><ins>+create index CALENDAR_MIGRATION_CA_cc68f4ec on CALENDAR_MIGRATION (
</ins><span class="cx">     CALENDAR_RESOURCE_ID
</span><span class="cx"> );
</span><span class="cx"> 
</span><span class="lines">@@ -672,6 +686,15 @@
</span><span class="cx">     CALENDAR_OBJECT_RESOURCE_ID
</span><span class="cx"> );
</span><span class="cx"> 
</span><ins>+create index CALENDAR_OBJECT_MIGRA_0502cbef on CALENDAR_OBJECT_MIGRATION (
+    CALENDAR_HOME_RESOURCE_ID,
+    LOCAL_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_MIGRA_3577efd9 on CALENDAR_OBJECT_MIGRATION (
+    LOCAL_RESOURCE_ID
+);
+
</ins><span class="cx"> create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
</span><span class="cx">     CALENDAR_HOME_RESOURCE_ID
</span><span class="cx"> );
</span><span class="lines">@@ -684,6 +707,15 @@
</span><span class="cx">     CALENDAR_OBJECT_RESOURCE_ID
</span><span class="cx"> );
</span><span class="cx"> 
</span><ins>+create index ATTACHMENT_MIGRATION__804bf85e on ATTACHMENT_MIGRATION (
+    CALENDAR_HOME_RESOURCE_ID,
+    LOCAL_RESOURCE_ID
+);
+
+create index ATTACHMENT_MIGRATION__816947fe on ATTACHMENT_MIGRATION (
+    LOCAL_RESOURCE_ID
+);
+
</ins><span class="cx"> create index SHARED_ADDRESSBOOK_BI_e9a2e6d4 on SHARED_ADDRESSBOOK_BIND (
</span><span class="cx">     OWNER_HOME_RESOURCE_ID
</span><span class="cx"> );
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastoresql_schemacurrentsql"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/current.sql        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -140,7 +140,7 @@
</span><span class="cx"> -- Calendar Migration --
</span><span class="cx"> ------------------------
</span><span class="cx"> 
</span><del>-create table CALENDAR_MIGRATION_STATE (
</del><ins>+create table CALENDAR_MIGRATION (
</ins><span class="cx">   CALENDAR_HOME_RESOURCE_ID        integer references CALENDAR_HOME on delete cascade,
</span><span class="cx">   REMOTE_RESOURCE_ID                        integer not null,
</span><span class="cx">   CALENDAR_RESOURCE_ID                        integer        references CALENDAR on delete cascade,
</span><span class="lines">@@ -149,8 +149,8 @@
</span><span class="cx">   primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
</span><span class="cx"> );
</span><span class="cx"> 
</span><del>-create index CALENDAR_MIGRATION_STATE_CALENDAR_RESOURCE_ID on
-  CALENDAR_MIGRATION_STATE(CALENDAR_RESOURCE_ID);
</del><ins>+create index CALENDAR_MIGRATION_CALENDAR_RESOURCE_ID on
+  CALENDAR_MIGRATION(CALENDAR_RESOURCE_ID);
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> ---------------------------
</span><span class="lines">@@ -381,6 +381,24 @@
</span><span class="cx"> );
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+-------------------------------
+-- Calendar Object Migration --
+-------------------------------
+
+create table CALENDAR_OBJECT_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID                integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID                        integer not null,
+  LOCAL_RESOURCE_ID                                integer        references CALENDAR_OBJECT on delete cascade,
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
+
+create index CALENDAR_OBJECT_MIGRATION_HOME_LOCAL on
+  CALENDAR_OBJECT_MIGRATION(CALENDAR_HOME_RESOURCE_ID, LOCAL_RESOURCE_ID);
+create index CALENDAR_OBJECT_MIGRATION_LOCAL_RESOURCE_ID on
+  CALENDAR_OBJECT_MIGRATION(LOCAL_RESOURCE_ID);
+
+
</ins><span class="cx"> ----------------
</span><span class="cx"> -- Attachment --
</span><span class="cx"> ----------------
</span><span class="lines">@@ -418,6 +436,24 @@
</span><span class="cx"> create index ATTACHMENT_CALENDAR_OBJECT_CALENDAR_OBJECT_RESOURCE_ID on
</span><span class="cx">   ATTACHMENT_CALENDAR_OBJECT(CALENDAR_OBJECT_RESOURCE_ID);
</span><span class="cx"> 
</span><ins>+-----------------------------------
+-- Calendar Attachment Migration --
+-----------------------------------
+
+create table ATTACHMENT_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID                integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID                        integer not null,
+  LOCAL_RESOURCE_ID                                integer        references ATTACHMENT on delete cascade,
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
+
+create index ATTACHMENT_MIGRATION_HOME_LOCAL on
+  ATTACHMENT_MIGRATION(CALENDAR_HOME_RESOURCE_ID, LOCAL_RESOURCE_ID);
+create index ATTACHMENT_MIGRATION_LOCAL_RESOURCE_ID on
+  ATTACHMENT_MIGRATION(LOCAL_RESOURCE_ID);
+
+
</ins><span class="cx"> -----------------------
</span><span class="cx"> -- Resource Property --
</span><span class="cx"> -----------------------
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastoresql_schemaupgradesoracledialectupgrade_from_51_to_52sql"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_51_to_52.sql (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_51_to_52.sql        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_51_to_52.sql        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -22,7 +22,7 @@
</span><span class="cx"> insert into HOME_STATUS (DESCRIPTION, ID) values ('migrating', 3);
</span><span class="cx"> 
</span><span class="cx"> -- New table
</span><del>-create table CALENDAR_MIGRATION_STATE (
</del><ins>+create table CALENDAR_MIGRATION (
</ins><span class="cx">     &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer references CALENDAR_HOME on delete cascade,
</span><span class="cx">     &quot;REMOTE_RESOURCE_ID&quot; integer not null,
</span><span class="cx">     &quot;CALENDAR_RESOURCE_ID&quot; integer references CALENDAR on delete cascade,
</span><span class="lines">@@ -30,10 +30,42 @@
</span><span class="cx">     primary key (&quot;CALENDAR_HOME_RESOURCE_ID&quot;, &quot;REMOTE_RESOURCE_ID&quot;)
</span><span class="cx"> );
</span><span class="cx"> 
</span><del>-create index CALENDAR_MIGRATION_ST_57f40e9a on CALENDAR_MIGRATION_STATE (
</del><ins>+create index CALENDAR_MIGRATION_CA_cc68f4ec on CALENDAR_MIGRATION (
</ins><span class="cx">     CALENDAR_RESOURCE_ID
</span><span class="cx"> );
</span><span class="cx"> 
</span><ins>+-- New table
+create table CALENDAR_OBJECT_MIGRATION (
+    &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer references CALENDAR_HOME on delete cascade,
+    &quot;REMOTE_RESOURCE_ID&quot; integer not null,
+    &quot;LOCAL_RESOURCE_ID&quot; integer references CALENDAR_OBJECT on delete cascade, 
+    primary key (&quot;CALENDAR_HOME_RESOURCE_ID&quot;, &quot;REMOTE_RESOURCE_ID&quot;)
+);
</ins><span class="cx"> 
</span><ins>+create index CALENDAR_OBJECT_MIGRA_0502cbef on CALENDAR_OBJECT_MIGRATION (
+    CALENDAR_HOME_RESOURCE_ID,
+    LOCAL_RESOURCE_ID
+);
+create index CALENDAR_OBJECT_MIGRA_3577efd9 on CALENDAR_OBJECT_MIGRATION (
+    LOCAL_RESOURCE_ID
+);
+
+-- New table
+create table ATTACHMENT_MIGRATION (
+    &quot;CALENDAR_HOME_RESOURCE_ID&quot; integer references CALENDAR_HOME on delete cascade,
+    &quot;REMOTE_RESOURCE_ID&quot; integer not null,
+    &quot;LOCAL_RESOURCE_ID&quot; integer references ATTACHMENT on delete cascade, 
+    primary key (&quot;CALENDAR_HOME_RESOURCE_ID&quot;, &quot;REMOTE_RESOURCE_ID&quot;)
+);
+
+create index ATTACHMENT_MIGRATION__804bf85e on ATTACHMENT_MIGRATION (
+    CALENDAR_HOME_RESOURCE_ID,
+    LOCAL_RESOURCE_ID
+);
+create index ATTACHMENT_MIGRATION__816947fe on ATTACHMENT_MIGRATION (
+    LOCAL_RESOURCE_ID
+);
+
+
</ins><span class="cx"> -- update the version
</span><span class="cx"> update CALENDARSERVER set VALUE = '52' where NAME = 'VERSION';
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboopod2podmigrationtxdavcommondatastoresql_schemaupgradespostgresdialectupgrade_from_51_to_52sql"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_51_to_52.sql (14402 => 14403)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_51_to_52.sql        2015-02-11 19:55:49 UTC (rev 14402)
+++ CalendarServer/branches/users/cdaboo/pod2pod-migration/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_51_to_52.sql        2015-02-11 20:34:55 UTC (rev 14403)
</span><span class="lines">@@ -22,7 +22,7 @@
</span><span class="cx"> insert into HOME_STATUS values (3, 'migrating');
</span><span class="cx"> 
</span><span class="cx"> -- New table
</span><del>-create table CALENDAR_MIGRATION_STATE (
</del><ins>+create table CALENDAR_MIGRATION (
</ins><span class="cx">   CALENDAR_HOME_RESOURCE_ID        integer references CALENDAR_HOME on delete cascade,
</span><span class="cx">   REMOTE_RESOURCE_ID                        integer not null,
</span><span class="cx">   CALENDAR_RESOURCE_ID                        integer        references CALENDAR on delete cascade,
</span><span class="lines">@@ -31,9 +31,39 @@
</span><span class="cx">   primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
</span><span class="cx"> );
</span><span class="cx"> 
</span><del>-create index CALENDAR_MIGRATION_STATE_CALENDAR_RESOURCE_ID on
-  CALENDAR_MIGRATION_STATE(CALENDAR_RESOURCE_ID);
</del><ins>+create index CALENDAR_MIGRATION_CALENDAR_RESOURCE_ID on
+  CALENDAR_MIGRATION(CALENDAR_RESOURCE_ID);
</ins><span class="cx"> 
</span><ins>+  
+-- New table
+create table CALENDAR_OBJECT_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID                integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID                        integer not null,
+  LOCAL_RESOURCE_ID                                integer        references CALENDAR_OBJECT on delete cascade,
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
</ins><span class="cx"> 
</span><ins>+create index CALENDAR_OBJECT_MIGRATION_HOME_LOCAL on
+  CALENDAR_OBJECT_MIGRATION(CALENDAR_HOME_RESOURCE_ID, LOCAL_RESOURCE_ID);
+create index CALENDAR_OBJECT_MIGRATION_LOCAL_RESOURCE_ID on
+  CALENDAR_OBJECT_MIGRATION(LOCAL_RESOURCE_ID);
+
+  
+-- New table
+create table ATTACHMENT_MIGRATION (
+  CALENDAR_HOME_RESOURCE_ID                integer references CALENDAR_HOME on delete cascade,
+  REMOTE_RESOURCE_ID                        integer not null,
+  LOCAL_RESOURCE_ID                                integer        references ATTACHMENT on delete cascade,
+   
+  primary key (CALENDAR_HOME_RESOURCE_ID, REMOTE_RESOURCE_ID) -- implicit index
+);
+
+create index ATTACHMENT_MIGRATION_HOME_LOCAL on
+  ATTACHMENT_MIGRATION(CALENDAR_HOME_RESOURCE_ID, LOCAL_RESOURCE_ID);
+create index ATTACHMENT_MIGRATION_LOCAL_RESOURCE_ID on
+  ATTACHMENT_MIGRATION(LOCAL_RESOURCE_ID);
+
+
</ins><span class="cx"> -- update the version
</span><span class="cx"> update CALENDARSERVER set VALUE = '52' where NAME = 'VERSION';
</span></span></pre>
</div>
</div>

</body>
</html>