Revision: 6489 http://trac.macosforge.org/projects/calendarserver/changeset/6489 Author: cdaboo@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 = {
participants (1)
-
source_changes@macosforge.org