[CalendarServer-changes] [7638] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Jun 20 13:03:51 PDT 2011


Revision: 7638
          http://trac.macosforge.org/projects/calendarserver/changeset/7638
Author:   glyph at apple.com
Date:     2011-06-20 13:03:49 -0700 (Mon, 20 Jun 2011)
Log Message:
-----------
Merge quota enforcement branch: add configuration and enforcement of per-home quota limits to the back-end.  Currently only for calendar attachments.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/util.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/twistedcaldav/test/test_wrapping.py
    CalendarServer/trunk/twistedcaldav/test/util.py
    CalendarServer/trunk/txdav/caldav/datastore/file.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/common.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_scheduling.py
    CalendarServer/trunk/txdav/caldav/datastore/util.py
    CalendarServer/trunk/txdav/caldav/icalendarstore.py
    CalendarServer/trunk/txdav/common/datastore/file.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/test/util.py

Property Changed:
----------------
    CalendarServer/trunk/


Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593

Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/calendarserver/tap/util.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -190,11 +190,15 @@
     if txnFactory is not None:
         return CommonSQLDataStore(
             txnFactory, notifierFactory, FilePath(config.AttachmentsRoot),
-            config.EnableCalDAV, config.EnableCardDAV
+            config.EnableCalDAV, config.EnableCardDAV,
+            quota=config.UserQuota
         )
     else:
-        return CommonFileDataStore(FilePath(config.DocumentRoot),
-            notifierFactory, config.EnableCalDAV, config.EnableCardDAV) 
+        return CommonFileDataStore(
+            FilePath(config.DocumentRoot),
+            notifierFactory, config.EnableCalDAV, config.EnableCardDAV,
+            quota=config.UserQuota
+        ) 
 
 
 

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -2081,7 +2081,7 @@
         @return: a C{int} containing the maximum allowed bytes if this
             collection is quota-controlled, or C{None} if not quota controlled.
         """
-        return config.UserQuota if config.UserQuota != 0 else None
+        return self._newStoreHome.quotaAllowedBytes()
 
     def currentQuotaUse(self, request):
         """

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -25,7 +25,11 @@
 from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
 from twext.web2.http import HTTPError, StatusResponse, Response
 from twext.web2.http_headers import ETag, MimeType
-from twext.web2.responsecode import FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED, BAD_REQUEST, OK
+from twext.web2.responsecode import (
+    FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
+    BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE
+)
+
 from twext.web2.stream import ProducerStream, readStream, MemoryStream
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue, maybeDeferred
 from twisted.internet.protocol import Protocol
@@ -43,13 +47,16 @@
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
 from twistedcaldav.method.put_common import StoreCalendarObjectResource
-from twistedcaldav.notifications import NotificationCollectionResource, NotificationResource
+from twistedcaldav.notifications import (
+    NotificationCollectionResource, NotificationResource
+)
 from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource
 from twistedcaldav.schedule import ScheduleInboxResource
 from twistedcaldav.scheduling.implicit import ImplicitScheduler
 from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
 
 from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.icommondatastore import NoSuchObjectResourceError
 from urlparse import urlsplit
 import time
@@ -1517,12 +1524,18 @@
 
         creating = (self._newStoreAttachment is None)
         if creating:
-            self._newStoreAttachment = self._newStoreObject =  (yield self._newStoreCalendarObject.createAttachmentWithName(
-                self.attachmentName,
-            ))
+            self._newStoreAttachment = self._newStoreObject = (
+                yield self._newStoreCalendarObject.createAttachmentWithName(
+                    self.attachmentName))
         t = self._newStoreAttachment.store(content_type)
         yield readStream(request.stream, t.write)
-        yield t.loseConnection()
+        try:
+            yield t.loseConnection()
+        except QuotaExceeded:
+            raise HTTPError(
+                ErrorResponse(INSUFFICIENT_STORAGE_SPACE,
+                              (dav_namespace, "quota-not-exceeded"))
+            )
         returnValue(CREATED if creating else NO_CONTENT)
 
 

Modified: CalendarServer/trunk/twistedcaldav/test/test_wrapping.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_wrapping.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/twistedcaldav/test/test_wrapping.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -46,6 +46,12 @@
     StubNotifierFactory
 
 
+from twext.web2.http import HTTPError
+from twext.web2.responsecode import INSUFFICIENT_STORAGE_SPACE
+from twext.web2.stream import MemoryStream
+from txdav.common.datastore.test.util import deriveQuota
+from twistedcaldav.test.util import patchConfig
+from twistedcaldav.directory.test.test_xmlfile import XMLFileBase
 from txdav.caldav.icalendarstore import ICalendarHome
 from txdav.carddav.iaddressbookstore import IAddressBookHome
 
@@ -72,8 +78,10 @@
     def finish(self):
         pass
 
+    remoteAddr = '127.0.0.1'
 
 
+
 class WrappingTests(TestCase):
     """
     Tests for L{twistedcaldav.static.CalDAVResource} creating the appropriate type
@@ -153,7 +161,7 @@
     requestUnderTest = None
 
     @inlineCallbacks
-    def getResource(self, path):
+    def getResource(self, path, method='GET', user=None):
         """
         Retrieve a resource from the site.
 
