[CalendarServer-changes] [6847] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Feb 1 18:05:44 PST 2011


Revision: 6847
          http://trac.macosforge.org/projects/calendarserver/changeset/6847
Author:   sagen at apple.com
Date:     2011-02-01 18:05:44 -0800 (Tue, 01 Feb 2011)
Log Message:
-----------
Adds calendarserver_purge_attachments

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tools/purge.py
    CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py
    CalendarServer/trunk/doc/calendarserver_purge_events.8
    CalendarServer/trunk/setup.py
    CalendarServer/trunk/support/Makefile.Apple
    CalendarServer/trunk/txdav/common/datastore/sql.py

Added Paths:
-----------
    CalendarServer/trunk/bin/calendarserver_purge_attachments
    CalendarServer/trunk/doc/calendarserver_purge_attachments.8

Added: CalendarServer/trunk/bin/calendarserver_purge_attachments
===================================================================
--- CalendarServer/trunk/bin/calendarserver_purge_attachments	                        (rev 0)
+++ CalendarServer/trunk/bin/calendarserver_purge_attachments	2011-02-02 02:05:44 UTC (rev 6847)
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        path = path.rstrip("\n")
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+        sys.argv[1:1] = ["-f", join(home, "conf", "caldavd-dev.plist")]
+
+    from calendarserver.tools.purge import main_purge_orphaned_attachments
+    main_purge_orphaned_attachments()


Property changes on: CalendarServer/trunk/bin/calendarserver_purge_attachments
___________________________________________________________________
Added: svn:executable
   + *

Modified: CalendarServer/trunk/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/purge.py	2011-02-02 01:09:18 UTC (rev 6846)
+++ CalendarServer/trunk/calendarserver/tools/purge.py	2011-02-02 02:05:44 UTC (rev 6847)
@@ -64,6 +64,27 @@
     else:
         sys.exit(0)
 
+def usage_purge_orphaned_attachments(e=None):
+
+    name = os.path.basename(sys.argv[0])
+    print "usage: %s [options]" % (name,)
+    print ""
+    print "  Remove orphaned attachments from the calendar server"
+    print ""
+    print "options:"
+    print "  -b --batch <number>: number of attachments to remove in each transaction (default=100)"
+    print "  -f --config <path>: Specify caldavd.plist configuration path"
+    print "  -h --help: print this help and exit"
+    print "  -n --dry-run: only calculate how many attachments to purge"
+    print "  -v --verbose: print progress information"
+    print ""
+
+    if e:
+        sys.stderr.write("%s\n" % (e,))
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
 def usage_purge_principal(e=None):
 
     name = os.path.basename(sys.argv[0])
@@ -111,6 +132,28 @@
             reactor.stop()
 
 
+class PurgeOrphanedAttachmentsService(Service):
+
+    batchSize = None
+    dryrun = False
+    verbose = False
+
+    def __init__(self, store):
+        self._store = store
+
+    @inlineCallbacks
+    def startService(self):
+        try:
+            (yield purgeOrphanedAttachments(self._store, self.batchSize,
+                verbose=self.verbose, dryrun=self.dryrun))
+        except Exception, e:
+            print "Error:", e
+            raise
+
+        finally:
+            reactor.stop()
+
+
 class PurgePrincipalService(Service):
 
     guids = None
@@ -234,6 +277,68 @@
     )
 
 
