[CalendarServer-changes] [2569] CalendarServer/branches/users/cdaboo/sqlprops-2557
source_changes at macosforge.org
source_changes at macosforge.org
Tue Jun 17 07:27:51 PDT 2008
Revision: 2569
http://trac.macosforge.org/projects/calendarserver/changeset/2569
Author: cdaboo at apple.com
Date: 2008-06-17 07:27:41 -0700 (Tue, 17 Jun 2008)
Log Message:
-----------
Merged forward. Note this is not a viable solution as live properties are not handled properly.
An alternative solution is being worked on.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch
CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.delete.patch
CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.propfind.patch
CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch
CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.resource.patch
CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/__init__.py
CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/extensions.py
CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/method/put_common.py
CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/root.py
CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/sql.py
CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/static.py
CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/tap.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/sqlprops.py
CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/test/test_sqlprops.py
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.copymove.patch 2008-06-17 14:27:41 UTC (rev 2569)
@@ -53,16 +53,27 @@
yield destparent
destparent = destparent.getResult()
-@@ -144,7 +155,19 @@
+@@ -144,9 +155,30 @@
log.err(msg)
raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
- x = waitForDeferred(move(self.fp, request.uri, destination.fp, destination_uri, depth))
+- yield x
+- yield x.getResult()
+ # Lets optimise a move within the same directory to a new resource as a simple move
+ # rather than using the full transaction based storeResource api. This allows simple
+ # "rename" operations to work quickly.
+ if (not destination.exists()) and destparent == parent:
+ x = waitForDeferred(move(self.fp, request.uri, destination.fp, destination_uri, depth))
++ yield x
++ result = x.getResult()
++
++ # Explicitly move properties - no one else does it
++ properties = self.deadProperties().getAll()
++ destination.deadProperties().setSeveral([p for p in properties.itervalues()])
++ self.deadProperties().deleteAll()
++
++ yield result
+ else:
+ x = waitForDeferred(put_common.storeResource(request,
+ source=self,
@@ -71,6 +82,8 @@
+ destination_uri=destination_uri,
+ deletesource=True,
+ depth=depth))
- yield x
- yield x.getResult()
++ yield x
++ yield x.getResult()
+ http_MOVE = deferredGenerator(http_MOVE)
+
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.delete.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.delete.patch 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.delete.patch 2008-06-17 14:27:41 UTC (rev 2569)
@@ -2,7 +2,7 @@
===================================================================
--- twisted/web2/dav/method/delete.py (revision 19773)
+++ twisted/web2/dav/method/delete.py (working copy)
-@@ -58,8 +58,28 @@
+@@ -58,8 +58,31 @@
yield x
x.getResult()
@@ -23,6 +23,9 @@
- yield x.getResult()
+ result = x.getResult()
++ # Explicitly delete properties - no one else does it
++ self.deadProperties().deleteAll()
++
+ # Adjust quota
+ if myquota is not None:
+ d = waitForDeferred(self.quotaSizeAdjust(request, -old_size))
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.propfind.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.propfind.patch 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.propfind.patch 2008-06-17 14:27:41 UTC (rev 2569)
@@ -2,8 +2,231 @@
===================================================================
--- twisted/web2/dav/method/propfind.py (revision 19773)
+++ twisted/web2/dav/method/propfind.py (working copy)
-@@ -200,7 +200,7 @@
+@@ -22,6 +22,7 @@
+ #
+ # DRI: Wilfredo Sanchez, wsanchez at apple.com
+ ##
++from twisted.web2.dav.util import joinURL
+ """
+ WebDAV PROPFIND method
+@@ -30,13 +31,12 @@
+ __all__ = ["http_PROPFIND"]
+
+ from twisted.python import log
+-from twisted.python.failure import Failure
+ from twisted.internet.defer import deferredGenerator, waitForDeferred
+ from twisted.web2.http import HTTPError
+ from twisted.web2 import responsecode
+ from twisted.web2.http import StatusResponse
+ from twisted.web2.dav import davxml
+-from twisted.web2.dav.http import MultiStatusResponse, statusForFailure
++from twisted.web2.dav.http import MultiStatusResponse
+ from twisted.web2.dav.util import normalizeURL, davXMLFromStream
+
+ def http_PROPFIND(self, request):
+@@ -114,79 +114,136 @@
+ yield filtered_aces
+ filtered_aces = filtered_aces.getResult()
+
+- resources = [(self, my_url)]
++ resources = [my_url.rstrip("/")]
+
+- d = self.findChildren(depth, request, lambda x, y: resources.append((x, y)), (davxml.Read(),), inherited_aces=filtered_aces)
+- x = waitForDeferred(d)
+- yield x
+- x.getResult()
++ if depth == "1":
++ d = self.findChildNames(request, lambda x: resources.append(joinURL(request.uri, x)), privileges=(davxml.Read(),), inherited_aces=filtered_aces)
++ x = waitForDeferred(d)
++ yield x
++ x.getResult()
++ # TODO: depth=infinity
+
+- for resource, uri in resources:
+- if search_properties is "names":
+- try:
+- resource_properties = waitForDeferred(resource.listProperties(request))
+- yield resource_properties
+- resource_properties = resource_properties.getResult()
+- except:
+- log.err("Unable to get properties for resource %r" % (resource,))
+- raise
+-
++ if search_properties is "names":
++ results = {}
++ results[request.uri] = self.deadProperties().list()
++
++ if depth == "1":
++ allowed_uris = set(resources)
++ child_results = self.deadProperties().listAll()
++ for key, value in child_results.iteritems():
++ uri = joinURL(request.uri, key)
++ if uri in allowed_uris:
++ results[uri] = value
++
++ uris = results.keys()
++ uris.sort()
++ for uri in uris:
+ properties_by_status = {
+- responsecode.OK: [propertyName(p) for p in resource_properties]
++ responsecode.OK: [propertyName(p) for p in results[uri]]
+ }
+- else:
++ propstats = []
++
++ for status in properties_by_status:
++ properties = properties_by_status[status]
++ if not properties: continue
++
++ xml_status = davxml.Status.fromResponseCode(status)
++ xml_container = davxml.PropertyContainer(*properties)
++ xml_propstat = davxml.PropertyStatus(xml_container, xml_status)
++
++ propstats.append(xml_propstat)
++
++ xml_resource = davxml.HRef(uri)
++ if propstats:
++ xml_response = davxml.PropertyStatusResponse(xml_resource, *propstats)
++ else:
++ xml_response = davxml.StatusResponse(xml_resource, davxml.Status.fromResponseCode(responsecode.OK))
++
++ xml_responses.append(xml_response)
++
++ elif search_properties is "all":
++ results = {}
++ results[request.uri] = self.deadProperties().getAll(nohidden=True)
++
++ if depth == "1":
++ allowed_uris = set(resources)
++ child_results = self.deadProperties().getAllResources(nohidden=True)
++ for key, value in child_results.iteritems():
++ uri = joinURL(request.uri, key)
++ if uri in allowed_uris:
++ results[uri] = value
++
++ uris = results.keys()
++ uris.sort()
++ for uri in uris:
+ properties_by_status = {
+- responsecode.OK : [],
+- responsecode.NOT_FOUND : [],
++ responsecode.OK: [p for p in results[uri].itervalues()]
+ }
+-
+- if search_properties is "all":
+- properties_to_enumerate = waitForDeferred(resource.listAllprop(request))
+- yield properties_to_enumerate
+- properties_to_enumerate = properties_to_enumerate.getResult()
++ propstats = []
++
++ for status in properties_by_status:
++ properties = properties_by_status[status]
++ if not properties: continue
++
++ xml_status = davxml.Status.fromResponseCode(status)
++ xml_container = davxml.PropertyContainer(*properties)
++ xml_propstat = davxml.PropertyStatus(xml_container, xml_status)
++
++ propstats.append(xml_propstat)
++
++ xml_resource = davxml.HRef(uri)
++ if propstats:
++ xml_response = davxml.PropertyStatusResponse(xml_resource, *propstats)
+ else:
+- properties_to_enumerate = search_properties
++ xml_response = davxml.StatusResponse(xml_resource, davxml.Status.fromResponseCode(responsecode.OK))
++
++ xml_responses.append(xml_response)
++
++ else:
++ results = {}
++ results[request.uri] = self.deadProperties().getSeveral(search_properties)
++
++ if depth == "1":
++ allowed_uris = set(resources)
++ child_results = self.deadProperties().getSeveralResources(search_properties)
++ for key, value in child_results.iteritems():
++ uri = joinURL(request.uri, key)
++ if uri in allowed_uris:
++ results[uri] = value
+
+- for property in properties_to_enumerate:
+- has = waitForDeferred(resource.hasProperty(property, request))
+- yield has
+- has = has.getResult()
+- if has:
+- try:
+- resource_property = waitForDeferred(resource.readProperty(property, request))
+- yield resource_property
+- resource_property = resource_property.getResult()
+- except:
+- f = Failure()
+-
+- log.err("Error reading property %r for resource %s: %s" % (property, uri, f.value))
+-
+- status = statusForFailure(f, "getting property: %s" % (property,))
+- if status not in properties_by_status:
+- properties_by_status[status] = []
+- properties_by_status[status].append(propertyName(property))
+- else:
+- properties_by_status[responsecode.OK].append(resource_property)
++ uris = results.keys()
++ uris.sort()
++ for uri in uris:
++ properties_by_status = {
++ responsecode.OK : [],
++ responsecode.NOT_FOUND : [],
++ }
++ for prop in search_properties:
++ if results[uri].has_key(prop):
++ properties_by_status[responsecode.OK].append(results[uri][prop])
+ else:
+- properties_by_status[responsecode.NOT_FOUND].append(propertyName(property))
++ properties_by_status[responsecode.NOT_FOUND].append(propertyName(prop))
++
++ propstats = []
++
++ for status in properties_by_status:
++ properties = properties_by_status[status]
++ if not properties: continue
++
++ xml_status = davxml.Status.fromResponseCode(status)
++ xml_container = davxml.PropertyContainer(*properties)
++ xml_propstat = davxml.PropertyStatus(xml_container, xml_status)
++
++ propstats.append(xml_propstat)
++
++ xml_resource = davxml.HRef(uri)
++ if propstats:
++ xml_response = davxml.PropertyStatusResponse(xml_resource, *propstats)
++ else:
++ xml_response = davxml.StatusResponse(xml_resource, davxml.Status.fromResponseCode(responsecode.OK))
++
++ xml_responses.append(xml_response)
+
+- propstats = []
+-
+- for status in properties_by_status:
+- properties = properties_by_status[status]
+- if not properties: continue
+-
+- xml_status = davxml.Status.fromResponseCode(status)
+- xml_container = davxml.PropertyContainer(*properties)
+- xml_propstat = davxml.PropertyStatus(xml_container, xml_status)
+-
+- propstats.append(xml_propstat)
+-
+- xml_resource = davxml.HRef(uri)
+- xml_response = davxml.PropertyStatusResponse(xml_resource, *propstats)
+-
+- xml_responses.append(xml_response)
+-
+ #
+ # Return response
+ #
+@@ -200,7 +257,7 @@
+
def propertyName(name):
property_namespace, property_name = name
- class PropertyName (davxml.WebDAVEmptyElement):
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.method.put_common.patch 2008-06-17 14:27:41 UTC (rev 2569)
@@ -2,7 +2,7 @@
===================================================================
--- twisted/web2/dav/method/put_common.py (revision 0)
+++ twisted/web2/dav/method/put_common.py (revision 0)
-@@ -0,0 +1,266 @@
+@@ -0,0 +1,274 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
@@ -211,6 +211,11 @@
+ yield response
+ response = response.getResult()
+
++ # Copy dead properties first, before adding overridden values
++ if source is not None:
++ properties = source.deadProperties().getAll()
++ destination.deadProperties().setSeveral([p for p in properties.itervalues()])
++
+ # Update the MD5 value on the resource
+ if source is not None:
+ # Copy MD5 value from source to destination
@@ -256,6 +261,9 @@
+ delete(source_uri, source.fp, depth)
+ rollback.source_deleted = True
+
++ # Explicitly delete properties - no one else does it
++ source.deadProperties().deleteAll()
++
+ # Can now commit changes and forget the rollback details
+ rollback.Commit()
+
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.resource.patch
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.resource.patch 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/lib-patches/Twisted/twisted.web2.dav.resource.patch 2008-06-17 14:27:41 UTC (rev 2569)
@@ -91,7 +91,7 @@
def ifAllowed(privileges, callback):
def onError(failure):
-@@ -286,7 +307,36 @@
+@@ -286,14 +307,39 @@
d.addCallback(gotACL)
return d
return ifAllowed((davxml.ReadACL(),), callback)
@@ -127,8 +127,16 @@
+
elif namespace == twisted_dav_namespace:
if name == "resource-class":
- class ResourceClass (davxml.WebDAVTextElement):
-@@ -363,15 +413,28 @@
+- class ResourceClass (davxml.WebDAVTextElement):
+- namespace = twisted_dav_namespace
+- name = "resource-class"
+- hidden = False
+- return ResourceClass(self.__class__.__name__)
++ return TwistedResourceClass(self.__class__.__name__)
+
+ elif namespace == twisted_private_namespace:
+ raise HTTPError(StatusResponse(
+@@ -363,15 +409,28 @@
"""
See L{IDAVResource.listProperties}.
"""
@@ -161,7 +169,7 @@
def listAllprop(self, request):
"""
Some DAV properties should not be returned to a C{DAV:allprop} query.
-@@ -465,8 +528,22 @@
+@@ -465,8 +524,22 @@
return super(DAVPropertyMixIn, self).displayName()
class DAVResource (DAVPropertyMixIn, StaticRenderMixin):
@@ -184,7 +192,7 @@
##
# DAV
##
-@@ -570,19 +647,21 @@
+@@ -570,19 +643,21 @@
See L{IDAVResource.authorize}.
"""
def onError(failure):
@@ -211,7 +219,7 @@
response = UnauthorizedResponse(request.credentialFactories,
request.remoteAddr)
else:
-@@ -593,7 +672,7 @@
+@@ -593,7 +668,7 @@
# class is supposed to be a FORBIDDEN status code and
# "Authorization will not help" according to RFC2616
#
@@ -220,7 +228,7 @@
d = self.checkPrivileges(request, privileges, recurse)
d.addErrback(onErrors)
-@@ -606,16 +685,21 @@
+@@ -606,16 +681,21 @@
def authenticate(self, request):
def loginSuccess(result):
@@ -246,7 +254,7 @@
authHeader = request.headers.getHeader('authorization')
-@@ -631,9 +715,10 @@
+@@ -631,9 +711,10 @@
# Try to match principals in each principal collection on the resource
def gotDetails(details):
@@ -260,7 +268,7 @@
def login(pcreds):
d = request.portal.login(pcreds, None, *request.loginInterfaces)
-@@ -641,13 +726,15 @@
+@@ -641,13 +722,15 @@
return d
@@ -280,7 +288,7 @@
##
# ACL
-@@ -656,49 +743,23 @@
+@@ -656,49 +739,23 @@
def currentPrincipal(self, request):
"""
@param request: the request being processed.
@@ -339,7 +347,7 @@
"""
@return: the L{davxml.ACL} element containing the default access control
list for this resource.
-@@ -710,6 +771,17 @@
+@@ -710,6 +767,17 @@
#
return readonlyACL
@@ -357,7 +365,7 @@
def setAccessControlList(self, acl):
"""
See L{IDAVResource.setAccessControlList}.
-@@ -748,13 +820,16 @@
+@@ -748,13 +816,16 @@
# 10. Verify that new acl is not in conflict with itself
# 11. Update acl on the resource
@@ -375,7 +383,7 @@
# Need to get list of supported privileges
supported = []
-@@ -1038,9 +1113,9 @@
+@@ -1038,9 +1109,9 @@
if myURL == "/":
# If we get to the root without any ACLs, then use the default.
@@ -387,7 +395,7 @@
# Dynamically update privileges for those ace's that are inherited.
if inheritance:
-@@ -1076,7 +1151,7 @@
+@@ -1076,7 +1147,7 @@
# Adjust ACE for inherit on this resource
children = list(ace.children)
children.remove(TwistedACLInheritable())
@@ -396,7 +404,7 @@
aces.append(davxml.ACE(*children))
else:
aces.extend(inherited_aces)
-@@ -1105,8 +1180,7 @@
+@@ -1105,8 +1176,7 @@
the child resource loop and supply those to the checkPrivileges on each child.
@param request: the L{IRequest} for the request in progress.
@@ -406,7 +414,7 @@
"""
# Get the parent ACLs with inheritance and preserve the <inheritable> element.
-@@ -1128,21 +1202,9 @@
+@@ -1128,21 +1198,9 @@
# Adjust ACE for inherit on this resource
children = list(ace.children)
children.remove(TwistedACLInheritable())
@@ -430,7 +438,7 @@
inheritedACEsforChildren = deferredGenerator(inheritedACEsforChildren)
-@@ -1152,49 +1214,69 @@
+@@ -1152,49 +1210,69 @@
This implementation returns an empty set.
"""
@@ -528,7 +536,7 @@
def samePrincipal(self, principal1, principal2):
"""
Check whether the two prinicpals are exactly the same in terms of
-@@ -1219,7 +1301,6 @@
+@@ -1219,7 +1297,6 @@
return False
def matchPrincipal(self, principal1, principal2, request):
@@ -536,7 +544,7 @@
"""
Check whether the principal1 is a principal in the set defined by
principal2.
-@@ -1244,6 +1325,9 @@
+@@ -1244,6 +1321,9 @@
if isinstance(principal1, davxml.Unauthenticated):
yield False
return
@@ -546,7 +554,7 @@
else:
yield True
return
-@@ -1271,7 +1355,6 @@
+@@ -1271,7 +1351,6 @@
assert principal2 is not None, "principal2 is None"
@@ -554,7 +562,7 @@
# Compare two HRefs and do group membership test as well
if principal1 == principal2:
yield True
-@@ -1289,6 +1372,7 @@
+@@ -1289,6 +1368,7 @@
matchPrincipal = deferredGenerator(matchPrincipal)
@@ -562,7 +570,7 @@
def principalIsGroupMember(self, principal1, principal2, request):
"""
Check whether one principal is a group member of another.
-@@ -1299,18 +1383,21 @@
+@@ -1299,18 +1379,21 @@
@return: L{Deferred} with result C{True} if principal1 is a member of principal2, C{False} otherwise
"""
@@ -595,7 +603,7 @@
def validPrincipal(self, ace_principal, request):
"""
-@@ -1351,11 +1438,16 @@
+@@ -1351,11 +1434,16 @@
@return C{True} if C{href_principal} is valid, C{False} otherwise.
This implementation tests for a href element that corresponds to
@@ -615,7 +623,7 @@
return d
def resolvePrincipal(self, principal, request):
-@@ -1432,7 +1524,7 @@
+@@ -1432,7 +1520,7 @@
log.err("DAV:self ACE is set on non-principal resource %r" % (self,))
yield None
return
@@ -624,7 +632,7 @@
if isinstance(principal, davxml.HRef):
yield principal
-@@ -1517,6 +1609,270 @@
+@@ -1517,6 +1605,270 @@
return None
##
@@ -895,7 +903,7 @@
# HTTP
##
-@@ -1525,10 +1881,6 @@
+@@ -1525,10 +1877,6 @@
#litmus = request.headers.getRawHeaders("x-litmus")
#if litmus: log.msg("*** Litmus test: %s ***" % (litmus,))
@@ -906,7 +914,7 @@
#
# If this is a collection and the URI doesn't end in "/", redirect.
#
-@@ -1567,7 +1919,7 @@
+@@ -1567,7 +1915,7 @@
def findChildren(self, depth, request, callback, privileges=None, inherited_aces=None):
return succeed(None)
@@ -915,7 +923,7 @@
"""
Resource representing a WebDAV principal. (RFC 3744, section 2)
"""
-@@ -1577,7 +1929,7 @@
+@@ -1577,7 +1925,7 @@
# WebDAV
##
@@ -924,7 +932,7 @@
(dav_namespace, "alternate-URI-set"),
(dav_namespace, "principal-URL" ),
(dav_namespace, "group-member-set" ),
-@@ -1585,14 +1937,11 @@
+@@ -1585,14 +1933,11 @@
)
def davComplianceClasses(self):
@@ -940,7 +948,7 @@
def readProperty(self, property, request):
def defer():
if type(property) is tuple:
-@@ -1610,10 +1959,20 @@
+@@ -1610,10 +1955,20 @@
return davxml.PrincipalURL(davxml.HRef(self.principalURL()))
if name == "group-member-set":
@@ -963,7 +971,7 @@
if name == "resourcetype":
if self.isCollection():
-@@ -1655,7 +2014,7 @@
+@@ -1655,7 +2010,7 @@
principals. Subclasses should override this method to provide member
URLs for this resource if appropriate.
"""
@@ -972,7 +980,7 @@
def groupMemberships(self):
"""
-@@ -1666,6 +2025,7 @@
+@@ -1666,6 +2021,7 @@
"""
unimplemented(self)
@@ -980,7 +988,7 @@
def principalMatch(self, href):
"""
Check whether the supplied principal matches this principal or is a
-@@ -1675,10 +2035,33 @@
+@@ -1675,10 +2031,33 @@
"""
uri = str(href)
if self.principalURL() == uri:
@@ -1016,7 +1024,7 @@
class AccessDeniedError(Exception):
def __init__(self, errors):
"""
-@@ -1718,6 +2101,37 @@
+@@ -1718,6 +2097,48 @@
davxml.registerElement(TwistedACLInheritable)
davxml.ACE.allowed_children[(twisted_dav_namespace, "inheritable")] = (0, 1)
@@ -1051,6 +1059,17 @@
+
+davxml.registerElement(TwistedQuotaUsedProperty)
+
++"""
++Indicates the Python class used, a type derived from DAVResource, that represents the WebDAV
++resource on which it is defined as a property.
++"""
++class TwistedResourceClass (davxml.WebDAVTextElement):
++ namespace = twisted_dav_namespace
++ name = "resource-class"
++ hidden = False
++
++davxml.registerElement(TwistedResourceClass)
++
allACL = davxml.ACL(
davxml.ACE(
davxml.Principal(davxml.All()),
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/__init__.py 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/__init__.py 2008-06-17 14:27:41 UTC (rev 2569)
@@ -41,12 +41,14 @@
"itip",
"log",
"memcache",
+ "memcachepool",
"memcacher",
"principalindex",
"resource",
"root",
"schedule",
"sql",
+ "sqlprops",
"static",
]
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/extensions.py 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/extensions.py 2008-06-17 14:27:41 UTC (rev 2569)
@@ -14,6 +14,7 @@
# limitations under the License.
##
+
"""
Extensions to web2.dav
"""
@@ -31,28 +32,37 @@
import cPickle as pickle
import urllib
import cgi
+import os
import time
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+from twisted.internet.defer import maybeDeferred
from twisted.internet.defer import succeed, deferredGenerator, waitForDeferred
-from twisted.internet.defer import maybeDeferred
from twisted.web2 import responsecode
-from twisted.web2.http import HTTPError, Response, RedirectResponse
-from twisted.web2.http_headers import MimeType
-from twisted.web2.stream import FileStream
-from twisted.web2.static import MetaDataMixin
from twisted.web2.dav import davxml
from twisted.web2.dav.davxml import dav_namespace
from twisted.web2.dav.http import StatusResponse
+from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.resource import DAVPrincipalResource as SuperDAVPrincipalResource
+from twisted.web2.dav.resource import DAVResource as SuperDAVResource
+from twisted.web2.dav.resource import TwistedACLInheritable
+from twisted.web2.dav.resource import TwistedResourceClass
from twisted.web2.dav.static import DAVFile as SuperDAVFile
-from twisted.web2.dav.resource import DAVResource as SuperDAVResource
-from twisted.web2.dav.resource import DAVPrincipalResource as SuperDAVPrincipalResource
from twisted.web2.dav.util import joinURL
+from twisted.web2.dav.util import parentForURL
from twisted.web2.dav.xattrprops import xattrPropertyStore
+from twisted.web2.http import HTTPError, Response, RedirectResponse
+from twisted.web2.http_headers import MimeType
+from twisted.web2.http_headers import generateContentType
+from twisted.web2.static import MetaDataMixin
+from twisted.web2.stream import FileStream
from twistedcaldav.log import Logger, LoggingMixIn
from twistedcaldav.util import submodule
from twistedcaldav.directory.sudo import SudoDirectoryService
from twistedcaldav.directory.directory import DirectoryService
+from twistedcaldav.sqlprops import sqlPropertyStore
log = Logger()
@@ -202,8 +212,462 @@
return fun
+class DAVPropertyHandlerMixin(object):
-class DAVResource (SudoSACLMixin, SuperDAVResource, LoggingMixIn):
+ def create(self):
+ self.updateProperties()
+
+ def update(self):
+ self.updateProperties()
+
+ def updateProperties(self):
+ """
+ Push "live" properties into the dead property store".
+ """
+
+ props_to_set = []
+ # resourcetype
+ if self.isCollection():
+ props_to_set.append(davxml.ResourceType.collection)
+ else:
+ props_to_set.append(davxml.ResourceType.empty)
+
+ # getetag
+ etag = self.etag()
+ if etag is not None:
+ props_to_set.append(davxml.GETETag(etag.generate()))
+
+ # getcontenttype
+ mimeType = self.contentType()
+ if mimeType is not None:
+ mimeType.params = None # WebDAV getcontenttype property does not include parameters
+ props_to_set.append(davxml.GETContentType(generateContentType(mimeType)))
+
+ # getcontentlength
+ length = self.contentLength()
+ if length is None:
+ # TODO: really we should "render" the resource and
+ # determine its size from that but for now we just
+ # return an empty element.
+ props_to_set.append(davxml.GETContentLength(""))
+ else:
+ props_to_set.append(davxml.GETContentLength(str(length)))
+
+ # getlastmodified
+ lastModified = self.lastModified()
+ if lastModified is not None:
+ props_to_set.append(davxml.GETLastModified.fromDate(lastModified))
+
+ # creationdate
+ creationDate = self.creationDate()
+ if creationDate is not None:
+ props_to_set.append(davxml.CreationDate.fromDate(creationDate))
+
+ # displayname
+ displayName = self.displayName()
+ if displayName is not None:
+ props_to_set.append(davxml.DisplayName(displayName))
+
+ # acl
+ props_to_set.append(self.defaultAccessControlList())
+ #from twistedcaldav.static import calendarPrivilegeSet
+ #props_to_set.append(calendarPrivilegeSet)
+
+ # resource class
+ props_to_set.append(TwistedResourceClass(self.__class__.__name__))
+
+ # Now write out all properties in one go
+ self.deadProperties().setSeveral(props_to_set)
+
+ def isClassProperty(self, property):
+ if isinstance(property, tuple):
+ qname = property
+ else:
+ qname = property.qname()
+
+ return qname in ((dav_namespace, "supported-privilege-set"),)
+
+ def computeClassProperty(self, classname, property):
+ from twistedcaldav.static import calendarPrivilegeSet
+
+ if isinstance(property, tuple):
+ namespace, name = property
+ else:
+ namespace, name = property.qname()
+
+ if namespace == dav_namespace:
+ if name == "supported-privilege-set":
+ return calendarPrivilegeSet
+
+class DAVFastACLMixin(object):
+
+ def findChildNames(self, request, callbackAllowed, callbackForbidden=None, privileges=None, inherited_aces=None):
+
+ # Get aces for children
+ acls = self.deadProperties().getSeveralResources((
+ davxml.ACL.qname(),
+ TwistedResourceClass.qname(),
+ davxml.GETETag.qname(),
+ ))
+ aclmap = {}
+ for name, props in acls.iteritems():
+ acl = props.get(davxml.ACL.qname(), None)
+ privyset = self.computeClassProperty(props.get(TwistedResourceClass.qname(), None), (dav_namespace, "supported-privilege-set"))
+ aclmap.setdefault((acl, privyset), []).append(name)
+
+ # Now determine whether each ace satisfies privileges
+ for (acl, privyset), names in aclmap.iteritems():
+ checked = waitForDeferred(self.checkACLPrivilege(request, acl, privyset, privileges, inherited_aces))
+ yield checked
+ checked = checked.getResult()
+ if checked:
+ for name in names:
+ callbackAllowed(name)
+ elif callbackForbidden:
+ for name in names:
+ callbackForbidden(name)
+
+ yield None
+
+ findChildNames = deferredGenerator(findChildNames)
+
+ def checkACLPrivilege(self, request, acl, privyset, privileges, inherited_aces):
+
+ principal = self.currentPrincipal(request)
+
+ # Other principals types don't make sense as actors.
+ assert (
+ principal.children[0].name in ("unauthenticated", "href"),
+ "Principal is not an actor: %r" % (principal,)
+ )
+
+ acl = self.fullAccessControlList(acl, inherited_aces)
+
+ pending = list(privileges)
+ denied = []
+
+ for ace in acl.children:
+ for privilege in tuple(pending):
+ if not self.matchPrivilege(davxml.Privilege(privilege), ace.privileges, privyset):
+ continue
+
+ match = waitForDeferred(self.matchPrincipal(principal, ace.principal, request))
+ yield match
+ match = match.getResult()
+
+ if match:
+ if ace.invert:
+ continue
+ else:
+ if not ace.invert:
+ continue
+
+ pending.remove(privilege)
+
+ if not ace.allow:
+ denied.append(privilege)
+
+ yield len(denied) + len(pending) == 0
+
+ checkACLPrivilege = deferredGenerator(checkACLPrivilege)
+
+ def fullAccessControlList(self, acl, inherited_aces):
+ """
+ See L{IDAVResource.accessControlList}.
+
+ This implementation looks up the ACL in the private property
+ C{(L{twisted_private_namespace}, "acl")}.
+ If no ACL has been stored for this resource, it returns the value
+ returned by C{defaultAccessControlList}.
+ If access is disabled it will return C{None}.
+ """
+ #
+ # Inheritance is problematic. Here is what we do:
+ #
+ # 1. A private element <Twisted:inheritable> is defined for use inside
+ # of a <DAV:ace>. This private element is removed when the ACE is
+ # exposed via WebDAV.
+ #
+ # 2. When checking ACLs with inheritance resolution, the server must
+ # examine all parent resources of the current one looking for any
+ # <Twisted:inheritable> elements.
+ #
+ # If those are defined, the relevant ace is applied to the ACL on the
+ # current resource.
+ #
+
+ # Dynamically update privileges for those ace's that are inherited.
+ if acl:
+ aces = list(acl.children)
+ else:
+ aces = []
+
+ aces.extend(inherited_aces)
+
+ acl = davxml.ACL(*aces)
+
+ return acl
+
+ def findChildrenBatch(self, depth, request, callback, privileges=None, inherited_aces=None):
+ """
+ See L{IDAVResource.findChildren}.
+
+ This implementation works for C{depth} values of C{"0"}, C{"1"},
+ and C{"infinity"}. As long as C{self.listChildren} is implemented
+ """
+ assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,)
+
+ if depth == "0" or not self.isCollection():
+ return succeed(None)
+
+ # Get aces for children
+ aclprops = self.deadProperties().getSeveralResources((
+ davxml.ACL.qname(),
+ ))
+ acls = {}
+ for name, props in acls.iteritems():
+ acls[name] = props.get(davxml.ACL.qname(), None)
+
+ completionDeferred = Deferred()
+ basepath = request.urlForResource(self)
+ children = list(self.listChildren())
+
+ def checkPrivilegesError(failure):
+ failure.trap(AccessDeniedError)
+ reactor.callLater(0, getChild)
+
+ def checkPrivileges(child):
+ if child is None:
+ return None
+
+ if privileges is None:
+ return child
+
+ cname = os.path.basename(request.urlForResource(self))
+ d = child.checkPrivilegesBatch(request, privileges, inherited_aces=inherited_aces, acl=acls.get(cname, None))
+ d.addCallback(lambda _: child)
+ return d
+
+ def gotChild(child, childpath):
+ if child is None:
+ callback(None, childpath + "/")
+ else:
+ if child.isCollection():
+ callback(child, childpath + "/")
+ if depth == "infinity":
+ d = child.findChildren(depth, request, callback, privileges)
+ d.addCallback(lambda x: reactor.callLater(0, getChild))
+ return d
+ else:
+ callback(child, childpath)
+
+ reactor.callLater(0, getChild)
+
+ def getChild():
+ try:
+ childname = children.pop()
+ except IndexError:
+ completionDeferred.callback(None)
+ else:
+ childpath = joinURL(basepath, childname)
+ d = request.locateChildResource(self, childname)
+ d.addCallback(checkPrivileges)
+ d.addCallbacks(gotChild, checkPrivilegesError, (childpath,))
+ d.addErrback(completionDeferred.errback)
+
+ getChild()
+
+ return completionDeferred
+
+ def checkPrivilegesBatch(self, request, privileges, recurse=False, principal=None, inherited_aces=None, acl=None):
+ """
+ Check whether the given principal has the given privileges.
+ (RFC 3744, section 5.5)
+ @param request: the request being processed.
+ @param privileges: an iterable of L{davxml.WebDAVElement} elements
+ denoting access control privileges.
+ @param recurse: C{True} if a recursive check on all child
+ resources of this resource should be performed as well,
+ C{False} otherwise.
+ @param principal: the L{davxml.Principal} to check privileges
+ for. If C{None}, it is deduced from C{request} by calling
+ L{currentPrincipal}.
+ @param inherited_aces: a list of L{davxml.ACE}s corresponding to the precomputed
+ inheritable aces from the parent resource hierarchy.
+ @return: a L{Deferred} that callbacks with C{None} or errbacks with an
+ L{AccessDeniedError}
+ """
+ if principal is None:
+ principal = self.currentPrincipal(request)
+
+ supportedPrivs = waitForDeferred(self.supportedPrivileges(request))
+ yield supportedPrivs
+ supportedPrivs = supportedPrivs.getResult()
+
+ # Other principals types don't make sense as actors.
+ assert (
+ principal.children[0].name in ("unauthenticated", "href"),
+ "Principal is not an actor: %r" % (principal,)
+ )
+
+ errors = []
+
+ resources = [(self, None)]
+
+ if recurse:
+ x = self.findChildren("infinity", request, lambda x, y: resources.append((x,y)))
+ x = waitForDeferred(x)
+ yield x
+ x.getResult()
+
+ for resource, uri in resources:
+ acl = waitForDeferred(resource.accessControlListBatch(request, inherited_aces=inherited_aces, acl=acl))
+ yield acl
+ acl = acl.getResult()
+
+ # Check for disabled
+ if acl is None:
+ errors.append((uri, list(privileges)))
+ continue
+
+ pending = list(privileges)
+ denied = []
+
+ for ace in acl.children:
+ for privilege in tuple(pending):
+ if not self.matchPrivilege(davxml.Privilege(privilege), ace.privileges, supportedPrivs):
+ continue
+
+ match = waitForDeferred(self.matchPrincipal(principal, ace.principal, request))
+ yield match
+ match = match.getResult()
+
+ if match:
+ if ace.invert:
+ continue
+ else:
+ if not ace.invert:
+ continue
+
+ pending.remove(privilege)
+
+ if not ace.allow:
+ denied.append(privilege)
+
+ denied += pending # If no matching ACE, then denied
+
+ if denied:
+ errors.append((uri, denied))
+
+ if errors:
+ raise AccessDeniedError(errors,)
+
+ yield None
+
+ checkPrivilegesBatch = deferredGenerator(checkPrivilegesBatch)
+
+ def accessControlListBatch(self, request, inheritance=True, expanding=False, inherited_aces=None, acl=None):
+ """
+ See L{IDAVResource.accessControlList}.
+
+ This implementation looks up the ACL in the private property
+ C{(L{twisted_private_namespace}, "acl")}.
+ If no ACL has been stored for this resource, it returns the value
+ returned by C{defaultAccessControlList}.
+ If access is disabled it will return C{None}.
+ """
+ #
+ # Inheritance is problematic. Here is what we do:
+ #
+ # 1. A private element <Twisted:inheritable> is defined for use inside
+ # of a <DAV:ace>. This private element is removed when the ACE is
+ # exposed via WebDAV.
+ #
+ # 2. When checking ACLs with inheritance resolution, the server must
+ # examine all parent resources of the current one looking for any
+ # <Twisted:inheritable> elements.
+ #
+ # If those are defined, the relevant ace is applied to the ACL on the
+ # current resource.
+ #
+ myURL = None
+
+ def getMyURL():
+ url = request.urlForResource(self)
+
+ assert url is not None, "urlForResource(self) returned None for resource %s" % (self,)
+
+ return url
+
+ if acl is None:
+ # Produce a sensible default for an empty ACL.
+ if myURL is None:
+ myURL = getMyURL()
+
+ if myURL == "/":
+ # If we get to the root without any ACLs, then use the default.
+ acl = self.defaultRootAccessControlList()
+ else:
+ acl = self.defaultAccessControlList()
+
+ # Dynamically update privileges for those ace's that are inherited.
+ if inheritance:
+ aces = list(acl.children)
+
+ if myURL is None:
+ myURL = getMyURL()
+
+ if inherited_aces is None:
+ if myURL != "/":
+ parentURL = parentForURL(myURL)
+
+ parent = waitForDeferred(request.locateResource(parentURL))
+ yield parent
+ parent = parent.getResult()
+
+ if parent:
+ parent_acl = waitForDeferred(
+ parent.accessControlList(request, inheritance=True, expanding=True)
+ )
+ yield parent_acl
+ parent_acl = parent_acl.getResult()
+
+ # Check disabled
+ if parent_acl is None:
+ yield None
+ return
+
+ for ace in parent_acl.children:
+ if ace.inherited:
+ aces.append(ace)
+ elif TwistedACLInheritable() in ace.children:
+ # Adjust ACE for inherit on this resource
+ children = list(ace.children)
+ children.remove(TwistedACLInheritable())
+ children.append(davxml.Inherited(davxml.HRef(parentURL)))
+ aces.append(davxml.ACE(*children))
+ else:
+ aces.extend(inherited_aces)
+
+ # Always filter out any remaining private properties when we are
+ # returning the ACL for the final resource after doing parent
+ # inheritance.
+ if not expanding:
+ aces = [
+ davxml.ACE(*[
+ c for c in ace.children
+ if c != TwistedACLInheritable()
+ ])
+ for ace in aces
+ ]
+
+ acl = davxml.ACL(*aces)
+
+ yield acl
+
+ accessControlListBatch = deferredGenerator(accessControlListBatch)
+
+class DAVResource (SudoSACLMixin, DAVPropertyHandlerMixin, DAVFastACLMixin, SuperDAVResource, LoggingMixIn):
"""
Extended L{twisted.web2.dav.resource.DAVResource} implementation.
"""
@@ -419,7 +883,7 @@
yield match
-class DAVPrincipalResource (SuperDAVPrincipalResource, LoggingMixIn):
+class DAVPrincipalResource (DAVPropertyHandlerMixin, DAVFastACLMixin, SuperDAVPrincipalResource, LoggingMixIn):
"""
Extended L{twisted.web2.dav.static.DAVFile} implementation.
"""
@@ -444,10 +908,15 @@
return davxml.ResourceType(davxml.Principal())
-class DAVFile (SudoSACLMixin, SuperDAVFile, LoggingMixIn):
+class DAVFile (SudoSACLMixin, DAVPropertyHandlerMixin, DAVFastACLMixin, SuperDAVFile, LoggingMixIn):
"""
Extended L{twisted.web2.dav.static.DAVFile} implementation.
"""
+ def deadProperties(self):
+ if not hasattr(self, "_dead_properties"):
+ self._dead_properties = sqlPropertyStore(self)
+ return self._dead_properties
+
def readProperty(self, property, request):
if type(property) is tuple:
qname = property
@@ -460,7 +929,7 @@
return super(DAVFile, self).readProperty(property, request)
def resourceType(self):
- # Allow live property to be overriden by dead property
+ # Allow live property to be overridden by dead property
if self.deadProperties().contains((dav_namespace, "resourcetype")):
return self.deadProperties().get((dav_namespace, "resourcetype"))
if self.isCollection():
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/method/put_common.py 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/method/put_common.py 2008-06-17 14:27:41 UTC (rev 2569)
@@ -523,17 +523,31 @@
yield response
response = response.getResult()
+ # Reset dead properties
+ if overwrite:
+ destination.update()
+ else:
+ destination.create()
+
+ # Copy dead properties first, before adding overridden values
+ if source is not None:
+ properties = source.deadProperties().getAll()
+ destination.deadProperties().setSeveral([p for p in properties.itervalues()])
+
# Update the MD5 value on the resource
if source is not None:
# Copy MD5 value from source to destination
if source.hasDeadProperty(TwistedGETContentMD5):
md5 = source.readDeadProperty(TwistedGETContentMD5)
destination.writeDeadProperty(md5)
+ etag = source.readDeadProperty(davxml.GETETag)
+ destination.writeDeadProperty(etag)
else:
# Finish MD5 calc and write dead property
md5.close()
md5 = md5.getMD5()
destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+ destination.writeDeadProperty(davxml.GETETag.fromString(md5))
# Update calendar-access property value on the resource
if access:
@@ -596,6 +610,9 @@
# Delete the source resource
delete(source_uri, source.fp, "0")
rollback.source_deleted = True
+ # Explicitly delete properties - no one else does it
+ source.deadProperties().deleteAll()
+
log.debug("Source removed %s" % (source.fp.path,))
def doSourceIndexRecover():
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/root.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/root.py 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/root.py 2008-06-17 14:27:41 UTC (rev 2569)
@@ -31,6 +31,7 @@
from twistedcaldav.log import Logger
from twistedcaldav.static import CalendarHomeFile
from twistedcaldav.directory.principal import DirectoryPrincipalResource
+from twistedcaldav.sqlprops import sqlPropertyStore
log = Logger()
@@ -70,7 +71,8 @@
def deadProperties(self):
if not hasattr(self, '_dead_properties'):
- self._dead_properties = CachingXattrPropertyStore(self)
+ self._dead_properties = sqlPropertyStore(self)
+# self._dead_properties = CachingXattrPropertyStore(self)
return self._dead_properties
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/sql.py 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/sql.py 2008-06-17 14:27:41 UTC (rev 2569)
@@ -40,9 +40,9 @@
A generic SQL database.
"""
- def __init__(self, dbpath, persistent, autocommit=False):
+ def __init__(self, dbpath, persistent, autocommit=False, utf8=False):
"""
-
+
@param dbpath: the path where the db file is stored.
@type dbpath: str
@param persistent: C{True} if the data in the DB must be perserved during upgrades,
@@ -50,10 +50,14 @@
@type persistent: bool
@param autocommit: C{True} if auto-commit mode is desired, C{False} otherwise
@type autocommit: bool
+ @param utf8: C{True} if utf8 encoded C{str} should be returned for SQl TEXT data,
+ C{False} if C{unicode} should be returned.
+ @type utf8: bool
"""
self.dbpath = dbpath
self.persistent = persistent
self.autocommit = autocommit
+ self.utf8 = utf8
def _db_version(self):
"""
@@ -82,6 +86,8 @@
except:
log.err("Unable to open database: %s" % (db_filename,))
raise
+ if self.utf8:
+ self._db_connection.text_factory = str
#
# Set up the schema
Copied: CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/sqlprops.py (from rev 2558, CalendarServer/branches/users/cdaboo/sqlprops-1202/twistedcaldav/sqlprops.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/sqlprops.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/sqlprops.py 2008-06-17 14:27:41 UTC (rev 2569)
@@ -0,0 +1,634 @@
+##
+# 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: Wilfredo Sanchez, wsanchez at apple.com
+##
+
+"""
+DAV Property store an sqlite database.
+
+This API is considered private to static.py and is therefore subject to
+change.
+"""
+
+__all__ = ["sqlPropertyStore"]
+
+import cPickle
+import os
+
+from twisted.python import log
+from twisted.web2 import responsecode
+from twisted.web2.http import HTTPError, StatusResponse
+
+from twistedcaldav.sql import AbstractSQLDatabase
+
+DEBUG_LOG = False
+
+class sqlPropertyStore (object):
+ """
+ A dead property store that uses an SQLite database back end.
+ """
+
+ def __init__(self, resource):
+ self.resource = resource
+ if os.path.exists(os.path.dirname(resource.fp.path)):
+ from twistedcaldav.root import RootResource
+ if resource.isCollection() and isinstance(resource, RootResource):
+ self.rname = ""
+ indexpath = resource.fp.path
+ else:
+ self.rname = os.path.basename(resource.fp.path)
+ indexpath = os.path.dirname(resource.fp.path)
+ self.index = SQLPropertiesDatabase(indexpath)
+ if resource.isCollection():
+ self.childindex = SQLPropertiesDatabase(resource.fp.path)
+ else:
+ self.childindex = None
+ else:
+ log.msg("No sqlPropertyStore file for %s" % (os.path.dirname(resource.fp.path),))
+ self.index = None
+ self.childindex = None
+
+ def get(self, qname):
+ """
+ Read property from index.
+
+ @param qname: C{tuple} of property namespace and name.
+ """
+ if not self.index:
+ raise HTTPError(StatusResponse(
+ responsecode.NOT_FOUND,
+ "No such property: {%s}%s" % qname
+ ))
+
+ value = self.index.getOnePropertyValue(self.rname, qname)
+ if not value:
+ raise HTTPError(StatusResponse(
+ responsecode.NOT_FOUND,
+ "No such property: {%s}%s" % qname
+ ))
+
+ return value
+
+ def getSeveral(self, qnames):
+ """
+ Read specific properties from index.
+
+ @param qnames: C{list} of C{tuple} of property namespace and name.
+ @return: a C{dict} containing property name/value.
+ """
+ if not qnames:
+ return None
+
+ if not self.index:
+ raise HTTPError(StatusResponse(
+ responsecode.NOT_FOUND,
+ "No such property: {%s}%s" % qnames[0]
+ ))
+
+ return self.index.getSeveralPropertyValues(self.rname, qnames)
+
+ def getAll(self, nohidden=True):
+ """
+ Read all properties from index.
+
+ @param nohidden: C{True} to not return hidden properties, C{False} otherwise.
+ @return: a C{dict} containing property name/value.
+ """
+ if not self.index:
+ raise HTTPError(StatusResponse(
+ responsecode.NOT_FOUND,
+ "No properties"
+ ))
+
+ return self.index.getAllPropertyValues(self.rname, nohidden)
+
+ def getSeveralResources(self, qnames):
+ """
+ Read specific properties for all child resources from index.
+
+ @param qnames: C{list} of C{tuple} of property namespace and name.
+ @return: a C{dict} with resource name as keys and C{dict} of property name/classes as values
+ """
+ if not qnames:
+ return None
+
+ if not self.childindex:
+ raise HTTPError(StatusResponse(
+ responsecode.NOT_FOUND,
+ "No such property: {%s}%s" % qnames[0]
+ ))
+
+ return self.childindex.getSeveralResourcePropertyValues(qnames)
+
+ def getAllResources(self, nohidden=True):
+ """
+ Read specific properties for all child resources from index.
+
+ @param nohidden: C{True} to not return hidden properties, C{False} otherwise.
+ @return: a C{dict} with resource name as keys and C{dict} of property name/classes as values
+ """
+
+ if not self.childindex:
+ raise HTTPError(StatusResponse(
+ responsecode.NOT_FOUND,
+ "No properties"
+ ))
+
+ return self.childindex.getAllResourcePropertyValues(nohidden)
+
+ def set(self, property):
+ """
+ Write property into index.
+
+ @param property: C{Property} to write
+ """
+
+ if self.index:
+ self.index.setOnePropertyValue(self.rname, property.qname(), property, property.hidden)
+
+ def setSeveral(self, properties):
+ """
+ Write specific properties into index.
+
+ @param properties: C{list} of properties to write
+ """
+
+ if self.index:
+ self.index.setSeveralPropertyValues(self.rname, [(p.qname(), p, p.hidden) for p in properties])
+
+ def delete(self, qname):
+ """
+ Delete property from index.
+
+ DELETE from PROPERTIES where NAME=<<rname>> and PROPNAME=<<pname>>
+
+ @param qname:
+ """
+
+ if self.index:
+ self.index.removeProperty(self.rname, qname)
+
+ def deleteSeveral(self, properties):
+ """
+ Delete property from index.
+
+ DELETE from PROPERTIES where NAME=<<rname>> and PROPNAME=<<pname>> ...
+
+ @param qname:
+ """
+
+ if self.index:
+ self.index.removeSeveralProperties(self.rname, [p.qname() for p in properties])
+
+ def deleteAll(self):
+ """
+ Delete property from index.
+
+ DELETE from PROPERTIES where NAME=<<rname>> and PROPNAME=<<pname>>
+
+ @param qname:
+ """
+
+ if self.index:
+ self.index.removeResource(self.rname)
+
+ def contains(self, qname):
+ if self.index:
+ value = self.index.getOnePropertyValue(self.rname, qname)
+ return value is not None
+ else:
+ return False
+
+ def list(self):
+ """
+ List all property names for this resource.
+
+ SELECT PROPNAME from PROPERTIES where NAME=<<rname>>
+
+ """
+
+ if self.index:
+ return self.index.listProperties(self.rname)
+ else:
+ return ()
+
+ def listAll(self):
+ """
+ List all property names for children of this resource.
+
+ SELECT RESOURCENAME, PROPNAME from PROPERTIES
+
+ """
+
+ if self.childindex:
+ return self.childindex.listAllProperties()
+ else:
+ return {}
+
+ def search(self, qname, text):
+ """
+ Search a specific property.
+
+ @param qname: C{tuple} of property namespace and name.
+ @param text: C{str} containing the text to search for.
+ @return: a C{dict} with a C{str} key for the resource name, and a C{set} of
+ property name C{tuple}'s as values, for each matching property.
+ """
+ if self.index:
+ return self.index.searchOneProperty(qname, text)
+ else:
+ return {}
+
+ def searchSeveral(self, qnames, text):
+ """
+ Search several properties.
+
+ @param qnames: a C{list} of C{tuple}'s of property namespace and name.
+ @param text: C{str} containing the text to search for.
+ @return: a C{dict} with a C{str} key for the resource name, and a C{set} of
+ property name C{tuple}'s as values, for each matching property.
+ """
+ if self.index:
+ return self.index.searchSeveralProperties(qnames, text)
+ else:
+ return {}
+
+ def searchAll(self, text):
+ """
+ Search all properties.
+
+ @param text: C{str} containing the text to search for.
+ @return: a C{dict} with a C{str} key for the resource name, and a C{set} of
+ property name C{tuple}'s as values, for each matching property.
+ """
+ if self.index:
+ return self.index.searchAllProperties(text)
+ else:
+ return {}
+
+
+class SQLPropertiesDatabase(AbstractSQLDatabase):
+ """
+ A database to maintain calendar user proxy group memberships.
+
+ SCHEMA:
+
+ Properties Database:
+
+ ROW: RESOURCENAME, PROPERTYNAME, PROPERTYOBJECT, PROPERTYVALUE
+
+ """
+
+ dbType = "SQLPROPERTIES"
+ dbFilename = ".db.sqlproperties"
+ dbFormatVersion = "1"
+
+ def _encode(cls, name):
+ return "{%s}%s" % name
+
+ def _decode(cls, name):
+ index = name.find("}")
+
+ if (index is -1 or not len(name) > index or not name[0] == "{"):
+ raise ValueError("Invalid encoded name: %r" % (name,))
+
+ return (name[1:index], name[index+1:])
+
+ _encode = classmethod(_encode)
+ _decode = classmethod(_decode)
+
+ def __init__(self, path):
+ path = os.path.join(path, SQLPropertiesDatabase.dbFilename)
+ super(SQLPropertiesDatabase, self).__init__(path, True, utf8=True)
+
+ def setOnePropertyValue(self, rname, pname, pvalue, hidden):
+ """
+ Add a property.
+
+ @param rname: a C{str} containing the resource name.
+ @param pname: a C{str} containing the name of the property to set.
+ @param hidden: C{True} for a hidden property, C{False} otherwise.
+ @param pvalue: a C{str} containing the property value to set.
+ """
+
+ # Remove what is there, then add it back.
+ self._delete_from_db(rname, self._encode(pname))
+ self._add_to_db(rname, self._encode(pname), cPickle.dumps(pvalue), pvalue.toxml(), hidden)
+ self._db_commit()
+
+ def setSeveralPropertyValues(self, rname, properties):
+ """
+ Add a set of properties.
+
+ @param rname: a C{str} containing the resource name.
+ @param pname: a C{str} containing the name of the property to set.
+ @param pvalue: a C{str} containing the property value to set.
+ """
+
+ # Remove what is there, then add it back.
+ for p in properties:
+ self._delete_from_db(rname, self._encode(p[0]))
+ self._add_to_db(rname, self._encode(p[0]), cPickle.dumps(p[1]), p[1].toxml(), p[2])
+ self._db_commit()
+
+ def getOnePropertyValue(self, rname, pname):
+ """
+ Get a property.
+
+ @param rname: a C{str} containing the resource name.
+ @param pname: a C{str} containing the name of the property to get.
+ @return: a C{str} containing the property value.
+ """
+
+ # Remove what is there, then add it back.
+ if DEBUG_LOG:
+ log.msg("getPropertyValue: %s \"%s\" \"%s\"" % (self.dbpath, rname, pname))
+ members = []
+ for row in self._db_execute("select PROPERTYOBJECT from PROPERTIES where RESOURCENAME = :1 and PROPERTYNAME = :2", rname, self._encode(pname)):
+ members.append(row[0])
+ setlength = len(members)
+ if setlength == 0:
+ return None
+ elif setlength == 1:
+ return cPickle.loads(members[0])
+ else:
+ raise ValueError("Multiple properties of the same name \"%s\" stored for resource \"%s\"" % (pname, rname,))
+
+ def getSeveralPropertyValues(self, rname, pnames):
+ """
+ Get specified property values from specific resource.
+
+ @param rname: a C{str} containing the resource name.
+ @param pnames: a C{list} of C{str} containing the name of the properties to get.
+ @return: a C{dict} containing property name/value.
+ """
+
+ # Remove what is there, then add it back.
+ if DEBUG_LOG:
+ log.msg("getSeveralPropertyValues: %s \"%s\"" % (self.dbpath, pnames))
+ properties = {}
+ statement = "select PROPERTYNAME, PROPERTYOBJECT from PROPERTIES where RESOURCENAME = :1 and PROPERTYNAME in ("
+ args = [rname]
+ for i, pname in enumerate(pnames):
+ if i != 0:
+ statement += ", "
+ statement += ":%s" % (i + 2,)
+ args.append(self._encode(pname))
+ statement += ")"
+
+ for row in self._db_execute(statement, *args):
+ properties[self._decode(row[0])] = cPickle.loads(row[1])
+
+ return properties
+
+ def getAllPropertyValues(self, rname, nohidden):
+ """
+ Get specified property values from specific resource.
+
+ @param rname: a C{str} containing the resource name.
+ @param nohidden: C{True} to not return hidden properties, C{False} otherwise.
+ @return: a C{dict} containing property name/value.
+ """
+
+ properties = {}
+ if nohidden:
+ statement = "select PROPERTYNAME, PROPERTYOBJECT from PROPERTIES where RESOURCENAME = :1 and HIDDEN = 'F'"
+ else:
+ statement = "select PROPERTYNAME, PROPERTYOBJECT from PROPERTIES where RESOURCENAME = :1"
+ for row in self._db_execute(statement, rname):
+ properties[self._decode(row[0])] = cPickle.loads(row[1])
+
+ return properties
+
+ def getSeveralResourcePropertyValues(self, pnames):
+ """
+ Get specified property values from all resources.
+
+ @param pnames: a C{list} of C{str} containing the name of the properties to get.
+ @return: a C{dict} containing C{str} keys (names of child resources) and C{dict} values of property name/value.
+ """
+
+ # Remove what is there, then add it back.
+ if DEBUG_LOG:
+ log.msg("getAllPropertyValues: %s \"%s\"" % (self.dbpath, pnames))
+ members = {}
+ statement = "select RESOURCENAME, PROPERTYNAME, PROPERTYOBJECT from PROPERTIES where PROPERTYNAME in ("
+ args = []
+ for i, pname in enumerate(pnames):
+ if i != 0:
+ statement += ", "
+ statement += ":%s" % (i + 1,)
+ args.append(self._encode(pname))
+ statement += ")"
+
+ for row in self._db_execute(statement, *args):
+ members.setdefault(row[0], {})[self._decode(row[1])] = cPickle.loads(row[2])
+
+ return members
+
+ def getAllResourcePropertyValues(self, nohidden):
+ """
+ Get specified property values from all resources.
+
+ @param nohidden: C{True} to not return hidden properties, C{False} otherwise.
+ @return: a C{dict} containing C{str} keys (names of child resources) and C{dict} values of property name/value.
+ """
+
+ members = {}
+ if nohidden:
+ statement = "select RESOURCENAME, PROPERTYNAME, PROPERTYOBJECT from PROPERTIES where HIDDEN='F'"
+ else:
+ statement = "select RESOURCENAME, PROPERTYNAME, PROPERTYOBJECT from PROPERTIES"
+ for row in self._db_execute(statement):
+ members.setdefault(row[0], {})[self._decode(row[1])] = cPickle.loads(row[2])
+
+ return members
+
+ def removeProperty(self, rname, pname):
+ """
+ Remove a property.
+
+ @param rname: a C{str} containing the resource name.
+ @param pname: a C{str} containing the name of the property to remove.
+ @return: a C{str} containing the property value.
+ """
+
+ self._delete_from_db(rname, self._encode(pname))
+ self._db_commit()
+
+ def removeSeveralProperties(self, rname, pnames):
+ """
+ Remove specified properties.
+
+ @param rname: a C{str} containing the resource name.
+ @param pnames: a C{list} containing the names of the properties to remove.
+ @return: a C{str} containing the property value.
+ """
+
+ for pname in pnames:
+ self._delete_from_db(rname, self._encode(pname))
+ self._db_commit()
+
+ def removeResource(self, rname):
+ """
+ Remove all properties for resource.
+
+ @param rname: a C{str} containing the resource name.
+ @return: a C{str} containing the property value.
+ """
+
+ self._delete_all_from_db(rname)
+ self._db_commit()
+
+ def listProperties(self, rname):
+ """
+ List all properties in resource.
+
+ @param rname: a C{str} containing the resource name.
+ @return: a C{set} containing the property names.
+ """
+
+ members = set()
+ for row in self._db_execute("select PROPERTYNAME from PROPERTIES where RESOURCENAME = :1", rname):
+ members.add(self._decode(row[0]))
+ return members
+
+ def listAllProperties(self):
+ """
+ List all properties in child resources.
+
+ @param rname: a C{str} containing the resource name.
+ @return: a C{dict} containing the resource names and property names.
+ """
+
+ results = {}
+ for row in self._db_execute("select RESOURCENAME, PROPERTYNAME from PROPERTIES"):
+ results.setdefault(row[0], set()).add(self._decode(row[1]))
+ return results
+
+ def searchOneProperty(self, pname, text):
+ results = {}
+ liketext = "%%%s%%" % (text,)
+ for row in self._db_execute("select RESOURCENAME, PROPERTYNAME from PROPERTIES where PROPERTYNAME = :1 and PROPERTYVALUE like :2", self._encode(pname), liketext):
+ results.setdefault(row[0], set()).add(self._decode(row[1]))
+ return results
+
+ def searchSeveralProperties(self, pnames, text):
+ results = {}
+ liketext = "%%%s%%" % (text,)
+ statement = "select RESOURCENAME, PROPERTYNAME from PROPERTIES where ("
+ args = []
+ count = 1
+ for p in pnames:
+ if count != 1:
+ statement += " or "
+ statement += "PROPERTYNAME = :%s" % (count,)
+ args.append(self._encode(p))
+ count += 1
+ statement += ") and PROPERTYVALUE like :%s" % (count,)
+ args.append(liketext)
+ for row in self._db_execute(statement, *args):
+ results.setdefault(row[0], set()).add(self._decode(row[1]))
+ return results
+
+ def searchAllProperties(self, text):
+ results = {}
+ liketext = "%%%s%%" % (text,)
+ for row in self._db_execute("select RESOURCENAME, PROPERTYNAME from PROPERTIES where PROPERTYVALUE like :1", liketext):
+ results.setdefault(row[0], set()).add(self._decode(row[1]))
+ return results
+
+ def _add_to_db(self, rname, pname, pobject, pvalue, hidden):
+ """
+ Add a property.
+
+ @param rname: a C{str} containing the resource name.
+ @param pname: a C{str} containing the name of the property to set.
+ @param pobject: a C{str} containing the pickled representation of the property object.
+ @param pvalue: a C{str} containing the text of the property value to set.
+ """
+
+ if hidden:
+ hidden_value = "T"
+ else:
+ hidden_value = "F"
+ self._db_execute(
+ """
+ insert into PROPERTIES (RESOURCENAME, PROPERTYNAME, PROPERTYOBJECT, PROPERTYVALUE, HIDDEN)
+ values (:1, :2, :3, :4, :5)
+ """, rname, pname, pobject, pvalue, hidden_value
+ )
+
+ def _delete_all_from_db(self, rname):
+ """
+ Remove a property.
+
+ @param rname: a C{str} containing the resource name.
+ @param pname: a C{str} containing the name of the property to get.
+ @return: a C{str} containing the property value.
+ """
+
+ self._db_execute("delete from PROPERTIES where RESOURCENAME = :1", rname)
+
+ def _delete_from_db(self, rname, pname):
+ """
+ Remove a property.
+
+ @param rname: a C{str} containing the resource name.
+ @param pname: a C{str} containing the name of the property to get.
+ @return: a C{str} containing the property value.
+ """
+
+ self._db_execute("delete from PROPERTIES where RESOURCENAME = :1 and PROPERTYNAME = :2", rname, pname)
+
+ def _db_type(self):
+ """
+ @return: the collection type assigned to this index.
+ """
+ return SQLPropertiesDatabase.dbType
+
+ def _db_version(self):
+ """
+ @return: the schema version assigned to this index.
+ """
+ return SQLPropertiesDatabase.dbFormatVersion
+
+ def _db_init_data_tables(self, q):
+ """
+ Initialise the underlying database tables.
+ @param q: a database cursor to use.
+ """
+
+ #
+ # PROPERTIES table
+ #
+ q.execute(
+ """
+ create table PROPERTIES (
+ RESOURCENAME text,
+ PROPERTYNAME text,
+ PROPERTYOBJECT text,
+ PROPERTYVALUE text,
+ HIDDEN text(1)
+ )
+ """
+ )
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/static.py 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/static.py 2008-06-17 14:27:41 UTC (rev 2569)
@@ -61,6 +61,7 @@
from twistedcaldav.index import Index, IndexSchedule
from twistedcaldav.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
+from twistedcaldav.sqlprops import sqlPropertyStore, SQLPropertiesDatabase
from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource
from twistedcaldav.directory.calendar import uidsResourceName
from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
@@ -87,7 +88,8 @@
def deadProperties(self):
if not hasattr(self, "_dead_properties"):
- self._dead_properties = CachingXattrPropertyStore(self)
+ self._dead_properties = sqlPropertyStore(self)
+ #self._dead_properties = CachingXattrPropertyStore(self)
return self._dead_properties
@@ -162,6 +164,7 @@
if status != responsecode.CREATED:
raise HTTPError(status)
+ self.create()
self.writeDeadProperty(resourceType)
return status
@@ -440,6 +443,8 @@
fp.open("w").close()
fp.restat(False)
+ self.create()
+
return True
class CalendarHomeProvisioningFile (AutoProvisioningFileMixIn, DirectoryCalendarHomeProvisioningResource, DAVFile):
Modified: CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/tap.py 2008-06-17 03:54:13 UTC (rev 2568)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/tap.py 2008-06-17 14:27:41 UTC (rev 2569)
@@ -521,7 +521,7 @@
root.putChild('principals', principalCollection)
root.putChild('calendars', calendarCollection)
- # Timezone service is optional
+ # Timezone service is optional
if config.EnableTimezoneService:
timezoneService = self.timezoneServiceResourceClass(
os.path.join(config.DocumentRoot, "timezones"),
@@ -529,6 +529,8 @@
)
root.putChild('timezones', timezoneService)
+ root.create()
+
# Configure default ACLs on the root resource
log.info("Setting up default ACEs on root resource")
Copied: CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/test/test_sqlprops.py (from rev 2558, CalendarServer/branches/users/cdaboo/sqlprops-1202/twistedcaldav/test/test_sqlprops.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/test/test_sqlprops.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/sqlprops-2557/twistedcaldav/test/test_sqlprops.py 2008-06-17 14:27:41 UTC (rev 2569)
@@ -0,0 +1,496 @@
+##
+# Copyright (c) 2005-2007 Apple 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: Wilfredo Sanchez, wsanchez at apple.com
+##
+from twistedcaldav.ical import Component
+from twisted.web2.dav.element.rfc2518 import DisplayName
+
+import os
+import time
+
+from twisted.trial.unittest import SkipTest
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.fileop import put
+from twisted.web2.dav.resource import TwistedGETContentMD5
+from twisted.web2.dav.test.util import serialize
+from twisted.web2.iweb import IResponse
+from twisted.web2.stream import MemoryStream
+from twisted.web2.test.test_server import SimpleRequest
+
+from twistedcaldav import caldavxml, customxml
+from twistedcaldav.root import RootResource
+from twistedcaldav.sqlprops import sqlPropertyStore, SQLPropertiesDatabase
+from twistedcaldav.static import CalDAVFile
+import twistedcaldav.test.util
+
+class SQLProps (twistedcaldav.test.util.TestCase):
+ """
+ SQL properties tests
+ """
+ data_dir = os.path.join(os.path.dirname(__file__), "data")
+
+ props = (
+ davxml.DisplayName.fromString("My Name"),
+ davxml.ACL(
+ davxml.ACE(
+ davxml.Principal(davxml.Authenticated()),
+ davxml.Grant(davxml.Privilege(davxml.Read())),
+ davxml.Protected(),
+ ),
+ ),
+ caldavxml.CalendarDescription.fromString("My Calendar"),
+ customxml.TwistedScheduleAutoRespond(),
+ )
+
+ def _setUpIndex(self):
+ self.collection_name, self.collection_uri = self.mkdtemp("sql")
+ rsrc = CalDAVFile(os.path.join(self.collection_name, "file.ics"))
+ return sqlPropertyStore(rsrc)
+
+ def _setOnePropertyAndTest(self, prop):
+ index = self._setUpIndex()
+ index.set(prop)
+ self.assertTrue(index.contains(prop.qname()),
+ msg="Could not find property %s." % prop)
+ self.assertTrue(index.get(prop.qname()) == prop,
+ msg="Could not get property %s." % prop)
+
+ def _setProperty(self, index, prop):
+ index.set(prop)
+
+ def _testProperty(self, index, prop, description = ""):
+ self.assertTrue(index.contains(prop.qname()),
+ msg="Could not find property %s %s." % (description, prop,))
+ self.assertTrue(index.get(prop.qname()) == prop,
+ msg="Could not get property %s %s." % (description, prop,))
+
+ def _testPropertyList(self, proplist):
+ self.assertTrue(len(proplist) == len(SQLProps.props),
+ msg="Number of properties returned %s not equal to number queried %s." % (len(proplist), len(SQLProps.props),))
+ for prop in SQLProps.props:
+ for k, v in proplist.iteritems():
+ if prop == v:
+ del proplist[k]
+ break
+ self.assertTrue(len(proplist) == 0,
+ msg="Incorrect properties returned %s." % proplist)
+
+ def _testResourcePropertyList(self, num_resources, resourcedict):
+ self.assertTrue(len(resourcedict) == num_resources,
+ msg="Number of resources returned %s not equal to number queried %s." % (len(resourcedict), num_resources,))
+ for i in xrange(num_resources):
+ fname = "file%04s.ics" % (i,)
+ self.assertTrue(resourcedict.has_key(fname),
+ msg="Resource %s not returned in query results" % (fname,))
+ self._testPropertyList(resourcedict[fname])
+
+ def _setupMultipleResources(self, number):
+ self.collection_name, self.collection_uri = self.mkdtemp("sql")
+ for i in xrange(number):
+ rsrc = CalDAVFile(os.path.join(self.collection_name, "file%04s.ics" % (i,)))
+ index = sqlPropertyStore(rsrc)
+ index.setSeveral(SQLProps.props)
+ return index
+
+ def _setupMultipleDifferentResources(self, number):
+ self.collection_name, self.collection_uri = self.mkdtemp("sql")
+ for i in xrange(number):
+ rsrc = CalDAVFile(os.path.join(self.collection_name, "file%04s.ics" % (i,)))
+ index = sqlPropertyStore(rsrc)
+ props = (
+ davxml.DisplayName.fromString("My Name %s" % (i,)),
+ davxml.ACL(
+ davxml.ACE(
+ davxml.Principal(davxml.HRef.fromString("/principals/users/user%s" % (i + 30,))),
+ davxml.Grant(davxml.Privilege(davxml.Read())),
+ davxml.Protected(),
+ ),
+ ),
+ caldavxml.CalendarDescription.fromString("My Calendar %s" % (i + 50,)),
+ customxml.TwistedScheduleAutoRespond(),
+ )
+ index.setSeveral(props)
+ return index
+
+ def test_db_init_directory(self):
+ self.collection_name, self.collection_uri = self.mkdtemp("sql")
+ rsrc = CalDAVFile(self.collection_name)
+ index = sqlPropertyStore(rsrc)
+ db = index.index._db()
+ self.assertTrue(os.path.exists(os.path.join(os.path.dirname(self.collection_name), SQLPropertiesDatabase.dbFilename)),
+ msg="Could not initialize index via collection resource.")
+
+ def test_db_init_root(self):
+ self.collection_name, self.collection_uri = self.mkdtemp("sql")
+ rsrc = RootResource(self.collection_name)
+ index = sqlPropertyStore(rsrc)
+ db = index.index._db()
+ self.assertTrue(os.path.exists(os.path.join(self.collection_name, SQLPropertiesDatabase.dbFilename)),
+ msg="Could not initialize index via collection resource.")
+
+ def test_db_init_file(self):
+ index = self._setUpIndex()
+ db = index.index._db()
+ self.assertTrue(os.path.exists(os.path.join(self.collection_name, SQLPropertiesDatabase.dbFilename)),
+ msg="Could not initialize index via file resource.")
+
+ def test_setoneproperty(self):
+ for prop in SQLProps.props:
+ self._setOnePropertyAndTest(prop)
+
+ def test_setunknownproperty(self):
+ doc = davxml.WebDAVDocument.fromString("""<?xml version="1.0" encoding="utf-8" ?><guess/>""")
+ self._setOnePropertyAndTest(doc.root_element)
+
+ def test_setmultipleproperties(self):
+ index = self._setUpIndex()
+ for prop in SQLProps.props:
+ self._setProperty(index, prop)
+ for prop in SQLProps.props:
+ self._testProperty(index, prop)
+ proplist = set(index.list())
+ expected_proplist = set([prop.qname() for prop in SQLProps.props])
+ self.assertTrue(proplist == expected_proplist,
+ msg="Property lists do not match: %s != %s." % (proplist, expected_proplist))
+
+ def test_setallproperties(self):
+ index = self._setUpIndex()
+ index.setSeveral(SQLProps.props)
+ for prop in SQLProps.props:
+ self._testProperty(index, prop)
+ proplist = set(index.list())
+ expected_proplist = set([prop.qname() for prop in SQLProps.props])
+ self.assertTrue(proplist == expected_proplist,
+ msg="Property lists do not match: %s != %s." % (proplist, expected_proplist))
+
+ def test_deleteproperties(self):
+ index = self._setUpIndex()
+ for prop in SQLProps.props:
+ self._setProperty(index, prop)
+
+ remaining_props = [p for p in SQLProps.props]
+ for prop in SQLProps.props:
+ remaining_props.pop(0)
+ index.delete(prop.qname())
+ proplist = set(index.list())
+ expected_proplist = set([prop.qname() for prop in remaining_props])
+ self.assertTrue(proplist == expected_proplist,
+ msg="Property lists do not match: %s != %s." % (proplist, expected_proplist))
+
+ def test_deleteseveralproperties(self):
+ index = self._setUpIndex()
+ for prop in SQLProps.props:
+ self._setProperty(index, prop)
+
+ delete_props = SQLProps.props[:2]
+ remaining_props = SQLProps.props[2:]
+ index.deleteSeveral(delete_props)
+ proplist = set(index.list())
+ expected_proplist = set([prop.qname() for prop in remaining_props])
+ self.assertTrue(proplist == expected_proplist,
+ msg="Property lists do not match: %s != %s." % (proplist, expected_proplist))
+
+ def test_deleteallproperties(self):
+ index = self._setUpIndex()
+ for prop in SQLProps.props:
+ self._setProperty(index, prop)
+
+ index.deleteAll()
+ for prop in SQLProps.props:
+ self.assertFalse(index.contains(prop.qname()),
+ msg="Could not find property %s." % prop)
+
+ def test_getallproperties(self):
+ index = self._setUpIndex()
+ for prop in SQLProps.props:
+ self._setProperty(index, prop)
+
+ result = index.getSeveral([p.qname() for p in SQLProps.props])
+ self._testPropertyList(result)
+
+ def test_getallresourceproperties(self):
+ num_resources = 10
+ index = self._setupMultipleResources(num_resources)
+ result = index.getSeveralResources([p.qname() for p in SQLProps.props])
+ self._testResourcePropertyList(num_resources, result)
+
+# def test_timegetallresourceproperties(self):
+# num_resources = 1000
+# index = self._setupMultipleResources(num_resources)
+# t1 = time.time()
+# result = index.getSeveralResources([p.qname() for p in SQLProps.props])
+# t2 = time.time()
+# self.assertTrue(t1 == t2,
+# msg="Time for 1000 prop query = %s" % (t2 - t1,))
+#
+# self._testResourcePropertyList(num_resources, result)
+
+ def _do_delete(self, parent):
+ fpath = self.docroot
+ if parent:
+ fpath = os.path.join(fpath, parent)
+ fpath = os.path.join(fpath, "file.ics")
+ rsrc = CalDAVFile(fpath)
+ ms = MemoryStream("Some Data")
+
+ def donePut(status):
+ self.assertTrue(status == responsecode.CREATED)
+ md5 = TwistedGETContentMD5.fromString("MD5")
+ rsrc.writeDeadProperty(md5)
+
+ # Check index
+ index = sqlPropertyStore(rsrc)
+ self._testProperty(index, md5)
+
+ def doneDelete(response):
+ response = IResponse(response)
+
+ if response.code != responsecode.NO_CONTENT:
+ self.fail("DELETE response %s != %s" % (response.code, responsecode.NO_CONTENT))
+
+ if os.path.exists(fpath):
+ self.fail("DELETE did not remove path %s" % (fpath,))
+
+ self.assertFalse(index.contains(md5.qname()),
+ msg="Property %s exists after resource was deleted." % md5)
+
+ def work():
+ # Delete resource and test
+ request = SimpleRequest(self.site, "DELETE", "/%sfile.ics" % (parent,))
+ yield (request, doneDelete)
+
+ return serialize(self.send, work())
+
+ d = put(ms, rsrc.fp)
+ d.addCallback(donePut)
+ return d
+
+ def test_deleteresource(self):
+ return self._do_delete("")
+
+ def test_deletecalendarresource(self):
+
+ def doneMake(response):
+ self.assertTrue(response.code == responsecode.CREATED)
+ return self._do_delete("calendar/")
+
+ # Make a calendar
+ request = SimpleRequest(self.site, "MKCALENDAR", "/calendar/")
+ return self.send(request, doneMake)
+
+ event = """BEGIN:VCALENDAR
+PRODID:Test Case
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+DTSTAMP:20070219T120000Z
+DTSTART:20070219T120000Z
+DTEND:20070219T130000Z
+UID:12345-67890-54321
+END:VEVENT
+END:VCALENDAR
+"""
+
+ def _do_copy(self, src, dst):
+ fpath = self.docroot
+ if src:
+ fpath = os.path.join(fpath, src)
+ fpath = os.path.join(fpath, "file.ics")
+ fpath_new = self.docroot
+ if dst:
+ fpath_new = os.path.join(fpath_new, dst)
+ fpath_new = os.path.join(fpath_new, "copy.ics")
+ rsrc = CalDAVFile(fpath)
+
+ def donePut(response):
+ self.assertTrue(response.code == responsecode.CREATED)
+ displayname = DisplayName.fromString("adisplayname")
+ rsrc.writeDeadProperty(displayname)
+
+ # Check index
+ index = sqlPropertyStore(rsrc)
+ self._testProperty(index, displayname)
+
+ def doneCopy(response):
+ response = IResponse(response)
+
+ if response.code != responsecode.CREATED:
+ self.fail("COPY response %s != %s" % (response.code, responsecode.NO_CONTENT))
+
+ if not os.path.exists(fpath):
+ self.fail("COPY removed original path %s" % (fpath,))
+
+ if not os.path.exists(fpath_new):
+ self.fail("COPY did not create new path %s" % (fpath_new,))
+
+ self._testProperty(index, displayname, "on original resource")
+
+ rsrc_new = CalDAVFile(fpath_new)
+ index_new = sqlPropertyStore(rsrc_new)
+ self._testProperty(index_new, displayname, "on new resource")
+
+ # Copy resource and test
+ request = SimpleRequest(self.site, "COPY", "/%sfile.ics" % (src,))
+ request.headers.setHeader("destination", "/%scopy.ics" % (dst,))
+ return self.send(request, doneCopy)
+
+ stream = file(os.path.join(self.data_dir, "Holidays", "C318AA54-1ED0-11D9-A5E0-000A958A3252.ics"))
+ try: calendar = str(Component.fromStream(stream))
+ finally: stream.close()
+
+ request = SimpleRequest(self.site, "PUT", "/%sfile.ics" % (src,))
+ request.stream = MemoryStream(calendar)
+ return self.send(request, donePut)
+
+ def test_copyresource(self):
+ return self._do_copy("", "")
+
+ def test_copycalendarresource(self):
+
+ def doneMake2(response):
+ self.assertTrue(response.code == responsecode.CREATED)
+ return self._do_copy("calendar1/", "calendar2/")
+
+ def doneMake1(response):
+ self.assertTrue(response.code == responsecode.CREATED)
+ request = SimpleRequest(self.site, "MKCALENDAR", "/calendar2/")
+ return self.send(request, doneMake2)
+
+ # Make a calendar
+ request = SimpleRequest(self.site, "MKCALENDAR", "/calendar1/")
+ return self.send(request, doneMake1)
+
+ def _do_move(self, src, dst):
+ fpath = self.docroot
+ if src:
+ fpath = os.path.join(fpath, src)
+ fpath = os.path.join(fpath, "file.ics")
+ fpath_new = self.docroot
+ if dst:
+ fpath_new = os.path.join(fpath_new, dst)
+ fpath_new = os.path.join(fpath_new, "move.ics")
+ rsrc = CalDAVFile(fpath)
+
+ def donePut(response):
+ self.assertTrue(response.code == responsecode.CREATED)
+ displayname = DisplayName.fromString("adisplayname")
+ rsrc.writeDeadProperty(displayname)
+
+ # Check index
+ index = sqlPropertyStore(rsrc)
+ self._testProperty(index, displayname)
+
+ def doneMove(response):
+ response = IResponse(response)
+
+ if response.code != responsecode.CREATED:
+ self.fail("MOVE response %s != %s" % (response.code, responsecode.NO_CONTENT))
+
+ if os.path.exists(fpath):
+ self.fail("MOVE did not remove original path %s" % (fpath,))
+
+ if not os.path.exists(fpath_new):
+ self.fail("MOVE did not create new path %s" % (fpath_new,))
+
+ self.assertFalse(index.contains(displayname.qname()),
+ msg="Property %s exists after resource was moved." % displayname)
+
+ rsrc_new = CalDAVFile(fpath_new)
+ index_new = sqlPropertyStore(rsrc_new)
+ self._testProperty(index_new, displayname, "on new resource")
+
+ def work():
+ # Move resource and test
+ request = SimpleRequest(self.site, "MOVE", "/%sfile.ics" % (src,))
+ request.headers.setHeader("destination", "/%smove.ics" % (dst,))
+ yield (request, doneMove)
+
+ return serialize(self.send, work())
+
+ stream = file(os.path.join(self.data_dir, "Holidays", "C318AA54-1ED0-11D9-A5E0-000A958A3252.ics"))
+ try: calendar = str(Component.fromStream(stream))
+ finally: stream.close()
+
+ request = SimpleRequest(self.site, "PUT", "/%sfile.ics" % (src,))
+ request.stream = MemoryStream(calendar)
+ return self.send(request, donePut)
+
+ def test_moveresource(self):
+ return self._do_move("", "")
+
+ def test_movecalendarresource(self):
+
+ def doneMake2(response):
+ self.assertTrue(response.code == responsecode.CREATED)
+ return self._do_move("calendar1/", "calendar2/")
+
+ def doneMake1(response):
+ self.assertTrue(response.code == responsecode.CREATED)
+ request = SimpleRequest(self.site, "MKCALENDAR", "/calendar2/")
+ return self.send(request, doneMake2)
+
+ # Make a calendar
+ request = SimpleRequest(self.site, "MKCALENDAR", "/calendar1/")
+ return self.send(request, doneMake1)
+
+ def test_searchone(self):
+ index = self._setupMultipleDifferentResources(20)
+ results = index.search(davxml.DisplayName.qname(), "2")
+ expected = {
+ "file 2.ics" : set((davxml.DisplayName.qname(),)),
+ "file 12.ics" : set((davxml.DisplayName.qname(),)),
+ }
+ self.assertTrue(results == expected,
+ msg="Search results %s != %s" % (results, expected,))
+
+ def test_searchseveral(self):
+ index = self._setupMultipleDifferentResources(20)
+ results = index.searchSeveral((davxml.DisplayName.qname(), davxml.ACL.qname()), "3")
+ expected = {
+ "file 0.ics" : set((davxml.ACL.qname(),)),
+ "file 1.ics" : set((davxml.ACL.qname(),)),
+ "file 2.ics" : set((davxml.ACL.qname(),)),
+ "file 3.ics" : set((davxml.DisplayName.qname(), davxml.ACL.qname(),)),
+ "file 4.ics" : set((davxml.ACL.qname(),)),
+ "file 5.ics" : set((davxml.ACL.qname(),)),
+ "file 6.ics" : set((davxml.ACL.qname(),)),
+ "file 7.ics" : set((davxml.ACL.qname(),)),
+ "file 8.ics" : set((davxml.ACL.qname(),)),
+ "file 9.ics" : set((davxml.ACL.qname(),)),
+ "file 13.ics" : set((davxml.DisplayName.qname(), davxml.ACL.qname(),)),
+ }
+ self.assertTrue(results == expected,
+ msg="Search results %s != %s" % (results, expected,))
+
+ def test_searchall(self):
+ index = self._setupMultipleDifferentResources(20)
+ results = index.searchAll("5")
+ expected = {
+ "file 0.ics" : set((caldavxml.CalendarDescription.qname(),)),
+ "file 1.ics" : set((caldavxml.CalendarDescription.qname(),)),
+ "file 2.ics" : set((caldavxml.CalendarDescription.qname(),)),
+ "file 3.ics" : set((caldavxml.CalendarDescription.qname(),)),
+ "file 4.ics" : set((caldavxml.CalendarDescription.qname(),)),
+ "file 5.ics" : set((davxml.DisplayName.qname(), caldavxml.CalendarDescription.qname(), davxml.ACL.qname(),)),
+ "file 6.ics" : set((caldavxml.CalendarDescription.qname(),)),
+ "file 7.ics" : set((caldavxml.CalendarDescription.qname(),)),
+ "file 8.ics" : set((caldavxml.CalendarDescription.qname(),)),
+ "file 9.ics" : set((caldavxml.CalendarDescription.qname(),)),
+ "file 15.ics" : set((davxml.DisplayName.qname(), caldavxml.CalendarDescription.qname(), davxml.ACL.qname(),)),
+ }
+ self.assertTrue(results == expected,
+ msg="Search results %s != %s" % (results, expected,))
+ pass
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080617/43d98fce/attachment-0001.htm
More information about the calendarserver-changes
mailing list