[CalendarServer-changes] [10162] CalendarServer/branches/users/gaya/sharedgroups

source_changes at macosforge.org source_changes at macosforge.org
Wed Dec 12 14:11:41 PST 2012


Revision: 10162
          http://trac.calendarserver.org//changeset/10162
Author:   gaya at apple.com
Date:     2012-12-12 14:11:41 -0800 (Wed, 12 Dec 2012)
Log Message:
-----------
merge from trunk

Modified Paths:
--------------
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/accesslog.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/purge.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge_old_events.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/webcal/resource.py
    CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist
    CalendarServer/branches/users/gaya/sharedgroups/contrib/tools/readStats.py
    CalendarServer/branches/users/gaya/sharedgroups/support/build.sh
    CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/dal/syntax.py
    CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/file.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_tables.py

Added Paths:
-----------
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/logAnalysis.py
    CalendarServer/branches/users/gaya/sharedgroups/calendarserver/test/test_methodDescriptor.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/python/test/test_timezone.py
    CalendarServer/branches/users/gaya/sharedgroups/twext/python/timezone.py
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/oracle-dialect/v13.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v13.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql
    CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_13_to_14.sql

Property Changed:
----------------
    CalendarServer/branches/users/gaya/sharedgroups/


Property changes on: CalendarServer/branches/users/gaya/sharedgroups
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:9885-10150
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:9885-10159

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/accesslog.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/accesslog.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/accesslog.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -35,30 +35,37 @@
     psutil = None
 import time
 
-from twisted.internet import protocol, task
-from twisted.protocols import amp
+from calendarserver.logAnalysis import getAdjustedMethodName, \
+    getAdjustedClientName
+
+from twext.python.log import Logger
 from twext.web2 import iweb
-from txdav.xml import element as davxml
 from twext.web2.log import BaseCommonAccessLoggingObserver
 from twext.web2.log import LogWrapperResource
 
-from twext.python.log import Logger
+from twisted.internet import protocol, task
+from twisted.protocols import amp
 
 from twistedcaldav.config import config
 from twistedcaldav.directory.directory import DirectoryService
 
+from txdav.xml import element as davxml
+
 log = Logger()
 
 class DirectoryLogWrapperResource(LogWrapperResource):
-    
+
     def __init__(self, resource, directory):
         super(DirectoryLogWrapperResource, self).__init__(resource)
-        
+
         self.directory = directory
-        
+
+
     def getDirectory(self):
         return self.directory
 
+
+
 class CommonAccessLoggingObserverExtensions(BaseCommonAccessLoggingObserver):
     """
     A base class for our extension to the L{BaseCommonAccessLoggingObserver}
@@ -69,7 +76,7 @@
         format = None
         formatArgs = None
         if eventDict.get("interface") is iweb.IRequest:
-            
+
             request = eventDict["request"]
             response = eventDict["response"]
             loginfo = eventDict["loginfo"]
@@ -82,7 +89,7 @@
                     uidz = None
                     if hasattr(request, "authzUser") and str(request.authzUser.children[0]) != uidn:
                         uidz = str(request.authzUser.children[0])
-                        
+
                     def convertUIDtoShortName(uid):
                         uid = uid.rstrip("/")
                         uid = uid[uid.rfind("/") + 1:]
@@ -94,11 +101,11 @@
                                 return "(%s)%s" % (record.recordType, record.shortNames[0],)
                         else:
                             return uid
-                        
+
                     uidn = convertUIDtoShortName(uidn)
                     if uidz:
                         uidz = convertUIDtoShortName(uidz)
-                        
+
                     if uidn and uidz:
                         uid = '"%s as %s"' % (uidn, uidz,)
                     else:
@@ -142,10 +149,10 @@
             if config.EnableExtendedAccessLog:
                 format += ' i=%(serverInstance)s'
                 formatArgs["serverInstance"] = config.LogID if config.LogID else "0"
-                
+
                 format += ' or=%(outstandingRequests)s'
                 formatArgs["outstandingRequests"] = request.chanRequest.channel.factory.outstandingRequests
-                
+
                 # Tags for time stamps collected along the way - the first one in the list is the initial
                 # time for request creation - we use that to track the entire request/response time
                 nowtime = time.time()
@@ -213,40 +220,42 @@
             if config.EnableExtendedAccessLog:
                 format += ' p=%(serverPort)s'
                 formatArgs["serverPort"] = overloaded.transport.server.port
-                
+
                 format += ' or=%(outstandingRequests)s'
                 formatArgs["outstandingRequests"] = overloaded.outstandingRequests
 
-
         # Write anything we got to the log and stats
         if format is not None:
             # sanitize output to mitigate log injection
-            for k,v in formatArgs.items():
+            for k, v in formatArgs.items():
                 if not isinstance(v, basestring):
                     continue
                 v = v.replace("\r", "\\r")
                 v = v.replace("\n", "\\n")
                 v = v.replace("\"", "\\\"")
                 formatArgs[k] = v
-    
+
             formatArgs["type"] = "access-log"
             formatArgs["log-format"] = format
             self.logStats(formatArgs)
 
+
+
 class RotatingFileAccessLoggingObserver(CommonAccessLoggingObserverExtensions):
     """
     Class to do "apache" style access logging to a rotating log file. The log
     file is rotated after midnight each day.
-    
+
     This class also currently handles the collection of system and log statistics.
     """
 
     def __init__(self, logpath):
-        self.logpath = logpath        
+        self.logpath = logpath
 
         self.systemStats = None
         self.statsByMinute = []
 
+
     def accessLog(self, message, allowrotate=True):
         """
         Log a message to the file and possibly rotate if date has changed.
@@ -261,6 +270,7 @@
             self.rotate()
         self.f.write(message + "\n")
 
+
     def start(self):
         """
         Start logging. Open the log file and log an "open" message.
@@ -270,6 +280,7 @@
         self._open()
         self.accessLog("Log opened - server start: [%s]." % (datetime.datetime.now().ctime(),))
 
+
     def stop(self):
         """
         Stop logging. Close the log file and log an "open" message.
@@ -278,10 +289,11 @@
         self.accessLog("Log closed - server stop: [%s]." % (datetime.datetime.now().ctime(),), False)
         super(RotatingFileAccessLoggingObserver, self).stop()
         self._close()
-        
+
         if self.systemStats is not None:
             self.systemStats.stop()
 
+
     def _open(self):
         """
         Open the log file.
@@ -290,6 +302,7 @@
         self.f = open(self.logpath, "a", 1)
         self.lastDate = self.toDate(os.stat(self.logpath)[8])
 
+
     def _close(self):
         """
         Close the log file.
@@ -297,6 +310,7 @@
 
         self.f.close()
 
+
     def flush(self):
         """
         Flush the log file.
@@ -304,6 +318,7 @@
 
         self.f.flush()
 
+
     def shouldRotate(self):
         """
         Rotate when the date has changed since last write
@@ -314,6 +329,7 @@
         else:
             return False
 
+
     def toDate(self, *args):
         """
         Convert a unixtime to (year, month, day) localtime tuple,
@@ -326,6 +342,7 @@
         # primarily so this can be unit tested easily
         return time.localtime(*args)[:3]
 
+
     def suffix(self, tupledate):
         """
         Return the suffix given a (year, month, day) tuple or unixtime
@@ -337,6 +354,7 @@
             # try taking a float unixtime
             return "_".join(map(str, self.toDate(tupledate)))
 
+
     def rotate(self):
         """
         Rotate the file and create a new one.
@@ -356,10 +374,11 @@
         self._open()
         self.accessLog("Log opened - rotated: [%s]." % (datetime.datetime.now().ctime(),), False)
 
-    def logStats(self, stats): 
-        """ 
+
+    def logStats(self, stats):
+        """
         Update stats
-        """ 
+        """
 
         if self.systemStats is None:
             self.systemStats = SystemMonitor()
@@ -367,18 +386,19 @@
         # Currently only storing stats for access log type
         if "type" not in stats or stats["type"] != "access-log":
             return
-    
+
         currentStats = self.ensureSequentialStats()
         self.updateStats(currentStats, stats)
-        
+
         if stats["type"] == "access-log":
             self.accessLog(stats["log-format"] % stats)
 
-    def getStats(self): 
-        """ 
-        Return the stats 
+
+    def getStats(self):
         """
-        
+        Return the stats
+        """
+
         if self.systemStats is None:
             self.systemStats = SystemMonitor()
 
@@ -407,17 +427,18 @@
             self.mergeStats(oneHour, stat)
 
         printStats = {
-            "System":self.systemStats.items,
-            "Current":currentStats,
-            "1 Minute":previousMinute,
-            "5 Minutes":fiveMinutes,
-            "1 Hour":oneHour,
+            "System": self.systemStats.items,
+            "Current": currentStats,
+            "1 Minute": previousMinute,
+            "5 Minutes": fiveMinutes,
+            "1 Hour": oneHour,
         }
         return json.dumps(printStats)
 
+
     def ensureSequentialStats(self):
         """
-        Make sure the list of timed stats is contiguous wrt time. 
+        Make sure the list of timed stats is contiguous wrt time.
         """
         dtindex = int(time.time() / 60.0) * 60
 
@@ -431,8 +452,9 @@
             self.statsByMinute.append((dtindex, self.initStats(),))
         return self.statsByMinute[-1][1]
 
+
     def initStats(self):
-        
+
         def initTimeHistogram():
             return {
                 "<10ms": 0,
@@ -447,33 +469,41 @@
             }
 
         return {
-            "requests" : 0,
-            "method"   : collections.defaultdict(int),
-            "uid"      : collections.defaultdict(int),
-            "500"      : 0,
-            "t"        : 0.0,
-            "t-resp-wr": 0.0,
-            "slots"    : 0,
-            "T"        : initTimeHistogram(),
-            "T-RESP-WR": initTimeHistogram(),
-            "T-MAX"    : 0.0,
-            "cpu"      : self.systemStats.items["cpu use"],
+            "requests"   : 0,
+            "method"     : collections.defaultdict(int),
+            "method-t"   : collections.defaultdict(float),
+            "uid"        : collections.defaultdict(int),
+            "user-agent" : collections.defaultdict(int),
+            "500"        : 0,
+            "t"          : 0.0,
+            "t-resp-wr"  : 0.0,
+            "slots"      : 0,
+            "T"          : initTimeHistogram(),
+            "T-RESP-WR"  : initTimeHistogram(),
+            "T-MAX"      : 0.0,
+            "cpu"        : self.systemStats.items["cpu use"],
         }
 
+
     def updateStats(self, current, stats):
         # Gather specific information and aggregate into our persistent stats
+        adjustedMethod = getAdjustedMethodName(stats)
+        adjustedClient = getAdjustedClientName(stats)
+
         if current["requests"] == 0:
             current["cpu"] = 0.0
         current["requests"] += 1
-        current["method"][stats["method"]] += 1
+        current["method"][adjustedMethod] += 1
+        current["method-t"][adjustedMethod] += stats.get("t", 0.0)
         current["uid"][stats["uid"]] += 1
+        current["user-agent"][adjustedClient] += 1
         if stats["statusCode"] >= 500:
             current["500"] += 1
         current["t"] += stats.get("t", 0.0)
         current["t-resp-wr"] += stats.get("t-resp-wr", 0.0)
         current["slots"] += stats.get("outstandingRequests", 0)
         current["cpu"] += self.systemStats.items["cpu use"]
-        
+
         def histogramUpdate(t, key):
             if t >= 60000.0:
                 current[key][">60s"] += 1
@@ -493,7 +523,7 @@
                 current[key]["Over 1s"] += 1
             elif t >= 10000.0:
                 current[key]["Over 10s"] += 1
-            
+
         t = stats.get("t", None)
         if t is not None:
             histogramUpdate(t, "T")
@@ -502,6 +532,7 @@
         if t is not None:
             histogramUpdate(t, "T-RESP-WR")
 
+
     def mergeStats(self, current, stats):
         # Gather specific information and aggregate into our persistent stats
         if current["requests"] == 0:
@@ -509,14 +540,18 @@
         current["requests"] += stats["requests"]
         for method in stats["method"].keys():
             current["method"][method] += stats["method"][method]
+        for method in stats["method-t"].keys():
+            current["method-t"][method] += stats["method-t"][method]
         for uid in stats["uid"].keys():
             current["uid"][uid] += stats["uid"][uid]
+        for ua in stats["user-agent"].keys():
+            current["user-agent"][ua] += stats["user-agent"][ua]
         current["500"] += stats["500"]
         current["t"] += stats["t"]
         current["t-resp-wr"] += stats["t-resp-wr"]
         current["slots"] += stats["slots"]
         current["cpu"] += stats["cpu"]
