[CalendarServer-changes] [6489] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Sun Oct 31 18:22:11 PDT 2010


Revision: 6489
          http://trac.macosforge.org/projects/calendarserver/changeset/6489
Author:   cdaboo at apple.com
Date:     2010-10-31 18:22:09 -0700 (Sun, 31 Oct 2010)
Log Message:
-----------
Move quota used bytes into newstore. Clean-up some missing pieces in the file store.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py
    CalendarServer/trunk/twistedcaldav/method/put_common.py
    CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py
    CalendarServer/trunk/twistedcaldav/method/report_common.py
    CalendarServer/trunk/twistedcaldav/notifications.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/txdav/caldav/datastore/file.py
    CalendarServer/trunk/txdav/caldav/datastore/index_file.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/common.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py
    CalendarServer/trunk/txdav/carddav/datastore/file.py
    CalendarServer/trunk/txdav/carddav/datastore/index_file.py
    CalendarServer/trunk/txdav/carddav/datastore/sql.py
    CalendarServer/trunk/txdav/carddav/datastore/test/common.py
    CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py
    CalendarServer/trunk/txdav/common/datastore/file.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql
    CalendarServer/trunk/txdav/common/datastore/sql_tables.py

Modified: CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -318,35 +318,6 @@
 
 
     @inlineCallbacks
-    def checkQuota(self):
-        """
-        Get quota details for destination and source before we start messing with adding other files.
-        """
-
-        if self.request is None:
-            self.destquota = None
-        else:
-            self.destquota = (yield self.destination.quota(self.request))
-            if self.destquota is not None and self.destination.exists():
-                self.old_dest_size = (yield self.destination.quotaSize(self.request))
-            else:
-                self.old_dest_size = 0
-            
-        if self.request is None:
-            self.sourcequota = None
-        elif self.source is not None:
-            self.sourcequota = (yield self.source.quota(self.request))
-            if self.sourcequota is not None and self.source.exists():
-                self.old_source_size = (yield self.source.quotaSize(self.request))
-            else:
-                self.old_source_size = 0
-        else:
-            self.sourcequota = None
-            self.old_source_size = 0
-
-        returnValue(None)
-
-    @inlineCallbacks
     def doStore(self):
         # Do put or copy based on whether source exists
         source = self.source
@@ -387,28 +358,15 @@
         returnValue(None)
 
     @inlineCallbacks
-    def doSourceQuotaCheck(self):
-        # Update quota
-        if self.sourcequota is not None:
-            delete_size = 0 - self.old_source_size
-            yield self.source.quotaSizeAdjust(self.request, delete_size)
-  
-        returnValue(None)
-
-    @inlineCallbacks
     def doDestinationQuotaCheck(self):
-        # Get size of new/old resources
-        new_dest_size = (yield self.destination.quotaSize(self.request))
-
-        diff_size = new_dest_size - self.old_dest_size
-
-        if diff_size >= self.destquota[0]:
-            log.err("Over quota: available %d, need %d" % (self.destquota[0], diff_size))
+        """
+        Look at current quota after changes and see if we have gone over the top.
+        """
+        quota = (yield self.destination.quota(self.request))
+        if quota[0] < 0:
+            log.err("Over quota by %d" % (-quota[0],))
             raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
-        yield self.destination.quotaSizeAdjust(self.request, diff_size)
 
-        returnValue(None)
-
     @inlineCallbacks
     def run(self):
         """
@@ -442,9 +400,6 @@
                         NoUIDConflict(davxml.HRef.fromString(joinURL(parentForURL(self.destination_uri), rname.encode("utf-8"))))
                     ))
             
-            # Get current quota state.
-            yield self.checkQuota()
-
             # Do the actual put or copy
             response = (yield self.doStore())
             
@@ -458,13 +413,8 @@
                     davxml.GETContentType.fromString(generateContentType(content_type))
                 )
 
-            # Delete the original source if needed.
-            if self.deletesource:
-                yield self.doSourceQuotaCheck()
-
             # Do quota check on destination
-            if self.destquota is not None:
-                yield self.doDestinationQuotaCheck()
+            yield self.doDestinationQuotaCheck()
     
             if reservation:
                 yield reservation.unreserve()

Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -499,36 +499,6 @@
         return succeed(None)
 
 
-    @inlineCallbacks
-    def checkQuota(self):
-        """
-        Get quota details for destination and source before we start messing with adding other files.
-        """
-
-        if self.request is None:
-            self.destquota = None
-        else:
-            self.destquota = (yield self.destination.quota(self.request))
-            if self.destquota is not None and self.destination.exists():
-                self.old_dest_size = (yield self.destination.quotaSize(self.request))
-            else:
-                self.old_dest_size = 0
-            
-        if self.request is None:
-            self.sourcequota = None
-        elif self.source is not None:
-            self.sourcequota = (yield self.source.quota(self.request))
-            if self.sourcequota is not None and self.source.exists():
-                self.old_source_size = (yield self.source.quotaSize(self.request))
-            else:
-                self.old_source_size = 0
-        else:
-            self.sourcequota = None
-            self.old_source_size = 0
-
-        returnValue(None)
-
-
     def truncateRecurrence(self):
         
         if config.MaxInstancesForRRULE != 0:
@@ -780,29 +750,16 @@
         returnValue(None)
 
     @inlineCallbacks
-    def doSourceQuotaCheck(self):
-        # Update quota
-        if self.sourcequota is not None:
-            delete_size = 0 - self.old_source_size
-            yield self.source.quotaSizeAdjust(self.request, delete_size)
-  
-        returnValue(None)
-
-    @inlineCallbacks
     def doDestinationQuotaCheck(self):
-        # Get size of new/old resources
-        new_dest_size = (yield self.destination.quotaSize(self.request))
-
-        diff_size = new_dest_size - self.old_dest_size
-
-        if diff_size >= self.destquota[0]:
-            log.err("Over quota: available %d, need %d" % (self.destquota[0], diff_size))
+        """
+        Look at current quota after changes and see if we have gone over the top.
+        """
+        quota = (yield self.destination.quota(self.request))
+        if quota[0] < 0:
+            log.err("Over quota by %d" % (-quota[0],))
             raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
-        yield self.destination.quotaSizeAdjust(self.request, diff_size)
 
-        returnValue(None)
 
-
     @inlineCallbacks
     def run(self):
         """
