[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