-        
+
         def histogramUpdate(t, key):
             if t >= 60000.0:
                 current[key][">60s"] += 1
@@ -536,7 +571,7 @@
                 current[key]["Over 1s"] += 1
             elif t >= 10000.0:
                 current[key]["Over 10s"] += 1
-        
+
         for bin in stats["T"].keys():
             current["T"][bin] += stats["T"][bin]
         current["T-MAX"] = max(current["T-MAX"], stats["T-MAX"])
@@ -547,10 +582,10 @@
 
 class SystemMonitor(object):
     """
-    Keeps track of system usage information. This installs a reacxtor task to
+    Keeps track of system usage information. This installs a reactor task to
     run about once per second and track system use.
     """
-    
+
     CPUStats = collections.namedtuple("CPUStats", ("total", "idle",))
 
     def __init__(self):
@@ -561,24 +596,26 @@
             "memory percent": 0.0,
             "start time"    : time.time(),
         }
-        
+
         if psutil is not None:
             times = psutil.cpu_times()
             self.previous_cpu = SystemMonitor.CPUStats(sum(times), times.idle,)
         else:
             self.previous_cpu = SystemMonitor.CPUStats(0, 0)
-        
+
         self.task = task.LoopingCall(self.update)
         self.task.start(1.0)
-    
+
+
     def stop(self):
         """
         Just stop the task
         """
         self.task.stop()
 
+
     def update(self):
-        
+
         # CPU usage based on diff'ing CPU times
         if psutil is not None:
             times = psutil.cpu_times()
@@ -588,18 +625,20 @@
             except ZeroDivisionError:
                 self.items["cpu use"] = 0.0
             self.previous_cpu = cpu_now
-        
+
         # Memory usage
         if psutil is not None:
             mem = psutil.virtual_memory()
             self.items["memory used"] = mem.used
             self.items["memory percent"] = mem.percent
 
-    
+
+
 class LogStats(amp.Command):
     arguments = [("message", amp.String())]
 
 
+
 class AMPCommonAccessLoggingObserver(CommonAccessLoggingObserverExtensions):
     def __init__(self):
         self.protocol = None
@@ -620,18 +659,18 @@
         self.flushBuffer()
 
 
-    def logStats(self, message): 
-        """ 
-        Log server stats via the remote AMP Protocol 
-        """ 
+    def logStats(self, message):
+        """
+        Log server stats via the remote AMP Protocol
+        """
 
         if self.protocol is not None:
-            message=json.dumps(message)
+            message = json.dumps(message)
             if isinstance(message, unicode):
                 message = message.encode("utf-8")
-            d = self.protocol.callRemote(LogStats, message=message) 
-            d.addErrback(log.err) 
-        else: 
+            d = self.protocol.callRemote(LogStats, message=message)
+            d.addErrback(log.err)
+        else:
             self._buffer.append(message)
 
 
@@ -646,10 +685,11 @@
 
         super(AMPLoggingProtocol, self).__init__()
 
-    def logStats(self, message): 
+
+    def logStats(self, message):
         stats = json.loads(message)
-        self.observer.logStats(stats) 
-        return {} 
+        self.observer.logStats(stats)
+        return {}
 
     LogStats.responder(logStats)
 
@@ -670,6 +710,3 @@
 
     def buildProtocol(self, addr):
         return AMPLoggingProtocol(self.observer)
-
-
-

