[CalendarServer-changes] [6074] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Wed Aug 11 19:36:29 PDT 2010
Revision: 6074
http://trac.macosforge.org/projects/calendarserver/changeset/6074
Author: glyph at apple.com
Date: 2010-08-11 19:36:27 -0700 (Wed, 11 Aug 2010)
Log Message:
-----------
merge sql-store branch
Modified Paths:
--------------
CalendarServer/trunk/calendarserver/tap/caldav.py
CalendarServer/trunk/calendarserver/tap/util.py
CalendarServer/trunk/conf/caldavd-test.plist
CalendarServer/trunk/support/build.sh
CalendarServer/trunk/twext/web2/dav/test/util.py
CalendarServer/trunk/twistedcaldav/resource.py
CalendarServer/trunk/twistedcaldav/sharing.py
CalendarServer/trunk/twistedcaldav/stdconfig.py
CalendarServer/trunk/twistedcaldav/storebridge.py
CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
CalendarServer/trunk/twistedcaldav/test/test_freebusyquery.py
CalendarServer/trunk/twistedcaldav/test/test_mkcalendar.py
CalendarServer/trunk/twistedcaldav/test/test_multiget.py
CalendarServer/trunk/twistedcaldav/test/test_sharing.py
CalendarServer/trunk/twistedcaldav/test/test_wrapping.py
CalendarServer/trunk/twistedcaldav/test/util.py
CalendarServer/trunk/txcaldav/calendarstore/file.py
CalendarServer/trunk/txcaldav/calendarstore/postgres.py
CalendarServer/trunk/txcaldav/calendarstore/postgres_schema_v1.sql
CalendarServer/trunk/txcaldav/calendarstore/scheduling.py
CalendarServer/trunk/txcaldav/calendarstore/test/common.py
CalendarServer/trunk/txcaldav/calendarstore/test/test_file.py
CalendarServer/trunk/txcaldav/icalendarstore.py
CalendarServer/trunk/txcarddav/addressbookstore/file.py
CalendarServer/trunk/txcarddav/addressbookstore/test/common.py
CalendarServer/trunk/txcarddav/iaddressbookstore.py
CalendarServer/trunk/txdav/common/datastore/file.py
CalendarServer/trunk/txdav/common/inotifications.py
CalendarServer/trunk/txdav/idav.py
Added Paths:
-----------
CalendarServer/trunk/txcaldav/calendarstore/test/test_postgres.py
CalendarServer/trunk/txcaldav/calendarstore/util.py
CalendarServer/trunk/txdav/datastore/subpostgres.py
CalendarServer/trunk/txdav/datastore/test/
CalendarServer/trunk/txdav/datastore/test/__init__.py
CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py
Removed Paths:
-------------
CalendarServer/trunk/txdav/datastore/test/__init__.py
CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py
Property Changed:
----------------
CalendarServer/trunk/
Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
- /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
+ /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -28,7 +28,7 @@
from time import time
from subprocess import Popen, PIPE
-from pwd import getpwuid
+from pwd import getpwuid, getpwnam
from grp import getgrnam
from OpenSSL.SSL import Error as SSLError
import OpenSSL
@@ -88,6 +88,10 @@
from calendarserver.tap.util import getRootResource, computeProcessCount
from calendarserver.tools.util import checkDirectory
+from txcaldav.calendarstore.postgres import v1_schema
+from txdav.datastore.subpostgres import PostgresService
+from twext.python.filepath import CachingFilePath
+
log = Logger()
@@ -681,6 +685,7 @@
logger = AMPLoggingFactory(
RotatingFileAccessLoggingObserver(config.AccessLogFile)
)
+
if config.GroupName:
try:
gid = getgrnam(config.GroupName).gr_gid
@@ -688,6 +693,15 @@
raise ConfigurationError("Invalid group name: %s" % (config.GroupName,))
else:
gid = os.getgid()
+
+ if config.UserName:
+ try:
+ uid = getpwnam(config.UserName).pw_uid
+ except KeyError, e:
+ raise ConfigurationError("Invalid user name: %s" % (config.UserName,))
+ else:
+ uid = os.getuid()
+
if config.ControlSocket:
loggingService = GroupOwnedUNIXServer(
gid, config.ControlSocket, logger, mode=0660
@@ -700,9 +714,30 @@
loggingService.setServiceParent(s)
monitor = DelayedStartupProcessMonitor()
- monitor.setServiceParent(s)
s.processMonitor = monitor
+ if config.UseDatabase:
+ # Postgres: delay spawning child processes until database is up
+
+ dbRoot = CachingFilePath(config.DatabaseRoot)
+
+ def subServiceFactory(connectionFactory):
+ return monitor
+
+ if os.getuid() == 0: # Only override if root
+ postgresUID = uid
+ postgresGID = gid
+ else:
+ postgresUID = None
+ postgresGID = None
+
+ PostgresService(dbRoot, subServiceFactory, v1_schema,
+ "caldav", logFile=config.PostgresLogFile,
+ uid=postgresUID, gid=postgresGID).setServiceParent(s)
+
+ else:
+ monitor.setServiceParent(s)
+
parentEnv = {
"PATH": os.environ.get("PATH", ""),
"PYTHONPATH": os.environ.get("PYTHONPATH", ""),
Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/calendarserver/tap/util.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -68,7 +68,11 @@
from calendarserver.webcal.resource import WebCalendarResource
from txdav.common.datastore.file import CommonDataStore
+from txcaldav.calendarstore.postgres import PostgresStore, v1_schema
+from txdav.datastore.subpostgres import PostgresService
+from twext.python.filepath import CachingFilePath
+
log = Logger()
@@ -286,11 +290,17 @@
else:
notifierFactory = None
+ if config.UseDatabase:
+ _dbRoot = CachingFilePath(config.DatabaseRoot)
+ _postgresService = PostgresService(_dbRoot, None, v1_schema, "caldav",
+ logFile=config.PostgresLogFile)
+ _newStore = PostgresStore(_postgresService.produceConnection,
+ notifierFactory, # config.EnableCalDAV, config.EnableCardDAV)
+ _dbRoot.child("attachments"))
+ else:
+ _newStore = CommonDataStore(FilePath(config.DocumentRoot),
+ notifierFactory, config.EnableCalDAV, config.EnableCardDAV)
- # Need a data store
- _newStore = CommonDataStore(FilePath(config.DocumentRoot),
- notifierFactory, config.EnableCalDAV, config.EnableCardDAV)
-
if config.EnableCalDAV:
log.info("Setting up calendar collection: %r" % (calendarResourceClass,))
calendarCollection = calendarResourceClass(
Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/conf/caldavd-test.plist 2010-08-12 02:36:27 UTC (rev 6074)
@@ -82,6 +82,10 @@
<key>DataRoot</key>
<string>Data</string>
+ <!-- Database root -->
+ <key>DatabaseRoot</key>
+ <string>Database</string>
+
<!-- Document root -->
<key>DocumentRoot</key>
<string>Documents</string>
Modified: CalendarServer/trunk/support/build.sh
===================================================================
--- CalendarServer/trunk/support/build.sh 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/support/build.sh 2010-08-12 02:36:27 UTC (rev 6074)
@@ -196,17 +196,17 @@
cache_file="${cache_deps}/${name}-$(echo "${url}" | hash)-$(basename "${url}")";
if [ ! -f "${cache_file}" ]; then
- echo "Downloading ${name}...";
- curl -L "${url}" -o "${cache_file}";
+ echo "Downloading ${name}...";
+ curl -L "${url}" -o "${cache_file}";
fi;
if [ -n "${md5}" ] && [ "${hash}" == "md5" ]; then
- echo "Checking MD5 sum for ${name}...";
- local sum="$(hash "${cache_file}" | perl -pe 's|^.*([0-9a-f]{32}).*$|\1|')";
- if [ "${md5}" != "${sum}" ]; then
- echo "ERROR: MD5 sum for cache file ${cache_file} ${sum} != ${md5}. Corrupt file?";
- exit 1;
- fi;
+ echo "Checking MD5 sum for ${name}...";
+ local sum="$(hash "${cache_file}" | perl -pe 's|^.*([0-9a-f]{32}).*$|\1|')";
+ if [ "${md5}" != "${sum}" ]; then
+ echo "ERROR: MD5 sum for cache file ${cache_file} ${sum} != ${md5}. Corrupt file?";
+ exit 1;
+ fi;
fi;
echo "Unpacking ${name} from cache...";
@@ -287,13 +287,13 @@
mkdir -p "${cache_deps}";
if [ -f "${cache_file}" ]; then
- echo "Unpacking ${name} from cache...";
- mkdir -p "${path}";
- tar -C "${path}" -xvzf "${cache_file}";
+ echo "Unpacking ${name} from cache...";
+ mkdir -p "${path}";
+ tar -C "${path}" -xvzf "${cache_file}";
else
- checkout;
- echo "Caching ${name}...";
- tar -C "${path}" -cvzf "${cache_file}" .;
+ checkout;
+ echo "Caching ${name}...";
+ tar -C "${path}" -cvzf "${cache_file}" .;
fi;
else
checkout;
Modified: CalendarServer/trunk/twext/web2/dav/test/util.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/test/util.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twext/web2/dav/test/util.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -200,7 +200,8 @@
return (os.path.abspath(path), uri)
- def send(self, request, callback):
+
+ def send(self, request, callback=None):
log.msg("Sending %s request for URI %s" % (request.method, request.uri))
d = request.locateResource(request.uri)
Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/resource.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -2257,7 +2257,7 @@
def createNotificationsCollection(self):
- txn = self._newStoreHome._transaction
+ txn = self._associatedTransaction
notifications = txn.notificationsWithUID(self._newStoreHome.uid())
from twistedcaldav.storebridge import StoreNotificationCollectionResource
Modified: CalendarServer/trunk/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharing.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/sharing.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -60,13 +60,17 @@
return self._invitesDB
def inviteProperty(self, request):
-
- # Build the CS:invite property from our DB
+ """
+ Calculate the customxml.Invite property (for readProperty) from the
+ invites database.
+ """
+
def sharedOK(isShared):
if config.Sharing.Enabled and isShared:
self.validateInvites()
return customxml.Invite(
- *[record.makePropertyElement() for record in self.invitesDB().allRecords()]
+ *[record.makePropertyElement() for
+ record in self.invitesDB().allRecords()]
)
else:
return None
@@ -339,8 +343,9 @@
def validUserIDForShare(self, userid):
"""
- Test the user id to see if it is a valid identifier for sharing and return a "normalized"
- form for our own use (e.g. convert mailto: to urn:uuid).
+ Test the user id to see if it is a valid identifier for sharing and
+ return a "normalized" form for our own use (e.g. convert mailto: to
+ urn:uuid).
@param userid: the userid to test
@type userid: C{str}
@@ -526,8 +531,10 @@
# Locate notifications collection for user
sharee = self.principalForCalendarUserAddress(record.userid)
if sharee is None:
- raise ValueError("sharee is None but userid was valid before")
- notifications = self._newStoreParentHome._transaction.notificationsWithUID(sharee.principalUID())
+ raise ValueError("sharee is None but userid was valid before")
+ notifications = self._associatedTransaction.notificationsWithUID(
+ sharee.principalUID()
+ )
# Look for existing notification
oldnotification = notifications.notificationObjectWithUID(record.inviteuid)
@@ -576,7 +583,7 @@
def _handleErrorResponse(error):
if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
return error.value.response
- return Response(code=responsecode.BAD_REQUEST)
+ return error
def _handleInvite(invitedoc):
def _handleInviteSet(inviteset):
@@ -935,8 +942,8 @@
class SharedHomeMixin(LinkFollowerMixIn):
"""
- A mix-in for calendar/addressbook homes that defines the operations for manipulating a sharee's
- set of shared calendars.
+ A mix-in for calendar/addressbook homes that defines the operations for
+ manipulating a sharee's set of shared calendars.
"""
def sharesDB(self):
@@ -1053,7 +1060,9 @@
@inlineCallbacks
def _changeShare(self, request, state, hostUrl, replytoUID, displayname=None):
- """ Accept an invite to a shared collection """
+ """
+ Accept or decline an invite to a shared collection.
+ """
# Change state in sharer invite
owner = (yield self.ownerPrincipal(request))
@@ -1074,7 +1083,6 @@
@inlineCallbacks
def sendReply(self, request, sharee, sharedCollection, state, hostUrl, replytoUID, displayname=None):
-
# Locate notifications collection for sharer
sharer = (yield sharedCollection.ownerPrincipal(request))
@@ -1107,7 +1115,7 @@
def _handleErrorResponse(error):
if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
return error.value.response
- return Response(code=responsecode.BAD_REQUEST)
+ return error
def _handleInviteReply(invitereplydoc):
""" Handle a user accepting or declining a sharing invite """
Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -149,6 +149,8 @@
"UseMetaFD": True, # Use a 'meta' FD, i.e. an FD to transmit other
# FDs to slave processes.
+ "UseDatabase" : True, # True: postgress; False: files
+
#
# Types of service provided
#
@@ -160,6 +162,7 @@
#
"ServerRoot" : "/Library/CalendarServer",
"DataRoot" : "Data",
+ "DatabaseRoot" : "Database",
"DocumentRoot" : "Documents",
"ConfigRoot" : "/etc/caldavd",
"LogRoot" : "/var/log/caldavd",
@@ -266,6 +269,7 @@
"ErrorLogEnabled" : True, # True = use log file, False = stdout
"ErrorLogRotateMB" : 10, # Rotate error log after so many megabytes
"ErrorLogMaxRotatedFiles" : 5, # Retain this many error log files
+ "PostgresLogFile" : "postgres.log", # Postgres log
"PIDFile" : "caldavd.pid",
"RotateAccessLog" : False,
"EnableExtendedAccessLog": True,
@@ -627,9 +631,11 @@
("ServerRoot", "ConfigRoot"),
("ServerRoot", "LogRoot"),
("ServerRoot", "RunRoot"),
+ ("DataRoot", "DatabaseRoot"),
("ConfigRoot", "SudoersFile"),
("LogRoot", "AccessLogFile"),
("LogRoot", "ErrorLogFile"),
+ ("LogRoot", "PostgresLogFile"),
("LogRoot", "AccountingLogRoot"),
("RunRoot", "PIDFile"),
("RunRoot", "GlobalStatsSocket"),
Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -660,6 +660,12 @@
self._initializeWithCalendar(calendar, home)
+ def __repr__(self):
+ return "<Calendar Collection Resource %r:%r>" % (
+ self._newStoreCalendar.ownerCalendarHome().uid(),
+ self._newStoreCalendar.name())
+
+
def name(self):
return self._newStoreCalendar.name()
@@ -944,16 +950,18 @@
the given request's resource-lookup mapping, transaction, and re-look-
up my calendar object in a new transaction.
- Return the new transaction so it can be committed.
+ @return: the new transaction so it can be committed.
"""
# FIXME: private names from 'file' implementation; maybe there should
# be a public way to do this? or maybe we should just have a real
# queue.
objectName = self._newStoreObject.name()
- calendarName = self._newStoreObject._calendar.name()
- homeUID = self._newStoreObject._calendar._calendarHome.uid()
- store = self._newStoreObject._transaction.store()
- txn = store.newTransaction("new transaction for " + self._newStoreObject.name())
+ calendar = self._newStoreObject.calendar()
+ calendarName = calendar.name()
+ ownerHome = calendar.ownerCalendarHome()
+ homeUID = ownerHome.uid()
+ txn = ownerHome.transaction().store().newTransaction(
+ "new transaction for " + self._newStoreObject.name())
newObject = (txn.calendarHomeWithUID(homeUID)
.calendarWithName(calendarName)
.calendarObjectWithName(objectName))
@@ -1551,34 +1559,10 @@
self._initializeWithObject(Object)
- def inNewTransaction(self, request):
- """
- Implicit auto-replies need to span multiple transactions. Clean out the
- given request's resource-lookup mapping, transaction, and re-look-up my
- addressbook object in a new transaction.
-
- Return the new transaction so it can be committed.
- """
- # FIXME: private names from 'file' implementation; maybe there should be
- # a public way to do this? or maybe we should just have a real queue.
- objectName = self._newStoreObject.name()
- Name = self._newStoreObject._addressbook.name()
- homeUID = self._newStoreObject._addressbook._addressbookHome.uid()
- store = self._newStoreObject._transaction.store()
- txn = store.newTransaction("new AB transaction for " + self._newStoreObject.name())
- newObject = (txn.addressbookHomeWithUID(homeUID)
- .addressbookWithName(Name)
- .addressbookObjectWithName(objectName))
- request._newStoreTransaction = txn
- request._resourcesByURL.clear()
- request._urlsByResource.clear()
- self._initializeWithObject(newObject)
- return txn
-
-
def isCollection(self):
return False
+
def exists(self):
# FIXME: Tests
return True
Modified: CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -15,24 +15,59 @@
##
import os
-import shutil
from twisted.trial.unittest import SkipTest
+
+from twext.python.filepath import CachingFilePath as FilePath
+
from twext.web2 import responsecode
from twext.web2.iweb import IResponse
from twext.web2.stream import MemoryStream
from twext.web2.dav import davxml
-from twext.web2.dav.fileop import rmdir
from twext.web2.dav.util import davXMLFromStream
from twext.web2.test.test_server import SimpleRequest
from twistedcaldav import caldavxml
from twistedcaldav import ical
-from twistedcaldav.index import db_basename
+
from twistedcaldav.query import calendarqueryfilter
from twistedcaldav.config import config
from twistedcaldav.test.util import HomeTestCase
+from twisted.internet.defer import inlineCallbacks, returnValue
+from txcaldav.calendarstore.test.test_postgres import buildStore
+from txcaldav.calendarstore.test.common import StubNotifierFactory
+
+ at inlineCallbacks
+def addEventsDir(testCase, eventsDir, uri):
+ """
+ Add events to a L{HomeTestCase} from a directory.
+
+ @param testCase: The test case to add events to.
+ @type testCase: L{HomeTestCase}
+
+ @param eventsDir: A directory full of events.
+ @type eventsDir: L{FilePath}
+
+ @param uri: The URI-path of the calendar to insert events into.
+ @type uri: C{str}
+
+ @return: a L{Deferred} which fires with the number of added calendar object
+ resources.
+ """
+ count = 0
+ for child in eventsDir.children():
+ count += 1
+ if child.basename().split(".")[-1] != "ics":
+ continue
+ request = SimpleRequest(testCase.site, "PUT",
+ uri + "/" + child.basename())
+ request.stream = MemoryStream(child.getContent())
+ yield testCase.send(request)
+ returnValue(count)
+
+
+
class CalendarQuery (HomeTestCase):
"""
calendar-query REPORT
@@ -180,7 +215,8 @@
All events.
(CalDAV-access-09, section 7.6.8)
"""
- uids = [r[0] for r in (os.path.splitext(f) for f in os.listdir(self.holidays_dir)) if r[1] == ".ics"]
+ uids = [r[0] for r in (os.path.splitext(f) for f in
+ os.listdir(self.holidays_dir)) if r[1] == ".ics"]
return self.simple_event_query("/calendar_query_events/", None, uids)
@@ -288,41 +324,42 @@
return self.calendar_query(cal_uri, query, got_xml)
+
+ @inlineCallbacks
def calendar_query(self, calendar_uri, query, got_xml):
- calendar_path = os.path.join(self.docroot, calendar_uri[1:])
- if os.path.exists(calendar_path): rmdir(calendar_path)
+ response = yield self.send(SimpleRequest(self.site, "MKCALENDAR", calendar_uri))
+ response = IResponse(response)
- mkrequest = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
+ if response.code != responsecode.CREATED:
+ self.fail("MKCALENDAR failed: %s" % (response.code,))
- def do_report(response):
- response = IResponse(response)
+ # Add holiday events to calendar
+ yield addEventsDir(self, FilePath(self.holidays_dir), calendar_uri)
- if response.code != responsecode.CREATED:
- self.fail("MKCALENDAR failed: %s" % (response.code,))
+ request = SimpleRequest(self.site, "REPORT", calendar_uri)
+ request.stream = MemoryStream(query.toxml())
+ response = yield self.send(request)
- # Add holiday events to calendar
- # We're cheating by simply copying the files in
- for filename in os.listdir(self.holidays_dir):
- if os.path.splitext(filename)[1] != ".ics": continue
- path = os.path.join(self.holidays_dir, filename)
- shutil.copy(path, calendar_path)
+ response = IResponse(response)
- # Delete the index because we cheated
- index_path = os.path.join(calendar_path, db_basename)
- if os.path.isfile(index_path): os.remove(index_path)
+ if response.code != responsecode.MULTI_STATUS:
+ self.fail("REPORT failed: %s" % (response.code,))
- request = SimpleRequest(self.site, "REPORT", calendar_uri)
- request.stream = MemoryStream(query.toxml())
+ returnValue(
+ (yield davXMLFromStream(response.stream).addCallback(got_xml))
+ )
- def do_test(response):
- response = IResponse(response)
- if response.code != responsecode.MULTI_STATUS:
- self.fail("REPORT failed: %s" % (response.code,))
+class DatabaseQueryTests(CalendarQuery):
- return davXMLFromStream(response.stream).addCallback(got_xml)
+ @inlineCallbacks
+ def setUp(self):
+ self.calendarStore = yield buildStore(self, StubNotifierFactory())
+ yield super(DatabaseQueryTests, self).setUp()
- return self.send(request, do_test)
- return self.send(mkrequest, do_report)
+ def createDataStore(self):
+ return self.calendarStore
+
+
Modified: CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -14,7 +14,7 @@
# limitations under the License.
##
-from twisted.internet.defer import DeferredList
+from twisted.internet.defer import inlineCallbacks
from twext.python.filepath import CachingFilePath as FilePath
from twext.web2 import responsecode
from twext.web2.iweb import IResponse
@@ -58,16 +58,14 @@
# Tests in this suite assume that the root resource is a calendar home.
# FIXME: there should be a centralized way of saying 'make this look
# like a calendar home'
- super(CollectionContents, self).setUp()
+ return super(CollectionContents, self).setUp()
def test_collection_in_calendar(self):
"""
Make (regular) collection in calendar
"""
- calendar_path, calendar_uri = self.mkdtemp("collection_in_calendar")
- calPath = FilePath(calendar_path)
- calPath.remove()
+ calendar_uri = "/collection_in_calendar/"
def mkcalendar_cb(response):
response = IResponse(response)
@@ -166,40 +164,33 @@
with the data from given stream and verifies that the response code from the
PUT request matches the given response_code.
"""
- calendar_path, calendar_uri = self.mkdtemp("calendar")
- calPath = FilePath(calendar_path)
- calPath.remove()
+ calendar_uri = "/testing_calendar/"
+
+ @inlineCallbacks
def mkcalendar_cb(response):
response = IResponse(response)
if response.code != responsecode.CREATED:
self.fail("MKCALENDAR failed: %s" % (response.code,))
- if not calPath.isdir():
- self.fail("MKCALENDAR did not create a collection")
-
- ds = []
c = 0
for stream, response_code in work:
- def put_cb(response, stream=stream, response_code=response_code):
- response = IResponse(response)
- if response.code != response_code:
- self.fail("Incorrect response to %s: %s (!= %s)" % (what, response.code, response_code))
-
dst_uri = "/".join([calendar_uri, "dst%d.ics" % (c,)])
request = SimpleRequest(self.site, "PUT", dst_uri)
request.headers.setHeader("if-none-match", "*")
request.headers.setHeader("content-type", MimeType("text", "calendar"))
request.stream = stream
- ds.append(self.send(request, put_cb))
+ response = yield self.send(request)
+ response = IResponse(response)
+ if response.code != response_code:
+ self.fail("Incorrect response to %s: %s (!= %s)" % (what, response.code, response_code))
+
c += 1
- return DeferredList(ds)
-
request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
return self.send(request, mkcalendar_cb)
@@ -208,9 +199,7 @@
"""
Make (regular) collection in calendar
"""
- calendar_path, calendar_uri = self.mkdtemp("dot_file_in_calendar")
- calPath = FilePath(calendar_path)
- calPath.remove()
+ calendar_uri = "/dot_file_in_calendar/"
def mkcalendar_cb(response):
response = IResponse(response)
@@ -236,7 +225,7 @@
request = SimpleRequest(self.site, "PUT", event_uri)
request.headers.setHeader("content-type", MimeType("text", "calendar"))
request.stream = MemoryStream(calendar)
- self.send(request, put_cb)
+ return self.send(request, put_cb)
request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
return self.send(request, mkcalendar_cb)
Modified: CalendarServer/trunk/twistedcaldav/test/test_freebusyquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_freebusyquery.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_freebusyquery.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -18,18 +18,21 @@
from twisted.trial.unittest import SkipTest
import os
-import shutil
+from twext.python.filepath import CachingFilePath as FilePath
+
from twext.web2 import responsecode
from twext.web2.iweb import IResponse
from twext.web2.stream import MemoryStream
-from twext.web2.dav.fileop import rmdir
+
from twext.web2.test.test_server import SimpleRequest
import twistedcaldav.test.util
from twistedcaldav import caldavxml
-from twistedcaldav.index import db_basename
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twistedcaldav.test.test_calendarquery import addEventsDir
+
class FreeBusyQuery (twistedcaldav.test.util.TestCase):
"""
free-busy-query REPORT
@@ -73,47 +76,35 @@
)
query = caldavxml.FreeBusyQuery(query_timerange,)
-
+
def got_calendar(calendar):
pass
return self.free_busy_query(cal_uri, query, got_calendar)
+
+ @inlineCallbacks
def free_busy_query(self, calendar_uri, query, got_calendar):
- calendar_path = os.path.join(self.docroot, calendar_uri[1:])
- if os.path.exists(calendar_path): rmdir(calendar_path)
+ request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
+ response = yield self.send(request)
+ response = IResponse(response)
- def do_report(response):
- response = IResponse(response)
+ if response.code != responsecode.CREATED:
+ self.fail("MKCALENDAR failed: %s" % (response.code,))
- if response.code != responsecode.CREATED:
- self.fail("MKCALENDAR failed: %s" % (response.code,))
+ yield addEventsDir(self, FilePath(self.holidays_dir), calendar_uri)
- # Add holiday events to calendar
- # We're cheating by simply copying the files in
- for filename in os.listdir(self.holidays_dir):
- if os.path.splitext(filename)[1] != ".ics": continue
- path = os.path.join(self.holidays_dir, filename)
- shutil.copy(path, calendar_path)
+ request = SimpleRequest(self.site, "REPORT", calendar_uri)
+ request.stream = MemoryStream(query.toxml())
+ response = yield self.send(request)
+ response = IResponse(response)
- # Delete the index because we cheated
- index_path = os.path.join(calendar_path, db_basename)
- if os.path.isfile(index_path): os.remove(index_path)
+ if response.code != responsecode.OK:
+ self.fail("REPORT failed: %s" % (response.code,))
- request = SimpleRequest(self.site, "REPORT", calendar_uri)
- request.stream = MemoryStream(query.toxml())
+ result = yield Component.fromIStream(response.stream).addCallback(
+ got_calendar
+ )
+ returnValue(result)
- def do_test(response):
- response = IResponse(response)
-
- if response.code != responsecode.OK:
- self.fail("REPORT failed: %s" % (response.code,))
-
- return Component.fromIStream(response.stream).addCallback(got_calendar)
-
- return self.send(request, do_test, calendar_path)
-
- request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
-
- return self.send(request, do_report, calendar_path)
Modified: CalendarServer/trunk/twistedcaldav/test/test_mkcalendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_mkcalendar.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_mkcalendar.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -26,7 +26,7 @@
from twext.web2.test.test_server import SimpleRequest
from twistedcaldav import caldavxml
-from twistedcaldav.test.util import HomeTestCase, todo
+from twistedcaldav.test.util import HomeTestCase
class MKCALENDAR (HomeTestCase):
"""
@@ -150,47 +150,7 @@
request.stream = MemoryStream(mk.toxml())
return self.send(request, do_test)
- @todo("Remove: Does not make sense with new store")
- def test_make_calendar_no_parent(self):
- """
- Make calendar with no parent
- """
- uri = "/no/parent/for/calendar/"
- def do_test(response):
- response = IResponse(response)
-
- if response.code != responsecode.CONFLICT:
- self.fail("Incorrect response to MKCALENDAR with no parent: %s" % (response.code,))
-
- # FIXME: Check for CalDAV:calendar-collection-location-ok element
-
- request = SimpleRequest(self.site, "MKCALENDAR", uri)
- return self.send(request, do_test)
-
- @todo("Remove: Does not make sense with new store")
- def test_make_calendar_on_resource(self):
- """
- Make calendar on existing resource
- """
- uri = "/calendar_on_resource/"
- path = os.path.join(self.docroot, uri[1:])
-
- if not os.path.exists(path):
- f = open(path[:-1], 'w')
- f.close()
-
- def do_test(response):
- response = IResponse(response)
-
- if response.code != responsecode.NOT_ALLOWED:
- self.fail("Incorrect response to MKCALENDAR on existing resource: %s" % (response.code,))
-
- # FIXME: Check for DAV:resource-must-be-null element
-
- request = SimpleRequest(self.site, "MKCALENDAR", uri)
- return self.send(request, do_test)
-
def test_make_calendar_on_collection(self):
"""
Make calendar on existing collection
Modified: CalendarServer/trunk/twistedcaldav/test/test_multiget.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_multiget.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_multiget.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,4 +1,3 @@
-##
# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,20 +14,18 @@
##
import os
-import shutil
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twext.python.filepath import CachingFilePath as FilePath
from twext.web2 import responsecode
from twext.web2.iweb import IResponse
from twext.web2.stream import MemoryStream
from twext.web2.dav import davxml
-from twext.web2.dav.fileop import rmdir
from twext.web2.dav.util import davXMLFromStream
from twext.web2.test.test_server import SimpleRequest
from twistedcaldav import caldavxml
from twistedcaldav import ical
-from twistedcaldav.index import db_basename
from twistedcaldav.test.util import HomeTestCase, todo
from twistedcaldav.config import config
@@ -247,52 +244,40 @@
return self.calendar_query(cal_uri, query, got_xml, data, no_init)
+ @inlineCallbacks
def calendar_query(self, calendar_uri, query, got_xml, data, no_init):
- calendar_path = os.path.join(self.docroot, calendar_uri[1:])
- if not no_init and os.path.exists(calendar_path): rmdir(calendar_path)
+ if not no_init:
+ response = yield self.send(SimpleRequest(self.site, "MKCALENDAR",
+ calendar_uri))
+ response = IResponse(response)
+ if response.code != responsecode.CREATED:
+ self.fail("MKCALENDAR failed: %s" % (response.code,))
- def do_report(response):
- if not no_init:
- response = IResponse(response)
-
- if response.code != responsecode.CREATED:
- self.fail("MKCALENDAR failed: %s" % (response.code,))
-
- if data:
- for filename, icaldata in data.iteritems():
- path = os.path.join(calendar_path, filename + ".ics")
- f = open(path, "w")
- f.write(icaldata)
- f.close()
- else:
- # Add holiday events to calendar
- # We're cheating by simply copying the files in
- for filename in os.listdir(self.holidays_dir):
- if os.path.splitext(filename)[1] != ".ics": continue
- path = os.path.join(self.holidays_dir, filename)
- shutil.copy(path, calendar_path)
-
- # Delete the index because we cheated
- index_path = os.path.join(calendar_path, db_basename)
- if os.path.isfile(index_path): os.remove(index_path)
+ if data:
+ for filename, icaldata in data.iteritems():
+ request = SimpleRequest(self.site, "PUT",
+ calendar_uri + "/" + filename + ".ics")
+ request.stream = MemoryStream(icaldata)
+ yield self.send(request)
+ else:
+ # Add holiday events to calendar
+ for child in FilePath(self.holidays_dir).children():
+ if os.path.splitext(child.basename())[1] != ".ics": continue
+ request = SimpleRequest(self.site, "PUT",
+ calendar_uri + "/" + child.basename())
+ request.stream = MemoryStream(child.getContent())
+ yield self.send(request)
- request = SimpleRequest(self.site, "REPORT", calendar_uri)
- request.stream = MemoryStream(query.toxml())
+ request = SimpleRequest(self.site, "REPORT", calendar_uri)
+ request.stream = MemoryStream(query.toxml())
+ response = yield self.send(request)
- def do_test(response):
- response = IResponse(response)
+ response = IResponse(response)
- if response.code != responsecode.MULTI_STATUS:
- self.fail("REPORT failed: %s" % (response.code,))
+ if response.code != responsecode.MULTI_STATUS:
+ self.fail("REPORT failed: %s" % (response.code,))
- return davXMLFromStream(response.stream).addCallback(got_xml)
-
- return self.send(request, do_test)
-
- if no_init:
- return do_report(None)
- else:
- request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
-
- return self.send(request, do_report)
+ returnValue(
+ (yield davXMLFromStream(response.stream).addCallback(got_xml))
+ )
Modified: CalendarServer/trunk/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_sharing.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_sharing.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -25,6 +25,8 @@
from twistedcaldav.config import config
from twistedcaldav.test.util import HomeTestCase, norequest
from twistedcaldav.resource import CalDAVResource
+from txcaldav.calendarstore.test.test_postgres import buildStore
+from txcaldav.calendarstore.test.common import StubNotifierFactory
class SharingTests(HomeTestCase):
@@ -54,8 +56,8 @@
def displayName(self):
return self.displayname
-
+
@inlineCallbacks
def setUp(self):
yield super(SharingTests, self).setUp()
@@ -63,11 +65,6 @@
self.patch(config.Sharing, "Enabled", True)
self.patch(config.Sharing.Calendars, "Enabled", True)
- self.resource = (
- yield self.site.resource.locateChild(norequest(), ["calendar"])
- )[0]
- self.site.resource.putChild("calendar", self.resource)
-
CalDAVResource.validUserIDForShare = self._fakeValidUserID
CalDAVResource.validUserIDWithCommonNameForShare = self._fakeValidUserID
CalDAVResource.sendInvite = lambda self, record, request: succeed(True)
@@ -76,6 +73,17 @@
CalDAVResource.principalForCalendarUserAddress = lambda self, cuaddr: SharingTests.FakePrincipal(cuaddr)
+ @inlineCallbacks
+ def _refreshRoot(self, request=None):
+ if request is None:
+ request = norequest()
+ result = yield super(SharingTests, self)._refreshRoot(request)
+ self.resource = (
+ yield self.site.resource.locateChild(request, ["calendar"])
+ )[0]
+ returnValue(result)
+
+
def _fakeValidUserID(self, userid, *args):
if userid.startswith("/principals/"):
return userid
@@ -400,7 +408,7 @@
@inlineCallbacks
def test_POSTaddRemoveSameInvitee(self):
-
+
self.resource.upgradeToShare()
yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
@@ -448,9 +456,9 @@
),
))
+
@inlineCallbacks
def test_POSTaddInvalidInvitee(self):
-
self.resource.upgradeToShare()
response = (yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
@@ -464,7 +472,6 @@
""",
responsecode.MULTI_STATUS
))
-
self.assertEqual(
str(response.stream.read()).replace("\r\n", "\n"),
"""<?xml version='1.0' encoding='UTF-8'?>
@@ -475,10 +482,13 @@
</response>
</multistatus>"""
)
-
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
- self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite())
+ self.assertEquals(
+ self._clearUIDElementValue(propInvite), customxml.Invite()
+ )
+
+
@inlineCallbacks
def test_POSTremoveInvalidInvitee(self):
@@ -530,3 +540,18 @@
propInvite = (yield self.resource.readProperty(customxml.Invite, None))
self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite())
+
+
+
+class DatabaseSharingTests(SharingTests):
+
+ @inlineCallbacks
+ def setUp(self):
+ self.calendarStore = yield buildStore(self, StubNotifierFactory())
+ yield super(DatabaseSharingTests, self).setUp()
+
+
+ def createDataStore(self):
+ return self.calendarStore
+
+
Modified: CalendarServer/trunk/twistedcaldav/test/test_wrapping.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_wrapping.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_wrapping.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -19,10 +19,12 @@
"""
+from twisted.trial.unittest import SkipTest
+
from twext.web2.server import Request
from twext.web2.responsecode import UNAUTHORIZED
from twext.web2.http_headers import Headers
-from txdav.idav import AlreadyFinishedError
+from txdav.idav import AlreadyFinishedError, IDataStore
from twext.web2.dav import davxml
from twistedcaldav.config import config
@@ -37,13 +39,19 @@
from twistedcaldav.test.util import TestCase
-from txcaldav.calendarstore.file import CalendarStore, CalendarHome
from txcaldav.calendarstore.test.test_file import event4_text
from txcarddav.addressbookstore.file import AddressBookStore, AddressBookHome
from txcarddav.addressbookstore.test.test_file import vcard4_text
+from txcaldav.calendarstore.test.test_postgres import buildStore
+from txcaldav.calendarstore.test.common import StubNotifierFactory, \
+ assertProvides
+from txcaldav.icalendarstore import ICalendarHome
+from txcarddav.iaddressbookstore import IAddressBookHome
+
+
class FakeChanRequest(object):
def writeHeaders(self, code, headers):
self.code = code
@@ -65,7 +73,6 @@
-
class WrappingTests(TestCase):
"""
Tests for L{twistedcaldav.static.CalDAVFile} creating the appropriate type
@@ -188,6 +195,9 @@
return req
+ pathTypes = ['calendar', 'addressbook']
+
+
@inlineCallbacks
def test_autoRevertUnCommitted(self):
"""
@@ -196,7 +206,7 @@
commit them. This can happen, for example, with resources that are
children of non-existent (proto-)resources.
"""
- for pathType in ['calendar', 'addressbook']:
+ for pathType in self.pathTypes:
req = self.requestForPath('/%ss/users/wsanchez/%s/forget/it'
% (pathType, pathType))
yield req.process()
@@ -211,7 +221,7 @@
Sanity check and integration test: an unauthorized request of calendar
and addressbook resources results in an L{UNAUTHORIZED} response code.
"""
- for pathType in ['calendar', 'addressbook']:
+ for pathType in self.pathTypes:
req = self.requestForPath('/%ss/users/wsanchez/%s/'
% (pathType, pathType))
yield req.process()
@@ -223,7 +233,7 @@
Creating a DirectoryCalendarHomeProvisioningResource will create a paired
CalendarStore.
"""
- self.assertIsInstance(self.calendarCollection._newStore, CalendarStore)
+ assertProvides(self, IDataStore, self.calendarCollection._newStore)
@inlineCallbacks
@@ -235,7 +245,7 @@
"""
calDavFile = yield self.getResource("calendars/users/wsanchez/")
self.commit()
- self.assertIsInstance(calDavFile._newStoreHome, CalendarHome)
+ assertProvides(self, ICalendarHome, calDavFile._newStoreHome)
@inlineCallbacks
@@ -264,9 +274,9 @@
C{CalendarHome.calendarWithName}.
"""
calDavFile = yield self.getResource("calendars/users/wsanchez/calendar")
- self.commit()
self.assertEquals(calDavFile.resourceType(),
davxml.ResourceType.calendar)
+ self.commit()
@inlineCallbacks
@@ -338,7 +348,7 @@
Creating a AddressBookHomeProvisioningFile will create a paired
AddressBookStore.
"""
- self.assertIsInstance(self.addressbookCollection._newStore, AddressBookStore)
+ assertProvides(self, IDataStore, self.addressbookCollection._newStore)
@inlineCallbacks
@@ -350,7 +360,7 @@
"""
calDavFile = yield self.getResource("addressbooks/users/wsanchez/")
self.commit()
- self.assertIsInstance(calDavFile._newStoreHome, AddressBookHome)
+ assertProvides(self, IAddressBookHome, calDavFile._newStoreHome)
@inlineCallbacks
@@ -412,3 +422,19 @@
self.commit()
self.assertEquals(calDavFileAddressBook._principalCollections,
frozenset([self.principalsResource]))
+
+
+
+class DatabaseWrappingTests(WrappingTests):
+
+ @inlineCallbacks
+ def setUp(self):
+ self.calendarStore = yield buildStore(self, StubNotifierFactory())
+ super(DatabaseWrappingTests, self).setUp()
+
+
+ def createDataStore(self):
+ return self.calendarStore
+
+
+
Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/util.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -15,18 +15,12 @@
##
from __future__ import with_statement
-from calendarserver.provision.root import RootResource
-__all__ = [
- "featureUnimplemented",
- "testUnimplemented",
- "todo",
- "TestCase",
-]
-
import os
import xattr
+from calendarserver.provision.root import RootResource
+
from twisted.python.failure import Failure
from twisted.internet.base import DelayedCall
from twisted.internet.defer import succeed, fail, inlineCallbacks, returnValue
@@ -34,7 +28,7 @@
from twisted.internet.protocol import ProcessProtocol
from twext.python.memcacheclient import ClientFactory
-from twext.python.filepath import CachingFilePath as FilePath, CachingFilePath
+from twext.python.filepath import CachingFilePath as FilePath
import twext.web2.dav.test.util
from twext.web2.dav import davxml
from twext.web2.http import HTTPError, StatusResponse
@@ -51,6 +45,13 @@
from txdav.common.datastore.file import CommonDataStore
+
+__all__ = [
+ "featureUnimplemented",
+ "testUnimplemented",
+ "todo",
+ "TestCase",
+]
DelayedCall.debug = True
def _todo(f, why):
@@ -67,6 +68,8 @@
augmentsFile = dirTest.child("augments.xml")
proxiesFile = dirTest.child("proxies.xml")
+
+
class TestCase(twext.web2.dav.test.util.TestCase):
resource_class = RootResource
@@ -96,6 +99,15 @@
)
+ def createDataStore(self):
+ """
+ Create an L{IDataStore} that can store calendars (but not
+ addressbooks.) By default returns a L{CommonDataStore}, but this is a
+ hook for subclasses to override to provide different data stores.
+ """
+ return CommonDataStore(FilePath(config.DocumentRoot), None, True, False)
+
+
def setupCalendars(self):
"""
Set up the resource at /calendars (a L{DirectoryCalendarHomeProvisioningResource}),
@@ -103,7 +115,7 @@
"""
# Need a data store
- _newStore = CommonDataStore(CachingFilePath(config.DocumentRoot), None, True, False)
+ _newStore = self.createDataStore()
self.calendarCollection = DirectoryCalendarHomeProvisioningResource(
self.directoryService,
@@ -133,7 +145,7 @@
config.ConfigRoot = "config"
config.LogRoot = "logs"
config.RunRoot = "logs"
-
+
if not os.path.exists(config.DataRoot):
os.makedirs(config.DataRoot)
if not os.path.exists(config.DocumentRoot):
@@ -150,6 +162,7 @@
config.DirectoryAddressBook.Enabled = False
+
def createHierarchy(self, structure, root=None):
if root is None:
root = self.mktemp()
@@ -295,6 +308,13 @@
than a top-level resource hierarchy.
"""
+ def createDataStore(self):
+ # FIXME: AddressBookHomeTestCase needs the same treatment.
+ fp = FilePath(self.mktemp())
+ fp.createDirectory()
+ return CommonDataStore(fp, None, True, False)
+
+
def setUp(self):
"""
Replace self.site.resource with an appropriately provisioned
@@ -303,13 +323,10 @@
"""
super(HomeTestCase, self).setUp()
- fp = FilePath(self.mktemp())
- fp.createDirectory()
-
self.createStockDirectoryService()
# Need a data store
- _newStore = CommonDataStore(fp, None, True, False)
+ _newStore = self.createDataStore()
self.homeProvisioner = DirectoryCalendarHomeProvisioningResource(
self.directoryService, "/calendars/",
@@ -318,21 +335,39 @@
def _defer(user):
# Commit the transaction
- self.site.resource._associatedTransaction.commit()
- self.docroot = user._newStoreHome._path.path
+ self.addCleanup(self.noRenderCommit)
+ # FIXME: nothing should use docroot any more.
+ aPath = getattr(user._newStoreHome, "_path", None)
+ if aPath is not None:
+ self.docroot = aPath.path
return self._refreshRoot().addCallback(_defer)
+ committed = True
+
+ def noRenderCommit(self):
+ """
+ A resource was retrieved but will not be rendered, so commit.
+ """
+ if not self.committed:
+ self.committed = True
+ self.site.resource._associatedTransaction.commit()
+
+
@inlineCallbacks
- def _refreshRoot(self):
+ def _refreshRoot(self, request=None):
"""
Refresh the user resource positioned at the root of this site, to give
it a new transaction.
"""
+ self.noRenderCommit()
+ if request is None:
+ request = norequest()
users = self.homeProvisioner.getChild("users")
- user, ignored = (yield users.locateChild(norequest(), ["wsanchez"]))
+ user, ignored = (yield users.locateChild(request, ["wsanchez"]))
+
# Force the request to succeed regardless of the implementation of
# accessControlList.
user.accessControlList = lambda request, *a, **k: succeed(
@@ -342,19 +377,27 @@
# Fix the site to point directly at the user's calendar home so that we
# can focus on testing just that rather than hierarchy traversal..
self.site.resource = user
+ self.committed = False
returnValue(user)
@inlineCallbacks
- def send(self, request, callback):
+ def send(self, request, callback=None):
"""
Override C{send} in order to refresh the 'user' resource each time, to
get a new transaction to associate with the calendar home.
"""
+ self.noRenderCommit()
+ yield self._refreshRoot(request)
+ result = (yield super(HomeTestCase, self).send(request))
+ self.committed = True
yield self._refreshRoot()
- result = (yield super(HomeTestCase, self).send(request, callback))
+ if callback is not None:
+ result = yield callback(result)
returnValue(result)
+
+
class AddressBookHomeTestCase(TestCase):
"""
Utility class for tests which wish to interact with a addressbook home rather
@@ -411,7 +454,7 @@
@inlineCallbacks
- def send(self, request, callback):
+ def send(self, request, callback=None):
"""
Override C{send} in order to refresh the 'user' resource each time, to
get a new transaction to associate with the calendar home.
@@ -422,7 +465,6 @@
-
class InMemoryPropertyStore(object):
def __init__(self):
class _FauxPath(object):
Modified: CalendarServer/trunk/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/file.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/file.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -33,6 +33,7 @@
from twisted.internet.interfaces import ITransport
from twisted.python.failure import Failure
+
from txdav.propertystore.xattr import PropertyStore
from twext.python.vcomponent import InvalidICalendarDataError
@@ -41,7 +42,6 @@
from twext.web2.dav.resource import TwistedGETContentMD5
from twext.web2.http_headers import generateContentType
-
from twistedcaldav import caldavxml, customxml
from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
from twistedcaldav.index import Index as OldIndex, IndexSchedule as OldInboxIndex
@@ -51,11 +51,17 @@
from txcaldav.icalendarstore import ICalendar, ICalendarObject
from txcaldav.icalendarstore import ICalendarHome
+from txcaldav.calendarstore.util import (
+ validateCalendarComponent, dropboxIDFromCalendarObject
+)
-from txdav.common.datastore.file import CommonDataStore, CommonStoreTransaction, \
- CommonHome, CommonHomeChild, CommonObjectResource
-from txdav.common.icommondatastore import InvalidObjectResourceError, \
- NoSuchObjectResourceError, InternalDataStoreError
+from txdav.common.datastore.file import (
+ CommonDataStore, CommonStoreTransaction, CommonHome, CommonHomeChild,
+ CommonObjectResource
+)
+
+from txdav.common.icommondatastore import (NoSuchObjectResourceError,
+ InternalDataStoreError)
from txdav.datastore.file import writeOperation, hidden, FileMetaDataMixin
from txdav.propertystore.base import PropertyName
@@ -196,12 +202,6 @@
),
)
- def _doValidate(self, component):
- # FIXME: should be separate class, not separate case!
- if self.name() == 'inbox':
- component.validateComponentsForCalDAV(True)
- else:
- component.validateForCalDAV()
class CalendarObject(CommonObjectResource):
@@ -222,26 +222,14 @@
return self._parentCollection
+ def calendar(self):
+ return self._calendar
+
+
@writeOperation
def setComponent(self, component):
- if not isinstance(component, VComponent):
- raise TypeError(type(component))
+ validateCalendarComponent(self, self._calendar, component)
- try:
- if component.resourceUID() != self.uid():
- raise InvalidObjectResourceError(
- "UID may not change (%s != %s)" % (
- component.resourceUID(), self.uid()
- )
- )
- except NoSuchObjectResourceError:
- pass
-
- try:
- self._calendar._doValidate(component)
- except InvalidICalendarDataError, e:
- raise InvalidObjectResourceError(e)
-
newRevision = self._calendar._updateSyncToken() # FIXME: test
self._calendar.retrieveOldIndex().addResource(
self.name(), component, newRevision
@@ -378,24 +366,9 @@
def dropboxID(self):
- # FIXME: direct tests
- dropboxProperty = self.component().getFirstPropertyInAnyComponent("X-APPLE-DROPBOX")
- if dropboxProperty is not None:
- componentDropboxID = dropboxProperty.value().split("/")[-1]
- return componentDropboxID
- attachProperty = self.component().getFirstPropertyInAnyComponent("ATTACH")
- if attachProperty is not None:
- # Make sure the value type is URI
- valueType = attachProperty.params().get("VALUE", ("TEXT",))
- if valueType[0] == "URI":
- # FIXME: more aggressive checking to see if this URI is really the
- # 'right' URI. Maybe needs to happen in the front end.
- attachPath = attachProperty.value().split("/")[-2]
- return attachPath
+ return dropboxIDFromCalendarObject(self)
- return self.uid() + ".dropbox"
-
def _dropboxPath(self):
dropboxPath = self._parentCollection._home._path.child(
"dropbox"
@@ -462,6 +435,7 @@
props.flush()
+
class Attachment(FileMetaDataMixin):
"""
An L{Attachment} is a container for the data associated with a I{locally-
Modified: CalendarServer/trunk/txcaldav/calendarstore/postgres.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/postgres.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/postgres.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -20,13 +20,2082 @@
"""
__all__ = [
- "CalendarStore",
- "CalendarHome",
- "Calendar",
- "CalendarObject",
+ "PostgresStore",
+ "PostgresCalendarHome",
+ "PostgresCalendar",
+ "PostgresCalendarObject",
+ "PostgresAddressBookHome",
+ "PostgresAddressBook",
+ "PostgresAddressBookObject",
]
+import StringIO
+
+from twisted.python import hashlib
+from twistedcaldav.sharing import SharedCollectionRecord #@UnusedImport
+
+from inspect import getargspec
+from zope.interface.declarations import implements
+
from twisted.python.modules import getModule
+from twisted.application.service import Service
+from twisted.internet.interfaces import ITransport
+from twisted.internet.error import ConnectionLost
+from twisted.python.failure import Failure
+from twext.web2.dav.element.rfc2518 import ResourceType
-v1_schema = getModule(__name__).filePath.sibling("postgres_schema_v1.sql").getContent()
+from txdav.idav import IDataStore, AlreadyFinishedError
+from txdav.common.inotifications import (INotificationCollection,
+ INotificationObject)
+
+from txdav.common.icommondatastore import (
+ ObjectResourceNameAlreadyExistsError, HomeChildNameAlreadyExistsError,
+ NoSuchHomeChildError, NoSuchObjectResourceError)
+from txcaldav.calendarstore.util import (validateCalendarComponent,
+ validateAddressBookComponent, dropboxIDFromCalendarObject, SyncTokenHelper)
+
+
+from txcaldav.icalendarstore import (ICalendarTransaction, ICalendarHome,
+ ICalendar, ICalendarObject, IAttachment)
+from txcarddav.iaddressbookstore import (IAddressBookTransaction,
+ IAddressBookHome, IAddressBook, IAddressBookObject)
+from txdav.propertystore.base import AbstractPropertyStore, PropertyName
+from txdav.propertystore.none import PropertyStore
+
+from twext.web2.http_headers import MimeType, generateContentType
+from twext.web2.dav.element.parser import WebDAVDocument
+
+from twext.python.vcomponent import VComponent
+from twistedcaldav.vcard import Component as VCard
+from twistedcaldav.sharing import Invite
+from twistedcaldav.notifications import NotificationRecord
+from twistedcaldav.query.sqlgenerator import sqlgenerator
+from twistedcaldav.index import IndexedSearchException
+
+
+v1_schema = getModule(__name__).filePath.sibling(
+ "postgres_schema_v1.sql").getContent()
+
+
+# FIXME: these constants are in the schema, and should probably be discovered
+# from there somehow.
+
+_BIND_STATUS_INVITED = 0
+_BIND_STATUS_ACCEPTED = 1
+_BIND_STATUS_DECLINED = 2
+_BIND_STATUS_INVALID = 3
+
+_ATTACHMENTS_MODE_WRITE = 1
+
+_BIND_MODE_OWN = 0
+_BIND_MODE_READ = 1
+_BIND_MODE_WRITE = 2
+
+
+
+def _getarg(argname, argspec, args, kw):
+ """
+ Get an argument from some arguments.
+
+ @param argname: The name of the argument to retrieve.
+
+ @param argspec: The result of L{inspect.getargspec}.
+
+ @param args: positional arguments passed to the function specified by
+ argspec.
+
+ @param kw: keyword arguments passed to the function specified by
+ argspec.
+
+ @return: The value of the argument named by 'argname'.
+ """
+ argnames = argspec[0]
+ try:
+ argpos = argnames.index(argname)
+ except ValueError:
+ argpos = None
+ if argpos is not None:
+ if len(args) > argpos:
+ return args[argpos]
+ if argname in kw:
+ return kw[argname]
+ else:
+ raise TypeError("could not find key argument %r in %r/%r (%r)" %
+ (argname, args, kw, argpos)
+ )
+
+
+
+def memoized(keyArgument, memoAttribute):
+ """
+ Decorator which memoizes the result of a method on that method's instance.
+
+ @param keyArgument: The name of the 'key' argument.
+
+ @type keyArgument: C{str}
+
+ @param memoAttribute: The name of the attribute on the instance which
+ should be used for memoizing the result of this method; the attribute
+ itself must be a dictionary.
+
+ @type memoAttribute: C{str}
+ """
+ def decorate(thunk):
+ spec = getargspec(thunk)
+ def outer(*a, **kw):
+ self = a[0]
+ memo = getattr(self, memoAttribute)
+ key = _getarg(keyArgument, spec, a, kw)
+ if key in memo:
+ return memo[key]
+ result = thunk(*a, **kw)
+ if result is not None:
+ memo[key] = result
+ return result
+ return outer
+ return decorate
+
+
+
+class PropertyStore(AbstractPropertyStore):
+
+ def __init__(self, peruser, defaultuser, txn, resourceID):
+ super(PropertyStore, self).__init__(peruser, defaultuser)
+ self._txn = txn
+ self._resourceID = resourceID
+
+
+ def _getitem_uid(self, key, uid):
+ rows = self._txn.execSQL(
+ "select VALUE from RESOURCE_PROPERTY where "
+ "RESOURCE_ID = %s and NAME = %s and VIEWER_UID = %s",
+ [self._resourceID, key.toString(), uid])
+ if not rows:
+ raise KeyError(key)
+ return WebDAVDocument.fromString(rows[0][0]).root_element
+
+
+ def _setitem_uid(self, key, value, uid):
+ self._delitem_uid(key, uid)
+ self._txn.execSQL(
+ "insert into RESOURCE_PROPERTY "
+ "(RESOURCE_ID, NAME, VALUE, VIEWER_UID) values (%s, %s, %s, %s)",
+ [self._resourceID, key.toString(), value.toxml(), uid])
+
+
+ def _delitem_uid(self, key, uid):
+ self._txn.execSQL(
+ "delete from RESOURCE_PROPERTY where VIEWER_UID = %s"
+ "and RESOURCE_ID = %s AND NAME = %s",
+ [uid, self._resourceID, key.toString()])
+
+
+ def _keys_uid(self, uid):
+ rows = self._txn.execSQL(
+ "select NAME from RESOURCE_PROPERTY where "
+ "VIEWER_UID = %s and RESOURCE_ID = %s",
+ [uid, self._resourceID]
+ )
+ for row in rows:
+ yield PropertyName.fromString(row[0])
+
+
+
+class PostgresCalendarObject(object):
+ implements(ICalendarObject)
+
+ def __init__(self, calendar, name, resid):
+ self._calendar = calendar
+ self._name = name
+ self._resourceID = resid
+ self._calendarText = None
+
+
+ @property
+ def _txn(self):
+ return self._calendar._txn
+
+
+ def uid(self):
+ return self.component().resourceUID()
+
+
+ def organizer(self):
+ return self.component().getOrganizer()
+
+
+ def dropboxID(self):
+ return dropboxIDFromCalendarObject(self)
+
+
+ def name(self):
+ return self._name
+
+
+ def calendar(self):
+ return self._calendar
+
+
+ def iCalendarText(self):
+ if self._calendarText is None:
+ text = self._txn.execSQL(
+ "select ICALENDAR_TEXT from CALENDAR_OBJECT where "
+ "RESOURCE_ID = %s", [self._resourceID]
+ )[0][0]
+ self._calendarText = text
+ return text
+ else:
+ return self._calendarText
+
+
+ def component(self):
+ return VComponent.fromString(self.iCalendarText())
+
+
+ def componentType(self):
+ return self.component().mainType()
+
+
+ def properties(self):
+ return PropertyStore(
+ self.uid(),
+ self.uid(),
+ self._txn,
+ self._resourceID
+ )
+
+
+ def setComponent(self, component):
+ validateCalendarComponent(self, self._calendar, component)
+ calendarText = str(component)
+ self._txn.execSQL(
+ "update CALENDAR_OBJECT set ICALENDAR_TEXT = %s "
+ "where RESOURCE_ID = %s", [calendarText, self._resourceID]
+ )
+ self._calendarText = calendarText
+ self._calendar._updateSyncToken()
+
+ if self._calendar._notifier:
+ self._calendar._home._txn.postCommit(self._calendar._notifier.notify)
+
+
+ def _attachmentPath(self, name):
+ attachmentRoot = self._calendar._home._txn._store.attachmentsPath
+ try:
+ attachmentRoot.createDirectory()
+ except:
+ pass
+ return attachmentRoot.child(
+ "%s-%s-%s-%s.attachment" % (
+ self._calendar._home.uid(), self._calendar.name(),
+ self.name(), name
+ )
+ )
+
+
+ def createAttachmentWithName(self, name, contentType):
+ path = self._attachmentPath(name)
+ attachment = PostgresAttachment(self, path)
+ self._txn.execSQL("""
+ insert into ATTACHMENT (CALENDAR_OBJECT_RESOURCE_ID, CONTENT_TYPE,
+ SIZE, MD5, PATH)
+ values (%s, %s, %s, %s, %s)
+ """,
+ [
+ self._resourceID, generateContentType(contentType), 0, "",
+ attachment._pathValue()
+ ]
+ )
+ return attachment.store(contentType)
+
+
+ def attachments(self):
+ rows = self._txn.execSQL("""
+ select PATH from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s
+ """, [self._resourceID])
+ for row in rows:
+ demangledName = _pathToName(row[0])
+ yield self.attachmentWithName(demangledName)
+
+
+ def attachmentWithName(self, name):
+ attachment = PostgresAttachment(self, self._attachmentPath(name))
+ if attachment._populate():
+ return attachment
+ else:
+ return None
+
+
+ def removeAttachmentWithName(self, name):
+ attachment = PostgresAttachment(self, self._attachmentPath(name))
+ self._calendar._home._txn.postCommit(attachment._path.remove)
+ self._txn.execSQL("""
+ delete from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s AND
+ PATH = %s
+ """, [self._resourceID, attachment._pathValue()])
+
+
+ # IDataStoreResource
+ def contentType(self):
+ """
+ The content type of Calendar objects is text/calendar.
+ """
+ return MimeType.fromString("text/calendar")
+
+
+ def md5(self):
+ return None
+
+
+ def size(self):
+ return 0
+
+
+ def created(self):
+ return None
+
+
+ def modified(self):
+ return None
+
+
+ def attendeesCanManageAttachments(self):
+ return self.component().hasPropertyInAnyComponent("X-APPLE-DROPBOX")
+
+
+
+def _pathToName(path):
+ return path.rsplit(".", 1)[0].split("-", 3)[-1]
+
+
+
+class PostgresAttachment(object):
+
+ implements(IAttachment)
+
+ def __init__(self, calendarObject, path):
+ self._calendarObject = calendarObject
+ self._path = path
+
+
+ @property
+ def _txn(self):
+ return self._calendarObject._txn
+
+
+ def _populate(self):
+ """
+ Execute necessary SQL queries to retrieve attributes.
+
+ @return: C{True} if this attachment exists, C{False} otherwise.
+ """
+ rows = self._txn.execSQL(
+ """
+ select CONTENT_TYPE, MD5 from ATTACHMENT where PATH = %s
+ """, [self._pathValue()])
+ if not rows:
+ return False
+ self._contentType = MimeType.fromString(rows[0][0])
+ self._md5 = rows[0][1]
+ return True
+
+
+ def store(self, contentType):
+ return PostgresAttachmentStorageTransport(self, contentType)
+
+
+ def retrieve(self, protocol):
+ protocol.dataReceived(self._path.getContent())
+ protocol.connectionLost(Failure(ConnectionLost()))
+
+
+ def properties(self):
+ pass # stub
+
+
+ # IDataStoreResource
+ def contentType(self):
+ return self._contentType
+
+
+ def md5(self):
+ return self._md5
+
+
+ def size(self):
+ return 0
+
+
+ def created(self):
+ return None
+
+
+ def modified(self):
+ return None
+
+
+ def name(self):
+ return _pathToName(self._pathValue())
+
+
+ def _pathValue(self):
+ """
+ Compute the value which should go into the 'path' column for this
+ attachment.
+ """
+ root = self._calendarObject._calendar._home._txn._store.attachmentsPath
+ return '/'.join(self._path.segmentsFrom(root))
+
+
+
+class PostgresAttachmentStorageTransport(object):
+
+ implements(ITransport)
+
+ def __init__(self, attachment, contentType):
+ self.attachment = attachment
+ self.contentType = contentType
+ self.buf = ''
+ self.hash = hashlib.md5()
+
+
+ @property
+ def _txn(self):
+ return self.attachment._txn
+
+
+ def write(self, data):
+ self.buf += data
+ self.hash.update(data)
+
+
+ def loseConnection(self):
+ self.attachment._path.setContent(self.buf)
+ pathValue = self.attachment._pathValue()
+ contentTypeString = generateContentType(self.contentType)
+ self._txn.execSQL(
+ "update ATTACHMENT set CONTENT_TYPE = %s, MD5 = %s "
+ "WHERE PATH = %s",
+ [contentTypeString, self.hash.hexdigest(), pathValue]
+ )
+
+
+
+class PostgresLegacyInvitesEmulator(object):
+ """
+ Emulator for the implicit interface specified by
+ L{twistedcaldav.sharing.InvitesDatabase}.
+ """
+
+
+ def __init__(self, calendar):
+ self._calendar = calendar
+
+
+ @property
+ def _txn(self):
+ return self._calendar._txn
+
+
+ def create(self):
+ "No-op, because the index implicitly always exists in the database."
+
+
+ def remove(self):
+ "No-op, because the index implicitly always exists in the database."
+
+
+ def allRecords(self):
+ for row in self._txn.execSQL(
+ """
+ select
+ INVITE.INVITE_UID, INVITE.NAME, INVITE.SENDER_ADDRESS,
+ CALENDAR_HOME.OWNER_UID, CALENDAR_BIND.BIND_MODE,
+ CALENDAR_BIND.BIND_STATUS, CALENDAR_BIND.MESSAGE
+ from
+ INVITE, CALENDAR_HOME, CALENDAR_BIND
+ where
+ INVITE.RESOURCE_ID = %s and
+ INVITE.HOME_RESOURCE_ID =
+ CALENDAR_HOME.RESOURCE_ID and
+ CALENDAR_BIND.CALENDAR_RESOURCE_ID =
+ INVITE.RESOURCE_ID and
+ CALENDAR_BIND.CALENDAR_HOME_RESOURCE_ID =
+ INVITE.HOME_RESOURCE_ID
+ order by
+ INVITE.NAME asc
+ """, [self._calendar._resourceID]):
+ [inviteuid, common_name, userid, ownerUID,
+ bindMode, bindStatus, summary] = row
+ # FIXME: this is really the responsibility of the protocol layer.
+ state = {
+ _BIND_STATUS_INVITED: "NEEDS-ACTION",
+ _BIND_STATUS_ACCEPTED: "ACCEPTED",
+ _BIND_STATUS_DECLINED: "DECLINED",
+ _BIND_STATUS_INVALID: "INVALID",
+ }[bindStatus]
+ access = {
+ _BIND_MODE_READ: "read-only",
+ _BIND_MODE_WRITE: "read-write"
+ }[bindMode]
+ principalURL = "/principals/__uids__/" + ownerUID
+ yield Invite(
+ inviteuid, userid, principalURL, common_name,
+ access, state, summary
+ )
+
+
+ def recordForUserID(self, userid):
+ for record in self.allRecords():
+ if record.userid == userid:
+ return record
+
+
+ def recordForPrincipalURL(self, principalURL):
+ for record in self.allRecords():
+ if record.principalURL == principalURL:
+ return record
+
+
+ def recordForInviteUID(self, inviteUID):
+ for record in self.allRecords():
+ if record.inviteuid == inviteUID:
+ return record
+
+
+ def addOrUpdateRecord(self, record):
+ bindMode = {'read-only': _BIND_MODE_READ,
+ 'read-write': _BIND_MODE_WRITE}[record.access]
+ bindStatus = {
+ "NEEDS-ACTION": _BIND_STATUS_INVITED,
+ "ACCEPTED": _BIND_STATUS_ACCEPTED,
+ "DECLINED": _BIND_STATUS_DECLINED,
+ "INVALID": _BIND_STATUS_INVALID,
+ }[record.state]
+ # principalURL is derived from a directory record's principalURL() so
+ # it will always contain the UID.
+ principalUID = record.principalURL.split("/")[-1]
+ shareeHome = self._txn.calendarHomeWithUID(principalUID, create=True)
+ rows = self._txn.execSQL(
+ "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where INVITE_UID = %s",
+ [record.inviteuid]
+ )
+ if rows:
+ [[resourceID, homeResourceID]] = rows
+ # Invite(inviteuid, userid, principalURL, common_name, access, state, summary)
+ self._txn.execSQL("""
+ update CALENDAR_BIND set BIND_MODE = %s,
+ BIND_STATUS = %s, MESSAGE = %s
+ where
+ CALENDAR_RESOURCE_ID = %s and
+ CALENDAR_HOME_RESOURCE_ID = %s
+ """, [bindMode, bindStatus, record.summary,
+ resourceID, homeResourceID])
+ self._txn.execSQL("""
+ update INVITE set NAME = %s, SENDER_ADDRESS = %s
+ where INVITE_UID = %s
+ """,
+ [record.name, record.userid, record.inviteuid]
+ )
+ else:
+ self._txn.execSQL(
+ """
+ insert into CALENDAR_BIND
+ (
+ CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID,
+ CALENDAR_RESOURCE_NAME, BIND_MODE, BIND_STATUS,
+ SEEN_BY_OWNER, SEEN_BY_SHAREE, MESSAGE
+ )
+ values (%s, %s, %s, %s, %s, %s, %s, %s)
+ """,
+ [
+ shareeHome._resourceID,
+ self._calendar._resourceID,
+ None, # this is NULL because it is not bound yet, let's be
+ # explicit about that.
+ bindMode,
+ bindStatus,
+ False,
+ False,
+ record.summary
+ ])
+ self._txn.execSQL(
+ """
+ insert into INVITE (
+ INVITE_UID, NAME,
+ HOME_RESOURCE_ID, RESOURCE_ID,
+ SENDER_ADDRESS
+ )
+ values (%s, %s, %s, %s, %s)
+ """,
+ [
+ record.inviteuid, record.name,
+ shareeHome._resourceID, self._calendar._resourceID,
+ record.userid
+ ])
+
+
+ def removeRecordForUserID(self, userid):
+ rec = self.recordForUserID(userid)
+ self.removeRecordForInviteUID(rec.inviteuid)
+
+
+ def removeRecordForPrincipalURL(self, principalURL):
+ raise NotImplementedError("removeRecordForPrincipalURL")
+
+
+ def removeRecordForInviteUID(self, inviteUID):
+ rows = self._txn.execSQL("""
+ select HOME_RESOURCE_ID, RESOURCE_ID from INVITE where
+ INVITE_UID = %s
+ """, [inviteUID])
+ if rows:
+ [[homeID, resourceID]] = rows
+ self._txn.execSQL(
+ "delete from CALENDAR_BIND where "
+ "CALENDAR_HOME_RESOURCE_ID = %s and CALENDAR_RESOURCE_ID = %s",
+ [homeID, resourceID])
+ self._txn.execSQL("delete from INVITE where INVITE_UID = %s",
+ [inviteUID])
+
+
+
+class PostgresLegacySharesEmulator(object):
+
+ def __init__(self, home):
+ self._home = home
+
+
+ @property
+ def _txn(self):
+ return self._home._txn
+
+
+ def create(self):
+ pass
+
+
+ def remove(self):
+ pass
+
+
+ def allRecords(self):
+ return []
+# c = self._home._txn._cursor
+# c.execute(
+# "select CALENDAR_RESOURCE_ID, CALENDAR_HOME_RESOURCE_ID from "
+# "CALENDAR_BIND where CALENDAR_BIND"
+# "",
+# [self._home.uid()])
+# ownedShares = c.fetchall()
+# for row in rows:
+# [calendarResourceID] = row
+# shareuid =
+# yield SharedCollectionRecord(
+# shareuid, sharetype, hosturl, localname, summary
+# )
+
+
+ def recordForLocalName(self, localname):
+ return None
+# c = self._home._txn.cursor()
+# return SharedCollectionRecord(shareuid, sharetype, hosturl, localname, summary)
+
+
+ def recordForShareUID(self, shareUID):
+ pass
+
+
+ def addOrUpdateRecord(self, record):
+ print record
+
+# self._db_execute("""insert or replace into SHARES (SHAREUID, SHARETYPE, HOSTURL, LOCALNAME, SUMMARY)
+# values (:1, :2, :3, :4, :5)
+# """, record.shareuid, record.sharetype, record.hosturl, record.localname, record.summary,
+# )
+
+ def removeRecordForLocalName(self, localname):
+ self._txn.execSQL(
+ "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
+ "and CALENDAR_HOME_RESOURCE_ID = %s",
+ [localname, self._home._resourceID]
+ )
+
+
+ def removeRecordForShareUID(self, shareUID):
+ pass
+# c = self._home._cursor()
+# c.execute(
+# "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
+# "and CALENDAR_HOME_RESOURCE_ID = %s",
+# [self._home._resourceID]
+# )
+
+
+
+class postgresqlgenerator(sqlgenerator):
+ """
+ Query generator for postgreSQL indexed searches. (Currently unused: work
+ in progress.)
+ """
+
+ def __init__(self, expr, calendar):
+ self.TIMESPANDB = "TIME_RANGE"
+ self.TIMESPANTEST = "((TIME_RANGE.FLOAT == 'N' AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOAT == 'Y' AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s))"
+ self.TIMESPANTEST_NOEND = "((TIME_RANGE.FLOAT == 'N' AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOAT == 'Y' AND TIME_RANGE.END_DATE > %s))"
+ self.TIMESPANTEST_NOSTART = "((TIME_RANGE.FLOAT == 'N' AND TIME_RANGE.START_DATE < %s) OR (TIME_RANGE.FLOAT == 'Y' AND TIME_RANGE.START_DATE < %s))"
+ self.TIMESPANTEST_TAIL_PIECE = " AND TIME_RANGE.CALENDAR_OBJECT_RESOURCE_ID == CALENDAR_OBJECT.RESOURCE_ID"
+
+ super(postgresqlgenerator, self).__init__(expr)
+ self.calendar = calendar
+
+
+ def generate(self):
+ """
+ Generate the actual SQL 'where ...' expression from the passed in
+ expression tree.
+
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
+ partial SQL statement, and the C{list} is the list of argument
+ substitutions to use with the SQL API execute method.
+ """
+
+ # Init state
+ self.sout = StringIO.StringIO()
+ self.arguments = []
+ self.usedtimespan = False
+
+ # Generate ' where ...' partial statement
+ self.sout.write(self.WHERE)
+ self.generateExpression(self.expression)
+
+ # Prefix with ' from ...' partial statement
+ select = self.FROM + self.RESOURCEDB
+ if self.usedtimespan:
+ select += ", %s, %s" % (
+ self.TIMESPANDB)
+ select += self.sout.getvalue()
+ return select, self.arguments
+
+
+ def addArgument(self, arg):
+ self.arguments.append(arg)
+ self.sout.write("%s")
+
+
+ def setArgument(self, arg):
+ self.arguments.append(arg)
+ return "%s"
+
+
+
+class PostgresLegacyIndexEmulator(object):
+ """
+ Emulator for L{twistedcaldv.index.Index} and
+ L{twistedcaldv.index.IndexSchedule}.
+ """
+
+ def __init__(self, calendar):
+ self.calendar = calendar
+
+
+ @property
+ def _txn(self):
+ return self.calendar._txn
+
+
+ def reserveUID(self, uid):
+ pass
+
+
+ def isAllowedUID(self, uid, *names):
+ """
+ @see: L{twistedcaldav.index.Index.isAllowedUID}
+ """
+ return True
+
+
+ def resourceUIDForName(self, name):
+ obj = self.calendar.calendarObjectWithName(name)
+ if obj is None:
+ return None
+ return obj.uid()
+
+
+ def resourceNameForUID(self, uid):
+ obj = self.calendar.calendarObjectWithUID(uid)
+ if obj is None:
+ return None
+ return obj.name()
+
+
+ def unreserveUID(self, uid):
+ pass
+
+
+ def notExpandedBeyond(self, minDate):
+ """
+ Gives all resources which have not been expanded beyond a given date
+ in the database. (Unused; see above L{postgresqlgenerator}.
+ """
+ return self._txn.execSQL(
+ "select RESOURCE_NAME from CALENDAR_OBJECT "
+ "where RECURRANCE_MAX < %s and CALENDAR_RESOURCE_ID = %s",
+ [minDate, self.calendar._resourceID]
+ )
+
+
+ def indexedSearch(self, filter, useruid='', fbtype=False):
+ """
+ Always raise L{IndexedSearchException}, since these indexes are not
+ fully implemented yet.
+ """
+ raise IndexedSearchException()
+
+
+ def bruteForceSearch(self):
+ return self._txn.execSQL(
+ "select RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from "
+ "CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = %s",
+ [self.calendar._resourceID]
+ )
+
+
+ def resourcesExist(self, names):
+ return list(set(names).intersection(
+ set(self.calendar.listCalendarObjects())))
+
+
+ def resourceExists(self, name):
+ return bool(
+ self._txn.execSQL(
+ "select RESOURCE_NAME from CALENDAR_OBJECT where "
+ "RESOURCE_NAME = %s and CALENDAR_RESOURCE_ID = %s",
+ [name, self.calendar._resourceID]
+ )
+ )
+
+
+
+class PostgresCalendar(SyncTokenHelper):
+
+ implements(ICalendar)
+
+ def __init__(self, home, name, resourceID, notifier):
+ self._home = home
+ self._name = name
+ self._resourceID = resourceID
+ self._objects = {}
+ self._notifier = notifier
+
+
+ @property
+ def _txn(self):
+ return self._home._txn
+
+
+ def retrieveOldInvites(self):
+ return PostgresLegacyInvitesEmulator(self)
+
+ def retrieveOldIndex(self):
+ return PostgresLegacyIndexEmulator(self)
+
+
+ def notifierID(self, label="default"):
+ return None
+
+
+ def name(self):
+ return self._name
+
+
+ def rename(self, name):
+ oldName = self._name
+ self._txn.execSQL(
+ "update CALENDAR_BIND set CALENDAR_RESOURCE_NAME = %s "
+ "where CALENDAR_RESOURCE_ID = %s AND "
+ "CALENDAR_HOME_RESOURCE_ID = %s",
+ [name, self._resourceID, self._home._resourceID]
+ )
+ self._name = name
+ # update memos
+ del self._home._calendars[oldName]
+ self._home._calendars[name] = self
+
+
+ def ownerCalendarHome(self):
+ return self._home
+
+
+ def listCalendarObjects(self):
+ # FIXME: see listChildren
+ rows = self._txn.execSQL(
+ "select RESOURCE_NAME from "
+ "CALENDAR_OBJECT where "
+ "CALENDAR_RESOURCE_ID = %s",
+ [self._resourceID])
+ return [row[0] for row in rows]
+
+
+ def calendarObjects(self):
+ for name in self.listCalendarObjects():
+ yield self.calendarObjectWithName(name)
+
+
+ @memoized('name', '_objects')
+ def calendarObjectWithName(self, name):
+ rows = self._txn.execSQL(
+ "select RESOURCE_ID from CALENDAR_OBJECT where "
+ "RESOURCE_NAME = %s and CALENDAR_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ if not rows:
+ return None
+ resid = rows[0][0]
+ return PostgresCalendarObject(self, name, resid)
+
+
+ def calendarObjectWithUID(self, uid):
+ rows = self._txn.execSQL(
+ "select RESOURCE_NAME from CALENDAR_OBJECT where "
+ "ICALENDAR_UID = %s",
+ [uid]
+ )
+ if not rows:
+ return None
+ name = rows[0][0]
+ return self.calendarObjectWithName(name)
+
+
+ def createCalendarObjectWithName(self, name, component):
+ rows = self._txn.execSQL(
+ "select RESOURCE_NAME from CALENDAR_OBJECT where "
+ " RESOURCE_NAME = %s AND CALENDAR_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ if rows:
+ raise ObjectResourceNameAlreadyExistsError()
+
+ self._updateSyncToken()
+
+ calendarObject = PostgresCalendarObject(self, name, None)
+ calendarObject.component = lambda : component
+
+ validateCalendarComponent(calendarObject, self, component)
+
+ componentText = str(component)
+ self._txn.execSQL(
+ """
+ insert into CALENDAR_OBJECT
+ (CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT,
+ ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE)
+ values
+ (%s, %s, %s, %s, %s, %s)
+ """,
+ # should really be filling out more fields: ORGANIZER,
+ # ORGANIZER_OBJECT, a correct ATTACHMENTS_MODE based on X-APPLE-
+ # DROPBOX
+ [self._resourceID, name, componentText, component.resourceUID(),
+ component.resourceType(), _ATTACHMENTS_MODE_WRITE]
+ )
+ if self._notifier:
+ self._home._txn.postCommit(self._notifier.notify)
+
+
+ def removeCalendarObjectWithName(self, name):
+ self._txn.execSQL(
+ "delete from CALENDAR_OBJECT where RESOURCE_NAME = %s and "
+ "CALENDAR_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ if self._txn._cursor.rowcount == 0:
+ raise NoSuchObjectResourceError()
+ self._objects.pop(name, None)
+
+ self._updateSyncToken()
+
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def removeCalendarObjectWithUID(self, uid):
+ rows = self._txn.execSQL(
+ "select RESOURCE_NAME from CALENDAR_OBJECT where "
+ "ICALENDAR_UID = %s AND CALENDAR_RESOURCE_ID = %s",
+ [uid, self._resourceID]
+ )
+ if not rows:
+ raise NoSuchObjectResourceError()
+ name = rows[0][0]
+ self._txn.execSQL(
+ "delete from CALENDAR_OBJECT where ICALENDAR_UID = %s and "
+ "CALENDAR_RESOURCE_ID = %s",
+ [uid, self._resourceID]
+ )
+ self._objects.pop(name, None)
+ self._updateSyncToken()
+
+ if self._notifier:
+ self._home._txn.postCommit(self._notifier.notify)
+
+
+ def syncToken(self):
+ return self._txn.execSQL(
+ "select SYNC_TOKEN from CALENDAR where RESOURCE_ID = %s",
+ [self._resourceID])[0][0]
+
+
+ def calendarObjectsInTimeRange(self, start, end, timeZone):
+ raise NotImplementedError()
+
+
+ def calendarObjectsSinceToken(self, token):
+ raise NotImplementedError()
+
+
+ def properties(self):
+ ownerUID = self.ownerCalendarHome().uid()
+ return PropertyStore(
+ ownerUID,
+ ownerUID,
+ self._txn,
+ self._resourceID
+ )
+
+
+ # IDataStoreResource
+ def contentType(self):
+ """
+ The content type of Calendar objects is text/calendar.
+ """
+ return MimeType.fromString("text/calendar")
+
+
+ def md5(self):
+ return None
+
+
+ def size(self):
+ return 0
+
+
+ def created(self):
+ return None
+
+
+ def modified(self):
+ return None
+
+
+
+class PostgresCalendarHome(object):
+
+ implements(ICalendarHome)
+
+ def __init__(self, transaction, ownerUID, resourceID, notifier):
+ self._txn = transaction
+ self._ownerUID = ownerUID
+ self._resourceID = resourceID
+ self._calendars = {}
+ self._notifier = notifier
+
+
+ def retrieveOldShares(self):
+ return PostgresLegacySharesEmulator(self)
+
+
+ def uid(self):
+ """
+ Retrieve the unique identifier for this calendar home.
+
+ @return: a string.
+ """
+ return self._ownerUID
+
+
+ def name(self):
+ """
+ Implement L{IDataStoreResource.name} to return the uid.
+ """
+ return self.uid()
+
+
+ def transaction(self):
+ return self._txn
+
+
+ def listChildren(self):
+ """
+ Retrieve the names of the children in this calendar home.
+
+ @return: an iterable of C{str}s.
+ """
+ # FIXME: not specified on the interface or exercised by the tests, but
+ # required by clients of the implementation!
+ rows = self._txn.execSQL(
+ "select CALENDAR_RESOURCE_NAME from CALENDAR_BIND where "
+ "CALENDAR_HOME_RESOURCE_ID = %s "
+ "AND BIND_STATUS != %s",
+ [self._resourceID, _BIND_STATUS_DECLINED]
+ )
+ names = [row[0] for row in rows]
+ return names
+
+
+ def calendars(self):
+ """
+ Retrieve calendars contained in this calendar home.
+
+ @return: an iterable of L{ICalendar}s.
+ """
+ names = self.listChildren()
+ for name in names:
+ yield self.calendarWithName(name)
+
+
+ @memoized('name', '_calendars')
+ def calendarWithName(self, name):
+ """
+ Retrieve the calendar with the given C{name} contained in this
+ calendar home.
+
+ @param name: a string.
+ @return: an L{ICalendar} or C{None} if no such calendar
+ exists.
+ """
+ data = self._txn.execSQL(
+ "select CALENDAR_RESOURCE_ID from CALENDAR_BIND where "
+ "CALENDAR_RESOURCE_NAME = %s",
+ [name]
+ )
+ if not data:
+ return None
+ resourceID = data[0][0]
+ if self._notifier:
+ childID = "%s/%s" % (self.uid(), name)
+ notifier = self._notifier.clone(label="collection", id=childID)
+ else:
+ notifier = None
+ return PostgresCalendar(self, name, resourceID, notifier)
+
+
+ def calendarObjectWithDropboxID(self, dropboxID):
+ """
+ Implement lookup with brute-force scanning.
+ """
+ for calendar in self.calendars():
+ for calendarObject in calendar.calendarObjects():
+ if dropboxID == calendarObject.dropboxID():
+ return calendarObject
+
+
+ def createCalendarWithName(self, name):
+ rows = self._txn.execSQL(
+ "select CALENDAR_RESOURCE_NAME from CALENDAR_BIND where "
+ "CALENDAR_RESOURCE_NAME = %s AND "
+ "CALENDAR_HOME_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ if rows:
+ raise HomeChildNameAlreadyExistsError()
+ rows = self._txn.execSQL("select nextval('RESOURCE_ID_SEQ')")
+ resourceID = rows[0][0]
+ self._txn.execSQL(
+ "insert into CALENDAR (SYNC_TOKEN, RESOURCE_ID) values "
+ "(%s, %s)",
+ ['uninitialized', resourceID])
+
+ self._txn.execSQL("""
+ insert into CALENDAR_BIND (
+ CALENDAR_HOME_RESOURCE_ID,
+ CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, BIND_MODE,
+ SEEN_BY_OWNER, SEEN_BY_SHAREE, BIND_STATUS) values (
+ %s, %s, %s, %s, %s, %s, %s)
+ """,
+ [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
+ _BIND_STATUS_ACCEPTED]
+ )
+
+ calendarType = ResourceType.calendar #@UndefinedVariable
+ newCalendar = self.calendarWithName(name)
+ newCalendar.properties()[
+ PropertyName.fromElement(ResourceType)] = calendarType
+ newCalendar._updateSyncToken(True)
+
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def removeCalendarWithName(self, name):
+ self._txn.execSQL(
+ "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s and "
+ "CALENDAR_HOME_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ self._calendars.pop(name, None)
+ if self._txn._cursor.rowcount == 0:
+ raise NoSuchHomeChildError()
+ # FIXME: the schema should probably cascade the calendar delete when
+ # the last bind is deleted.
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def properties(self):
+ return PropertyStore(
+ self.uid(),
+ self.uid(),
+ self._txn,
+ self._resourceID
+ )
+
+
+ # IDataStoreResource
+ def contentType(self):
+ """
+ The content type of Calendar objects is text/calendar.
+ """
+ return MimeType.fromString("text/calendar")
+
+
+ def md5(self):
+ return None
+
+
+ def size(self):
+ return 0
+
+
+ def created(self):
+ return None
+
+
+ def modified(self):
+ return None
+
+
+ def notifierID(self, label="default"):
+ return None
+
+
+
+class PostgresNotificationObject(object):
+ implements(INotificationObject)
+
+ def __init__(self, home, resourceID):
+ self._home = home
+ self._resourceID = resourceID
+
+
+ def name(self):
+ return self.uid() + ".xml"
+
+
+ @property
+ def _txn(self):
+ return self._home._txn
+
+
+ def setData(self, uid, xmltype, xmldata):
+ return self._txn.execSQL(
+ """
+ update NOTIFICATION set NOTIFICATION_UID = %s, XML_TYPE = %s,
+ XML_DATA = %s where RESOURCE_ID = %s
+ """,
+ [uid, xmltype, xmldata, self._resourceID]
+ )
+
+
+ def _fieldQuery(self, field):
+ [[data]] = self._txn.execSQL(
+ "select " + field + " from NOTIFICATION where "
+ "RESOURCE_ID = %s",
+ [self._resourceID])
+ return data
+
+
+ def xmldata(self):
+ return self._fieldQuery("XML_DATA")
+
+
+ def uid(self):
+ return self._fieldQuery("NOTIFICATION_UID")
+
+
+ def properties(self):
+ return PropertyStore(self._home.uid(),
+ self._home.uid(),
+ self._txn,
+ self._resourceID)
+
+
+ def md5(self):
+ return None
+
+
+ def modified(self):
+ return None
+
+
+ def created(self):
+ return None
+
+
+
+class PostgresLegacyNotificationsEmulator(object):
+ def __init__(self, notificationsCollection):
+ self._collection = notificationsCollection
+
+
+ def _recordForObject(self, notificationObject):
+ return NotificationRecord(
+ notificationObject.uid(),
+ notificationObject.name(),
+ notificationObject._fieldQuery("XML_TYPE"))
+
+
+ def recordForName(self, name):
+ return self._recordForObject(
+ self._collection.notificationObjectWithName(name)
+ )
+
+
+ def recordForUID(self, uid):
+ return self._recordForObject(
+ self._collection.notificationObjectWithUID(uid)
+ )
+
+
+ def removeRecordForUID(self, uid):
+ self._collection.removeNotificationObjectWithUID(uid)
+
+
+ def removeRecordForName(self, name):
+ self._collection.removeNotificationObjectWithName(name)
+
+
+
+class PostgresNotificationsCollection(object):
+
+ implements(INotificationCollection)
+
+ def __init__(self, txn, uid, resourceID):
+ self._txn = txn
+ self._uid = uid
+ self._resourceID = resourceID
+
+
+ def retrieveOldIndex(self):
+ return PostgresLegacyNotificationsEmulator(self)
+
+
+ def name(self):
+ return 'notification'
+
+
+ def uid(self):
+ return self._uid
+
+
+ def notificationObjects(self):
+ for [uid] in self._txn.execSQL(
+ "select (NOTIFICATION_UID) "
+ "from NOTIFICATION "
+ "where NOTIFICATION_HOME_RESOURCE_ID = %s",
+ [self._resourceID]):
+ yield self.notificationObjectWithUID(uid)
+
+
+ def _nameToUID(self, name):
+ """
+ Based on the file-backed implementation, the 'name' is just uid +
+ ".xml".
+ """
+ return name.rsplit(".", 1)[0]
+
+
+ def notificationObjectWithName(self, name):
+ return self.notificationObjectWithUID(self._nameToUID(name))
+
+
+ def notificationObjectWithUID(self, uid):
+ rows = self._txn.execSQL(
+ "select RESOURCE_ID from NOTIFICATION where NOTIFICATION_UID = %s"
+ " and NOTIFICATION_HOME_RESOURCE_ID = %s",
+ [uid, self._resourceID])
+ if rows:
+ [[resourceID]] = rows
+ return PostgresNotificationObject(self, resourceID)
+ else:
+ return None
+
+
+ def writeNotificationObject(self, uid, xmltype, xmldata):
+ xmltype = PropertyName.fromElement(xmltype).toString()
+ self._txn.execSQL(
+ "insert into NOTIFICATION (NOTIFICATION_HOME_RESOURCE_ID, NOTIFICATION_UID, XML_TYPE, XML_DATA) "
+ "values (%s, %s, %s, %s)", [self._resourceID, uid, xmltype, xmldata])
+
+
+ def removeNotificationObjectWithName(self, name):
+ self.removeNotificationObjectWithUID(self._nameToUID(name))
+
+
+ def removeNotificationObjectWithUID(self, uid):
+ self._txn.execSQL(
+ "delete from NOTIFICATION where NOTIFICATION_UID = %s and "
+ "NOTIFICATION_HOME_RESOURCE_ID = %s",
+ [uid, self._resourceID])
+
+
+ def syncToken(self):
+ return 'dummy-sync-token'
+
+
+ def notificationObjectsSinceToken(self, token):
+ changed = []
+ removed = []
+ token = self.syncToken()
+ return (changed, removed, token)
+
+
+ def properties(self):
+ return PropertyStore(
+ self._uid, self._uid, self._txn, self._resourceID
+ )
+
+
+
+class PostgresTransaction(object):
+ """
+ Transaction implementation for postgres database.
+ """
+ implements(ICalendarTransaction, IAddressBookTransaction)
+
+ def __init__(self, store, connection, notifierFactory, label):
+ # print 'STARTING', label
+ self._store = store
+ self._connection = connection
+ self._cursor = connection.cursor()
+ self._completed = False
+ self._homes = {}
+ self._postCommitOperations = []
+ self._notifierFactory = notifierFactory
+ self._label = label
+
+
+ def store(self):
+ return self._store
+
+
+ def __repr__(self):
+ return 'PG-TXN<%s>' % (self._label,)
+
+
+ def execSQL(self, sql, args=[]):
+ # print 'EXECUTE %s: %s' % (self._label, sql)
+ self._cursor.execute(sql, args)
+ if self._cursor.description:
+ return self._cursor.fetchall()
+ else:
+ return None
+
+
+ def __del__(self):
+ if not self._completed:
+ self._connection.rollback()
+ self._connection.close()
+
+
+ @memoized('uid', '_homes')
+ def calendarHomeWithUID(self, uid, create=False):
+ data = self.execSQL(
+ "select RESOURCE_ID from CALENDAR_HOME where OWNER_UID = %s",
+ [uid]
+ )
+ if not data:
+ if not create:
+ return None
+ self.execSQL(
+ "insert into CALENDAR_HOME (OWNER_UID) values (%s)",
+ [uid]
+ )
+ home = self.calendarHomeWithUID(uid)
+ home.createCalendarWithName("calendar")
+ return home
+ resid = data[0][0]
+
+ if self._notifierFactory:
+ notifier = self._notifierFactory.newNotifier(id=uid)
+ else:
+ notifier = None
+
+ return PostgresCalendarHome(self, uid, resid, notifier)
+
+
+ @memoized('uid', '_homes')
+ def addressbookHomeWithUID(self, uid, create=False):
+ data = self.execSQL(
+ "select RESOURCE_ID from ADDRESSBOOK_HOME where OWNER_UID = %s",
+ [uid]
+ )
+ if not data:
+ if not create:
+ return None
+ self.execSQL(
+ "insert into ADDRESSBOOK_HOME (OWNER_UID) values (%s)",
+ [uid]
+ )
+ home = self.addressbookHomeWithUID(uid)
+ home.createAddressBookWithName("addressbook")
+ return home
+ resid = data[0][0]
+
+ if self._notifierFactory:
+ notifier = self._notifierFactory.newNotifier(id=uid)
+ else:
+ notifier = None
+
+ return PostgresAddressBookHome(self, uid, resid, notifier)
+
+
+ def notificationsWithUID(self, uid):
+ """
+ Implement notificationsWithUID.
+ """
+ rows = self.execSQL(
+ """
+ select RESOURCE_ID from NOTIFICATION_HOME where
+ OWNER_UID = %s
+ """, [uid])
+ if rows:
+ [[resourceID]] = rows
+ else:
+ [[resourceID]] = self.execSQL("select nextval('RESOURCE_ID_SEQ')")
+ resourceID = str(resourceID)
+ self.execSQL(
+ "insert into NOTIFICATION_HOME (RESOURCE_ID, OWNER_UID) "
+ "values (%s, %s)", [resourceID, uid])
+ return PostgresNotificationsCollection(self, uid, resourceID)
+
+
+ def abort(self):
+ if not self._completed:
+ # print 'ABORTING', self._label
+ self._completed = True
+ self._connection.rollback()
+ self._connection.close()
+ else:
+ raise AlreadyFinishedError()
+
+
+ def commit(self):
+ if not self._completed:
+ # print 'COMPLETING', self._label
+ self._completed = True
+ self._connection.commit()
+ self._connection.close()
+ for operation in self._postCommitOperations:
+ operation()
+ else:
+ raise AlreadyFinishedError()
+
+
+ def postCommit(self, operation):
+ """
+ Run things after 'commit.'
+ """
+ self._postCommitOperations.append(operation)
+ # FIXME: implement.
+
+# CARDDAV
+
+class PostgresAddressBookObject(object):
+
+ implements(IAddressBookObject)
+
+ def __init__(self, addressbook, name, resid):
+ self._addressbook = addressbook
+ self._name = name
+ self._resourceID = resid
+ self._vCardText = None
+
+
+ @property
+ def _txn(self):
+ return self._addressbook._txn
+
+
+ def uid(self):
+ return self.component().resourceUID()
+
+
+ def name(self):
+ return self._name
+
+
+ def addressbook(self):
+ return self._addressbook
+
+
+ def vCardText(self):
+ if self._vCardText is None:
+ text = self._txn.execSQL(
+ "select VCARD_TEXT from ADDRESSBOOK_OBJECT where "
+ "RESOURCE_ID = %s", [self._resourceID]
+ )[0][0]
+ self._vCardText = text
+ return text
+ else:
+ return self._vCardText
+
+
+ def component(self):
+ return VCard.fromString(self.vCardText())
+
+
+ def componentType(self):
+ return self.component().mainType()
+
+
+ def properties(self):
+ return PropertyStore(
+ self.uid(),
+ self.uid(),
+ self._txn,
+ self._resourceID
+ )
+
+
+ def setComponent(self, component):
+ validateAddressBookComponent(self, self._addressbook, component)
+
+ vCardText = str(component)
+ self._txn.execSQL(
+ "update ADDRESSBOOK_OBJECT set VCARD_TEXT = %s "
+ "where RESOURCE_ID = %s", [vCardText, self._resourceID]
+ )
+ self._vCardText = vCardText
+ if self._addressbook._notifier:
+ self._addressbook._home._txn.postCommit(self._addressbook._notifier.notify)
+
+
+
+ # IDataStoreResource
+ def contentType(self):
+ """
+ The content type of Addressbook objects is text/x-vcard.
+ """
+ return MimeType.fromString("text/x-vcard")
+
+
+ def md5(self):
+ return None
+
+
+ def size(self):
+ return 0
+
+
+ def created(self):
+ return None
+
+
+ def modified(self):
+ return None
+
+
+
+class PostgresAddressBook(object):
+
+ implements(IAddressBook)
+
+ def __init__(self, home, name, resourceID, notifier):
+ self._home = home
+ self._name = name
+ self._resourceID = resourceID
+ self._objects = {}
+ self._notifier = notifier
+
+
+ @property
+ def _txn(self):
+ return self._home._txn
+
+
+ def retrieveOldInvites(self):
+ return PostgresLegacyInvitesEmulator(self)
+
+ def retrieveOldIndex(self):
+ return PostgresLegacyIndexEmulator(self)
+
+
+ def notifierID(self, label="default"):
+ return None
+
+
+ def name(self):
+ return self._name
+
+
+ def rename(self, name):
+ oldName = self._name
+ self._txn.execSQL(
+ "update ADDRESSBOOK_BIND set ADDRESSBOOK_RESOURCE_NAME = %s "
+ "where ADDRESSBOOK_RESOURCE_ID = %s AND "
+ "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
+ [name, self._resourceID, self._home._resourceID]
+ )
+ self._name = name
+ # update memos
+ del self._home._addressbooks[oldName]
+ self._home._addressbooks[name] = self
+
+
+ def ownerAddressBookHome(self):
+ return self._home
+
+
+ def listAddressbookObjects(self):
+ # FIXME: see listChildren
+ rows = self._txn.execSQL(
+ "select RESOURCE_NAME from "
+ "ADDRESSBOOK_OBJECT where "
+ "ADDRESSBOOK_RESOURCE_ID = %s",
+ [self._resourceID])
+ return [row[0] for row in rows]
+
+
+ def addressbookObjects(self):
+ for name in self.listAddressbookObjects():
+ yield self.addressbookObjectWithName(name)
+
+
+ @memoized('name', '_objects')
+ def addressbookObjectWithName(self, name):
+ rows = self._txn.execSQL(
+ "select RESOURCE_ID from ADDRESSBOOK_OBJECT where "
+ "RESOURCE_NAME = %s and ADDRESSBOOK_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ if not rows:
+ return None
+ resid = rows[0][0]
+ return PostgresAddressBookObject(self, name, resid)
+
+
+ def addressbookObjectWithUID(self, uid):
+ rows = self._txn.execSQL(
+ "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
+ "VCARD_UID = %s",
+ [uid]
+ )
+ if not rows:
+ return None
+ name = rows[0][0]
+ return self.addressbookObjectWithName(name)
+
+
+ def createAddressBookObjectWithName(self, name, component):
+ rows = self._txn.execSQL(
+ "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
+ " RESOURCE_NAME = %s AND ADDRESSBOOK_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ if rows:
+ raise ObjectResourceNameAlreadyExistsError()
+
+ addressbookObject = PostgresAddressBookObject(self, name, None)
+ addressbookObject.component = lambda : component
+
+ validateAddressBookComponent(addressbookObject, self, component)
+
+ componentText = str(component)
+ self._txn.execSQL(
+ """
+ insert into ADDRESSBOOK_OBJECT
+ (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, VCARD_TEXT,
+ VCARD_UID, VCARD_TYPE)
+ values
+ (%s, %s, %s, %s, %s)
+ """,
+ [self._resourceID, name, componentText, component.resourceUID(),
+ "VCARD"] # component.resourceType()] FIXME: what value(s) here?
+ )
+ if self._notifier:
+ self._home._txn.postCommit(self._notifier.notify)
+
+
+ def removeAddressBookObjectWithName(self, name):
+ self._txn.execSQL(
+ "delete from ADDRESSBOOK_OBJECT where RESOURCE_NAME = %s and "
+ "ADDRESSBOOK_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ if self._txn._cursor.rowcount == 0:
+ raise NoSuchObjectResourceError()
+ self._objects.pop(name, None)
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def removeAddressBookObjectWithUID(self, uid):
+ rows = self._txn.execSQL(
+ "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
+ "VCARD_UID = %s AND ADDRESSBOOK_RESOURCE_ID = %s",
+ [uid, self._resourceID]
+ )
+ if not rows:
+ raise NoSuchObjectResourceError()
+ name = rows[0][0]
+ self._txn.execSQL(
+ "delete from ADDRESSBOOK_OBJECT where VCARD_UID = %s and "
+ "ADDRESSBOOK_RESOURCE_ID = %s",
+ [uid, self._resourceID]
+ )
+ self._objects.pop(name, None)
+ if self._notifier:
+ self._home._txn.postCommit(self._notifier.notify)
+
+
+ def syncToken(self):
+ return self._txn.execSQL(
+ "select SYNC_TOKEN from ADDRESSBOOK where RESOURCE_ID = %s",
+ [self._resourceID])[0][0]
+
+
+ def addressbookObjectsSinceToken(self, token):
+ raise NotImplementedError()
+
+
+ def properties(self):
+ ownerUID = self.ownerAddressBookHome().uid()
+ return PropertyStore(
+ ownerUID,
+ ownerUID,
+ self._txn,
+ self._resourceID
+ )
+
+
+ # IDataStoreResource
+ def contentType(self):
+ """
+ The content type of Addressbook objects is ???
+ """
+ return None # FIXME: verify
+
+
+ def md5(self):
+ return None
+
+
+ def size(self):
+ return 0
+
+
+ def created(self):
+ return None
+
+
+ def modified(self):
+ return None
+
+
+
+
+class PostgresAddressBookHome(object):
+
+ implements(IAddressBookHome)
+
+ def __init__(self, transaction, ownerUID, resourceID, notifier):
+ self._txn = transaction
+ self._ownerUID = ownerUID
+ self._resourceID = resourceID
+ self._addressbooks = {}
+ self._notifier = notifier
+
+
+ def retrieveOldShares(self):
+ return PostgresLegacySharesEmulator(self)
+
+
+ def uid(self):
+ """
+ Retrieve the unique identifier for this calendar home.
+
+ @return: a string.
+ """
+ return self._ownerUID
+
+
+ def name(self):
+ """
+ Implement L{IDataStoreResource.name} to return the uid.
+ """
+ return self.uid()
+
+
+ def listChildren(self):
+ """
+ Retrieve the names of the children in this addressbook home.
+
+ @return: an iterable of C{str}s.
+ """
+ # FIXME: not specified on the interface or exercised by the tests, but
+ # required by clients of the implementation!
+ rows = self._txn.execSQL(
+ "select ADDRESSBOOK_RESOURCE_NAME from ADDRESSBOOK_BIND where "
+ "ADDRESSBOOK_HOME_RESOURCE_ID = %s "
+ "AND BIND_STATUS != %s",
+ [self._resourceID, _BIND_STATUS_DECLINED]
+ )
+ names = [row[0] for row in rows]
+ return names
+
+
+ def addressbooks(self):
+ """
+ Retrieve addressbooks contained in this addressbook home.
+
+ @return: an iterable of L{IAddressBook}s.
+ """
+ names = self.listChildren()
+ for name in names:
+ yield self.addressbookWithName(name)
+
+
+ @memoized('name', '_addressbooks')
+ def addressbookWithName(self, name):
+ """
+ Retrieve the addressbook with the given C{name} contained in this
+ addressbook home.
+
+ @param name: a string.
+ @return: an L{IAddressBook} or C{None} if no such addressbook
+ exists.
+ """
+ data = self._txn.execSQL(
+ "select ADDRESSBOOK_RESOURCE_ID from ADDRESSBOOK_BIND where "
+ "ADDRESSBOOK_RESOURCE_NAME = %s",
+ [name]
+ )
+ if not data:
+ return None
+ resourceID = data[0][0]
+ if self._notifier:
+ childID = "%s/%s" % (self.uid(), name)
+ notifier = self._notifier.clone(label="collection", id=childID)
+ else:
+ notifier = None
+ return PostgresAddressBook(self, name, resourceID, notifier)
+
+
+ def createAddressBookWithName(self, name):
+ rows = self._txn.execSQL(
+ "select ADDRESSBOOK_RESOURCE_NAME from ADDRESSBOOK_BIND where "
+ "ADDRESSBOOK_RESOURCE_NAME = %s AND "
+ "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ if rows:
+ raise HomeChildNameAlreadyExistsError()
+ rows = self._txn.execSQL("select nextval('RESOURCE_ID_SEQ')")
+ resourceID = rows[0][0]
+ self._txn.execSQL(
+ "insert into ADDRESSBOOK (SYNC_TOKEN, RESOURCE_ID) values "
+ "(%s, %s)",
+ ['uninitialized', resourceID])
+
+ self._txn.execSQL("""
+ insert into ADDRESSBOOK_BIND (
+ ADDRESSBOOK_HOME_RESOURCE_ID,
+ ADDRESSBOOK_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME, BIND_MODE,
+ SEEN_BY_OWNER, SEEN_BY_SHAREE, BIND_STATUS) values (
+ %s, %s, %s, %s, %s, %s, %s)
+ """,
+ [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
+ _BIND_STATUS_ACCEPTED]
+ )
+
+ addressbookType = ResourceType.addressbook #@UndefinedVariable
+ self.addressbookWithName(name).properties()[
+ PropertyName.fromElement(ResourceType)] = addressbookType
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def removeAddressBookWithName(self, name):
+ self._txn.execSQL(
+ "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s and "
+ "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
+ [name, self._resourceID]
+ )
+ self._addressbooks.pop(name, None)
+ if self._txn._cursor.rowcount == 0:
+ raise NoSuchHomeChildError()
+ # FIXME: the schema should probably cascade the addressbook delete when
+ # the last bind is deleted.
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def properties(self):
+ return PropertyStore(
+ self.uid(),
+ self.uid(),
+ self._txn,
+ self._resourceID
+ )
+
+
+ # IDataStoreResource
+ def contentType(self):
+ """
+ The content type of Addressbook home objects is ???
+ """
+ return None # FIXME: verify
+
+
+ def md5(self):
+ return None
+
+
+ def size(self):
+ return 0
+
+
+ def created(self):
+ return None
+
+
+ def modified(self):
+ return None
+
+
+ def notifierID(self, label="default"):
+ return None
+
+
+#
+
+
+class PostgresStore(Service, object):
+
+ implements(IDataStore)
+
+ def __init__(self, connectionFactory, notifierFactory, attachmentsPath):
+ self.connectionFactory = connectionFactory
+ self.notifierFactory = notifierFactory
+ self.attachmentsPath = attachmentsPath
+
+
+ def newTransaction(self, label="unlabeled"):
+ return PostgresTransaction(
+ self,
+ self.connectionFactory(),
+ self.notifierFactory,
+ label
+ )
+
Modified: CalendarServer/trunk/txcaldav/calendarstore/postgres_schema_v1.sql
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/postgres_schema_v1.sql 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/postgres_schema_v1.sql 2010-08-12 02:36:27 UTC (rev 6074)
@@ -25,6 +25,40 @@
);
+------------------------
+-- Sharing Invitation --
+------------------------
+
+create table INVITE (
+ INVITE_UID varchar(255) not null,
+ NAME varchar(255) not null,
+ SENDER_ADDRESS varchar(255) not null,
+ HOME_RESOURCE_ID varchar(255) not null,
+ RESOURCE_ID varchar(255) not null
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+ RESOURCE_ID varchar(255) primary key default nextval('RESOURCE_ID_SEQ'),
+ OWNER_UID varchar(255) not null unique
+);
+
+
+create table NOTIFICATION (
+ RESOURCE_ID varchar(255)
+ primary key default nextval('RESOURCE_ID_SEQ'),
+ NOTIFICATION_HOME_RESOURCE_ID
+ varchar(255) not null references NOTIFICATION_HOME,
+ NOTIFICATION_UID varchar(255) not null,
+ XML_TYPE varchar not null,
+ XML_DATA varchar not null
+);
+
+
-------------------
-- Calendar Bind --
-------------------
@@ -34,12 +68,16 @@
create table CALENDAR_BIND (
CALENDAR_HOME_RESOURCE_ID varchar(255) not null references CALENDAR_HOME,
CALENDAR_RESOURCE_ID varchar(255) not null references CALENDAR,
- CALENDAR_RESOURCE_NAME varchar(255) not null,
+
+ -- An invitation which hasn't been accepted yet will not yet have a resource
+ -- name, so this field may be null.
+
+ CALENDAR_RESOURCE_NAME varchar(255),
BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
SEEN_BY_OWNER bool not null,
SEEN_BY_SHAREE bool not null,
- MESSAGE text, -- FIXME: xml?
+ MESSAGE text,
primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID),
unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)
@@ -66,6 +104,7 @@
insert into CALENDAR_BIND_STATUS values (0, 'invited' );
insert into CALENDAR_BIND_STATUS values (1, 'accepted');
insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
---------------------
@@ -82,9 +121,15 @@
ATTACHMENTS_MODE int not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
ORGANIZER varchar(255),
ORGANIZER_OBJECT varchar(255) references CALENDAR_OBJECT,
+ RECURRANCE_MAX date, -- maximum date that recurrences have been expanded to.
- unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME),
- unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+ unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME)
+
+ -- since the 'inbox' is a 'calendar resource' for the purpose of storing
+ -- calendar objects, this constraint has to be selectively enforced by the
+ -- application layer.
+
+ -- unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
);
-- Enumeration of attachment modes
@@ -131,11 +176,11 @@
----------------
create table ATTACHMENT (
- CALENDAR_OBJECT_RESOURCE_ID varchar(255) not null references CALENDAR_OBJECT,
- CONTENT_TYPE varchar(255) not null,
- SIZE int not null,
- MD5 char(32) not null,
- PATH varchar(255) not null unique
+ CALENDAR_OBJECT_RESOURCE_ID varchar(255) not null references CALENDAR_OBJECT,
+ CONTENT_TYPE varchar(255) not null,
+ SIZE int not null,
+ MD5 char(32) not null,
+ PATH varchar(1024) not null unique
);
@@ -164,3 +209,57 @@
primary key(RESOURCE_ID, NAME, VIEWER_UID)
);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+ RESOURCE_ID varchar(255) primary key default nextval('RESOURCE_ID_SEQ'),
+ OWNER_UID varchar(255) not null unique
+);
+
+
+-----------------
+-- AddressBook --
+-----------------
+
+create table ADDRESSBOOK (
+ RESOURCE_ID varchar(255) primary key default nextval('RESOURCE_ID_SEQ'),
+ SYNC_TOKEN varchar(255)
+);
+
+
+----------------------
+-- AddressBook Bind --
+----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK
+
+create table ADDRESSBOOK_BIND (
+ ADDRESSBOOK_HOME_RESOURCE_ID varchar(255) not null references ADDRESSBOOK_HOME,
+ ADDRESSBOOK_RESOURCE_ID varchar(255) not null references ADDRESSBOOK,
+ ADDRESSBOOK_RESOURCE_NAME varchar(255) not null,
+ BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
+ BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
+ SEEN_BY_OWNER bool not null,
+ SEEN_BY_SHAREE bool not null,
+ MESSAGE text, -- FIXME: xml?
+
+ primary key(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID),
+ unique(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)
+);
+
+
+create table ADDRESSBOOK_OBJECT (
+ RESOURCE_ID varchar(255) primary key default nextval('RESOURCE_ID_SEQ'),
+ ADDRESSBOOK_RESOURCE_ID varchar(255) not null references ADDRESSBOOK,
+ RESOURCE_NAME varchar(255) not null,
+ VCARD_TEXT text not null,
+ VCARD_UID varchar(255) not null,
+ VCARD_TYPE varchar(255) not null,
+
+ unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME),
+ unique(ADDRESSBOOK_RESOURCE_ID, VCARD_UID)
+);
Modified: CalendarServer/trunk/txcaldav/calendarstore/scheduling.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/scheduling.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/scheduling.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -153,9 +153,9 @@
self._calendarStore = calendarStore
- def newTransaction(self):
+ def newTransaction(self, label="unlabeled"):
"""
Wrap an underlying L{ITransaction}.
"""
return ImplicitTransaction(
- self._calendarStore.newTransaction())
+ self._calendarStore.newTransaction(label))
Modified: CalendarServer/trunk/txcaldav/calendarstore/test/common.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/test/common.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/test/common.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -144,6 +144,22 @@
+def assertProvides(testCase, interface, provider):
+ """
+ Verify that C{provider} properly provides C{interface}
+
+ @type interface: L{zope.interface.Interface}
+ @type provider: C{provider}
+ """
+ try:
+ verifyObject(interface, provider)
+ except BrokenMethodImplementation, e:
+ testCase.fail(e)
+ except DoesNotImplement, e:
+ testCase.fail("%r does not provide %s.%s" %
+ (provider, interface.__module__, interface.getName()))
+
+
class CommonTests(object):
"""
Tests for common functionality of interfaces defined in
@@ -157,6 +173,7 @@
"2.ics": cal1Root.child("2.ics").getContent(),
"3.ics": cal1Root.child("3.ics").getContent()
},
+ "calendar_2": {},
"calendar_empty": {},
"not_a_calendar": None
},
@@ -188,7 +205,7 @@
return self.lastTransaction
if self.savedStore is None:
self.savedStore = self.storeUnderTest()
- txn = self.lastTransaction = self.savedStore.newTransaction()
+ txn = self.lastTransaction = self.savedStore.newTransaction(self.id())
return txn
@@ -210,6 +227,10 @@
self.lastTransaction = None
+ def setUp(self):
+ self.notifierFactory = StubNotifierFactory()
+
+
def tearDown(self):
if self.lastTransaction is not None:
self.commit()
@@ -219,8 +240,7 @@
"""
Get the calendar home detailed by C{requirements['home1']}.
"""
- return self.transactionUnderTest().calendarHomeWithUID(
- "home1")
+ return self.transactionUnderTest().calendarHomeWithUID("home1")
def calendarUnderTest(self):
@@ -238,22 +258,8 @@
return self.calendarUnderTest().calendarObjectWithName("1.ics")
- def assertProvides(self, interface, provider):
- """
- Verify that C{provider} properly provides C{interface}
+ assertProvides = assertProvides
- @type interface: L{zope.interface.Interface}
- @type provider: C{provider}
- """
- try:
- verifyObject(interface, provider)
- except BrokenMethodImplementation, e:
- self.fail(e)
- except DoesNotImplement, e:
- self.fail("%r does not provide %s.%s" %
- (provider, interface.__module__, interface.getName()))
-
-
def test_calendarStoreProvides(self):
"""
The calendar store provides L{IDataStore} and its required attributes.
@@ -268,7 +274,7 @@
L{ICommonStoreTransaction}, L{ICalendarTransaction}, and their
respectively required attributes.
"""
- txn = self.storeUnderTest().newTransaction()
+ txn = self.transactionUnderTest()
self.assertProvides(ICommonTransaction, txn)
self.assertProvides(ICalendarTransaction, txn)
@@ -303,9 +309,8 @@
provides L{ICalendarHome} and has a C{uid()} method that returns the
same value that was passed in.
"""
- calendarHome = (self.storeUnderTest().newTransaction()
+ calendarHome = (self.transactionUnderTest()
.calendarHomeWithUID("home1"))
-
self.assertEquals(calendarHome.uid(), "home1")
self.assertProvides(ICalendarHome, calendarHome)
@@ -315,11 +320,8 @@
L{ICommonStoreTransaction.calendarHomeWithUID} should return C{None}
when asked for a non-existent calendar home.
"""
- self.assertEquals(
- self.storeUnderTest().newTransaction()
- .calendarHomeWithUID("xyzzy"),
- None
- )
+ txn = self.transactionUnderTest()
+ self.assertEquals(txn.calendarHomeWithUID("xyzzy"), None)
def test_calendarWithName_exists(self):
@@ -330,6 +332,8 @@
home = self.homeUnderTest()
for name in home1_calendarNames:
calendar = home.calendarWithName(name)
+ if calendar is None:
+ self.fail("calendar %r didn't exist" % (name,))
self.assertProvides(ICalendar, calendar)
self.assertEquals(calendar.name(), name)
@@ -380,24 +384,27 @@
calendarProperties[
PropertyName.fromString(davxml.ResourceType.sname())
],
- davxml.ResourceType.calendar) #@UndefinedVariable
+ davxml.ResourceType.calendar
+ ) #@UndefinedVariable
checkProperties()
self.commit()
# Make sure notification fired after commit
- self.assertTrue(self.notifierFactory.compare([("update", "home1")]))
+ self.assertEquals(self.notifierFactory.history, [("update", "home1")])
# Make sure it's available in a new transaction; i.e. test the commit.
home = self.homeUnderTest()
self.assertNotIdentical(home.calendarWithName(name), None)
- home = self.calendarStore.newTransaction().calendarHomeWithUID(
- "home1")
- # Sanity check: are the properties actually persisted?
- # FIXME: no independent testing of this right now
+
+ # Sanity check: are the properties actually persisted? Check in
+ # subsequent transaction.
checkProperties()
+ # FIXME: no independent testing of the property store's persistence
+ # right now
+
def test_createCalendarWithName_exists(self):
"""
L{ICalendarHome.createCalendarWithName} raises
@@ -427,8 +434,10 @@
self.commit()
# Make sure notification fired after commit
- self.assertTrue(self.notifierFactory.compare(
- [("update", "home1"), ("update", "home1"), ("update", "home1")]))
+ self.assertEquals(
+ self.notifierFactory.history,
+ [("update", "home1"), ("update", "home1"), ("update", "home1")]
+ )
def test_removeCalendarWithName_absent(self):
@@ -456,8 +465,8 @@
)
self.assertEquals(
- list(o.name() for o in calendarObjects),
- calendar1_objectNames
+ set(list(o.name() for o in calendarObjects)),
+ set(calendar1_objectNames)
)
@@ -527,17 +536,16 @@
# Make sure notifications are fired after commit
self.commit()
- self.assertTrue(
- self.notifierFactory.compare(
- [
- ("update", "home1"),
- ("update", "home1/calendar_1"),
- ("update", "home1"),
- ("update", "home1/calendar_1"),
- ("update", "home1"),
- ("update", "home1/calendar_1"),
- ]
- )
+ self.assertEquals(
+ self.notifierFactory.history,
+ [
+ ("update", "home1"),
+ ("update", "home1/calendar_1"),
+ ("update", "home1"),
+ ("update", "home1/calendar_1"),
+ ("update", "home1"),
+ ("update", "home1/calendar_1"),
+ ]
)
def test_removeCalendarObjectWithName_exists(self):
@@ -689,13 +697,12 @@
self.commit()
# Make sure notifications fire after commit
- self.assertTrue(
- self.notifierFactory.compare(
- [
- ("update", "home1"),
- ("update", "home1/calendar_1"),
- ]
- )
+ self.assertEquals(
+ self.notifierFactory.history,
+ [
+ ("update", "home1"),
+ ("update", "home1/calendar_1"),
+ ]
)
@@ -705,10 +712,12 @@
L{CalendarObjectNameAlreadyExistsError} if a calendar object with the
given name already exists in that calendar.
"""
+ cal = self.calendarUnderTest()
+ comp = VComponent.fromString(event4_text)
self.assertRaises(
ObjectResourceNameAlreadyExistsError,
- self.calendarUnderTest().createCalendarObjectWithName,
- "1.ics", VComponent.fromString(event4_text)
+ cal.createCalendarObjectWithName,
+ "1.ics", comp
)
@@ -764,9 +773,13 @@
create=True
)
def readOtherTxn():
- return self.savedStore.newTransaction().calendarHomeWithUID(
- noHomeUID)
+ otherTxn = self.savedStore.newTransaction(self.id() + "other txn")
+ self.addCleanup(otherTxn.commit)
+ return otherTxn.calendarHomeWithUID(noHomeUID)
self.assertProvides(ICalendarHome, calendarHome)
+ # Default calendar should be automatically created.
+ self.assertProvides(ICalendar,
+ calendarHome.calendarWithName("calendar"))
# A concurrent transaction shouldn't be able to read it yet:
self.assertIdentical(readOtherTxn(), None)
self.commit()
@@ -795,13 +808,12 @@
self.commit()
# Make sure notification fired after commit
- self.assertTrue(
- self.notifierFactory.compare(
- [
- ("update", "home1"),
- ("update", "home1/calendar_1"),
- ]
- )
+ self.assertEquals(
+ self.notifierFactory.history,
+ [
+ ("update", "home1"),
+ ("update", "home1/calendar_1"),
+ ]
)
@@ -930,7 +942,7 @@
def test_dropboxID(self):
"""
L{ICalendarObject.dropboxID} should synthesize its dropbox from the X
- -APPLE-DROPBOX property.
+ -APPLE-DROPBOX property, if available.
"""
cal = self.calendarUnderTest()
cal.createCalendarObjectWithName("drop.ics", VComponent.fromString(
@@ -1087,10 +1099,7 @@
return Notifier(self, label=label, id=id)
def send(self, op, id):
- self._history.append((op, id))
+ self.history.append((op, id))
def reset(self):
- self._history = []
-
- def compare(self, expected):
- return self._history == expected
+ self.history = []
Modified: CalendarServer/trunk/txcaldav/calendarstore/test/test_file.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/test/test_file.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/test/test_file.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -18,6 +18,10 @@
File calendar store tests.
"""
+# FIXME: all test cases in this file aside from FileStorageTests should be
+# deleted and replaced with either implementation-specific methods on
+# FileStorageTests, or implementation-agnostic methods on CommonTests.
+
from twext.python.filepath import CachingFilePath as FilePath
from twisted.trial import unittest
@@ -33,7 +37,7 @@
from txcaldav.calendarstore.file import Calendar, CalendarObject
from txcaldav.calendarstore.test.common import (
- CommonTests, event4_text, event1modified_text, StubNotifierFactory)
+ CommonTests, event4_text, event1modified_text)
storePath = FilePath(__file__).parent().child("calendar_store")
@@ -58,7 +62,6 @@
calendarPath.parent().makedirs()
storePath.copyTo(calendarPath)
- test.notifierFactory = StubNotifierFactory()
test.calendarStore = CalendarStore(storeRootPath, test.notifierFactory)
test.txn = test.calendarStore.newTransaction()
assert test.calendarStore is not None, "No calendar store?"
@@ -84,6 +87,8 @@
Test cases for L{CalendarStore}.
"""
+ notifierFactory = None
+
def setUp(self):
setUpCalendarStore(self)
@@ -102,6 +107,7 @@
class CalendarHomeTest(unittest.TestCase):
+ notifierFactory = None
def setUp(self):
setUpHome1(self)
@@ -158,6 +164,8 @@
class CalendarTest(unittest.TestCase):
+ notifierFactory = None
+
def setUp(self):
setUpCalendar1(self)
@@ -400,6 +408,8 @@
class CalendarObjectTest(unittest.TestCase):
+ notifierFactory = None
+
def setUp(self):
setUpCalendar1(self)
self.object1 = self.calendar1.calendarObjectWithName("1.ics")
Copied: CalendarServer/trunk/txcaldav/calendarstore/test/test_postgres.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/test_postgres.py)
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/test/test_postgres.py (rev 0)
+++ CalendarServer/trunk/txcaldav/calendarstore/test/test_postgres.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,242 @@
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+Tests for txcaldav.calendarstore.postgres, mostly based on
+L{txcaldav.calendarstore.test.common}.
+"""
+
+import gc
+
+from txcaldav.calendarstore.test.common import CommonTests as CalendarCommonTests
+from txcarddav.addressbookstore.test.common import CommonTests as AddressBookCommonTests
+from txdav.common.icommondatastore import (
+ NoSuchHomeChildError, HomeChildNameAlreadyExistsError)
+
+from twisted.trial import unittest
+from txdav.datastore.subpostgres import (PostgresService,
+ DiagnosticConnectionWrapper)
+from txcaldav.calendarstore.postgres import PostgresStore, v1_schema
+from twisted.internet.defer import Deferred, inlineCallbacks, succeed
+from twisted.internet import reactor
+from twext.python.filepath import CachingFilePath
+from twext.python.vcomponent import VComponent
+from twistedcaldav.vcard import Component as VCard
+from twisted.internet.task import deferLater
+from twisted.python import log
+
+
+
+def allInstancesOf(cls):
+ for o in gc.get_referrers(cls):
+ if isinstance(o, cls):
+ yield o
+
+
+
+def dumpConnectionStatus():
+ print '+++ ALL CONNECTIONS +++'
+ for connection in allInstancesOf(DiagnosticConnectionWrapper):
+ print connection.label, connection.state
+ print '--- CONNECTIONS END ---'
+
+
+
+class StoreBuilder(object):
+ """
+ Test-fixture-builder which can construct a PostgresStore.
+ """
+ sharedService = None
+ currentTestID = None
+
+ SHARED_DB_PATH = "../_test_postgres_db"
+
+ def buildStore(self, testCase, notifierFactory):
+ """
+ Do the necessary work to build a store for a particular test case.
+
+ @return: a L{Deferred} which fires with an L{IDataStore}.
+ """
+ currentTestID = testCase.id()
+ dbRoot = CachingFilePath(self.SHARED_DB_PATH)
+ if self.sharedService is None:
+ ready = Deferred()
+ def getReady(connectionFactory):
+ attachmentRoot = dbRoot.child("attachments")
+ try:
+ attachmentRoot.createDirectory()
+ except OSError:
+ pass
+ try:
+ self.store = PostgresStore(
+ lambda label=None: connectionFactory(
+ label or currentTestID
+ ),
+ notifierFactory,
+ attachmentRoot
+ )
+ except:
+ ready.errback()
+ raise
+ else:
+ self.cleanDatabase(testCase)
+ ready.callback(self.store)
+ return self.store
+ self.sharedService = PostgresService(
+ dbRoot, getReady, v1_schema, "caldav", resetSchema=True,
+ testMode=True
+ )
+ self.sharedService.startService()
+ def startStopping():
+ log.msg("Starting stopping.")
+ self.sharedService.unpauseMonitor()
+ return self.sharedService.stopService()
+ reactor.addSystemEventTrigger(#@UndefinedVariable
+ "before", "shutdown", startStopping)
+ result = ready
+ else:
+ self.store.notifierFactory = notifierFactory
+ self.cleanDatabase(testCase)
+ result = succeed(self.store)
+
+ def cleanUp():
+ # FIXME: clean up any leaked connections and report them with an
+ # immediate test failure.
+ def stopit():
+ self.sharedService.pauseMonitor()
+ return deferLater(reactor, 0.1, stopit)
+ testCase.addCleanup(cleanUp)
+ return result
+
+
+ def cleanDatabase(self, testCase):
+ cleanupConn = self.store.connectionFactory(
+ "%s schema-cleanup" % (testCase.id(),)
+ )
+ cursor = cleanupConn.cursor()
+ tables = ['INVITE',
+ 'RESOURCE_PROPERTY',
+ 'ATTACHMENT',
+ 'ADDRESSBOOK_OBJECT',
+ 'CALENDAR_OBJECT',
+ 'CALENDAR_BIND',
+ 'ADDRESSBOOK_BIND',
+ 'CALENDAR',
+ 'ADDRESSBOOK',
+ 'CALENDAR_HOME',
+ 'ADDRESSBOOK_HOME']
+ for table in tables:
+ try:
+ cursor.execute("delete from "+table)
+ except:
+ log.err()
+ cleanupConn.commit()
+ cleanupConn.close()
+
+
+
+theStoreBuilder = StoreBuilder()
+buildStore = theStoreBuilder.buildStore
+
+
+
+class CalendarSQLStorageTests(CalendarCommonTests, unittest.TestCase):
+ """
+ Calendar SQL storage tests.
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+ super(CalendarSQLStorageTests, self).setUp()
+ self.calendarStore = yield buildStore(self, self.notifierFactory)
+ self.populate()
+
+
+ def populate(self):
+ populateTxn = self.calendarStore.newTransaction()
+ for homeUID in self.requirements:
+ calendars = self.requirements[homeUID]
+ if calendars is not None:
+ home = populateTxn.calendarHomeWithUID(homeUID, True)
+ # We don't want the default calendar to appear unless it's
+ # explicitly listed.
+ try:
+ home.removeCalendarWithName("calendar")
+ except NoSuchHomeChildError:
+ pass
+ for calendarName in calendars:
+ calendarObjNames = calendars[calendarName]
+ if calendarObjNames is not None:
+ home.createCalendarWithName(calendarName)
+ calendar = home.calendarWithName(calendarName)
+ for objectName in calendarObjNames:
+ objData = calendarObjNames[objectName]
+ calendar.createCalendarObjectWithName(
+ objectName, VComponent.fromString(objData)
+ )
+ populateTxn.commit()
+ self.notifierFactory.reset()
+
+
+ def storeUnderTest(self):
+ """
+ Create and return a L{CalendarStore} for testing.
+ """
+ return self.calendarStore
+
+
+class AddressBookSQLStorageTests(AddressBookCommonTests, unittest.TestCase):
+ """
+ AddressBook SQL storage tests.
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+ super(AddressBookSQLStorageTests, self).setUp()
+ self.addressbookStore = yield buildStore(self, self.notifierFactory)
+ self.populate()
+
+ def populate(self):
+ populateTxn = self.addressbookStore.newTransaction()
+ for homeUID in self.requirements:
+ addressbooks = self.requirements[homeUID]
+ if addressbooks is not None:
+ home = populateTxn.addressbookHomeWithUID(homeUID, True)
+ # We don't want the default addressbook to appear unless it's
+ # explicitly listed.
+ home.removeAddressBookWithName("addressbook")
+ for addressbookName in addressbooks:
+ addressbookObjNames = addressbooks[addressbookName]
+ if addressbookObjNames is not None:
+ home.createAddressBookWithName(addressbookName)
+ addressbook = home.addressbookWithName(addressbookName)
+ for objectName in addressbookObjNames:
+ objData = addressbookObjNames[objectName]
+ addressbook.createAddressBookObjectWithName(
+ objectName, VCard.fromString(objData)
+ )
+
+ populateTxn.commit()
+ self.notifierFactory.reset()
+
+
+
+ def storeUnderTest(self):
+ """
+ Create and return a L{AddressBookStore} for testing.
+ """
+ return self.addressbookStore
+
Copied: CalendarServer/trunk/txcaldav/calendarstore/util.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/util.py)
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/util.py (rev 0)
+++ CalendarServer/trunk/txcaldav/calendarstore/util.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,155 @@
+##
+# Copyright (c) 2010 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.
+##
+"""
+Utility logic common to multiple backend implementations.
+"""
+
+from twext.python.vcomponent import InvalidICalendarDataError
+from twext.python.vcomponent import VComponent
+from twistedcaldav.vcard import Component as VCard
+from twistedcaldav.vcard import InvalidVCardDataError
+
+from txdav.common.icommondatastore import InvalidObjectResourceError,\
+ NoSuchObjectResourceError
+from twistedcaldav.customxml import GETCTag
+from uuid import uuid4
+from txdav.propertystore.base import PropertyName
+
+
+def validateCalendarComponent(calendarObject, calendar, component):
+ """
+ Validate a calendar component for a particular calendar.
+
+ @param calendarObject: The calendar object whose component will be replaced.
+ @type calendarObject: L{ICalendarObject}
+
+ @param calendar: The calendar which the L{ICalendarObject} is present in.
+ @type calendar: L{ICalendar}
+
+ @param component: The VComponent to be validated.
+ @type component: L{VComponent}
+ """
+
+ if not isinstance(component, VComponent):
+ raise TypeError(type(component))
+
+ try:
+ if component.resourceUID() != calendarObject.uid():
+ raise InvalidObjectResourceError(
+ "UID may not change (%s != %s)" % (
+ component.resourceUID(), calendarObject.uid()
+ )
+ )
+ except NoSuchObjectResourceError:
+ pass
+
+ try:
+ # FIXME: This is a bad way to do this test, there should be a
+ # Calendar-level API for it.
+ if calendar.name() == 'inbox':
+ component.validateComponentsForCalDAV(True)
+ else:
+ component.validateForCalDAV()
+ except InvalidICalendarDataError, e:
+ raise InvalidObjectResourceError(e)
+
+
+def dropboxIDFromCalendarObject(calendarObject):
+ """
+ Helper to implement L{ICalendarObject.dropboxID}.
+
+ @param calendarObject: The calendar object to retrieve a dropbox ID for.
+ @type calendarObject: L{ICalendarObject}
+ """
+ dropboxProperty = calendarObject.component(
+ ).getFirstPropertyInAnyComponent("X-APPLE-DROPBOX")
+ if dropboxProperty is not None:
+ componentDropboxID = dropboxProperty.value().split("/")[-1]
+ return componentDropboxID
+ attachProperty = calendarObject.component().getFirstPropertyInAnyComponent("ATTACH")
+ if attachProperty is not None:
+ # Make sure the value type is URI
+ valueType = attachProperty.params().get("VALUE", ("TEXT",))
+ if valueType[0] == "URI":
+ # FIXME: more aggressive checking to see if this URI is really the
+ # 'right' URI. Maybe needs to happen in the front end.
+ attachPath = attachProperty.value().split("/")[-2]
+ return attachPath
+
+ return calendarObject.uid() + ".dropbox"
+
+
+def validateAddressBookComponent(addressbookObject, vcard, component):
+ """
+ Validate an addressbook component for a particular addressbook.
+
+ @param addressbookObject: The addressbook object whose component will be replaced.
+ @type addressbookObject: L{IAddressBookObject}
+
+ @param addressbook: The addressbook which the L{IAddressBookObject} is present in.
+ @type addressbook: L{IAddressBook}
+
+ @param component: The VComponent to be validated.
+ @type component: L{VComponent}
+ """
+
+ if not isinstance(component, VCard):
+ raise TypeError(type(component))
+
+ try:
+ if component.resourceUID() != addressbookObject.uid():
+ raise InvalidObjectResourceError(
+ "UID may not change (%s != %s)" % (
+ component.resourceUID(), addressbookObject.uid()
+ )
+ )
+ except NoSuchObjectResourceError:
+ pass
+
+ try:
+ component.validForCardDAV()
+ except InvalidVCardDataError, e:
+ raise InvalidObjectResourceError(e)
+
+
+
+class SyncTokenHelper(object):
+ """
+ Implement a basic _updateSyncToken in terms of an object with a property
+ store. This is a mixin for use by data store implementations.
+ """
+
+ def _updateSyncToken(self, reset=False):
+ # FIXME: add locking a-la CalDAVResource.bumpSyncToken
+ # FIXME: tests for desired concurrency properties
+ ctag = PropertyName.fromString(GETCTag.sname())
+ props = self.properties()
+ token = props.get(ctag)
+ if token is None or reset:
+ tokenuuid = uuid4()
+ revision = 1
+ else:
+ # FIXME: no direct tests for update
+ token = str(token)
+ tokenuuid, revision = token.split("#", 1)
+ revision = int(revision) + 1
+ token = "%s#%d" % (tokenuuid, revision)
+ props[ctag] = GETCTag(token)
+ # FIXME: no direct tests for commit
+ return revision
+
+
+
Modified: CalendarServer/trunk/txcaldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txcaldav/icalendarstore.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/icalendarstore.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -196,7 +196,7 @@
@param name: a string.
@param component: a C{VCALENDAR} L{Component}
- @raise CalendarObjectNameAlreadyExistsError: if a calendar
+ @raise ObjectResourceNameAlreadyExistsError: if a calendar
object with the given C{name} already exists.
@raise CalendarObjectUIDAlreadyExistsError: if a calendar
object with the same UID as the given C{component} already
@@ -265,6 +265,12 @@
object.
"""
+ def calendar():
+ """
+ @return: The calendar which this calendar object is a part of.
+ @rtype: L{ICalendar}
+ """
+
def setComponent(component):
"""
Rewrite this calendar object to match the given C{component}.
@@ -375,7 +381,15 @@
"""
+ def attendeesCanManageAttachments():
+ """
+ Are attendees allowed to manage attachments?
+ @return: C{True} if they can, C{False} if they can't.
+ """
+
+
+
class IAttachment(IDataStoreResource):
"""
Information associated with an attachment to a calendar object.
Modified: CalendarServer/trunk/txcarddav/addressbookstore/file.py
===================================================================
--- CalendarServer/trunk/txcarddav/addressbookstore/file.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcarddav/addressbookstore/file.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -152,6 +152,11 @@
def _addressbook(self):
return self._parentCollection
+
+ def addressbook(self):
+ return self._addressbook
+
+
@writeOperation
def setComponent(self, component):
if not isinstance(component, VComponent):
Modified: CalendarServer/trunk/txcarddav/addressbookstore/test/common.py
===================================================================
--- CalendarServer/trunk/txcarddav/addressbookstore/test/common.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcarddav/addressbookstore/test/common.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,4 +1,4 @@
-# -*- test-case-name: txcarddav.addressbookstore -*-
+# -*- test-case-name: txcarddav.addressbookstore,txcaldav.calendarstore.test.test_postgres.AddressBookSQLStorageTests -*-
##
# Copyright (c) 2010 Apple Inc. All rights reserved.
#
@@ -103,7 +103,23 @@
)
+def assertProvides(testCase, interface, provider):
+ """
+ Verify that C{provider} properly provides C{interface}
+ @type interface: L{zope.interface.Interface}
+ @type provider: C{provider}
+ """
+ try:
+ verifyObject(interface, provider)
+ except BrokenMethodImplementation, e:
+ testCase.fail(e)
+ except DoesNotImplement, e:
+ testCase.fail("%r does not provide %s.%s" %
+ (provider, interface.__module__, interface.getName()))
+
+
+
class CommonTests(object):
"""
Tests for common functionality of interfaces defined in
@@ -117,6 +133,7 @@
"2.vcf": adbk1Root.child("2.vcf").getContent(),
"3.vcf": adbk1Root.child("3.vcf").getContent()
},
+ "addressbook_2": {},
"addressbook_empty": {},
"not_a_addressbook": None
},
@@ -148,7 +165,7 @@
return self.lastTransaction
if self.savedStore is None:
self.savedStore = self.storeUnderTest()
- txn = self.lastTransaction = self.savedStore.newTransaction()
+ txn = self.lastTransaction = self.savedStore.newTransaction(self.id())
return txn
@@ -169,7 +186,15 @@
self.lastTransaction.abort()
self.lastTransaction = None
+ def setUp(self):
+ self.notifierFactory = StubNotifierFactory()
+ def tearDown(self):
+ if self.lastTransaction is not None:
+ self.commit()
+
+
+
def homeUnderTest(self):
"""
Get the addressbook home detailed by C{requirements['home1']}.
@@ -192,22 +217,9 @@
return self.addressbookUnderTest().addressbookObjectWithName("1.vcf")
- def assertProvides(self, interface, provider):
- """
- Verify that C{provider} properly provides C{interface}
+ assertProvides = assertProvides
- @type interface: L{zope.interface.Interface}
- @type provider: C{provider}
- """
- try:
- verifyObject(interface, provider)
- except BrokenMethodImplementation, e:
- self.fail(e)
- except DoesNotImplement, e:
- self.fail("%r does not provide %s.%s" %
- (provider, interface.__module__, interface.getName()))
-
def test_addressbookStoreProvides(self):
"""
The addressbook store provides L{IAddressBookStore} and its required
@@ -222,7 +234,7 @@
The transactions generated by the addressbook store provide
L{IAddressBookStoreTransaction} and its required attributes.
"""
- txn = self.storeUnderTest().newTransaction()
+ txn = self.transactionUnderTest()
self.assertProvides(ICommonTransaction, txn)
self.assertProvides(IAddressBookTransaction, txn)
@@ -257,9 +269,8 @@
provides L{IAddressBookHome} and has a C{uid()} method that returns the
same value that was passed in.
"""
- addressbookHome = (self.storeUnderTest().newTransaction()
+ addressbookHome = (self.transactionUnderTest()
.addressbookHomeWithUID("home1"))
-
self.assertEquals(addressbookHome.uid(), "home1")
self.assertProvides(IAddressBookHome, addressbookHome)
@@ -269,11 +280,8 @@
L{IAddressBookStoreTransaction.addressbookHomeWithUID} should return C{None}
when asked for a non-existent addressbook home.
"""
- self.assertEquals(
- self.storeUnderTest().newTransaction()
- .addressbookHomeWithUID("xyzzy"),
- None
- )
+ txn = self.transactionUnderTest()
+ self.assertEquals(txn.addressbookHomeWithUID("xyzzy"), None)
def test_addressbookWithName_exists(self):
@@ -284,6 +292,8 @@
home = self.homeUnderTest()
for name in home1_addressbookNames:
addressbook = home.addressbookWithName(name)
+ if addressbook is None:
+ self.fail("addressbook %r didn't exist" % (name,))
self.assertProvides(IAddressBook, addressbook)
self.assertEquals(addressbook.name(), name)
@@ -334,18 +344,22 @@
addressbookProperties[
PropertyName.fromString(davxml.ResourceType.sname())
],
- davxml.ResourceType.addressbook) #@UndefinedVariable
+ davxml.ResourceType.addressbook
+ ) #@UndefinedVariable
checkProperties()
self.commit()
# Make sure notification fired after commit
- self.assertTrue(self.notifierFactory.compare([("update", "home1")]))
+ self.assertEquals(self.notifierFactory.history, [("update", "home1")])
# Make sure it's available in a new transaction; i.e. test the commit.
home = self.homeUnderTest()
self.assertNotIdentical(home.addressbookWithName(name), None)
- home = self.addressbookStore.newTransaction().addressbookHomeWithUID(
- "home1")
+
+ # FIXME: These two lines aren't in the calendar common tests:
+ # home = self.addressbookStore.newTransaction().addressbookHomeWithUID(
+ # "home1")
+
# Sanity check: are the properties actually persisted?
# FIXME: no independent testing of this right now
checkProperties()
@@ -379,8 +393,10 @@
self.commit()
# Make sure notification fired after commit
- self.assertTrue(self.notifierFactory.compare(
- [("update", "home1"), ("update", "home1"), ("update", "home1")]))
+ self.assertEquals(
+ self.notifierFactory.history,
+ [("update", "home1"), ("update", "home1"), ("update", "home1")]
+ )
def test_removeAddressBookWithName_absent(self):
@@ -408,8 +424,8 @@
)
self.assertEquals(
- list(o.name() for o in addressbookObjects),
- addressbook1_objectNames
+ set(o.name() for o in addressbookObjects),
+ set(addressbook1_objectNames)
)
@@ -494,17 +510,16 @@
# Make sure notifications are fired after commit
self.commit()
- self.assertTrue(
- self.notifierFactory.compare(
- [
- ("update", "home1"),
- ("update", "home1/addressbook_1"),
- ("update", "home1"),
- ("update", "home1/addressbook_1"),
- ("update", "home1"),
- ("update", "home1/addressbook_1"),
- ]
- )
+ self.assertEquals(
+ self.notifierFactory.history,
+ [
+ ("update", "home1"),
+ ("update", "home1/addressbook_1"),
+ ("update", "home1"),
+ ("update", "home1/addressbook_1"),
+ ("update", "home1"),
+ ("update", "home1/addressbook_1"),
+ ]
)
@@ -595,8 +610,8 @@
home.addressbookWithName(addressbook.name()))
self.assertEquals(
- list(c.name() for c in addressbooks),
- home1_addressbookNames
+ set(c.name() for c in addressbooks),
+ set(home1_addressbookNames)
)
@@ -629,13 +644,12 @@
self.commit()
# Make sure notifications fire after commit
- self.assertTrue(
- self.notifierFactory.compare(
- [
- ("update", "home1"),
- ("update", "home1/addressbook_1"),
- ]
- )
+ self.assertEquals(
+ self.notifierFactory.history,
+ [
+ ("update", "home1"),
+ ("update", "home1/addressbook_1"),
+ ]
)
@@ -704,12 +718,13 @@
create=True
)
def readOtherTxn():
- return self.savedStore.newTransaction().addressbookHomeWithUID(
- noHomeUID)
+ otherTxn = self.savedStore.newTransaction()
+ self.addCleanup(otherTxn.commit)
+ return otherTxn.addressbookHomeWithUID(noHomeUID)
self.assertProvides(IAddressBookHome, addressbookHome)
# A concurrent transaction shouldn't be able to read it yet:
self.assertIdentical(readOtherTxn(), None)
- txn.commit()
+ self.commit()
# But once it's committed, other transactions should see it.
self.assertProvides(IAddressBookHome, readOtherTxn())
@@ -735,13 +750,12 @@
self.commit()
# Make sure notification fired after commit
- self.assertTrue(
- self.notifierFactory.compare(
- [
- ("update", "home1"),
- ("update", "home1/addressbook_1"),
- ]
- )
+ self.assertEquals(
+ self.notifierFactory.history,
+ [
+ ("update", "home1"),
+ ("update", "home1/addressbook_1"),
+ ]
)
def checkPropertiesMethod(self, thunk):
@@ -841,10 +855,7 @@
return Notifier(self, label=label, id=id)
def send(self, op, id):
- self._history.append((op, id))
+ self.history.append((op, id))
def reset(self):
- self._history = []
-
- def compare(self, expected):
- return self._history == expected
+ self.history = []
Modified: CalendarServer/trunk/txcarddav/iaddressbookstore.py
===================================================================
--- CalendarServer/trunk/txcarddav/iaddressbookstore.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcarddav/iaddressbookstore.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,3 +1,4 @@
+# -*- test-case-name: txcarddav.addressbookstore,txcaldav.calendarstore.test.test_postgres.AddressBookSQLStorageTests -*-
##
# Copyright (c) 2010 Apple Inc. All rights reserved.
#
@@ -222,6 +223,13 @@
An addressbook object describes a contact (vCard).
"""
+ def addressbook():
+ """
+ @return: The address book which this address book object is a part of.
+ @rtype: L{IAddressBook}
+ """
+
+
def setComponent(component):
"""
Rewrite this addressbook object to match the given C{component}.
Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/common/datastore/file.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
+from txcaldav.calendarstore.util import SyncTokenHelper
"""
Common utility functions for a file based datastore.
@@ -26,7 +27,7 @@
from twisted.python.util import FancyEqMixin
from twistedcaldav import customxml
-from twistedcaldav.customxml import GETCTag, NotificationType
+from twistedcaldav.customxml import NotificationType
from twistedcaldav.notifications import NotificationRecord
from twistedcaldav.notifications import NotificationsDatabase as OldNotificationIndex
from twistedcaldav.sharing import SharedCollectionsDatabase
@@ -44,7 +45,6 @@
from txdav.propertystore.xattr import PropertyStore
from errno import EEXIST, ENOENT
-from uuid import uuid4
from zope.interface import implements, directlyProvides
ECALENDARTYPE = 0
@@ -475,8 +475,10 @@
return None
-class CommonHomeChild(FileMetaDataMixin, LoggingMixIn, FancyEqMixin):
+class CommonHomeChild(FileMetaDataMixin, LoggingMixIn, FancyEqMixin,
+ SyncTokenHelper):
"""
+ Common ancestor class of AddressBooks and Calendars.
"""
compareAttributes = '_name _home _transaction'.split()
@@ -678,26 +680,6 @@
raise NotImplementedError()
- def _updateSyncToken(self, reset=False):
- # FIXME: add locking a-la CalDAVResource.bumpSyncToken
- # FIXME: tests for desired concurrency properties
- ctag = PropertyName.fromString(GETCTag.sname())
- props = self.properties()
- token = props.get(ctag)
- if token is None or reset:
- tokenuuid = uuid4()
- revision = 1
- else:
- # FIXME: no direct tests for update
- token = str(token)
- tokenuuid, revision = token.split("#", 1)
- revision = int(revision) + 1
- token = "%s#%d" % (tokenuuid, revision)
- props[ctag] = GETCTag(token)
- # FIXME: no direct tests for commit
- return revision
-
-
def objectResourcesSinceToken(self, token):
raise NotImplementedError()
Modified: CalendarServer/trunk/txdav/common/inotifications.py
===================================================================
--- CalendarServer/trunk/txdav/common/inotifications.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/common/inotifications.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -78,9 +78,8 @@
given C{xmltype}. Create or overwrite are OK.
@param uid: a string.
- @param xmltype: a string.
+ @param xmltype: a custom XML class (not an instance)
@param xmldata: a string.
- @param component: a C{VCARD} L{Component}
"""
def removeNotificationObjectWithName(name):
Copied: CalendarServer/trunk/txdav/datastore/subpostgres.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txdav/datastore/subpostgres.py)
===================================================================
--- CalendarServer/trunk/txdav/datastore/subpostgres.py (rev 0)
+++ CalendarServer/trunk/txdav/datastore/subpostgres.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,445 @@
+# -*- test-case-name: txdav.datastore.test.test_subpostgres -*-
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+Run and manage PostgreSQL as a subprocess.
+"""
+import os
+import pwd
+from hashlib import md5
+
+from twisted.python.procutils import which
+from twisted.internet.protocol import ProcessProtocol
+from twisted.internet.error import ProcessDone
+from twisted.python.reflect import namedAny
+from twisted.python import log
+from twext.python.filepath import CachingFilePath
+
+
+pgdb = namedAny("pgdb")
+from pg import DatabaseError
+
+from twisted.protocols.basic import LineReceiver
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+
+from twisted.application.service import MultiService
+
+
+# This appears in the postgres log to indicate that it is accepting
+# connections.
+_MAGIC_READY_COOKIE = "database system is ready to accept connections"
+
+
+class DiagnosticCursorWrapper(object):
+ """
+ Diagnostic wrapper around a DB-API 2.0 cursor for debugging connection
+ status.
+ """
+
+ def __init__(self, realCursor, connectionWrapper):
+ self.realCursor = realCursor
+ self.connectionWrapper = connectionWrapper
+
+
+ @property
+ def rowcount(self):
+ return self.realCursor.rowcount
+
+
+ @property
+ def description(self):
+ return self.realCursor.description
+
+
+ def execute(self, sql, args=()):
+ self.connectionWrapper.state = 'executing %r' % (sql,)
+ self.realCursor.execute(sql, args)
+
+
+ def close(self):
+ self.realCursor.close()
+
+
+ def fetchall(self):
+ return self.realCursor.fetchall()
+
+
+
+class DiagnosticConnectionWrapper(object):
+ """
+ Diagnostic wrapper around a DB-API 2.0 connection for debugging connection
+ status.
+ """
+
+ def __init__(self, realConnection, label):
+ self.realConnection = realConnection
+ self.label = label
+ self.state = 'idle (start)'
+
+
+ def cursor(self):
+ return DiagnosticCursorWrapper(self.realConnection.cursor(), self)
+
+
+ def close(self):
+ self.realConnection.close()
+ self.state = 'closed'
+
+
+ def commit(self):
+ self.realConnection.commit()
+ self.state = 'idle (after commit)'
+
+
+ def rollback(self):
+ self.realConnection.rollback()
+ self.state = 'idle (after rollback)'
+
+
+
+class _PostgresMonitor(ProcessProtocol):
+ """
+ A monitoring protocol which watches the postgres subprocess.
+ """
+
+ def __init__(self, svc=None):
+ self.lineReceiver = LineReceiver()
+ self.lineReceiver.delimiter = '\n'
+ self.lineReceiver.lineReceived = self.lineReceived
+ self.svc = svc
+ self.isReady = False
+ self.completionDeferred = Deferred()
+
+
+ def lineReceived(self, line):
+ if self.svc is None:
+ return
+ if not self.isReady:
+ if _MAGIC_READY_COOKIE in line:
+ self.svc.ready()
+
+
+ disconnecting = False
+ def connectionMade(self):
+ self.lineReceiver.makeConnection(self)
+
+
+ def outReceived(self, out):
+ log.msg("received postgres stdout %r" % (out,))
+ # self.lineReceiver.dataReceived(out)
+
+
+ def errReceived(self, err):
+ log.msg("received postgres stderr %r" % (err,))
+ self.lineReceiver.dataReceived(err)
+
+
+ def processEnded(self, reason):
+ log.msg("postgres process ended %r" % (reason,))
+ self.lineReceiver.connectionLost(reason)
+ self.completionDeferred.callback(None)
+
+
+class ErrorOutput(Exception):
+ """
+ The process produced some error output and exited with a non-zero exit
+ code.
+ """
+
+
+class CapturingProcessProtocol(ProcessProtocol):
+ """
+ A L{ProcessProtocol} that captures its output and error.
+
+ @ivar output: a C{list} of all C{str}s received to stderr.
+
+ @ivar error: a C{list} of all C{str}s received to stderr.
+ """
+
+ def __init__(self, deferred, inputData):
+ """
+ Initialize a L{CapturingProcessProtocol}.
+
+ @param deferred: the L{Deferred} to fire when the process is complete.
+
+ @param inputData: a C{str} to feed to the subprocess's stdin.
+ """
+ self.deferred = deferred
+ self.input = inputData
+ self.output = []
+ self.error = []
+
+
+ def connectionMade(self):
+ """
+ The process started; feed its input on stdin.
+ """
+ if self.input is not None:
+ self.transport.write(self.input)
+ self.transport.closeStdin()
+
+
+ def outReceived(self, data):
+ """
+ Some output was received on stdout.
+ """
+ self.output.append(data)
+
+ def errReceived(self, data):
+ """
+ Some output was received on stderr.
+ """
+ self.output.append(data)
+
+
+ def processEnded(self, why):
+ """
+ The process is over, fire the Deferred with the output.
+ """
+ self.deferred.callback(''.join(self.output))
+
+
+class PostgresService(MultiService):
+
+ def __init__(self, dataStoreDirectory, subServiceFactory,
+ schema, databaseName='subpostgres', resetSchema=False,
+ logFile="postgres.log", testMode=False,
+ uid=None, gid=None):
+ """
+ Initialize a L{PostgresService} pointed at a data store directory.
+
+ @param dataStoreDirectory: the directory to
+ @type dataStoreDirectory: L{twext.python.filepath.CachingFilePath}
+
+ @param subServiceFactory: a 1-arg callable that will be called with a
+ 1-arg callable which returns a DB-API cursor.
+ @type subServiceFactory: C{callable}
+ """
+ MultiService.__init__(self)
+ self.subServiceFactory = subServiceFactory
+ self.dataStoreDirectory = dataStoreDirectory
+ self.resetSchema = resetSchema
+ self.socketDir = CachingFilePath("/tmp/ccs_postgres_%s/" %
+ (md5(dataStoreDirectory.path).hexdigest()))
+ self.databaseName = databaseName
+ self.logFile = logFile
+ self.uid = uid
+ self.gid = gid
+ self.schema = schema
+ self.monitor = None
+ self.openConnections = []
+
+ # FIXME: By default there is very little (4MB) shared memory available,
+ # so at the moment I am lowering these postgres config options to allow
+ # multiple servers to run. We might want to look into raising
+ # kern.sysv.shmmax.
+ # See: http://www.postgresql.org/docs/8.4/static/kernel-resources.html
+ if testMode:
+ self.sharedBuffers = 16
+ self.maxConnections = 2
+ else:
+ self.sharedBuffers = 30
+ self.maxConnections = 20
+
+
+ def produceConnection(self, label="<unlabeled>", databaseName=None):
+ """
+ Produce a DB-API 2.0 connection pointed at this database.
+ """
+ if databaseName is None:
+ databaseName = self.databaseName
+
+ if self.uid is not None:
+ dsn = "%s:dbname=%s:%s" % (self.socketDir.path, databaseName,
+ pwd.getpwuid(self.uid).pw_name)
+ else:
+ dsn = "%s:dbname=%s" % (self.socketDir.path, databaseName)
+ connection = pgdb.connect(dsn)
+
+ w = DiagnosticConnectionWrapper(connection, label)
+ c = w.cursor()
+ # Turn on standard conforming strings. This option is _required_ if
+ # you want to get correct behavior out of parameter-passing with the
+ # pgdb module. If it is not set then the server is potentially
+ # vulnerable to certain types of SQL injection.
+ c.execute("set standard_conforming_strings=on")
+
+ # Abort any second that takes more than 2 seconds (2000ms) to execute.
+ # This is necessary as a temporary workaround since it's hypothetically
+ # possible that different database operations could block each other,
+ # while executing SQL in the same process (in the same thread, since
+ # SQL executes in the main thread now). It's preferable to see some
+ # exceptions while we're in this state than to have the entire worker
+ # process hang.
+ c.execute("set statement_timeout=2000")
+ w.commit()
+ c.close()
+ return w
+
+
+ def ready(self):
+ """
+ Subprocess is ready. Time to initialize the subservice.
+ """
+ createDatabaseConn = self.produceConnection(
+ 'schema creation', 'postgres'
+ )
+ createDatabaseCursor = createDatabaseConn.cursor()
+ createDatabaseCursor.execute("commit")
+
+ if self.resetSchema:
+ try:
+ createDatabaseCursor.execute(
+ "drop database %s" % (self.databaseName)
+ )
+ except DatabaseError:
+ pass
+
+ try:
+ createDatabaseCursor.execute(
+ "create database %s" % (self.databaseName)
+ )
+ except:
+ execSchema = False
+ else:
+ execSchema = True
+
+ createDatabaseCursor.close()
+ createDatabaseConn.close()
+
+ if execSchema:
+ connection = self.produceConnection()
+ cursor = connection.cursor()
+ cursor.execute(self.schema)
+ connection.commit()
+ connection.close()
+
+ connection = self.produceConnection()
+ cursor = connection.cursor()
+
+ self.subServiceFactory(self.produceConnection).setServiceParent(self)
+
+
+ def pauseMonitor(self):
+ """
+ Pause monitoring. This is a testing hook for when (if) we are
+ continuously monitoring output from the 'postgres' process.
+ """
+# for pipe in self.monitor.transport.pipes.values():
+# pipe.stopReading()
+# pipe.stopWriting()
+
+
+ def unpauseMonitor(self):
+ """
+ Unpause monitoring.
+
+ @see: L{pauseMonitor}
+ """
+# for pipe in self.monitor.transport.pipes.values():
+# pipe.startReading()
+# pipe.startWriting()
+
+
+ def startDatabase(self):
+ """
+ Start the database and initialize the subservice.
+ """
+ monitor = _PostgresMonitor(self)
+ pg_ctl = which("pg_ctl")[0]
+ # check consistency of initdb and postgres?
+ reactor.spawnProcess(
+ monitor, pg_ctl,
+ [
+ pg_ctl,
+ "start",
+ "-l", self.logFile,
+ "-w",
+ # XXX what are the quoting rules for '-o'? do I need to repr()
+ # the path here?
+ "-o", "-c listen_addresses='' -k '%s' -c standard_conforming_strings=on -c shared_buffers=%d -c max_connections=%d"
+ % (self.socketDir.path, self.sharedBuffers, self.maxConnections),
+ ],
+ self.env,
+ uid=self.uid, gid=self.gid,
+ )
+ self.monitor = monitor
+ def gotReady(result):
+ self.ready()
+ def reportit(f):
+ log.err(f)
+ self.monitor.completionDeferred.addCallback(
+ gotReady).addErrback(reportit)
+
+
+ def startService(self):
+ MultiService.startService(self)
+ clusterDir = self.dataStoreDirectory.child("cluster")
+ workingDir = self.dataStoreDirectory.child("working")
+ env = self.env = os.environ.copy()
+ env.update(PGDATA=clusterDir.path,
+ PGHOST=self.socketDir.path)
+ initdb = which("initdb")[0]
+ if not self.socketDir.isdir():
+ self.socketDir.createDirectory()
+ if self.uid and self.gid:
+ os.chown(self.socketDir.path, self.uid, self.gid)
+ if self.dataStoreDirectory.isdir():
+ self.startDatabase()
+ else:
+ self.dataStoreDirectory.createDirectory()
+ workingDir.createDirectory()
+ if self.uid and self.gid:
+ os.chown(self.dataStoreDirectory.path, self.uid, self.gid)
+ os.chown(workingDir.path, self.uid, self.gid)
+ dbInited = Deferred()
+ reactor.spawnProcess(
+ CapturingProcessProtocol(dbInited, None),
+ initdb, [initdb], env, workingDir.path,
+ uid=self.uid, gid=self.gid,
+ )
+ def doCreate(result):
+ self.startDatabase()
+ dbInited.addCallback(doCreate)
+
+
+ def stopService(self):
+ """
+ Stop all child services, then stop the subprocess, if it's running.
+ """
+ d = MultiService.stopService(self)
+ def superStopped(result):
+ # Probably want to stop and wait for startup if that hasn't
+ # completed yet...
+ monitor = _PostgresMonitor()
+ pg_ctl = which("pg_ctl")[0]
+ reactor.spawnProcess(monitor, pg_ctl,
+ [pg_ctl, '-l', 'logfile', 'stop'],
+ self.env,
+ uid=self.uid, gid=self.gid,
+ )
+ return monitor.completionDeferred
+ return d.addCallback(superStopped)
+
+# def maybeStopSubprocess(result):
+# if self.monitor is not None:
+# self.monitor.transport.signalProcess("INT")
+# return self.monitor.completionDeferred
+# return result
+# d.addCallback(maybeStopSubprocess)
+# return d
Deleted: CalendarServer/trunk/txdav/datastore/test/__init__.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/__init__.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/datastore/test/__init__.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,16 +0,0 @@
-##
-# Copyright (c) 2010 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.
-##
-
Copied: CalendarServer/trunk/txdav/datastore/test/__init__.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/datastore/test/__init__.py (rev 0)
+++ CalendarServer/trunk/txdav/datastore/test/__init__.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2010 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.
+##
+
Deleted: CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/test_subpostgres.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,86 +0,0 @@
-##
-# Copyright (c) 2010 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.
-##
-
-"""
-Tests for txdav.datastore.subpostgres.
-"""
-
-from twisted.trial.unittest import TestCase
-
-from twext.python.filepath import CachingFilePath
-
-from txdav.datastore.subpostgres import PostgresService
-from twisted.internet.defer import inlineCallbacks, Deferred
-from twisted.application.service import Service
-
-class SubprocessStartup(TestCase):
- """
- Tests for starting and stopping the subprocess.
- """
-
- @inlineCallbacks
- def test_startService(self):
- """
- Assuming a properly configured environment ($PATH points at an 'initdb'
- and 'postgres', $PYTHONPATH includes pgdb), starting a
- L{PostgresService} will start the service passed to it, after executing
- the schema.
- """
-
- test = self
- class SimpleService(Service):
-
- instances = []
- rows = []
- ready = Deferred()
-
- def __init__(self, connectionFactory):
- self.connection = connectionFactory()
- test.addCleanup(self.connection.close)
- self.instances.append(self)
-
-
- def startService(self):
- cursor = self.connection.cursor()
- try:
- cursor.execute(
- "insert into test_dummy_table values ('dummy')"
- )
- except:
- self.ready.errback()
- else:
- self.ready.callback(None)
- finally:
- cursor.close()
-
-
- dbPath = "../_postgres_test_db"
- svc = PostgresService(
- CachingFilePath(dbPath),
- SimpleService,
- "create table TEST_DUMMY_TABLE (stub varchar)",
- "dummy_db",
- testMode=True
- )
-
- svc.startService()
- self.addCleanup(svc.stopService)
- yield SimpleService.ready
- connection = SimpleService.instances[0].connection
- cursor = connection.cursor()
- cursor.execute("select * from test_dummy_table")
- values = cursor.fetchall()
- self.assertEquals(values, [["dummy"]])
Copied: CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/test_subpostgres.py)
===================================================================
--- CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py (rev 0)
+++ CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,86 @@
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+Tests for txdav.datastore.subpostgres.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twext.python.filepath import CachingFilePath
+
+from txdav.datastore.subpostgres import PostgresService
+from twisted.internet.defer import inlineCallbacks, Deferred
+from twisted.application.service import Service
+
+class SubprocessStartup(TestCase):
+ """
+ Tests for starting and stopping the subprocess.
+ """
+
+ @inlineCallbacks
+ def test_startService(self):
+ """
+ Assuming a properly configured environment ($PATH points at an 'initdb'
+ and 'postgres', $PYTHONPATH includes pgdb), starting a
+ L{PostgresService} will start the service passed to it, after executing
+ the schema.
+ """
+
+ test = self
+ class SimpleService(Service):
+
+ instances = []
+ rows = []
+ ready = Deferred()
+
+ def __init__(self, connectionFactory):
+ self.connection = connectionFactory()
+ test.addCleanup(self.connection.close)
+ self.instances.append(self)
+
+
+ def startService(self):
+ cursor = self.connection.cursor()
+ try:
+ cursor.execute(
+ "insert into test_dummy_table values ('dummy')"
+ )
+ except:
+ self.ready.errback()
+ else:
+ self.ready.callback(None)
+ finally:
+ cursor.close()
+
+
+ dbPath = "../_postgres_test_db"
+ svc = PostgresService(
+ CachingFilePath(dbPath),
+ SimpleService,
+ "create table TEST_DUMMY_TABLE (stub varchar)",
+ "dummy_db",
+ testMode=True
+ )
+
+ svc.startService()
+ self.addCleanup(svc.stopService)
+ yield SimpleService.ready
+ connection = SimpleService.instances[0].connection
+ cursor = connection.cursor()
+ cursor.execute("select * from test_dummy_table")
+ values = cursor.fetchall()
+ self.assertEquals(values, [["dummy"]])
Modified: CalendarServer/trunk/txdav/idav.py
===================================================================
--- CalendarServer/trunk/txdav/idav.py 2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/idav.py 2010-08-12 02:36:27 UTC (rev 6074)
@@ -68,7 +68,7 @@
Property name.
"""
namespace = Attribute("Namespace")
- name = Attribute("Name")
+ name = Attribute("Name")
def toString():
"""
@@ -92,26 +92,20 @@
# FIXME: the type for values isn't quite right, there should be some more
# specific interface for that.
- def flush():
- """
- Write out any pending changes.
- """
- def abort():
- """
- Abort any pending changes.
- """
-
-
class IDataStore(Interface):
"""
An L{IDataStore} is a storage of some objects.
"""
- def newTransaction():
+ def newTransaction(label=None):
"""
Create a new transaction.
+
+ @param label: A label to assign to this transaction for diagnostic
+ purposes.
+ @type label: C{str}
@return: a new transaction which provides L{ITransaction}, as well as
sub-interfaces to request appropriate data objects.
@@ -124,7 +118,7 @@
"""
An L{IDataStoreResource} are the objects stored in an L{IDataStore}.
"""
-
+
def name():
"""
Identify the name of the object
@@ -175,7 +169,9 @@
@return: an L{IPropertyStore}.
"""
-
+
+
+
class ITransaction(Interface):
"""
Transaction that can be aborted and either succeeds or fails in
@@ -211,6 +207,16 @@
@param operation: a callable.
"""
+
+ def store():
+ """
+ The store that this transaction was initiated from.
+
+ @rtype: L{IDataStore}
+ """
+
+
+
class INotifier(Interface):
"""
Push notification interface
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100811/09001aa4/attachment-0001.html>
More information about the calendarserver-changes
mailing list