[CalendarServer-changes] [10294] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Jan 8 14:07:49 PST 2013


Revision: 10294
          http://trac.calendarserver.org//changeset/10294
Author:   cdaboo at apple.com
Date:     2013-01-08 14:07:49 -0800 (Tue, 08 Jan 2013)
Log Message:
-----------
Initial attachment migration work. This is not actually hooked up to do the migration yet.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/common.py
    CalendarServer/trunk/txdav/caldav/icalendarstore.py

Added Paths:
-----------
    CalendarServer/trunk/txdav/caldav/datastore/test/attachments/
    CalendarServer/trunk/txdav/caldav/datastore/test/attachments/accounts.xml
    CalendarServer/trunk/txdav/caldav/datastore/test/attachments/resources.xml
    CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2013-01-08 21:57:32 UTC (rev 10293)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2013-01-08 22:07:49 UTC (rev 10294)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.test.test_icalendar -*-
 ##
-# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -954,6 +954,18 @@
         self._markAsDirty()
 
 
+    def removePropertiesWithName(self, pname):
+        """
+        Remove all properties with the given name from this component.
+
+        @param pname: the property name to remove from this component.
+        @type pname: C{str}
+        """
+
+        for property in self.properties(pname):
+            self.removeProperty(property)
+
+
     def replaceProperty(self, property):
         """
         Add or replace a property in this component.

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-01-08 21:57:32 UTC (rev 10293)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-01-08 22:07:49 UTC (rev 10294)
@@ -1,6 +1,6 @@
 # -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
 ##
-# Copyright (c) 2010-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2010-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -60,7 +60,8 @@
 from txdav.caldav.datastore.util import validateCalendarComponent, \
     dropboxIDFromCalendarObject
 from txdav.caldav.icalendarstore import ICalendarHome, ICalendar, ICalendarObject, \
-    IAttachment, AttachmentStoreFailed, AttachmentStoreValidManagedID
+    IAttachment, AttachmentStoreFailed, AttachmentStoreValidManagedID, \
+    AttachmentMigrationFailed
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE
@@ -71,7 +72,7 @@
     _ATTACHMENTS_MODE_NONE, _ATTACHMENTS_MODE_READ, _ATTACHMENTS_MODE_WRITE, \
     CALENDAR_HOME_TABLE, CALENDAR_HOME_METADATA_TABLE, \
     CALENDAR_AND_CALENDAR_BIND, CALENDAR_OBJECT_REVISIONS_AND_BIND_TABLE, \
-    CALENDAR_OBJECT_AND_BIND_TABLE, schema
+    CALENDAR_OBJECT_AND_BIND_TABLE, schema, _BIND_MODE_OWN
 from txdav.common.icommondatastore import IndexedSearchException, \
     InternalDataStoreError, HomeChildNameAlreadyExistsError, \
     HomeChildNameNotAllowedError
@@ -89,6 +90,147 @@
 import tempfile
 import uuid
 
+class CalendarStoreFeatures(object):
+    """
+    Manages store-wide operations specific to calendars.
+    """
+
+    def __init__(self, store):
+        """
+        @param store: the underlying store object to use.
+        @type store: L{twext.common.datastore.sql.CommonDataStore}
+        """
+        self._store = store
+
+
+    @inlineCallbacks
+    def upgradeToManagedAttachments(self, txn, batchSize=10):
+        """
+        Upgrade the calendar server from old-style dropbox attachments to the new
+        managed attachments. This is a one-way, one-time migration step.
+
+        Things to do:
+
+        1. For any CALENDAR_OBJECT rows with a DROPBOX_ID not matching an existing DROPBOX_ID
+        in the ATTACHMENT table, null out CALENDAR_OBJECT.DROPBOX_ID. Do not rewrite calendar
+        data to remove X-APPLE-DROPBOX.
+
+        2. For each item in the ATTACHMENT table, convert into a managed attachment and re-write
+        all calendar data referring to that attachment.
+
+        TODO: parallelize this as much as possible as it will have to touch a lot of data.
+        """
+
+        # Clear out unused CALENDAR_OBJECT.DROPBOX_IDs
+        co = schema.CALENDAR_OBJECT
+        at = schema.ATTACHMENT
+        yield Update(
+            {co.DROPBOX_ID: None},
+            Where=co.RESOURCE_ID.In(Select(
+                (co.RESOURCE_ID,),
+                From=co.join(at, co.DROPBOX_ID == at.DROPBOX_ID, "left outer"),
+                Where=(co.DROPBOX_ID != None).And(at.DROPBOX_ID == None),
+            )),
+        ).on(txn)
+
+        # For each remaining attachment
+        while True:
+            rows = (yield Select(
+                (at.DROPBOX_ID,),
+                From=at,
+                Where=at.DROPBOX_ID != ".",
+                Limit=batchSize,
+            ).on(txn))
+            if len(rows) == 0:
+                break
+            for dropbox_id in rows:
+                (yield self._upgradeDropbox(txn, dropbox_id))
+
+
+    @inlineCallbacks
+    def _upgradeDropbox(self, txn, dropbox_id):
+        """
+        Upgrade attachments for the corresponding dropbox box to managed attachments. This is tricky
+        in that we have to spot the case of a dropbox attachment being used by more than one event
+        in the owner's home (e.g., the case of a recurrence split). We have to give each owned event
+        its own managed attachment reference (though they point to the same actual attachment data).
+        So we need to detect owned attachments and group by UID.
+
+        @param dropbox_id: the dropbox id to upgrade
+        @type dropbox_id: C{str}
+        """
+
+        # Get all affected calendar objects
+        cobjs = (yield self._loadCalendarObjectsForDropboxID(txn, dropbox_id))
+
+        # Get names of each matching attachment
+        at = schema.ATTACHMENT
+        names = (yield Select(
+            (at.PATH,),
+            From=at,
+            Where=at.DROPBOX_ID == dropbox_id,
+        ).on(txn))
+
+        # For each attachment, update each calendar object
+        for name in names:
+            attachment = (yield DropBoxAttachment.load(txn, dropbox_id, name))
+
+            # Find owner objects and group all by UID
+            owners = []
+            cobj_by_UID = collections.defaultdict(list)
+            for cobj in cobjs:
+                if cobj._parentCollection.ownerHome()._resourceID == attachment._ownerHomeID:
+                    owners.append(cobj)
+                cobj_by_UID[cobj.uid()].append(cobj)
+
+            if owners:
+                # Create the managed attachment without references to calendar objects.
+                managed = (yield attachment.convertToManaged())
+
+                # Do conversion for each owner object
+                for owner_obj in owners:
+
+                    # Add a reference to the managed attachment
+                    mattachment = (yield managed.newReference(owner_obj._resourceID))
+
+                    # Rewrite calendar data
+                    for cobj in cobj_by_UID[owner_obj.uid()]:
+                        (yield cobj.convertAttachments(attachment, mattachment))
+            else:
+                # TODO: look for cobjs that were not changed and remove their ATTACH properties.
+                # These could happen if the owner object no longer exists.
+                pass
+
+
+    @inlineCallbacks
+    def _loadCalendarObjectsForDropboxID(self, txn, dropbox_id):
+        """
+        Load all calendar objects (and associated calendars and homes) that match the
+        specified dropbox id.
+
+        @param dropbox_id: the dropbox id to match.
+        @type dropbox_id: C{str}
+        """
+
+        co = schema.CALENDAR_OBJECT
+        cb = schema.CALENDAR_BIND
+        rows = (yield Select(
+            (cb.CALENDAR_HOME_RESOURCE_ID, co.CALENDAR_RESOURCE_ID, co.RESOURCE_ID,),
+            From=co.join(cb, co.CALENDAR_RESOURCE_ID == cb.CALENDAR_RESOURCE_ID),
+            Where=(co.DROPBOX_ID == dropbox_id).And(cb.BIND_MODE == _BIND_MODE_OWN)
+        ).on(txn))
+
+        results = []
+        for home_rid, calendar_rid, cobj_rid in rows:
+            home = (yield txn.calendarHomeWithResourceID(home_rid))
+            calendar = (yield home.childWithID(calendar_rid))
+            cobj = (yield calendar.objectResourceWithID(cobj_rid))
+            results.append(cobj)
+
+        returnValue(results)
+
+
+
 class CalendarHome(CommonHome):
 
     implements(ICalendarHome)
@@ -940,12 +1082,13 @@
                 if component.hasPropertyInAnyComponent("X-APPLE-DROPBOX"):
                     self._attachment = _ATTACHMENTS_MODE_WRITE
                     self._dropboxID = (yield self.dropboxID())
-                elif component.hasPropertyInAnyComponent("ATTACH"):
-                    # FIXME: really we ought to check to see if the ATTACH
-                    # properties have URI values and if those are pointing to our
-                    # server dropbox collections and only then set the read mode
-                    self._attachment = _ATTACHMENTS_MODE_READ
-                    self._dropboxID = (yield self.dropboxID())
+                else:
+                    # Only include a dropbox id if dropbox attachments exist
+                    attachments = component.getAllPropertiesInAnyComponent("ATTACH")
+                    has_dropbox = any([attachment.value().find("/dropbox/") != -1 for attachment in attachments])
+                    if has_dropbox:
+                        self._attachment = _ATTACHMENTS_MODE_READ
+                        self._dropboxID = (yield self.dropboxID())
 
             values = {
                 co.CALENDAR_RESOURCE_ID            : self._calendar._resourceID,
@@ -1484,14 +1627,7 @@
         # Now try and adjust the actual calendar data
         #calendar = (yield self.component())
 
-        location = (yield attachment.location())
-        attach = Property("ATTACH", location, params={
-            "MANAGED-ID": attachment.managedID(),
-            "MTAG": attachment.md5(),
-            "FMTTYPE": "%s/%s" % (attachment.contentType().mediaType, attachment.contentType().mediaSubtype),
-            "FILENAME": attachment.name(),
-            "SIZE": str(attachment.size()),
-        }, valuetype=PyCalendarValue.VALUETYPE_URI)
+        attach, location = (yield attachment.attachProperty())
         if rids is None:
             calendar.addPropertyToAllComponents(attach)
         else:
@@ -1536,17 +1672,7 @@
         # Now try and adjust the actual calendar data
         #calendar = (yield self.component())
 
-        location = self._txn._store.attachmentsURIPattern % {
-            "home": self._parentCollection.ownerHome().name(),
-            "name": attachment.managedID(),
-        }
-        attach = Property("ATTACH", location, params={
-            "MANAGED-ID": attachment.managedID(),
-            "MTAG": attachment.md5(),
-            "FMTTYPE": "%s/%s" % (attachment.contentType().mediaType, attachment.contentType().mediaSubtype),
-            "FILENAME": attachment.name(),
-            "SIZE": str(attachment.size()),
-        }, valuetype=PyCalendarValue.VALUETYPE_URI)
+        attach, location = (yield attachment.attachProperty())
         calendar.replaceAllPropertiesWithParameterMatch(attach, "MANAGED-ID", managed_id)
 
         # TODO: Here is where we want to store data implicitly - for now we have to let app layer deal with it
@@ -1583,6 +1709,45 @@
 
 
     @inlineCallbacks
+    def convertAttachments(self, oldattachment, newattachment):
+        """
+        Convert ATTACH properties in the calendar data from a dropbox attachment to a managed attachment.
+        This is only used when migrating from dropbox to managed attachments. The ATTACH/ATTACH_CALENDAR_OBJECT
+        DB tables have already been updated to reflect the new managed attachment entry, however the CALENDAR_OBJECT.
+        DROPBOX_ID column has not.
+
+        @param oldattachment: the old dropbox attachment being converted
+        @type oldattachment: L{DropBoxAttachment}
+        @param newattachment: the new managed attachment
+        @type newattachment: L{ManagedAttachment}
+        """
+
+        # Scan each component looking for an ATTACH matching the old dropbox, remove
+        # that and add a new managed ATTACH property
+        cal = (yield self.component())
+        for component in cal.subcomponents():
+            attachments = component.properties("ATTACH")
+            removed = False
+            still_contains_dropbox = False
+            for attachment in tuple(attachments):
+                if attachment.value().endswith("/dropbox/%s/%s" % (oldattachment.dropboxID(), oldattachment.name(),)):
+                    component.removeProperty(attachment)
+                    removed = True
+                elif attachment.value().find("/dropbox/") != -1:
+                    still_contains_dropbox = True
+            if removed:
+                attach, _ignore_location = (yield newattachment.attachProperty())
+                component.addProperty(attach)
+            if not still_contains_dropbox:
+                component.removePropertiesWithName("X-APPLE-DROPBOX")
+
+        # Write the component back (and no need to re-index as we have not
+        # changed any timing properties in the calendar data).
+        cal.noInstanceIndexing = True
+        yield self.setComponent(cal)
+
+
+    @inlineCallbacks
     def createManagedAttachment(self):
 
         # We need to know the resource_ID of the home collection of the owner
@@ -2120,14 +2285,55 @@
         )
 
 
+    @inlineCallbacks
+    def convertToManaged(self):
+        """
+        Convert this dropbox attachment into a managed attachment by updating the
+        database and returning a new ManagedAttachment object that does not reference
+        any calendar object. Referencing will be added later.
 