Copied: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/logAnalysis.py (from rev 10159, CalendarServer/trunk/calendarserver/logAnalysis.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/logAnalysis.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/logAnalysis.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -0,0 +1,373 @@
+##
+# Copyright (c) 2012 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.
+##
+
+# Adjust method names
+
+# PROPFINDs
+METHOD_PROPFIND_CALENDAR_HOME = "PROPFIND Calendar Home"
+METHOD_PROPFIND_CACHED_CALENDAR_HOME = "PROPFIND cached Calendar Home"
+METHOD_PROPFIND_CALENDAR = "PROPFIND Calendar"
+METHOD_PROPFIND_INBOX = "PROPFIND Inbox"
+METHOD_PROPFIND_ADDRESSBOOK_HOME = "PROPFIND Adbk Home"
+METHOD_PROPFIND_CACHED_ADDRESSBOOK_HOME = "PROPFIND cached Adbk Home"
+METHOD_PROPFIND_ADDRESSBOOK = "PROPFIND Adbk"
+METHOD_PROPFIND_DIRECTORY = "PROPFIND Directory"
+METHOD_PROPFIND_PRINCIPALS = "PROPFIND Principals"
+METHOD_PROPFIND_CACHED_PRINCIPALS = "PROPFIND cached Principals"
+
+# PROPPATCHs
+METHOD_PROPPATCH_CALENDAR = "PROPPATCH Calendar"
+METHOD_PROPPATCH_ADDRESSBOOK = "PROPPATCH Adbk Home"
+
+# REPORTs
+METHOD_REPORT_CALENDAR_MULTIGET = "REPORT cal-multi"
+METHOD_REPORT_CALENDAR_QUERY = "REPORT cal-query"
+METHOD_REPORT_CALENDAR_FREEBUSY = "REPORT freebusy"
+METHOD_REPORT_CALENDAR_SYNC = "REPORT cal-sync"
+METHOD_REPORT_ADDRESSBOOK_MULTIGET = "REPORT adbk-multi"
+METHOD_REPORT_ADDRESSBOOK_QUERY = "REPORT adbk-query"
+METHOD_REPORT_DIRECTORY_QUERY = "REPORT dir-query"
+METHOD_REPORT_ADDRESSBOOK_SYNC = "REPORT adbk-sync"
+METHOD_REPORT_P_SEARCH_P_SET = "REPORT p-set"
+METHOD_REPORT_P_P_SEARCH = "REPORT p-search"
+METHOD_REPORT_EXPAND_P = "REPORT expand"
+
+# POSTs
+METHOD_POST_CALENDAR_HOME = "POST Calendar Home"
+METHOD_POST_CALENDAR = "POST Calendar"
+METHOD_POST_CALENDAR_OBJECT = "POST Calendar Object"
+METHOD_POST_ADDRESSBOOK_HOME = "POST Adbk Home"
+METHOD_POST_ADDRESSBOOK = "POST Adbk"
+METHOD_POST_ISCHEDULE_FREEBUSY = "POST Freebusy iSchedule"
+METHOD_POST_ISCHEDULE = "POST iSchedule"
+METHOD_POST_TIMEZONES = "POST Timezones"
+METHOD_POST_FREEBUSY = "POST Freebusy"
+METHOD_POST_ORGANIZER = "POST Organizer"
+METHOD_POST_ATTENDEE = "POST Attendee"
+METHOD_POST_OUTBOX = "POST Outbox"
+METHOD_POST_APNS = "POST apns"
+
+# PUTs
+METHOD_PUT_ICS = "PUT ics"
+METHOD_PUT_ORGANIZER = "PUT Organizer"
+METHOD_PUT_ATTENDEE = "PUT Attendee"
+METHOD_PUT_DROPBOX = "PUT dropbox"
+METHOD_PUT_VCF = "PUT VCF"
+
+# GETs
+METHOD_GET_CALENDAR_HOME = "GET Calendar Home"
+METHOD_GET_CALENDAR = "GET Calendar"
+METHOD_GET_ICS = "GET ics"
+METHOD_GET_INBOX_ICS = "GET inbox ics"
+METHOD_GET_DROPBOX = "GET dropbox"
+METHOD_GET_ADDRESSBOOK_HOME = "GET Adbk Home"
+METHOD_GET_ADDRESSBOOK = "GET Adbk"
+METHOD_GET_VCF = "GET VCF"
+METHOD_GET_TIMEZONES = "GET Timezones"
+
+# DELETEs
+METHOD_DELETE_CALENDAR_HOME = "DELETE Calendar Home"
+METHOD_DELETE_CALENDAR = "DELETE Calendar"
+METHOD_DELETE_ICS = "DELETE ics"
+METHOD_DELETE_INBOX_ICS = "DELETE inbox ics"
+METHOD_DELETE_DROPBOX = "DELETE dropbox"
+METHOD_DELETE_ADDRESSBOOK_HOME = "DELETE Adbk Home"
+METHOD_DELETE_ADDRESSBOOK = "DELETE Adbk"
+METHOD_DELETE_VCF = "DELETE vcf"
+
+
+def getAdjustedMethodName(stats):
+
+    method = stats["method"]
+    uribits = stats["uri"].rstrip("/").split('/')[1:]
+    if len(uribits) == 0:
+        uribits = [stats["uri"]]
+
+    calendar_specials = ("attachments", "dropbox", "notification", "freebusy", "outbox",)
+    adbk_specials = ("notification",)
+
+    def _PROPFIND():
+        cached = "cached" in stats
+
+        if uribits[0] == "calendars":
+
+            if len(uribits) == 3:
+                return METHOD_PROPFIND_CACHED_CALENDAR_HOME if cached else METHOD_PROPFIND_CALENDAR_HOME
+            elif len(uribits) > 3:
+                if uribits[3] in calendar_specials:
+                    return "PROPFIND %s" % (uribits[3],)
+                elif len(uribits) == 4:
+                    if uribits[3] == "inbox":
+                        return METHOD_PROPFIND_INBOX
+                    else:
+                        return METHOD_PROPFIND_CALENDAR
+
+        elif uribits[0] == "addressbooks":
+
+            if len(uribits) == 3:
+                return METHOD_PROPFIND_CACHED_ADDRESSBOOK_HOME if cached else METHOD_PROPFIND_ADDRESSBOOK_HOME
+            elif len(uribits) > 3:
+                if uribits[3] in adbk_specials:
+                    return "PROPFIND %s" % (uribits[3],)
+                elif len(uribits) == 4:
+                    return METHOD_PROPFIND_ADDRESSBOOK
+
+        elif uribits[0] == "directory":
+            return METHOD_PROPFIND_DIRECTORY
+
+        elif uribits[0] == "principals":
+            return METHOD_PROPFIND_CACHED_PRINCIPALS if cached else METHOD_PROPFIND_PRINCIPALS
+
+        return method
+
+
+    def _REPORT():
+
+        if "(" in method:
+            report_type = method.split("}" if "}" in method else ":")[1][:-1]
+            if report_type == "addressbook-query":
+                if uribits[0] == "directory":
+                    report_type = "directory-query"
+            if report_type == "sync-collection":
+                if uribits[0] == "calendars":
+                    report_type = "cal-sync"
+                elif uribits[0] == "addressbooks":
+                    report_type = "adbk-sync"
+            mappedNames = {
+                "calendar-multiget"             : METHOD_REPORT_CALENDAR_MULTIGET,
+                "calendar-query"                : METHOD_REPORT_CALENDAR_QUERY,
+                "free-busy-query"               : METHOD_REPORT_CALENDAR_FREEBUSY,
+                "cal-sync"                      : METHOD_REPORT_CALENDAR_SYNC,
+                "addressbook-multiget"          : METHOD_REPORT_ADDRESSBOOK_MULTIGET,
+                "addressbook-query"             : METHOD_REPORT_ADDRESSBOOK_QUERY,
+                "directory-query"               : METHOD_REPORT_DIRECTORY_QUERY,
+                "adbk-sync"                     : METHOD_REPORT_ADDRESSBOOK_SYNC,
+                "principal-search-property-set" : METHOD_REPORT_P_SEARCH_P_SET,
+                "principal-property-search"     : METHOD_REPORT_P_P_SEARCH,
+                "expand-property"               : METHOD_REPORT_EXPAND_P,
+            }
+            return mappedNames.get(report_type, "REPORT %s" % (report_type,))
+
+        return method
+
+
+    def _PROPPATCH():
+
+        if uribits[0] == "calendars":
+            return METHOD_PROPPATCH_CALENDAR
+        elif uribits[0] == "addressbooks":
+            return METHOD_PROPPATCH_ADDRESSBOOK
+
+        return method
+
+
+    def _POST():
+
+        if uribits[0] == "calendars":
+
+            if len(uribits) == 3:
+                return METHOD_POST_CALENDAR_HOME
+            elif len(uribits) == 4:
+                if uribits[3] == "outbox":
+                    if "recipients" in stats:
+                        return METHOD_POST_FREEBUSY
+                    elif "freebusy" in stats:
+                        return METHOD_POST_FREEBUSY
+                    elif "itip.request" in stats or "itip.cancel" in stats:
+                        return METHOD_POST_ORGANIZER
+                    elif "itip.reply" in stats:
+                        return METHOD_POST_ATTENDEE
+                    else:
+                        return METHOD_POST_OUTBOX
+                elif uribits[3] in calendar_specials:
+                    pass
+                else:
+                    return METHOD_POST_CALENDAR
+            elif len(uribits) == 5:
+                return METHOD_POST_CALENDAR_OBJECT
+
+        elif uribits[0] == "addressbooks":
+
+            if len(uribits) == 3:
+                return METHOD_POST_ADDRESSBOOK_HOME
+            elif len(uribits) == 4:
+                if uribits[3] in adbk_specials:
+                    pass
+                else:
+                    return METHOD_POST_ADDRESSBOOK
+
+        elif uribits[0] == "ischedule":
+            if "fb-cached" in stats or "fb-uncached" in stats or "freebusy" in stats:
+                return METHOD_POST_ISCHEDULE_FREEBUSY
+            else:
+                return METHOD_POST_ISCHEDULE
+
+        elif uribits[0].startswith("timezones"):
+            return METHOD_POST_TIMEZONES
+
+        elif uribits[0].startswith("apns"):
+            return METHOD_POST_APNS
+
+        return method
+
+
+    def _PUT():
+
+        if uribits[0] == "calendars":
+            if len(uribits) > 3:
+                if uribits[3] in calendar_specials:
+                    return "PUT %s" % (uribits[3],)
+                elif len(uribits) == 4:
+                    pass
+                else:
+                    if "itip.requests" in stats:
+                        return METHOD_PUT_ORGANIZER
+                    elif "itip.reply" in stats:
+                        return METHOD_PUT_ATTENDEE
+                    else:
+                        return METHOD_PUT_ICS
+
+        elif uribits[0] == "addressbooks":
+            if len(uribits) > 3:
+                if uribits[3] in adbk_specials:
+                    return "PUT %s" % (uribits[3],)
+                elif len(uribits) == 4:
+                    pass
+                else:
+                    return METHOD_PUT_VCF
+
+        return method
+
+
+    def _GET():
+
+        if uribits[0] == "calendars":
+
+            if len(uribits) == 3:
+                return METHOD_GET_CALENDAR_HOME
+            elif len(uribits) > 3:
+                if uribits[3] in calendar_specials:
+                    return "GET %s" % (uribits[3],)
+                elif len(uribits) == 4:
+                    return METHOD_GET_CALENDAR
+                elif uribits[3] == "inbox":
+                    return METHOD_GET_INBOX_ICS
+                else:
+                    return METHOD_GET_ICS
+
+        elif uribits[0] == "addressbooks":
+
+            if len(uribits) == 3:
+                return METHOD_GET_ADDRESSBOOK_HOME
+            elif len(uribits) > 3:
+                if uribits[3] in adbk_specials:
+                    return "GET %s" % (uribits[3],)
+                elif len(uribits) == 4:
+                    return METHOD_GET_ADDRESSBOOK
+                else:
+                    return METHOD_GET_VCF
+
+        elif uribits[0].startswith("timezones"):
+            return METHOD_GET_TIMEZONES
+
+        return method
+
+
+    def _DELETE():
+
+        if uribits[0] == "calendars":
+
+            if len(uribits) == 3:
+                return METHOD_DELETE_CALENDAR_HOME
+            elif len(uribits) > 3:
+                if uribits[3] in calendar_specials:
+                    return "DELETE %s" % (uribits[3],)
+                elif len(uribits) == 4:
+                    return METHOD_DELETE_CALENDAR
+                elif uribits[3] == "inbox":
+                    return METHOD_DELETE_INBOX_ICS
+                else:
+                    return METHOD_DELETE_ICS
+
+        elif uribits[0] == "addressbooks":
+
+            if len(uribits) == 3:
+                return METHOD_DELETE_ADDRESSBOOK_HOME
+            elif len(uribits) > 3:
+                if uribits[3] in adbk_specials:
+                    return "DELETE %s" % (uribits[3],)
+                elif len(uribits) == 4:
+                    return METHOD_DELETE_ADDRESSBOOK
+                else:
+                    return METHOD_DELETE_VCF
+
+        return method
+
+
+    def _ANY():
+        return method
+
+    return {
+        "DELETE" : _DELETE,
+        "GET" : _GET,
+        "POST" : _POST,
+        "PROPFIND" : _PROPFIND,
+        "PROPPATCH" : _PROPPATCH,
+        "PUT" : _PUT,
+        "REPORT" : _REPORT,
+    }.get(method.split("(")[0], _ANY)()
+
+
+
+versionClients = (
+    "Mac OS X/",
+    "iOS/",
+    "iCal/",
+    "iPhone/",
+    "CalendarAgent",
+    "Calendar/",
+    "CoreDAV/",
+    "Safari/",
+    "dataaccessd",
+    "curl/",
+    "DAVKit",
+)
+
+quickclients = (
+    ("InterMapper/", "InterMapper"),
+    ("CardDAVPlugin/", "CardDAVPlugin"),
+    ("Address%20Book/", "AddressBook"),
+    ("AddressBook/", "AddressBook"),
+    ("Mail/", "Mail"),
+    ("iChat/", "iChat"),
+)
+
+def getAdjustedClientName(stats):
+
+    userAgent = stats["userAgent"]
+    for client in versionClients:
+        index = userAgent.find(client)
+        if index != -1:
+            l = len(client)
+            endex = userAgent[index + l:].find(' ', index)
+            return userAgent[index:] if endex == -1 else userAgent[index:endex + l]
+
+    for quick, result in quickclients:
+        index = userAgent.find(quick)
+        if index != -1:
+            return result
+
+    return userAgent[:20]

Copied: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/test/test_methodDescriptor.py (from rev 10159, CalendarServer/trunk/calendarserver/test/test_methodDescriptor.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/test/test_methodDescriptor.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/test/test_methodDescriptor.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -0,0 +1,67 @@
+##
+# Copyright (c) 2012 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.
+##
+
+from twisted.trial.unittest import TestCase
+from calendarserver.methodDescriptor import getAdjustedMethodName
+
+class MethodDescriptor(TestCase):
+    """
+    Tests for L{getAdjustedMethodName}.
+    """
+    def test_getAdjustedMethodName(self):
+        """
+        L{getAdjustedMethodName} returns the appropriate method.
+        """
+
+        data = (
+            ("PROPFIND", "/calendars/users/user01/", {}, "PROPFIND Calendar Home",),
+            ("PROPFIND", "/calendars/users/user01/", {"cached": "1"}, "PROPFIND cached Calendar Home",),
+            ("PROPFIND", "/calendars/users/user01/ABC/", {}, "PROPFIND Calendar",),
+            ("PROPFIND", "/calendars/users/user01/inbox/", {}, "PROPFIND Inbox",),
+            ("PROPFIND", "/addressbooks/users/user01/", {}, "PROPFIND Adbk Home",),
+            ("PROPFIND", "/addressbooks/users/user01/", {"cached": "1"}, "PROPFIND cached Adbk Home",),
+            ("PROPFIND", "/addressbooks/users/user01/ABC/", {}, "PROPFIND Adbk",),
+            ("PROPFIND", "/addressbooks/users/user01/inbox/", {}, "PROPFIND Adbk",),
+            ("PROPFIND", "/principals/users/user01/", {}, "PROPFIND Principals",),
+            ("PROPFIND", "/principals/users/user01/", {"cached": "1"}, "PROPFIND cached Principals",),
+            ("PROPFIND", "/.well-known/caldav", {}, "PROPFIND",),
+
+            ("REPORT(CalDAV:sync-collection)", "/calendars/users/user01/ABC/", {}, "REPORT cal-sync",),
+            ("REPORT(CalDAV:calendar-query)", "/calendars/users/user01/ABC/", {}, "REPORT cal-query",),
+
+            ("POST", "/calendars/users/user01/", {}, "POST Calendar Home",),
+            ("POST", "/calendars/users/user01/outbox/", {"recipients": "1"}, "POST Freebusy",),
+            ("POST", "/calendars/users/user01/outbox/", {}, "POST Outbox",),
+            ("POST", "/apns", {}, "POST apns",),
+
+            ("PUT", "/calendars/users/user01/calendar/1.ics", {}, "PUT ics",),
+            ("PUT", "/calendars/users/user01/calendar/1.ics", {"itip.requests": "1"}, "PUT Organizer",),
+            ("PUT", "/calendars/users/user01/calendar/1.ics", {"itip.reply": "1"}, "PUT Attendee",),
+
+            ("GET", "/calendars/users/user01/", {}, "GET Calendar Home",),
+            ("GET", "/calendars/users/user01/calendar/", {}, "GET Calendar",),
+            ("GET", "/calendars/users/user01/calendar/1.ics", {}, "GET ics",),
+
+            ("DELETE", "/calendars/users/user01/", {}, "DELETE Calendar Home",),
+            ("DELETE", "/calendars/users/user01/calendar/", {}, "DELETE Calendar",),
+            ("DELETE", "/calendars/users/user01/calendar/1.ics", {}, "DELETE ics",),
+            ("DELETE", "/calendars/users/user01/inbox/1.ics", {}, "DELETE inbox ics",),
+
+            ("ACL", "/calendars/users/user01/", {}, "ACL",),
+        )
+
+        for method, uri, extras, result in data:
+            self.assertEqual(getAdjustedMethodName(method, uri, extras), result, "Failed getAdjustedMethodName: %s" % (result,))

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/purge.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/purge.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -44,7 +44,10 @@
 
 from calendarserver.tools.cmdline import utilityMain
 from calendarserver.tools.principals import removeProxy
+from calendarserver.tools import tables
 
+import collections
+
 log = Logger()
 
 DEFAULT_BATCH_SIZE = 100
@@ -61,7 +64,7 @@
     print "  -h --help: print this help and exit"
     print "  -f --config <path>: Specify caldavd.plist configuration path"
     print "  -d --days <number>: specify how many days in the past to retain (default=%d)" % (DEFAULT_RETAIN_DAYS,)
-   #print "  -b --batch <number>: number of events to remove in each transaction (default=%d)" % (DEFAULT_BATCH_SIZE,)
+    #print "  -b --batch <number>: number of events to remove in each transaction (default=%d)" % (DEFAULT_BATCH_SIZE,)
     print "  -n --dry-run: calculate how many events to purge, but do not purge data"
     print "  -v --verbose: print progress information"
     print ""
@@ -72,6 +75,8 @@
     else:
         sys.exit(0)
 
+
+
 def usage_purge_orphaned_attachments(e=None):
 
     name = os.path.basename(sys.argv[0])
@@ -82,7 +87,7 @@
     print "options:"
     print "  -h --help: print this help and exit"
     print "  -f --config <path>: Specify caldavd.plist configuration path"
-   #print "  -b --batch <number>: number of attachments to remove in each transaction (default=%d)" % (DEFAULT_BATCH_SIZE,)
+    #print "  -b --batch <number>: number of attachments to remove in each transaction (default=%d)" % (DEFAULT_BATCH_SIZE,)
     print "  -n --dry-run: calculate how many attachments to purge, but do not purge data"
     print "  -v --verbose: print progress information"
     print ""
@@ -93,6 +98,8 @@
     else:
         sys.exit(0)
 
+
+
 def usage_purge_principal(e=None):
 
     name = os.path.basename(sys.argv[0])
@@ -115,11 +122,13 @@
         sys.exit(0)
 
 
+
 class WorkerService(Service):
 
     def __init__(self, store):
         self._store = store
 
+
     def rootResource(self):
         try:
             rootResource = getRootResource(config, self._store)
@@ -209,7 +218,6 @@
 
 
 
-
 def main_purge_events():
 
     try:
@@ -285,6 +293,7 @@
     )
 
 
+
 def main_purge_orphaned_attachments():
 
     try:
@@ -347,6 +356,7 @@
     )
 
 
+
 def main_purge_principals():
 
     try:
@@ -401,13 +411,13 @@
     PurgePrincipalService.verbose = verbose
     PurgePrincipalService.doimplicit = doimplicit
 
-
     utilityMain(
         configFileName,
         PurgePrincipalService
     )
 
 
+
 @inlineCallbacks
 def purgeOldEvents(store, directory, root, date, batchSize, verbose=False,
     dryrun=False):
@@ -462,16 +472,42 @@
             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" % (orphanCount,)
-        returnValue(orphanCount)
+            # Print aggregate details by user
+            byuser = collections.defaultdict(int)
+            for owner_uid, _ignore_dropbox_id, _ignore_path, size in orphans:
+                byuser[owner_uid] += size
 
