[CalendarServer-changes] [10190] CalendarServer/branches/release/CalendarServer-4.3-dev

source_changes at macosforge.org source_changes at macosforge.org
Tue Dec 18 14:06:38 PST 2012


Revision: 10190
          http://trac.calendarserver.org//changeset/10190
Author:   cdaboo at apple.com
Date:     2012-12-18 14:06:38 -0800 (Tue, 18 Dec 2012)
Log Message:
-----------
Remove dropbox attachment when no longer referenced. Always adjust quota when an attachment is removed - even by purge.

Modified Paths:
--------------
    CalendarServer/branches/release/CalendarServer-4.3-dev/calendarserver/tools/test/test_purge_old_events.py
    CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/file.py
    CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/sql.py
    CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/common/datastore/sql.py

Modified: CalendarServer/branches/release/CalendarServer-4.3-dev/calendarserver/tools/test/test_purge_old_events.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-4.3-dev/calendarserver/tools/test/test_purge_old_events.py	2012-12-18 16:44:30 UTC (rev 10189)
+++ CalendarServer/branches/release/CalendarServer-4.3-dev/calendarserver/tools/test/test_purge_old_events.py	2012-12-18 22:06:38 UTC (rev 10190)
@@ -17,9 +17,14 @@
 """
 Tests for calendarserver.tools.purge
 """
+
 from calendarserver.tap.util import getRootResource
 from calendarserver.tools.purge import purgeOldEvents, purgeUID, purgeOrphanedAttachments
 
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.timezone import PyCalendarTimezone
+
+from twext.enterprise.dal.syntax import Update
 from twext.web2.http_headers import MimeType
 
 from twisted.internet.defer import inlineCallbacks, returnValue
@@ -28,11 +33,9 @@
 from twistedcaldav.config import config
 from twistedcaldav.vcard import Component as VCardComponent
 
+from txdav.common.datastore.sql_tables import schema
 from txdav.common.datastore.test.util import buildStore, populateCalendarsFrom, CommonCommonTests
 
-from pycalendar.datetime import PyCalendarDateTime
-from pycalendar.timezone import PyCalendarTimezone
-
 import os
 
 
@@ -84,7 +87,7 @@
 SEQUENCE:2
 END:VEVENT
 END:VCALENDAR
-""".replace("\n", "\r\n") % {"year":now-5}
+""".replace("\n", "\r\n") % {"year": now - 5}
 
 OLD_ATTACHMENT_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -134,7 +137,7 @@
 SEQUENCE:2
 END:VEVENT
 END:VCALENDAR
-""".replace("\n", "\r\n") % {"year":now-5}
+""".replace("\n", "\r\n") % {"year": now - 5}
 
 ENDLESS_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -183,7 +186,7 @@
 SEQUENCE:4
 END:VEVENT
 END:VCALENDAR
-""".replace("\n", "\r\n") % {"year":now-5}
+""".replace("\n", "\r\n") % {"year": now - 5}
 
 REPEATING_AWHILE_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -232,7 +235,7 @@
 SEQUENCE:6
 END:VEVENT
 END:VCALENDAR
-""".replace("\n", "\r\n") % {"year":now-5}
+""".replace("\n", "\r\n") % {"year": now - 5}
 
 STRADDLING_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -267,7 +270,7 @@
 SEQUENCE:5
 END:VEVENT
 END:VCALENDAR
-""".replace("\n", "\r\n") % {"year":now-2, "until":now+1}
+""".replace("\n", "\r\n") % {"year": now - 2, "until": now + 1}
 
 RECENT_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -301,7 +304,7 @@
 SEQUENCE:2
 END:VEVENT
 END:VCALENDAR
-""".replace("\n", "\r\n") % {"year":now}
+""".replace("\n", "\r\n") % {"year": now}
 
 
 VCARD_1 = """BEGIN:VCARD
@@ -421,7 +424,7 @@
         count = (yield txn.removeOldEvents(cutoff))
         self.assertEquals(count, 3)
         results = (yield txn.eventsOlderThan(cutoff))
-        self.assertEquals(results, [ ])
+        self.assertEquals(results, [])
 
         # Remove oldest events (none left)
         count = (yield txn.removeOldEvents(cutoff))
@@ -429,7 +432,7 @@
 
 
     @inlineCallbacks
-    def _addAttachment(self):
+    def _addAttachment(self, orphan=False):
 
         txn = self._sqlCalendarStore.newTransaction()
 
@@ -442,6 +445,15 @@
         t.write("old attachment")
         t.write(" text")
         (yield t.loseConnection())
+
+        if orphan:
+            # Reset dropbox id in calendar_object
+            co = schema.CALENDAR_OBJECT
+            Update(
+                {co.DROPBOX_ID: None, },
+                Where=co.RESOURCE_ID == event._resourceID,
+            ).on(txn)
+
         (yield txn.commit())
 
         returnValue(attachment)
@@ -449,43 +461,48 @@
 
     @inlineCallbacks
     def test_removeOrphanedAttachments(self):
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        self.assertEqual(quota, 0)
+
         attachment = (yield self._addAttachment())
-        txn = self._sqlCalendarStore.newTransaction()
         attachmentPath = attachment._path.path
         self.assertTrue(os.path.exists(attachmentPath))
+        (yield self.commit())
 
-        orphans = (yield txn.orphanedAttachments())
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        self.assertNotEqual(quota, 0)
+
+        orphans = (yield self.transactionUnderTest().orphanedAttachments())
         self.assertEquals(len(orphans), 0)
 
-        count = (yield txn.removeOrphanedAttachments(batchSize=100))
+        count = (yield self.transactionUnderTest().removeOrphanedAttachments(batchSize=100))
         self.assertEquals(count, 0)
+        (yield self.commit())
 
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        self.assertNotEqual(quota, 0)
+
         # File still exists
         self.assertTrue(os.path.exists(attachmentPath))
 
         # Delete all old events (including the event containing the attachment)
         cutoff = PyCalendarDateTime(now, 4, 1, 0, 0, 0)
-        count = (yield txn.removeOldEvents(cutoff))
+        count = (yield self.transactionUnderTest().removeOldEvents(cutoff))
+        (yield self.commit())
 
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        self.assertEqual(quota, 0)
+
         # Just look for orphaned attachments but don't delete
-        orphans = (yield txn.orphanedAttachments())
-        self.assertEquals(len(orphans), 1)
+        orphans = (yield self.transactionUnderTest().orphanedAttachments())
+        self.assertEquals(len(orphans), 0)
 
-        # Remove orphaned attachments, should be 1
-        count = (yield txn.removeOrphanedAttachments(batchSize=100))
-        self.assertEquals(count, 1)
 
-        # Remove orphaned attachments, shouldn't be any
-        count = (yield txn.removeOrphanedAttachments())
-        self.assertEquals(count, 0)
-
-        # File isn't actually removed until after commit
-        (yield txn.commit())
-
-        # Verify the file itself is gone
-        self.assertFalse(os.path.exists(attachmentPath))
-
-
     @inlineCallbacks
     def test_purgeOldEvents(self):
 
@@ -515,12 +532,12 @@
         abColl = (yield abHome.addressbookWithName("addressbook"))
         (yield abColl.createAddressBookObjectWithName("card1",
             VCardComponent.fromString(VCARD_1)))
-        self.assertEquals(len( (yield abColl.addressbookObjects()) ), 1)
+        self.assertEquals(len((yield abColl.addressbookObjects())), 1)
 
         # Verify there are 3 events in calendar1
         calHome = (yield txn.calendarHomeWithUID("home1"))
         calColl = (yield calHome.calendarWithName("calendar1"))
-        self.assertEquals(len( (yield calColl.calendarObjects()) ), 3)
+        self.assertEquals(len((yield calColl.calendarObjects())), 3)
 
         # Make the newly created objects available to the purgeUID transaction
         (yield txn.commit())
@@ -540,7 +557,7 @@
 
         calHome = (yield txn.calendarHomeWithUID("home1"))
         calColl = (yield calHome.calendarWithName("calendar1"))
-        self.assertEquals(len( (yield calColl.calendarObjects()) ), 2)
+        self.assertEquals(len((yield calColl.calendarObjects())), 2)
 
 
     @inlineCallbacks
@@ -552,12 +569,12 @@
         abColl = (yield abHome.addressbookWithName("addressbook"))
         (yield abColl.createAddressBookObjectWithName("card1",
             VCardComponent.fromString(VCARD_1)))
-        self.assertEquals(len( (yield abColl.addressbookObjects()) ), 1)
+        self.assertEquals(len((yield abColl.addressbookObjects())), 1)
 
         # Verify there are 3 events in calendar1
         calHome = (yield txn.calendarHomeWithUID("home1"))
         calColl = (yield calHome.calendarWithName("calendar1"))
-        self.assertEquals(len( (yield calColl.calendarObjects()) ), 3)
+        self.assertEquals(len((yield calColl.calendarObjects())), 3)
 
         # Make the newly created objects available to the purgeUID transaction
         (yield txn.commit())
@@ -580,25 +597,47 @@
     @inlineCallbacks
     def test_purgeOrphanedAttachments(self):
 
-        (yield self._addAttachment())
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        self.assertEqual(quota, 0)
 
+        (yield self._addAttachment(orphan=True))
+        (yield self.commit())
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        self.assertNotEqual(quota, 0)
+
         # Remove old events first
         total = (yield purgeOldEvents(self._sqlCalendarStore, self.directory,
             self.rootResource, PyCalendarDateTime(now, 4, 1, 0, 0, 0), 2, verbose=False))
         self.assertEquals(total, 4)
 
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        self.assertNotEqual(quota, 0)
+
         # Dry run
         total = (yield purgeOrphanedAttachments(self._sqlCalendarStore, 2,
             dryrun=True, verbose=False))
         self.assertEquals(total, 1)
+        (yield self.commit())
 
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        self.assertNotEqual(quota, 0)
+
         # Actually remove
         total = (yield purgeOrphanedAttachments(self._sqlCalendarStore, 2,
             dryrun=False, verbose=False))
         self.assertEquals(total, 1)
+        (yield self.commit())
 
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quotaAfter = (yield home.quotaUsedBytes())
+        self.assertEqual(quotaAfter, 0)
+
         # There should be no more left
         total = (yield purgeOrphanedAttachments(self._sqlCalendarStore, 2,
             dryrun=False, verbose=False))
         self.assertEquals(total, 0)
-

Modified: CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/file.py	2012-12-18 16:44:30 UTC (rev 10189)
+++ CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/file.py	2012-12-18 22:06:38 UTC (rev 10190)
@@ -50,7 +50,7 @@
 from txdav.caldav.icalendarstore import ICalendar, ICalendarObject
 from txdav.caldav.icalendarstore import ICalendarHome
 
-from txdav.caldav.datastore.index_file import Index as OldIndex,\
+from txdav.caldav.datastore.index_file import Index as OldIndex, \
     IndexSchedule as OldInboxIndex
 from txdav.caldav.datastore.util import (
     validateCalendarComponent, dropboxIDFromCalendarObject, CalendarObjectBase,
@@ -90,7 +90,6 @@
 
         self._childClass = Calendar
 
-
     createCalendarWithName = CommonHome.createChildWithName
     removeCalendarWithName = CommonHome.removeChildWithName
 
@@ -129,7 +128,7 @@
 
     @inlineCallbacks
     def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, type):
-        
+
         objectResources = (yield self.objectResourcesWithUID(uid, ("inbox",)))
         for objectResource in objectResources:
             if ok_object and objectResource._path == ok_object._path:
@@ -137,20 +136,22 @@
             matched_type = "schedule" if objectResource.isScheduleObject else "calendar"
             if type == "schedule" or matched_type == "schedule":
                 returnValue(True)
-            
+
         returnValue(False)
 
+
     @inlineCallbacks
     def getCalendarResourcesForUID(self, uid, allow_shared=False):
-        
+
         results = []
         objectResources = (yield self.objectResourcesWithUID(uid, ("inbox",)))
         for objectResource in objectResources:
             if allow_shared or objectResource._parentCollection._owned:
                 results.append(objectResource)
-            
+
         returnValue(results)
 
+
     @inlineCallbacks
     def calendarObjectWithDropboxID(self, dropboxID):
         """
