[CalendarServer-changes] [14518] CalendarServer/branches/users/sagen/trashcan-5

source_changes at macosforge.org source_changes at macosforge.org
Fri Mar 6 11:34:31 PST 2015


Revision: 14518
          http://trac.calendarserver.org//changeset/14518
Author:   sagen at apple.com
Date:     2015-03-06 11:34:30 -0800 (Fri, 06 Mar 2015)
Log Message:
-----------
Checkpoint of trash recovery command line tool

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/trashcan-5/calendarserver/push/notifier.py
    CalendarServer/branches/users/sagen/trashcan-5/calendarserver/tools/cmdline.py
    CalendarServer/branches/users/sagen/trashcan-5/calendarserver/tools/trash.py
    CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py

Modified: CalendarServer/branches/users/sagen/trashcan-5/calendarserver/push/notifier.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/calendarserver/push/notifier.py	2015-03-06 01:01:12 UTC (rev 14517)
+++ CalendarServer/branches/users/sagen/trashcan-5/calendarserver/push/notifier.py	2015-03-06 19:34:30 UTC (rev 14518)
@@ -204,7 +204,8 @@
 
 
     def pushKeyForId(self, prefix, id):
-        return "/%s/%s/%s/" % (prefix, self.hostname, id)
+        key = "/%s/%s/%s/" % (prefix, self.hostname, id)
+        return key[:255]
 
 
 

Modified: CalendarServer/branches/users/sagen/trashcan-5/calendarserver/tools/cmdline.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/calendarserver/tools/cmdline.py	2015-03-06 01:01:12 UTC (rev 14517)
+++ CalendarServer/branches/users/sagen/trashcan-5/calendarserver/tools/cmdline.py	2015-03-06 19:34:30 UTC (rev 14518)
@@ -33,13 +33,14 @@
 from calendarserver.tap.util import getRootResource
 from errno import ENOENT, EACCES
 from twext.enterprise.jobqueue import NonPerformingQueuer