+            # Print table of results
+            table = tables.Table()
+            table.addHeader(("User", "Current Quota", "Orphaned Size", "Orphaned Count"))
+            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),
+                )
+            )
+            total = 0
+            for user, quota, size, count in sorted(orphans):
+                table.addRow((
+                    user,
+                    quota,
+                    size,
+                    count,
+                ))
+                total += count
+            table.addFooter(("Total:", "", "", total))
+
+            print "\n"
+            print "Orphaned Attachments by User:\n"
+            table.printTable()
+        else:
+            total = sum([x[3] for x in orphans])
+
+        returnValue(total)
+
     if verbose:
         print "Removing orphaned attachments..."
 
@@ -499,14 +535,12 @@
 
 
 
-
-
 @inlineCallbacks
 def purgeUIDs(store, directory, root, uids, verbose=False, dryrun=False,
     completely=False, doimplicit=True):
     total = 0
 
-    allAssignments = { }
+    allAssignments = {}
 
     for uid in uids:
         count, allAssignments[uid] = (yield purgeUID(store, uid, directory, root,
@@ -607,7 +641,6 @@
                     main.removeProperty(exdate_rdate)
                     dirty = True
 
-
     # Remove any overridden components beyond the cutoff
     for component in tuple(event.subcomponents()):
         if component.name() == "VEVENT":
@@ -625,6 +658,7 @@
         return CANCELEVENT_NOT_MODIFIED
 
 
+
 @inlineCallbacks
 def purgeUID(store, uid, directory, root, verbose=False, dryrun=False, proxies=True,
     when=None, completely=False, doimplicit=True):
@@ -686,7 +720,7 @@
 
     # Anything in the past is left alone
     whenString = when.getText()
-    filter =  caldavxml.Filter(
+    filter = caldavxml.Filter(
           caldavxml.ComponentFilter(
               caldavxml.ComponentFilter(
                   TimeRange(start=whenString,),
@@ -717,7 +751,7 @@
                             childNames.append(childName)
                     else:
                         # events matching filter
-                        for childName, childUid, childType in (yield collection.index().indexedSearch(filter)):
+                        for childName, _ignore_childUid, _ignore_childType in (yield collection.index().indexedSearch(filter)):
                             childNames.append(childName)
 
                     for childName in childNames:
@@ -791,7 +825,6 @@
                             if incrementCount:
                                 count += 1
 
-
         txn = getattr(request, "_newStoreTransaction", None)
         # Commit
         if txn is not None:
@@ -834,12 +867,11 @@
                 if not dryrun:
                     (yield storeCalHome.remove())
 
-
         # Remove VCards
         storeAbHome = (yield txn.addressbookHomeWithUID(uid))
         if storeAbHome is not None:
-            for abColl in list( (yield storeAbHome.addressbooks()) ):
-                for card in list( (yield abColl.addressbookObjects()) ):
+            for abColl in list((yield storeAbHome.addressbooks())):
+                for card in list((yield abColl.addressbookObjects())):
                     cardName = card.name()
                     if verbose:
                         uri = "/addressbooks/__uids__/%s/%s/%s" % (uid, abColl.name(), cardName)
@@ -884,6 +916,7 @@
     returnValue((count, assignments))
 
 
+
 @inlineCallbacks
 def purgeProxyAssignments(principal):
 
@@ -904,4 +937,3 @@
         (yield subPrincipal.writeProperty(davxml.GroupMemberSet(), None))
 
     returnValue(assignments)
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge_old_events.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge_old_events.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/tools/test/test_purge_old_events.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -84,7 +84,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 +134,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 +183,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 +232,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 +267,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 +301,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 +421,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))
@@ -470,6 +470,7 @@
         # Just look for orphaned attachments but don't delete
         orphans = (yield txn.orphanedAttachments())
         self.assertEquals(len(orphans), 1)
+        self.assertEquals(orphans, [["home1", 19, 19, 1]])
 
         # Remove orphaned attachments, should be 1
         count = (yield txn.removeOrphanedAttachments(batchSize=100))
@@ -515,12 +516,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 +541,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 +553,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())
@@ -601,4 +602,3 @@
         total = (yield purgeOrphanedAttachments(self._sqlCalendarStore, 2,
             dryrun=False, verbose=False))
         self.assertEquals(total, 0)
-

Modified: CalendarServer/branches/users/gaya/sharedgroups/calendarserver/webcal/resource.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/calendarserver/webcal/resource.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/calendarserver/webcal/resource.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -40,7 +40,7 @@
 
 from twisted.internet.defer import succeed
 
-from calendarserver.platform.darwin.timezone import getLocalTimezone
+from twext.python.timezone import getLocalTimezone
 
 
 class WebCalendarResource (ReadOnlyResourceMixIn, DAVFile):

Modified: CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/conf/caldavd-test.plist	2012-12-12 22:11:41 UTC (rev 10162)
@@ -901,6 +901,10 @@
     <key>EnableDropBox</key>
     <true/>
 
+    <!-- Calendar Managed Attachments -->
+    <key>EnableManagedAttachments</key>
+    <true/>
+
     <!-- Private Events -->
     <key>EnablePrivateEvents</key>
     <true/>

Modified: CalendarServer/branches/users/gaya/sharedgroups/contrib/tools/readStats.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/contrib/tools/readStats.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/contrib/tools/readStats.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -54,23 +54,23 @@
 
 
 
-def printStats(stats, multimode, showMethods, topUsers):
+def printStats(stats, multimode, showMethods, topUsers, showAgents):
     if len(stats) == 1:
         if "Failed" in stats[0]:
             printFailedStats(stats[0]["Failed"])
         else:
             try:
-                printStat(stats[0], multimode[0], showMethods, topUsers)
+                printStat(stats[0], multimode[0], showMethods, topUsers, showAgents)
             except KeyError, e:
                 printFailedStats("Unable to find key '%s' in statistics from server socket" % (e,))
                 sys.exit(1)
 
     else:
-        printMultipleStats(stats, multimode, showMethods, topUsers)
+        printMultipleStats(stats, multimode, showMethods, topUsers, showAgents)
 
 
 
-def printStat(stats, index, showMethods, topUsers):
+def printStat(stats, index, showMethods, topUsers, showAgents):
 
     print "- " * 40
     print "Server: %s" % (stats["Server"],)
@@ -91,15 +91,17 @@
         print "Current Memory Used: Unavailable"
     print
     printRequestSummary(stats)
-    printHistogramSummary(stats[index])
+    printHistogramSummary(stats[index], index)
     if showMethods:
         printMethodCounts(stats[index])
     if topUsers:
         printUserCounts(stats[index], topUsers)
+    if showAgents:
+        printAgentCounts(stats[index])
 
 
 
-def printMultipleStats(stats, multimode, showMethods, topUsers):
+def printMultipleStats(stats, multimode, showMethods, topUsers, showAgents):
 
     labels = serverLabels(stats)
 
@@ -130,6 +132,8 @@
         printMultiMethodCounts(stats, multimode[0])
     if topUsers:
         printMultiUserCounts(stats, multimode[0], topUsers)
+    if showAgents:
+        printMultiAgentCounts(stats, multimode[0])
 
 
 
@@ -274,9 +278,9 @@
 
 
 
-def printHistogramSummary(stat):
+def printHistogramSummary(stat, index):
 
-    print "5 minute average response histogram"
+    print "%s average response histogram" % (index,)
     table = tables.Table()
     table.addHeader(
         ("", "<10ms", "10ms<->100ms", "100ms<->1s", "1s<->10s", "10s<->30s", "30s<->60s", ">60s", "Over 1s", "Over 10s"),
@@ -329,41 +333,7 @@
             for k in keys[1:]:
                 totals[i][k] += stat[index][i][k]
 
-    print "%s average response histogram" % (index,)
-    table = tables.Table()
-    table.addHeader(
-        ("", "<10ms", "10ms<->100ms", "100ms<->1s", "1s<->10s", "10s<->30s", "30s<->60s", ">60s", "Over 1s", "Over 10s"),
-    )
-    table.setDefaultColumnFormats(
-       (
-            tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
-            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            tables.Table.ColumnFormat("%d (%.1f%%)", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-        )
-    )
-    for i in ("T", "T-RESP-WR",):
-        table.addRow((
-            "Overall Response" if i == "T" else "Response Write",
-            (totals[i]["<10ms"], safeDivision(totals[i]["<10ms"], totals[i]["requests"], 100.0)),
-            (totals[i]["10ms<->100ms"], safeDivision(totals[i]["10ms<->100ms"], totals[i]["requests"], 100.0)),
-            (totals[i]["100ms<->1s"], safeDivision(totals[i]["100ms<->1s"], totals[i]["requests"], 100.0)),
-            (totals[i]["1s<->10s"], safeDivision(totals[i]["1s<->10s"], totals[i]["requests"], 100.0)),
-            (totals[i]["10s<->30s"], safeDivision(totals[i]["10s<->30s"], totals[i]["requests"], 100.0)),
-            (totals[i]["30s<->60s"], safeDivision(totals[i]["30s<->60s"], totals[i]["requests"], 100.0)),
-            (totals[i][">60s"], safeDivision(totals[i][">60s"], totals[i]["requests"], 100.0)),
-            safeDivision(totals[i]["Over 1s"], totals[i]["requests"], 100.0),
-            safeDivision(totals[i]["Over 10s"], totals[i]["requests"], 100.0),
-        ))
-    os = StringIO()
-    table.printTable(os=os)
-    print os.getvalue()
+    printHistogramSummary(totals, index)
 
 
 
@@ -372,22 +342,38 @@
     print "Method Counts"
     table = tables.Table()
     table.addHeader(
-        ("Method", "Total", "Percentage"),
+        ("Method", "Count", "%", "Av. Response", "%", "Total Resp. %"),
     )
+    table.addHeader(
+        ("", "", "", "(ms)", "", ""),
+    )
     table.setDefaultColumnFormats(
        (
             tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
             tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
             tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
         )
     )
 
-    total = sum(stat["method"].values())
+    response_average = {}
+    for method in stat["method"].keys():
+        response_average[method] = stat["method-t"][method] / stat["method"][method]
+
+    total_count = sum(stat["method"].values())
+    total_avresponse = sum(response_average.values())
+    total_response = sum(stat["method-t"].values())
+
     for method in sorted(stat["method"].keys()):
         table.addRow((
             method,
             stat["method"][method],
-            safeDivision(stat["method"][method], total, 100.0),
+            safeDivision(stat["method"][method], total_count, 100.0),
+            response_average[method],
+            safeDivision(response_average[method], total_avresponse, 100.0),
+            safeDivision(stat["method-t"][method], total_response, 100.0),
         ))
     os = StringIO()
     table.printTable(os=os)
@@ -398,36 +384,17 @@
 def printMultiMethodCounts(stats, index):
 
     methods = collections.defaultdict(int)
+    method_times = collections.defaultdict(float)
     for stat in stats:
         for method in stat[index]["method"]:
             methods[method] += stat[index]["method"][method]
+        for method_time in stat[index]["method-t"]:
+            method_times[method_time] += stat[index]["method-t"][method_time]
 
-    print "Method Counts"
-    table = tables.Table()
-    table.addHeader(
-        ("Method", "Total", "Percentage"),
-    )
-    table.setDefaultColumnFormats(
-       (
-            tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
-            tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-        )
-    )
+    printMethodCounts({"method": methods, "method-t": method_times})
 
-    total = sum(methods.values())
-    for method in sorted(methods.keys()):
-        table.addRow((
-            method,
-            methods[method],
-            safeDivision(methods[method], total, 100.0),
-        ))
-    os = StringIO()
-    table.printTable(os=os)
-    print os.getvalue()
 
 
-
 def printUserCounts(stat, topUsers):
 
     print "User Counts"
@@ -444,11 +411,11 @@
     )
 
     total = sum(stat["uid"].values())
-    for uid in sorted(stat["uid"].items(), key=lambda x: x[1], reverse=True)[:topUsers]:
+    for uid, value in sorted(stat["uid"].items(), key=lambda x: x[1], reverse=True)[:topUsers]:
         table.addRow((
             uid,
-            stat["uid"][uid],
-            safeDivision(stat["uid"][uid], total, 100.0),
+            value,
+            safeDivision(value, total, 100.0),
         ))
     os = StringIO()
     table.printTable(os=os)
@@ -463,25 +430,31 @@
         for uid in stat[index]["uid"]:
             uids[uid] += stat[index]["uid"][uid]
 
-    print "User Counts"
+    printUserCounts({"uid": uids}, topUsers)
+
+
+
+def printAgentCounts(stat):
+
+    print "User-Agent Counts"
     table = tables.Table()
     table.addHeader(
-        ("User", "Total", "Percentage"),
+        ("User-Agent", "Total", "Percentage"),
     )
     table.setDefaultColumnFormats(
-        (
+       (
             tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
             tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
             tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
         )
     )
 
-    total = sum(uids.values())
-    for uid, count in sorted(uids.items(), key=lambda x: x[1], reverse=True)[:topUsers]:
+    total = sum(stat["user-agent"].values())
+    for ua in sorted(stat["user-agent"].keys()):
         table.addRow((
-            uid,
-            count,
-            safeDivision(count, total, 100.0),
+            ua,
+            stat["user-agent"][ua],
+            safeDivision(stat["user-agent"][ua], total, 100.0),
         ))
     os = StringIO()
     table.printTable(os=os)
@@ -489,6 +462,17 @@
 
 
 
+def printMultiAgentCounts(stats, index):
+
+    uas = collections.defaultdict(int)
+    for stat in stats:
+        for ua in stat[index]["user-agent"]:
+            uas[ua] += stat[index]["user-agent"][ua]
+
+    printUserCounts({"user-agent": uas})
+
+
+
 def usage(error_msg=None):
     if error_msg:
         print error_msg
@@ -505,6 +489,7 @@
     --60          Display multiserver 1 hour average
     --methods     Include details about HTTP method usage
     --users N     Include details about top N users
+    --agents      Include details about HTTP User-Agent usage
 
 Description:
     This utility will print a summary of statistics read from a
@@ -525,11 +510,12 @@
     useTCP = False
     showMethods = False
     topUsers = 0
+    showAgents = False
 
     multimodes = (("Current", 60,), ("1 Minute", 60,), ("5 Minutes", 5 * 60,), ("1 Hour", 60 * 60,),)
     multimode = multimodes[2]
 
-    options, args = getopt.getopt(sys.argv[1:], "hs:t:", ["tcp=", "0", "1", "5", "60", "methods", "users="])
+    options, args = getopt.getopt(sys.argv[1:], "hs:t:", ["tcp=", "0", "1", "5", "60", "methods", "users=", "agents"])
 
     for option, value in options:
         if option == "-h":
@@ -553,7 +539,9 @@
             showMethods = True
         elif option == "--users":
             topUsers = int(value)
+        elif option == "--agents":
+            showAgents = True
 
     while True:
-        printStats([readSock(server, useTCP) for server in servers], multimode, showMethods, topUsers)
+        printStats([readSock(server, useTCP) for server in servers], multimode, showMethods, topUsers, showAgents)
         time.sleep(delay)

Modified: CalendarServer/branches/users/gaya/sharedgroups/support/build.sh
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/support/build.sh	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/support/build.sh	2012-12-12 22:11:41 UTC (rev 10162)
@@ -254,6 +254,8 @@
       }
 
       if [ ! -f "${cache_file}" ]; then
+        echo "No cache file: ${cache_file}";
+
         echo "Downloading ${name}...";
 
         local pkg_host="static.calendarserver.org";

Modified: CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/dal/syntax.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/dal/syntax.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/enterprise/dal/syntax.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -41,6 +41,8 @@
 except ImportError:
     cx_Oracle = None
 
+
+
 class DALError(Exception):
     """
     Base class for exceptions raised by this module.  This can be raised
@@ -327,8 +329,8 @@
     # 0'.)
     __add__ = comparison("+")
     __sub__ = comparison("-")
-    __div__= comparison("/")
-    __mul__= comparison("*")
+    __div__ = comparison("/")
+    __mul__ = comparison("*")
 
 
     def __nonzero__(self):
@@ -365,6 +367,8 @@
     def Contains(self, other):
         return CompoundComparison(self, "like", CompoundComparison(Constant('%'), '||', CompoundComparison(Constant(other), '||', Constant('%'))))
 
+
+
 class FunctionInvocation(ExpressionSyntax):
     def __init__(self, function, *args):
         self.function = function
@@ -453,6 +457,7 @@
 
 
 Count = Function("count")
+Sum = Function("sum")
 Max = Function("max")
 Len = Function("character_length", "length")
 Upper = Function("upper")
@@ -780,23 +785,28 @@
         return self.model.table.name + '.' + name
 
 
+
 class ResultAliasSyntax(ExpressionSyntax):
 
     def __init__(self, expression, alias=None):
         self.expression = expression
         self.alias = alias
 
+
     def aliasName(self, queryGenerator):
         if self.alias is None:
             self.alias = queryGenerator.nextGeneratedID()
         return self.alias
 
+
     def columnReference(self):
         return AliasReferenceSyntax(self)
 
+
     def allColumns(self):
         return self.expression.allColumns()
 
+
     def subSQL(self, queryGenerator, allTables):
         result = SQLFragment()
         result.append(self.expression.subSQL(queryGenerator, allTables))
@@ -804,18 +814,22 @@
         return result
 
 
+
 class AliasReferenceSyntax(ExpressionSyntax):
 
     def __init__(self, resultAlias):
         self.resultAlias = resultAlias
 
+
     def allColumns(self):
         return self.resultAlias.allColumns()
 
+
     def subSQL(self, queryGenerator, allTables):
         return SQLFragment(self.resultAlias.aliasName(queryGenerator))
 
 
+
 class AliasedColumnSyntax(ColumnSyntax):
     """
     An L{AliasedColumnSyntax} is like a L{ColumnSyntax}, but it generates SQL
@@ -898,9 +912,9 @@
 
 
     def subSQL(self, queryGenerator, allTables):
-        if ( queryGenerator.dialect == ORACLE_DIALECT
+        if (queryGenerator.dialect == ORACLE_DIALECT
              and isinstance(self.b, Constant) and self.b.value == ''
-             and self.op in ('=', '!=') ):
+             and self.op in ('=', '!=')):
             return NullComparison(self.a, self.op).subSQL(queryGenerator, allTables)
         stmt = SQLFragment()
         result = self._subexpression(self.a, queryGenerator, allTables)
@@ -948,6 +962,7 @@
     def __init__(self):
         self.name = "*"
 
+
     def allColumns(self):
         return []
 
@@ -1024,6 +1039,7 @@
         return self.columns
 
 
+
 class SetExpression(object):
     """
     A UNION, INTERSECT, or EXCEPT construct used inside a SELECT.
@@ -1052,6 +1068,7 @@
         if self.optype not in (None, SetExpression.OPTYPE_ALL, SetExpression.OPTYPE_DISTINCT,):
             raise DALError("Must have either 'all' or 'distinct' in a set expression")
 
+
     def subSQL(self, queryGenerator, allTables):
         result = SQLFragment()
         for select in self.selects:
@@ -1063,9 +1080,12 @@
             result.append(select.subSQL(queryGenerator, allTables))
         return result
 
+
     def allColumns(self):
         return []
 
+
+
 class Union(SetExpression):
     """
     A UNION construct used inside a SELECT.
@@ -1073,6 +1093,8 @@
     def setOpSQL(self, queryGenerator):
         return SQLFragment(" UNION ")
 
+
+
 class Intersect(SetExpression):
     """
     An INTERSECT construct used inside a SELECT.
@@ -1080,6 +1102,8 @@
     def setOpSQL(self, queryGenerator):
         return SQLFragment(" INTERSECT ")
 
+
+
 class Except(SetExpression):
     """
     An EXCEPT construct used inside a SELECT.
@@ -1092,6 +1116,8 @@
         else:
             raise NotImplementedError("Unsupported dialect")
 
+
+
 class Select(_Statement):
     """
     'select' statement.
@@ -1131,6 +1157,7 @@
             if self.From.As is None:
                 self.From.As = ""
 
+
     def __eq__(self, other):
         """
         Create a comparison.
@@ -1238,6 +1265,7 @@
             for column in self.columns.columns:
                 yield column
 
+
     def tables(self):
         """
         Determine the tables used by the result columns.
@@ -1255,6 +1283,7 @@
             return [TableSyntax(table) for table in tables]
 
 
+
 def _commaJoined(stmts):
     first = True
     cstatement = SQLFragment()
@@ -1663,6 +1692,7 @@
         return SQLFragment('savepoint %s' % (self.name,))
 
 
+
 class RollbackToSavepoint(_LockingStatement):
     """
     An SQL 'rollback to savepoint' statement.
@@ -1676,6 +1706,7 @@
         return SQLFragment('rollback to savepoint %s' % (self.name,))
 
 
+
 class ReleaseSavepoint(_LockingStatement):
     """
     An SQL 'release savepoint' statement.
@@ -1821,4 +1852,3 @@
 # (Although this is a special keyword in a CREATE statement, in an INSERT it
 # behaves like an expression to the best of my knowledge.)
 default = NamedValue('default')
-

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/python/test/test_timezone.py (from rev 10159, CalendarServer/trunk/twext/python/test/test_timezone.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/python/test/test_timezone.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/python/test/test_timezone.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -0,0 +1,65 @@
+##
+# Copyright (c) 2012 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.
+##
+
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.config import config
+import twext.python.timezone
+import twistedcaldav.timezones
+from twext.python.timezone import getLocalTimezone, DEFAULT_TIMEZONE
+
+class DefaultTimezoneTests(TestCase):
+
+    def stubLookup(self):
+        return self._storedLookup
+
+    def stubHasTZ(self, ignored):
+        return self._storedHasTZ.pop()
+
+    def setUp(self):
+        self.patch(twext.python.timezone, "lookupSystemTimezone", self.stubLookup)
+        self.patch(twistedcaldav.timezones,
+            "hasTZ", self.stubHasTZ)
+
+    def test_getLocalTimezone(self):
+
+        # Empty config, system timezone known = use system timezone
+        self.patch(config, "DefaultTimezone", "")
+        self._storedLookup = "America/New_York"
+        self._storedHasTZ = [True]
+        self.assertEquals(getLocalTimezone(), "America/New_York")
+
+        # Empty config, system timezone unknown = use DEFAULT_TIMEZONE
+        self.patch(config, "DefaultTimezone", "")
+        self._storedLookup = "Unknown/Unknown"
+        self._storedHasTZ = [False]
+        self.assertEquals(getLocalTimezone(), DEFAULT_TIMEZONE)
+
+        # Known config value = use config value
+        self.patch(config, "DefaultTimezone", "America/New_York")
+        self._storedHasTZ = [True]
+        self.assertEquals(getLocalTimezone(), "America/New_York")
+
+        # Unknown config value, system timezone known = use system timezone
+        self.patch(config, "DefaultTimezone", "Unknown/Unknown")
+        self._storedLookup = "America/New_York"
+        self._storedHasTZ = [True, False]
+        self.assertEquals(getLocalTimezone(), "America/New_York")
+
+        # Unknown config value, system timezone unknown = use DEFAULT_TIMEZONE
+        self.patch(config, "DefaultTimezone", "Unknown/Unknown")
+        self._storedLookup = "Unknown/Unknown"
+        self._storedHasTZ = [False, False]
+        self.assertEquals(getLocalTimezone(), DEFAULT_TIMEZONE)

Copied: CalendarServer/branches/users/gaya/sharedgroups/twext/python/timezone.py (from rev 10159, CalendarServer/trunk/twext/python/timezone.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twext/python/timezone.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/twext/python/timezone.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -0,0 +1,51 @@
+##
+# Copyright (c) 2012 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.
+##
+
+
+from twistedcaldav.config import config
+import twistedcaldav.timezones
+
+DEFAULT_TIMEZONE = "America/Los_Angeles"
+
+try:
+    from Foundation import NSTimeZone
+    def lookupSystemTimezone():
+        return NSTimeZone.localTimeZone().name().encode("utf-8")
+
+except:
+    def lookupSystemTimezone():
+        return ""
+
+
+def getLocalTimezone():
+    """
+    Returns the default timezone for the server.  The order of precedence is:
+    config.DefaultTimezone, lookupSystemTimezone( ), DEFAULT_TIMEZONE.
+    Also, if neither of the first two values in that list are in the timezone
+    database, DEFAULT_TIMEZONE is returned.
+
+    @return: The server's local timezone name
+    @rtype: C{str}
+    """
+    if config.DefaultTimezone:
+        if twistedcaldav.timezones.hasTZ(config.DefaultTimezone):
+            return config.DefaultTimezone
+
+    systemTimezone = lookupSystemTimezone()
+    if twistedcaldav.timezones.hasTZ(systemTimezone):
+        return systemTimezone
+
+    return DEFAULT_TIMEZONE

Modified: CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/twistedcaldav/stdconfig.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -525,7 +525,7 @@
     "EnableWellKnown"             : True, # /.well-known resource
     "EnableCalendarQueryExtended" : True, # Extended calendar-query REPORT
 
-    "EnableManagedAttachments"    : True, # Support Managed Attachments
+    "EnableManagedAttachments"    : False, # Support Managed Attachments
 
     #
     # Non-standard CalDAV extensions

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/file.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/file.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -89,7 +89,6 @@
 
         self._childClass = Calendar
 
-
     createCalendarWithName = CommonHome.createChildWithName
     removeCalendarWithName = CommonHome.removeChildWithName
 
@@ -139,6 +138,7 @@
 
         returnValue(False)
 
+
     @inlineCallbacks
     def getCalendarResourcesForUID(self, uid, allow_shared=False):
 
@@ -150,6 +150,7 @@
 
         returnValue(results)
 
+
     @inlineCallbacks
     def calendarObjectWithDropboxID(self, dropboxID):
         """
@@ -197,6 +198,7 @@
 
         self.createCalendarWithName("inbox")
 
+
     def ensureDefaultCalendarsExist(self):
         """
         Double check that we have calendars supporting at least VEVENT and VTODO,
@@ -225,6 +227,8 @@
             _requireCalendarWithType("VEVENT", "calendar")
             _requireCalendarWithType("VTODO", "tasks")
 
+
+
 class Calendar(CommonHomeChild):
     """
     File-based implementation of L{ICalendar}.
@@ -289,10 +293,12 @@
         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:
@@ -300,6 +306,7 @@
         else:
             return True
 
+
     def initPropertyStore(self, props):
         # Setup peruser special properties
         props.setSpecialProperties(
@@ -313,12 +320,14 @@
             ),
         )
 
+
     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
@@ -329,22 +338,24 @@
         # 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}
         """
@@ -352,6 +363,7 @@
         # 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.
@@ -360,6 +372,7 @@
         # TODO: implement this for filestore
         pass
 
+
     def _transferCalendarObjects(self, newcalendar, component):
         """
         Move all calendar components of the specified type to the specified calendar.
@@ -522,19 +535,23 @@
         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
@@ -544,9 +561,11 @@
         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:
@@ -566,6 +585,7 @@
             prop = str(prop) == "true"
         return prop
 
+
     def _set_isScheduleObject(self, value):
         pname = PropertyName.fromElement(customxml.TwistedSchedulingObjectResource)
         if value is not None:
@@ -578,6 +598,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:
@@ -590,6 +611,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]
@@ -605,6 +627,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:
@@ -722,6 +745,7 @@
             ),
         )
 