@@ -186,18 +187,19 @@
         defaultCal = self.createCalendarWithName("calendar")
         props = defaultCal.properties()
         props[PropertyName(*ScheduleCalendarTransp.qname())] = ScheduleCalendarTransp(Opaque())
-        
+
         # Check whether components type must be separate
         if config.RestrictCalendarsToOneComponentType:
             defaultCal.setSupportedComponents("VEVENT")
-            
+
             # Default tasks
             defaultTasks = self.createCalendarWithName("tasks")
             props = defaultTasks.properties()
             defaultTasks.setSupportedComponents("VTODO")
-            
+
         self.createCalendarWithName("inbox")
 
+
     def ensureDefaultCalendarsExist(self):
         """
         Double check that we have calendars supporting at least VEVENT and VTODO,
@@ -222,10 +224,12 @@
                         newname = str(uuid.uuid4())
                     newcal = self.createCalendarWithName(newname)
                     newcal.setSupportedComponents(support_component)
-            
+
             _requireCalendarWithType("VEVENT", "calendar")
             _requireCalendarWithType("VTODO", "tasks")
 
+
+
 class Calendar(CommonHomeChild):
     """
     File-based implementation of L{ICalendar}.
@@ -270,7 +274,6 @@
         # TODO: implement me.
         raise NotImplementedError()
 
-
     ownerCalendarHome = CommonHomeChild.ownerHome
     viewerCalendarHome = CommonHomeChild.viewerHome
     calendarObjects = CommonHomeChild.objectResources
@@ -292,17 +295,19 @@
         Update the private property with the supported components. Technically this should only happen once
         on collection creation, but for migration we may need to change after the fact - hence a separate api.
         """
-        
+
         pname = PropertyName.fromElement(customxml.TwistedCalendarSupportedComponents)
         if supported_components:
             self.properties()[pname] = customxml.TwistedCalendarSupportedComponents.fromString(supported_components)
         elif pname in self.properties():
             del self.properties()[pname]
 
+
     def getSupportedComponents(self):
         result = str(self.properties().get(PropertyName.fromElement(customxml.TwistedCalendarSupportedComponents), ""))
         return result if result else None
 
+
     def isSupportedComponent(self, componentType):
         supported = self.getSupportedComponents()
         if supported:
@@ -310,6 +315,7 @@
         else:
             return True
 
+
     def initPropertyStore(self, props):
         # Setup peruser special properties
         props.setSpecialProperties(
@@ -323,61 +329,69 @@
             ),
         )
 
+
     def contentType(self):
         """
         The content type of Calendar objects is text/calendar.
         """
         return MimeType.fromString("text/calendar; charset=utf-8")
 
+
     def splitCollectionByComponentTypes(self):
         """
         If the calendar contains iCalendar data with different component types, then split it into separate collections
         each containing only one component type. When doing this make sure properties and sharing state are preserved
         on any new calendars created.
         """
-        
+
         # TODO: implement this for filestore
         pass
 
+
     def _countComponentTypes(self):
         """
         Count each component type in this calendar.
-        
-        @return: a C{tuple} of C{tuple} containing the component type name and count. 
+
+        @return: a C{tuple} of C{tuple} containing the component type name and count.
         """
 
         rows = self._index._oldIndex.componentTypeCounts()
         result = tuple([(componentType, componentCount) for componentType, componentCount in sorted(rows, key=lambda x:x[0])])
         return result
 
+
     def _splitComponentType(self, component):
         """
         Create a new calendar and move all components of the specified component type into the new one.
         Make sure properties and sharing state is preserved on the new calendar.
-        
+
         @param component: Component type to split out
         @type component: C{str}
         """
-        
+
         # TODO: implement this for filestore
         pass
 
+
     def _transferSharingDetails(self, newcalendar, component):
         """
         If the current calendar is shared, make the new calendar shared in the same way, but tweak the name.
         """
-        
+
         # TODO: implement this for filestore
         pass
-    
+
+
     def _transferCalendarObjects(self, newcalendar, component):
         """
         Move all calendar components of the specified type to the specified calendar.
         """
-        
+
         # TODO: implement this for filestore
         pass
 
+
+
 class CalendarObject(CommonObjectResource, CalendarObjectBase):
     """
     @ivar _path: The path of the .ics file on disk
@@ -389,7 +403,7 @@
     def __init__(self, name, calendar, metadata=None):
         super(CalendarObject, self).__init__(name, calendar)
         self._attachments = {}
-        
+
         if metadata is not None:
             self.accessMode = metadata.get("accessMode", "")
             self.isScheduleObject = metadata.get("isScheduleObject", False)
@@ -428,7 +442,7 @@
             if self._path.exists():
                 backup = hidden(self._path.temporarySibling())
                 self._path.moveTo(backup)
-            
+
             fh = self._path.open("w")
             try:
                 # FIXME: concurrency problem; if this write is interrupted
@@ -476,13 +490,17 @@
 
         if unfixed:
             self.log_error("Calendar data at %s had unfixable problems:\n  %s" % (self._path.path, "\n  ".join(unfixed),))
-        
+
         if fixed:
             self.log_error("Calendar data at %s had fixable problems:\n  %s" % (self._path.path, "\n  ".join(fixed),))
 
         return component
 
 
+    def remove(self):
+        pass
+
+
     def _text(self):
         if self._objectText is not None:
             return self._objectText
@@ -516,35 +534,41 @@
                     "File corruption detected (improper start) in file: %s"
                     % (self._path.path,)
                 )
-        
+
         self._objectText = text
         return text
 
+
     def uid(self):
         if not hasattr(self, "_uid"):
             self._uid = self.component().resourceUID()
         return self._uid
 
+
     def componentType(self):
         if not hasattr(self, "_componentType"):
             self._componentType = self.component().mainType()
         return self._componentType
 
+
     def organizer(self):
         return self.component().getOrganizer()
 
+
     def getMetadata(self):
         metadata = {}
-        metadata["accessMode"] = self.accessMode 
+        metadata["accessMode"] = self.accessMode
         metadata["isScheduleObject"] = self.isScheduleObject
         metadata["scheduleTag"] = self.scheduleTag
         metadata["scheduleEtags"] = self.scheduleEtags
         metadata["hasPrivateComment"] = self.hasPrivateComment
         return metadata
 
+
     def _get_accessMode(self):
         return str(self.properties().get(PropertyName.fromElement(customxml.TwistedCalendarAccessProperty), ""))
 
+
     def _set_accessMode(self, value):
         pname = PropertyName.fromElement(customxml.TwistedCalendarAccessProperty)
         if value:
@@ -564,6 +588,7 @@
             prop = str(prop) == "true"
         return prop
 
+
     def _set_isScheduleObject(self, value):
         pname = PropertyName.fromElement(customxml.TwistedSchedulingObjectResource)
         if value is not None:
@@ -576,6 +601,7 @@
     def _get_scheduleTag(self):
         return str(self.properties().get(PropertyName.fromElement(caldavxml.ScheduleTag), ""))
 
+
     def _set_scheduleTag(self, value):
         pname = PropertyName.fromElement(caldavxml.ScheduleTag)
         if value:
@@ -588,6 +614,7 @@
     def _get_scheduleEtags(self):
         return tuple([str(etag) for etag in self.properties().get(PropertyName.fromElement(customxml.TwistedScheduleMatchETags), customxml.TwistedScheduleMatchETags()).children])
 
+
     def _set_scheduleEtags(self, value):
         if value:
             etags = [davxml.GETETag.fromString(etag) for etag in value]
@@ -603,6 +630,7 @@
     def _get_hasPrivateComment(self):
         return PropertyName.fromElement(customxml.TwistedCalendarHasPrivateCommentsProperty) in self.properties()
 
+
     def _set_hasPrivateComment(self, value):
         pname = PropertyName.fromElement(customxml.TwistedCalendarHasPrivateCommentsProperty)
         if value:
@@ -707,6 +735,7 @@
             ),
         )
 