+from twistedcaldav.timezones import TimezoneCache
 
 # TODO: direct unit tests for these functions.
 
 
 def utilityMain(
     configFileName, serviceClass, reactor=None, serviceMaker=None,
-    patchConfig=None, onShutdown=None, verbose=False
+    patchConfig=None, onShutdown=None, verbose=False, loadTimezones=False
 ):
     """
     Shared main-point for utilities.
@@ -124,6 +125,10 @@
         if onShutdown is not None:
             reactor.addSystemEventTrigger("before", "shutdown", onShutdown)
 
+        if loadTimezones:
+            TimezoneCache.create()
+
+
     except (ConfigurationError, OSError), e:
         sys.stderr.write("Error: %s\n" % (e,))
         return

Modified: CalendarServer/branches/users/sagen/trashcan-5/calendarserver/tools/trash.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/calendarserver/tools/trash.py	2015-03-06 01:01:12 UTC (rev 14517)
+++ CalendarServer/branches/users/sagen/trashcan-5/calendarserver/tools/trash.py	2015-03-06 19:34:30 UTC (rev 14518)
@@ -17,48 +17,27 @@
 ##
 from __future__ import print_function
 
-import collections
+from argparse import ArgumentParser
 import datetime
-from getopt import getopt, GetoptError
-import os
-import sys
 
-from calendarserver.tools import tables
 from calendarserver.tools.cmdline import utilityMain, WorkerService
-
-from pycalendar.datetime import DateTime
-
-from twext.enterprise.dal.record import fromTable
-from twext.enterprise.dal.syntax import Delete, Select, Union
-from twext.enterprise.jobqueue import WorkItem, RegeneratingWorkItem
+from calendarserver.tools.util import prettyRecord
 from twext.python.log import Logger
+from twisted.internet.defer import inlineCallbacks, returnValue
+from txdav.base.propertystore.base import PropertyName
+from txdav.xml import element
 
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-
-from twistedcaldav import caldavxml
-from twistedcaldav.config import config
-
-from txdav.caldav.datastore.query.filter import Filter
-from txdav.common.datastore.sql_tables import schema, _HOME_STATUS_NORMAL
-from txdav.caldav.datastore.sql import CalendarStoreFeatures
-
-from argparse import ArgumentParser
-
-
 log = Logger()
 
 
 
 class TrashRestorationService(WorkerService):
 
-    principals = []
+    operation = None
+    operationArgs = []
 
     def doWork(self):
-        rootResource = self.rootResource()
-        directory = rootResource.getDirectory()
-        return restoreFromTrash(
-            self.store, directory, rootResource, self.principals
-        )
+        return self.operation(self.store, *self.operationArgs)
 
 
 
@@ -67,41 +46,277 @@
     parser = ArgumentParser(description='Restore events from trash')
     parser.add_argument('-f', '--config', dest='configFileName', metavar='CONFIGFILE', help='caldavd.plist configuration file path')
     parser.add_argument('-d', '--debug', action='store_true', help='show debug logging')
-    parser.add_argument('principal', help='one or more principals to restore', nargs='+')  # Required
+    parser.add_argument('-p', '--principal', dest='principal', help='the principal to use (uid)')
+    parser.add_argument('-e', '--events', action='store_true', help='list trashed events')
+    parser.add_argument('-c', '--collections', action='store_true', help='list trashed collections for principal (uid)')
+    parser.add_argument('-r', '--recover', dest='resourceID', type=int, help='recover trashed collection or event (by resource ID)')
+    parser.add_argument('--empty', action='store_true', help='empty the principal\'s trash')
+    parser.add_argument('--days', type=int, default=0, help='number of past days to retain')
+
     args = parser.parse_args()
 
-    TrashRestorationService.principals = args.principal
+    if not args.principal:
+        print("--principal missing")
+        return
 
+    if args.empty:
+        operation = emptyTrashForPrincipal
+        operationArgs = [args.principal, args.days]
+    elif args.collections:
+        if args.resourceID:
+            operation = restoreTrashedCollection
+            operationArgs = [args.principal, args.resourceID]
+        else:
+            operation = listTrashedCollectionsForPrincipal
+            operationArgs = [args.principal]
+    elif args.events:
+        if args.resourceID:
+            operation = restoreTrashedEvent
+            operationArgs = [args.principal, args.resourceID]
+        else:
+            operation = listTrashedEventsForPrincipal
+            operationArgs = [args.principal]
+    else:
+        operation = listTrashedCollectionsForPrincipal
+        operationArgs = [args.principal]
+
+    TrashRestorationService.operation = operation
+    TrashRestorationService.operationArgs = operationArgs
+
     utilityMain(
         args.configFileName,
         TrashRestorationService,
         verbose=args.debug,
+        loadTimezones=True
     )
 
 
+ at inlineCallbacks
+def listTrashedCollectionsForPrincipal(service, store, principalUID):
+    directory = store.directoryService()
+    record = yield directory.recordWithUID(principalUID)
+    if record is None:
+        print("No record found for:", principalUID)
+        returnValue(None)
 
+    txn = store.newTransaction(label="List trashed collections")
+    home = yield txn.calendarHomeWithUID(principalUID)
+    if home is None:
+        print("No home for principal")
+        returnValue(None)
+
+    trash = yield home.childWithName("trash")
+
+    trashedCollections = yield home.children(onlyInTrash=True)
+    if len(trashedCollections) == 0:
+        print("No trashed collections for:", prettyRecord(record))
+        returnValue(None)
+
+    print("Listing trashed collections for:", prettyRecord(record))
+    for collection in trashedCollections:
+        displayName = displayNameForCollection(collection)
+        print(
+            "Collection = \"{}\", trashed = {}, id = {}".format(
+                displayName.encode("utf-8"), collection.whenTrashed(),
+                collection._resourceID
+            )
+        )
+        startTime = collection.whenTrashed() - datetime.timedelta(minutes=5)
+        children = yield trash.trashForCollection(
+            collection._resourceID, start=startTime
+        )
+        print(" ...containing events:")
+        for child in children:
+            component = yield child.component()
+            summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
+            whenTrashed = yield child.whenTrashed()
+            print(" \"{}\", trashed = {}".format(summary.encode("utf-8"), whenTrashed))
+
+    yield txn.commit()
+
+
 @inlineCallbacks
-def restoreFromTrash(store, directory, root, principals):
+def listTrashedEventsForPrincipal(service, store, principalUID):
+    directory = store.directoryService()
+    record = yield directory.recordWithUID(principalUID)
+    if record is None:
+        print("No record found for:", principalUID)
+        returnValue(None)
 
-    for principalUID in principals:
-        txn = store.newTransaction(label="Restore trashed events")
-        home = yield txn.calendarHomeWithUID(principalUID)
-        if home is None:
+    txn = store.newTransaction(label="List trashed collections")
+    home = yield txn.calendarHomeWithUID(principalUID)
+    if home is None:
+        print("No home for principal")
+        returnValue(None)
+
+    trash = yield home.childWithName("trash")
+
+    untrashedCollections = yield home.children(onlyInTrash=False)
+    if len(untrashedCollections) == 0:
+        print("No untrashed collections for:", prettyRecord(record))
+        returnValue(None)
+
+    # print("Listing trashed collections for:", prettyRecord(record))
+    for collection in untrashedCollections:
+        displayName = displayNameForCollection(collection)
+        children = yield trash.trashForCollection(collection._resourceID)
+        if len(children) == 0:
             continue
-        trash = yield home.childWithName("trash")
-        names = yield trash.listObjectResources()
-        for name in names:
-            cobj = yield trash.calendarObjectWithName(name)
-            print(name, cobj)
 
-            if cobj is not None:
-                # If it's still in the trash, restore it from trash
-                if (yield cobj.isInTrash()):
-                    print("Restoring:", name)
-                    yield cobj.fromTrash()
+        print("Collection = \"{}\"".format(displayName.encode("utf-8")))
+        for child in children:
+            component = yield child.component()
+            summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
+            whenTrashed = yield child.whenTrashed()
+            print(
+                " \"{}\", trashed = {}, id = {}".format(
+                    summary.encode("utf-8"), whenTrashed, child._resourceID
+                )
+            )
 
-        yield txn.commit()
+    yield txn.commit()
 
 
+ at inlineCallbacks
+def restoreTrashedCollection(service, store, principalUID, resourceID):
+    directory = store.directoryService()
+    record = yield directory.recordWithUID(principalUID)
+    if record is None:
+        print("No record found for:", principalUID)
+        returnValue(None)
+
+    txn = store.newTransaction(label="Restore trashed collection")
+    home = yield txn.calendarHomeWithUID(principalUID)
+    if home is None:
+        print("No home for principal")
+        returnValue(None)
+
+    collection = yield home.childWithID(resourceID, onlyInTrash=True)
+    if collection is None:
+        print("Collection {} is not in the trash".format(resourceID))
+        returnValue(None)
+
+    yield collection.fromTrash(
+        restoreChildren=True, delta=datetime.timedelta(minutes=5), verbose=True
+    )
+
+    yield txn.commit()
+
+
+ at inlineCallbacks
+def restoreTrashedEvent(service, store, principalUID, resourceID):
+    directory = store.directoryService()
+    record = yield directory.recordWithUID(principalUID)
+    if record is None:
+        print("No record found for:", principalUID)
+        returnValue(None)
+
+    txn = store.newTransaction(label="Restore trashed collection")
+    home = yield txn.calendarHomeWithUID(principalUID)
+    if home is None:
+        print("No home for principal")
+        returnValue(None)
+
+    trash = yield home.childWithName("trash")
+    child = yield trash.objectResourceWithID(resourceID)
+    if child is None:
+        print("Event not found")
+        returnValue(None)
+
+    component = yield child.component()
+    summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
+    print("Restoring \"{}\"".format(summary.encode("utf-8")))
+    yield child.fromTrash()
+
+    yield txn.commit()
+
+
+
+ at inlineCallbacks
+def emptyTrashForPrincipal(service, store, principalUID, days):
+    directory = store.directoryService()
+    record = yield directory.recordWithUID(principalUID)
+    if record is None:
+        print("No record found for:", principalUID)
+        returnValue(None)
+
+    txn = store.newTransaction(label="List trashed collections")
+    home = yield txn.calendarHomeWithUID(principalUID)
+    if home is None:
+        print("No home for principal")
+        returnValue(None)
+
+    trash = yield home.childWithName("trash")
+
+    untrashedCollections = yield home.children(onlyInTrash=False)
+    if len(untrashedCollections) == 0:
+        print("No untrashed collections for:", prettyRecord(record))
+        returnValue(None)
+
+    endTime = datetime.datetime.utcnow() - datetime.timedelta(days=-days)
+    # print("Listing trashed collections for:", prettyRecord(record))
+    for collection in untrashedCollections:
+        displayName = displayNameForCollection(collection)
+        children = yield trash.trashForCollection(
+            collection._resourceID, end=endTime
+        )
+        if len(children) == 0:
+            continue
+
+        print("Collection = \"{}\"".format(displayName.encode("utf-8")))
+        for child in children:
+            component = yield child.component()
+            summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
+            whenTrashed = yield child.whenTrashed()
+            print(
+                " \"{}\", trashed = {}, id = {}".format(
+                    summary.encode("utf-8"), whenTrashed, child._resourceID
+                )
+            )
+            print("Removing...")
+            yield child.reallyRemove()
+
+    yield txn.commit()
+
+# @inlineCallbacks
+# def restoreFromTrash(store, directory, root, principals):
+
+#     for principalUID in principals:
+#         txn = store.newTransaction(label="Restore trashed events")
+#         home = yield txn.calendarHomeWithUID(principalUID)
+#         if home is None:
+#             continue
+#         trashedCollections = yield home.children(onlyInTrash=True)
+#         for collection in trashedCollections:
+#             displayName = displayNameForCollection(collection)
+#             print("Restoring collection", displayName, collection._resourceID)
+#             yield collection.fromTrash(restoreChildren=True)
+#         # This code is for untrashing all objects:
+#         # names = yield trash.listObjectResources()
+#         # for name in names:
+#         #     cobj = yield trash.calendarObjectWithName(name)
+#         #     print(name, cobj)
+
+#         #     if cobj is not None:
+#         #         # If it's still in the trash, restore it from trash
+#         #         if (yield cobj.isInTrash()):
+#         #             print("Restoring:", name)
+#         #             yield cobj.fromTrash()
+
+#         yield txn.commit()
+
+
+def displayNameForCollection(collection):
+    try:
+        displayName = collection.properties()[
+            PropertyName.fromElement(element.DisplayName)
+        ]
+        displayName = displayName.toString()
+    except:
+        displayName = collection.name()
+
+    return displayName
+
+
 if __name__ == "__main__":
     main()

Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py	2015-03-06 01:01:12 UTC (rev 14517)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py	2015-03-06 19:34:30 UTC (rev 14518)
@@ -4927,6 +4927,7 @@
                 splitter = iCalSplitter()
                 willSplit = splitter.willSplit(caldata)
                 if willSplit:
+                    log.debug("Splitting scheduled event being recovered by organizer from trash")
                     yield self.split(
                         coercePartstatsInExistingResource=True,
                         splitter=splitter
@@ -4946,6 +4947,7 @@
                     instances = sorted(instances.instances.values(), key=lambda x: x.start)
                     if instances[0].start >= now:
                         # future
+                        log.debug("Scheduled event being recovered by organizer from trash, fully in the future")
                         newdata = caldata.duplicate()
                         newdata.bumpiTIPInfo(doSequence=True)
                         for attendee in newdata.getAllAttendeeProperties():
@@ -4959,6 +4961,7 @@
 
                     else:
                         # past
+                        log.debug("Scheduled event being recovered by organizer from trash, fully in the past")
                         yield ImplicitScheduler().refreshAllAttendeesExceptSome(
                             self._txn,
                             self,
@@ -4966,10 +4969,13 @@
 
             else:
                 # If an ATTENDEE is moving the event from trash
+                log.debug("Scheduled event being recovered by attendee from trash")
                 yield ImplicitScheduler().sendAttendeeReply(
                     self._txn,
                     self
                 )
+        else:
+            log.debug("Recovered un-scheduled event from trash")
 
         returnValue(name)
 

Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py	2015-03-06 01:01:12 UTC (rev 14517)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py	2015-03-06 19:34:30 UTC (rev 14518)
@@ -6168,7 +6168,7 @@
         yield self._updateIsInTrashQuery.on(
             self._txn, isInTrash=True, trashed=whenTrashed, resourceID=self._resourceID
         )
-        newName = "{}-{}".format(self._name[:200], str(uuid4()))
+        newName = "{}-{}".format(self._name[:36], str(uuid4()))
         yield self.rename(newName)
         # yield self.notifyPropertyChanged()
         # yield self.invalidateQueryCache()
@@ -6192,9 +6192,10 @@
 
 
     @inlineCallbacks
-    def fromTrash(self, restoreChildren=True, delta=datetime.timedelta(minutes=3)):
-
-        # print("XYZZY collection fromTrash")
+    def fromTrash(
+        self, restoreChildren=True, delta=datetime.timedelta(minutes=5),
+        verbose=False
+    ):
         if not self._isInTrash:
             returnValue(None)
 
@@ -6202,7 +6203,6 @@
         if delta is not None:
             startTime = startTime - delta
 
-
         yield self._updateIsInTrashQuery.on(
             self._txn, isInTrash=False, trashed=None, resourceID=self._resourceID
         )
@@ -6231,7 +6231,11 @@
                 self._resourceID, start=startTime
             )
             for child in childrenToRestore:
-                # print("Restoring", child)
+                if verbose:
+                    component = yield child.component()
+                    summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
+                    print("Recovering \"{}\"".format(summary.encode("utf-8")))
+
                 yield child.fromTrash()
 
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150306/4755b4f8/attachment-0001.html>


More information about the calendarserver-changes mailing list