@@ -838,9 +795,6 @@
                                         rname.encode("utf-8"))))))
 
 
-            # Get current quota state.
-            yield self.checkQuota()
-
             # Handle RRULE truncation
             rruleChanged = self.truncateRecurrence()
 
@@ -956,13 +910,8 @@
                     davxml.GETContentType.fromString(generateContentType(content_type))
                 )
 
-            # Delete the original source if needed.
-            if self.deletesource:
-                yield self.doSourceQuotaCheck()
-
             # Do quota check on destination
-            if self.destquota is not None:
-                yield self.doDestinationQuotaCheck()
+            yield self.doDestinationQuotaCheck()
     
             if reservation:
                 yield reservation.unreserve()

Modified: CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -25,7 +25,8 @@
 from twext.python.log import Logger
 from twext.web2.dav.http import ErrorResponse
 
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue,\
+    maybeDeferred
 from twext.web2 import responsecode
 from twext.web2.dav import davxml
 from twext.web2.dav.element.base import PCDATAElement
@@ -175,9 +176,9 @@
                 try:
                     # Get list of children that match the search and have read
                     # access
-                    records = yield calresource.index().indexedSearch(filter)
+                    records = yield maybeDeferred(calresource.index().indexedSearch, filter)
                 except IndexedSearchException:
-                    records = yield calresource.index().bruteForceSearch()
+                    records = yield maybeDeferred(calresource.index().bruteForceSearch)
                     index_query_ok = False
                 names = [name for name, ignore_uid, ignore_type in records]
 

Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -39,7 +39,7 @@
 
 from vobject.icalendar import utc
 
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
 from twisted.python.failure import Failure
 from twext.web2 import responsecode
 from twext.web2.dav import davxml
@@ -460,11 +460,11 @@
     else:
         useruid = ""
     try:
-        resources = yield calresource.index().indexedSearch(
+        resources = yield maybeDeferred(calresource.index().indexedSearch,
             filter, useruid=useruid, fbtype=True
         )
     except IndexedSearchException:
-        resources = yield calresource.index().bruteForceSearch()
+        resources = yield maybeDeferred(calresource.index().bruteForceSearch)
 
     # We care about separate instances for VEVENTs only
     aggregated_resources = {}

Modified: CalendarServer/trunk/twistedcaldav/notifications.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/notifications.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/twistedcaldav/notifications.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -33,6 +33,8 @@
 from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn, CalDAVResource
 from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
 
+from txdav.common.icommondatastore import SyncTokenValidException
+
 import os
 import types
 
@@ -167,15 +169,66 @@
             values (:1, :2, :3)
             """, record.uid, record.name, record.xmltype,
         )
+            
+        self._db_execute(
+            """
+            insert or replace into REVISIONS (NAME, REVISION, DELETED)
+            values (:1, :2, :3)
+            """, record.name, self.bumpRevision(fast=True), 'N',
+        )
     
     def removeRecordForUID(self, uid):
 
-        self._db_execute("delete from NOTIFICATIONS where UID = :1", uid)
+        record = self.recordForUID(uid)
+        self.removeRecordForName(record.name)
     
     def removeRecordForName(self, rname):
 
         self._db_execute("delete from NOTIFICATIONS where NAME = :1", rname)
+        self._db_execute(
+            """
+            update REVISIONS SET REVISION = :1, DELETED = :2
+            where NAME = :3
+            """, self.bumpRevision(fast=True), 'Y', rname
+        )
     
+    def whatchanged(self, revision):
+
+        results = [(name.encode("utf-8"), deleted) for name, deleted in self._db_execute("select NAME, DELETED from REVISIONS where REVISION > :1", revision)]
+        results.sort(key=lambda x:x[1])
+        
+        changed = []
+        deleted = []
+        for name, wasdeleted in results:
+            if name:
+                if wasdeleted == 'Y':
+                    if revision:
+                        deleted.append(name)
+                else:
+                    changed.append(name)
+            else:
+                raise SyncTokenValidException
+        
+        return changed, deleted,
+
+    def lastRevision(self):
+        return self._db_value_for_sql(
+            "select REVISION from REVISION_SEQUENCE"
+        )
+
+    def bumpRevision(self, fast=False):
+        self._db_execute(
+            """
+            update REVISION_SEQUENCE set REVISION = REVISION + 1
+            """,
+        )
+        self._db_commit()
+        return self._db_value_for_sql(
+            """
+            select REVISION from REVISION_SEQUENCE
+            """,
+        )
+
     def _db_version(self):
         """
         @return: the schema version assigned to this index.
@@ -215,6 +268,39 @@
             """
         )
 
+        #
+        # REVISIONS table tracks changes
+        #   NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key)
+        #   REVISION: revision number
+        #   WASDELETED: Y if revision deleted, N if added or changed
+        #
+        q.execute(
+            """
+            create table REVISION_SEQUENCE (
+                REVISION        integer
+            )
+            """
+        )
+        q.execute(
+            """
+            insert into REVISION_SEQUENCE (REVISION) values (0)
+            """
+        )
+        q.execute(
+            """
+            create table REVISIONS (
+                NAME            text unique,
+                REVISION        integer,
+                DELETED         text(1)
+            )
+            """
+        )
+        q.execute(
+            """
+            create index REVISION on REVISIONS (REVISION)
+            """
+        )
+
     def _db_upgrade_data_tables(self, q, old_version):
         """
         Upgrade the data from an older version of the DB.

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -2008,6 +2008,12 @@
         """
         return config.UserQuota if config.UserQuota != 0 else None
 
+    def currentQuotaUse(self, request):
+        """
+        Get the quota use value
+        """  
+        return maybeDeferred(self._newStoreHome.quotaUsedBytes)
+
     def supportedReports(self):
         result = super(CommonHomeResource, self).supportedReports()
         if config.EnableSyncReport:
@@ -2546,7 +2552,6 @@
         returnValue((changed, deleted, notallowed))
 
 
-
 class GlobalAddressBookResource (ReadOnlyResourceMixIn, CalDAVResource):
     """
     Global address book. All we care about is making sure permissions are setup.

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -280,12 +280,7 @@
         returnValue(sorted(children))
 
 
-    def quotaSize(self, request):
-        # FIXME: tests, workingness
-        return succeed(0)
 
-
-
 class StoreScheduleInboxResource(_CalendarChildHelper, ScheduleInboxResource):
 
     def __init__(self, *a, **kw):
@@ -774,7 +769,7 @@
         isowner = (yield self.isOwner(request, adminprincipals=True, readprincipals=True))
         accessPrincipal = (yield self.resourceOwnerPrincipal(request))
 
-        for name, uid, type in (yield self.index().bruteForceSearch()): #@UnusedVariable
+        for name, uid, type in (yield maybeDeferred(self.index().bruteForceSearch)): #@UnusedVariable
             try:
                 child = yield request.locateChildResource(self, name)
             except TypeError:
@@ -1043,12 +1038,7 @@
         # FIXME: should be deleted, or raise an exception
 
 
-    def quotaSize(self, request):
-        # FIXME: tests, workingness
-        return succeed(0)
 
-
-
 class CalendarObjectResource(_NewStoreFileMetaDataHelper, CalDAVResource, FancyEqMixin):
     """
     A resource wrapping a calendar object.
@@ -1104,10 +1094,8 @@
         return True
 
 
-    @inlineCallbacks
     def quotaSize(self, request):
-        # FIXME: tests
-        returnValue(len((yield self.iCalendarText())))
+        return succeed(self._newStoreObject.size())
 
 
     def iCalendarText(self):
@@ -1132,6 +1120,7 @@
 
     @inlineCallbacks
     def storeStream(self, stream):
+
         # FIXME: direct tests
         component = vcomponent.VComponent.fromString(
             (yield allDataFromStream(stream))
@@ -1199,13 +1188,6 @@
         if not isinbox:
             self.validIfScheduleMatch(request)
 
-        # Do quota checks before we start deleting things
-        myquota = (yield self.quota(request))
-        if myquota is not None:
-            old_size = (yield self.quotaSize(request))
-        else:
-            old_size = 0
-
         scheduler = None
         lock = None
         if not isinbox and implicitly:
@@ -1240,10 +1222,6 @@
             del self._newStoreObject
             self.__class__ = ProtoCalendarObjectResource
 
-            # Adjust quota
-            if myquota is not None:
-                yield self.quotaSizeAdjust(request, -old_size)
-
             # Do scheduling
             if not isinbox and implicitly:
                 yield scheduler.doImplicitScheduling()
@@ -1319,7 +1297,6 @@
         return self._name
 
     def quotaSize(self, request):
-        # FIXME: tests, workingness
         return succeed(0)
 
 
@@ -1427,12 +1404,6 @@
 
 
 
-    def quotaSize(self, request):
-        # FIXME: tests, workingness
-        return succeed(0)
-
-
-
 class AddressBookCollectionResource(_AddressBookChildHelper, CalDAVResource):
     """
     Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
@@ -1677,11 +1648,6 @@
         # FIXME: should be deleted, or raise an exception
 
 
-    def quotaSize(self, request):
-        # FIXME: tests, workingness
-        return succeed(0)
-
-
 class GlobalAddressBookCollectionResource(GlobalAddressBookResource, AddressBookCollectionResource):
     """
     Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
@@ -1722,10 +1688,8 @@
         return True
 
 
-    @inlineCallbacks
     def quotaSize(self, request):
-        # FIXME: tests
-        returnValue(len((yield self._newStoreObject.vCardText())))
+        return succeed(self._newStoreObject.size())
 
 
     def vCardText(self, ignored=None):
@@ -1756,6 +1720,7 @@
 
     @inlineCallbacks
     def storeStream(self, stream):
+
         # FIXME: direct tests
         component = VCard.fromString(
             (yield allDataFromStream(stream))
@@ -1769,12 +1734,6 @@
         """
         Remove this addressbook object.
         """
-        # Do quota checks before we start deleting things
-        myquota = (yield self.quota(request))
-        if myquota is not None:
-            old_size = (yield self.quotaSize(request))
-        else:
-            old_size = 0
 
         try:
 
@@ -1792,10 +1751,6 @@
             del self._newStoreObject
             self.__class__ = ProtoAddressBookObjectResource
 
-            # Adjust quota
-            if myquota is not None:
-                yield self.quotaSizeAdjust(request, -old_size)
-
         except MemcacheLockTimeoutError:
             raise HTTPError(StatusResponse(CONFLICT, "Resource: %s currently in use on the server." % (where,)))
 
@@ -1950,12 +1905,7 @@
         returnValue(children)
 
 
-    def quotaSize(self, request):
-        # FIXME: tests, workingness
-        return succeed(0)
 
-
-
 class StoreNotificationCollectionResource(_NotificationChildHelper,
                                       NotificationCollectionResource):
     """
@@ -2079,12 +2029,7 @@
         # FIXME: should be deleted, or raise an exception
 
 
-    def quotaSize(self, request):
-        # FIXME: tests, workingness
-        return succeed(0)
 
-
-
 class StoreNotificationObjectFile(NotificationResource):
     """
     A resource wrapping a calendar object.
@@ -2130,10 +2075,8 @@
         return self._newStoreObject.properties()
 
 
-    @inlineCallbacks
     def quotaSize(self, request):
-        # FIXME: tests
-        returnValue(len((yield self._newStoreObject.xmldata())))
+        return succeed(self._newStoreObject.size())
 
 
     def text(self, ignored=None):
@@ -2163,13 +2106,6 @@
         """
         Remove this notification object.
         """
-        # Do quota checks before we start deleting things
-        myquota = (yield self.quota(request))
-        if myquota is not None:
-            old_size = (yield self.quotaSize(request))
-        else:
-            old_size = 0
-
         try:
 
             storeNotifications = self._newStoreObject.notificationCollection()
@@ -2186,10 +2122,6 @@
             del self._newStoreObject
             self.__class__ = ProtoStoreNotificationObjectFile
 
-            # Adjust quota
-            if myquota is not None:
-                yield self.quotaSizeAdjust(request, -old_size)
-
         except MemcacheLockTimeoutError:
             raise HTTPError(StatusResponse(CONFLICT, "Resource: %s currently in use on the server." % (where,)))
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/file.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/caldav/datastore/file.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -235,6 +235,9 @@
 
     @writeOperation
     def setComponent(self, component, inserting=False):
+
+        old_size = 0 if inserting else self.size()
+
         validateCalendarComponent(self, self._calendar, component, inserting)
 
         self._calendar.retrieveOldIndex().addResource(
@@ -269,11 +272,16 @@
             # Now re-write the original properties on the updated file
             self.properties().flush()
 
+            # Adjust quota
+            quota_adjustment = self.size() - old_size
+            self._calendar._home.adjustQuotaUsedBytes(quota_adjustment)
+
             def undo():
                 if backup:
                     backup.moveTo(self._path)
                 else:
                     self._path.remove()
+                self._calendar._home.adjustQuotaUsedBytes(-quota_adjustment)
             return undo
         self._transaction.addOperation(do, "set calendar component %r" % (self.name(),))
 
@@ -363,11 +371,18 @@
         Implement L{ICalendarObject.removeAttachmentWithName}.
         """
         # FIXME: rollback, tests for rollback
+
+        attachment = (yield self.attachmentWithName(name))
+        old_size = attachment.size()
+
         (yield self._dropboxPath()).child(name).remove()
         if name in self._attachments:
             del self._attachments[name]
 
+        # Adjust quota
+        self._calendar._home.adjustQuotaUsedBytes(-old_size)
 
+
     @inlineCallbacks
     def attachmentWithName(self, name):
         # Attachments can be local or remote, but right now we only care about
@@ -458,6 +473,9 @@
 
 
     def loseConnection(self):
+        
+        old_size = self._attachment.size()
+
         # FIXME: do anything
         self._file.close()
 
@@ -465,6 +483,9 @@
         props = self._attachment.properties()
         props[contentTypeKey] = GETContentType(generateContentType(self._contentType))
         props[md5key] = TwistedGETContentMD5.fromString(md5)
+
+        # Adjust quota
+        self._attachment._calendarObject._calendar._home.adjustQuotaUsedBytes(self._attachment.size() - old_size)
         props.flush()
 
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/index_file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/index_file.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/caldav/datastore/index_file.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -28,7 +28,6 @@
     "MemcachedUIDReserver",
     "Index",
     "IndexSchedule",
-    "IndexedSearchException",
 ]
 
 import datetime