+
     # IDataStoreObject
     def contentType(self):
         """

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_sql.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/caldav/datastore/test/test_sql.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -63,8 +63,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())
@@ -354,6 +355,7 @@
 END:VCALENDAR
 """.replace("\n", "\r\n") % self.nowYear)
 
+
     @inlineCallbacks
     def test_migrateDuplicateAttachmentsCalendarFromFile(self):
         """
@@ -371,6 +373,7 @@
         self.assertEqual(ok, 3)
         self.assertEqual(bad, 0)
 
+
     @inlineCallbacks
     def test_migrateCalendarFromFile_Transparency(self):
         """
@@ -403,6 +406,7 @@
         self.assertEquals(uid, "uid4")
         self.assertEquals(transp, 'T')
 
+
     @inlineCallbacks
     def test_migrateHomeFromFile(self):
         """
@@ -470,6 +474,7 @@
 
         self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
 
+
     @inlineCallbacks
     def test_migrateHomeNoSplits(self):
         """
@@ -497,6 +502,7 @@
 
         self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
 
+
     def test_calendarHomeVersion(self):
         """
         The DATAVERSION column for new calendar homes must match the
@@ -695,6 +701,7 @@
         yield d1
         yield d2
 
+
     @inlineCallbacks
     def test_datetimes(self):
         calendarStore = self._sqlCalendarStore
@@ -714,6 +721,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):
         """