+        @return: the managed attachment object
+        @rtype: L{ManagedAttachment}
+        """
+
+        # Change the DROPBOX_ID to a single "." to indicate a managed attachment.
+        att = schema.ATTACHMENT
+        (yield Update(
+            {att.DROPBOX_ID    : ".", },
+            Where=(att.ATTACHMENT_ID == self._attachmentID),
+        ).on(self._txn))
+
+        # Create an "orphaned" ManagedAttachment that points to the updated data but without
+        # an actual managed-id (which only exists when there is a reference to a calendar object).
+        mattach = (yield ManagedAttachment.load(self._txn, None, attachmentID=self._attachmentID))
+        if mattach is None:
+            raise AttachmentMigrationFailed
+
+        # Then move the file on disk from the old path to the new one
+        mattach._path.parent().makedirs()
+        oldpath = self._path
+        newpath = mattach._path
+        oldpath.moveTo(newpath)
+
+        returnValue(mattach)
+
+
+
 class ManagedAttachment(Attachment):
+    """
+    Managed attachments are ones that the server is in total control of. Clients do POSTs on calendar objects
+    to store the attachment data and have ATTACH properties added, updated or remove from the calendar objects.
+    Each ATTACH property in a calendar object has a MANAGED-ID iCalendar parameter that is used in the POST requests
+    to target a specific attachment. The MANAGED-ID values are unique to each calendar object resource, though
+    multiple calendar object resources can point to the same underlying attachment as there is a separate database
+    table that maps calendar objects/managed-ids to actual attachments.
+    """
 
     @classmethod
     @inlineCallbacks
     def _create(cls, txn, managedID, ownerHomeID):
         """