+
     # IDataStoreObject
     def contentType(self):
         """
@@ -881,6 +910,7 @@
             yield calendarObject
 
 
+
 class Invites(object):
     #
     # OK, here's where we get ugly.

Modified: CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/sql.py	2012-12-18 16:44:30 UTC (rev 10189)
+++ CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/sql.py	2012-12-18 22:06:38 UTC (rev 10190)
@@ -37,27 +37,27 @@
 from twistedcaldav import caldavxml, customxml
 from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
 from twistedcaldav.config import config
-from twistedcaldav.dateops import normalizeForIndex, datetimeMktime,\
+from twistedcaldav.dateops import normalizeForIndex, datetimeMktime, \
     parseSQLTimestamp, pyCalendarTodatetime, parseSQLDateToPyCalendar
 from twistedcaldav.ical import Component, InvalidICalendarDataError
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 from twistedcaldav.memcacher import Memcacher
 
 from txdav.base.propertystore.base import PropertyName
-from txdav.caldav.datastore.util import validateCalendarComponent,\
+from txdav.caldav.datastore.util import validateCalendarComponent, \
     dropboxIDFromCalendarObject
-from txdav.caldav.icalendarstore import ICalendarHome, ICalendar, ICalendarObject,\
+from txdav.caldav.icalendarstore import ICalendarHome, ICalendar, ICalendarObject, \
     IAttachment
-from txdav.common.datastore.sql import CommonHome, CommonHomeChild,\
+from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE
 from txdav.common.datastore.sql_legacy import \
-    PostgresLegacyIndexEmulator, SQLLegacyCalendarInvites,\
+    PostgresLegacyIndexEmulator, SQLLegacyCalendarInvites, \
     SQLLegacyCalendarShares, PostgresLegacyInboxIndexEmulator
-from txdav.common.datastore.sql_tables import CALENDAR_TABLE,\
-    CALENDAR_BIND_TABLE, CALENDAR_OBJECT_REVISIONS_TABLE, CALENDAR_OBJECT_TABLE,\
-    _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,\
+from txdav.common.datastore.sql_tables import CALENDAR_TABLE, \
+    CALENDAR_BIND_TABLE, CALENDAR_OBJECT_REVISIONS_TABLE, CALENDAR_OBJECT_TABLE, \
+    _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, _BIND_STATUS_INVITED, schema
 from twext.enterprise.dal.syntax import Select, Count, ColumnSyntax
 from twext.enterprise.dal.syntax import Insert
@@ -71,8 +71,8 @@
 from txdav.caldav.icalendarstore import QuotaExceeded
 
 from txdav.caldav.datastore.util import StorageTransportBase
-from txdav.common.icommondatastore import IndexedSearchException,\
-    InternalDataStoreError, HomeChildNameAlreadyExistsError,\
+from txdav.common.icommondatastore import IndexedSearchException, \
+    InternalDataStoreError, HomeChildNameAlreadyExistsError, \
     HomeChildNameNotAllowedError
 
 from pycalendar.datetime import PyCalendarDateTime
@@ -117,7 +117,6 @@
         super(CalendarHome, self).__init__(transaction, ownerUID, notifiers)
         self._shares = SQLLegacyCalendarShares(self)
 
-
     createCalendarWithName = CommonHome.createChildWithName
     removeCalendarWithName = CommonHome.removeChildWithName
     calendarWithName = CommonHome.childWithName
@@ -273,41 +272,43 @@
 
     @inlineCallbacks
     def createdHome(self):
-        
+
         # Default calendar
         defaultCal = yield self.createCalendarWithName("calendar")
         props = defaultCal.properties()
         props[PropertyName(*ScheduleCalendarTransp.qname())] = ScheduleCalendarTransp(Opaque())
-        
+
         # Check whether components type must be separate
         if config.RestrictCalendarsToOneComponentType:
             yield defaultCal.setSupportedComponents("VEVENT")
-            
+
             # Default tasks
             defaultTasks = yield self.createCalendarWithName("tasks")
             yield defaultTasks.setSupportedComponents("VTODO")
-            
+
         yield self.createCalendarWithName("inbox")
 
+
     @inlineCallbacks
     def splitCalendars(self):
         """
         Split all regular calendars by component type
         """
-        
+
         # Make sure the loop does not operate on any new calendars created during the loop
         self.log_warn("Splitting calendars for user %s" % (self._ownerUID,))
         calendars = yield self.calendars()
         for calendar in calendars:
-            
-            # Ignore inbox - also shared calendars are not part of .calendars() 
+
+            # Ignore inbox - also shared calendars are not part of .calendars()
             if calendar.name() == "inbox":
                 continue
             split_count = yield calendar.splitCollectionByComponentTypes()
-            self.log_warn("  Calendar: '%s', split into %d" % (calendar.name(), split_count+1,))
+            self.log_warn("  Calendar: '%s', split into %d" % (calendar.name(), split_count + 1,))
 
         yield self.ensureDefaultCalendarsExist()
 
+
     @inlineCallbacks
     def ensureDefaultCalendarsExist(self):
         """
@@ -358,7 +359,7 @@
         cb = schema.CALENDAR_BIND
         rows = yield self._unacceptedSharesQuery.on(self._txn, homeResourceID=self._resourceID)
         for (resourceName,) in rows:
-            kwds = { "ResourceName" : resourceName }
+            kwds = {"ResourceName": resourceName}
             yield Delete(
                 From=inv,
                 Where=(
@@ -380,7 +381,7 @@
         Remove all remaining invite entries for this home.
         """
         inv = schema.INVITE
-        kwds = { "HomeResourceID" : self._resourceID }
+        kwds = {"HomeResourceID": self._resourceID}
         yield Delete(
             From=inv,
             Where=(inv.HOME_RESOURCE_ID == Parameter("HomeResourceID"))
@@ -433,15 +434,16 @@
         different child classes to have their own type specific data, but still make use of the
         common base logic.
         """
-        
+
         # Common behavior is to have created and modified
-        
+
         return (
             cls._homeChildMetaDataSchema.CREATED,
             cls._homeChildMetaDataSchema.MODIFIED,
             cls._homeChildMetaDataSchema.SUPPORTED_COMPONENTS,
         )
-        
+
+
     @classmethod
     def metadataAttributes(cls):
         """
@@ -449,15 +451,16 @@
         different child classes to have their own type specific data, but still make use of the
         common base logic.
         """
-        
+
         # Common behavior is to have created and modified
-        
+
         return (
             "_created",
             "_modified",
             "_supportedComponents",
         )
-        
+
+
     @property
     def _calendarHome(self):
         return self._home
@@ -467,7 +470,6 @@
     def resourceType(self):
         return ResourceType.calendar #@UndefinedVariable
 
-
     ownerCalendarHome = CommonHomeChild.ownerHome
     viewerCalendarHome = CommonHomeChild.viewerHome
     calendarObjects = CommonHomeChild.objectResources
@@ -513,15 +515,18 @@
             cacheKey = queryCacher.keyForHomeChildMetaData(self._resourceID)
             yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
 
+
     def getSupportedComponents(self):
         return self._supportedComponents
 
+
     def isSupportedComponent(self, componentType):
         if self._supportedComponents:
             return componentType.upper() in self._supportedComponents.split(",")
         else:
             return True
 
+
     def initPropertyStore(self, props):
         # Setup peruser special properties
         props.setSpecialProperties(
@@ -535,6 +540,7 @@
             ),
         )
 
+
     # FIXME: this is DAV-ish.  Data store calendar objects don't have
     # mime types.  -wsv
     def contentType(self):
@@ -543,6 +549,7 @@
         """
         return MimeType.fromString("text/calendar; charset=utf-8")
 
+
     @inlineCallbacks
     def splitCollectionByComponentTypes(self):
         """
@@ -551,7 +558,7 @@
         on any new calendars created. Also restrict the new calendars to only the one appropriate component type. Return
         the number of splits done.
         """
-        
+
         # First see how many different component types there are
         split_count = 0
         components = yield self._countComponentTypes()
@@ -562,11 +569,11 @@
             yield self.setSupportedComponents(component.upper())
 
             returnValue(split_count)
-        
+
         # We will leave the component type with the highest count in the current calendar and create new calendars
         # for the others which will be moved over
-        maxComponent = max(components, key=lambda x:x[1])[0]
-        
+        maxComponent = max(components, key=lambda x: x[1])[0]
+
         for component, _ignore_count in components:
             if component == maxComponent:
                 continue
@@ -578,12 +585,13 @@
 
         returnValue(split_count)
 
+
     @inlineCallbacks
     def _countComponentTypes(self):
         """
         Count each component type in this calendar.
-        
-        @return: a C{tuple} of C{tuple} containing the component type name and count. 
+
+        @return: a C{tuple} of C{tuple} containing the component type name and count.
         """
 
         ob = self._objectSchema
@@ -597,17 +605,18 @@
         rows = yield _componentsQuery.on(self._txn, calID=self._resourceID)
         result = tuple([(componentType, componentCount) for componentType, componentCount in sorted(rows, key=lambda x:x[0])])
         returnValue(result)
-        
+
+
     @inlineCallbacks
     def _splitComponentType(self, component):
         """
         Create a new calendar and move all components of the specified component type into the new one.
         Make sure properties and sharing state is preserved on the new calendar.
-        
+
         @param component: Component type to split out
         @type component: C{str}
         """
-        
+
         # Create the new calendar
         try:
             newcalendar = yield self._home.createCalendarWithName("%s-%s" % (self._name, component.lower(),))
@@ -615,25 +624,26 @@
             # If the name we want exists, try repeating with up to ten more
             for ctr in range(10):
                 try:
-                    newcalendar = yield self._home.createCalendarWithName("%s-%s-%d" % (self._name, component.lower(), ctr+1,))
+                    newcalendar = yield self._home.createCalendarWithName("%s-%s-%d" % (self._name, component.lower(), ctr + 1,))
                 except HomeChildNameAlreadyExistsError:
                     continue
             else:
                 # At this point we are stuck
                 raise HomeChildNameNotAllowedError
-        
+
         # Restrict calendar to single component type
         yield newcalendar.setSupportedComponents(component.upper())
-        
+
         # Transfer properties over
         yield newcalendar._properties.copyAllProperties(self._properties)
-        
+
         # Transfer sharing
         yield self._transferSharingDetails(newcalendar, component)
-        
+
         # Now move calendar data over
         yield self._transferCalendarObjects(newcalendar, component)
-        
+
+
     @inlineCallbacks
     def _transferSharingDetails(self, newcalendar, component):
         """
@@ -653,27 +663,28 @@
                 cb.CALENDAR_RESOURCE_ID == inv.RESOURCE_ID).And(
                 cb.CALENDAR_HOME_RESOURCE_ID == inv.HOME_RESOURCE_ID)
         )