@@ -753,6 +761,7 @@
         self.assertNotEqual(notification_uid1_1, None)
         self.assertNotEqual(notification_uid1_2, None)
 
+
     @inlineCallbacks
     def test_removeCalendarPropertiesOnDelete(self):
         """
@@ -794,6 +803,7 @@
         self.assertEqual(len(tuple(rows)), 0)
         yield self.commit()
 
+
     @inlineCallbacks
     def test_removeCalendarObjectPropertiesOnDelete(self):
         """
@@ -839,6 +849,7 @@
         self.assertEqual(len(tuple(rows)), 0)
         yield self.commit()
 
+
     @inlineCallbacks
     def test_removeInboxObjectPropertiesOnDelete(self):
         """
@@ -891,6 +902,7 @@
         self.assertEqual(len(tuple(rows)), 0)
         yield self.commit()
 
+
     @inlineCallbacks
     def test_directShareCreateConcurrency(self):
         """
@@ -942,6 +954,7 @@
         yield d1
         yield d2
 
+
     @inlineCallbacks
     def test_transferSharingDetails(self):
         """
@@ -982,6 +995,7 @@
         self.assertTrue(sharedCalendar is not None)
         self.assertEqual(sharedCalendar._resourceID, newcalendar._resourceID)
 
+
     @inlineCallbacks
     def test_moveCalendarObjectResource(self):
         """
@@ -1003,6 +1017,7 @@
         child = yield calendar1.calendarObjectWithName("5.ics")
         self.assertTrue(child is not None)
 
+
     @inlineCallbacks
     def test_splitCalendars(self):
         """
@@ -1073,6 +1088,7 @@
         self.assertTrue(pkey in calendar2.properties())
         self.assertEqual(str(calendar2.properties()[pkey]), "A birthday calendar")
 
+
     @inlineCallbacks
     def test_noSplitCalendars(self):
         """
@@ -1099,6 +1115,7 @@
 
         self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
 
+
     @inlineCallbacks
     def test_resourceLock(self):
         """
@@ -1176,6 +1193,7 @@
         self.assertEqual(rMin, None)
         self.assertEqual(rMax, None)
 
+
     @inlineCallbacks
     def test_notExpandedWithin(self):
         """
@@ -1292,8 +1310,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()
 
@@ -1304,13 +1322,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):
         """
@@ -1390,17 +1409,18 @@
         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_objectResourceWithID(self):
         """

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/file.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -61,7 +61,6 @@
 import os
 import types
 
-
 ECALENDARTYPE = 0
 EADDRESSBOOKTYPE = 1
 
@@ -531,7 +530,6 @@
 
         return SharedCollectionRecord(*[str(item) if type(item) == types.UnicodeType else item for item in row])
 
-
 class CommonHome(FileMetaDataMixin, LoggingMixIn):
 
     # All these need to be initialized by derived classes for each store type
@@ -1285,8 +1283,6 @@
         yield None
         returnValue([])
 
-
-
 class CommonObjectResource(FileMetaDataMixin, LoggingMixIn, FancyEqMixin):
     """
     @ivar _path: The path of the file on disk

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -74,7 +74,7 @@
 
 from twext.enterprise.dal.syntax import \
     Delete, utcNowSQL, Union, Insert, Len, Max, Parameter, SavepointAction, \
-    Select, Update, ColumnSyntax, TableSyntax, Upper, Count, ALL_COLUMNS
+    Select, Update, ColumnSyntax, TableSyntax, Upper, Count, ALL_COLUMNS, Sum
 
 from twistedcaldav.config import config
 
@@ -969,39 +969,63 @@
         returnValue(count)
 
 
-    def _orphanedBase(limited): #@NoSelf
+    def _orphanedSummary(limited): #@NoSelf
         at = schema.ATTACHMENT
         co = schema.CALENDAR_OBJECT
+        ch = schema.CALENDAR_HOME
+        chm = schema.CALENDAR_HOME_METADATA
         kwds = {}
         if limited:
             kwds["Limit"] = Parameter('batchSize')
         return Select(
-            [at.DROPBOX_ID, at.PATH],
-            From=at.join(co, at.DROPBOX_ID == co.DROPBOX_ID, "left outer"),
+            [ch.OWNER_UID, chm.QUOTA_USED_BYTES, Sum(at.SIZE), Count(at.DROPBOX_ID)],
+            From=at.join(
+                co, at.DROPBOX_ID == co.DROPBOX_ID, "left outer").join(
+                ch, at.CALENDAR_HOME_RESOURCE_ID == ch.RESOURCE_ID).join(
+                chm, ch.RESOURCE_ID == chm.RESOURCE_ID
+            ),
             Where=co.DROPBOX_ID == None,
+            GroupBy=(ch.OWNER_UID, chm.QUOTA_USED_BYTES),
             **kwds
         )
 
-    _orphanedLimited = _orphanedBase(True)
-    _orphanedUnlimited = _orphanedBase(False)
-    del _orphanedBase
+    _orphanedSummaryLimited = _orphanedSummary(True)
+    _orphanedSummaryUnlimited = _orphanedSummary(False)
+    del _orphanedSummary
 
-
     def orphanedAttachments(self, batchSize=None):
         """
         Find attachments no longer referenced by any events.
 
-        Returns a deferred to a list of (dropbox_id, path) tuples.
+        Returns a deferred to a list of (calendar_home_owner_uid, dropbox_id, path, size) tuples.
         """
         if batchSize is not None:
             kwds = {'batchSize': batchSize}
-            query = self._orphanedLimited
+            query = self._orphanedSummaryLimited
         else:
             kwds = {}
-            query = self._orphanedUnlimited
+            query = self._orphanedSummaryUnlimited
         return query.on(self, **kwds)
 
 
+    def _orphanedBase(limited): #@NoSelf
+        at = schema.ATTACHMENT
+        co = schema.CALENDAR_OBJECT
+        kwds = {}
+        if limited:
+            kwds["Limit"] = Parameter('batchSize')
+        return Select(
+            [at.DROPBOX_ID, at.PATH],
+            From=at.join(co, at.DROPBOX_ID == co.DROPBOX_ID, "left outer"),
+            Where=co.DROPBOX_ID == None,
+            **kwds
+        )
+
+    _orphanedLimited = _orphanedBase(True)
+    _orphanedUnlimited = _orphanedBase(False)
+    del _orphanedBase
+
+
     @inlineCallbacks
     def removeOrphanedAttachments(self, batchSize=None):
         """
@@ -1011,7 +1035,13 @@
         # TODO: see if there is a better way to import Attachment
         from txdav.caldav.datastore.sql import DropBoxAttachment
 
-        results = (yield self.orphanedAttachments(batchSize=batchSize))
+        if batchSize is not None:
+            kwds = {'batchSize': batchSize}
+            query = self._orphanedLimited
+        else:
+            kwds = {}
+            query = self._orphanedUnlimited
+        results = (yield query.on(self, **kwds))
         count = 0
         for dropboxID, path in results:
             attachment = (yield DropBoxAttachment.load(self, dropboxID, path))

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2012-12-12 22:11:41 UTC (rev 10162)
@@ -34,14 +34,6 @@
     "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
 );
 
-create table INVITE (
-    "INVITE_UID" nvarchar2(255),
-    "NAME" nvarchar2(255),
-    "RECIPIENT_ADDRESS" nvarchar2(255),
-    "HOME_RESOURCE_ID" integer not null,
-    "RESOURCE_ID" integer not null
-);
-
 create table NOTIFICATION_HOME (
     "RESOURCE_ID" integer primary key,
     "OWNER_UID" nvarchar2(255) unique
@@ -65,8 +57,6 @@
     "CALENDAR_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
-    "SEEN_BY_OWNER" integer not null,
-    "SEEN_BY_SHAREE" integer not null,
     "MESSAGE" nclob, 
     primary key("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_ID"), 
     unique("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_NAME")
@@ -216,8 +206,6 @@
     "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
-    "SEEN_BY_OWNER" integer not null,
-    "SEEN_BY_SHAREE" integer not null,
     "MESSAGE" nclob, 
     primary key("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_ID"), 
     unique("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")
@@ -277,21 +265,10 @@
     "VALUE" nvarchar2(255)
 );
 
-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '13');
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '14');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '3');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '1');
-create index INVITE_INVITE_UID_9b0902ff on INVITE (
-    INVITE_UID
-);
 
-create index INVITE_RESOURCE_ID_b36ddc23 on INVITE (
-    RESOURCE_ID
-);
-
-create index INVITE_HOME_RESOURCE__e9bdf77e on INVITE (
-    HOME_RESOURCE_ID
-);
-
 create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
     NOTIFICATION_HOME_RESOURCE_ID
 );

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current.sql	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/current.sql	2012-12-12 22:11:41 UTC (rev 10162)
@@ -46,7 +46,7 @@
 create table CALENDAR_HOME (
   RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
   OWNER_UID        varchar(255) not null unique,                                 -- implicit index
-  DATAVERSION	   integer      default 0 not null
+  DATAVERSION      integer      default 0 not null
 );
 
 ----------------------------
