[CalendarServer-changes] [129] CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted

source_changes at macosforge.org source_changes at macosforge.org
Tue Sep 12 14:18:23 PDT 2006


Revision: 129
Author:   cdaboo at apple.com
Date:     2006-09-12 14:18:22 -0700 (Tue, 12 Sep 2006)

Log Message:
-----------
Initial Quota (rfc4331) support. 4331 properties have been defined along with a pair
of private properties that indicate the current quota root and the current quota usage
on a collection. Quota checking and updating can be done. Put has been modified to use
a new storeResource api that takes care of quota checking and rollback if quota is
exceeded (basic behaviour is copied from CalDAV implementation). Copy/move have not yet
been modified to support this.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.resource.patch
    CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.static.patch

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.davxml.patch
    CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch
    CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch
    CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.idav.patch
    CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put
    CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch

Added: CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.davxml.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.davxml.patch	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.davxml.patch	2006-09-12 21:18:22 UTC (rev 129)
@@ -0,0 +1,28 @@
+Index: twisted/web2/dav/davxml.py
+===================================================================
+--- twisted/web2/dav/davxml.py	(revision 17935)
++++ 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)
+ )

Added: CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.__init__.patch	2006-09-12 21:18:22 UTC (rev 129)
@@ -0,0 +1,10 @@
+Index: twisted/web2/dav/element/__init__.py
+===================================================================
+--- twisted/web2/dav/element/__init__.py	(revision 17935)
++++ twisted/web2/dav/element/__init__.py	(working copy)
+@@ -35,4 +35,5 @@
+     "rfc2518",
+     "rfc3253",
+     "rfc3744",
++    "rfc4331",
+ ]

Added: CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.element.rfc4331.patch	2006-09-12 21:18:22 UTC (rev 129)
@@ -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

Added: CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.idav.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.idav.patch	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.idav.patch	2006-09-12 21:18:22 UTC (rev 129)
@@ -0,0 +1,77 @@
+Index: twisted/web2/dav/idav.py
+===================================================================
+--- twisted/web2/dav/idav.py	(revision 18078)
++++ twisted/web2/dav/idav.py	(working copy)
+@@ -187,6 +187,72 @@
+             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 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.
++        TODO: Take into account size of dead-properties.
++
++        @return: a C{int} 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: a C{int} containing the current used byte 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: a C{int} containing the current used byte if this collection
++            is quota-controlled, or C{None} if not quota controlled.
++        """
++        
++    def determineActualQuotaUse(request):
++        """
++        Brute force determination of quota used by this collection.
++
++        @return: a C{int} 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)

Added: CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put
===================================================================
--- CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put	2006-09-12 21:18:22 UTC (rev 129)
@@ -0,0 +1,19 @@
+Index: twisted/web2/dav/method/put.py
+===================================================================
+--- twisted/web2/dav/method/put.py	(revision 17951)
++++ twisted/web2/dav/method/put.py	(working copy)
+@@ -22,6 +22,7 @@
+ #
+ # DRI: Wilfredo Sanchez, wsanchez at apple.com
+ ##
++from twisted.web2.dav.method import put_common
+ 
+ """
+ WebDAV PUT method
+@@ -107,4 +108,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)

Added: CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch	2006-09-12 21:18:22 UTC (rev 129)
@@ -0,0 +1,237 @@
+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,232 @@
++##
++# 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
++##
++from twisted.internet.defer import waitForDeferred
++from twisted.internet.defer import deferredGenerator
++from twisted.web2.http import StatusResponse
++
++"""
++PUT/COPY/MOVE common behavior.
++"""
++
++__version__ = "0.0"
++
++__all__ = ["storeCalendarObjectResource"]
++
++from twisted.internet.defer import maybeDeferred
++from twisted.python import failure
++from twisted.python import log
++from twisted.python.filepath import FilePath
++from twisted.web2 import responsecode
++from twisted.web2.dav.fileop import copy
++from twisted.web2.dav.fileop import delete
++from twisted.web2.dav.fileop import put
++from twisted.web2.http import HTTPError
++from twisted.web2.iweb import IResponse
++
++def storeResource(
++    request,
++    source=None, source_uri=None,
++    destination=None, destination_uri=None,
++    deletesource=False
++):
++    """
++    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.
++    @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,))
++        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 = destination.quotaSize(request)
++        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 = source.quotaSize(request)
++            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, "0")
++        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 = destination.quotaSize(request)
++            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))
++                rollback.Rollback()
++                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
++                source.updateQuotaUse(request, delete_size)
++                d = waitForDeferred(source.quotaSizeAdjust(request, delete_size))
++                yield d
++                d.getResult()
++
++            delete(source_uri, source.fp, "0")
++            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)

Modified: CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.resource.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.resource.patch	2006-09-12 21:02:19 UTC (rev 128)
+++ CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.resource.patch	2006-09-12 21:18:22 UTC (rev 129)
@@ -2,7 +2,49 @@
 ===================================================================
 --- twisted/web2/dav/resource.py	(revision 18074)
 +++ twisted/web2/dav/resource.py	(working copy)