@@ -161,15 +169,31 @@
             slash)
 
         @type path: C{str}
+
+        @param method: the HTTP method to initialize the request with.
+            Defaults to GET.  (This should I{mostly} be irrelevant to path
+            traversal, but may be interesting to subsequent operations on
+            C{self.requestUnderTest}).
+
+        @param user: the username (shortname in the test XML file) of the user
+            to forcibly authenticate this request as.
+
+        @return: a L{Deferred} that fires with an L{IResource}.
         """
         if self.requestUnderTest is None:
-            req = self.requestForPath(path)
+            req = self.requestForPath(path, method)
             self.requestUnderTest = req
         else:
+            # How should this handle mismatched methods?
             req = self.requestUnderTest
         aResource = yield req.locateResource(
             "http://localhost:8008/" + path
         )
+        if user is not None:
+            guid = XMLFileBase.users[user]["guid"]
+            req.authnUser = req.authzUser = (
+                davxml.Principal(davxml.HRef('/principals/__uids__/' + guid + '/'))
+            )
         returnValue(aResource)
 
 
@@ -182,9 +206,9 @@
         return self.requestUnderTest._newStoreTransaction.commit()
 
 
-    def requestForPath(self, path):
+    def requestForPath(self, path, method='GET'):
         """
-        Get a L{Request} with a L{FakeChanRequest} for a given path.
+        Get a L{Request} with a L{FakeChanRequest} for a given path and method.
         """
         headers = Headers()
         headers.addRawHeader("Host", "localhost:8008")
@@ -192,13 +216,17 @@
         req = Request(
             site=self.site,
             chanRequest=chanReq,
-            command='GET',
+            command=method,
             path=path,
             version=('1', '1'),
             contentLength=0,
             headers=headers
         )
-        req.path = path # normally process( ) sets request.path
+
+        # 'process()' normally sets these.  Shame on web2, having so much
+        # partially-initialized stuff floating around.
+        req.remoteAddr = '127.0.0.1'
+        req.path = path
         req.credentialFactories = {}
         return req
 
@@ -367,6 +395,28 @@
 
 
     @inlineCallbacks
+    def test_attachmentQuotaExceeded(self):
+        """
+        Exceeding quota on an attachment returns an HTTP error code.
+        """
+        patchConfig(testCase=self, EnableDropBox=True)
+        yield self.populateOneObject("1.ics", event4_text)
+        calendarObject = yield self.getResource(
+            "/calendars/users/wsanchez/dropbox/uid4.dropbox/too-big-attachment",
+            "PUT", "wsanchez"
+        )
+        self.requestUnderTest.stream = MemoryStream(
+            "x" * deriveQuota(self.id()) * 2)
+        try:
+            result = yield calendarObject.http_PUT(self.requestUnderTest)
+        except HTTPError, he:
+            self.assertEquals(he.response.code, INSUFFICIENT_STORAGE_SPACE)
+        else:
+            self.fail("Error not raised, %r returned instead." %
+                      (result,))
+
+
+    @inlineCallbacks
     def test_lookupNewCalendarObject(self):
         """
         When a L{CalDAVResource} representing a new calendar object on a

Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/twistedcaldav/test/util.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -43,6 +43,7 @@
     DirectoryPrincipalProvisioningResource)
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 
+from txdav.common.datastore.test.util import deriveQuota
 from txdav.common.datastore.file import CommonDataStore
 
 
@@ -103,13 +104,15 @@
         addressbooks.)  By default returns a L{CommonDataStore}, but this is a
         hook for subclasses to override to provide different data stores.
         """
-        return CommonDataStore(FilePath(config.DocumentRoot), None, True, False)
+        return CommonDataStore(FilePath(config.DocumentRoot), None, True, False,
+                               quota=deriveQuota(self.id()))
 
 
     def setupCalendars(self):
         """