@@ -81,7 +81,6 @@
 );
 
 
-
 ---------------------------
 -- Sharing Notifications --
 ---------------------------
@@ -279,6 +278,7 @@
 create index TRANSPARENCY_TIME_RANGE_INSTANCE_ID on
   TRANSPARENCY(TIME_RANGE_INSTANCE_ID);
 
+
 ----------------
 -- Attachment --
 ----------------
@@ -287,11 +287,11 @@
 
 create table ATTACHMENT (
   ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
-  CALENDAR_HOME_RESOURCE_ID   integer       not null references CALENDAR_HOME,
+  CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
   DROPBOX_ID                  varchar(255),
-  CONTENT_TYPE                varchar(255)  not null,
-  SIZE                        integer       not null,
-  MD5                         char(32)      not null,
+  CONTENT_TYPE                varchar(255)      not null,
+  SIZE                        integer           not null,
+  MD5                         char(32)          not null,
   CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
   MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
   PATH                        varchar(1024)     not null
@@ -332,7 +332,7 @@
 create table ADDRESSBOOK_HOME (
   RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
   OWNER_UID        varchar(255) not null unique,                                -- implicit index
-  DATAVERSION	   integer      default 0 not null
+  DATAVERSION      integer      default 0 not null
 );
 
 -------------------------------

Copied: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/oracle-dialect/v13.sql (from rev 10159, CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v13.sql)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/oracle-dialect/v13.sql	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/oracle-dialect/v13.sql	2012-12-12 22:11:41 UTC (rev 10162)
@@ -0,0 +1,379 @@
+create sequence RESOURCE_ID_SEQ;
+create sequence INSTANCE_ID_SEQ;
+create sequence ATTACHMENT_ID_SEQ;
+create sequence REVISION_SEQ;
+create table NODE_INFO (
+    "HOSTNAME" nvarchar2(255),
+    "PID" integer not null,
+    "PORT" integer not null,
+    "TIME" timestamp default CURRENT_TIMESTAMP at time zone 'UTC' not null, 
+    primary key("HOSTNAME", "PORT")
+);
+
+create table CALENDAR_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table CALENDAR_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR (
+    "RESOURCE_ID" integer primary key
+);
+
+create table CALENDAR_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR on delete cascade,
+    "SUPPORTED_COMPONENTS" nvarchar2(255) default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table INVITE (
+    "INVITE_UID" nvarchar2(255),
+    "NAME" nvarchar2(255),
+    "RECIPIENT_ADDRESS" nvarchar2(255),
+    "HOME_RESOURCE_ID" integer not null,
+    "RESOURCE_ID" integer not null
+);
+
+create table NOTIFICATION_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique
+);
+
+create table NOTIFICATION (
+    "RESOURCE_ID" integer primary key,
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME,
+    "NOTIFICATION_UID" nvarchar2(255),
+    "XML_TYPE" nvarchar2(255),
+    "XML_DATA" nclob,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique("NOTIFICATION_UID", "NOTIFICATION_HOME_RESOURCE_ID")
+);
+
+create table CALENDAR_BIND (
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "CALENDAR_RESOURCE_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "SEEN_BY_OWNER" integer not null,
+    "SEEN_BY_SHAREE" integer not null,
+    "MESSAGE" nclob, 
+    primary key("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_ID"), 
+    unique("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_NAME")
+);
+
+create table CALENDAR_BIND_MODE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('own', 0);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('write', 2);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('direct', 3);
+create table CALENDAR_BIND_STATUS (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invited', 0);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('accepted', 1);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('declined', 2);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invalid', 3);
+create table CALENDAR_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob,
+    "ICALENDAR_UID" nvarchar2(255),
+    "ICALENDAR_TYPE" nvarchar2(255),
+    "ATTACHMENTS_MODE" integer default 0 not null,
+    "DROPBOX_ID" nvarchar2(255),
+    "ORGANIZER" nvarchar2(255),
+    "ORGANIZER_OBJECT" integer references CALENDAR_OBJECT,
+    "RECURRANCE_MIN" date,
+    "RECURRANCE_MAX" date,
+    "ACCESS" integer default 0 not null,
+    "SCHEDULE_OBJECT" integer default 0,
+    "SCHEDULE_TAG" nvarchar2(36) default null,
+    "SCHEDULE_ETAGS" nclob default null,
+    "PRIVATE_COMMENTS" integer default 0 not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique("CALENDAR_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MO (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('none', 0);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('write', 2);
+create table CALENDAR_ACCESS_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(32) unique
+);
+
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('', 0);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('public', 1);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('private', 2);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('confidential', 3);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('restricted', 4);
+create table TIME_RANGE (
+    "INSTANCE_ID" integer primary key,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "FLOATING" integer not null,
+    "START_DATE" timestamp not null,
+    "END_DATE" timestamp not null,
+    "FBTYPE" integer not null,
+    "TRANSPARENT" integer not null
+);
+
+create table FREE_BUSY_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('unknown', 0);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('free', 1);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy', 2);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-unavailable', 3);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-tentative', 4);
+create table TRANSPARENCY (
+    "TIME_RANGE_INSTANCE_ID" integer not null references TIME_RANGE on delete cascade,
+    "USER_ID" nvarchar2(255),
+    "TRANSPARENT" integer not null
+);
+
+create table ATTACHMENT (
+    "ATTACHMENT_ID" integer primary key,
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "DROPBOX_ID" nvarchar2(255),
+    "CONTENT_TYPE" nvarchar2(255),
+    "SIZE" integer not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "PATH" nvarchar2(1024)
+);
+
+create table ATTACHMENT_CALENDAR_OBJECT (
+    "ATTACHMENT_ID" integer not null references ATTACHMENT on delete cascade,
+    "MANAGED_ID" nvarchar2(255),
+    "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade, 
+    primary key("ATTACHMENT_ID", "CALENDAR_OBJECT_RESOURCE_ID"), 
+    unique("MANAGED_ID", "CALENDAR_OBJECT_RESOURCE_ID")
+);
+
+create table RESOURCE_PROPERTY (
+    "RESOURCE_ID" integer not null,
+    "NAME" nvarchar2(255),
+    "VALUE" nclob,
+    "VIEWER_UID" nvarchar2(255), 
+    primary key("RESOURCE_ID", "NAME", "VIEWER_UID")
+);
+
+create table ADDRESSBOOK_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table ADDRESSBOOK_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references ADDRESSBOOK_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table ADDRESSBOOK (
+    "RESOURCE_ID" integer primary key
+);
+
+create table ADDRESSBOOK_METADATA (
+    "RESOURCE_ID" integer primary key references ADDRESSBOOK on delete cascade,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table ADDRESSBOOK_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "ADDRESSBOOK_RESOURCE_ID" integer not null references ADDRESSBOOK on delete cascade,
+    "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
+    "BIND_MODE" integer not null,
+    "BIND_STATUS" integer not null,
+    "SEEN_BY_OWNER" integer not null,
+    "SEEN_BY_SHAREE" integer not null,
+    "MESSAGE" nclob, 
+    primary key("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_ID"), 
+    unique("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "ADDRESSBOOK_RESOURCE_ID" integer not null references ADDRESSBOOK on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "VCARD_TEXT" nclob,
+    "VCARD_UID" nvarchar2(255),
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique("ADDRESSBOOK_RESOURCE_ID", "RESOURCE_NAME"), 
+    unique("ADDRESSBOOK_RESOURCE_ID", "VCARD_UID")
+);
+
+create table CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "CALENDAR_RESOURCE_ID" integer references CALENDAR,
+    "CALENDAR_NAME" nvarchar2(255) default null,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null
+);
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "ADDRESSBOOK_RESOURCE_ID" integer references ADDRESSBOOK,
+    "ADDRESSBOOK_NAME" nvarchar2(255) default null,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null
+);
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null, 
+    unique("NOTIFICATION_HOME_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table APN_SUBSCRIPTIONS (
+    "TOKEN" nvarchar2(255),
+    "RESOURCE_KEY" nvarchar2(255),
+    "MODIFIED" integer not null,
+    "SUBSCRIBER_GUID" nvarchar2(255),
+    "USER_AGENT" nvarchar2(255) default null,
+    "IP_ADDR" nvarchar2(255) default null, 
+    primary key("TOKEN", "RESOURCE_KEY")
+);
+
+create table CALENDARSERVER (
+    "NAME" nvarchar2(255) primary key,
+    "VALUE" nvarchar2(255)
+);
+
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '13');
+insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '3');
+insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '1');
+create index INVITE_INVITE_UID_9b0902ff on INVITE (
+    INVITE_UID
+);
+
+create index INVITE_RESOURCE_ID_b36ddc23 on INVITE (
+    RESOURCE_ID
+);
+
+create index INVITE_HOME_RESOURCE__e9bdf77e on INVITE (
+    HOME_RESOURCE_ID
+);
+
+create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
+    NOTIFICATION_HOME_RESOURCE_ID
+);
+
+create index CALENDAR_BIND_RESOURC_e57964d4 on CALENDAR_BIND (
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_CALEN_a9a453a9 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_CALEN_96e83b73 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    RECURRANCE_MAX
+);
+
+create index CALENDAR_OBJECT_ORGAN_7ce24750 on CALENDAR_OBJECT (
+    ORGANIZER_OBJECT
+);
+
+create index CALENDAR_OBJECT_DROPB_de041d80 on CALENDAR_OBJECT (
+    DROPBOX_ID
+);
+
+create index TIME_RANGE_CALENDAR_R_beb6e7eb on TIME_RANGE (
+    CALENDAR_RESOURCE_ID
+);
+
+create index TIME_RANGE_CALENDAR_O_acf37bd1 on TIME_RANGE (
+    CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index TRANSPARENCY_TIME_RAN_5f34467f on TRANSPARENCY (
+    TIME_RANGE_INSTANCE_ID
+);
+
+create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
+    CALENDAR_HOME_RESOURCE_ID
+);
+
+create index ADDRESSBOOK_BIND_RESO_205aa75c on ADDRESSBOOK_BIND (
+    ADDRESSBOOK_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_3a3956c4 on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_HOME_RESOURCE_ID,
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_2643d556 on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    RESOURCE_NAME
+);
+
+create index CALENDAR_OBJECT_REVIS_265c8acf on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_f460d62d on ADDRESSBOOK_OBJECT_REVISIONS (
+    ADDRESSBOOK_HOME_RESOURCE_ID,
+    ADDRESSBOOK_RESOURCE_ID
+);
+
+create index ADDRESSBOOK_OBJECT_RE_9a848f39 on ADDRESSBOOK_OBJECT_REVISIONS (
+    ADDRESSBOOK_RESOURCE_ID,
+    RESOURCE_NAME
+);
+
+create index ADDRESSBOOK_OBJECT_RE_cb101e6b on ADDRESSBOOK_OBJECT_REVISIONS (
+    ADDRESSBOOK_RESOURCE_ID,
+    REVISION
+);
+
+create index NOTIFICATION_OBJECT_R_036a9cee on NOTIFICATION_OBJECT_REVISIONS (
+    NOTIFICATION_HOME_RESOURCE_ID,
+    REVISION
+);
+
+create index APN_SUBSCRIPTIONS_RES_9610d78e on APN_SUBSCRIPTIONS (
+    RESOURCE_KEY
+);
+

