[CalendarServer-changes] [191] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Mon Sep 25 09:49:31 PDT 2006
Revision: 191
http://trac.macosforge.org/projects/calendarserver/changeset/191
Author: cdaboo at apple.com
Date: 2006-09-25 09:49:30 -0700 (Mon, 25 Sep 2006)
Log Message:
-----------
merge -r119:190 https://svn.opensource.apple.com/repository/calendarserver/CalendarServer/branches/users/cdaboo/quota .
Modified Paths:
--------------
CalendarServer/trunk/bin/caldavd
CalendarServer/trunk/conf/caldavd-dev.plist
CalendarServer/trunk/conf/caldavd.plist
CalendarServer/trunk/conf/launchd.plist
CalendarServer/trunk/conf/repository.dtd
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.server.patch
CalendarServer/trunk/lib-patches/vObject/icalendar.patch
CalendarServer/trunk/run
CalendarServer/trunk/twistedcaldav/caldavxml.py
CalendarServer/trunk/twistedcaldav/itip.py
CalendarServer/trunk/twistedcaldav/logging.py
CalendarServer/trunk/twistedcaldav/method/put_common.py
CalendarServer/trunk/twistedcaldav/method/schedule_common.py
CalendarServer/trunk/twistedcaldav/repository.py
CalendarServer/trunk/twistedcaldav/resource.py
CalendarServer/trunk/twistedcaldav/static.py
Added Paths:
-----------
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.davxml.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.idav.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.delete.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.proppatch.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.log.patch
Modified: CalendarServer/trunk/bin/caldavd
===================================================================
--- CalendarServer/trunk/bin/caldavd 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/bin/caldavd 2006-09-25 16:49:30 UTC (rev 191)
@@ -56,10 +56,14 @@
self.keyfile = "/etc/certificates/Default.key"
self.certfile = "/etc/certificates/Default.crt"
- self.logfile = "/var/log/caldavd/server.log"
+ self.serverlogfile = "/var/log/caldavd/server.log"
+ self.errorlogfile = "/var/log/caldavd/error.log"
self.pidfile = "/var/run/caldavd.pid"
self.twistd = "/usr/share/caldavd/bin/twistd"
+
+ self.maxsize = 1048576 # 1 Mb
+ self.quota = 104857600 # 100 Mb
self.action = None
@@ -83,9 +87,12 @@
print "Only Use SSL: %s" % (self.onlyssl,)
print "SSL Private Key File: %s" % (self.keyfile,)
print "SSL Certificate File: %s" % (self.certfile,)
- print "Log File: %s" % (self.logfile,)
+ print "Server Log File: %s" % (self.serverlogfile,)
+ print "Error Log File: %s" % (self.errorlogfile,)
print "PID File: %s" % (self.pidfile,)
print "twistd Location: %s" % (self.twistd,)
+ print "Maximum Calendar Resource Size: %d bytes" % (self.maxsize,)
+ print "Global per-user quota limit: %d bytes" % (self.quota,)
def run(self):
"""
@@ -125,7 +132,7 @@
args.append(self.twistd)
if not self.daemonize:
args.append("-n")
- args.append("--logfile=%s" % (self.logfile,))
+ args.append("--logfile=%s" % (self.errorlogfile,))
args.append("--pidfile=%s" % (self.pidfile,))
args.append("-y")
args.append(tac)
@@ -247,21 +254,24 @@
# dict that maps between plist keys and class attributes
mapper = {
- "Verbose": "verbose",
- "RunStandalone": "daemonize",
- "DocumentRoot": "docroot",
- "Port": "port",
- "SSLEnable": "dossl",
- "SSLPort": "sslport",
- "SSLOnly": "onlyssl",
- "SSLPrivateKey": "keyfile",
- "SSLCertificate": "certfile",
- "LogFile": "logfile",
- "PIDFile": "pidfile",
- "Repository": "repo",
- "CreateAccounts": "doacct",
- "ResetAccountACLs": "doacl",
- "twistdLocation": "twistd",
+ "Verbose": "verbose",
+ "RunStandalone": "daemonize",
+ "DocumentRoot": "docroot",
+ "Port": "port",
+ "SSLEnable": "dossl",
+ "SSLPort": "sslport",
+ "SSLOnly": "onlyssl",
+ "SSLPrivateKey": "keyfile",
+ "SSLCertificate": "certfile",
+ "ServerLogFile": "serverlogfile",
+ "ErrorLogFile": "errorlogfile",
+ "PIDFile": "pidfile",
+ "Repository": "repo",
+ "CreateAccounts": "doacct",
+ "ResetAccountACLs": "doacl",
+ "twistdLocation": "twistd",
+ "MaximumAttachmentSizeBytes": "maxsize",
+ "UserQuotaBytes": "quota",
}
for k,v in root.items():
@@ -292,7 +302,7 @@
self.dossl = True
if not self.daemonize:
- self.logfile = "-"
+ self.errorlogfile = "-"
if not os.path.exists(self.twistd):
print "twistd does not exist: %s" % (self.twistd,)
@@ -317,17 +327,21 @@
import os
from os.path import dirname, join
-docroot = "%(docroot)s"
-repo = "%(repo)s"
-doacct = %(doacct)s
-doacl = %(doacl)s
-dossl = %(dossl)s
-keyfile = "%(keyfile)s"
-certfile = "%(certfile)s"
-onlyssl = %(onlyssl)s
-port = %(port)d
-sslport = %(sslport)d
+docroot = "%(docroot)s"
+repo = "%(repo)s"
+doacct = %(doacct)s
+doacl = %(doacl)s
+dossl = %(dossl)s
+keyfile = "%(keyfile)s"
+certfile = "%(certfile)s"
+onlyssl = %(onlyssl)s
+port = %(port)d
+sslport = %(sslport)d
+maxsize = %(maxsize)d
+quota = %(quota)d
+serverlog = "%(serverlogfile)s"
+
if not dossl and onlyssl:
dossl = True
@@ -352,26 +366,48 @@
else:
raise IOError("SSL Certificate file does not exist: %%s" %% (certfile,))
-from twisted.application.service import Application, IServiceCollection
+from twisted.application.service import Application, IServiceCollection, MultiService
from twisted.application.internet import TCPServer
-from twisted.cred.portal import Portal
-from twisted.web2.auth import basic
+from twisted.cred.portal import Portal
+from twisted.web2.auth import basic
from twisted.web2.dav import davxml, auth
+from twisted.web2.log import LogWrapperResource
from twisted.web2.server import Site
from twisted.web2.channel.http import HTTPFactory
if dossl:
from twisted.application.internet import SSLServer
- from twisted.internet.ssl import DefaultOpenSSLContextFactory
+ from twisted.internet.ssl import DefaultOpenSSLContextFactory
+from twistedcaldav.logging import RotatingFileAccessLoggingObserver
from twistedcaldav.repository import RepositoryBuilder
-builder = RepositoryBuilder(docroot, doAccounts=doacct, resetACLs=doacl)
+class Web2Service(MultiService):
+ def __init__(self, logObserver):
+ self.logObserver = logObserver
+ MultiService.__init__(self)
+
+ def startService(self):
+ MultiService.startService(self)
+ self.logObserver.start()
+
+ def stopService(self):
+ MultiService.stopService(self)
+ self.logObserver.stop()
+
+builder = RepositoryBuilder(docroot,
+ doAccounts=doacct,
+ resetACLs=doacl,
+ maxsize=maxsize,
+ quota=quota)
builder.buildFromFile(repo)
rootresource = builder.docRoot.collection.resource
application = Application("CalDAVServer")
parent = IServiceCollection(application)
+web2 = Web2Service(RotatingFileAccessLoggingObserver(serverlog))
+web2.setServiceParent(parent)
+parent = web2
portal = Portal(auth.DavRealm())
portal.registerChecker(auth.TwistedPropertyChecker())
@@ -380,10 +416,10 @@
loginInterfaces = (auth.IPrincipal,)
-site = Site(auth.AuthenticationWrapper(rootresource,
- portal,
- credentialFactories,
- loginInterfaces))
+site = Site(LogWrapperResource(auth.AuthenticationWrapper(rootresource,
+ portal,
+ credentialFactories,
+ loginInterfaces)))
factory = HTTPFactory(site)
@@ -395,16 +431,19 @@
print "Starting https server"
sslContext = DefaultOpenSSLContextFactory(keyfile, certfile)
sslserver = SSLServer(sslport, factory, sslContext).setServiceParent(parent)
-""" % {"docroot": self.docroot,
- "repo": self.repo,
- "doacct": self.doacct,
- "doacl": self.doacl,
- "dossl": self.dossl,
- "keyfile": self.keyfile,
- "certfile": self.certfile,
- "onlyssl": self.onlyssl,
- "port": self.port,
- "sslport": self.sslport}
+""" % {"docroot": self.docroot,
+ "repo": self.repo,
+ "doacct": self.doacct,
+ "doacl": self.doacl,
+ "dossl": self.dossl,
+ "keyfile": self.keyfile,
+ "certfile": self.certfile,
+ "onlyssl": self.onlyssl,
+ "port": self.port,
+ "sslport": self.sslport,
+ "maxsize": self.maxsize,
+ "quota": self.quota,
+ "serverlogfile": self.serverlogfile}
if __name__ == "__main__":
Modified: CalendarServer/trunk/conf/caldavd-dev.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-dev.plist 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/conf/caldavd-dev.plist 2006-09-25 16:49:30 UTC (rev 191)
@@ -47,9 +47,12 @@
<key>SSLCertificate</key>
<string>conf/server.pem</string>
- <key>LogFile</key>
- <string>bin/caldavd.log</string>
+ <key>ServerLogFile</key>
+ <string>bin/server.log</string>
+ <key>ErrorLogFile</key>
+ <string>bin/error.log</string>
+
<key>PIDFile</key>
<string>bin/twistd-caldavd.pid</string>
@@ -62,5 +65,17 @@
<key>ResetAccountACLs</key>
<true/>
+ <key>twistdLocation</key>
+ <string>../Twisted/bin/twistd</string>
+
+ <key>EventExpirationDays</key>
+ <integer>0</integer>
+
+ <key>MaximumAttachmentSizeBytes</key>
+ <integer>1048576</integer><!-- 1Mb -->
+
+ <key>UserQuotaBytes</key>
+ <integer>104857600</integer><!-- 100Mb -->
+
</dict>
</plist>
Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/conf/caldavd.plist 2006-09-25 16:49:30 UTC (rev 191)
@@ -47,9 +47,12 @@
<key>SSLCertificate</key>
<string>/etc/certificates/Default.crt</string>
- <key>LogFile</key>
+ <key>ServerLogFile</key>
<string>/var/log/caldavd/server.log</string>
+ <key>ErrorLogFile</key>
+ <string>/var/log/caldavd/error.log</string>
+
<key>PIDFile</key>
<string>/var/log/caldavd/caldavd.pid</string>
@@ -69,7 +72,10 @@
<integer>0</integer>
<key>MaximumAttachmentSizeBytes</key>
- <integer>102400</integer>
+ <integer>1048576</integer><!-- 1Mb -->
+ <key>UserQuotaBytes</key>
+ <integer>104857600</integer><!-- 100Mb -->
+
</dict>
</plist>
Modified: CalendarServer/trunk/conf/launchd.plist
===================================================================
--- CalendarServer/trunk/conf/launchd.plist 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/conf/launchd.plist 2006-09-25 16:49:30 UTC (rev 191)
@@ -46,7 +46,7 @@
<true/>
<key>StandardOutPath</key>
- <string>/var/log/caldavd/error.log</string>
+ <string>/var/log/caldavd/server.log</string>
<key>StandardErrorPath</key>
<string>/var/log/caldavd/error.log</string>
Modified: CalendarServer/trunk/conf/repository.dtd
===================================================================
--- CalendarServer/trunk/conf/repository.dtd 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/conf/repository.dtd 2006-09-25 16:49:30 UTC (rev 191)
@@ -50,11 +50,12 @@
<!ELEMENT accounts (user*) >
- <!ELEMENT user (uid, pswd, name, cuaddr*, calendar*, acl?, autorespond?)>
+ <!ELEMENT user (uid, pswd, name, cuaddr*, calendar*, acl?, quota?, autorespond?)>
<!ATTLIST user repeat CDATA "1">
<!ELEMENT uid (#PCDATA)>
<!ELEMENT pswd (#PCDATA)>
<!ELEMENT name (#PCDATA)>
<!ELEMENT cuaddr (#PCDATA)>
<!ELEMENT calendar (#PCDATA)>
+ <!ELEMENT quota (#PCDATA)>
<!ELEMENT autorespond EMPTY>
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.davxml.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.davxml.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.davxml.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.davxml.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,28 @@
+Index: twisted/web2/dav/davxml.py
+===================================================================
+--- twisted/web2/dav/davxml.py (revision 18219)
++++ twisted/web2/dav/davxml.py (working copy)
+@@ -45,6 +45,7 @@
+ from twisted.web2.dav.element.rfc2518 import *
+ from twisted.web2.dav.element.rfc3253 import *
+ from twisted.web2.dav.element.rfc3744 import *
++from twisted.web2.dav.element.rfc4331 import *
+
+ #
+ # Register all XML elements with the parser
+@@ -56,6 +57,7 @@
+ import twisted.web2.dav.element.rfc2518
+ import twisted.web2.dav.element.rfc3253
+ import twisted.web2.dav.element.rfc3744
++import twisted.web2.dav.element.rfc4331
+
+ __all__ = (
+ registerElements(twisted.web2.dav.element.base ) +
+@@ -62,5 +64,6 @@
+ registerElements(twisted.web2.dav.element.parser ) +
+ registerElements(twisted.web2.dav.element.rfc2518) +
+ registerElements(twisted.web2.dav.element.rfc3253) +
+- registerElements(twisted.web2.dav.element.rfc3744)
++ registerElements(twisted.web2.dav.element.rfc3744) +
++ registerElements(twisted.web2.dav.element.rfc4331)
+ )
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,10 @@
+Index: twisted/web2/dav/element/__init__.py
+===================================================================
+--- twisted/web2/dav/element/__init__.py (revision 18219)
++++ twisted/web2/dav/element/__init__.py (working copy)
+@@ -35,4 +35,5 @@
+ "rfc2518",
+ "rfc3253",
+ "rfc3744",
++ "rfc4331",
+ ]
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,60 @@
+Index: twisted/web2/dav/element/rfc4331.py
+===================================================================
+--- twisted/web2/dav/element/rfc4331.py (revision 0)
++++ twisted/web2/dav/element/rfc4331.py (revision 0)
+@@ -0,0 +1,55 @@
++##
++# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
++#
++# Permission is hereby granted, free of charge, to any person obtaining a copy
++# of this software and associated documentation files (the "Software"), to deal
++# in the Software without restriction, including without limitation the rights
++# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++# copies of the Software, and to permit persons to whom the Software is
++# furnished to do so, subject to the following conditions:
++#
++# The above copyright notice and this permission notice shall be included in all
++# copies or substantial portions of the Software.
++#
++# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++# SOFTWARE.
++#
++# DRI: Cyrus Daboo, cdaboo at apple.com
++##
++
++"""
++RFC 4331 (Quota and Size Properties for WebDAV Collections) XML Elements
++
++This module provides XML element definitions for use with WebDAV.
++
++See RFC 4331: http://www.ietf.org/rfc/rfc4331.txt
++"""
++
++from twisted.web2.dav.element.base import WebDAVTextElement
++
++##
++# Section 3 & 4 (Quota Properties)
++##
++
++class QuotaAvailableBytes (WebDAVTextElement):
++ """
++ Property which contains the the number of bytes available under the
++ current quota to store data in a collection (RFC 4331, section 3)
++ """
++ name = "quota-available-bytes"
++ hidden = True
++ protected = True
++
++class QuotaUsedBytes (WebDAVTextElement):
++ """
++ Property which contains the the number of bytes used under the
++ current quota to store data in a collection (RFC 4331, section 4)
++ """
++ name = "quota-used-bytes"
++ hidden = True
++ protected = True
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.idav.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.idav.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.idav.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.idav.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,85 @@
+Index: twisted/web2/dav/idav.py
+===================================================================
+--- twisted/web2/dav/idav.py (revision 18219)
++++ twisted/web2/dav/idav.py (working copy)
+@@ -180,6 +180,80 @@
+ the specified principal.
+ """
+
++ ##
++ # Quota
++ ##
++
++ def quota(request):
++ """
++ Get current available & used quota values for this resource's quota root
++ collection.
++
++ @return: a C{tuple} containing two C{int}'s the first is
++ quota-available-bytes, the second is quota-used-bytes, or
++ C{None} if quota is not defined on the resource.
++ """
++
++ def hasQuota(request):
++ """
++ Check whether this resource is undre quota control by checking each parent to see if
++ it has a quota root.
++
++ @return: C{True} if under quota control, C{False} if not.
++ """
++
++ def hasQuotaRoot(request):
++ """
++ Determine whether the resource has a quota root.
++
++ @return: a C{True} if this resource has quota root, C{False} otherwise.
++ """
++
++
++ def quotaRoot(request):
++ """
++ Get the quota root (max. allowed bytes) value for this collection.
++
++ @return: a C{int} containing the maximum allowed bytes if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++
++ def setQuotaRoot(request, maxsize):
++ """
++ Set the quota root (max. allowed bytes) value for this collection.
++
++ @param maxsize: a C{int} containing the maximum allowed bytes for the contents
++ of this collection.
++ """
++
++ def quotaSize(request):
++ """
++ Get the size of this resource (if its a collection get total for all children as well).
++ TODO: Take into account size of dead-properties.
++
++ @return: a L{Deferred} with a C{int} result containing the size of the resource.
++ """
++
++ def currentQuotaUse(request):
++ """
++ Get the cached quota use value, or if not present (or invalid) determine
++ quota use by brute force.
++
++ @return: an L{Deferred} with a C{int} result containing the current used byte count if
++ this collection is quota-controlled, or C{None} if not quota controlled.
++ """
++
++ def updateQuotaUse(request, adjust):
++ """
++ Adjust current quota use on this all all parent collections that also
++ have quota roots.
++
++ @param adjust: a C{int} containing the number of bytes added (positive) or
++ removed (negative) that should be used to adjust the cached total.
++ @return: an L{Deferred} with a C{int} result containing the current used byte if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++
+ class IDAVPrincipalResource (IDAVResource):
+ """
+ WebDAV principal resource. (RFC 3744, section 2)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.__init__.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,12 @@
+Index: twisted/web2/dav/method/__init__.py
+===================================================================
+--- twisted/web2/dav/method/__init__.py (revision 18219)
++++ twisted/web2/dav/method/__init__.py (working copy)
+@@ -40,6 +40,7 @@
+ "proppatch",
+ "prop_common",
+ "put",
++ "put_common",
+ "report",
+ "report_acl_principal_prop_set",
+ "report_expand",
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,46 @@
+Index: twisted/web2/dav/method/copymove.py
+===================================================================
+--- twisted/web2/dav/method/copymove.py (revision 18219)
++++ twisted/web2/dav/method/copymove.py (working copy)
+@@ -38,7 +38,7 @@
+ from twisted.web2.filter.location import addLocation
+ from twisted.web2.dav import davxml
+ from twisted.web2.dav.idav import IDAVResource
+-from twisted.web2.dav.fileop import copy, move
++from twisted.web2.dav.method import put_common
+ from twisted.web2.dav.util import parentForURL
+
+ # FIXME: This is circular
+@@ -81,7 +81,15 @@
+ # May need to add a location header
+ addLocation(request, destination_uri)
+
+- x = waitForDeferred(copy(self.fp, destination.fp, destination_uri, depth))
++ #x = waitForDeferred(copy(self.fp, destination.fp, destination_uri, depth))
++ x = waitForDeferred(put_common.storeResource(request,
++ source=self,
++ source_uri=request.uri,
++ destination=destination,
++ destination_uri=destination_uri,
++ deletesource=False,
++ depth=depth
++ ))
+ yield x
+ yield x.getResult()
+
+@@ -144,7 +152,14 @@
+ # May need to add a location header
+ addLocation(request, destination_uri)
+
+- x = waitForDeferred(move(self.fp, request.uri, destination.fp, destination_uri, depth))
++ #x = waitForDeferred(move(self.fp, request.uri, destination.fp, destination_uri, depth))
++ x = waitForDeferred(put_common.storeResource(request,
++ source=self,
++ source_uri=request.uri,
++ destination=destination,
++ destination_uri=destination_uri,
++ deletesource=True,
++ depth=depth))
+ yield x
+ yield x.getResult()
+
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.delete.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.delete.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.delete.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.delete.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,34 @@
+Index: twisted/web2/dav/method/delete.py
+===================================================================
+--- twisted/web2/dav/method/delete.py (revision 18219)
++++ twisted/web2/dav/method/delete.py (working copy)
+@@ -58,8 +58,28 @@
+ yield x
+ x.getResult()
+
++ # Do quota checks before we start deleting things
++ myquota = waitForDeferred(self.quota(request))
++ yield myquota
++ myquota = myquota.getResult()
++ if myquota is not None:
++ old_size = waitForDeferred(self.quotaSize(request))
++ yield old_size
++ old_size = old_size.getResult()
++ else:
++ old_size = 0
++
++ # Do delete
+ x = waitForDeferred(delete(request.uri, self.fp, depth))
+ yield x
+- yield x.getResult()
++ result = x.getResult()
++
++ # Adjust quota
++ if myquota is not None:
++ d = waitForDeferred(self.quotaSizeAdjust(request, -old_size))
++ yield d
++ d.getResult()
++
++ yield result
+
+ http_DELETE = deferredGenerator(http_DELETE)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.proppatch.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.proppatch.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.proppatch.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.proppatch.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,13 @@
+Index: twisted/web2/dav/method/proppatch.py
+===================================================================
+--- twisted/web2/dav/method/proppatch.py (revision 18219)
++++ twisted/web2/dav/method/proppatch.py (working copy)
+@@ -105,7 +105,7 @@
+ if has:
+ oldProperty = waitForDeferred(self.readProperty(property, request))
+ yield oldProperty
+- oldProperty.getResult()
++ oldProperty = oldProperty.getResult()
+
+ def undo():
+ return self.writeProperty(oldProperty, request)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,20 @@
+Index: twisted/web2/dav/method/put.py
+===================================================================
+--- twisted/web2/dav/method/put.py (revision 18219)
++++ twisted/web2/dav/method/put.py (working copy)
+@@ -34,7 +34,7 @@
+ from twisted.web2 import responsecode
+ from twisted.web2.http import HTTPError, StatusResponse
+ from twisted.web2.dav import davxml
+-from twisted.web2.dav.fileop import put
++from twisted.web2.dav.method import put_common
+ from twisted.web2.dav.util import parentForURL
+
+ def preconditions_PUT(self, request):
+@@ -107,4 +107,5 @@
+ # to return a MULTI_STATUS response, which is WebDAV-specific (and PUT is
+ # not).
+ #
+- return put(request.stream, self.fp)
++ #return put(request.stream, self.fp)
++ return put_common.storeResource(request, destination=self, destination_uri=request.uri)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,239 @@
+Index: twisted/web2/dav/method/put_common.py
+===================================================================
+--- twisted/web2/dav/method/put_common.py (revision 0)
++++ twisted/web2/dav/method/put_common.py (revision 0)
+@@ -0,0 +1,234 @@
++##
++# Copyright (c) 2005-2006 Apple Computer, Inc. All rights reserved.
++#
++# Licensed under the Apache License, Version 2.0 (the "License");
++# you may not use this file except in compliance with the License.
++# You may obtain a copy of the License at
++#
++# http://www.apache.org/licenses/LICENSE-2.0
++#
++# Unless required by applicable law or agreed to in writing, software
++# distributed under the License is distributed on an "AS IS" BASIS,
++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++# See the License for the specific language governing permissions and
++# limitations under the License.
++#
++# DRI: Cyrus Daboo, cdaboo at apple.com
++##
++
++"""
++PUT/COPY/MOVE common behavior.
++"""
++
++__version__ = "0.0"
++
++__all__ = ["storeCalendarObjectResource"]
++
++from twisted.internet.defer import deferredGenerator, maybeDeferred, waitForDeferred
++from twisted.python import failure, log
++from twisted.python.filepath import FilePath
++from twisted.web2 import responsecode
++from twisted.web2.dav.fileop import copy, delete, put
++from twisted.web2.http import HTTPError
++from twisted.web2.http import StatusResponse
++from twisted.web2.iweb import IResponse
++
++def storeResource(
++ request,
++ source=None, source_uri=None,
++ destination=None, destination_uri=None,
++ deletesource=False,
++ depth="0"
++):
++ """
++ Function that does common PUT/COPY/MOVE behaviour.
++
++ @param request: the L{twisted.web2.server.Request} for the current HTTP request.
++ @param source: the L{DAVFile} for the source resource to copy from, or None if source data
++ is to be read from the request.
++ @param source_uri: the URI for the source resource.
++ @param destination: the L{DAVFile} for the destination resource to copy into.
++ @param destination_uri: the URI for the destination resource.
++ @param deletesource: True if the source resource is to be deleted on successful completion, False otherwise.
++ @param depth: a C{str} containing the COPY/MOVE Depth header value.
++ @return: status response.
++ """
++
++ try:
++ assert request is not None and destination is not None and destination_uri is not None
++ assert (source is None) or (source is not None and source_uri is not None)
++ assert not deletesource or (deletesource and source is not None)
++ except AssertionError:
++ log.err("Invalid arguments to storeResource():")
++ log.err("request=%s\n" % (request,))
++ log.err("source=%s\n" % (source,))
++ log.err("source_uri=%s\n" % (source_uri,))
++ log.err("destination=%s\n" % (destination,))
++ log.err("destination_uri=%s\n" % (destination_uri,))
++ log.err("deletesource=%s\n" % (deletesource,))
++ log.err("depth=%s\n" % (depth,))
++ raise
++
++ class RollbackState(object):
++ """
++ This class encapsulates the state needed to rollback the entire PUT/COPY/MOVE
++ transaction, leaving the server state the same as it was before the request was
++ processed. The DoRollback method will actually execute the rollback operations.
++ """
++
++ def __init__(self):
++ self.active = True
++ self.source_copy = None
++ self.destination_copy = None
++ self.destination_created = False
++ self.source_deleted = False
++
++ def Rollback(self):
++ """
++ Rollback the server state. Do not allow this to raise another exception. If
++ rollback fails then we are going to be left in an awkward state that will need
++ to be cleaned up eventually.
++ """
++ if self.active:
++ self.active = False
++ log.err("Rollback: rollback")
++ try:
++ if self.source_copy and self.source_deleted:
++ self.source_copy.moveTo(source.fp)
++ log.err("Rollback: source restored %s to %s" % (self.source_copy.path, source.fp.path))
++ self.source_copy = None
++ self.source_deleted = False
++ if self.destination_copy:
++ destination.fp.remove()
++ log.err("Rollback: destination restored %s to %s" % (self.destination_copy.path, destination.fp.path))
++ self.destination_copy.moveTo(destination.fp)
++ self.destination_copy = None
++ elif self.destination_created:
++ destination.fp.remove()
++ log.err("Rollback: destination removed %s" % (destination.fp.path,))
++ self.destination_created = False
++ except:
++ log.err("Rollback: exception caught and not handled: %s" % failure.Failure())
++
++ def Commit(self):
++ """
++ Commit the resource changes by wiping the rollback state.
++ """
++ if self.active:
++ log.err("Rollback: commit")
++ self.active = False
++ if self.source_copy:
++ self.source_copy.remove()
++ log.err("Rollback: removed source backup %s" % (self.source_copy.path,))
++ self.source_copy = None
++ if self.destination_copy:
++ self.destination_copy.remove()
++ log.err("Rollback: removed destination backup %s" % (self.destination_copy.path,))
++ self.destination_copy = None
++ self.destination_created = False
++ self.source_deleted = False
++
++ rollback = RollbackState()
++
++ try:
++ """
++ Handle validation operations here.
++ """
++
++ """
++ Handle rollback setup here.
++ """
++
++ # Do quota checks on destination and source before we start messing with adding other files
++ destquota = waitForDeferred(destination.quota(request))
++ yield destquota
++ destquota = destquota.getResult()
++ if destquota is not None and destination.exists():
++ old_dest_size = waitForDeferred(destination.quotaSize(request))
++ yield old_dest_size
++ old_dest_size = old_dest_size.getResult()
++ else:
++ old_dest_size = 0
++
++ if source is not None:
++ sourcequota = waitForDeferred(source.quota(request))
++ yield sourcequota
++ sourcequota = sourcequota.getResult()
++ if sourcequota is not None and source.exists():
++ old_source_size = waitForDeferred(source.quotaSize(request))
++ yield old_source_size
++ old_source_size = old_source_size.getResult()
++ else:
++ old_source_size = 0
++ else:
++ sourcequota = None
++ old_source_size = 0
++
++ # We may need to restore the original resource data if the PUT/COPY/MOVE fails,
++ # so rename the original file in case we need to rollback.
++ overwrite = destination.exists()
++ if overwrite:
++ rollback.destination_copy = FilePath(destination.fp.path)
++ rollback.destination_copy.path += ".rollback"
++ destination.fp.copyTo(rollback.destination_copy)
++ else:
++ rollback.destination_created = True
++
++ if deletesource:
++ rollback.source_copy = FilePath(source.fp.path)
++ rollback.source_copy.path += ".rollback"
++ source.fp.copyTo(rollback.source_copy)
++
++ """
++ Handle actual store operations here.
++ """
++
++ # Do put or copy based on whether source exists
++ if source is not None:
++ response = maybeDeferred(copy, source.fp, destination.fp, destination_uri, depth)
++ else:
++ response = maybeDeferred(put, request.stream, destination.fp)
++ response = waitForDeferred(response)
++ yield response
++ response = response.getResult()
++
++ response = IResponse(response)
++
++ # Do quota check on destination
++ if destquota is not None:
++ # Get size of new/old resources
++ new_dest_size = waitForDeferred(destination.quotaSize(request))
++ yield new_dest_size
++ new_dest_size = new_dest_size.getResult()
++ diff_size = new_dest_size - old_dest_size
++ if diff_size >= destquota[0]:
++ log.err("Over quota: available %d, need %d" % (destquota[0], diff_size))
++ raise HTTPError(StatusResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, "Over quota"))
++ d = waitForDeferred(destination.quotaSizeAdjust(request, diff_size))
++ yield d
++ d.getResult()
++
++ if deletesource:
++ # Delete the source resource
++ if sourcequota is not None:
++ delete_size = 0 - old_source_size
++ d = waitForDeferred(source.quotaSizeAdjust(request, delete_size))
++ yield d
++ d.getResult()
++
++ delete(source_uri, source.fp, depth)
++ rollback.source_deleted = True
++
++ # Can now commit changes and forget the rollback details
++ rollback.Commit()
++
++ yield response
++ return
++
++ except:
++ # Roll back changes to original server state. Note this may do nothing
++ # if the rollback has already ocurred or changes already committed.
++ rollback.Rollback()
++ raise
++
++storeResource = deferredGenerator(storeResource)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.resource.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,386 @@
+Index: twisted/web2/dav/resource.py
+===================================================================
+--- twisted/web2/dav/resource.py (revision 18219)
++++ twisted/web2/dav/resource.py (working copy)
+@@ -130,6 +130,8 @@
+ (dav_namespace, "acl-restrictions" ), # RFC 3744, section 5.6
+ (dav_namespace, "inherited-acl-set" ), # RFC 3744, section 5.7
+ (dav_namespace, "principal-collection-set" ), # RFC 3744, section 5.8
++ (dav_namespace, "quota-available-bytes" ), # RFC 4331, section 3
++ (dav_namespace, "quota-used-bytes" ), # RFC 4331, section 4
+
+ (twisted_dav_namespace, "resource-class"),
+ )
+@@ -166,6 +168,14 @@
+ if qname[0] == twisted_private_namespace:
+ return succeed(False)
+
++ # Need to special case the dynamic live properties
++ namespace, name = qname
++ if namespace == dav_namespace:
++ if name in ("quota-available-bytes", "quota-used-bytes"):
++ d = self.hasQuota(request)
++ d.addCallback(lambda result: result)
++ return d
++
+ return succeed(qname in self.liveProperties or self.deadProperties().contains(qname))
+
+ def readProperty(self, property, request):
+@@ -272,6 +282,32 @@
+ d.addCallback(gotACL)
+ return d
+ return ifAllowed((davxml.ReadACL(),), callback)
++
++ if name == "quota-available-bytes":
++ def callback(qvalue):
++ if qvalue is None:
++ raise HTTPError(StatusResponse(
++ responsecode.NOT_FOUND,
++ "Property %s does not exist." % (sname,)
++ ))
++ else:
++ return davxml.QuotaAvailableBytes(str(qvalue[0]))
++ d = self.quota(request)
++ d.addCallback(callback)
++ return d
++
++ if name == "quota-used-bytes":
++ def callback(qvalue):
++ if qvalue is None:
++ raise HTTPError(StatusResponse(
++ responsecode.NOT_FOUND,
++ "Property %s does not exist." % (sname,)
++ ))
++ else:
++ return davxml.QuotaUsedBytes(str(qvalue[1]))
++ d = self.quota(request)
++ d.addCallback(callback)
++ return d
+
+ elif namespace == twisted_dav_namespace:
+ if name == "resource-class":
+@@ -352,6 +388,18 @@
+ # FIXME: A set would be better here, that that's a python 2.4+ feature.
+ qnames = list(self.liveProperties)
+
++ # Add dynamic live properties that exist
++ dynamicLiveProperties = (
++ (dav_namespace, "quota-available-bytes" ),
++ (dav_namespace, "quota-used-bytes" ),
++ )
++ for dqname in dynamicLiveProperties:
++ has = waitForDeferred(self.hasProperty(dqname, request))
++ yield has
++ has = has.getResult()
++ if not has:
++ qnames.remove(dqname)
++
+ for qname in self.deadProperties().list():
+ if (qname not in qnames) and (qname[0] != twisted_private_namespace):
+ qnames.append(qname)
+@@ -356,7 +404,9 @@
+ if (qname not in qnames) and (qname[0] != twisted_private_namespace):
+ qnames.append(qname)
+
+- return succeed(qnames)
++ yield qnames
++
++ listProperties = deferredGenerator(listProperties)
+
+ def listAllprop(self, request):
+ """
+@@ -1501,6 +1551,265 @@
+ return None
+
+ ##
++ # Quota
++ ##
++
++ """
++ The basic policy here is to define a private 'quota-root' property on a collection.
++ That property will contain the maximum allowed bytes for the collections and all
++ its contents.
++
++ In order to determine the quota property values on a resource, the server must look
++ for the private property on that resource and any of its parents. If found on a parent,
++ then that parent should be queried for quota information. If not found, no quota
++ exists for the resource.
++
++ To determine tha actual quota in use we will cache the used byte count on the quota-root
++ collection in another private property. It is the servers responsibility to
++ keep that property up to date by adjusting it after every PUT, DELETE, COPY,
++ MOVE, MKCOL, PROPPATCH, ACL, POST or any other method that may affect the size of
++ stored data. If the private property is not present, the server will fall back to
++ getting the size by iterating over all resources (this is done in static.py).
++
++ """
++
++ def quota(self, request):
++ """
++ Get current available & used quota values for this resource's quota root
++ collection.
++
++ @return: an L{Defered} with result C{tuple} containing two C{int}'s the first is
++ quota-available-bytes, the second is quota-used-bytes, or
++ C{None} if quota is not defined on the resource.
++ """
++
++ # See if already cached
++ if hasattr(request, "quota"):
++ yield request.quota
++ return
++
++ # Check this resource first
++ if self.isCollection():
++ qroot = self.quotaRoot(request)
++ if qroot is not None:
++ used = waitForDeferred(self.currentQuotaUse(request))
++ yield used
++ used = used.getResult()
++ available = qroot - used
++ if available < 0:
++ available = 0
++ request.quota = (available, used)
++ yield request.quota
++ return
++
++ # Check the next parent
++ url = request.urlForResource(self)
++ if url != "/":
++ parent = waitForDeferred(request.locateResource(parentForURL(url)))
++ yield parent
++ parent = parent.getResult()
++ d = waitForDeferred(parent.quota(request))
++ yield d
++ request.quota = d.getResult()
++ else:
++ request.quota = None
++
++ yield request.quota
++ return
++
++ quota = deferredGenerator(quota)
++
++ def hasQuota(self, request):
++ """
++ Check whether this resource is undre quota control by checking each parent to see if
++ it has a quota root.
++
++ @return: C{True} if under quota control, C{False} if not.
++ """
++
++ # Check this one first
++ if self.hasQuotaRoot(request):
++ yield True
++ return
++
++ # Look at each parent
++ url = request.urlForResource(self)
++ if url != "/":
++ parent = waitForDeferred(request.locateResource(parentForURL(url)))
++ yield parent
++ parent = parent.getResult()
++ d = waitForDeferred(parent.hasQuota(request))
++ yield d
++ yield d.getResult()
++ else:
++ yield False
++
++ hasQuota = deferredGenerator(hasQuota)
++
++ def hasQuotaRoot(self, request):
++ """
++ @return: a C{True} if this resource has quota root, C{False} otherwise.
++ """
++ return self.hasDeadProperty(TwistedQuotaRootProperty)
++
++ def quotaRoot(self, request):
++ """
++ @return: a C{int} containing the maximum allowed bytes if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++ if self.hasDeadProperty(TwistedQuotaRootProperty):
++ return int(str(self.readDeadProperty(TwistedQuotaRootProperty)))
++ else:
++ return None
++
++ def quotaRootParent(self, request):
++ """
++ Return the next quota root above this resource.
++
++ @return: L{DAVResource} or C{None}
++ """
++
++ # Check the next parent
++ url = request.urlForResource(self)
++ while (url != "/"):
++ url = parentForURL(url)
++ parent = waitForDeferred(request.locateResource(url))
++ yield parent
++ parent = parent.getResult()
++ if parent.hasQuotaRoot(request):
++ yield parent
++ return
++
++ yield None
++
++ quotaRootParent = deferredGenerator(quotaRootParent)
++
++ def setQuotaRoot(self, request, maxsize):
++ """
++ @param maxsize: a C{int} containing the maximum allowed bytes for the contents
++ of this collection, or C{None} tp remove quota restriction.
++ """
++ assert self.isCollection(), "Only collections can have a quota root"
++ assert maxsize is None or isinstance(maxsize, int), "maxsize must be an int or None"
++
++ if maxsize is not None:
++ self.writeDeadProperty(TwistedQuotaRootProperty.fromString(str(maxsize)))
++ else:
++ # Remove both the root and the cached used value
++ self.removeDeadProperty(TwistedQuotaRootProperty)
++ self.removeDeadProperty(TwistedQuotaUsedProperty)
++
++ def quotaSize(self, request):
++ """
++ Get the size of this resource (if its a collection get total for all children as well).
++ TODO: Take into account size of dead-properties.
++
++ @return: a C{int} containing the size of the resource.
++ """
++ unimplemented(self)
++
++ def checkQuota(self, request, available):
++ """
++ Check to see whether all quota roots have sufficient available bytes.
++ We currently do not use hierarchical quota checks - i.e. only the most
++ immediate quota root parent is checked for quota.
++
++ @param available: a C{int} containing the additional quota required.
++ @return: C{True} if there is sufficient quota remaining on all quota roots,
++ C{False} otherwise.
++ """
++
++ quotaroot = self
++ while(quotaroot is not None):
++ # Check quota on this root (if it has one)
++ quota = quotaroot.quotaRoot(request)
++ if quota is not None:
++ if available > quota[0]:
++ yield False
++ return
++
++ # Check the next parent with a quota root
++ quotaroot = waitForDeferred(quotaroot.quotaRootParent(request))
++ yield quotaroot
++ quotaroot = quotaroot.getResult()
++
++ yield True
++
++ checkQuota = deferredGenerator(checkQuota)
++
++ def quotaSizeAdjust(self, request, adjust):
++ """
++ Update the quota used value on all quota root parents of this resource.
++
++ @param adjust: a C{int} containing the number of bytes added (positive) or
++ removed (negative) that should be used to adjust the cached total.
++ """
++
++ # Check this resource first
++ if self.isCollection():
++ if self.hasQuotaRoot(request):
++ d = waitForDeferred(self.updateQuotaUse(request, adjust))
++ yield d
++ d.getResult()
++
++ # Check the next parent
++ url = request.urlForResource(self)
++ if url != "/":
++ parent = waitForDeferred(request.locateResource(parentForURL(url)))
++ yield parent
++ parent = parent.getResult()
++ d = waitForDeferred(parent.quotaSizeAdjust(request, adjust))
++ yield d
++ d.getResult()
++
++ yield None
++
++ quotaSizeAdjust = deferredGenerator(quotaSizeAdjust)
++
++ def currentQuotaUse(self, request):
++ """
++ Get the cached quota use value, or if not present (or invalid) determine
++ quota use by brute force.
++
++ @return: an L{Deferred} with a C{int} result containing the current used byte if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++ assert self.isCollection(), "Only collections can have a quota root"
++ assert self.hasQuotaRoot(request), "Quota use only on quota root collection"
++
++ # Try to get the cached value property
++ if self.hasDeadProperty(TwistedQuotaUsedProperty):
++ return succeed(int(str(self.readDeadProperty(TwistedQuotaUsedProperty))))
++ else:
++ # Do brute force size determination and cache the result in the private property
++ def _defer(result):
++ self.writeDeadProperty(TwistedQuotaUsedProperty.fromString(str(result)))
++ return result
++ d = self.quotaSize(request)
++ d.addCallback(_defer)
++ return d
++
++ def updateQuotaUse(self, request, adjust):
++ """
++ Update the quota used value on this resource.
++
++ @param adjust: a C{int} containing the number of bytes added (positive) or
++ removed (negative) that should be used to adjust the cached total.
++ @return: an L{Deferred} with a C{int} result containing the current used byte if this collection
++ is quota-controlled, or C{None} if not quota controlled.
++ """
++ assert self.isCollection(), "Only collections can have a quota root"
++
++ # Get current value
++ def _defer(size):
++ size += adjust
++ self.writeDeadProperty(TwistedQuotaUsedProperty.fromString(str(size)))
++
++ d = self.currentQuotaUse(request)
++ d.addCallback(_defer)
++ return d
++
++ ##
+ # HTTP
+ ##
+
+@@ -1702,6 +2011,28 @@
+ davxml.registerElement(TwistedACLInheritable)
+ davxml.ACE.allowed_children[(twisted_dav_namespace, "inheritable")] = (0, 1)
+
++"""
++When set on a collection, this property indicates that the collection has a quota limit for
++the size of all resources stored in the collection (and any associate meta-data such as properties).
++The value is a number - the maximum size in bytes allowed.
++"""
++class TwistedQuotaRootProperty (davxml.WebDAVTextElement):
++ namespace = twisted_private_namespace
++ name = "quota-root"
++
++davxml.registerElement(TwistedQuotaRootProperty)
++
++"""
++When set on a collection, this property contains the cached running total of the size of all
++resources stored in the collection (and any associate meta-data such as properties).
++The value is a number - the size in bytes used.
++"""
++class TwistedQuotaUsedProperty (davxml.WebDAVTextElement):
++ namespace = twisted_private_namespace
++ name = "quota-used"
++
++davxml.registerElement(TwistedQuotaUsedProperty)
++
+ allACL = davxml.ACL(
+ davxml.ACE(
+ davxml.Principal(davxml.All()),
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.static.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,55 @@
+Index: twisted/web2/dav/static.py
+===================================================================
+--- twisted/web2/dav/static.py (revision 18219)
++++ twisted/web2/dav/static.py (working copy)
+@@ -98,6 +98,50 @@
+ return succeed(davPrivilegeSet)
+
+ ##
++ # Quota
++ ##
++
++ def quotaSize(self, request):
++ """
++ Get the size of this resource.
++ TODO: Take into account size of dead-properties. Does stat
++ include xattrs size?
++
++ @return: an L{Deferred} with a C{int} result containing the size of the resource.
++ """
++ if self.isCollection():
++ def walktree(top):
++ """
++ Recursively descend the directory tree rooted at top,
++ calling the callback function for each regular file
++
++ @param top: L{FilePath} for the directory to walk.
++ """
++
++ total = 0
++ for f in top.listdir():
++ child = top.child(f)
++ if child.isdir():
++ # It's a directory, recurse into it
++ result = waitForDeferred(walktree(child))
++ yield result
++ total += result.getResult()
++ elif child.isfile():
++ # It's a file, call the callback function
++ total += child.getsize()
++ else:
++ # Unknown file type, print a message
++ pass
++
++ yield total
++
++ walktree = deferredGenerator(walktree)
++
++ return walktree(self.fp)
++ else:
++ return succeed(self.fp.getsize())
++
++ ##
+ # Workarounds for issues with File
+ ##
+
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.log.patch (from rev 190, CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.log.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.log.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.log.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -0,0 +1,13 @@
+Index: twisted/web2/log.py
+===================================================================
+--- twisted/web2/log.py (revision 18219)
++++ twisted/web2/log.py (working copy)
+@@ -88,7 +88,7 @@
+ class LogWrapperResource(resource.WrapperResource):
+ def hook(self, request):
+ # Insert logger
+- request.addResponseFilter(logFilter, atEnd=True)
++ request.addResponseFilter(logFilter, atEnd=True, onlyOnce=True)
+
+ monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.server.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.server.patch 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.server.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -1,44 +1,15 @@
Index: twisted/web2/server.py
===================================================================
---- twisted/web2/server.py (revision 17932)
+--- twisted/web2/server.py (revision 18219)
+++ twisted/web2/server.py (working copy)
-@@ -11,7 +11,7 @@
- import cStringIO as StringIO
+@@ -156,7 +156,9 @@
+ self.resources = []
+ http.Request.__init__(self, *args, **kw)
- import cgi, time, urlparse
--from urllib import unquote
-+from urllib import quote, unquote
- from urlparse import urlsplit
-
- import weakref
-@@ -316,7 +316,12 @@
- if newpath is StopTraversal:
- # We need to rethink how to do this.
- #if newres is res:
-- self._rememberURLForResource(path, res)
-+ if path:
-+ url = "/" + "/".join(path)
-+ else:
-+ url = "/"
-+
-+ self._rememberURLForResource(quote(url), res)
- return res
- #else:
- # raise ValueError("locateChild must not return StopTraversal with a resource other than self.")
-@@ -337,7 +342,7 @@
- self.prepath.append(self.postpath.pop(0))
-
- child = self._getChild(None, newres, newpath, updatepaths=updatepaths)
-- self._rememberURLForResource(url, child)
-+ self._rememberURLForResource(quote(url), child)
-
- return child
-
-@@ -404,6 +409,7 @@
- segments = path.split("/")
- assert segments[0] == "", "URL path didn't begin with '/': %s" % (path,)
- segments = segments[1:]
-+ segments = map(unquote, segments)
-
- def notFound(f):
- f.trap(http.HTTPError)
+- def addResponseFilter(self, f, atEnd=False):
++ def addResponseFilter(self, f, atEnd=False, onlyOnce = False):
++ if onlyOnce and f in self.responseFilters:
++ return
+ if atEnd:
+ self.responseFilters.append(f)
+ else:
Modified: CalendarServer/trunk/lib-patches/vObject/icalendar.patch
===================================================================
--- CalendarServer/trunk/lib-patches/vObject/icalendar.patch 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/lib-patches/vObject/icalendar.patch 2006-09-25 16:49:30 UTC (rev 191)
@@ -40,6 +40,15 @@
isDate = datetime.date == type(dtstart)
if isDate:
dtstart = datetime.datetime(dtstart.year,dtstart.month, dtstart.day)
+@@ -488,7 +506,7 @@
+
+ if rule._bymonth is not None and len(rule._bymonth) > 0:
+ if (rule._byweekday is not None or
+- len(rrule._bynweekday or ()) > 0 or
++ len(rule._bynweekday or ()) > 0 or
+ not (rule._freq == dateutil.rrule.YEARLY and
+ len(rule._bymonth) == 1 and
+ rule._bymonth[0] == rule._dtstart.month)):
@@ -580,6 +598,8 @@
if obj.value.tzinfo is None:
obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE']
Modified: CalendarServer/trunk/run
===================================================================
--- CalendarServer/trunk/run 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/run 2006-09-25 16:49:30 UTC (rev 191)
@@ -415,7 +415,7 @@
;;
esac;
svn_uri="${proto}://svn.twistedmatrix.com/svn/Twisted/branches/dav-acl-1608";
- svn_get "Twisted" "${twisted}" "${svn_uri}" 18218;
+ svn_get "Twisted" "${twisted}" "${svn_uri}" 18219;
fi;
py_install "Twisted" "${twisted}";
Modified: CalendarServer/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py 2006-09-25 16:49:30 UTC (rev 191)
@@ -254,6 +254,15 @@
allowed_children = { (caldav_namespace, "calendar-data"): (0, None) }
+class MaxResourceSize (CalDAVTextElement):
+ """
+ Specifies restrictions on a calendar collection.
+ (CalDAV-access-15, section 5.2.5)
+ """
+ name = "max-resource-size"
+ hidden = True
+ protected = True
+
class Calendar (CalDAVEmptyElement):
"""
Denotes a calendar collection.
Modified: CalendarServer/trunk/twistedcaldav/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/itip.py 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/twistedcaldav/itip.py 2006-09-25 16:49:30 UTC (rev 191)
@@ -634,9 +634,12 @@
# Create a new name if one was not provided
if name is None:
name = md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
- newchild = CalDAVFile(os.path.join(collection.fp.path, name))
- else:
- newchild = collection.getChild(name)
+
+ # Get a resource for the new item
+ newchildURL = joinURL(collURL, name)
+ newchild = waitForDeferred(request.locateResource(newchildURL))
+ yield newchild
+ newchild = newchild.getResult()
# Modify the original calendar data by removing the METHOD property - everything else is left as-is,
# as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
@@ -648,29 +651,26 @@
itipper = False
# Now write it to the resource
-
- # Get a resource for the new item
- newchildURL = joinURL(collURL, name)
+ try:
+ d = waitForDeferred(storeCalendarObjectResource(
+ request=request,
+ sourcecal = False,
+ destination = newchild,
+ destination_uri = newchildURL,
+ calendardata = str(calendar),
+ destinationparent = collection,
+ destinationcal = True,
+ isiTIP = itipper
+ ))
+ yield d
+ d.getResult()
+ except:
+ yield None
+ return
- # Copy calendar to inbox (doing fan-out)
- def _defer(result):
- return newchild
- def _deferErr(f):
- return None
+ yield newchild
- d = maybeDeferred(
- storeCalendarObjectResource,
- request=request,
- sourcecal = False,
- destination = newchild,
- destination_uri = newchildURL,
- calendardata = str(calendar),
- destinationparent = collection,
- destinationcal = True,
- isiTIP = itipper
- )
- d.addCallbacks(_defer, _deferErr)
- return d
+writeResource = deferredGenerator(writeResource)
def newInboxResource(child, newchild):
"""
Modified: CalendarServer/trunk/twistedcaldav/logging.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/logging.py 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/twistedcaldav/logging.py 2006-09-25 16:49:30 UTC (rev 191)
@@ -17,20 +17,30 @@
##
"""
+Classes and functions to do better logging.
+"""
+
+import datetime
+import os
+import time
+
+from twisted.python import log
+from twisted.web2.log import BaseCommonAccessLoggingObserver
+
+"""
Logging levels:
0 - no logging
1 - errors only
2 - errors and warnings only
-3 - errors, warnings and debug
+3 - errors, warnings and info
+3 - errors, warnings, info and debug
"""
-from twisted.python import log
+logtypes = {"none": 0, "error": 1, "warning": 2, "info": 3, "debug": 4}
-logtypes = {"none": 0, "info": 1, "warning": 2, "error": 3, "debug": 4}
+currentLogLevel = logtypes["error"]
-currentLogLevel = 1
-
def canLog(type):
"""
Determine whether a particular log level type is current active.
@@ -85,3 +95,115 @@
if canLog("debug"):
log.msg(message, debug=True, **kwargs)
+class RotatingFileAccessLoggingObserver(BaseCommonAccessLoggingObserver):
+ """
+ Class to do 'apache' style access logging to a rotating log file. The log
+ file is rotated after midnight each day.
+ """
+
+ def __init__(self, logpath):
+ self.logpath = logpath
+
+ def logMessage(self, message, allowrotate=True):
+ """
+ Log a message to the file and possibly rotate if date has changed.
+
+ @param message: C{str} for the message to log.
+ @param allowrotate: C{True} if log rotate allowed, C{False} to log to current file
+ without testing for rotation.
+ """
+
+ if self.shouldRotate() and allowrotate:
+ self.flush()
+ self.rotate()
+ self.f.write(message + '\n')
+
+ def start(self):
+ """
+ Start logging. Open the log file and log an 'open' message.
+ """
+
+ super(RotatingFileAccessLoggingObserver, self).start()
+ self._open()
+ self.logMessage("Log opened - server start: [%s]." % (datetime.datetime.now().ctime(),))
+
+ def stop(self):
+ """
+ Stop logging. Close the log file and log an 'open' message.
+ """
+
+ self.logMessage("Log closed - server stop: [%s]." % (datetime.datetime.now().ctime(),), False)
+ super(RotatingFileAccessLoggingObserver, self).stop()
+ self._close()
+
+ def _open(self):
+ """
+ Open the log file.
+ """
+
+ self.f = open(self.logpath, 'a', 1)
+ self.lastDate = self.toDate(os.stat(self.logpath)[8])
+
+ def _close(self):
+ """
+ Close the log file.
+ """
+
+ self.f.close()
+
+ def flush(self):
+ """
+ Flush the log file.
+ """
+
+ self.f.flush()
+
+ def shouldRotate(self):
+ """
+ Rotate when the date has changed since last write
+ """
+
+ return self.toDate() > self.lastDate
+
+ def toDate(self, *args):
+ """
+ Convert a unixtime to (year, month, day) localtime tuple,
+ or return the current (year, month, day) localtime tuple.
+
+ This function primarily exists so you may overload it with
+ gmtime, or some cruft to make unit testing possible.
+ """
+
+ # primarily so this can be unit tested easily
+ return time.localtime(*args)[:3]
+
+ def suffix(self, tupledate):
+ """
+ Return the suffix given a (year, month, day) tuple or unixtime
+ """
+
+ try:
+ return '_'.join(map(str, tupledate))
+ except:
+ # try taking a float unixtime
+ return '_'.join(map(str, self.toDate(tupledate)))
+
+ def rotate(self):
+ """
+ Rotate the file and create a new one.
+
+ If it's not possible to open new logfile, this will fail silently,
+ and continue logging to old logfile.
+ """
+
+ newpath = "%s.%s" % (self.logpath, self.suffix(self.lastDate))
+ if os.path.exists(newpath):
+ log.msg("Cannot rotate log file to %s because it already exists." % (newpath,))
+ return
+ self.logMessage("Log closed - rotating: [%s]." % (datetime.datetime.now().ctime(),), False)
+ info("Rotating log file to: %s" % (newpath,), system="Logging")
+ self.f.close()
+ os.rename(self.logpath, newpath)
+ self._open()
+ self.logMessage("Log opened - rotated: [%s]." % (datetime.datetime.now().ctime(),), False)
+
Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2006-09-25 16:49:30 UTC (rev 191)
@@ -22,19 +22,18 @@
__all__ = ["storeCalendarObjectResource"]
+from twisted.internet.defer import deferredGenerator
from twisted.internet.defer import maybeDeferred
-from twisted.python import failure
-from twisted.python import log
+from twisted.internet.defer import waitForDeferred
+from twisted.python import failure, log
from twisted.python.filepath import FilePath
from twisted.web2 import responsecode
from twisted.web2.dav import davxml
from twisted.web2.dav.element.base import PCDATAElement
-from twisted.web2.dav.fileop import copy
-from twisted.web2.dav.fileop import delete
-from twisted.web2.dav.fileop import put
+from twisted.web2.dav.fileop import copy, delete, put
from twisted.web2.dav.http import ErrorResponse
from twisted.web2.dav.util import joinURL, parentForURL
-from twisted.web2.http import HTTPError
+from twisted.web2.http import HTTPError, StatusResponse
from twisted.web2.iweb import IResponse
from twisted.web2.stream import MemoryStream
@@ -44,6 +43,7 @@
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.ical import Component
from twistedcaldav.instance import TooManyInstancesError
+from twistedcaldav.resource import CalDAVResource
def storeCalendarObjectResource(
request,
@@ -234,6 +234,23 @@
return result, message
+ def validSizeCheck():
+ """
+ Make sure that the content-type of the source resource is text/calendar.
+ This test is only needed when the source is not in a calendar collection.
+ @param request: the L{twisted.web2.server.Request} for the current HTTP request.
+ @param source: the L{Component} for the calendar to test.
+ """
+ result = True
+ message = ""
+ if CalDAVResource.sizeLimit is not None:
+ calsize = len(str(calendar))
+ if calsize > CalDAVResource.sizeLimit:
+ result = False
+ message = "Data size %d bytes is larger than allowed limit %d bytes" % (calsize, CalDAVResource.sizeLimit)
+
+ return result, message
+
def noUIDConflict(uid):
"""
Check that the UID of the new calendar object conforms to the requirements of
@@ -282,12 +299,12 @@
return result, message, rname
-
try:
"""
Handle validation operations here.
"""
+ reserved = False
if destinationcal:
if not sourcecal:
# Valid content type check on the source resource if its not in a calendar collection
@@ -331,7 +348,13 @@
# FIXME: We need this here because we have to re-index the destination. Ideally it
# would be better to copy the index entries from the source and add to the destination.
calendar = source.iCalendar()
-
+
+ # Valid calendar data size check
+ result, message = validSizeCheck()
+ if not result:
+ log.err(message)
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "max-resource-size")))
+
# uid conflict check
if not isiTIP:
result, message, rname = noUIDConflict(uid)
@@ -348,11 +371,37 @@
# deferreds.
destination_index = destinationparent.index()
destination_index.reserveUID(uid)
+ reserved = True
"""
Handle rollback setup here.
"""
+ # Do quota checks on destination and source before we start messing with adding other files
+ destquota = waitForDeferred(destination.quota(request))
+ yield destquota
+ destquota = destquota.getResult()
+ if destquota is not None and destination.exists():
+ old_dest_size = waitForDeferred(destination.quotaSize(request))
+ yield old_dest_size
+ old_dest_size = old_dest_size.getResult()
+ else:
+ old_dest_size = 0
+
+ if source is not None:
+ sourcequota = waitForDeferred(source.quota(request))
+ yield sourcequota
+ sourcequota = sourcequota.getResult()
+ if sourcequota is not None and source.exists():
+ old_source_size = waitForDeferred(source.quotaSize(request))
+ yield old_source_size
+ old_source_size = old_source_size.getResult()
+ else:
+ old_source_size = 0
+ else:
+ sourcequota = None
+ old_source_size = 0
+
# We may need to restore the original resource data if the PUT/COPY/MOVE fails,
# so rename the original file in case we need to rollback.
overwrite = destination.exists()
@@ -387,9 +436,14 @@
# Do put or copy based on whether source exists
if source is not None:
- d = maybeDeferred(copy, source.fp, destination.fp, destination_uri, "0")
+ response = maybeDeferred(copy, source.fp, destination.fp, destination_uri, "0")
else:
- d = maybeDeferred(put, MemoryStream(calendardata), destination.fp)
+ response = maybeDeferred(put, MemoryStream(calendardata), destination.fp)
+ response = waitForDeferred(response)
+ yield response
+ response = response.getResult()
+
+ response = IResponse(response)
def doDestinationIndex(caltoindex):
"""
@@ -438,7 +492,7 @@
delete(source_uri, source.fp, "0")
rollback.source_deleted = True
logging.debug("Source removed %s" % (source.fp.path,), system="Store Resource")
-
+
def doSourceIndexRecover():
"""
Do source resource indexing. This only gets called when restoring
@@ -459,45 +513,55 @@
source.writeProperty(davxml.GETContentType.fromString("text/calendar"), request)
return None
- def doIndexing(response):
- """
- Callback after initial store operation succeeds.
- """
- logging.debug("Write to destination completed %r" % response, system="Store Resource")
- response = IResponse(response)
- if response.code in [responsecode.NO_CONTENT, responsecode.CREATED]:
- if deletesource:
- doSourceDelete()
-
- if destinationcal:
- result = doDestinationIndex(calendar)
- if result is not None:
- rollback.Rollback()
- return result
-
- # Can now commit changes and forget the rollback details
- rollback.Commit()
+ if deletesource:
+ doSourceDelete()
+ # Update quota
+ if sourcequota is not None:
+ delete_size = 0 - old_source_size
+ d = waitForDeferred(source.quotaSizeAdjust(request, delete_size))
+ yield d
+ d.getResult()
- return response
- def cleanUpIndex(f):
- if destinationcal:
- destination_index.unreserveUID(uid)
-
- # Always do the rollback operation: actually this will not
- # rollback if the PUT was successful as the rollback will have
- # been deactivated by a commit.
- rollback.Rollback()
+ if destinationcal:
+ result = doDestinationIndex(calendar)
+ if result is not None:
+ rollback.Rollback()
+ yield result
+ return
- return f
+ # Do quota check on destination
+ if destquota is not None:
+ # Get size of new/old resources
+ new_dest_size = waitForDeferred(destination.quotaSize(request))
+ yield new_dest_size
+ new_dest_size = new_dest_size.getResult()
+ diff_size = new_dest_size - old_dest_size
+ if diff_size >= destquota[0]:
+ log.err("Over quota: available %d, need %d" % (destquota[0], diff_size))
+ raise HTTPError(StatusResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, "Over quota"))
+ d = waitForDeferred(destination.quotaSizeAdjust(request, diff_size))
+ yield d
+ d.getResult()
- d.addCallback(doIndexing)
- d.addBoth(cleanUpIndex)
+ # Can now commit changes and forget the rollback details
+ rollback.Commit()
- return d
+ if reserved:
+ destination_index.unreserveUID(uid)
+ reserved = False
+ yield response
+ return
+
except:
+ if reserved:
+ destination_index.unreserveUID(uid)
+ reserved = False
+
# Roll back changes to original server state. Note this may do nothing
# if the rollback has already ocurred or changes already committed.
rollback.Rollback()
raise
+
+storeCalendarObjectResource = deferredGenerator(storeCalendarObjectResource)
\ No newline at end of file
Modified: CalendarServer/trunk/twistedcaldav/method/schedule_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/schedule_common.py 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/twistedcaldav/method/schedule_common.py 2006-09-25 16:49:30 UTC (rev 191)
@@ -41,10 +41,8 @@
from twistedcaldav.method import report_common
from twistedcaldav.method.put_common import storeCalendarObjectResource
from twistedcaldav.resource import CalendarPrincipalCollectionResource, isScheduleOutboxResource, isCalendarCollectionResource
-from twistedcaldav.static import CalDAVFile
import md5
-import os
import time
def processScheduleRequest(self, method, request):
@@ -193,8 +191,10 @@
name = md5.new(str(calendar) + str(time.time()) + self.fp.path).hexdigest() + ".ics"
# Save a copy of the calendar data into the Outbox
- child = CalDAVFile(os.path.join(self.fp.path, name))
- childURL = request.uri + name
+ childURL = joinURL(request.uri, name)
+ child = waitForDeferred(request.locateResource(childURL))
+ yield child
+ child = child.getResult()
responses.setLocation(childURL)
d = waitForDeferred(
@@ -295,7 +295,6 @@
# properly manage the free busy set that should not prevent us from working.
continue
- # TODO: make this a waitForDeferred and yield it
matchtotal = waitForDeferred(report_common.generateFreeBusyInfo(request, cal, fbinfo, timerange, matchtotal))
yield matchtotal
matchtotal = matchtotal.getResult()
@@ -317,8 +316,10 @@
name = md5.new(str(calendar) + str(time.time()) + inbox.fp.path).hexdigest() + ".ics"
# Get a resource for the new item
- child = CalDAVFile(os.path.join(inbox.fp.path, name))
childURL = joinURL(inboxURL, name)
+ child = waitForDeferred(request.locateResource(childURL))
+ yield child
+ child = child.getResult()
# Copy calendar to inbox (doing fan-out)
d = waitForDeferred(
Modified: CalendarServer/trunk/twistedcaldav/repository.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/repository.py 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/twistedcaldav/repository.py 2006-09-25 16:49:30 UTC (rev 191)
@@ -39,6 +39,7 @@
from twistedcaldav.directory import DirectoryGroupPrincipalProvisioningResource
from twistedcaldav.directory import DirectoryUserPrincipalProvisioningResource
from twistedcaldav.directory import DirectoryPrincipalProvisioningResource
+from twistedcaldav.resource import CalDAVResource
from twistedcaldav.static import CalDAVFile, CalendarHomeFile, CalendarPrincipalFile
from twistedcaldav.static import CalendarHomeProvisioningFile
from twistedcaldav.static import CalendarPrincipalProvisioningResource
@@ -97,6 +98,7 @@
ELEMENT_NAME = "name"
ELEMENT_CUADDR = "cuaddr"
ELEMENT_CALENDAR = "calendar"
+ELEMENT_QUOTA = "quota"
ELEMENT_AUTORESPOND = "autorespond"
ATTRIBUTE_REPEAT = "repeat"
@@ -129,19 +131,30 @@
and optionally provisions accounts.
"""
- def __init__(self, docroot, doAccounts, resetACLs = False):
+ def __init__(self, docroot, doAccounts, resetACLs = False, maxsize = None, quota = None):
"""
@param docroot: file system path to use as the root.
@param doAccounts: if True accounts will be auto-provisioned, if False
no auto-provisioning is done
@param resetACLs: if True, when auto-provisioning access control privileges are initialised
in an appropriate fashion for user accounts, if False no privileges are set or changed.
+ @param maxsize: maximum size in bytes for any calendar object resource, C{int} to set size,
+ if <= 0, then no limit will be set.
+ @param quota: maximum quota size in bytes for a user's calendar home, C{int} to set size,
+ if <= 0, then no limit will be set.
"""
self.docRoot = DocRoot(docroot)
self.doAccounts = doAccounts
self.accounts = Provisioner()
self.resetACLs = resetACLs
+ self.maxsize = maxsize
+ self.quota = quota
+ if self.maxsize <= 0:
+ self.maxsized = None
+ if self.quota <= 0:
+ self.quota = None
+
def buildFromFile(self, file):
"""
Parse the required information from an XML file.
@@ -162,6 +175,10 @@
self.docRoot.build()
if self.doAccounts:
self.accounts.provision(self.docRoot.principalCollection, self.docRoot.calendarHome, self.resetACLs)
+
+ # Handle global quota value
+ CalendarHomeFile.quotaLimit = self.quota
+ CalDAVResource.sizeLimit = self.maxsize
def parseXML(self, node):
"""
@@ -505,7 +522,7 @@
else:
repeat = 1
- principal = ProvisionPrincipal("", "", "", [], [], None, False)
+ principal = ProvisionPrincipal("", "", "", [], [], None, None, False)
principal.parseXML( child )
self.items.append((repeat, principal))
@@ -584,6 +601,7 @@
home.createDirectory()
home = CalendarHomeFile(home.path)
+ # Handle ACLs on calendar home
if resetACLs or not home_exists:
if item.acl:
home.setAccessControlList(item.acl.acl)
@@ -606,6 +624,9 @@
)
)
+ # Handle quota on calendar home
+ home.setQuotaRoot(None, item.quota)
+
# Save the calendar-home-set, schedule-inbox and schedule-outbox properties
principal.writeDeadProperty(caldavxml.CalendarHomeSet(davxml.HRef.fromString(homeURL + "/")))
principal.writeDeadProperty(caldavxml.ScheduleInboxURL(davxml.HRef.fromString(joinURL(homeURL, "inbox/"))))
@@ -665,7 +686,7 @@
"""
Contains provision information for one user.
"""
- def __init__(self, uid, pswd, name, cuaddrs, calendars, acl, autorespond):
+ def __init__(self, uid, pswd, name, cuaddrs, calendars, acl, quota, autorespond):
"""
@param uid: user id.
@param pswd: clear-text password for this user.
@@ -673,6 +694,8 @@
@param cuaddr: list of calendar user addresses.
@param calendars: list of calendars to auto-create.
@param acl: ACL to apply to calendar home
+ @param quota: quota allowed on user's calendar home C{int} size in bytes
+ or C{None} if no quota
@param autorespond auto-respond to scheduling requests
"""
@@ -682,6 +705,7 @@
self.cuaddrs = cuaddrs
self.calendars = calendars
self.acl = acl
+ self.quota = quota
self.autorespond = autorespond
def repeat(self, ctr):
@@ -710,7 +734,7 @@
else:
cuaddrs.append(cuaddr)
- return ProvisionPrincipal(uid, pswd, name, cuaddrs, self.calendars, self.acl, self.autorespond)
+ return ProvisionPrincipal(uid, pswd, name, cuaddrs, self.calendars, self.acl, self.quota, self.autorespond)
def parseXML( self, node ):
for child in node._get_childNodes():
@@ -729,6 +753,9 @@
elif child._get_localName() == ELEMENT_CALENDAR:
if child.firstChild is not None:
self.calendars.append(child.firstChild.data.encode("utf-8"))
+ elif child._get_localName() == ELEMENT_QUOTA:
+ if child.firstChild is not None:
+ self.quota = int(child.firstChild.data.encode("utf-8"))
elif child._get_localName() == ELEMENT_ACL:
self.acl = ACL()
self.acl.parseXML(child)
Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/twistedcaldav/resource.py 2006-09-25 16:49:30 UTC (rev 191)
@@ -41,8 +41,8 @@
from twisted.internet.defer import Deferred, maybeDeferred, succeed
from twisted.internet.defer import deferredGenerator, waitForDeferred
from twisted.web2 import responsecode
-from twisted.web2.dav import auth, davxml
-from twisted.web2.dav.resource import DAVPrincipalResource
+from twisted.web2.dav import davxml
+from twisted.web2.dav.resource import AccessDeniedError, DAVPrincipalResource
from twisted.web2.dav.davxml import dav_namespace
from twisted.web2.dav.http import ErrorResponse
from twisted.web2.dav.resource import DAVResource, TwistedACLInheritable
@@ -72,6 +72,10 @@
"""
implements(ICalDAVResource)
+ # A global limit for the size of calendar object resources. Either a C{int} (size in bytes) to limit
+ # resources to that size, or C{None} for no limit.
+ sizeLimit = None
+
##
# HTTP
##
@@ -150,6 +154,12 @@
"version" : "2.0",
}),
))
+ elif name == "max-resource-size":
+ # CalDAV-access-15, section 5.2.5
+ if CalDAVResource.sizeLimit is not None:
+ return succeed(caldavxml.MaxResourceSize.fromString(
+ str(CalDAVResource.sizeLimit)
+ ))
return super(CalDAVResource, self).readProperty(property, request)
@@ -220,7 +230,6 @@
assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,)
def checkPrivilegesError(failure):
- from twisted.web2.dav.acl import AccessDeniedError
failure.trap(AccessDeniedError)
reactor.callLater(0, getChild)
Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py 2006-09-25 15:18:22 UTC (rev 190)
+++ CalendarServer/trunk/twistedcaldav/static.py 2006-09-25 16:49:30 UTC (rev 191)
@@ -37,7 +37,6 @@
from twisted.internet.defer import deferredGenerator, fail, succeed, waitForDeferred
from twisted.python import log
-from twisted.python.failure import Failure
from twisted.python.filepath import FilePath
from twisted.web2 import responsecode
from twisted.web2.http import HTTPError, StatusResponse
@@ -47,6 +46,7 @@
from twisted.web2.dav.http import ErrorResponse
from twisted.web2.dav.idav import IDAVResource
from twisted.web2.dav.resource import TwistedACLInheritable
+from twisted.web2.dav.resource import TwistedQuotaRootProperty
from twisted.web2.dav.static import DAVFile
from twisted.web2.dav.util import parentForURL, joinURL, bindMethods
@@ -54,7 +54,6 @@
from twistedcaldav import customxml
from twistedcaldav.ical import Component as iComponent
from twistedcaldav.ical import Property as iProperty
-from twistedcaldav.icaldav import ICalDAVResource
from twistedcaldav.index import Index, IndexSchedule, db_basename
from twistedcaldav.resource import CalDAVResource, isPseudoCalendarCollectionResource, CalendarPrincipalResource
from twistedcaldav.resource import ScheduleInboxResource, ScheduleOutboxResource, CalendarPrincipalCollectionResource
@@ -304,6 +303,55 @@
]
##
+ # Quota
+ ##
+
+ def quotaSize(self, request):
+ """
+ Get the size of this resource.
+ TODO: Take into account size of dead-properties. Does stat
+ include xattrs size?
+
+ @return: an L{Deferred} with a C{int} result containing the size of the resource.
+ """
+ if self.isCollection():
+ def walktree(top, top_level = False):
+ """
+ Recursively descend the directory tree rooted at top,
+ calling the callback function for each regular file
+
+ @param top: L{FilePath} for the directory to walk.
+ """
+
+ total = 0
+ for f in top.listdir():
+
+ # Ignore the database
+ if top_level and f == db_basename:
+ continue
+
+ child = top.child(f)
+ if child.isdir():
+ # It's a directory, recurse into it
+ result = waitForDeferred(walktree(child))
+ yield result
+ total += result.getResult()
+ elif child.isfile():
+ # It's a file, call the callback function
+ total += child.getsize()
+ else:
+ # Unknown file type, print a message
+ pass
+
+ yield total
+
+ walktree = deferredGenerator(walktree)
+
+ return walktree(self.fp, True)
+ else:
+ return succeed(self.fp.getsize())
+
+ ##
# Utilities
##
@@ -533,6 +581,11 @@
"""
L{CalDAVFile} calendar home collection resource.
"""
+
+ # A global quota limit for all calendar homes. Either a C{int} (size in bytes) to limit
+ # quota to that size, or C{None} for no limit.
+ quotaLimit = None
+
def __init__(self, path):
"""
@param path: the path to the file which will back the resource.
@@ -574,11 +627,31 @@
if self.isDisabled():
return succeed(None)
- super(CalDAVResource, self).accessControlList(*args, **kwargs)
+ return super(CalendarHomeFile, self).accessControlList(*args, **kwargs)
def createSimilarFile(self, path):
return CalDAVFile(path)
+ ##
+ # Quota
+ ##
+
+ def hasQuotaRoot(self, request):
+ """
+ @return: a C{True} if this resource has quota root, C{False} otherwise.
+ """
+ return self.hasDeadProperty(TwistedQuotaRootProperty) or CalendarHomeFile.quotaLimit is not None
+
+ def quotaRoot(self, request):
+ """
+ @return: a C{int} containing the maximum allowed bytes if this collection
+ is quota-controlled, or C{None} if not quota controlled.
+ """
+ if self.hasDeadProperty(TwistedQuotaRootProperty):
+ return int(str(self.readDeadProperty(TwistedQuotaRootProperty)))
+ else:
+ return CalendarHomeFile.quotaLimit
+
class CalendarHomeProvisioningFile (CalDAVFile):
"""
L{CalDAVFile} resource which provisions calendar home collections as needed.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20060925/6b8404ec/attachment.html
More information about the calendarserver-changes
mailing list