-        Set up the resource at /calendars (a L{DirectoryCalendarHomeProvisioningResource}),
-        and assign it as C{self.calendarCollection}.
+        Set up the resource at /calendars (a
+        L{DirectoryCalendarHomeProvisioningResource}), and assign it as
+        C{self.calendarCollection}.
         """
 
         # Need a data store

Modified: CalendarServer/trunk/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/file.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/caldav/datastore/file.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -31,8 +31,8 @@
 
 from errno import ENOENT
 
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.internet.interfaces import ITransport
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail
+
 from twisted.python.failure import Failure
 
 from txdav.base.propertystore.xattr import PropertyStore
@@ -55,12 +55,14 @@
 from txdav.caldav.datastore.index_file import Index as OldIndex,\
     IndexSchedule as OldInboxIndex
 from txdav.caldav.datastore.util import (
-    validateCalendarComponent, dropboxIDFromCalendarObject, CalendarObjectBase
+    validateCalendarComponent, dropboxIDFromCalendarObject, CalendarObjectBase,
+    StorageTransportBase
 )
 
 from txdav.common.datastore.file import (
     CommonDataStore, CommonStoreTransaction, CommonHome, CommonHomeChild,
     CommonObjectResource, CommonStubResource)
+from txdav.caldav.icalendarstore import QuotaExceeded
 
 from txdav.common.icommondatastore import (NoSuchObjectResourceError,
     InternalDataStoreError)
@@ -85,7 +87,8 @@
     _notifierPrefix = "CalDAV"
 
     def __init__(self, uid, path, calendarStore, transaction, notifiers):
-        super(CalendarHome, self).__init__(uid, path, calendarStore, transaction, notifiers)
+        super(CalendarHome, self).__init__(uid, path, calendarStore,
+                                           transaction, notifiers)
 
         self._childClass = Calendar
 
@@ -494,14 +497,14 @@
         # FIXME: rollback, tests for rollback
 
         attachment = (yield self.attachmentWithName(name))
-        old_size = attachment.size()
+        oldSize = 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)
+        self._calendar._home.adjustQuotaUsedBytes(-oldSize)
 
 
     @inlineCallbacks
@@ -577,10 +580,8 @@
 
 
 
-class AttachmentStorageTransport(object):
+class AttachmentStorageTransport(StorageTransportBase):
 
-    implements(ITransport)
-
     def __init__(self, attachment, contentType):
         """
         Initialize this L{AttachmentStorageTransport} and open its file for
@@ -589,9 +590,10 @@
         @param attachment: The attachment whose data is being filled out.
         @type attachment: L{Attachment}
         """
-        self._attachment = attachment
-        self._contentType = contentType
-        self._file = self._attachment._path.open("w")
+        super(AttachmentStorageTransport, self).__init__(
+            attachment, contentType)
+        self._path = self._attachment._path.temporarySibling()
+        self._file = self._path.open("w")
 
 
     def write(self, data):
@@ -600,20 +602,30 @@
 
 
     def loseConnection(self):
-        
-        old_size = self._attachment.size()
-
+        home = self._attachment._calendarObject._calendar._home
+        oldSize = self._attachment.size()
+        newSize = self._file.tell()
         # FIXME: do anything
         self._file.close()
 
+        if home.quotaAllowedBytes() < (home.quotaUsedBytes()
+                                       + (newSize - oldSize)):
+            self._path.remove()
+            return fail(QuotaExceeded())
+
+        self._path.moveTo(self._attachment._path)
+
         md5 = hashlib.md5(self._attachment._path.getContent()).hexdigest()
         props = self._attachment.properties()
-        props[contentTypeKey] = GETContentType(generateContentType(self._contentType))
+        props[contentTypeKey] = GETContentType(
+            generateContentType(self._contentType)
+        )
         props[md5key] = TwistedGETContentMD5.fromString(md5)
 
         # Adjust quota
-        self._attachment._calendarObject._calendar._home.adjustQuotaUsedBytes(self._attachment.size() - old_size)
+        home.adjustQuotaUsedBytes(newSize - oldSize)
         props.flush()
+        return succeed(None)
 
 
 
@@ -641,7 +653,7 @@
 
     def properties(self):
         uid = self._calendarObject._parentCollection._home.uid()
-        return PropertyStore(uid, lambda :self._path)
+        return PropertyStore(uid, lambda: self._path)
 
 
     def store(self, contentType):

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -31,7 +31,6 @@
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.error import ConnectionLost
-from twisted.internet.interfaces import ITransport
 from twisted.python import hashlib
 from twisted.python.failure import Failure
 
@@ -69,6 +68,9 @@
 from twext.enterprise.dal.syntax import Len
 
 from txdav.caldav.datastore.util import CalendarObjectBase
+from txdav.caldav.icalendarstore import QuotaExceeded
+
+from txdav.caldav.datastore.util import StorageTransportBase
 from txdav.common.icommondatastore import IndexedSearchException
 
 from pycalendar.datetime import PyCalendarDateTime
@@ -105,6 +107,7 @@
         super(CalendarHome, self).__init__(transaction, ownerUID, notifiers)
         self._shares = SQLLegacyCalendarShares(self)
 
+
     createCalendarWithName = CommonHome.createChildWithName
     removeCalendarWithName = CommonHome.removeChildWithName
     calendarWithName = CommonHome.childWithName
@@ -685,7 +688,7 @@
         yield attachment.remove()
 
     def attachmentWithName(self, name):
-        return Attachment.attachmentWithName(self._txn, self._dropboxID, name)
+        return Attachment.loadWithName(self._txn, self._dropboxID, name)
 
     def attendeesCanManageAttachments(self):
         return self._attachment == _ATTACHMENTS_MODE_WRITE
@@ -733,56 +736,73 @@
         """
         return MimeType.fromString("text/calendar; charset=utf-8")
 
-class AttachmentStorageTransport(object):
 
-    implements(ITransport)
 
-    def __init__(self, attachment, contentType):
-        self.attachment = attachment
-        self.contentType = contentType
-        self.buf = ''
-        self.hash = hashlib.md5()
+class AttachmentStorageTransport(StorageTransportBase):
 
