[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