@@ -269,7 +268,7 @@
             self.log_info("Search falls outside range of index for %s %s" % (name, minDate))
             self.reExpandResource(name, minDate)
 
-    def whatchanged(self, revision, depth):
+    def whatchanged(self, revision):
 
         results = [(name.encode("utf-8"), deleted) for name, deleted in self._db_execute("select NAME, DELETED from REVISIONS where REVISION > :1", revision)]
         results.sort(key=lambda x:x[1])
@@ -341,7 +340,7 @@
                         maxDate += datetime.timedelta(days=365)
                     self.testAndUpdateIndex(maxDate)
             else:
-                # We cannot handler this filter in an indexed search
+                # We cannot handle this filter in an indexed search
                 raise IndexedSearchException()
 
         else:
@@ -368,7 +367,7 @@
                 rowiter = self._db_execute("select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE" + qualifiers[0], *qualifiers[1])
 
         # Check result for missing resources
-
+        results = []
         for row in rowiter:
             name = row[0]
             if self.resource.getChild(name.encode("utf-8")):
@@ -377,12 +376,14 @@
                     if row[9]:
                         row[8] = row[9]
                     del row[9]
-                yield row
+                results.append(row)
             else:
                 log.err("Calendar resource %s is missing from %s. Removing from index."
                         % (name, self.resource))
                 self.deleteResource(name)
 
+        return results
+
     def bruteForceSearch(self):
         """
         List the whole index and tests for existence, updating the index
@@ -393,16 +394,19 @@
 
         # Check result for missing resources:
 
+        results = []
         for row in rowiter:
             name = row[0]
             if self.resource.getChild(name.encode("utf-8")):
-                yield row
+                results.append(row)
             else:
                 log.err("Calendar resource %s is missing from %s. Removing from index."
                         % (name, self.resource))
                 self.deleteResource(name)
 
+        return results
 
+
     def _db_version(self):
         """
         @return: the schema version assigned to this index.
@@ -666,7 +670,7 @@
                 expand = datetime.date.today() + default_future_expansion_duration
     
             if expand > (datetime.date.today() + maximum_future_expansion_duration):
-                raise IndexedSearchException
+                raise IndexedSearchException()
 
         try:
             instances = calendar.expandTimeRanges(expand, ignoreInvalidInstances=reCreate)

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -53,7 +53,7 @@
     SQLLegacyCalendarShares, PostgresLegacyInboxIndexEmulator
 from txdav.common.datastore.sql_tables import CALENDAR_TABLE,\
     CALENDAR_BIND_TABLE, CALENDAR_OBJECT_REVISIONS_TABLE, CALENDAR_OBJECT_TABLE,\
-    _ATTACHMENTS_MODE_WRITE
+    _ATTACHMENTS_MODE_WRITE, CALENDAR_HOME_TABLE
 from txdav.base.propertystore.base import PropertyName
 
 from vobject.icalendar import utc
@@ -66,14 +66,15 @@
 
     implements(ICalendarHome)
 
-    def __init__(self, transaction, ownerUID, resourceID, notifier):
+    def __init__(self, transaction, ownerUID, notifier):
 
+        self._homeTable = CALENDAR_HOME_TABLE
         self._childClass = Calendar
         self._childTable = CALENDAR_TABLE
         self._bindTable = CALENDAR_BIND_TABLE
         self._revisionsTable = CALENDAR_OBJECT_REVISIONS_TABLE
 
-        super(CalendarHome, self).__init__(transaction, ownerUID, resourceID, notifier)
+        super(CalendarHome, self).__init__(transaction, ownerUID, notifier)
         self._shares = SQLLegacyCalendarShares(self)
 
     createCalendarWithName = CommonHome.createChildWithName
@@ -244,6 +245,9 @@
 
     @inlineCallbacks
     def setComponent(self, component, inserting=False):
+
+        old_size = 0 if inserting else self.size()
+
         validateCalendarComponent(self, self._calendar, component, inserting)
 
         yield self.updateDatabase(component, inserting=inserting)