+    def __init__(self, attachment, contentType, creating=False):
+        super(AttachmentStorageTransport, self).__init__(
+            attachment, contentType)
+        self._buf = ''
+        self._hash = hashlib.md5()
+        self._creating = creating
 
+
     @property
     def _txn(self):
-        return self.attachment._txn
+        return self._attachment._txn
 
 
     def write(self, data):
-        self.buf += data
-        self.hash.update(data)
+        if isinstance(data, buffer):
+            data = str(data)
+        self._buf += data
+        self._hash.update(data)
 
 
     @inlineCallbacks
     def loseConnection(self):
 
-        old_size = self.attachment.size()
+        # FIXME: this should be synchronously accessible; IAttachment should
+        # have a method for getting its parent just as CalendarObject/Calendar
+        # do.
 
-        self.attachment._path.setContent(self.buf)
-        self.attachment._contentType = self.contentType
-        self.attachment._md5 = self.hash.hexdigest()
-        self.attachment._size = len(self.buf)
+        # FIXME: If this method isn't called, the transaction should be
+        # prevented from committing successfully.  It's not valid to have an
+        # attachment that doesn't point to a real file.
+
+        home = (yield self._txn.calendarHomeWithResourceID(
+                    self._attachment._ownerHomeID))
+
+        oldSize = self._attachment.size()
+
+        if home.quotaAllowedBytes() < ((yield home.quotaUsedBytes())
+                                       + (len(self._buf) - oldSize)):
+            if self._creating:
+                yield self._attachment._internalRemove()
+            raise QuotaExceeded()
+
+        self._attachment._path.setContent(self._buf)
+        self._attachment._contentType = self._contentType
+        self._attachment._md5 = self._hash.hexdigest()
+        self._attachment._size = len(self._buf)
         att = schema.ATTACHMENT
