[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