-        
+
         rows = yield _bindInviteQuery.on(
             self._txn,
             calID=self._resourceID,
             homeID=self._home._resourceID,
         )
-        
+
         if len(rows) == 0:
             returnValue(None)
-        
+
         for row in rows:
             columnMap = dict(zip(bindcolumns, row[:len(bindcolumns)]))
             columnMap[cb.CALENDAR_RESOURCE_ID] = newcalendar._resourceID
             columnMap[cb.CALENDAR_RESOURCE_NAME] = "%s-%s" % (columnMap[cb.CALENDAR_RESOURCE_NAME], component.lower(),)
-            yield Insert(columnMap).on(self._txn)   
+            yield Insert(columnMap).on(self._txn)
 
             columnMap = dict(zip(invitecolumns, row[len(bindcolumns):]))
             columnMap[inv.INVITE_UID] = "%s-%s" % (columnMap[inv.INVITE_UID], component.lower(),)
             columnMap[inv.RESOURCE_ID] = newcalendar._resourceID
             yield Insert(columnMap).on(self._txn)
 
+
     @inlineCallbacks
     def _transferCalendarObjects(self, newcalendar, component):
         """
@@ -694,15 +705,16 @@
             calID=self._resourceID,
             componentType=component,
         )
-        
+
         if len(rows) == 0:
             returnValue(None)
-        
+
         for row in rows:
             resourceID = row[0]
             child = yield self.objectResourceWithID(resourceID)
             yield self.moveObjectResource(child, newcalendar)
 
+
     @classproperty
     def _moveTimeRangeUpdateQuery(cls): #@NoSelf
         """
@@ -714,6 +726,7 @@
             Where=tr.CALENDAR_OBJECT_RESOURCE_ID == Parameter("resourceID")
         )
 
+
     @inlineCallbacks
     def _movedObjectResource(self, child, newparent):
         """
@@ -725,6 +738,7 @@
             resourceID=child._resourceID
         )
 
+
     def unshare(self):
         """
         Unshares a collection, regardless of which "direction" it was shared.
@@ -755,7 +769,7 @@
     Component.ACCESS_CONFIDENTIAL: 3,
     Component.ACCESS_RESTRICTED  : 4,
 }
-accesstype_to_accessMode = dict([(v, k) for k,v in accessMode_to_type.items()])
+accesstype_to_accessMode = dict([(v, k) for k, v in accessMode_to_type.items()])
 
 def _pathToName(path):
     return path.rsplit(".", 1)[0]
@@ -780,7 +794,6 @@
         self.scheduleEtags = metadata.get("scheduleEtags", "")
         self.hasPrivateComment = metadata.get("hasPrivateComment", False)
 
-
     _allColumns = [
         _objectSchema.RESOURCE_ID,
         _objectSchema.RESOURCE_NAME,
@@ -866,7 +879,7 @@
         # freebusy related properties have changed (e.g. an attendee reply and refresh). In those cases
         # the component will have a special attribute present to let us know to suppress the instance indexing.
         instanceIndexingRequired = not hasattr(component, "noInstanceIndexing") or inserting or reCreate
-        
+
         if instanceIndexingRequired:
 
             # Decide how far to expand based on the component. doInstanceIndexing will indicate whether we
@@ -874,28 +887,28 @@
             # operation.
             doInstanceIndexing = False
             master = component.masterComponent()
-            if ( master is None or not component.isRecurring() ):
+            if (master is None or not component.isRecurring()):
                 # When there is no master we have a set of overridden components -
                 #   index them all.
                 # When there is one instance - index it.
                 expand = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
                 doInstanceIndexing = True
             else:
-    
+
                 # If migrating or re-creating or config option for delayed indexing is off, always index
                 if reCreate or txn._migrating or (not config.FreeBusyIndexDelayedExpand and not isInboxItem):
                     doInstanceIndexing = True
-    
+
                 # Duration into the future through which recurrences are expanded in the index
                 # by default.  This is a caching parameter which affects the size of the index;
                 # it does not affect search results beyond this period, but it may affect
                 # performance of such a search.
                 expand = (PyCalendarDateTime.getToday() +
                           PyCalendarDuration(days=config.FreeBusyIndexExpandAheadDays))
-    
+
                 if expand_until and expand_until > expand:
                     expand = expand_until
-    
+
                 # Maximum duration into the future through which recurrences are expanded in the
                 # index.  This is a caching parameter which affects the size of the index; it
                 # does not affect search results beyond this period, but it may affect
@@ -911,7 +924,7 @@
                 if expand > (PyCalendarDateTime.getToday() +
                              PyCalendarDuration(days=config.FreeBusyIndexExpandMaxDays)):
                     raise IndexedSearchException
-    
+
             if config.FreeBusyIndexLowerLimitDays:
                 truncateLowerLimit = PyCalendarDateTime.getToday()
                 truncateLowerLimit.offsetDay(-config.FreeBusyIndexLowerLimitDays)
@@ -927,7 +940,7 @@
             except InvalidOverriddenInstanceError, e:
                 self.log_error("Invalid instance %s when indexing %s in %s" %
                                (e.rid, self._name, self._calendar,))
-    
+
                 if txn._migrating:
                     # TODO: fix the data here by re-writing component then re-index
                     instances = component.expandTimeRanges(expand, lowerLimit=truncateLowerLimit, ignoreInvalidInstances=True)
@@ -935,7 +948,7 @@
                     recurrenceLowerLimit = instances.lowerLimit
                 else:
                     raise
-    
+
             # Now coerce indexing to off if needed
             if not doInstanceIndexing:
                 instances = None
@@ -1015,7 +1028,7 @@
                         Where=co.RESOURCE_ID == self._resourceID
                     ).on(txn)
                 )[0][0]