-        Create a new Attachment object.
+        Create a new managed Attachment object.
 
         @param txn: The transaction to use
         @type txn: L{CommonStoreTransaction}
@@ -2246,22 +2452,30 @@
 
     @classmethod
     @inlineCallbacks
-    def load(cls, txn, managedID):
-        attco = schema.ATTACHMENT_CALENDAR_OBJECT
-        rows = (yield Select(
-            [attco.ATTACHMENT_ID, attco.CALENDAR_OBJECT_RESOURCE_ID, ],
-            From=attco,
-            Where=(attco.MANAGED_ID == managedID),
-        ).on(txn))
-        if len(rows) == 0:
-            returnValue(None)
-        elif len(rows) != 1:
-            raise AttachmentStoreValidManagedID
+    def load(cls, txn, managedID, attachmentID=None):
+        """
+        Create a ManagedAttachment via either its managedID or attachmentID.
+        """
 
-        attachment = cls(txn, rows[0][0], None, None)
+        if managedID:
+            attco = schema.ATTACHMENT_CALENDAR_OBJECT
+            rows = (yield Select(
+                [attco.ATTACHMENT_ID, attco.CALENDAR_OBJECT_RESOURCE_ID, ],
+                From=attco,
+                Where=(attco.MANAGED_ID == managedID),
+            ).on(txn))
+            if len(rows) == 0:
+                returnValue(None)
+            elif len(rows) != 1:
+                raise AttachmentStoreValidManagedID
+            rows = rows[0]
+        else:
+            rows = (attachmentID, None,)
+
+        attachment = cls(txn, rows[0], None, None)
         attachment = (yield attachment.initFromStore())
         attachment._managedID = managedID