@@ -252,6 +256,9 @@
         else:
             yield self._calendar._updateRevision(self._name)
 
+        # Adjust quota
+        yield self._calendar._home.adjustQuotaUsedBytes(self.size() - old_size)
+
         self._calendar.notifyChanged()
 
 
@@ -485,7 +492,8 @@
 
     @inlineCallbacks
     def removeAttachmentWithName(self, name):
-        attachment = Attachment(self, name)
+        attachment = (yield self.attachmentWithName(name))
+        old_size = attachment.size()
         self._txn.postCommit(attachment._path.remove)
         yield self._txn.execSQL(
             """
@@ -494,7 +502,10 @@
             """, [self._resourceID, name]
         )
 
+        # Adjust quota
+        yield self._calendar._home.adjustQuotaUsedBytes(-old_size)
 
+
     @inlineCallbacks
     def attachmentWithName(self, name):
         attachment = Attachment(self, name)
@@ -578,6 +589,9 @@
 
     @inlineCallbacks
     def loseConnection(self):
+        
+        old_size = self.attachment.size()
+
         self.attachment._path.setContent(self.buf)
         self.attachment._contentType = self.contentType
         self.attachment._md5 = self.hash.hexdigest()
@@ -597,6 +611,8 @@
             ]
         ))[0]
 
+        # Adjust quota
+        yield self.attachment._calendarObject._calendar._home.adjustQuotaUsedBytes(self.attachment.size() - old_size)
 
 
 class Attachment(object):
@@ -606,6 +622,7 @@
     def __init__(self, calendarObject, name):
         self._calendarObject = calendarObject
         self._name = name
+        self._size = 0
 
 
     @property

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -1147,7 +1147,7 @@
         @inlineCallbacks
         def deleteIt(ignored):
             obj = yield self.calendarObjectUnderTest()
-            obj.removeAttachmentWithName("new.attachment")
+            yield obj.removeAttachmentWithName("new.attachment")
             obj = yield refresh(obj)
             self.assertIdentical(
                 None, (yield obj.attachmentWithName("new.attachment"))

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_index_file.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -927,8 +927,7 @@
         )
         
         for revision, results in tests:
-            for depth in ("1", "infinity"):
-                self.assertEquals(self.db.whatchanged(revision, depth), results, "Mismatched results for whatchanged with revision %d" % (revision,))
+            self.assertEquals(self.db.whatchanged(revision), results, "Mismatched results for whatchanged with revision %d" % (revision,))
 
 class MemcacheTests(SQLIndexTests):
     def setUp(self):

Modified: CalendarServer/trunk/txdav/carddav/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/file.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/carddav/datastore/file.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -165,6 +165,9 @@
 
     @writeOperation
     def setComponent(self, component, inserting=False):