-                
+
                 # Need to wipe the existing time-range for this and rebuild if required
                 if instanceIndexingRequired:
                     yield Delete(
@@ -1041,8 +1054,8 @@
 
         if instanceIndexingRequired and doInstanceIndexing:
             yield self._addInstances(component, instances, truncateLowerLimit, txn)
-    
-    
+
+
     @inlineCallbacks
     def _addInstances(self, component, instances, truncateLowerLimit, txn):
         """
@@ -1070,7 +1083,7 @@
             start.setTimezoneUTC(True)
             end.setTimezoneUTC(True)
 
-            # Ignore if below the lower limit            
+            # Ignore if below the lower limit
             if truncateLowerLimit and end < truncateLowerLimit:
                 lowerLimitApplied = True
                 continue
@@ -1087,7 +1100,7 @@
         # Special - for unbounded recurrence we insert a value for "infinity"
         # that will allow an open-ended time-range to always match it.
         # We also need to add the "infinity" value if the event was bounded but
-        # starts after the future expansion cut-off limit. 
+        # starts after the future expansion cut-off limit.
         if component.isRecurringUnbounded() or instances.limit and len(instances.instances) == 0:
             start = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
             end = PyCalendarDateTime(2100, 1, 1, 1, 0, 0, tzid=PyCalendarTimezone(utc=True))
@@ -1153,6 +1166,14 @@
         returnValue(component)
 
 
+    @inlineCallbacks
+    def remove(self):
+        # Need to also remove attachments
+        if self._dropboxID:
+            yield Attachment.resourceRemoved(self._txn, self._resourceID, self._dropboxID)
+        yield super(CalendarObject, self).remove()
+
+
     @classproperty
     def _recurrenceMinMaxByIDQuery(cls): #@NoSelf
         """
@@ -1160,7 +1181,7 @@
         """
         co = schema.CALENDAR_OBJECT
         return Select(
-            [co.RECURRANCE_MIN, co.RECURRANCE_MAX,],
+            [co.RECURRANCE_MIN, co.RECURRANCE_MAX, ],
             From=co,
             Where=co.RESOURCE_ID == Parameter("resourceID"),
         )
@@ -1171,7 +1192,7 @@
         """
         Get the RECURRANCE_MIN, RECURRANCE_MAX value from the database. Occasionally we might need to do an
         update to time-range data via a separate transaction, so we allow that to be passed in.
-    
+
         @return: L{PyCalendarDateTime} result
         """
         # Setup appropriate txn
@@ -1208,7 +1229,7 @@
     def instances(self, txn=None):
         """
         Get the set of instances from the database.
-    
+
         @return: C{list} result
         """
         # Setup appropriate txn
@@ -1235,9 +1256,11 @@
         metadata["hasPrivateComment"] = self.hasPrivateComment
         return metadata
 
+
     def _get_accessMode(self):
         return accesstype_to_accessMode[self._access]
 
+
     def _set_accessMode(self, value):
         self._access = accessMode_to_type[value]
 
@@ -1246,6 +1269,7 @@
     def _get_isScheduleObject(self):
         return self._schedule_object
 
+
     def _set_isScheduleObject(self, value):
         self._schedule_object = value
 
@@ -1254,6 +1278,7 @@
     def _get_scheduleTag(self):
         return self._schedule_tag
 
+
     def _set_scheduleTag(self, value):
         self._schedule_tag = value
 
@@ -1262,6 +1287,7 @@
     def _get_scheduleEtags(self):
         return tuple(self._schedule_etags.split(",")) if self._schedule_etags else ()
 
+
     def _set_scheduleEtags(self, value):
         self._schedule_etags = ",".join(value) if value else ""
 
@@ -1270,6 +1296,7 @@
     def _get_hasPrivateComment(self):
         return self._private_comments
 
+
     def _set_hasPrivateComment(self, value):
         self._private_comments = value
 
@@ -1287,21 +1314,22 @@
             )
         ))
 
+
     @inlineCallbacks
     def removeAttachmentWithName(self, name):
         attachment = (yield self.attachmentWithName(name))
         yield attachment.remove()
 
+
     def attachmentWithName(self, name):
         return Attachment.loadWithName(self._txn, self._dropboxID, name)
 
+
     def attendeesCanManageAttachments(self):
         return self._attachment == _ATTACHMENTS_MODE_WRITE
 
-
     dropboxID = dropboxIDFromCalendarObject
 
-
     _attachmentsQuery = Select(
         [schema.ATTACHMENT.PATH],
         From=schema.ATTACHMENT,
@@ -1334,6 +1362,7 @@
             ),
         )
 
+
     # IDataStoreObject
     def contentType(self):
         """
@@ -1452,6 +1481,8 @@
 def sqltime(value):
     return datetimeMktime(parseSQLTimestamp(value))
 
+
+
 class Attachment(object):
 
     implements(IAttachment)
@@ -1507,7 +1538,37 @@
         returnValue(attachment)
 
 
+    @classmethod
     @inlineCallbacks
+    def resourceRemoved(cls, txn, resourceID, dropboxID):
+        """
+        Remove all attachments referencing the specified resource.
+        """
+
+        # See if any other resources still reference this dropbox ID
+        co = schema.CALENDAR_OBJECT
+        rows = (yield Select(
+            [co.RESOURCE_ID, ],
+            From=co,
+            Where=(co.DROPBOX_ID == dropboxID).And(
+                co.RESOURCE_ID != resourceID)
+        ).on(txn))
+
+        if not rows:
+            # Find each attachment with matching dropbox ID
+            att = schema.ATTACHMENT
+            rows = (yield Select(
+                [att.PATH],
+                From=att,
+                Where=(att.DROPBOX_ID == dropboxID)
+            ).on(txn))
+            for name in rows:
+                name = name[0]
+                attachment = yield cls.loadWithName(txn, dropboxID, name)
+                yield attachment.remove()
+
+
+    @inlineCallbacks
     def initFromStore(self):
         """
         Execute necessary SQL queries to retrieve attributes.
@@ -1555,7 +1616,6 @@
     def retrieve(self, protocol):
         return AttachmentRetrievalTransport(self._path).start(protocol)
 
-
     _removeStatement = Delete(
         From=schema.ATTACHMENT,
         Where=(schema.ATTACHMENT.DROPBOX_ID == Parameter("dropboxID")).And(

Modified: CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/test/test_sql.py	2012-12-18 16:44:30 UTC (rev 10189)
+++ CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/caldav/datastore/test/test_sql.py	2012-12-18 22:06:38 UTC (rev 10190)
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from twext.web2.http_headers import MimeType
+import os
 
 """
 Tests for txdav.caldav.datastore.postgres, mostly based on
@@ -66,8 +68,9 @@
         self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
         yield self.populate()
 
-        self.nowYear = {"now":PyCalendarDateTime.getToday().getYear()}
+        self.nowYear = {"now": PyCalendarDateTime.getToday().getYear()}
 
+
     @inlineCallbacks
     def populate(self):
         yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
@@ -357,6 +360,7 @@
 END:VCALENDAR
 """.replace("\n", "\r\n") % self.nowYear)
 
+
     @inlineCallbacks
     def test_migrateDuplicateAttachmentsCalendarFromFile(self):
         """
@@ -374,6 +378,7 @@
         self.assertEqual(ok, 3)
         self.assertEqual(bad, 0)
 
+
     @inlineCallbacks
     def test_migrateCalendarFromFile_Transparency(self):
         """
@@ -406,6 +411,7 @@
         self.assertEquals(uid, "uid4")
         self.assertEquals(transp, 'T')
 
+
     @inlineCallbacks
     def test_migrateHomeFromFile(self):
         """
@@ -473,6 +479,7 @@
 
         self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
 
+
     @inlineCallbacks
     def test_migrateHomeNoSplits(self):
         """
@@ -500,6 +507,7 @@
 
         self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
 
+
     def test_calendarHomeVersion(self):
         """
         The DATAVERSION column for new calendar homes must match the
@@ -521,7 +529,6 @@
         self.assertEqual(int(homeVersion, version))
 
 
-
     def test_eachCalendarHome(self):
         """
         L{ICalendarStore.eachCalendarHome} is currently stubbed out by
@@ -529,11 +536,9 @@
         """
         return super(CalendarSQLStorageTests, self).test_eachCalendarHome()
 
-
     test_eachCalendarHome.todo = (
         "stubbed out, as migration only needs to go from file->sql currently")
 
-
     @inlineCallbacks
     def test_homeProvisioningConcurrency(self):
         """
@@ -711,6 +716,7 @@
         yield d1
         yield d2
 
+
     @inlineCallbacks
     def test_datetimes(self):
         calendarStore = self._sqlCalendarStore
@@ -730,6 +736,7 @@
         self.assertEqual(obj.created(), datetimeMktime(datetime.datetime(2011, 2, 7, 11, 22, 47)))
         self.assertEqual(obj.modified(), datetimeMktime(datetime.datetime(2011, 2, 8, 11, 22, 47)))
 
+
     @inlineCallbacks
     def test_notificationsProvisioningConcurrency(self):
         """
@@ -769,6 +776,7 @@
         self.assertNotEqual(notification_uid1_1, None)
         self.assertNotEqual(notification_uid1_2, None)
 
+
     @inlineCallbacks
     def test_removeCalendarPropertiesOnDelete(self):
         """
@@ -810,6 +818,7 @@
         self.assertEqual(len(tuple(rows)), 0)
         yield self.commit()
 
+
     @inlineCallbacks
     def test_removeCalendarObjectPropertiesOnDelete(self):
         """
@@ -855,6 +864,7 @@
         self.assertEqual(len(tuple(rows)), 0)
         yield self.commit()
 
+
     @inlineCallbacks
     def test_removeInboxObjectPropertiesOnDelete(self):
         """
@@ -907,6 +917,7 @@
         self.assertEqual(len(tuple(rows)), 0)
         yield self.commit()
 
+
     @inlineCallbacks
     def test_directShareCreateConcurrency(self):
         """
@@ -956,6 +967,7 @@
         yield d1
         yield d2
 
+
     @inlineCallbacks
     def test_transferSharingDetails(self):
         """
@@ -1013,6 +1025,7 @@
         records = (yield invites.allRecords())
         self.assertEqual(len(records), 1)
 
+
     @inlineCallbacks
     def test_moveCalendarObjectResource(self):
         """
@@ -1034,6 +1047,7 @@
         child = yield calendar1.calendarObjectWithName("5.ics")
         self.assertTrue(child is not None)
 
+
     @inlineCallbacks
     def test_splitCalendars(self):
         """
@@ -1104,6 +1118,7 @@
         self.assertTrue(pkey in calendar2.properties())
         self.assertEqual(str(calendar2.properties()[pkey]), "A birthday calendar")
 
+
     @inlineCallbacks
     def test_noSplitCalendars(self):
         """
@@ -1130,6 +1145,7 @@
 
         self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
 
+
     @inlineCallbacks
     def test_resourceLock(self):
         """
@@ -1207,6 +1223,7 @@
         self.assertEqual(rMin, None)
         self.assertEqual(rMax, None)
 
+
     @inlineCallbacks
     def test_notExpandedWithin(self):
         """
@@ -1323,8 +1340,8 @@
         yield calendarObject.setComponent(component)
         instances2 = yield calendarObject.instances()
         self.assertNotEqual(
-            sorted(instances, key=lambda x:x[0])[0],
-            sorted(instances2, key=lambda x:x[0])[0],
+            sorted(instances, key=lambda x: x[0])[0],
+            sorted(instances2, key=lambda x: x[0])[0],
         )
         yield self.commit()
 
@@ -1335,13 +1352,14 @@
         yield calendarObject.setComponent(component)
         instances3 = yield calendarObject.instances()
         self.assertEqual(
-            sorted(instances2, key=lambda x:x[0])[0],
-            sorted(instances3, key=lambda x:x[0])[0],
+            sorted(instances2, key=lambda x: x[0])[0],
+            sorted(instances3, key=lambda x: x[0])[0],
         )
 
         yield calendar.removeCalendarObjectWithName("indexing.ics")
         yield self.commit()
 
+
     @inlineCallbacks
     def test_loadObjectResourcesWithName(self):
         """
@@ -1421,13 +1439,210 @@
         self.assertEqual(resources[0].properties()[PropertyName.fromElement(prop)], prop)
 
         resources = yield inbox.objectResourcesWithNames(("1.ics", "2.ics",))
-        resources.sort(key=lambda x:x._name)
+        resources.sort(key=lambda x: x._name)
         prop = caldavxml.CalendarDescription.fromString("p1")
         self.assertEqual(resources[0].properties()[PropertyName.fromElement(prop)], prop)
         prop = caldavxml.CalendarDescription.fromString("p2")
         self.assertEqual(resources[1].properties()[PropertyName.fromElement(prop)], prop)
 
         resources = yield inbox.objectResourcesWithNames(("bogus1.ics", "2.ics",))
-        resources.sort(key=lambda x:x._name)
+        resources.sort(key=lambda x: x._name)
         prop = caldavxml.CalendarDescription.fromString("p2")
         self.assertEqual(resources[0].properties()[PropertyName.fromElement(prop)], prop)
+
+
+    @inlineCallbacks
+    def test_cleanupAttachments(self):
+        """
+        L{ICalendarObject.remove} will remove an associated calendar
+        attachment.
+        """
+
+        # Create attachment
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        apath = attachment._path.path
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest()
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertFalse(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertEqual(quota, 0)
+
+
+    @inlineCallbacks
+    def test_cleanupMultipleAttachments(self):
+        """
+        L{ICalendarObject.remove} will remove all associated calendar
+        attachments.
+        """
+
+        # Create attachment
+        obj = yield self.calendarObjectUnderTest()
+
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        apath1 = attachment._path.path
+
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment2",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("new attachment 2")
+        t.write(" text")
+        yield t.loseConnection()
+        apath2 = attachment._path.path
+
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath1))
+        self.assertTrue(os.path.exists(apath2))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest()
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertFalse(os.path.exists(apath1))
+        self.assertFalse(os.path.exists(apath2))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertEqual(quota, 0)
+
+
+    @inlineCallbacks
+    def test_cleanupAttachmentsOnMultipleResources(self):
+        """
+        L{ICalendarObject.remove} will remove all associated calendar
+        attachments unless used in another resource.
+        """
+
+        # Create attachment
+        obj = yield self.calendarObjectUnderTest()
+
+        attachment = yield obj.createAttachmentWithName(
+            "new.attachment",
+        )
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("new attachment")
+        t.write(" text")
+        yield t.loseConnection()
+        apath = attachment._path.path
+
+        new_component = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+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
+ATTENDEE;CN="Wilfredo Sanchez";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailt
+ o:wsanchez at example.com
+ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cda
+ boo at example.com
+DTEND;TZID=US/Pacific:%(now)s0324T124500
+TRANSP:OPAQUE
+ORGANIZER;CN="Wilfredo Sanchez":mailto:wsanchez at example.com
+UID:uid1-attachmenttest
+DTSTAMP:20090326T145447Z
+LOCATION:Wilfredo's Office
+SEQUENCE:2
+X-APPLE-EWS-BUSYSTATUS:BUSY
+X-APPLE-DROPBOX:/calendars/__uids__/user01/dropbox/FE5CDC6F-7776-4607-83
+ A9-B90FF7ACC8D0.dropbox
+SUMMARY:CalDAV protocol updates
+DTSTART;TZID=US/Pacific:%(now)s0324T121500
+CREATED:20090326T145440Z
+BEGIN:VALARM
+X-WR-ALARMUID:DB39AB67-449C-441C-89D2-D740B5F41A73
+TRIGGER;VALUE=DATE-TIME:%(now)s0324T180009Z
+ACTION:AUDIO
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {"now": 2012}
+
+        calendar = yield self.calendarUnderTest()
+        yield calendar.createCalendarObjectWithName(
+            "test.ics", VComponent.fromString(new_component)
+        )
+
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest()
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertTrue(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertNotEqual(quota, 0)
+
+        # Remove resource
+        obj = yield self.calendarObjectUnderTest("test.ics")
+        yield obj.remove()
+        yield self.commit()
+
+        self.assertFalse(os.path.exists(apath))
+
+        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        quota = (yield home.quotaUsedBytes())
+        yield self.commit()
+        self.assertEqual(quota, 0)

Modified: CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/common/datastore/sql.py	2012-12-18 16:44:30 UTC (rev 10189)
+++ CalendarServer/branches/release/CalendarServer-4.3-dev/txdav/common/datastore/sql.py	2012-12-18 22:06:38 UTC (rev 10190)
@@ -80,7 +80,7 @@
 
 from txdav.common.icommondatastore import ConcurrentModification
 from twistedcaldav.customxml import NotificationType
-from twistedcaldav.dateops import datetimeMktime, parseSQLTimestamp,\
+from twistedcaldav.dateops import datetimeMktime, parseSQLTimestamp, \
     pyCalendarTodatetime
 from txdav.xml.rfc2518 import DisplayName
 
@@ -218,6 +218,7 @@
         self._migrating = state
         self._enableNotifications = not state
 
+
     def setUpgrading(self, state):
         """
         Set the "upgrading" state
@@ -225,17 +226,19 @@
         self._enableNotifications = not state
 
 
+
 class TransactionStatsCollector(object):
     """
     Used to log each SQL query and statistics about that query during the course of a single transaction.
     Results can be printed out where ever needed at the end of the transaction.
     """
-    
+
     def __init__(self, label, logFileName=None):
         self.label = label
         self.logFileName = logFileName
         self.statements = []
-    
+
+
     def startStatement(self, sql, args):
         """
         Called prior to an SQL query being run.
@@ -244,7 +247,7 @@
         @type sql: C{str}
         @param args: the arguments (binds) to the SQL statement
         @type args: C{list}
-        
+
         @return: C{tuple} containing the index in the statement list for this statement, and the start time
         """
         args = ["%s" % (arg,) for arg in args]
@@ -252,6 +255,7 @@
         self.statements.append(["%s %s" % (sql, args,), 0, 0])
         return len(self.statements) - 1, time.time()
 
+
     def endStatement(self, context, rows):
         """
         Called after an SQL query has executed.
@@ -264,12 +268,13 @@
         index, tstamp = context
         self.statements[index][1] = len(rows) if rows else 0
         self.statements[index][2] = time.time() - tstamp
-        
+
+
     def printReport(self):
         """
         Print a report of all the SQL statements executed to date.
         """
-        
+
         toFile = StringIO()
         toFile.write("*** SQL Stats ***\n")
         toFile.write("\n")
@@ -284,18 +289,20 @@
             toFile.write("Rows: %s\n" % (rows,))
             toFile.write("Time (ms): %.3f\n" % (t,))
         toFile.write("***\n\n")
-        
+
         if self.logFileName:
             open(self.logFileName, "a").write(toFile.getvalue())
         else:
             log.error(toFile.getvalue())
 
+
+
 class CommonStoreTransactionMonitor(object):
     """
     Object that monitors the state of a transaction over time and logs or times out
     the transaction.
     """
-    
+
     callLater = reactor.callLater
 
     def __init__(self, txn, logTimerSeconds, timeoutSeconds):
@@ -304,13 +311,14 @@
         self.delayedTimeout = None
         self.logTimerSeconds = logTimerSeconds
         self.timeoutSeconds = timeoutSeconds
-        
+
         self.txn.postCommit(self._cleanTxn)
         self.txn.postAbort(self._cleanTxn)
-        
+
         self._installLogTimer()
         self._installTimeout()
-    
+
+
     def _cleanTxn(self):
         self.txn = None
         if self.delayedLog:
@@ -320,6 +328,7 @@
             self.delayedTimeout.cancel()
             self.delayedTimeout = None
 
+
     def _installLogTimer(self):
         def _logTransactionWait():
             if self.txn is not None:
@@ -328,7 +337,8 @@
 
         if self.logTimerSeconds:
             self.delayedLog = self.callLater(self.logTimerSeconds, _logTransactionWait)
-    
+
+
     def _installTimeout(self):
         def _forceAbort():
             if self.txn is not None:
@@ -440,11 +450,12 @@
     def _calendarserver(cls): #@NoSelf
         cs = schema.CALENDARSERVER
         return Select(
-            [cs.VALUE,],
+            [cs.VALUE, ],
             From=cs,
             Where=cs.NAME == Parameter('name'),
         )
 
+
     @inlineCallbacks
     def calendarserverValue(self, key):
         result = yield self._calendarserver.on(self, name=key)
@@ -508,6 +519,7 @@
         """
         return NotificationCollection.notificationsWithUID(self, uid, create)
 
+
     @classproperty
     def _insertAPNSubscriptionQuery(cls): #@NoSelf
         apn = schema.APN_SUBSCRIPTIONS
@@ -649,9 +661,11 @@
     def isNotifiedAlready(self, obj):
         return obj in self._notifiedAlready
 
+
     def notificationAddedForObject(self, obj):
         self._notifiedAlready.add(obj)
 
+
     def isBumpedAlready(self, obj):
         """
         Indicates whether or not bumpAddedForObject has already been
@@ -660,6 +674,7 @@
         """
         return obj in self._bumpedAlready
 
+
     def bumpAddedForObject(self, obj):
         """
         Records the fact that a bumpModified( ) call has already been
@@ -759,12 +774,13 @@
             # way they have to be prepared for anyway.
             end()
 
+
     @inlineCallbacks
     def execSQL(self, *a, **kw):
         """
         Execute some SQL (delegate to L{IAsyncTransaction}).
         """
-        if self._stats:        
+        if self._stats:
             statsContext = self._stats.startStatement(a[0], a[1])
         self.currentStatement = a[0]
         if self._store.logTransactionWaits and a[0].split(" ", 1)[0].lower() in ("insert", "update", "delete",):
@@ -779,18 +795,19 @@
             results = (yield self._sqlTxn.execSQL(*a, **kw))
         finally:
             self.currentStatement = None
-            if self._stats:        
+            if self._stats:
                 self._stats.endStatement(statsContext, results)
         returnValue(results)
 
+
     @inlineCallbacks
     def execSQLBlock(self, sql):
         """
         Execute a block of SQL by parsing it out into individual statements and execute
         each of those.
-        
+
         FIXME: temporary measure for handling large schema upgrades. This should NOT be used
-        for regular SQL operations - only upgrades. 
+        for regular SQL operations - only upgrades.
         """
         parsed = parse(sql)
         for stmt in parsed:
@@ -801,6 +818,7 @@
             stmt = str(stmt).rstrip(";")
             yield self.execSQL(stmt)
 
+
     def commit(self):
         """
         Commit the transaction and execute any post-commit hooks.
@@ -822,7 +840,7 @@
         co = schema.CALENDAR_OBJECT
         cb = schema.CALENDAR_BIND
         tr = schema.TIME_RANGE
-        kwds = { }
+        kwds = {}
         if limited:
             kwds["Limit"] = Parameter("batchSize")
         return Select(
@@ -834,9 +852,9 @@
             ],
             From=ch.join(co).join(cb).join(tr),
             Where=(
-                ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID     ).And(
-                tr.CALENDAR_OBJECT_RESOURCE_ID == co.RESOURCE_ID   ).And(
-                cb.CALENDAR_RESOURCE_ID == tr.CALENDAR_RESOURCE_ID ).And(
+                ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID).And(
+                tr.CALENDAR_OBJECT_RESOURCE_ID == co.RESOURCE_ID).And(
+                cb.CALENDAR_RESOURCE_ID == tr.CALENDAR_RESOURCE_ID).And(
                 cb.BIND_MODE == _BIND_MODE_OWN
             ),
             GroupBy=(
@@ -870,7 +888,7 @@
             if cutoff < truncateLowerLimit:
                 raise ValueError("Cannot query events older than %s" % (truncateLowerLimit.getText(),))
 
-        kwds = { "CutOff" : pyCalendarTodatetime(cutoff) }
+        kwds = {"CutOff": pyCalendarTodatetime(cutoff)}
         if batchSize is not None:
             kwds["batchSize"] = batchSize
             query = self._oldEventsLimited
@@ -948,21 +966,28 @@
         results = (yield self.orphanedAttachments(batchSize=batchSize))
         count = 0
         for dropboxID, path in results:
-            attachment = Attachment(self, dropboxID, path)
-            (yield attachment.remove( ))
+            attachment = (yield Attachment.loadWithName(self, dropboxID, path))
+            (yield attachment.remove())
             count += 1
         returnValue(count)
 
 
+
 class _EmptyCacher(object):
+
     def set(self, key, value):
         return succeed(True)
+
+
     def get(self, key, withIdentifier=False):
         return succeed(None)
+
+
     def delete(self, key):
         return succeed(True)
 
 
+
 class CommonHome(LoggingMixIn):
 
     # All these need to be initialized by derived classes for each store type
@@ -975,10 +1000,10 @@
     _notifierPrefix = None
     _revisionsTable = None
     _notificationRevisionsTable = NOTIFICATION_OBJECT_REVISIONS_TABLE
-    
+
     _dataVersionKey = None
     _dataVersionValue = None
-    
+
     _cacher = None  # Initialize in derived classes
 
     def __init__(self, transaction, ownerUID, notifiers):
@@ -1025,6 +1050,7 @@
         return Select([home.RESOURCE_ID],
                       From=home, Where=home.OWNER_UID == Parameter("ownerUID"))
 
+
     @classproperty
     def _ownerFromFromResourceID(cls): #@NoSelf
         home = cls._homeSchema
@@ -1032,6 +1058,7 @@
                       From=home,
                       Where=home.RESOURCE_ID == Parameter("resourceID"))
 
+
     @classproperty
     def _metaDataQuery(cls): #@NoSelf
         metadata = cls._homeMetaDataSchema
@@ -1039,6 +1066,7 @@
                       From=metadata,
                       Where=metadata.RESOURCE_ID == Parameter("resourceID"))
 