Copied: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v13.sql (from rev 10159, CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v13.sql)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v13.sql	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/old/postgres-dialect/v13.sql	2012-12-12 22:11:41 UTC (rev 10162)
@@ -0,0 +1,531 @@
+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2012 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.
+----
+
+-----------------
+-- Resource ID --
+-----------------
+
+create sequence RESOURCE_ID_SEQ;
+
+-------------------------
+-- Cluster Bookkeeping --
+-------------------------
+
+-- Information about a process connected to this database.
+
+-- Note that this must match the node info schema in twext.enterprise.queue.
+create table NODE_INFO (
+  HOSTNAME  varchar(255) not null,
+  PID       integer not null,
+  PORT      integer not null,
+  TIME      timestamp not null default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (HOSTNAME, PORT)
+);
+
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                 -- implicit index
+  DATAVERSION      integer      default 0 not null
+);
+
+----------------------------
+-- Calendar Home Metadata --
+----------------------------
+
+create table CALENDAR_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references CALENDAR_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+-----------------------
+-- Calendar Metadata --
+-----------------------
+
+create table CALENDAR_METADATA (
+  RESOURCE_ID           integer   primary key references CALENDAR on delete cascade, -- implicit index
+  SUPPORTED_COMPONENTS  varchar(255) default null,
+  CREATED               timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED              timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+------------------------
+-- Sharing Invitation --
+------------------------
+
+create table INVITE (
+    INVITE_UID         varchar(255) not null,
+    NAME               varchar(255) not null,
+    RECIPIENT_ADDRESS  varchar(255) not null,
+    HOME_RESOURCE_ID   integer      not null,
+    RESOURCE_ID        integer      not null
+
+    -- Need primary key on (INVITE_UID, NAME, RECIPIENT_ADDRESS)?
+);
+
+create index INVITE_INVITE_UID on INVITE(INVITE_UID);
+create index INVITE_RESOURCE_ID on INVITE(RESOURCE_ID);
+create index INVITE_HOME_RESOURCE_ID on INVITE(HOME_RESOURCE_ID);
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID   varchar(255) not null unique                                 -- implicit index
+);
+
+create table NOTIFICATION (
+  RESOURCE_ID                   integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME,
+  NOTIFICATION_UID              varchar(255) not null,
+  XML_TYPE                      varchar(255) not null,
+  XML_DATA                      text         not null,
+  MD5                           char(32)     not null,
+  CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID) -- implicit index
+);
+
+create index NOTIFICATION_NOTIFICATION_HOME_RESOURCE_ID on
+  NOTIFICATION(NOTIFICATION_HOME_RESOURCE_ID);
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
+
+  -- An invitation which hasn't been accepted yet will not yet have a resource
+  -- name, so this field may be null.
+
+  CALENDAR_RESOURCE_NAME    varchar(255),
+  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  SEEN_BY_OWNER             boolean      not null,
+  SEEN_BY_SHAREE            boolean      not null,
+  MESSAGE                   text,
+
+  primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID), -- implicit index
+  unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)     -- implicit index
+);
+
+create index CALENDAR_BIND_RESOURCE_ID on CALENDAR_BIND(CALENDAR_RESOURCE_ID);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own'  );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+insert into CALENDAR_BIND_MODE values (3, 'direct');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
+  RESOURCE_NAME        varchar(255) not null,
+  ICALENDAR_TEXT       text         not null,
+  ICALENDAR_UID        varchar(255) not null,
+  ICALENDAR_TYPE       varchar(255) not null,
+  ATTACHMENTS_MODE     integer      default 0 not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
+  DROPBOX_ID           varchar(255),
+  ORGANIZER            varchar(255),
+  ORGANIZER_OBJECT     integer      references CALENDAR_OBJECT,
+  RECURRANCE_MIN       date,        -- minimum date that recurrences have been expanded to.
+  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
+  ACCESS               integer      default 0 not null,
+  SCHEDULE_OBJECT      boolean      default false,
+  SCHEDULE_TAG         varchar(36)  default null,
+  SCHEDULE_ETAGS       text         default null,
+  PRIVATE_COMMENTS     boolean      default false not null,
+  MD5                  char(32)     not null,
+  CREATED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+
+  -- since the 'inbox' is a 'calendar resource' for the purpose of storing
+  -- calendar objects, this constraint has to be selectively enforced by the
+  -- application layer.
+
+  -- unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_AND_ICALENDAR_UID on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_RECURRANCE_MAX on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX);
+
+create index CALENDAR_OBJECT_ORGANIZER_OBJECT on
+  CALENDAR_OBJECT(ORGANIZER_OBJECT);
+
+create index CALENDAR_OBJECT_DROPBOX_ID on
+  CALENDAR_OBJECT(DROPBOX_ID);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (2, 'write');
+
+
+-- Enumeration of calendar access types
+
+create table CALENDAR_ACCESS_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(32) not null unique
+);
+
+insert into CALENDAR_ACCESS_TYPE values (0, ''             );
+insert into CALENDAR_ACCESS_TYPE values (1, 'public'       );
+insert into CALENDAR_ACCESS_TYPE values (2, 'private'      );
+insert into CALENDAR_ACCESS_TYPE values (3, 'confidential' );
+insert into CALENDAR_ACCESS_TYPE values (4, 'restricted'   );
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID        integer        not null references CALENDAR on delete cascade,
+  CALENDAR_OBJECT_RESOURCE_ID integer        not null references CALENDAR_OBJECT on delete cascade,
+  FLOATING                    boolean        not null,
+  START_DATE                  timestamp      not null,
+  END_DATE                    timestamp      not null,
+  FBTYPE                      integer        not null,
+  TRANSPARENT                 boolean        not null
+);
+
+create index TIME_RANGE_CALENDAR_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_RESOURCE_ID);
+create index TIME_RANGE_CALENDAR_OBJECT_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_OBJECT_RESOURCE_ID);
+
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown'         );
+insert into FREE_BUSY_TYPE values (1, 'free'            );
+insert into FREE_BUSY_TYPE values (2, 'busy'            );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
+
+
+------------------
+-- Transparency --
+------------------
+
+create table TRANSPARENCY (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null
+);
+
+create index TRANSPARENCY_TIME_RANGE_INSTANCE_ID on
+  TRANSPARENCY(TIME_RANGE_INSTANCE_ID);
+
+
+----------------
+-- Attachment --
+----------------
+
+create sequence ATTACHMENT_ID_SEQ;
+
+create table ATTACHMENT (
+  ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
+  CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
+  DROPBOX_ID                  varchar(255),
+  CONTENT_TYPE                varchar(255)      not null,
+  SIZE                        integer           not null,
+  MD5                         char(32)          not null,
+  CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  PATH                        varchar(1024)     not null
+);
+
+create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
+  ATTACHMENT(CALENDAR_HOME_RESOURCE_ID);
+
+-- Many-to-many relationship between attachments and calendar objects
+create table ATTACHMENT_CALENDAR_OBJECT (
+  ATTACHMENT_ID                  integer      not null references ATTACHMENT on delete cascade,
+  MANAGED_ID                     varchar(255) not null,
+  CALENDAR_OBJECT_RESOURCE_ID    integer      not null references CALENDAR_OBJECT on delete cascade,
+
+  primary key (ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+  unique (MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
+);
+
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+  RESOURCE_ID integer      not null, -- foreign key: *.RESOURCE_ID
+  NAME        varchar(255) not null,
+  VALUE       text         not null, -- FIXME: xml?
+  VIEWER_UID  varchar(255),
+
+  primary key (RESOURCE_ID, NAME, VIEWER_UID) -- implicit index
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                -- implicit index
+  DATAVERSION      integer      default 0 not null
+);
+
+-------------------------------
+-- AddressBook Home Metadata --
+-------------------------------
+
+create table ADDRESSBOOK_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+-----------------
+-- AddressBook --
+-----------------
+
+create table ADDRESSBOOK (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+--------------------------
+-- AddressBook Metadata --
+--------------------------
+
+create table ADDRESSBOOK_METADATA (
+  RESOURCE_ID integer   primary key references ADDRESSBOOK on delete cascade, -- implicit index
+  CREATED     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED    timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+----------------------
+-- AddressBook Bind --
+----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK
+
+create table ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID integer      not null references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_RESOURCE_ID      integer      not null references ADDRESSBOOK on delete cascade,
+
+  -- An invitation which hasn't been accepted yet will not yet have a resource
+  -- name, so this field may be null.
+
+  ADDRESSBOOK_RESOURCE_NAME    varchar(255),
+  BIND_MODE                    integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                  integer      not null, -- enum CALENDAR_BIND_STATUS
+  SEEN_BY_OWNER                boolean      not null,
+  SEEN_BY_SHAREE               boolean      not null,
+  MESSAGE                      text,                  -- FIXME: xml?
+
+  primary key (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
+);
+
+create index ADDRESSBOOK_BIND_RESOURCE_ID on
+  ADDRESSBOOK_BIND(ADDRESSBOOK_RESOURCE_ID);
+
+create table ADDRESSBOOK_OBJECT (
+  RESOURCE_ID             integer      primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  ADDRESSBOOK_RESOURCE_ID integer      not null references ADDRESSBOOK on delete cascade,
+  RESOURCE_NAME           varchar(255) not null,
+  VCARD_TEXT              text         not null,
+  VCARD_UID               varchar(255) not null,
+  MD5                     char(32)     not null,
+  CREATED                 timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME), -- implicit index
+  unique (ADDRESSBOOK_RESOURCE_ID, VCARD_UID)      -- implicit index
+);
+
+---------------
+-- Revisions --
+---------------
+
+create sequence REVISION_SEQ;
+
+
+---------------
+-- Revisions --
+---------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      references CALENDAR,
+  CALENDAR_NAME             varchar(255) default null,
+  RESOURCE_NAME             varchar(255),
+  REVISION                  integer      default nextval('REVISION_SEQ') not null,
+  DELETED                   boolean      not null
+);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_CALENDAR_RESOURCE_ID
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+-------------------------------
+-- AddressBook Object Revisions --
+-------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_HOME_RESOURCE_ID integer      not null references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_RESOURCE_ID      integer      references ADDRESSBOOK,
+  ADDRESSBOOK_NAME             varchar(255) default null,
+  RESOURCE_NAME                varchar(255),
+  REVISION                     integer      default nextval('REVISION_SEQ') not null,
+  DELETED                      boolean      not null
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_HOME_RESOURCE_ID_ADDRESSBOOK_RESOURCE_ID
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_RESOURCE_ID, REVISION);
+
+-----------------------------------
+-- Notification Object Revisions --
+-----------------------------------
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+
+  unique(NOTIFICATION_HOME_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+);
+
+create index NOTIFICATION_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on NOTIFICATION_OBJECT_REVISIONS(NOTIFICATION_HOME_RESOURCE_ID, REVISION);
+
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  USER_AGENT                    varchar(255) default null,
+  IP_ADDR                       varchar(255) default null,
+
+  primary key (TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+   on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+  NAME                          varchar(255) primary key, -- implicit index
+  VALUE                         varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '13');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '3');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '1');

Copied: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql (from rev 10159, CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql	2012-12-12 22:11:41 UTC (rev 10162)
@@ -0,0 +1,33 @@
+----
+-- Copyright (c) 2012 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.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 13 to 14 --
+---------------------------------------------------
+
+-- sharing-related cleanup 
+
+drop table INVITE;
+
+alter table CALENDAR_BIND
+ drop column SEEN_BY_OWNER, SEEN_BY_SHAREE;
+
+alter table ADDRESSBOOK_BIND
+ drop column SEEN_BY_OWNER, SEEN_BY_SHAREE;
+
+-- Now update the version
+-- No data upgrades
+update CALENDARSERVER set VALUE = '14' where NAME = 'VERSION';

Copied: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_13_to_14.sql (from rev 10159, CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_13_to_14.sql)
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_13_to_14.sql	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_13_to_14.sql	2012-12-12 22:11:41 UTC (rev 10162)
@@ -0,0 +1,37 @@
+----
+-- Copyright (c) 2012 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.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 13 to 14 --
+---------------------------------------------------
+
+-- sharing-related cleanup 
+
+drop table INVITE;
+
+alter table CALENDAR_BIND
+ drop column SEEN_BY_OWNER;
+alter table CALENDAR_BIND
+ drop column SEEN_BY_SHAREE;
+
+alter table ADDRESSBOOK_BIND
+ drop column SEEN_BY_OWNER;
+alter table ADDRESSBOOK_BIND
+ drop column SEEN_BY_SHAREE;
+
+-- Now update the version
+-- No data upgrades
+update CALENDARSERVER set VALUE = '14' where NAME = 'VERSION';

Modified: CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_tables.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_tables.py	2012-12-12 21:59:33 UTC (rev 10161)
+++ CalendarServer/branches/users/gaya/sharedgroups/txdav/common/datastore/sql_tables.py	2012-12-12 22:11:41 UTC (rev 10162)
@@ -114,8 +114,8 @@
     """
     result = {}
     result['name'] = tableSyntax.model.name
-    #pkey = tableSyntax.model.primaryKey
-    #if pkey is not None:
+    # pkey = tableSyntax.model.primaryKey
+    # if pkey is not None:
     #    default = pkey.default
     #    if isinstance(default, Sequence):
     #        result['sequence'] = default.name
@@ -399,11 +399,8 @@
 if __name__ == '__main__':
     import sys
     if len(sys.argv) == 2:
-        # Argument is the name of a old/postgres-dialect file (without the .sql suffix), e.g. "v4" 
+        # Argument is the name of a old/postgres-dialect file (without the .sql suffix), e.g. "v4"
         schema = _populateSchema(sys.argv[1])
     else:
         schema = _populateSchema()
     _translateSchema(sys.stdout, schema=schema)
-
-
-
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121212/d2c9f558/attachment-0001.html>


More information about the calendarserver-changes mailing list