+def main_purge_orphaned_attachments():
+
+    try:
+        (optargs, args) = getopt(
+            sys.argv[1:], "d:b:f:hnv", [
+                "batch=",
+                "dry-run",
+                "config=",
+                "help",
+                "verbose",
+            ],
+        )
+    except GetoptError, e:
+        usage_purge_orphaned_attachments(e)
+
+    #
+    # Get configuration
+    #
+    configFileName = None
+    batchSize = 100
+    dryrun = False
+    verbose = False
+
+    for opt, arg in optargs:
+        if opt in ("-h", "--help"):
+            usage_purge_orphaned_attachments()
+
+        elif opt in ("-b", "--batch"):
+            try:
+                batchSize = int(arg)
+            except ValueError, e:
+                print "Invalid value for --batch: %s" % (arg,)
+                usage_purge_orphaned_attachments(e)
+
+        elif opt in ("-v", "--verbose"):
+            verbose = True
+
+        elif opt in ("-n", "--dry-run"):
+            dryrun = True
+
+        elif opt in ("-f", "--config"):
+            configFileName = arg
+
+        else:
+            raise NotImplementedError(opt)
+
+    if args:
+        usage_purge_orphaned_attachments("Too many arguments: %s" % (args,))
+
+    if dryrun:
+        verbose = True
+
+    PurgeOrphanedAttachmentsService.batchSize = batchSize
+    PurgeOrphanedAttachmentsService.dryrun = dryrun
+    PurgeOrphanedAttachmentsService.verbose = verbose
+
+    shared_main(
+        configFileName,
+        PurgeOrphanedAttachmentsService,
+    )
+
+
 def main_purge_principals():
 
     try:
@@ -342,73 +447,54 @@
     returnValue(totalRemoved)
 
 
-    """
 
-    oldEvents = (yield store.eventsOlderThan(date))
+ at inlineCallbacks
+def purgeOrphanedAttachments(store, batchSize, verbose=False, dryrun=False):
 
-    request = FakeRequest(root, None, None)
-    request.checkedSACL = True
-    eventCount = 0
-    prevHomeName = None
+    if dryrun:
+        if verbose:
+            print "(Dry run) Searching for orphaned attachments..."
+        txn = store.newTransaction(label="Find orphaned attachments")
+        orphans = (yield txn.orphanedAttachments())
+        orphanCount = len(orphans)
+        if verbose:
+            if orphanCount == 0:
+                print "No orphaned attachments"
+            elif orphanCount == 1:
+                print "1 orphaned attachment"
+            else:
+                print "%d orphaned attachments" % (eventCount,)
+        returnValue(orphanCount)
 
-    for homeName, calendarName, eventName, maxDate in oldEvents:
-        if homeName != prevHomeName:
+    if verbose:
+        print "Removing orphaned attachments..."
 
-            # Retrieve the calendar home
-            record = directory.recordWithGUID(homeName)
-            if record is None:
-                # The user has already been removed from the directory service.  We
-                # need to fashion a temporary, fake record
+    numOrphansRemoved = -1
+    totalRemoved = 0
+    while numOrphansRemoved:
+        txn = store.newTransaction(label="Remove orphaned attachments")
+        numOrphansRemoved = (yield txn.removeOrphanedAttachments(batchSize=batchSize))
+        (yield txn.commit())
+        if numOrphansRemoved:
+            totalRemoved += numOrphansRemoved
+            if verbose:
+                print "%d," % (totalRemoved,),
 
-                # FIXME: probaby want a more elegant way to accomplish this,
-                # since it requires the aggregate directory to examine these first:
-                record = DirectoryRecord(directory, "users", homeName, shortNames=(homeName,),
-                    enabledForCalendaring=True)
-                record.enabled = True
-                directory._tmpRecords["shortNames"][homeName] = record
-                directory._tmpRecords["guids"][homeName] = record
+    if verbose:
+        print
+        if totalRemoved == 0:
+            print "No orphaned attachments were removed"
+        elif totalRemoved == 1:
+            print "1 orphaned attachment was removed in total"
+        else:
+            print "%d orphaned attachments were removed in total" % (totalRemoved,)
 
-            principalCollection = directory.principalCollection
-            principal = principalCollection.principalForRecord(record)
-            calendarHome = (yield principal.calendarHome(request))
-            prevHomeName = homeName
+    returnValue(totalRemoved)
 
-        eventCount += 1
-        if verbose:
-            print "%s %s/%s/%s ends: %s" % (record.shortNames[0], homeName,
-                calendarName, eventName, maxDate)
-        if not dryrun:
-            calendar = (yield calendarHome.getChild(calendarName))
-            event = (yield calendar.getChild(eventName))
-            uri = "/calendars/__uids__/%s/%s/%s" % (
-                homeName,
-                calendarName,
-                eventName
-            )
-            request.authnUser = request.authzUser = davxml.Principal(
-                davxml.HRef.fromString("/principals/__uids__/%s/" % (homeName,))
-            )
-            request.path = uri
-            request._rememberResource(event, uri)
-            if verbose:
-                print "Deleting %s/%s/%s" % (homeName, calendarName, eventName)
-            result = (yield event.storeRemove(request, True, uri))
-            if result != NO_CONTENT:
-                print "Error deleting %s/%s/%s: %s" % (homeName, calendarName,
-                    eventName, result)
 
-    if eventCount and not dryrun:
-        if verbose:
-            print "Committing changes"
-        txn = request._newStoreTransaction
-        (yield txn.commit())
 
-    returnValue(eventCount)
 
-    """
 