+
     @inlineCallbacks
     def initFromStore(self, no_cache=False):
         """
@@ -1205,7 +1233,7 @@
 
         @return: an iterable of C{str}s.
         """
-        
+
         if self._childrenLoaded:
             return succeed(self._children.keys())
         else:
@@ -1217,7 +1245,7 @@
         Retrieve the names of the children in this home.
 
         @return: an iterable of C{str}s.
-        """ 
+        """
         if self._childrenLoaded:
             return succeed(self._sharedChildren.keys())
         else:
@@ -1235,6 +1263,7 @@
         """
         return self._childClass.objectWithName(self, name, owned=True)
 
+
     @memoizedKey("resourceID", "_children")
     def childWithID(self, resourceID):
         """
@@ -1246,6 +1275,7 @@
         """
         return self._childClass.objectWithID(self, resourceID)
 
+
     @memoizedKey("name", "_sharedChildren")
     def sharedChildWithName(self, name):
         """
@@ -1274,6 +1304,7 @@
         child = (yield self.childWithName(name))
         returnValue(child)
 
+
     @inlineCallbacks
     def removeChildWithName(self, name):
         child = yield self.childWithName(name)
@@ -1534,7 +1565,7 @@
     def _preLockResourceIDQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
         return Select(From=meta,
-                      Where=meta.RESOURCE_ID==Parameter("resourceID"),
+                      Where=meta.RESOURCE_ID == Parameter("resourceID"),
                       ForUpdate=True)
 
 