-        attachment._objectResourceID = rows[0][1]
+        attachment._objectResourceID = rows[1]
         returnValue(attachment)
 
 
@@ -2412,6 +2626,31 @@
 
 
     @inlineCallbacks
+    def newReference(self, resourceID):
+        """
+        Create a new managed-id that references the supplied calendar object resource id, and
+        return a ManagedAttachment for the new reference.
+
+        @param resourceID: the resource id to reference
+        @type resourceID: C{int}
+
+        @return: the new managed attachment
+        @rtype: L{ManagedAttachment}
+        """
+
+        managed_id = str(uuid.uuid4())
+        attco = schema.ATTACHMENT_CALENDAR_OBJECT
+        yield Insert({
+            attco.ATTACHMENT_ID               : self._attachmentID,
+            attco.MANAGED_ID                  : managed_id,
+            attco.CALENDAR_OBJECT_RESOURCE_ID : resourceID,
+        }).on(self._txn)
+
+        mattach = (yield ManagedAttachment.load(self._txn, managed_id))
+        returnValue(mattach)
+
+
+    @inlineCallbacks
     def removeFromResource(self, resourceID):
 
         # Delete the reference
@@ -2432,4 +2671,23 @@
             yield self.remove()
 
 
+    @inlineCallbacks
+    def attachProperty(self):
+        """
+        Return an iCalendar ATTACH property for this attachment.
+        """
+
+        location = (yield self.location())
+
+        attach = Property("ATTACH", location, params={
+            "MANAGED-ID": self.managedID(),
+            "MTAG": self.md5(),
+            "FMTTYPE": "%s/%s" % (self.contentType().mediaType, self.contentType().mediaSubtype),
+            "FILENAME": self.name(),
+            "SIZE": str(self.size()),
+        }, valuetype=PyCalendarValue.VALUETYPE_URI)
+
+        returnValue((attach, location,))
+
+
 Calendar._objectResourceClass = CalendarObject

Added: CalendarServer/trunk/txdav/caldav/datastore/test/attachments/accounts.xml
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/attachments/accounts.xml	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/attachments/accounts.xml	2013-01-08 22:07:49 UTC (rev 10294)
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE accounts SYSTEM "../../../conf/auth/accounts.dtd">
+
+<accounts realm="/Search">
+  <user>
+    <uid>example</uid>
+    <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+    <password>example</password>
+    <name>Example User</name>
+    <email-address>example at example.com</email-address>
+  </user>
+</accounts>

Added: CalendarServer/trunk/txdav/caldav/datastore/test/attachments/resources.xml
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/attachments/resources.xml	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/attachments/resources.xml	2013-01-08 22:07:49 UTC (rev 10294)
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<accounts realm="/Search">
+</accounts>


Property changes on: CalendarServer/trunk/txdav/caldav/datastore/test/attachments/resources.xml
___________________________________________________________________
Added: svn:executable
   + *

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2013-01-08 21:57:32 UTC (rev 10293)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2013-01-08 22:07:49 UTC (rev 10294)
@@ -1,6 +1,6 @@
 # -*- test-case-name: txdav.caldav.datastore -*-
 ##
-# Copyright (c) 2010-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2010-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -118,6 +118,7 @@
         "DTSTART;TZID=US/Pacific:20100207T170000\r\n"
         "DTSTAMP:20100203T013909Z\r\n"
         "SEQUENCE:3\r\n"
+        "X-APPLE-DROPBOX:/calendars/users/wsanchez/dropbox/uid-test.dropbox\r\n"
         "BEGIN:VALARM\r\n"
           "X-WR-ALARMUID:1377CCC7-F85C-4610-8583-9513D4B364E1\r\n"
           "TRIGGER:-PT20M\r\n"

