<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[14970] CalendarServer/trunk/calendarserver/tools</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/14970">14970</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2015-07-15 10:25:10 -0700 (Wed, 15 Jul 2015)</dd>
</dl>
<h3>Log Message</h3>
<pre>Merge old event purge changes from -5.4-dev branch.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertoolsgatewaypy">CalendarServer/trunk/calendarserver/tools/gateway.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertoolspurgepy">CalendarServer/trunk/calendarserver/tools/purge.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertoolstesttest_purge_old_eventspy">CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertoolsgatewaypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/gateway.py (14969 => 14970)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/gateway.py        2015-07-15 16:10:33 UTC (rev 14969)
+++ CalendarServer/trunk/calendarserver/tools/gateway.py        2015-07-15 17:25:10 UTC (rev 14970)
</span><span class="lines">@@ -577,7 +577,7 @@
</span><span class="cx"> cutoff = DateTime.getToday()
</span><span class="cx"> cutoff.setDateOnly(False)
</span><span class="cx"> cutoff.offsetDay(-retainDays)
</span><del>- eventCount = (yield PurgeOldEventsService.purgeOldEvents(self.store, cutoff, DEFAULT_BATCH_SIZE))
</del><ins>+ eventCount = (yield PurgeOldEventsService.purgeOldEvents(self.store, None, cutoff, DEFAULT_BATCH_SIZE))
</ins><span class="cx"> self.respond(command, {'EventsRemoved': eventCount, "RetainDays": retainDays})
</span><span class="cx">
</span><span class="cx">
</span></span></pre></div>
<a id="CalendarServertrunkcalendarservertoolspurgepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/purge.py (14969 => 14970)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/purge.py        2015-07-15 16:10:33 UTC (rev 14969)
+++ CalendarServer/trunk/calendarserver/tools/purge.py        2015-07-15 17:25:10 UTC (rev 14970)
</span><span class="lines">@@ -29,7 +29,7 @@
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><span class="cx">
</span><span class="cx"> from twext.enterprise.dal.record import fromTable
</span><del>-from twext.enterprise.dal.syntax import Delete, Select, Union
</del><ins>+from twext.enterprise.dal.syntax import Delete, Select, Union, Parameter, Max
</ins><span class="cx"> from twext.enterprise.jobqueue import WorkItem, RegeneratingWorkItem
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx">
</span><span class="lines">@@ -37,9 +37,11 @@
</span><span class="cx">
</span><span class="cx"> from twistedcaldav import caldavxml
</span><span class="cx"> from twistedcaldav.config import config
</span><ins>+from twistedcaldav.dateops import parseSQLDateToPyCalendar, pyCalendarToSQLTimestamp
+from twistedcaldav.ical import Component, InvalidICalendarDataError
</ins><span class="cx">
</span><span class="cx"> from txdav.caldav.datastore.query.filter import Filter
</span><del>-from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_NORMAL
</del><ins>+from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_NORMAL, _BIND_MODE_OWN
</ins><span class="cx">
</span><span class="cx"> log = Logger()
</span><span class="cx">
</span><span class="lines">@@ -277,10 +279,11 @@
</span><span class="cx">
</span><span class="cx"> class PurgeOldEventsService(WorkerService):
</span><span class="cx">
</span><ins>+ uuid = None
</ins><span class="cx"> cutoff = None
</span><span class="cx"> batchSize = None
</span><span class="cx"> dryrun = False
</span><del>- verbose = False
</del><ins>+ debug = False
</ins><span class="cx">
</span><span class="cx"> @classmethod
</span><span class="cx"> def usage(cls, e=None):
</span><span class="lines">@@ -293,8 +296,8 @@
</span><span class="cx"> print("options:")
</span><span class="cx"> print(" -h --help: print this help and exit")
</span><span class="cx"> print(" -f --config <path>: Specify caldavd.plist configuration path")
</span><ins>+ print(" -u --uuid <uuid>: Only process this user(s) [REQUIRED]")
</ins><span class="cx"> print(" -d --days <number>: specify how many days in the past to retain (default=%d)" % (DEFAULT_RETAIN_DAYS,))
</span><del>- # print(" -b --batch <number>: number of events to remove in each transaction (default=%d)" % (DEFAULT_BATCH_SIZE,))
</del><span class="cx"> print(" -n --dry-run: calculate how many events to purge, but do not purge data")
</span><span class="cx"> print(" -v --verbose: print progress information")
</span><span class="cx"> print(" -D --debug: debug logging")
</span><span class="lines">@@ -312,11 +315,12 @@
</span><span class="cx">
</span><span class="cx"> try:
</span><span class="cx"> (optargs, args) = getopt(
</span><del>- sys.argv[1:], "Dd:b:f:hnv", [
</del><ins>+ sys.argv[1:], "Dd:b:f:hnu:v", [
</ins><span class="cx"> "days=",
</span><span class="cx"> "batch=",
</span><span class="cx"> "dry-run",
</span><span class="cx"> "config=",
</span><ins>+ "uuid=",
</ins><span class="cx"> "help",
</span><span class="cx"> "verbose",
</span><span class="cx"> "debug",
</span><span class="lines">@@ -329,6 +333,7 @@
</span><span class="cx"> # Get configuration
</span><span class="cx"> #
</span><span class="cx"> configFileName = None
</span><ins>+ uuid = None
</ins><span class="cx"> days = DEFAULT_RETAIN_DAYS
</span><span class="cx"> batchSize = DEFAULT_BATCH_SIZE
</span><span class="cx"> dryrun = False
</span><span class="lines">@@ -365,12 +370,19 @@
</span><span class="cx"> elif opt in ("-f", "--config"):
</span><span class="cx"> configFileName = arg
</span><span class="cx">
</span><ins>+ elif opt in ("-u", "--uuid"):
+ uuid = arg
+
</ins><span class="cx"> else:
</span><span class="cx"> raise NotImplementedError(opt)
</span><span class="cx">
</span><span class="cx"> if args:
</span><span class="cx"> cls.usage("Too many arguments: %s" % (args,))
</span><span class="cx">
</span><ins>+ if uuid is None:
+ cls.usage("uuid must be specified")
+ cls.uuid = uuid
+
</ins><span class="cx"> if dryrun:
</span><span class="cx"> verbose = True
</span><span class="cx">
</span><span class="lines">@@ -380,68 +392,321 @@
</span><span class="cx"> cls.cutoff = cutoff
</span><span class="cx"> cls.batchSize = batchSize
</span><span class="cx"> cls.dryrun = dryrun
</span><del>- cls.verbose = verbose
</del><ins>+ cls.debug = debug
</ins><span class="cx">
</span><span class="cx"> utilityMain(
</span><span class="cx"> configFileName,
</span><span class="cx"> cls,
</span><del>- verbose=debug,
</del><ins>+ verbose=verbose,
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @classmethod
</span><span class="cx"> @inlineCallbacks
</span><del>- def purgeOldEvents(cls, store, cutoff, batchSize, verbose=False, dryrun=False):
</del><ins>+ def purgeOldEvents(cls, store, uuid, cutoff, batchSize, debug=False, dryrun=False):
</ins><span class="cx">
</span><span class="cx"> service = cls(store)
</span><ins>+ service.uuid = uuid
</ins><span class="cx"> service.cutoff = cutoff
</span><span class="cx"> service.batchSize = batchSize
</span><span class="cx"> service.dryrun = dryrun
</span><del>- service.verbose = verbose
</del><ins>+ service.debug = debug
</ins><span class="cx"> result = yield service.doWork()
</span><span class="cx"> returnValue(result)
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><ins>+ def getMatchingHomeUIDs(self):
+ """
+ Find all the calendar homes that match the uuid cli argument.
+ """
+ log.debug("Searching for calendar homes matching: '{}'".format(self.uuid))
+ txn = self.store.newTransaction(label="Find matching homes")
+ ch = schema.CALENDAR_HOME
+ if self.uuid:
+ kwds = {"uuid": self.uuid}
+ rows = (yield Select(
+ [ch.RESOURCE_ID, ch.OWNER_UID, ],
+ From=ch,
+ Where=(ch.OWNER_UID.StartsWith(Parameter("uuid"))),
+ ).on(txn, **kwds))
+ else:
+ rows = (yield Select(
+ [ch.RESOURCE_ID, ch.OWNER_UID, ],
+ From=ch,
+ ).on(txn))
+
+ yield txn.commit()
+ log.debug(" Found {} calendar homes".format(len(rows)))
+ returnValue(sorted(rows, key=lambda x: x[1]))
+
+
+ @inlineCallbacks
+ def getMatchingCalendarIDs(self, home_id, owner_uid):
+ """
+ Find all the owned calendars for the specified calendar home.
+
+ @param home_id: resource-id of calendar home to check
+ @type home_id: L{int}
+ @param owner_uid: owner UUID of home to check
+ @type owner_uid: L{str}
+ """
+ log.debug("Checking calendar home: {} '{}'".format(home_id, owner_uid))
+ txn = self.store.newTransaction(label="Find matching calendars")
+ cb = schema.CALENDAR_BIND
+ kwds = {"home_id": home_id}
+ rows = (yield Select(
+ [cb.CALENDAR_RESOURCE_ID, cb.CALENDAR_RESOURCE_NAME, ],
+ From=cb,
+ Where=(cb.CALENDAR_HOME_RESOURCE_ID == Parameter("home_id")).And(
+ cb.BIND_MODE == _BIND_MODE_OWN
+ ),
+ ).on(txn, **kwds))
+ yield txn.commit()
+ log.debug(" Found {} calendars".format(len(rows)))
+ returnValue(rows)
+
+
+ PurgeEvent = collections.namedtuple("PurgeEvent", ("home", "calendar", "resource",))
+
+ @inlineCallbacks
+ def getResourceIDsToPurge(self, home_id, calendar_id, calendar_name):
+ """
+ For the given calendar find which calendar objects are older than the cut-off and return the
+ resource-ids of those.
+
+ @param home_id: resource-id of calendar home
+ @type home_id: L{int}
+ @param calendar_id: resource-id of the calendar to check
+ @type calendar_id: L{int}
+ @param calendar_name: name of the calendar to check
+ @type calendar_name: L{str}
+ """
+
+ log.debug(" Checking calendar: {} '{}'".format(calendar_id, calendar_name))
+ purge = set()
+ txn = self.store.newTransaction(label="Find matching resources")
+ co = schema.CALENDAR_OBJECT
+ tr = schema.TIME_RANGE
+ kwds = {"calendar_id": calendar_id}
+ rows = (yield Select(
+ [co.RESOURCE_ID, co.RECURRANCE_MAX, co.RECURRANCE_MIN, Max(tr.END_DATE)],
+ From=co.join(tr, on=(co.RESOURCE_ID == tr.CALENDAR_OBJECT_RESOURCE_ID)),
+ Where=(co.CALENDAR_RESOURCE_ID == Parameter("calendar_id")).And(
+ co.ICALENDAR_TYPE == "VEVENT"
+ ),
+ GroupBy=(co.RESOURCE_ID, co.RECURRANCE_MAX, co.RECURRANCE_MIN,),
+ Having=(
+ (co.RECURRANCE_MAX == None).And(Max(tr.END_DATE) < pyCalendarToSQLTimestamp(self.cutoff))
+ ).Or(
+ (co.RECURRANCE_MAX != None).And(co.RECURRANCE_MAX < pyCalendarToSQLTimestamp(self.cutoff))
+ ),
+ ).on(txn, **kwds))
+
+ log.debug(" Found {} resources to check".format(len(rows)))
+ for resource_id, recurrence_max, recurrence_min, max_end_date in rows:
+
+ recurrence_max = parseSQLDateToPyCalendar(recurrence_max) if recurrence_max else None
+ recurrence_min = parseSQLDateToPyCalendar(recurrence_min) if recurrence_min else None
+ max_end_date = parseSQLDateToPyCalendar(max_end_date) if max_end_date else None
+
+ # Find events where we know the max(end_date) represents a valid,
+ # untruncated expansion
+ if recurrence_min is None or recurrence_min < self.cutoff:
+ if recurrence_max is None:
+ # Here we know max_end_date is the fully expand final instance
+ if max_end_date < self.cutoff:
+ purge.add(self.PurgeEvent(home_id, calendar_id, resource_id,))
+ continue
+ elif recurrence_max > self.cutoff:
+ # Here we know that there are instances newer than the cut-off
+ # but they have not yet been indexed out that far
+ continue
+
+ # Manually detect the max_end_date from the actual calendar data
+ calendar = yield self.getCalendar(txn, resource_id)
+ if calendar is not None:
+ if self.checkLastInstance(calendar):
+ purge.add(self.PurgeEvent(home_id, calendar_id, resource_id,))
+
+ yield txn.commit()
+ log.debug(" Found {} resources to purge".format(len(purge)))
+ returnValue(purge)
+
+
+ @inlineCallbacks
+ def getCalendar(self, txn, resid):
+ """
+ Get the calendar data for a calendar object resource.
+
+ @param resid: resource-id of the calendar object resource to load
+ @type resid: L{int}
+ """
+ co = schema.CALENDAR_OBJECT
+ kwds = {"ResourceID" : resid}
+ rows = (yield Select(
+ [co.ICALENDAR_TEXT],
+ From=co,
+ Where=(
+ co.RESOURCE_ID == Parameter("ResourceID")
+ ),
+ ).on(txn, **kwds))
+ try:
+ caldata = Component.fromString(rows[0][0]) if rows else None
+ except InvalidICalendarDataError:
+ returnValue(None)
+
+ returnValue(caldata)
+
+
+ def checkLastInstance(self, calendar):
+ """
+ Determine the last instance of a calendar event. Try a "static" analysis of the data first,
+ and only if needed, do an instance expansion.
+
+ @param calendar: the calendar object to examine
+ @type calendar: L{Component}
+ """
+
+ # Is it recurring
+ master = calendar.masterComponent()
+ if not calendar.isRecurring() or master is None:
+ # Just check the end date
+ for comp in calendar.subcomponents():
+ if comp.name() == "VEVENT":
+ if comp.getEndDateUTC() > self.cutoff:
+ return False
+ else:
+ return True
+ elif calendar.isRecurringUnbounded():
+ return False
+ else:
+ # First test all sub-components
+ # Just check the end date
+ for comp in calendar.subcomponents():
+ if comp.name() == "VEVENT":
+ if comp.getEndDateUTC() > self.cutoff:
+ return False
+
+ # If we get here we need to test the RRULE - if there is an until use
+ # that as the end point, if a count, we have to expand
+ rrules = tuple(master.properties("RRULE"))
+ if len(rrules):
+ if rrules[0].value().getUseUntil():
+ return rrules[0].value().getUntil() < self.cutoff
+ else:
+ return not calendar.hasInstancesAfter(self.cutoff)
+
+ return True
+
+
+ @inlineCallbacks
+ def getResourcesToPurge(self, home_id, owner_uid):
+ """
+ Find all the resource-ids of calendar object resources that need to be purged in the specified home.
+
+ @param home_id: resource-id of calendar home to check
+ @type home_id: L{int}
+ @param owner_uid: owner UUID of home to check
+ @type owner_uid: L{str}
+ """
+
+ purge = set()
+ calendars = yield self.getMatchingCalendarIDs(home_id, owner_uid)
+ for calendar_id, calendar_name in calendars:
+ purge.update((yield self.getResourceIDsToPurge(home_id, calendar_id, calendar_name)))
+
+ returnValue(purge)
+
+
+ @inlineCallbacks
+ def purgeResources(self, events):
+ """
+ Remove up to batchSize events and return how
+ many were removed.
+ """
+
+ txn = self.store.newTransaction(label="Remove old events")
+ count = 0
+ last_home = None
+ last_calendar = None
+ for event in events:
+ if event.home != last_home:
+ home = (yield txn.calendarHomeWithResourceID(event.home))
+ last_home = event.home
+ if event.calendar != last_calendar:
+ calendar = (yield home.childWithID(event.calendar))
+ last_calendar = event.calendar
+ resource = (yield calendar.objectResourceWithID(event.resource))
+ yield resource.purge(implicitly=False)
+ log.debug("Removed resource {} '{}' from calendar {} '{}' of calendar home '{}'".format(
+ resource.id(),
+ resource.name(),
+ resource.parentCollection().id(),
+ resource.parentCollection().name(),
+ resource.parentCollection().ownerHome().uid()
+ ))
+ count += 1
+ yield txn.commit()
+ returnValue(count)
+
+
+ @inlineCallbacks
</ins><span class="cx"> def doWork(self):
</span><span class="cx">
</span><ins>+ if self.debug:
+ # Turn on debug logging for this module
+ config.LogLevels[__name__] = "debug"
+ else:
+ config.LogLevels[__name__] = "info"
+ config.update()
+
+ homes = yield self.getMatchingHomeUIDs()
+ if not homes:
+ log.info("No homes to process")
+ returnValue(0)
+
</ins><span class="cx"> if self.dryrun:
</span><del>- if self.verbose:
- print("(Dry run) Searching for old events...")
- txn = self.store.newTransaction(label="Find old events")
- oldEvents = yield txn.eventsOlderThan(self.cutoff)
- eventCount = len(oldEvents)
- if self.verbose:
- if eventCount == 0:
- print("No events are older than %s" % (self.cutoff,))
- elif eventCount == 1:
- print("1 event is older than %s" % (self.cutoff,))
- else:
- print("%d events are older than %s" % (eventCount, self.cutoff))
</del><ins>+ log.info("Purge dry run only")
+
+ log.info("Searching for old events...")
+
+ purge = set()
+ homes = yield self.getMatchingHomeUIDs()
+ for home_id, owner_uid in homes:
+ purge.update((yield self.getResourcesToPurge(home_id, owner_uid)))
+
+ if self.dryrun:
+ eventCount = len(purge)
+ if eventCount == 0:
+ log.info("No events are older than %s" % (self.cutoff,))
+ elif eventCount == 1:
+ log.info("1 event is older than %s" % (self.cutoff,))
+ else:
+ log.info("%d events are older than %s" % (eventCount, self.cutoff))
</ins><span class="cx"> returnValue(eventCount)
</span><span class="cx">
</span><del>- if self.verbose:
- print("Removing events older than %s..." % (self.cutoff,))
</del><ins>+ purge = list(purge)
+ purge.sort()
+ totalEvents = len(purge)
</ins><span class="cx">
</span><ins>+ log.info("Removing {} events older than {}...".format(len(purge), self.cutoff,))
+
</ins><span class="cx"> numEventsRemoved = -1
</span><span class="cx"> totalRemoved = 0
</span><span class="cx"> while numEventsRemoved:
</span><del>- txn = self.store.newTransaction(label="Remove old events")
- numEventsRemoved = yield txn.removeOldEvents(self.cutoff, batchSize=self.batchSize)
- yield txn.commit()
</del><ins>+ numEventsRemoved = (yield self.purgeResources(purge[:self.batchSize]))
</ins><span class="cx"> if numEventsRemoved:
</span><span class="cx"> totalRemoved += numEventsRemoved
</span><del>- if self.verbose:
- print("%d," % (totalRemoved,),)
</del><ins>+ log.debug(" Removed {} of {} events...".format(totalRemoved, totalEvents))
+ purge = purge[numEventsRemoved:]
</ins><span class="cx">
</span><del>- if self.verbose:
- print("")
- if totalRemoved == 0:
- print("No events were removed")
- elif totalRemoved == 1:
- print("1 event was removed in total")
- else:
- print("%d events were removed in total" % (totalRemoved,))
</del><ins>+ if totalRemoved == 0:
+ log.info("No events were removed")
+ elif totalRemoved == 1:
+ log.info("1 event was removed in total")
+ else:
+ log.info("%d events were removed in total" % (totalRemoved,))
</ins><span class="cx">
</span><span class="cx"> returnValue(totalRemoved)
</span><span class="cx">
</span><span class="lines">@@ -671,20 +936,18 @@
</span><span class="cx"> # Print table of results
</span><span class="cx"> table = tables.Table()
</span><span class="cx"> table.addHeader(("User", "Current Quota", "Orphan Size", "Orphan Count", "Dropbox Size", "Dropbox Count", "Managed Size", "Managed Count", "Total Size", "Total Count"))
</span><del>- table.setDefaultColumnFormats(
- (
- tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
- tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
- tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
- tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
- tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
- tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
- tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
- tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
- tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
- tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
- )
- )
</del><ins>+ table.setDefaultColumnFormats((
+ tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
+ tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+ tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+ tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+ tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+ tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+ tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+ tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+ tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+ tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+ ))
</ins><span class="cx">
</span><span class="cx"> totals = [0] * 8
</span><span class="cx"> for user, data in sorted(byuser.items(), key=lambda x: x[0]):
</span></span></pre></div>
<a id="CalendarServertrunkcalendarservertoolstesttest_purge_old_eventspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py (14969 => 14970)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py        2015-07-15 16:10:33 UTC (rev 14969)
+++ CalendarServer/trunk/calendarserver/tools/test/test_purge_old_events.py        2015-07-15 17:25:10 UTC (rev 14970)
</span><span class="lines">@@ -642,33 +642,123 @@
</span><span class="cx"> # Dry run
</span><span class="cx"> total = (yield PurgeOldEventsService.purgeOldEvents(
</span><span class="cx"> self._sqlCalendarStore,
</span><ins>+ None,
</ins><span class="cx"> DateTime(now, 4, 1, 0, 0, 0),
</span><span class="cx"> 2,
</span><span class="cx"> dryrun=True,
</span><del>- verbose=False
</del><ins>+ debug=True
</ins><span class="cx"> ))
</span><span class="cx"> self.assertEquals(total, 13)
</span><span class="cx">
</span><span class="cx"> # Actually remove
</span><span class="cx"> total = (yield PurgeOldEventsService.purgeOldEvents(
</span><span class="cx"> self._sqlCalendarStore,
</span><ins>+ None,
</ins><span class="cx"> DateTime(now, 4, 1, 0, 0, 0),
</span><span class="cx"> 2,
</span><del>- verbose=False
</del><ins>+ debug=True
</ins><span class="cx"> ))
</span><span class="cx"> self.assertEquals(total, 13)
</span><span class="cx">
</span><span class="cx"> # There should be no more left
</span><span class="cx"> total = (yield PurgeOldEventsService.purgeOldEvents(
</span><span class="cx"> self._sqlCalendarStore,
</span><ins>+ None,
</ins><span class="cx"> DateTime(now, 4, 1, 0, 0, 0),
</span><span class="cx"> 2,
</span><del>- verbose=False
</del><ins>+ debug=True
</ins><span class="cx"> ))
</span><span class="cx"> self.assertEquals(total, 0)
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><ins>+ def test_purgeOldEvents_home_filtering(self):
+
+ # Dry run
+ total = (yield PurgeOldEventsService.purgeOldEvents(
+ self._sqlCalendarStore,
+ "ho",
+ DateTime(now, 4, 1, 0, 0, 0),
+ 2,
+ dryrun=True,
+ debug=True
+ ))
+ self.assertEquals(total, 13)
+
+ # Dry run
+ total = (yield PurgeOldEventsService.purgeOldEvents(
+ self._sqlCalendarStore,
+ "home",
+ DateTime(now, 4, 1, 0, 0, 0),
+ 2,
+ dryrun=True,
+ debug=True
+ ))
+ self.assertEquals(total, 13)
+
+ # Dry run
+ total = (yield PurgeOldEventsService.purgeOldEvents(
+ self._sqlCalendarStore,
+ "home1",
+ DateTime(now, 4, 1, 0, 0, 0),
+ 2,
+ dryrun=True,
+ debug=True
+ ))
+ self.assertEquals(total, 5)
+
+ # Dry run
+ total = (yield PurgeOldEventsService.purgeOldEvents(
+ self._sqlCalendarStore,
+ "home2",
+ DateTime(now, 4, 1, 0, 0, 0),
+ 2,
+ dryrun=True,
+ debug=True
+ ))
+ self.assertEquals(total, 8)
+
+
+ @inlineCallbacks
+ def test_purgeOldEvents_old_cutoff(self):
+
+ # Dry run
+ cutoff = DateTime.getToday()
+ cutoff.setDateOnly(False)
+ cutoff.offsetDay(-400)
+
+ total = (yield PurgeOldEventsService.purgeOldEvents(
+ self._sqlCalendarStore,
+ "ho",
+ cutoff,
+ 2,
+ dryrun=True,
+ debug=True
+ ))
+ self.assertEquals(total, 12)
+
+ # Actually remove
+ total = (yield PurgeOldEventsService.purgeOldEvents(
+ self._sqlCalendarStore,
+ None,
+ cutoff,
+ 2,
+ debug=True
+ ))
+ self.assertEquals(total, 12)
+
+ total = (yield PurgeOldEventsService.purgeOldEvents(
+ self._sqlCalendarStore,
+ "ho",
+ cutoff,
+ 2,
+ dryrun=True,
+ debug=True
+ ))
+ self.assertEquals(total, 0)
+
+
+ @inlineCallbacks
</ins><span class="cx"> def test_purgeUID(self):
</span><span class="cx"> txn = self._sqlCalendarStore.newTransaction()
</span><span class="cx">
</span><span class="lines">@@ -753,9 +843,10 @@
</span><span class="cx"> # Remove old events first
</span><span class="cx"> total = (yield PurgeOldEventsService.purgeOldEvents(
</span><span class="cx"> self._sqlCalendarStore,
</span><ins>+ None,
</ins><span class="cx"> DateTime(now, 4, 1, 0, 0, 0),
</span><span class="cx"> 2,
</span><del>- verbose=False
</del><ins>+ debug=False
</ins><span class="cx"> ))
</span><span class="cx"> self.assertEquals(total, 13)
</span><span class="cx">
</span></span></pre>
</div>
</div>
</body>
</html>