@@ -1577,7 +1608,7 @@
                                            resourceID=self._resourceID)
             self._quotaUsedBytes = 0
 
-    
+
     def addNotifier(self, notifier):
         if self._notifiers is None:
             self._notifiers = ()
@@ -1601,6 +1632,7 @@
         else:
             returnValue(None)
 
+
     @classproperty
     def _lockLastModifiedQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
@@ -1611,6 +1643,7 @@
             NoWait=True
         )
 
+
     @classproperty
     def _changeLastModifiedQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
@@ -1618,6 +1651,7 @@
                       Where=meta.RESOURCE_ID == Parameter("resourceID"),
                       Return=meta.MODIFIED)
 
+
     @inlineCallbacks
     def bumpModified(self):
         """
@@ -1640,7 +1674,7 @@
             yield self._lockLastModifiedQuery.on(subtxn, resourceID=self._resourceID)
             result = (yield self._changeLastModifiedQuery.on(subtxn, resourceID=self._resourceID))
             returnValue(result)
-            
+
         try:
             self._modified = (yield self._txn.subtransaction(_bumpModified, retries=0, failureOK=True))[0][0]
             queryCacher = self._txn._queryCacher
@@ -1651,6 +1685,7 @@
         except AllRetriesFailed:
             log.debug("CommonHome.bumpModified failed")
 
+
     @inlineCallbacks
     def notifyChanged(self):
         """
@@ -1690,6 +1725,7 @@
         _ignore_uuid, revision = token.split("_", 1)
         return int(revision)
 
+
     @inlineCallbacks
     def syncToken(self):
         if self._syncTokenRevision is None:
@@ -1716,7 +1752,7 @@
 
     @inlineCallbacks
     def resourceNamesSinceToken(self, token):
-        
+
         if token is None:
             token = 0
         elif isinstance(token, str):
@@ -1728,7 +1764,7 @@
             (yield self._objectNamesSinceRevisionQuery.on(
                 self._txn, revision=token, resourceID=self._resourceID))
         ]
-        results.sort(key=lambda x:x[1])
+        results.sort(key=lambda x: x[1])
 
         changed = []
         deleted = []
@@ -1939,16 +1975,16 @@
                 self._syncTokenRevision = rows[0][0]
             else:
                 action = "insert"
-        
+
         if action == "insert":
             # Note that an "insert" may happen for a resource that previously
             # existed and then was deleted. In that case an entry in the
             # REVISIONS table still exists so we have to detect that and do db
             # INSERT or UPDATE as appropriate
 
-            found = bool( (
+            found = bool((
                 yield self._insertFindPreviouslyNamedQuery.on(
-                    self._txn, resourceID=self._resourceID, name=name)) )
+                    self._txn, resourceID=self._resourceID, name=name)))
             if found:
                 self._syncTokenRevision = (
                     yield self._updatePreviouslyNamedQuery.on(
@@ -1987,18 +2023,18 @@
 
     _objectResourceClass = None
 
-    _bindSchema              = None
-    _homeChildSchema         = None
+    _bindSchema = None
+    _homeChildSchema = None
     _homeChildMetaDataSchema = None
-    _revisionsSchema         = None
-    _objectSchema            = None
+    _revisionsSchema = None
+    _objectSchema = None
 
-    _bindTable           = None
-    _homeChildTable      = None
-    _homeChildBindTable  = None
-    _revisionsTable      = None
-    _revisionsBindTable  = None
-    _objectTable         = None
+    _bindTable = None
+    _homeChildTable = None
+    _homeChildBindTable = None
+    _revisionsTable = None
+    _revisionsBindTable = None
+    _objectTable = None
 
 
     def __init__(self, home, name, resourceID, owned, mode):
@@ -2010,19 +2046,19 @@
         else:
             notifiers = None
 
-        self._home              = home
-        self._name              = name
-        self._resourceID        = resourceID
-        self._owned             = owned
-        self._bindMode          = mode
-        self._created           = None
-        self._modified          = None
-        self._objects           = {}
-        self._objectNames       = None
+        self._home = home
+        self._name = name
+        self._resourceID = resourceID
+        self._owned = owned
+        self._bindMode = mode
+        self._created = None
+        self._modified = None
+        self._objects = {}
+        self._objectNames = None
         self._syncTokenRevision = None
-        self._notifiers         = notifiers
-        self._index             = None  # Derived classes need to set this
-        self._invites           = None  # Derived classes need to set this
+        self._notifiers = notifiers
+        self._index = None  # Derived classes need to set this
+        self._invites = None  # Derived classes need to set this
 
 
     @classproperty
@@ -2041,14 +2077,15 @@
         different child classes to have their own type specific data, but still make use of the
         common base logic.
         """
-        
+
         # Common behavior is to have created and modified
-        
+
         return (
             cls._homeChildMetaDataSchema.CREATED,
             cls._homeChildMetaDataSchema.MODIFIED,
         )
-        
+
+
     @classmethod
     def metadataAttributes(cls):
         """
@@ -2056,14 +2093,15 @@
         different child classes to have their own type specific data, but still make use of the
         common base logic.
         """
-        
+
         # Common behavior is to have created and modified
-        
+
         return (
             "_created",
             "_modified",
         )
-        
+
+
     @classproperty
     def _sharedChildListQuery(cls): #@NoSelf
         bind = cls._bindSchema
@@ -2073,6 +2111,7 @@
                                  bind.BIND_MODE != _BIND_MODE_OWN).And(
                                  bind.BIND_STATUS == _BIND_STATUS_ACCEPTED))
 
+
     @classmethod
     @inlineCallbacks
     def listObjects(cls, home, owned):
@@ -2145,8 +2184,7 @@
         return Update({bind.BIND_MODE: Parameter("mode"),
                        bind.BIND_STATUS: Parameter("status"),
                        bind.MESSAGE: Parameter("message")},
-                      Where=
-                      (bind.RESOURCE_ID == Parameter("resourceID"))
+                      Where=(bind.RESOURCE_ID == Parameter("resourceID"))
                       .And(bind.HOME_RESOURCE_ID == Parameter("homeID")),
                       Return=bind.RESOURCE_NAME)
 
@@ -2256,7 +2294,6 @@
             (yield sharerCollection.unshareWith(self._home))
 
 
-
     @classproperty
     def _bindEntriesFor(cls): #@NoSelf
         bind = cls._bindSchema
@@ -2690,9 +2727,9 @@
 
         # Set to non-existent state
         self._resourceID = None
-        self._created    = None
-        self._modified   = None
-        self._objects    = {}
+        self._created = None
+        self._modified = None
+        self._objects = {}
 
         yield self.notifyChanged()
 
@@ -2946,7 +2983,7 @@
 
     @inlineCallbacks
     def removeObjectResourceWithName(self, name):
-        
+
         child = (yield self.objectResourceWithName(name))
         if child is None:
             raise NoSuchObjectResourceError
@@ -2955,24 +2992,26 @@
 
     @inlineCallbacks
     def removeObjectResourceWithUID(self, uid):
-        
+
         child = (yield self.objectResourceWithUID(uid))
         if child is None:
             raise NoSuchObjectResourceError
         yield self._removeObjectResource(child)
 
+
     @inlineCallbacks
     def _removeObjectResource(self, child):
         name = child.name()
         uid = child.uid()
         try:
             yield child.remove()
-        finally:        
+        finally:
             self._objects.pop(name, None)
             self._objects.pop(uid, None)
             yield self._deleteRevision(name)
             yield self.notifyChanged()
 
+
     @classproperty
     def _moveParentUpdateQuery(cls): #@NoSelf
         """
@@ -2984,6 +3023,7 @@
             Where=obj.RESOURCE_ID == Parameter("resourceID")
         )
 
+
     def _movedObjectResource(self, child, newparent):
         """
         Method that subclasses can override to do an extra DB adjustments when a resource
@@ -2991,12 +3031,13 @@
         """
         return succeed(True)
 
+
     @inlineCallbacks
     def moveObjectResource(self, child, newparent):
         """
         Move a child of this collection into another collection without actually removing/re-inserting the data.
         Make sure sync and cache details for both collections are updated.
-        
+
         TODO: check that the resource name does not exist in the new parent, or that the UID
         does not exist there too.
 
@@ -3015,7 +3056,7 @@
         self._objects.pop(child._resourceID, None)
         yield self._deleteRevision(name)
         yield self.notifyChanged()
-        
+
         # Adjust the child to be a child of the new parent and update ancillary tables
         yield self._moveParentUpdateQuery.on(
             self._txn,
@@ -3029,6 +3070,7 @@
         yield newparent._insertRevision(name)
         yield newparent.notifyChanged()
 
+
     def objectResourcesHaveProperties(self):
         return False
 
@@ -3058,7 +3100,6 @@
         @param props: the L{PropertyStore} from C{properties()}.
         """
 
-
     # IDataStoreObject
     def contentType(self):
         raise NotImplementedError()
@@ -3084,13 +3125,15 @@
         if self._notifiers is None:
             self._notifiers = ()
         self._notifiers += (notifier,)
- 
+
+
     def notifierID(self, label="default"):
         if self._notifiers:
             return self._notifiers[0].getID(label)
         else:
             return None
 
+
     @inlineCallbacks
     def nodeName(self, label="default"):
         if self._notifiers:
@@ -3101,6 +3144,7 @@
         else:
             returnValue(None)
 
+
     @classproperty
     def _lockLastModifiedQuery(cls): #@NoSelf
         schema = cls._homeChildMetaDataSchema
@@ -3111,6 +3155,7 @@
             NoWait=True
         )
 