Added: CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py	2013-01-08 22:07:49 UTC (rev 10294)
@@ -0,0 +1,626 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.trial import unittest
+from txdav.common.datastore.test.util import CommonCommonTests, buildStore, \
+    populateCalendarsFrom
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twistedcaldav.config import config
+import os
+from calendarserver.tap.util import getRootResource
+from twext.enterprise.dal.syntax import Delete
+from txdav.common.datastore.sql_tables import schema
+from pycalendar.datetime import PyCalendarDateTime
+from txdav.caldav.datastore.sql import CalendarStoreFeatures, DropBoxAttachment, \
+    ManagedAttachment
+from twext.web2.http_headers import MimeType
+from twistedcaldav.ical import Property
+from pycalendar.value import PyCalendarValue
+
+"""
+Tests for txdav.caldav.datastore.sql attachment handling.
+"""
+
+now = PyCalendarDateTime.getToday().getYear()
+
+PLAIN_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:STANDARD
+TZOFFSETFROM:-0700
+RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYMONTH=10;BYDAY=-1SU
+DTSTART:19621028T020000
+TZNAME:PST
+TZOFFSETTO:-0800
+END:STANDARD
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0800
+RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYMONTH=4;BYDAY=1SU
+DTSTART:19870405T020000
+TZNAME:PDT
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0800
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:PDT
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0700
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:PST
+TZOFFSETTO:-0800
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:685BC3A1-195A-49B3-926D-388DDACA78A6-%(uid)s
+DTEND;TZID=US/Pacific:%(year)s0307T151500
+TRANSP:OPAQUE
+SUMMARY:Event without attachment
+DTSTART;TZID=US/Pacific:%(year)s0307T111500
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+ATTACHMENT_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:STANDARD
+TZOFFSETFROM:-0700
+RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYMONTH=10;BYDAY=-1SU
+DTSTART:19621028T020000
+TZNAME:PST
+TZOFFSETTO:-0800
+END:STANDARD
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0800
+RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYMONTH=4;BYDAY=1SU
+DTSTART:19870405T020000
+TZNAME:PDT
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0800
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:PDT
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0700
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:PST
+TZOFFSETTO:-0800
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:57A5D1F6-9A57-4F74-9520-25C617F54B88-%(uid)s
+TRANSP:OPAQUE
+SUMMARY:Event with attachment
+DTSTART;TZID=US/Pacific:%(year)s0308T111500
+DTEND;TZID=US/Pacific:%(year)s0308T151500
+DTSTAMP:20100303T181220Z
+X-APPLE-DROPBOX:/calendars/__uids__/%(userid)s/dropbox/%(dropboxid)s.dropbox
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+
+class AttachmentMigrationTests(CommonCommonTests, unittest.TestCase):
+    """
+    Test migrating dropbox to managed attachments.
+    """
+
+    metadata = {
+        "accessMode": "PUBLIC",
+        "isScheduleObject": True,
+        "scheduleTag": "abc",
+        "scheduleEtags": (),
+        "hasPrivateComment": False,
+    }
+
+    requirements = {
+        "home1" : {
+            "calendar1" : {
+                "1.1.ics" : (PLAIN_ICS % {"year": now, "uid": "1.1", }, metadata,),
+                "1.2.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "1.2", "userid": "user01", "dropboxid": "1.2"}, metadata,),
+                "1.3.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "1.3", "userid": "user01", "dropboxid": "1.3"}, metadata,),
+                "1.4.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "1.4", "userid": "user01", "dropboxid": "1.4"}, metadata,),
+                "1.5.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "1.5", "userid": "user01", "dropboxid": "1.4"}, metadata,),
+            }
+        },
+        "home2" : {
+            "calendar2" : {
+                "2-2.1.ics" : (PLAIN_ICS % {"year": now, "uid": "2-2.1", }, metadata,),
+                "2-2.2.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "2-2.2", "userid": "user02", "dropboxid": "2.2"}, metadata,),
+                "2-2.3.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "1.3", "userid": "user01", "dropboxid": "1.3"}, metadata,),
+            },
+            "calendar3" : {
+                "2-3.1.ics" : (PLAIN_ICS % {"year": now, "uid": "2-3.1", }, metadata,),
+                "2-3.2.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "1.4", "userid": "user01", "dropboxid": "1.4"}, metadata,),
+                "2-3.3.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "1.5", "userid": "user01", "dropboxid": "1.4"}, metadata,),
+            }
+        }
+    }
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(AttachmentMigrationTests, self).setUp()
+        self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
+        yield self.populate()
+
+        self.patch(config.DirectoryService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "attachments", "accounts.xml"
+            )
+        )
+        self.patch(config.ResourceService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "attachments", "resources.xml"
+            )
+        )
+        self.rootResource = getRootResource(config, self._sqlCalendarStore)
+        self.directory = self.rootResource.getDirectory()
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+        txn = self._sqlCalendarStore.newTransaction()
+        Delete(
+            From=schema.ATTACHMENT,
+            Where=None
+        ).on(txn)
+        Delete(
+            From=schema.ATTACHMENT_CALENDAR_OBJECT,
+            Where=None
+        ).on(txn)
+
+        yield txn.commit()
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{CalendarStore} for testing.
+        """
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def _addAttachment(self, home, calendar, event, dropboxid, name):
+
+        txn = self._sqlCalendarStore.newTransaction()
+
+        # Create an event with an attachment
+        home = (yield txn.calendarHomeWithUID(home))
+        calendar = (yield home.calendarWithName(calendar))
+        event = (yield calendar.calendarObjectWithName(event))
+        attachment = (yield event.createAttachmentWithName(name))
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("%s/%s/%s/%s" % (home, calendar, event, name,))
+        t.write(" attachment")
+        yield t.loseConnection()
+
+        cal = (yield event.component())
+        cal.mainComponent().addProperty(Property(
+            "ATTACH",
+            "http://localhost/calendars/users/%s/dropbox/%s.dropbox/%s" % (home.name(), dropboxid, name,),
+            valuetype=PyCalendarValue.VALUETYPE_URI
+        ))
+        yield event.setComponent(cal)
+        yield txn.commit()
+
+        returnValue(attachment)
+
+
+    @inlineCallbacks
+    def _addAttachmentProperty(self, home, calendar, event, dropboxid, owner_home, name):
+
+        txn = self._sqlCalendarStore.newTransaction()
+
+        # Create an event with an attachment
+        home = (yield txn.calendarHomeWithUID(home))
+        calendar = (yield home.calendarWithName(calendar))
+        event = (yield calendar.calendarObjectWithName(event))
+
+        cal = (yield event.component())
+        cal.mainComponent().addProperty(Property(
+            "ATTACH",
+            "http://localhost/calendars/users/%s/dropbox/%s.dropbox/%s" % (owner_home, dropboxid, name,),
+            valuetype=PyCalendarValue.VALUETYPE_URI
+        ))
+        yield event.setComponent(cal)
+        yield txn.commit()
+
+
+    @inlineCallbacks
+    def _addAllAttachments(self):
+        """
+        Add the full set of attachments to be used for testing.
+        """
+        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_1.txt")
+        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_2.txt")
+        yield self._addAttachment("home1", "calendar1", "1.3.ics", "1.3", "attach_1_3.txt")
+        yield self._addAttachment("home1", "calendar1", "1.4.ics", "1.4", "attach_1_4.txt")
+        yield self._addAttachmentProperty("home1", "calendar1", "1.5.ics", "1.4", "home1", "attach_1_4.txt")
+
+        yield self._addAttachment("home2", "calendar2", "2-2.2.ics", "2.2", "attach_2_2.txt")
+        yield self._addAttachmentProperty("home2", "calendar2", "2-2.3.ics", "1.3", "home1", "attach_1_3.txt")
+        yield self._addAttachmentProperty("home2", "calendar3", "2-3.2.ics", "1.4", "home1", "attach_1_4.txt")
+        yield self._addAttachmentProperty("home2", "calendar3", "2-3.3.ics", "1.4", "home1", "attach_1_4.txt")
+
+
+    @inlineCallbacks
+    def _verifyConversion(self, home, calendar, event, filenames):
+        """
+        Verify that the specified event contains managed attachments only.
+        """
+        txn = self._sqlCalendarStore.newTransaction()
+        home = (yield txn.calendarHomeWithUID(home))
+        calendar = (yield home.calendarWithName(calendar))
+        event = (yield calendar.calendarObjectWithName(event))
+        component = (yield event.component()).mainComponent()
+
+        # No more X-APPLE-DROPBOX
+        self.assertFalse(component.hasProperty("X-APPLE-DROPBOX"))
+
+        # Check only managed attachments exist
+        attachments = (yield event.component()).mainComponent().properties("ATTACH")
+        dropbox_count = 0
+        managed_count = 0
+        for attach in attachments:
+            if attach.hasParameter("MANAGED-ID"):
+                managed_count += 1
+                self.assertTrue(attach.value().find("/dropbox/") == -1)
+                self.assertTrue(attach.parameterValue("FILENAME") in filenames)
+            else:
+                dropbox_count += 1
+        self.assertEqual(managed_count, len(filenames))
+        self.assertEqual(dropbox_count, 0)
+        yield txn.commit()
+
+
+    @inlineCallbacks
+    def _verifyNoConversion(self, home, calendar, event, filenames):
+        """
+        Verify that the specified event does not contain managed attachments.
+        """
+        txn = self._sqlCalendarStore.newTransaction()
+        home = (yield txn.calendarHomeWithUID(home))
+        calendar = (yield home.calendarWithName(calendar))
+        event = (yield calendar.calendarObjectWithName(event))
+        component = (yield event.component()).mainComponent()
+
+        # X-APPLE-DROPBOX present
+        self.assertTrue(component.hasProperty("X-APPLE-DROPBOX"))
+
+        # Check only managed attachments exist
+        attachments = (yield event.component()).mainComponent().properties("ATTACH")
+        dropbox_count = 0
+        managed_count = 0
+        for attach in attachments:
+            if attach.hasParameter("MANAGED-ID"):
+                managed_count += 1
+            else:
+                dropbox_count += 1
+                self.assertTrue(attach.value().find("/dropbox/") != -1)
+                self.assertTrue(any([attach.value().endswith(filename) for filename in filenames]))
+        self.assertEqual(managed_count, 0)
+        self.assertEqual(dropbox_count, len(filenames))
+        yield txn.commit()
+
+
+    @inlineCallbacks
+    def test_loadCalendarObjectsForDropboxID(self):
+        """
+        Test L{txdav.caldav.datastore.sql.CalendarStore._loadCalendarObjectsForDropboxID} returns the right set of
+        calendar objects.
+        """
+        txn = self._sqlCalendarStore.newTransaction()
+        calstore = CalendarStoreFeatures(self._sqlCalendarStore)
+
+        for dropbox_id, result_count, result_names  in (
+            ("1.2", 1, ("1.2.ics",)),
+            ("1.3", 2, ("1.3.ics", "2-2.3.ics",)),
+            ("1.4", 4, ("1.4.ics", "1.5.ics", "2-3.2.ics", "2-3.3.ics",)),
+            ("2.2", 1, ("2-2.2.ics",)),
+        ):
+            cobjs = (yield calstore._loadCalendarObjectsForDropboxID(txn, "%s.dropbox" % (dropbox_id,)))
+            self.assertEqual(len(cobjs), result_count, "Failed count with dropbox id: %s" % (dropbox_id,))
+            names = set([cobj.name() for cobj in cobjs])
+            self.assertEqual(names, set(result_names), "Failed names with dropbox id: %s" % (dropbox_id,))
+
+
+    @inlineCallbacks
+    def test_convertToManaged(self):
+        """
+        Test L{txdav.caldav.datastore.sql.DropboxAttachment.convertToManaged} converts properly to a ManagedAttachment.
+        """
+        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2.txt")
+
+        txn = self._sqlCalendarStore.newTransaction()
+
+        dattachment = (yield DropBoxAttachment.load(txn, "1.2.dropbox", "attach_1_2.txt"))
+        self.assertNotEqual(dattachment, None)
+        self.assertTrue(dattachment._path.exists())
+        mattachment = (yield dattachment.convertToManaged())
+        self.assertNotEqual(mattachment, None)
+        yield txn.commit()
+        self.assertFalse(dattachment._path.exists())
+        self.assertTrue(mattachment._path.exists())
+
+        # Dropbox attachment gone
+        txn = self._sqlCalendarStore.newTransaction()
+        dattachment2 = (yield DropBoxAttachment.load(txn, "1.2", "attach_1_2.txt"))
+        self.assertEqual(dattachment2, None)
+
+        # Managed attachment present
+        txn = self._sqlCalendarStore.newTransaction()
+        mattachment2 = (yield ManagedAttachment.load(txn, None, attachmentID=dattachment._attachmentID))
+        self.assertNotEqual(mattachment2, None)
+        self.assertTrue(mattachment2.isManaged())
+
+
+    @inlineCallbacks
+    def test_newReference(self):
+        """
+        Test L{txdav.caldav.datastore.sql.ManagedAttachment.newReference} creates a new managed attachment reference.
+        """
+        yield self._addAttachment("home1", "calendar1", "1.4.ics", "1.4", "attach_1_4.txt")
+
+        txn = self._sqlCalendarStore.newTransaction()
+
+        home = (yield txn.calendarHomeWithUID("home1"))
+        calendar = (yield home.calendarWithName("calendar1"))
+        event4 = (yield calendar.calendarObjectWithName("1.4.ics"))
+        event5 = (yield calendar.calendarObjectWithName("1.5.ics"))
+
+        dattachment = (yield DropBoxAttachment.load(txn, "1.4.dropbox", "attach_1_4.txt"))
+        self.assertNotEqual(dattachment, None)
+        self.assertTrue(dattachment._path.exists())
+        mattachment = (yield dattachment.convertToManaged())
+        self.assertNotEqual(mattachment, None)
+        self.assertEqual(mattachment.managedID(), None)
+
+        mnew4 = (yield mattachment.newReference(event4._resourceID))
+        self.assertNotEqual(mnew4, None)
+        self.assertNotEqual(mnew4.managedID(), None)
+
+        mnew5 = (yield mattachment.newReference(event5._resourceID))
+        self.assertNotEqual(mnew5, None)
+        self.assertNotEqual(mnew5.managedID(), None)
+
+        yield txn.commit()
+
+        # Managed attachment present
+        txn = self._sqlCalendarStore.newTransaction()
+        mtest4 = (yield ManagedAttachment.load(txn, mnew4.managedID()))
+        self.assertNotEqual(mtest4, None)
+        self.assertTrue(mtest4.isManaged())
+        self.assertEqual(mtest4._objectResourceID, event4._resourceID)
+        yield txn.commit()
+
+        # Managed attachment present
+        txn = self._sqlCalendarStore.newTransaction()
+        mtest5 = (yield ManagedAttachment.load(txn, mnew5.managedID()))
+        self.assertNotEqual(mtest5, None)
+        self.assertTrue(mtest5.isManaged())
+        self.assertEqual(mtest5._objectResourceID, event5._resourceID)
+        yield txn.commit()
+
+
+    @inlineCallbacks
+    def test_convertAttachments(self):
+        """
+        Test L{txdav.caldav.datastore.sql.CalendarObject.convertAttachments} re-writes calendar data.
+        """
+        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_1.txt")
+        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_2.txt")
+
+        txn = self._sqlCalendarStore.newTransaction()
+
+        home = (yield txn.calendarHomeWithUID("home1"))
+        calendar = (yield home.calendarWithName("calendar1"))
+        event = (yield calendar.calendarObjectWithName("1.2.ics"))
+
+        # Check that dropbox ATTACH exists
+        attachments = (yield event.component()).mainComponent().properties("ATTACH")
+        for attach in attachments:
+            self.assertTrue(attach.value().find("1.2.dropbox") != -1)
+            self.assertTrue(attach.value().endswith("attach_1_2_1.txt") or attach.value().endswith("attach_1_2_2.txt"))
+            self.assertFalse(attach.value().find("MANAGED-ID") != -1)
+
+        dattachment = (yield DropBoxAttachment.load(txn, "1.2.dropbox", "attach_1_2_1.txt"))
+        mattachment = (yield dattachment.convertToManaged())
+        mnew = (yield mattachment.newReference(event._resourceID))
+        yield event.convertAttachments(dattachment, mnew)
+        yield txn.commit()
+
+        txn = self._sqlCalendarStore.newTransaction()
+
+        home = (yield txn.calendarHomeWithUID("home1"))
+        calendar = (yield home.calendarWithName("calendar1"))
+        event = (yield calendar.calendarObjectWithName("1.2.ics"))
+        component = (yield event.component()).mainComponent()
+
+        # Still has X-APPLE-DROPBOX
+        self.assertTrue(component.hasProperty("X-APPLE-DROPBOX"))
+
+        # Check that one managed-id and one dropbox ATTACH exist
+        attachments = (yield event.component()).mainComponent().properties("ATTACH")
+        dropbox_count = 0
+        managed_count = 0
+        for attach in attachments:
+            if attach.hasParameter("MANAGED-ID"):
+                managed_count += 1
+                self.assertTrue(attach.value().find("1.2.dropbox") == -1)
+                self.assertEqual(attach.parameterValue("MANAGED-ID"), mnew.managedID())
+                self.assertEqual(attach.parameterValue("FILENAME"), mnew.name())
+            else:
+                dropbox_count += 1
+                self.assertTrue(attach.value().find("1.2.dropbox") != -1)
+                self.assertTrue(attach.value().endswith("attach_1_2_2.txt"))
+        self.assertEqual(managed_count, 1)
+        self.assertEqual(dropbox_count, 1)
+        yield txn.commit()
+
+        # Convert the second dropbox attachment
+        txn = self._sqlCalendarStore.newTransaction()
+        home = (yield txn.calendarHomeWithUID("home1"))
+        calendar = (yield home.calendarWithName("calendar1"))
+        event = (yield calendar.calendarObjectWithName("1.2.ics"))
+        dattachment = (yield DropBoxAttachment.load(txn, "1.2.dropbox", "attach_1_2_2.txt"))
+        mattachment = (yield dattachment.convertToManaged())
+        mnew = (yield mattachment.newReference(event._resourceID))
+        yield event.convertAttachments(dattachment, mnew)
+        yield txn.commit()
+
+        txn = self._sqlCalendarStore.newTransaction()
+        home = (yield txn.calendarHomeWithUID("home1"))
+        calendar = (yield home.calendarWithName("calendar1"))
+        event = (yield calendar.calendarObjectWithName("1.2.ics"))
+        component = (yield event.component()).mainComponent()
+
+        # No more X-APPLE-DROPBOX
+        self.assertFalse(component.hasProperty("X-APPLE-DROPBOX"))
+
+        # Check that one managed-id and one dropbox ATTACH exist
+        attachments = (yield event.component()).mainComponent().properties("ATTACH")
+        dropbox_count = 0
+        managed_count = 0
+        for attach in attachments:
+            if attach.hasParameter("MANAGED-ID"):
+                managed_count += 1
+                self.assertTrue(attach.value().find("1.2.dropbox") == -1)
+                self.assertTrue(attach.parameterValue("FILENAME") in ("attach_1_2_1.txt", "attach_1_2_2.txt"))
+            else:
+                dropbox_count += 1
+        self.assertEqual(managed_count, 2)
+        self.assertEqual(dropbox_count, 0)
+        yield txn.commit()
+
+
+    @inlineCallbacks
+    def test_upgradeDropbox_oneEvent(self):
+        """
+        Test L{txdav.caldav.datastore.sql.CalendarStoreFeatures._upgradeDropbox} re-writes calendar data
+        for one event with an attachment.
+        """
+
+        yield self._addAllAttachments()
+
+        txn = self._sqlCalendarStore.newTransaction()
+        calstore = CalendarStoreFeatures(self._sqlCalendarStore)
+        yield calstore._upgradeDropbox(txn, "1.2.dropbox")
+        yield txn.commit()
+
+        yield self._verifyConversion("home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
+        yield self._verifyNoConversion("home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
+        yield self._verifyNoConversion("home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion("home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion("home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
+        yield self._verifyNoConversion("home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
+        yield self._verifyNoConversion("home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion("home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
+
+
+    @inlineCallbacks
+    def test_upgradeDropbox_oneEventTwoHomes(self):
+        """
+        Test L{txdav.caldav.datastore.sql.CalendarStoreFeatures._upgradeDropbox} re-writes calendar data
+        for multiple events across different homes with the same attachment.
+        """
+
+        yield self._addAllAttachments()
+
+        txn = self._sqlCalendarStore.newTransaction()
+        calstore = CalendarStoreFeatures(self._sqlCalendarStore)
+        yield calstore._upgradeDropbox(txn, "1.3.dropbox")
+        yield txn.commit()
+
+        yield self._verifyNoConversion("home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
+        yield self._verifyConversion("home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
+        yield self._verifyNoConversion("home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion("home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion("home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
+        yield self._verifyConversion("home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
+        yield self._verifyNoConversion("home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion("home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
+
+
+    @inlineCallbacks
+    def test_upgradeDropbox_twoEventsTwoHomes(self):
+        """
+        Test L{txdav.caldav.datastore.sql.CalendarStoreFeatures._upgradeDropbox} re-writes calendar data
+        for multiple events across different homes with the same attachment.
+        """
+
+        yield self._addAllAttachments()
+
+        txn = self._sqlCalendarStore.newTransaction()
+        calstore = CalendarStoreFeatures(self._sqlCalendarStore)
+        yield calstore._upgradeDropbox(txn, "1.4.dropbox")
+        yield txn.commit()
+
+        yield self._verifyNoConversion("home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
+        yield self._verifyNoConversion("home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
+        yield self._verifyConversion("home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion("home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion("home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
+        yield self._verifyNoConversion("home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
+        yield self._verifyConversion("home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion("home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
+
+
+    @inlineCallbacks
+    def test_upgradeToManagedAttachments(self):
+        """
+        Test L{txdav.caldav.datastore.sql.CalendarStoreFeatures.upgradeToManagedAttachments} re-writes calendar data
+        for all events with an attachment.
+        """
+
+        yield self._addAllAttachments()
+
+        txn = self._sqlCalendarStore.newTransaction()
+        calstore = CalendarStoreFeatures(self._sqlCalendarStore)
+        yield calstore.upgradeToManagedAttachments(txn, 2)
+        yield txn.commit()
+
+        yield self._verifyConversion("home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
+        yield self._verifyConversion("home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
+        yield self._verifyConversion("home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion("home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion("home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
+        yield self._verifyConversion("home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
+        yield self._verifyConversion("home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion("home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))

Modified: CalendarServer/trunk/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendarstore.py	2013-01-08 21:57:32 UTC (rev 10293)
+++ CalendarServer/trunk/txdav/caldav/icalendarstore.py	2013-01-08 22:07:49 UTC (rev 10294)
@@ -1,6 +1,6 @@
 # -*- test-case-name: txdav.caldav.datastore -*-
 ##
-# Copyright (c) 2010-2012 Apple Inc. All rights reserved.
+# Copyright (c) 2010-2013 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -77,6 +77,13 @@
 
 
 
+class AttachmentMigrationFailed(Exception):
+    """
+    Unable to migrate an attachment.
+    """
+
+
+
 class QuotaExceeded(Exception):
     """
     The quota for a particular user has been exceeded.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130108/5b9c9ac3/attachment-0001.html>


More information about the calendarserver-changes mailing list