-        self.attachment._created, self.attachment._modified = map(
+        self._attachment._created, self._attachment._modified = map(
             sqltime,
             (yield Update(
                 {
-                    att.CONTENT_TYPE : generateContentType(self.contentType),
-                    att.SIZE         : self.attachment._size,
-                    att.MD5          : self.attachment._md5,
+                    att.CONTENT_TYPE : generateContentType(self._contentType),
+                    att.SIZE         : self._attachment._size,
+                    att.MD5          : self._attachment._md5,
                     att.MODIFIED     : utcNowSQL
                 },
-                Where=att.PATH == self.attachment.name(),
+                Where=att.PATH == self._attachment.name(),
                 Return=(att.CREATED, att.MODIFIED)).on(self._txn))[0]
         )
 
-        home = (
-            yield self._txn.calendarHomeWithResourceID(
-                self.attachment._ownerHomeID))
         if home:
             # Adjust quota
-            yield home.adjustQuotaUsedBytes(self.attachment.size() - old_size)
+            yield home.adjustQuotaUsedBytes(self._attachment.size() - oldSize)
 
             # Send change notification to home
             yield home.notifyChanged()
@@ -796,12 +816,13 @@
 
     implements(IAttachment)
 
-    def __init__(self, txn, dropboxID, name, ownerHomeID=None):
+    def __init__(self, txn, dropboxID, name, ownerHomeID=None, justCreated=False):
         self._txn = txn
         self._dropboxID = dropboxID
         self._name = name
         self._ownerHomeID = ownerHomeID
         self._size = 0
+        self._justCreated = justCreated
 
 
     @classmethod
@@ -825,7 +846,7 @@
             pass
 
         # Now create the DB entry
-        attachment = cls(txn, dropboxID, name, ownerHomeID)
+        attachment = cls(txn, dropboxID, name, ownerHomeID, True)
         att = schema.ATTACHMENT
         yield Insert({
             att.CALENDAR_HOME_RESOURCE_ID : ownerHomeID,
@@ -840,8 +861,8 @@
 
     @classmethod
     @inlineCallbacks
-    def attachmentWithName(cls, txn, dropboxID, name):
-        attachment = Attachment(txn, dropboxID, name)
+    def loadWithName(cls, txn, dropboxID, name):
+        attachment = cls(txn, dropboxID, name)
         attachment = (yield attachment.initFromStore())
         returnValue(attachment)
 
@@ -888,7 +909,7 @@
 
 
     def store(self, contentType):
-        return AttachmentStorageTransport(self, contentType)
+        return AttachmentStorageTransport(self, contentType, self._justCreated)
 
 
     def retrieve(self, protocol):
@@ -905,19 +926,28 @@
 
     @inlineCallbacks
     def remove(self):
-        old_size = self._size
+        oldSize = self._size
         self._txn.postCommit(self._path.remove)
-        yield self._removeStatement.on(self._txn, dropboxID=self._dropboxID,
-                                       path=self._name)
+        yield self._internalRemove()
         # Adjust quota
         home = (yield self._txn.calendarHomeWithResourceID(self._ownerHomeID))
         if home:
-            yield home.adjustQuotaUsedBytes(-old_size)
+            yield home.adjustQuotaUsedBytes(-oldSize)
 
             # Send change notification to home
             yield home.notifyChanged()
 
 
+    def _internalRemove(self):
+        """
+        Just delete the row; don't do any accounting / bookkeeping.  (This is
+        for attachments that have failed to be created due to errors during
+        storage.)
+        """
+        return self._removeStatement.on(self._txn, dropboxID=self._dropboxID,
+                                        path=self._name)
+
+
     # IDataStoreObject
     def contentType(self):
         return self._contentType

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -19,6 +19,8 @@
 Tests for common calendar store API functions.
 """
 
+from StringIO import StringIO
+
 from twisted.internet.defer import Deferred, inlineCallbacks, returnValue,\
     maybeDeferred
 from twisted.internet.protocol import Protocol
@@ -49,6 +51,9 @@
 
 
 from twistedcaldav.customxml import InviteNotification, InviteSummary
+from txdav.caldav.icalendarstore import IAttachmentStorageTransport
+from txdav.caldav.icalendarstore import QuotaExceeded
+from txdav.common.datastore.test.util import deriveQuota
 from twistedcaldav.ical import Component
 
 storePath = FilePath(__file__).parent().child("calendar_store")
@@ -860,10 +865,14 @@
         self.assertIsInstance(calendar.created(), int)
         self.assertIsInstance(calendar.modified(), int)
 
-        self.assertEqual(calendar.accessMode, CommonTests.metadata1["accessMode"])
-        self.assertEqual(calendar.isScheduleObject, CommonTests.metadata1["isScheduleObject"])
-        self.assertEqual(calendar.scheduleEtags, CommonTests.metadata1["scheduleEtags"])
-        self.assertEqual(calendar.hasPrivateComment, CommonTests.metadata1["hasPrivateComment"])
+        self.assertEqual(calendar.accessMode,
+                         CommonTests.metadata1["accessMode"])
+        self.assertEqual(calendar.isScheduleObject,
+                         CommonTests.metadata1["isScheduleObject"])
+        self.assertEqual(calendar.scheduleEtags,
+                         CommonTests.metadata1["scheduleEtags"])
+        self.assertEqual(calendar.hasPrivateComment,
+                         CommonTests.metadata1["hasPrivateComment"])
 
         calendar.accessMode = Component.ACCESS_PRIVATE
         calendar.isScheduleObject = True
@@ -1535,6 +1544,7 @@
             "new.attachment",
         )
         t = attachment.store(MimeType("text", "x-fixture"))
+        self.assertProvides(IAttachmentStorageTransport, t)
         t.write("new attachment")
         t.write(" text")
         yield t.loseConnection()
@@ -1585,6 +1595,108 @@
         return self.createAttachmentTest(refresh)
 
 
+    @inlineCallbacks
+    def test_quotaAllowedBytes(self):
+        """
+        L{ICalendarHome.quotaAllowedBytes} should return the configuration value
+        passed to the calendar store's constructor.
+        """
+        expected = deriveQuota(self.id())
+        home = yield self.homeUnderTest()
+        actual = home.quotaAllowedBytes()
+        self.assertEquals(expected, actual)
+
+
+    @inlineCallbacks
+    def test_quotaTransportAddress(self):
+        """
+        Since L{IAttachmentStorageTransport} is a subinterface of L{ITransport},
+        it must provide peer and host addresses.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        name = 'a-fun-attachment'
+        attachment = yield obj.createAttachmentWithName(name)
+        transport = attachment.store(MimeType("test", "x-something"))
+        peer = transport.getPeer()
+        host = transport.getHost()
+        self.assertIdentical(peer.attachment, attachment)
+        self.assertIdentical(host.attachment, attachment)
+        self.assertIn(name, repr(peer))
+        self.assertIn(name, repr(host))
+
+
+    @inlineCallbacks
+    def exceedQuotaTest(self, getit):
+        """
+        If too many bytes are passed to the transport returned by
+        L{ICalendarObject.createAttachmentWithName},
+        L{IAttachmentStorageTransport.loseConnection} will return a L{Deferred}
+        that fails with L{QuotaExceeded}.
+        """
+        home = yield self.homeUnderTest()
+        attachment = yield getit() 
+        t = attachment.store(MimeType("text", "x-fixture"))
+        sample = "all work and no play makes jack a dull boy"
+        chunk = (sample * (home.quotaAllowedBytes() / len(sample)))
+
+        t.write(chunk)
+        t.writeSequence([chunk, chunk])
+
+        d = t.loseConnection()
+        yield self.failUnlessFailure(d, QuotaExceeded)
+
+
+    @inlineCallbacks
+    def test_exceedQuotaNew(self):
+        """
+        When quota is exceeded on a new attachment, that attachment will no
+        longer exist.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        yield self.exceedQuotaTest(
+            lambda: obj.createAttachmentWithName("too-big.attachment")
+        )
+        self.assertEquals((yield obj.attachments()), [])
+        yield self.commit()
+        obj = yield self.calendarObjectUnderTest()
+        self.assertEquals((yield obj.attachments()), [])
+
+
+    @inlineCallbacks
+    def test_exceedQuotaReplace(self):
+        """
+        When quota is exceeded while replacing an attachment, that attachment's
+        contents will not be replaced.
+        """
+        obj = yield self.calendarObjectUnderTest()
+        create = lambda: obj.createAttachmentWithName("exists.attachment")
+        get = lambda: obj.attachmentWithName("exists.attachment")
+        attachment = yield create()
+        t = attachment.store(MimeType("text", "x-fixture"))
+        sampleData = "a reasonably sized attachment"
+        t.write(sampleData)
+        yield t.loseConnection()
+        yield self.exceedQuotaTest(get)
+        def checkOriginal():
+            catch = StringIO()
+            catch.dataReceived = catch.write
+            lost = []
+            catch.connectionLost = lost.append
+            attachment.retrieve(catch)
+            expected = sampleData
+            # note: 60 is less than len(expected); trimming is just to make
+            # the error message look sane when the test fails.
+            actual = catch.getvalue()[:60]
+            self.assertEquals(actual, expected)
+        checkOriginal()
+        yield self.commit()
+        # Make sure that things go back to normal after a commit of that
+        # transaction.
+        obj = yield self.calendarObjectUnderTest()
+        attachment = yield get()
+        checkOriginal()
+
+
     def test_removeAttachmentWithName(self, refresh=lambda x:x):
         """
         L{ICalendarObject.removeAttachmentWithName} will remove the calendar
@@ -1648,7 +1760,7 @@
         yield self.calendarObjectUnderTest()
         txn = self.lastTransaction
         yield self.commit()
-        
+
         yield self.failUnlessFailure(
             maybeDeferred(txn.commit),
             AlreadyFinishedError

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -38,6 +38,7 @@
 from txdav.caldav.datastore.file import CalendarStore, CalendarHome
 from txdav.caldav.datastore.file import Calendar, CalendarObject
 
+from txdav.common.datastore.test.util import deriveQuota
 from txdav.caldav.datastore.test.common import (
     CommonTests, event4_text, event1modified_text)
 
@@ -64,8 +65,10 @@
     calendarPath.parent().makedirs()
     storePath.copyTo(calendarPath)
 
-    test.calendarStore = CalendarStore(storeRootPath, test.notifierFactory)
-    test.txn = test.calendarStore.newTransaction(test.id() + "(old)")
+    testID = test.id()
+    test.calendarStore = CalendarStore(storeRootPath, test.notifierFactory,
+                                       quota=deriveQuota(testID))
+    test.txn = test.calendarStore.newTransaction(testID + "(old)")
     assert test.calendarStore is not None, "No calendar store?"
 
 
@@ -77,6 +80,7 @@
     assert test.home1 is not None, "No calendar home?"
 
 
+
 @inlineCallbacks
 def setUpCalendar1(test):
     yield setUpHome1(test)

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_scheduling.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_scheduling.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_scheduling.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -53,7 +53,7 @@
 
     def storeUnderTest(self):
         if self.implicitStore is None:
-            sut = FileStorageTests.storeUnderTest(self)
+            sut = super(ImplicitStoreTests, self).storeUnderTest()
             self.implicitStore = ImplicitStore(sut)
         return self.implicitStore
 
@@ -62,3 +62,5 @@
 
     test_calendarObjectsWithDotFile = skipit
     test_init = skipit
+
+del FileStorageTests

Modified: CalendarServer/trunk/txdav/caldav/datastore/util.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/util.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/caldav/datastore/util.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -30,6 +30,8 @@
 
 from twistedcaldav.datafilters.privateevents import PrivateEventFilter
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
+from zope.interface.declarations import implements
+from txdav.caldav.icalendarstore import IAttachmentStorageTransport
 from twext.python.log import Logger
 log = Logger()
 
@@ -218,7 +220,6 @@
     # released versions of CalendarServer.
 
 
-
 class CalendarObjectBase(object):
     """
     Base logic shared between file- and sql-based L{ICalendarObject}
@@ -260,3 +261,63 @@
         returnValue(component)
 
 
+
+class StorageTransportAddress(object):
+    """
+    Peer / host address for L{IAttachmentStorageTransport} implementations.
+
+    @ivar attachment: the L{IAttachment} being stored.
+
+    @type attachment: L{IAttachment} provider
+
+    @ivar isHost: is this a host address or peer address?
+
+    @type isHost: C{bool}
+    """
+
+    def __init__(self, attachment, isHost):
+        """
+        Initialize with the attachment being stored.
+        """
+        self.attachment = attachment
+        self.isHost = isHost
+
+
+    def __repr__(self):
+        if self.isHost:
+            host = " (host)"
+        else:
+            host = ""
+        return '<Storing Attachment: %r%s>' % (self.attachment.name(), host)
+
+
+
+class StorageTransportBase(object):
+    """
+    Base logic shared between file- and sql-based L{IAttachmentStorageTransport}
+    implementations.
+    """
+
+    implements(IAttachmentStorageTransport)
+
+    def __init__(self, attachment, contentType):
+        """
+        Create a storage transport with a reference to an L{IAttachment} and a
+        L{twext.web2.http_headers.MimeType}.
+        """
+        self._attachment = attachment
+        self._contentType = contentType
+
+
+    def getPeer(self):
+        return StorageTransportAddress(self._attachment, False)
+
+
+    def getHost(self):
+        return StorageTransportAddress(self._attachment, True)
+
+
+    def writeSequence(self, seq):
+        return self.write(''.join(seq))
+
+

Modified: CalendarServer/trunk/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendarstore.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/caldav/icalendarstore.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -23,18 +23,30 @@
     IShareableCollection
 from txdav.idav import IDataStoreObject, IDataStore
 
+from twisted.internet.interfaces import ITransport
 from txdav.idav import INotifier
 
 
 __all__ = [
-    # Classes
+    # Interfaces
     "ICalendarTransaction",
     "ICalendarHome",
     "ICalendar",
     "ICalendarObject",
+
+    # Exceptions
+    "QuotaExceeded",
 ]
 
 
+
+class QuotaExceeded(Exception):
+    """
+    The quota for a particular user has been exceeded.
+    """
+
+
+
 class ICalendarTransaction(ICommonTransaction):
     """
     Transaction functionality required to be implemented by calendar stores.
@@ -163,15 +175,34 @@
         """
 
 
+    def quotaAllowedBytes():
+        """
+        The number of bytes of quota that the user is allowed to access.
+
+        Currently this is only enforced / tracked against attachment data.
+
+        @rtype: C{int}
+        """
+
+
     def quotaUsedBytes():
         """
         The number of bytes counted towards the user's quota.
+
+        @rtype: C{int}
         """
 
 
     def adjustQuotaUsedBytes(delta):
         """
-        Increase the number of bytes that count towards the user's quota.
+        Increase or decrease the number of bytes that count towards the user's
+        quota.
+
+        @param delta: The number of bytes to adjust the quota by.
+
+        @type delta: C{int}
+
+        @raise QuotaExceeded: when the quota is exceeded.
         """
 
 
@@ -395,12 +426,18 @@
 
     def attachmentWithName(name):
         """
-        Retrieve an attachment from this calendar object.
+        Asynchronously retrieve an attachment with the given name from this
+        calendar object.
 
         @param name: An identifier, unique to this L{ICalendarObject}, which
             names the attachment for future retrieval.
 
         @type name: C{str}
+
+        @return: a L{Deferred} which fires with an L{IAttachment} with the given
+            name, or L{None} if no such attachment exists.
+
+        @rtype: L{Deferred}
         """
         # FIXME: MIME-type?
 
@@ -432,6 +469,36 @@
 
 
 
+class IAttachmentStorageTransport(ITransport):
+    """
+    An L{IAttachmentStorageTransport} is a transport which stores the bytes
+    written to in a calendar attachment.
+
+    The user of an L{IAttachmentStorageTransport} must call C{loseConnection} on
+    its result to indicate that the attachment upload was successfully
+    completed.  If the transaction associated with this upload is committed or
+    aborted before C{loseConnection} is called, the upload will be presumed to
+    have failed, and no attachment data will be stored.
+    """
+
+    # Note: should also require IConsumer
+
+    def loseConnection():
+        """
+        The attachment has completed being uploaded successfully.
+
+        Unlike L{ITransport.loseConnection}, which returns C{None}, providers of
+        L{IAttachmentStorageTransport} must return a L{Deferred} from
+        C{loseConnection}, which may fire with a few different types of error;
+        for example, it may fail with a L{QuotaExceeded}.
+
+        If the upload fails for some reason, the transaction should be
+        terminated with L{ICalendarTransaction.abort} and this method should
+        never be called.
+        """
+
+
+
 class IAttachment(IDataStoreObject):
     """
     Information associated with an attachment to a calendar object.
@@ -439,17 +506,15 @@
 
     def store(contentType):
         """
+        Store an attachment (of the given MIME content/type).
+
         @param contentType: The content type of the data which will be stored.
+
         @type contentType: L{twext.web2.http_headers.MimeType}
 
-        @return: An L{ITransport}/L{IConsumer} provider that will store the
-            bytes passed to its 'write' method.
+        @return: A transport which stores the contents written to it.
 
-            The caller of C{store} must call C{loseConnection} on its result to
-            indicate that the attachment upload was successfully completed.  If
-            the transaction associated with this upload is committed or aborted
-            before C{loseConnection} is called, the upload will be presumed to
-            have failed, and no attachment data will be stored.
+        @rtype: L{IAttachmentStorageTransport}
         """
         # If you do a big write()/loseConnection(), how do you tell when the
         # data has actually been written?  you don't: commit() ought to return

Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/common/datastore/file.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -75,15 +75,21 @@
 
 class CommonDataStore(DataStore):
     """
-    An implementation of data store.
+    Shared logic for SQL-based data stores, between calendar and addressbook
+    storage.
 
     @ivar _path: A L{CachingFilePath} referencing a directory on disk that
         stores all calendar and addressbook data for a group of UIDs.
+
+    @ivar quota: the amount of space granted to each calendar home (in bytes)
+        for storing attachments.
+
+    @type quota: C{int}
     """
     implements(ICalendarStore)
 
     def __init__(self, path, notifierFactory, enableCalendars=True,
-        enableAddressBooks=True):
+        enableAddressBooks=True, quota=(2 ** 20)):
         """
         Create a store.
 
@@ -96,6 +102,7 @@
         self.enableAddressBooks = enableAddressBooks
         self._notifierFactory = notifierFactory
         self._transactionClass = CommonStoreTransaction
+        self.quota = quota
 
 
     def newTransaction(self, name='no name', migrating=False):
@@ -250,6 +257,10 @@
         self._cachedChildren = {}
 
 
+    def quotaAllowedBytes(self):
+        return self._transaction.store().quota
+
+
     @classmethod
     def homeWithUID(cls, txn, uid, create=False, withNotifications=False):
 

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -93,12 +93,39 @@
 }
 
 class CommonDataStore(Service, object):
+    """
+    Shared logic for SQL-based data stores, between calendar and addressbook
+    storage.
 
+    @ivar sqlTxnFactory: A 0-arg factory callable that produces an
+        L{IAsyncTransaction}.
+
+    @ivar notifierFactory: a L{twistedcaldav.notify.NotifierFactory} (or
+        similar) that produces new notifiers for homes and collections.
+
+    @ivar attachmentsPath: a L{FilePath} indicating a directory where
+        attachments may be stored.
+
+    @ivar enableCalendars: a boolean, C{True} if this data store should provide
+        L{ICalendarStore}, C{False} if not.
+
+    @ivar enableAddressBooks: a boolean, C{True} if this data store should
+        provide L{IAddressbookStore}, C{False} if not.
+
+    @ivar label: A string, used for tagging debug messages in the case where
+        there is more than one store.  (Useful mostly for unit tests.)
+
+    @ivar quota: the amount of space granted to each calendar home (in bytes)
+        for storing attachments.
+
+    @type quota: C{int}
+    """
+
     implements(ICalendarStore)
 
     def __init__(self, sqlTxnFactory, notifierFactory, attachmentsPath,
                  enableCalendars=True, enableAddressBooks=True,
-                 label="unlabeled"):
+                 label="unlabeled", quota=(2 ** 20)):
         assert enableCalendars or enableAddressBooks
 
         self.sqlTxnFactory = sqlTxnFactory
@@ -107,6 +134,7 @@
         self.enableCalendars = enableCalendars
         self.enableAddressBooks = enableAddressBooks
         self.label = label
+        self.quota = quota
 
 
     def eachCalendarHome(self):
@@ -500,6 +528,10 @@
             self._revisionBindJoinTable["BIND:%s" % (key,)] = value
 
 
+    def quotaAllowedBytes(self):
+        return self._txn.store().quota
+
+
     @classproperty
     def _resourceIDFromOwnerQuery(cls): #@NoSelf
         home = cls._homeSchema

Modified: CalendarServer/trunk/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/util.py	2011-06-20 19:39:17 UTC (rev 7637)
+++ CalendarServer/trunk/txdav/common/datastore/test/util.py	2011-06-20 20:03:49 UTC (rev 7638)
@@ -20,6 +20,8 @@
 """
 
 import gc
+from hashlib import md5
+from random import Random
 from zope.interface.verify import verifyObject
 from zope.interface.exceptions import BrokenMethodImplementation,\
     DoesNotImplement
@@ -126,11 +128,12 @@
             attachmentRoot.createDirectory()
         except OSError:
             pass
+        currentTestID = testCase.id()
         cp = ConnectionPool(self.sharedService.produceConnection)
+        quota = deriveQuota(currentTestID)
         store = CommonDataStore(
-            cp.connection, notifierFactory, attachmentRoot
+            cp.connection, notifierFactory, attachmentRoot, quota=quota
         )
-        currentTestID = testCase.id()
         store.label = currentTestID
         cp.startService()
         def stopIt():
@@ -183,6 +186,31 @@
 
 
 
+def deriveQuota(testID):
+    """
+    Derive a distinctive quota number for a specific test, based on its ID.
+    This generates a quota which is small enough that tests may trivially exceed
+    it if they wish to do so, but distinctive enough that it may be compared
+    without the risk of testing only a single value for quota.
+
+    Since SQL stores are generally built during test construction, it's awkward
+    to have tests which specifically construct a store to inspect quota-related
+    state; this allows us to have the test and the infrastructure agree on a
+    number.
+
+    @param testID: The identifier for a test, as returned by L{TestCase.id}.
+
+    @type testID: C{str}
+    """
+    h = md5(testID)
+    seed = int(h.hexdigest(), 16)
+    r = Random(seed)
+    baseline = 2000
+    fuzz = r.randint(1, 1000)
+    return baseline + fuzz
+
+
+
 @inlineCallbacks
 def populateCalendarsFrom(requirements, store):
     """
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110620/78641003/attachment-0001.html>


More information about the calendarserver-changes mailing list