+
     @classproperty
     def _changeLastModifiedQuery(cls): #@NoSelf
         schema = cls._homeChildMetaDataSchema
@@ -3149,6 +3194,7 @@
         except AllRetriesFailed:
             log.debug("CommonHomeChild.bumpModified failed")
 
+
     @inlineCallbacks
     def notifyChanged(self):
         """
@@ -3166,6 +3212,7 @@
             self._txn.notificationAddedForObject(self)
 
 
+
 class CommonObjectResource(LoggingMixIn, FancyEqMixin):
     """
     Base class for object resources.
@@ -3192,7 +3239,7 @@
         self._created = None
         self._modified = None
         self._objectText = None
-        
+
         self._locked = False
 
 
@@ -3222,7 +3269,7 @@
         if dataRows:
             # Get property stores for all these child resources (if any found)
             if parent.objectResourcesHaveProperties():
-                propertyStores =(yield PropertyStore.forMultipleResources(
+                propertyStores = (yield PropertyStore.forMultipleResources(
                     parent._home.uid(),
                     parent._txn,
                     cls._objectSchema.RESOURCE_ID,
@@ -3243,6 +3290,7 @@
 
         returnValue(results)
 
+
     @classmethod
     def _allColumnsWithParentAndNames(cls, names): #@NoSelf
         obj = cls._objectSchema
@@ -3263,9 +3311,10 @@
             result_batch = (yield cls._loadAllObjectsWithNames(parent, names[:cls.BATCH_LOAD_SIZE]))
             results.extend(result_batch)
             names = names[cls.BATCH_LOAD_SIZE:]
-        
+
         returnValue(results)
-            
+
+
     @classmethod
     @inlineCallbacks
     def _loadAllObjectsWithNames(cls, parent, names):
@@ -3290,7 +3339,7 @@
         if dataRows:
             # Get property stores for all these child resources
             if parent.objectResourcesHaveProperties():
-                propertyStores =(yield PropertyStore.forMultipleResourcesWithResourceIDs(
+                propertyStores = (yield PropertyStore.forMultipleResourcesWithResourceIDs(
                     parent._home.uid(),
                     parent._txn,
                     tuple([row[0] for row in dataRows]),
@@ -3315,11 +3364,13 @@
         objectResource = cls(parent, name, uid, None)
         return objectResource.initFromStore()
 
+
     @classmethod
     def objectWithID(cls, parent, resourceID):
         objectResource = cls(parent, None, None, resourceID)
         return objectResource.initFromStore()
 
+
     @classmethod
     @inlineCallbacks
     def create(cls, parent, name, component, metadata):
@@ -3330,14 +3381,14 @@
 
         if name.startswith("."):
             raise ObjectResourceNameNotAllowedError(name)
-        
+
         objectResource = cls(parent, name, None, None, metadata=metadata)
         yield objectResource.setComponent(component, inserting=True)
         yield objectResource._loadPropertyStore(created=True)
 
         # Note: setComponent triggers a notification, so we don't need to
         # call notify( ) here like we do for object removal.
-        
+
         returnValue(objectResource)
 
 
@@ -3448,7 +3499,7 @@
         self.initPropertyStore(props)
         self._propertyStore = props
 
-    
+
     def properties(self):
         return self._propertyStore
 
@@ -3495,11 +3546,11 @@
         @type wait: C{bool}
         @param txn: alternative transaction to use
         @type txn: L{CommonStoreTransaction}
-        
+
         @raise: L{NoSuchObjectResourceError} if resource does not exist, other L{Exception}
                 if already locked and NOWAIT is used.
         """
-        
+
         txn = txn if txn is not None else self._txn
         yield self._selectForUpdateQuery(not wait).on(txn, NoSuchObjectResourceError, resourceID=self._resourceID)
         self._locked = True
@@ -3517,6 +3568,7 @@
     def componentType(self):
         returnValue((yield self.component()).mainType())
 
+
     @classproperty
     def _deleteQuery(cls): #@NoSelf
         """
@@ -3541,6 +3593,7 @@
         self._modified = None
         self._objectText = None
 
+
     def uid(self):
         return self._uid
 
@@ -3549,7 +3602,6 @@
         return self._name
 
 
-
     # IDataStoreObject
     def contentType(self):
         raise NotImplementedError()
@@ -3637,12 +3689,10 @@
             notifiers = None
         self._notifiers = notifiers
 
-
     _resourceIDFromUIDQuery = Select(
         [_homeSchema.RESOURCE_ID], From=_homeSchema,
         Where=_homeSchema.OWNER_UID == Parameter("uid"))
 
-
     _provisionNewNotificationsQuery = Insert(
         {_homeSchema.OWNER_UID: Parameter("uid")},
         Return=_homeSchema.RESOURCE_ID
@@ -3737,7 +3787,6 @@
         self._notificationNames = sorted([result.name() for result in results])
         returnValue(results)
 
-
     _notificationUIDsForHomeQuery = Select(
         [schema.NOTIFICATION.NOTIFICATION_UID], From=schema.NOTIFICATION,
         Where=schema.NOTIFICATION.NOTIFICATION_HOME_RESOURCE_ID ==
@@ -3795,7 +3844,6 @@
     def removeNotificationObjectWithName(self, name):
         return self.removeNotificationObjectWithUID(self._nameToUID(name))
 
-
     _removeByUIDQuery = Delete(
         From=schema.NOTIFICATION,
         Where=(schema.NOTIFICATION.NOTIFICATION_UID == Parameter("uid")).And(
@@ -3810,7 +3858,6 @@
         self._notifications.pop(uid, None)
         yield self._deleteRevision("%s.xml" % (uid,))
 
-
     _initSyncTokenQuery = Insert(
         {
             _revisionsSchema.HOME_RESOURCE_ID : Parameter("resourceID"),
@@ -3826,7 +3873,6 @@
         self._syncTokenRevision = (yield self._initSyncTokenQuery.on(
             self._txn, resourceID=self._resourceID))[0][0]
 
-
     _syncTokenQuery = Select(
         [Max(_revisionsSchema.REVISION)], From=_revisionsSchema,
         Where=_revisionsSchema.HOME_RESOURCE_ID == Parameter("resourceID")
@@ -3851,7 +3897,8 @@
         if self._notifiers is None:
             self._notifiers = ()
         self._notifiers += (notifier,)
- 
+
+
     def notifierID(self, label="default"):
         if self._notifiers:
             return self._notifiers[0].getID(label)
@@ -3907,7 +3954,7 @@
         """
         # Delete NOTIFICATION rows
         no = schema.NOTIFICATION
-        kwds = { "ResourceID" : self._resourceID }
+        kwds = {"ResourceID": self._resourceID}
         yield Delete(
             From=no,
             Where=(
@@ -3925,6 +3972,7 @@
         ).on(self._txn, **kwds)
 
 
+
 class NotificationObject(LoggingMixIn, FancyEqMixin):
 
     implements(INotificationObject)
@@ -3985,7 +4033,7 @@
 
         if dataRows:
             # Get property stores for all these child resources (if any found)
-            propertyStores =(yield PropertyStore.forMultipleResources(
+            propertyStores = (yield PropertyStore.forMultipleResources(
                 parent.uid(),
                 parent._txn,
                 schema.NOTIFICATION.RESOURCE_ID,
@@ -4134,7 +4182,6 @@
             self._modified = rows[0][0]
         self._objectText = xmldata
 
-
     _xmlDataFromID = Select(
         [_objectSchema.XML_DATA], From=_objectSchema,
         Where=_objectSchema.RESOURCE_ID == Parameter("resourceID"))
@@ -4163,6 +4210,7 @@
     def size(self):
         return self._size
 
+
     def xmlType(self):
         # NB This is the NotificationType property element
         if isinstance(self._xmlType, str):
@@ -4171,6 +4219,7 @@
 
         return self._xmlType
 
+
     def created(self):
         return datetimeMktime(parseSQLTimestamp(self._created))
 
@@ -4530,6 +4579,3 @@
         # obscure bug.
     else:
         yield t.commit()
-
-
-
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121218/e72c9452/attachment-0001.html>


More information about the calendarserver-changes mailing list