-
-
 @inlineCallbacks
 def purgeGUIDs(directory, root, guids, verbose=False, dryrun=False):
     total = 0

Modified: CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py	2011-02-02 01:09:18 UTC (rev 6846)
+++ CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py	2011-02-02 02:05:44 UTC (rev 6847)
@@ -19,12 +19,13 @@
 """
 
 from twisted.trial import unittest
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twext.web2.http_headers import MimeType
 
 from txdav.common.datastore.test.util import buildStore, populateCalendarsFrom, CommonCommonTests
 
 from calendarserver.tap.util import getRootResource
-from calendarserver.tools.purge import purgeOldEvents, purgeGUID
+from calendarserver.tools.purge import purgeOldEvents, purgeGUID, purgeOrphanedAttachments
 from twistedcaldav.config import config
 from vobject.icalendar import utc
 
@@ -81,6 +82,56 @@
 END:VCALENDAR
 """.replace("\n", "\r\n")
 
+OLD_ATTACHMENT_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:STANDARD
+TZOFFSETFROM:-0700
+RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYMONTH=10;BYDAY=-1SU
+DTSTART:19621028T020000
+TZNAME:PST
+TZOFFSETTO:-0800
+END:STANDARD
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0800
+RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYMONTH=4;BYDAY=1SU
+DTSTART:19870405T020000
+TZNAME:PDT
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0800
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:PDT
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0700
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:PST
+TZOFFSETTO:-0800
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:57A5D1F6-9A57-4F74-9520-25C617F54B88
+TRANSP:OPAQUE
+SUMMARY:Ancient event with attachment
+DTSTART;TZID=US/Pacific:20000308T111500
+DTEND;TZID=US/Pacific:20000308T151500
+DTSTAMP:20100303T181220Z
+X-APPLE-DROPBOX:/calendars/__uids__/user01/dropbox/57A5D1F6-9A57-4F74-95
+ 20-25C617F54B88.dropbox
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
 ENDLESS_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//Apple Inc.//iCal 4.0.1//EN
