Revision: 9266 http://trac.macosforge.org/projects/calendarserver/changeset/9266 Author: sagen@apple.com Date: 2012-05-23 17:25:40 -0700 (Wed, 23 May 2012) Log Message: ----------- Purge tool now handles shared collections, and removes home resource properties. Modified Paths: -------------- CalendarServer/trunk/calendarserver/tools/purge.py CalendarServer/trunk/calendarserver/tools/test/test_purge.py CalendarServer/trunk/txdav/caldav/datastore/sql.py CalendarServer/trunk/txdav/caldav/datastore/test/common.py CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py CalendarServer/trunk/txdav/carddav/datastore/sql.py CalendarServer/trunk/txdav/common/datastore/sql.py Modified: CalendarServer/trunk/calendarserver/tools/purge.py =================================================================== --- CalendarServer/trunk/calendarserver/tools/purge.py 2012-05-22 21:29:28 UTC (rev 9265) +++ CalendarServer/trunk/calendarserver/tools/purge.py 2012-05-24 00:25:40 UTC (rev 9266) @@ -660,7 +660,42 @@ # See if calendar home is provisioned txn = store.newTransaction() - calHomeProvisioned = ((yield txn.calendarHomeWithUID(uid)) is not None) + storeCalHome = (yield txn.calendarHomeWithUID(uid)) + calHomeProvisioned = storeCalHome is not None + + # If in "completely" mode, unshare collections, remove notifications + if calHomeProvisioned and completely: + + # Process shared-to-me calendars + names = list((yield storeCalHome.listSharedChildren())) + for name in names: + if verbose: + if dryrun: + print "Would unshare: %s" % (name,) + else: + print "Unsharing: %s" % (name,) + if not dryrun: + child = (yield storeCalHome.sharedChildWithName(name)) + (yield child.unshare()) + + # Process shared calendars + children = list((yield storeCalHome.children())) + for child in children: + if verbose: + if dryrun: + print "Would unshare: %s" % (child.name(),) + else: + print "Unsharing: %s" % (child.name(),) + if not dryrun: + (yield child.unshare()) + + if not dryrun: + (yield storeCalHome.removeUnacceptedShares()) + (yield storeCalHome.removeInvites()) + notificationHome = (yield txn.notificationsWithUID(uid)) + if notificationHome is not None: + (yield notificationHome.remove()) + (yield txn.commit()) # Anything in the past is left alone @@ -686,8 +721,8 @@ calendarHome = yield principal.calendarHome(request) for collName in (yield calendarHome.listChildren()): collection = (yield calendarHome.getChild(collName)) + if collection.isCalendarCollection() or collName == "inbox": - childNames = [] if completely: Modified: CalendarServer/trunk/calendarserver/tools/test/test_purge.py =================================================================== --- CalendarServer/trunk/calendarserver/tools/test/test_purge.py 2012-05-22 21:29:28 UTC (rev 9265) +++ CalendarServer/trunk/calendarserver/tools/test/test_purge.py 2012-05-24 00:25:40 UTC (rev 9266) @@ -29,6 +29,7 @@ from twisted.trial import unittest from twisted.internet.defer import inlineCallbacks from txdav.common.datastore.test.util import buildStore, populateCalendarsFrom, CommonCommonTests +from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE from twext.web2.http_headers import MimeType @@ -783,6 +784,7 @@ Tests for purging the data belonging to a given principal """ uid = "6423F94A-6B76-4A3A-815B-D52CFD77935D" + uid2 = "37DB0C90-4DB1-4932-BC69-3DAB66F374F5" metadata = { "accessMode": "PUBLIC", @@ -798,6 +800,10 @@ "attachment.ics" : (ATTACHMENT_ICS, metadata,), } }, + uid2 : { + "calendar2" : { + } + }, } @inlineCallbacks @@ -819,8 +825,9 @@ self.rootResource = getRootResource(config, self._sqlCalendarStore) self.directory = self.rootResource.getDirectory() + txn = self._sqlCalendarStore.newTransaction() + # Add attachment to attachment.ics - txn = self._sqlCalendarStore.newTransaction() home = (yield txn.calendarHomeWithUID(self.uid)) calendar = (yield home.calendarWithName("calendar1")) event = (yield calendar.calendarObjectWithName("attachment.ics")) @@ -829,9 +836,25 @@ t.write("attachment") t.write(" text") (yield t.loseConnection()) + + # Share calendars each way + home2 = (yield txn.calendarHomeWithUID(self.uid2)) + calendar2 = (yield home2.calendarWithName("calendar2")) + self.sharedName = (yield calendar2.shareWith(home, _BIND_MODE_WRITE)) + self.sharedName2 = (yield calendar.shareWith(home2, _BIND_MODE_WRITE)) + (yield txn.commit()) + txn = self._sqlCalendarStore.newTransaction() + home = (yield txn.calendarHomeWithUID(self.uid)) + calendar2 = (yield home.sharedChildWithName(self.sharedName)) + self.assertNotEquals(calendar2, None) + home2 = (yield txn.calendarHomeWithUID(self.uid2)) + calendar1 = (yield home2.sharedChildWithName(self.sharedName2)) + self.assertNotEquals(calendar1, None) + (yield txn.commit()) + @inlineCallbacks def populate(self): yield populateCalendarsFrom(self.requirements, self.storeUnderTest()) @@ -865,6 +888,9 @@ txn = self._sqlCalendarStore.newTransaction() home = (yield txn.calendarHomeWithUID(self.uid)) self.assertEquals(home, None) + # Verify calendar1 was unshared to uid2 + home2 = (yield txn.calendarHomeWithUID(self.uid2)) + self.assertEquals((yield home2.sharedChildWithName(self.sharedName)), None) (yield txn.commit()) count, ignored = (yield purgeUID(self.storeUnderTest(), self.uid, self.directory, Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py =================================================================== --- CalendarServer/trunk/txdav/caldav/datastore/sql.py 2012-05-22 21:29:28 UTC (rev 9265) +++ CalendarServer/trunk/txdav/caldav/datastore/sql.py 2012-05-24 00:25:40 UTC (rev 9266) @@ -60,7 +60,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, _BIND_STATUS_INVITED, schema from twext.enterprise.dal.syntax import Select, Count, ColumnSyntax from twext.enterprise.dal.syntax import Insert from twext.enterprise.dal.syntax import Update @@ -131,6 +131,7 @@ cb = schema.CALENDAR_BIND cor = schema.CALENDAR_OBJECT_REVISIONS at = schema.ATTACHMENT + rp = schema.RESOURCE_PROPERTY # delete attachments corresponding to this home, also removing from disk rows = (yield Select( @@ -167,6 +168,11 @@ Where=ch.RESOURCE_ID == self._resourceID ).on(self._txn) + yield Delete( + From=rp, + Where=rp.RESOURCE_ID == self._resourceID + ).on(self._txn) + yield self._cacher.delete(str(self._ownerUID)) @@ -334,7 +340,52 @@ yield _requireCalendarWithType("VTODO", "tasks") + @classproperty + def _unacceptedSharesQuery(cls): #@NoSelf + cb = schema.CALENDAR_BIND + return Select([cb.CALENDAR_RESOURCE_NAME], + From=cb, + Where=(cb.CALENDAR_HOME_RESOURCE_ID == Parameter("homeResourceID")).And(cb.BIND_STATUS == _BIND_STATUS_INVITED)) + + @inlineCallbacks + def removeUnacceptedShares(self): + """ + Unbinds any collections that have been shared to this home but not yet + accepted. Associated invite entries are also removed. + """ + inv = schema.INVITE + cb = schema.CALENDAR_BIND + rows = yield self._unacceptedSharesQuery.on(self._txn, homeResourceID=self._resourceID) + for (resourceName,) in rows: + kwds = { "ResourceName" : resourceName } + yield Delete( + From=inv, + Where=( + inv.INVITE_UID == Parameter("ResourceName") + ), + ).on(self._txn, **kwds) + + yield Delete( + From=cb, + Where=( + cb.CALENDAR_RESOURCE_NAME == Parameter("ResourceName") + ), + ).on(self._txn, **kwds) + + + @inlineCallbacks + def removeInvites(self): + """ + Remove all remaining invite entries for this home. + """ + inv = schema.INVITE + kwds = { "HomeResourceID" : self._resourceID } + yield Delete( + From=inv, + Where=(inv.HOME_RESOURCE_ID == Parameter("HomeResourceID")) + ).on(self._txn, **kwds) + CalendarHome._register(ECALENDARTYPE) @@ -663,7 +714,14 @@ newParentID=newparent._resourceID, resourceID=child._resourceID ) - + + def unshare(self): + """ + Unshares a collection, regardless of which "direction" it was shared. + """ + return super(Calendar, self).unshare(ECALENDARTYPE) + + icalfbtype_to_indexfbtype = { "UNKNOWN" : 0, "FREE" : 1, Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py =================================================================== --- CalendarServer/trunk/txdav/caldav/datastore/test/common.py 2012-05-22 21:29:28 UTC (rev 9265) +++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py 2012-05-24 00:25:40 UTC (rev 9266) @@ -1043,8 +1043,6 @@ L{ICalendar.unshareWith} will remove a previously-shared calendar from another user's calendar home. """ - # XXX: ideally this would actually be using the shared calendar object - # from the shareee's home and just calling .unshare() on it. yield self.test_shareWith() if commit: yield self.commit() @@ -1058,8 +1056,49 @@ shares = yield other.retrieveOldShares().allRecords() self.assertEqual(len(shares), 0) + @inlineCallbacks + def test_unshareSharerSide(self, commit=False): + """ + Verify the coll.unshare( ) method works when called from the + sharer's copy + """ + yield self.test_shareWith() + if commit: + yield self.commit() + cal = yield self.calendarUnderTest() + other = yield self.homeUnderTest(name=OTHER_HOME_UID) + otherCal = yield other.sharedChildWithName(self.sharedName) + self.assertNotEqual(otherCal, None) + yield cal.unshare() + otherCal = yield other.sharedChildWithName(self.sharedName) + self.assertEqual(otherCal, None) + invites = yield cal.retrieveOldInvites().allRecords() + self.assertEqual(len(invites), 0) + shares = yield other.retrieveOldShares().allRecords() + self.assertEqual(len(shares), 0) @inlineCallbacks + def test_unshareShareeSide(self, commit=False): + """ + Verify the coll.unshare( ) method works when called from the + sharee's copy + """ + yield self.test_shareWith() + if commit: + yield self.commit() + cal = yield self.calendarUnderTest() + other = yield self.homeUnderTest(name=OTHER_HOME_UID) + otherCal = yield other.sharedChildWithName(self.sharedName) + self.assertNotEqual(otherCal, None) + yield otherCal.unshare() + otherCal = yield other.sharedChildWithName(self.sharedName) + self.assertEqual(otherCal, None) + invites = yield cal.retrieveOldInvites().allRecords() + self.assertEqual(len(invites), 0) + shares = yield other.retrieveOldShares().allRecords() + self.assertEqual(len(shares), 0) + + @inlineCallbacks def test_unshareWithInDifferentTransaction(self): """ L{ICalendar.unshareWith} will remove a previously-shared calendar from Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py =================================================================== --- CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py 2012-05-22 21:29:28 UTC (rev 9265) +++ CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py 2012-05-24 00:25:40 UTC (rev 9266) @@ -477,6 +477,8 @@ test_unshareWith = test_shareWith test_unshareWithInDifferentTransaction = test_shareWith test_asShared = test_shareWith + test_unshareSharerSide = test_shareWith + test_unshareShareeSide = test_shareWith def test_init(self): Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py =================================================================== --- CalendarServer/trunk/txdav/carddav/datastore/sql.py 2012-05-22 21:29:28 UTC (rev 9265) +++ CalendarServer/trunk/txdav/carddav/datastore/sql.py 2012-05-24 00:25:40 UTC (rev 9266) @@ -107,6 +107,7 @@ ah = schema.ADDRESSBOOK_HOME ab = schema.ADDRESSBOOK_BIND aor = schema.ADDRESSBOOK_OBJECT_REVISIONS + rp = schema.RESOURCE_PROPERTY yield Delete( From=ab, @@ -123,6 +124,11 @@ Where=ah.RESOURCE_ID == self._resourceID ).on(self._txn) + yield Delete( + From=rp, + Where=rp.RESOURCE_ID == self._resourceID + ).on(self._txn) + yield self._cacher.delete(str(self._ownerUID)) @@ -201,7 +207,13 @@ return MimeType.fromString("text/vcard; charset=utf-8") + def unshare(self): + """ + Unshares a collection, regardless of which "direction" it was shared. + """ + return super(AddressBook, self).unshare(EADDRESSBOOKTYPE) + class AddressBookObject(CommonObjectResource): implements(IAddressBookObject) Modified: CalendarServer/trunk/txdav/common/datastore/sql.py =================================================================== --- CalendarServer/trunk/txdav/common/datastore/sql.py 2012-05-22 21:29:28 UTC (rev 9265) +++ CalendarServer/trunk/txdav/common/datastore/sql.py 2012-05-24 00:25:40 UTC (rev 9266) @@ -437,7 +437,6 @@ """ return NotificationCollection.notificationsWithUID(self, uid) - @classproperty def _insertAPNSubscriptionQuery(cls): #@NoSelf apn = schema.APN_SUBSCRIPTIONS @@ -2106,6 +2105,28 @@ return self._bindMode + @inlineCallbacks + def unshare(self, homeType): + """ + Unshares a collection, regardless of which "direction" it was shared. + + @param homeType: a valid store type (ECALENDARTYPE or EADDRESSBOOKTYPE) + """ + mode = self.shareMode() + if mode == _BIND_MODE_OWN: + # This collection may be shared to others + for sharedToHome in [x.viewerHome() for x in (yield self.asShared())]: + (yield self.unshareWith(sharedToHome)) + else: + # This collection is shared to me + sharerHomeID = (yield self.sharerHomeID()) + sharerHome = (yield self._txn.homeWithResourceID(homeType, + sharerHomeID)) + sharerCollection = (yield sharerHome.childWithID(self._resourceID)) + (yield sharerCollection.unshareWith(self._home)) + + + @classproperty def _bindEntriesFor(cls): bind = cls._bindSchema @@ -3641,7 +3662,31 @@ self.notifyChanged() + @inlineCallbacks + def remove(self): + """ + Remove DB rows corresponding to this notification home. + """ + # Delete NOTIFICATION rows + no = schema.NOTIFICATION + kwds = { "ResourceID" : self._resourceID } + yield Delete( + From=no, + Where=( + no.NOTIFICATION_HOME_RESOURCE_ID == Parameter("ResourceID") + ), + ).on(self._txn, **kwds) + # Delete NOTIFICATION_HOME (will cascade to NOTIFICATION_OBJECT_REVISIONS) + nh = schema.NOTIFICATION_HOME + yield Delete( + From=nh, + Where=( + nh.RESOURCE_ID == Parameter("ResourceID") + ), + ).on(self._txn, **kwds) + + class NotificationObject(LoggingMixIn, FancyEqMixin): implements(INotificationObject)