+
+        old_size = 0 if inserting else self.size()
+
         validateAddressBookComponent(self, self._addressbook, component, inserting)
 
         self._addressbook.retrieveOldIndex().addResource(
@@ -199,11 +202,16 @@
             # Now re-write the original properties on the updated file
             self.properties().flush()
 
+            # Adjust quota
+            quota_adjustment = self.size() - old_size
+            self._addressbook._home.adjustQuotaUsedBytes(quota_adjustment)
+
             def undo():
                 if backup:
                     backup.moveTo(self._path)
                 else:
                     self._path.remove()
+                self._addressbook._home.adjustQuotaUsedBytes(-quota_adjustment)
             return undo
         self._transaction.addOperation(do, "set addressbook component %r" % (self.name(),))
 

Modified: CalendarServer/trunk/txdav/carddav/datastore/index_file.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/index_file.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/carddav/datastore/index_file.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -380,7 +380,7 @@
         results = self._db_values_for_sql(statement, *names)
         return results
     
-    def whatchanged(self, revision, depth):
+    def whatchanged(self, revision):
 
         results = [(name.encode("utf-8"), deleted) for name, deleted in self._db_execute("select NAME, DELETED from REVISIONS where REVISION > :1", revision)]
         results.sort(key=lambda x:x[1])

Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/sql.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -48,7 +48,7 @@
     CommonObjectResource
 from txdav.common.datastore.sql_tables import ADDRESSBOOK_TABLE,\
     ADDRESSBOOK_BIND_TABLE, ADDRESSBOOK_OBJECT_REVISIONS_TABLE,\
-    ADDRESSBOOK_OBJECT_TABLE
+    ADDRESSBOOK_OBJECT_TABLE, ADDRESSBOOK_HOME_TABLE
 from txdav.base.propertystore.base import PropertyName
 
 
@@ -57,14 +57,15 @@
 
     implements(IAddressBookHome)
 
-    def __init__(self, transaction, ownerUID, resourceID, notifier):
+    def __init__(self, transaction, ownerUID, notifier):
 
+        self._homeTable = ADDRESSBOOK_HOME_TABLE
         self._childClass = AddressBook
         self._childTable = ADDRESSBOOK_TABLE
         self._bindTable = ADDRESSBOOK_BIND_TABLE
         self._revisionsTable = ADDRESSBOOK_OBJECT_REVISIONS_TABLE
 
-        super(AddressBookHome, self).__init__(transaction, ownerUID, resourceID, notifier)
+        super(AddressBookHome, self).__init__(transaction, ownerUID, notifier)
         self._shares = SQLLegacyAddressBookShares(self)
 
 
@@ -178,6 +179,9 @@
 
     @inlineCallbacks
     def setComponent(self, component, inserting=False):
+
+        old_size = 0 if inserting else self.size()
+
         validateAddressBookComponent(self, self._addressbook, component, inserting)
 
         yield self.updateDatabase(component, inserting=inserting)
@@ -186,6 +190,9 @@
         else:
             yield self._addressbook._updateRevision(self._name)
 
+        # Adjust quota
+        yield self._addressbook._home.adjustQuotaUsedBytes(self.size() - old_size)
+
         self._addressbook.notifyChanged()
 
 

Modified: CalendarServer/trunk/txdav/carddav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/common.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/common.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -413,7 +413,7 @@
         transaction, even if it has not yet been committed.
         """
         addressbook1 = yield self.addressbookUnderTest()
-        addressbook1.removeAddressBookObjectWithName("2.vcf")
+        yield addressbook1.removeAddressBookObjectWithName("2.vcf")
         addressbookObjects = list((yield addressbook1.addressbookObjects()))
         self.assertEquals(set(o.name() for o in addressbookObjects),
                           set(addressbook1_objectNames) - set(["2.vcf"]))

Modified: CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/test_index_file.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -197,8 +197,7 @@
         )
         
         for revision, results in tests:
-            for depth in ("1", "infinity"):
-                self.assertEquals(self.db.whatchanged(revision, depth), results, "Mismatched results for whatchanged with revision %d" % (revision,))
+            self.assertEquals(self.db.whatchanged(revision), results, "Mismatched results for whatchanged with revision %d" % (revision,))
 
 class MemcacheTests(SQLIndexTests):
     def setUp(self):

Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/common/datastore/file.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -23,8 +23,10 @@
 from twext.web2.dav.element.rfc2518 import ResourceType, GETContentType, HRef
 from twext.web2.dav.element.rfc5842 import ResourceID
 from twext.web2.http_headers import generateContentType, MimeType
-from twext.web2.dav.resource import TwistedGETContentMD5
+from twext.web2.dav.resource import TwistedGETContentMD5,\
+    TwistedQuotaUsedProperty
 
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
 from twisted.python.util import FancyEqMixin
 from twisted.python import hashlib
 
@@ -511,6 +513,27 @@
 
         child.notifyChanged()
 
+    @inlineCallbacks
+    def syncToken(self):
+        
+        maxrev = 0
+        for child in self.children():
+            maxrev = max(int((yield child.syncToken()).split("#")[1]), maxrev)
+            
+        try:
+            urnuuid = str(self.properties()[PropertyName.fromElement(ResourceID)].children[0])
+        except KeyError:
+            urnuuid = uuid.uuid4().urn
+            self.properties()[PropertyName(*ResourceID.qname())] = ResourceID(HRef.fromString(urnuuid))
+        returnValue("%s#%s" % (urnuuid[9:], maxrev))
+
+
+    def resourceNamesSinceToken(self, token, depth):
+        deleted = []
+        changed = []
+        return succeed((changed, deleted))
+
+
     # @cached
     def properties(self):
         # FIXME: needs tests for actual functionality
@@ -520,6 +543,27 @@
         self._transaction.addOperation(props.flush, "flush home properties")
         return props
 
+    def quotaUsedBytes(self):
+
+        try:
+            return int(str(self.properties()[PropertyName.fromElement(TwistedQuotaUsedProperty)]))
+        except KeyError:
+            return 0
+
+    def adjustQuotaUsedBytes(self, delta):
+        """
+        Adjust quota used. We need to get a lock on the row first so that the adjustment
+        is done atomically.
+        """
+        
+        old_used = self.quotaUsedBytes()
+        new_used = old_used + delta
+        if new_used < 0:
+            self.log_error("Fixing quota adjusted below zero to %s by change amount %s" % (new_used, delta,))
+            new_used = 0
+        self.properties()[PropertyName.fromElement(TwistedQuotaUsedProperty)] = TwistedQuotaUsedProperty(str(new_used))
+            
+
     def notifierID(self, label="default"):
         if self._notifier:
             return self._notifier.getID(label)
@@ -714,6 +758,10 @@
 
         objectResourcePath = self._path.child(name)
         if objectResourcePath.isfile():
+            # Handle quota adjustment
+            child = self.objectResourceWithName(name)
+            old_size = child.size()
+
             self._removedObjectResources.add(name)
             # FIXME: test for undo
             def do():
@@ -721,6 +769,10 @@
                 return lambda: None
             self._transaction.addOperation(do, "remove object resource object %r" %
                                            (name,))
+
+            # Adjust quota
+            self._home.adjustQuotaUsedBytes(-old_size)
+
             self.notifyChanged()
         else:
             raise NoSuchObjectResourceError(name)
@@ -739,13 +791,17 @@
         except KeyError:
             urnuuid = uuid.uuid4().urn
             self.properties()[PropertyName(*ResourceID.qname())] = ResourceID(HRef.fromString(urnuuid))
-        return "%s#%s" % (urnuuid[9:], self.retrieveOldIndex().lastRevision())
+        return succeed("%s#%s" % (urnuuid[9:], self.retrieveOldIndex().lastRevision()))
 
 
     def objectResourcesSinceToken(self, token):
         raise NotImplementedError()
 
 
+    def resourceNamesSinceToken(self, token):
+        return succeed(self.retrieveOldIndex().whatchanged(token))
+
+
     # FIXME: property writes should be a write operation
     @cached
     def properties(self):

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -216,7 +216,11 @@
     Transaction implementation for SQL database.
     """
     _homeClass = {}
+    _homeTable = {}
 
+    noisy = False
+    id = 0
+
     def __init__(self, store, connectionFactory,
                  enableCalendars, enableAddressBooks,
                  notifierFactory, label, migrating=False):
@@ -229,6 +233,8 @@
         self._notifierFactory = notifierFactory
         self._label = label
         self._migrating = migrating
+        CommonStoreTransaction.id += 1
+        self._txid = CommonStoreTransaction.id
 
         extraInterfaces = []
         if enableCalendars:
@@ -241,6 +247,8 @@
         from txdav.carddav.datastore.sql import AddressBookHome
         CommonStoreTransaction._homeClass[ECALENDARTYPE] = CalendarHome
         CommonStoreTransaction._homeClass[EADDRESSBOOKTYPE] = AddressBookHome
+        CommonStoreTransaction._homeTable[ECALENDARTYPE] = CALENDAR_HOME_TABLE
+        CommonStoreTransaction._homeTable[EADDRESSBOOKTYPE] = ADDRESSBOOK_HOME_TABLE
         self._holder = ThreadHolder(reactor)
         self._holder.start()
         def initCursor():
@@ -273,8 +281,6 @@
             return None
 
 
-    noisy = False
-
     def execSQL(self, *args, **kw):
         result = self._holder.submit(
             lambda : self._reallyExecSQL(*args, **kw)
@@ -283,8 +289,8 @@
             def reportResult(results):
                 sys.stdout.write("\n".join([
                     "",
-                    "SQL: %r %r" % (args, kw),
-                    "Results: %r" % (results,),
+                    "SQL (%d): %r %r" % (self._txid, args, kw),
+                    "Results (%d): %r" % (self._txid, results,),
                     "",
                     ]))
                 return results
@@ -310,19 +316,20 @@
 
     @inlineCallbacks
     def homeWithUID(self, storeType, uid, create=False):
-        if storeType == ECALENDARTYPE:
-            homeTable = CALENDAR_HOME_TABLE
-        elif storeType == EADDRESSBOOKTYPE:
-            homeTable = ADDRESSBOOK_HOME_TABLE
-        else:
+        if storeType not in (ECALENDARTYPE, EADDRESSBOOKTYPE):
             raise RuntimeError("Unknown home type.")
 
-        data = yield self.execSQL(
-            "select %(column_RESOURCE_ID)s from %(name)s"
-            " where %(column_OWNER_UID)s = %%s" % homeTable,
-            [uid]
-        )
-        if not data:
+        if self._notifierFactory:
+            notifier = self._notifierFactory.newNotifier(
+                id=uid, prefix=NotifierPrefixes[storeType]
+            )
+        else:
+            notifier = None
+        homeObject = self._homeClass[storeType](self, uid, notifier)
+        homeObject = (yield homeObject.initFromStore())
+        if homeObject is not None:
+            returnValue(homeObject)
+        else:
             if not create:
                 returnValue(None)
             # Need to lock to prevent race condition
@@ -331,34 +338,49 @@
             # does allow concurrent reads so the only thing we block is other
             # attempts to provision a home, which is not too bad
             yield self.execSQL(
-                "lock %(name)s in exclusive mode" % homeTable,
+                "lock %(name)s in exclusive mode" % CommonStoreTransaction._homeTable[storeType],
             )
             # Now test again
-            data = yield self.execSQL(
+            exists = yield self.execSQL(
                 "select %(column_RESOURCE_ID)s from %(name)s"
-                " where %(column_OWNER_UID)s = %%s" % homeTable,
+                " where %(column_OWNER_UID)s = %%s" % CommonStoreTransaction._homeTable[storeType],
                 [uid]
             )
-            if not data:
+            if not exists:
                 yield self.execSQL(
-                    "insert into %(name)s (%(column_OWNER_UID)s) values (%%s)" % homeTable,
+                    "insert into %(name)s (%(column_OWNER_UID)s) values (%%s)" % CommonStoreTransaction._homeTable[storeType],
                     [uid]
                 )
-                home = yield self.homeWithUID(storeType, uid)
+            home = yield self.homeWithUID(storeType, uid)
+            if not exists:
                 yield home.createdHome()
-                returnValue(home)
-        resid = data[0][0]
-        if self._notifierFactory:
-            notifier = self._notifierFactory.newNotifier(
-                id=uid, prefix=NotifierPrefixes[storeType]
+            returnValue(home)
+
+    def createHomeWithUIDLocked(self, storeType, uid):
+        # Need to lock to prevent race condition
+        # FIXME: this is an entire table lock - ideally we want a row lock
+        # but the row does not exist yet. However, the "exclusive" mode
+        # does allow concurrent reads so the only thing we block is other
+        # attempts to provision a home, which is not too bad
+
+        if storeType not in (ECALENDARTYPE, EADDRESSBOOKTYPE):
+            raise RuntimeError("Unknown home type.")
+
+        yield self.execSQL(
+            "lock %(name)s in exclusive mode" % CommonStoreTransaction._homeTable[storeType],
+        )
+        # Now test again
+        exists = yield self.execSQL(
+            "select %(column_RESOURCE_ID)s from %(name)s"
+            " where %(column_OWNER_UID)s = %%s" % CommonStoreTransaction._homeTable[storeType],
+            [uid]
+        )
+        if not exists:
+            yield self.execSQL(
+                "insert into %(name)s (%(column_OWNER_UID)s) values (%%s)" % CommonStoreTransaction._homeTable[storeType],
+                [uid]
             )
-        else:
-            notifier = None
-        homeObject = self._homeClass[storeType](self, uid, resid, notifier)
-        yield homeObject._loadPropertyStore()
-        returnValue(homeObject)
 
-
     @memoizedKey("uid", "_notificationHomes")
     @inlineCallbacks
     def notificationsWithUID(self, uid):
@@ -426,16 +448,17 @@
 
 class CommonHome(LoggingMixIn):
 
+    _homeTable = None
     _childClass = None
     _childTable = None
     _bindTable = None
     _revisionsTable = None
     _notificationRevisionsTable = NOTIFICATION_OBJECT_REVISIONS_TABLE
 
-    def __init__(self, transaction, ownerUID, resourceID, notifier):
+    def __init__(self, transaction, ownerUID, notifier):
         self._txn = transaction
         self._ownerUID = ownerUID
-        self._resourceID = resourceID
+        self._resourceID = None
         self._shares = None
         self._children = {}
         self._sharedChildren = {}
@@ -448,6 +471,25 @@
         for key, value in self._bindTable.iteritems():
             self._revisionBindJoinTable["BIND:%s" % (key,)] = value 
 
+    @inlineCallbacks
+    def initFromStore(self):
+        """
+        Initialise this object from the store. We read in and cache all the extra metadata
+        from the DB to avoid having to do DB queries for those individually later.
+        """
+
+        result = yield self._txn.execSQL(
+            "select %(column_RESOURCE_ID)s from %(name)s"
+            " where %(column_OWNER_UID)s = %%s" % self._homeTable,
+            [self._ownerUID]
+        )
+        if result:
+            self._resourceID = result[0][0]
+            yield self._loadPropertyStore()
+            returnValue(self)
+        else:
+            returnValue(None)
+        
     def __repr__(self):
         return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
 
@@ -831,6 +873,51 @@
         return None
 
 
+    @inlineCallbacks
+    def quotaUsedBytes(self):
+        returnValue((yield self._txn.execSQL(
+            "select %(column_QUOTA_USED_BYTES)s from %(name)s"
+            " where %(column_OWNER_UID)s = %%s" % self._homeTable,
+            [self._ownerUID]
+        ))[0][0])
+
+    @inlineCallbacks
+    def adjustQuotaUsedBytes(self, delta):
+        """
+        Adjust quota used. We need to get a lock on the row first so that the adjustment
+        is done atomically. It is import to do the 'select ... for update' because a race also
+        exists in the 'update ... x = x + 1' case as seen via unit tests.
+        """
+        
+        yield self._txn.execSQL("""
+            select * from %(name)s
+            where %(column_RESOURCE_ID)s = %%s
+            for update
+            """ % self._homeTable,
+            [self._resourceID]
+        )
+
+        quotaUsedBytes = (yield self._txn.execSQL("""
+            update %(name)s
+            set %(column_QUOTA_USED_BYTES)s = %(column_QUOTA_USED_BYTES)s + %%s
+            where %(column_RESOURCE_ID)s = %%s
+            returning %(column_QUOTA_USED_BYTES)s
+            """ % self._homeTable,
+            [delta, self._resourceID]
+        ))[0][0]
+        
+        # Double check integrity
+        if quotaUsedBytes < 0:
+            log.error("Fixing quota adjusted below zero to %s by change amount %s" % (quotaUsedBytes, delta,))
+            yield self._txn.execSQL("""
+                update %(name)s
+                set %(column_QUOTA_USED_BYTES)s = 0
+                where %(column_RESOURCE_ID)s = %%s
+                """ % self._homeTable,
+                [self._resourceID]
+            )
+            
+
     def notifierID(self, label="default"):
         if self._notifier:
             return self._notifier.getID(label)
@@ -1050,35 +1137,41 @@
 
     @inlineCallbacks
     def removeObjectResourceWithName(self, name):
-        rows = yield self._txn.execSQL(
+        
+        uid, old_size = (yield self._txn.execSQL(
             "delete from %(name)s "
             "where %(column_RESOURCE_NAME)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s "
-            "returning %(column_UID)s" % self._objectTable,
+            "returning %(column_UID)s, character_length(%(column_TEXT)s)" % self._objectTable,
             [name, self._resourceID],
             raiseOnZeroRowCount=lambda:NoSuchObjectResourceError()
-        )
-        uid = rows[0][0]
+        ))[0]
         self._objects.pop(name, None)
         self._objects.pop(uid, None)
         yield self._deleteRevision(name)
 
+        # Adjust quota
+        yield self._home.adjustQuotaUsedBytes(-old_size)
+
         self.notifyChanged()
 
 
     @inlineCallbacks
     def removeObjectResourceWithUID(self, uid):
-        rows = yield self._txn.execSQL(
+
+        name, old_size = (yield self._txn.execSQL(
             "delete from %(name)s "
             "where %(column_UID)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s "
-            "returning %(column_RESOURCE_NAME)s" % self._objectTable,
+            "returning %(column_RESOURCE_NAME)s, character_length(%(column_TEXT)s)" % self._objectTable,
             [uid, self._resourceID],
             raiseOnZeroRowCount=lambda:NoSuchObjectResourceError()
-        )
-        name = rows[0][0]
+        ))[0]
         self._objects.pop(name, None)
         self._objects.pop(uid, None)
         yield self._deleteRevision(name)
 
+        # Adjust quota
+        yield self._home.adjustQuotaUsedBytes(-old_size)
+
         self.notifyChanged()
 
 
@@ -1411,7 +1504,7 @@
     @inlineCallbacks
     def _loadPropertyStore(self):
         props = yield PropertyStore.load(
-            self.uid(),
+            self._parentCollection.ownerHome().uid(),
             self._txn,
             self._resourceID
         )
@@ -1423,6 +1516,14 @@
         return self._propertyStore
 
 
+    def initPropertyStore(self, props):
+        """
+        A hook for subclasses to override in order to set up their property
+        store after it's been created.
+
+        @param props: the L{PropertyStore} from C{properties()}.
+        """
+
     def __repr__(self):
         return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
 
@@ -1453,15 +1554,7 @@
         return self._name
 
 
-    def initPropertyStore(self, props):
-        """
-        A hook for subclasses to override in order to set up their property
-        store after it's been created.
 
-        @param props: the L{PropertyStore} from C{properties()}.
-        """
-
-
     # IDataStoreResource
     def contentType(self):
         raise NotImplementedError()

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema_v1.sql	2010-11-01 01:22:09 UTC (rev 6489)
@@ -10,8 +10,9 @@
 -------------------
 
 create table CALENDAR_HOME (
-  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'),
-  OWNER_UID   varchar(255) not null unique
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'),
+  OWNER_UID        varchar(255) not null unique,
+  QUOTA_USED_BYTES integer      default 0 not null
 );
 
 
@@ -247,8 +248,9 @@
 ----------------------
 
 create table ADDRESSBOOK_HOME (
-  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'),
-  OWNER_UID   varchar(255) not null unique
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'),
+  OWNER_UID        varchar(255) not null unique,
+  QUOTA_USED_BYTES integer      default 0 not null
 );
 
 

Modified: CalendarServer/trunk/txdav/common/datastore/sql_tables.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_tables.py	2010-10-31 01:35:59 UTC (rev 6488)
+++ CalendarServer/trunk/txdav/common/datastore/sql_tables.py	2010-11-01 01:22:09 UTC (rev 6489)
@@ -20,15 +20,17 @@
 """
 
 CALENDAR_HOME_TABLE = {
-    "name"               : "CALENDAR_HOME",
-    "column_RESOURCE_ID" : "RESOURCE_ID",
-    "column_OWNER_UID"   : "OWNER_UID",
+    "name"                    : "CALENDAR_HOME",
+    "column_RESOURCE_ID"      : "RESOURCE_ID",
+    "column_OWNER_UID"        : "OWNER_UID",
+    "column_QUOTA_USED_BYTES" : "QUOTA_USED_BYTES",
 }
 
 ADDRESSBOOK_HOME_TABLE = {
-    "name"               : "ADDRESSBOOK_HOME",
-    "column_RESOURCE_ID" : "RESOURCE_ID",
-    "column_OWNER_UID"   : "OWNER_UID",
+    "name"                    : "ADDRESSBOOK_HOME",
+    "column_RESOURCE_ID"      : "RESOURCE_ID",
+    "column_OWNER_UID"        : "OWNER_UID",
+    "column_QUOTA_USED_BYTES" : "QUOTA_USED_BYTES",
 }
 
 NOTIFICATION_HOME_TABLE = {
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20101031/77f96ab2/attachment-0001.html>


More information about the calendarserver-changes mailing list