@@ -260,6 +311,7 @@
             "calendar1" : {
                 "old.ics" : OLD_ICS,
                 "endless.ics" : ENDLESS_ICS,
+                "oldattachment.ics" : OLD_ATTACHMENT_ICS,
             }
         },
         "home2" : {
@@ -303,6 +355,7 @@
         self.assertEquals(results,
             [
                 ['home1', 'calendar1', 'old.ics', '2000-03-07 23:15:00'],
+                ['home1', 'calendar1', 'oldattachment.ics', '2000-03-08 23:15:00'],
                 ['home2', 'calendar3', 'repeating_awhile.ics', '2002-03-09 23:15:00'],
                 ['home2', 'calendar2', 'recent.ics', '2010-03-04 22:15:00'],
             ]
@@ -327,6 +380,7 @@
         results = (yield txn.eventsOlderThan(cutoff))
         self.assertEquals(results,
             [
+                ['home1', 'calendar1', 'oldattachment.ics', '2000-03-08 23:15:00'],
                 ['home2', 'calendar3', 'repeating_awhile.ics', '2002-03-09 23:15:00'],
                 ['home2', 'calendar2', 'recent.ics', '2010-03-04 22:15:00'],
             ]
@@ -334,7 +388,7 @@
 
         # Remove remaining oldest events
         count = (yield txn.removeOldEvents(cutoff))
-        self.assertEquals(count, 2)
+        self.assertEquals(count, 3)
         results = (yield txn.eventsOlderThan(cutoff))
         self.assertEquals(results, [ ])
 
@@ -342,7 +396,64 @@
         count = (yield txn.removeOldEvents(cutoff))
         self.assertEquals(count, 0)
 
+
     @inlineCallbacks
+    def _addAttachment(self):
+
+        txn = self._sqlCalendarStore.newTransaction()
+
+        # Create an event with an attachment
+        home = (yield txn.calendarHomeWithUID("home1"))
+        calendar = (yield home.calendarWithName("calendar1"))
+        event = (yield calendar.calendarObjectWithName("oldattachment.ics"))
+        attachment = (yield event.createAttachmentWithName("oldattachment.ics"))
+        t = attachment.store(MimeType("text", "x-fixture"))
+        t.write("old attachment")
+        t.write(" text")
+        (yield t.loseConnection())
+        (yield txn.commit())
+
+        returnValue(attachment)
+
+    @inlineCallbacks
+    def test_removeOrphanedAttachments(self):
+        attachment = (yield self._addAttachment())
+        txn = self._sqlCalendarStore.newTransaction()
+        attachmentPath = attachment._path.path
+        self.assertTrue(os.path.exists(attachmentPath))
+
+        orphans = (yield txn.orphanedAttachments())
+        self.assertEquals(len(orphans), 0)
+
+        count = (yield txn.removeOrphanedAttachments(batchSize=100))
+        self.assertEquals(count, 0)
+
+        # File still exists
+        self.assertTrue(os.path.exists(attachmentPath))
+
+        # Delete all old events (including the event containing the attachment)
+        cutoff = datetime.datetime(2010, 4, 1)
+        count = (yield txn.removeOldEvents(cutoff))
+
+        # Just look for orphaned attachments but don't delete
+        orphans = (yield txn.orphanedAttachments())
+        self.assertEquals(len(orphans), 1)
+
+        # 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):
         self.patch(config.DirectoryService.params, "xmlFile",
             os.path.join(
@@ -362,12 +473,12 @@
         total = (yield purgeOldEvents(self._sqlCalendarStore, directory,
             rootResource, datetime.datetime(2010, 4, 1), 2, dryrun=True,
             verbose=False))
-        self.assertEquals(total, 3)
+        self.assertEquals(total, 4)
 
         # Actually remove
         total = (yield purgeOldEvents(self._sqlCalendarStore, directory,
             rootResource, datetime.datetime(2010, 4, 1), 2, verbose=False))
-        self.assertEquals(total, 3)
+        self.assertEquals(total, 4)
 
         # There should be no more left
         total = (yield purgeOldEvents(self._sqlCalendarStore, directory,
@@ -393,3 +504,43 @@
             verbose=False, proxies=False,
             when=datetime.datetime(2010, 4, 1, 12, 0, 0, 0, utc)))
         self.assertEquals(total, 1)
+
+
+    @inlineCallbacks
+    def test_purgeOrphanedAttachments(self):
+
+        (yield self._addAttachment())
+
+        self.patch(config.DirectoryService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "purge", "accounts.xml"
+            )
+        )
+        self.patch(config.ResourceService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "purge", "resources.xml"
+            )
+        )
+        self.patch(config.Memcached.Pools.Default, "ClientEnabled", False)
+        rootResource = getRootResource(config, self._sqlCalendarStore)
+        directory = rootResource.getDirectory()
+
+        # Remove old events first
+        total = (yield purgeOldEvents(self._sqlCalendarStore, directory,
+            rootResource, datetime.datetime(2010, 4, 1), 2, verbose=False))
+        self.assertEquals(total, 4)
+
+        # Dry run
+        total = (yield purgeOrphanedAttachments(self._sqlCalendarStore, 2,
+            dryrun=True, verbose=False))
+        self.assertEquals(total, 1)
+
+        # Actually remove
+        total = (yield purgeOrphanedAttachments(self._sqlCalendarStore, 2,
+            dryrun=False, verbose=False))
+        self.assertEquals(total, 1)
+
+        # There should be no more left
+        total = (yield purgeOrphanedAttachments(self._sqlCalendarStore, 2,
+            dryrun=False, verbose=False))
+        self.assertEquals(total, 0)

Added: CalendarServer/trunk/doc/calendarserver_purge_attachments.8
===================================================================
--- CalendarServer/trunk/doc/calendarserver_purge_attachments.8	                        (rev 0)
+++ CalendarServer/trunk/doc/calendarserver_purge_attachments.8	2011-02-02 02:05:44 UTC (rev 6847)
@@ -0,0 +1,54 @@
+.\"
+.\" Copyright (c) 2006-2011 Apple Inc. All rights reserved.
+.\"
+.\" Licensed under the Apache License, Version 2.0 (the "License");
+.\" you may not use this file except in compliance with the License.
+.\" You may obtain a copy of the License at
+.\"
+.\"     http://www.apache.org/licenses/LICENSE-2.0
+.\"
+.\" Unless required by applicable law or agreed to in writing, software
+.\" distributed under the License is distributed on an "AS IS" BASIS,
+.\" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.\" See the License for the specific language governing permissions and
+.\" limitations under the License.
+.\"
+.\" The following requests are required for all man pages.
+.Dd June 17, 2009
+.Dt CALENDARSERVER_PURGE_ATTACHMENTS 8
+.Os
+.Sh NAME
+.Nm calendarserver_purge_attachments
+.Nd Darwin Calendar Server orphaned attachments clean-up utility
+.Sh SYNOPSIS
+.Nm
+.Op Fl -config Ar file
+.Op Fl -dry-run
+.Op Fl -verbose
+.Op Fl -help
+.Sh DESCRIPTION
+.Nm
+is a tool for removing attachments that are no longer referenced by any calendar events.
+.Pp
+.Nm
+should be run as a user with the same priviledges as the Calendar
+Server itself, as it needs to read and write data that belongs to the
+server.
+.Sh OPTIONS
+.Bl -tag -width flag
+.It Fl h, -help
+Display usage information
+.It Fl f, -config Ar FILE
+Use the Calendar Server configuration specified in the given file.  Defaults to /etc/caldavd/caldavd.plist.
+.It Fl n, -dry-run
+Calculate and display how many orphaned attachments would be removed, but don't actually remove them.
+.It Fl v, -verbose
+Print progress information.
+.El
+.Sh FILES
+.Bl -tag -width flag
+.It /etc/caldavd/caldavd.plist
+The Calendar Server configuration file.
+.El
+.Sh SEE ALSO
+.Xr caldavd 8

Modified: CalendarServer/trunk/doc/calendarserver_purge_events.8
===================================================================
--- CalendarServer/trunk/doc/calendarserver_purge_events.8	2011-02-02 01:09:18 UTC (rev 6846)
+++ CalendarServer/trunk/doc/calendarserver_purge_events.8	2011-02-02 02:05:44 UTC (rev 6847)
@@ -44,7 +44,7 @@
 .It Fl n, -dry-run
 Calculate and display how many events would be removed, but don't actually remove them.  
 .It Fl v, -verbose
-Print progress information including purged events per calendar home.  
+Print progress information.
 .It Fl d, -days Ar NUMBER
 Specify how many days in the past to retain.  Defaults to 365 days.
 .El

Modified: CalendarServer/trunk/setup.py
===================================================================
--- CalendarServer/trunk/setup.py	2011-02-02 01:09:18 UTC (rev 6846)
+++ CalendarServer/trunk/setup.py	2011-02-02 02:05:44 UTC (rev 6847)
@@ -121,6 +121,7 @@
                              "bin/calendarserver_manage_principals",
                              "bin/calendarserver_command_gateway",
                              "bin/calendarserver_purge_events",
+                             "bin/calendarserver_purge_attachments",
                              "bin/calendarserver_purge_principals",
                              "bin/calendarserver_migrate_resources",
                            ],

Modified: CalendarServer/trunk/support/Makefile.Apple
===================================================================
--- CalendarServer/trunk/support/Makefile.Apple	2011-02-02 01:09:18 UTC (rev 6846)
+++ CalendarServer/trunk/support/Makefile.Apple	2011-02-02 02:05:44 UTC (rev 6847)
@@ -88,6 +88,7 @@
 	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_migrate_resources.8" "$(DSTROOT)$(MANDIR)/man8"
 	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_command_gateway.8"   "$(DSTROOT)$(MANDIR)/man8"
 	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_purge_events.8"   "$(DSTROOT)$(MANDIR)/man8"
+	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_purge_attachments.8"   "$(DSTROOT)$(MANDIR)/man8"
 	$(_v) $(INSTALL_FILE) "$(Sources)/doc/calendarserver_purge_principals.8"   "$(DSTROOT)$(MANDIR)/man8"
 	$(_v) gzip -9 -f "$(DSTROOT)$(MANDIR)/man8/"*.[0-9]
 	@echo "Installing launchd config..."

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2011-02-02 01:09:18 UTC (rev 6846)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2011-02-02 02:05:44 UTC (rev 6847)
@@ -258,6 +258,9 @@
         """
         Return up to the oldest batchSize events which exist completely earlier
         than "cutoff" (datetime)
+
+        Returns a deferred to a list of (uid, calendarName, eventName, maxDate)
+        tuples.
         """
 
         query = """
@@ -308,8 +311,51 @@
             count += 1
         returnValue(count)
 
+    def orphanedAttachments(self, batchSize=None):
+        """
+        Find attachments no longer referenced by any events.
 
+        Returns a deferred to a list of (dropbox_id, path) tuples.
+        """
 
+        query = """
+            select
+                at.DROPBOX_ID,
+                at.PATH
+            from
+                ATTACHMENT at
+            left outer join
+                CALENDAR_OBJECT co
+            on
+                at.DROPBOX_ID = co.DROPBOX_ID
+            where
+                co.DROPBOX_ID is null
+            """
+        args = []
+        if batchSize is not None:
+            query += "limit %s"
+            args.append(batchSize)
+
+        return self.execSQL(query, args)
+
+    @inlineCallbacks
+    def removeOrphanedAttachments(self, batchSize=None):
+        """
+        Remove attachments that no longer have any references to them
+        """
+
+        # TODO: see if there is a better way to import Attachment
+        from txdav.caldav.datastore.sql import Attachment
+
+        results = (yield self.orphanedAttachments(batchSize=batchSize))
+        count = 0
+        for dropboxID, path in results:
+            attachment = Attachment(self, dropboxID, path)
+            (yield attachment.remove( ))
+            count += 1
+        returnValue(count)
+
+
 class CommonHome(LoggingMixIn):
 
     # All these need to be initialized by derived classes for each store type
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110201/db76ad1b/attachment-0001.html>


More information about the calendarserver-changes mailing list