-@@ -599,7 +599,7 @@
+@@ -125,6 +125,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"),
+     )
+@@ -264,6 +266,32 @@
+                         # TODO: Merge change from original patch
+                         lambda: self.safeAccessControlList(request)
+                     )
++                    
++                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
+ 
+             if namespace == twisted_dav_namespace:
+                 if name == "resource-class":
+@@ -599,7 +627,7 @@
              else:
                  factory = request.credentialFactories[authHeader[0]]
  
@@ -11,3 +53,212 @@
  
                  # Try to match principals in each principal collection on 
                  # the resource
+@@ -1526,6 +1554,179 @@
+         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 = self.currentQuotaUse(request)
++                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 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.
++        """
++        assert self.isCollection(), "Only collections can have a quota root"
++        if self.hasDeadProperty(TwistedQuotaRootProperty):
++            return int(str(self.readDeadProperty(TwistedQuotaRootProperty)))
++        else:
++            return None
++    
++    def setQuotaRoot(self, request, maxsize):
++        """
++        @param maxsize: a C{int} containing the maximum allowed bytes for the contents
++            of this collection.
++        """
++        assert self.isCollection(), "Only collections can have a quota root"
++        assert isinstance(maxsize, int), "maxsize must be an int"
++        
++        self.writeDeadProperty(TwistedQuotaRootProperty.fromString(str(maxsize)))
++    
++    def quotaSize(self, request):
++        """
++        Get the size of this resource.
++        TODO: Take into account size of dead-properties.
++
++        @return: a C{int} containing the size of the resource.
++        """
++        unimplemented(self)
++
++    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.hasDeadProperty(TwistedQuotaRootProperty):
++                self.updateQuotaUse(request, adjust)
++        
++        # 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: a C{int} 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.hasDeadProperty(TwistedQuotaRootProperty), "Quota use only on quota root collection"
++        
++        # Try to get the cached value property
++        if self.hasDeadProperty(TwistedQuotaUsedProperty):
++            return int(str(self.readDeadProperty(TwistedQuotaUsedProperty)))
++        else:
++            # Do brute force size determination
++            result = self.determineActualQuotaUse(request)
++            
++            # Cache the brute force value in the private property
++            self.writeDeadProperty(TwistedQuotaUsedProperty.fromString(str(result)))
++            
++            return result
++
++    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: a C{int} 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
++        size = self.currentQuotaUse(request)
++        size += adjust
++        self.writeDeadProperty(TwistedQuotaUsedProperty.fromString(str(size)))
++
++    def determineActualQuotaUse(self, request):
++        """
++        Brute force determination of quota used by this collection.
++
++        @return: a C{int} 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"
++        unimplemented(self)
++        
++    ##
+     # HTTP
+     ##
+ 
+@@ -1631,6 +1832,28 @@
+ 
+ davxml.registerElement(TwistedAccessDisabledProperty)
+ 
++"""
++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()),

Modified: CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.static.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.static.patch	2006-09-12 21:02:19 UTC (rev 128)
+++ CalendarServer/branches/users/cdaboo/quota/lib-patches/Twisted/twisted.web2.dav.static.patch	2006-09-12 21:18:22 UTC (rev 129)
@@ -2,8 +2,16 @@
 ===================================================================
 --- twisted/web2/dav/static.py	(revision 18074)
 +++ twisted/web2/dav/static.py	(working copy)
-@@ -34,6 +34,8 @@
+@@ -29,6 +29,7 @@
+ __all__ = ["DAVFile"]
+ 
+ import os
++import stat
+ 
+ from twisted.python import log
  from twisted.internet.defer import succeed, deferredGenerator, waitForDeferred
+@@ -34,6 +35,8 @@
+ from twisted.internet.defer import succeed, deferredGenerator, waitForDeferred
  from twisted.web2.static import File
  from twisted.web2 import dirlist
 +from twisted.web2 import http
@@ -11,3 +19,59 @@
  from twisted.web2.dav import davxml
  from twisted.web2.dav.idav import IDAVResource
  from twisted.web2.dav.resource import DAVResource
+@@ -145,6 +148,55 @@
+         return succeed(DAVFile._supportedPrivilegeSet)
+ 
+     ##
++    # 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: a C{int} containing the size of the resource.
++        """
++        result = os.stat(self.fp.path)
++        return result[stat.ST_SIZE]
++
++    def determineActualQuotaUse(self, request):
++        """
++        Brute force determination of quota used by this collection.
++
++        @return: a C{int} 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"
++
++        def walktree(top):
++            """
++            Recursively descend the directory tree rooted at top,
++            calling the callback function for each regular file
++            """
++        
++            total = 0
++            for f in os.listdir(top):
++                pathname = os.path.join(top, f)
++                result = os.stat(pathname)
++                mode = result[stat.ST_MODE]
++                if stat.S_ISDIR(mode):
++                    # It's a directory, recurse into it
++                    total += walktree(pathname)
++                elif stat.S_ISREG(mode):
++                    # It's a file, call the callback function
++                    total += result[stat.ST_SIZE]
++                else:
++                    # Unknown file type, print a message
++                    pass
++        
++            return total
++        
++        return walktree(self.fp.path)
++
++    ##
+     # Workarounds for issues with File
+     ##
+ 

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20060912/4e91e2c5/attachment.html


More information about the calendarserver-changes mailing list