[CalendarServer-changes] [5459] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Fri Apr 9 12:20:33 PDT 2010


Revision: 5459
          http://trac.macosforge.org/projects/calendarserver/changeset/5459
Author:   cdaboo at apple.com
Date:     2010-04-09 12:20:32 -0700 (Fri, 09 Apr 2010)
Log Message:
-----------
WebDAV sync REPORT support for address book collections. Required some fixes to MKCOL stuff.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
    CalendarServer/trunk/twistedcaldav/index.py
    CalendarServer/trunk/twistedcaldav/method/delete_common.py
    CalendarServer/trunk/twistedcaldav/method/mkcol.py
    CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py
    CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/static.py
    CalendarServer/trunk/twistedcaldav/vcardindex.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/test/test_vcardindex.py

Modified: CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	2010-04-09 19:08:40 UTC (rev 5458)
+++ CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -426,7 +426,6 @@
                                                             destinationparent = newAddressBook,
                                                             vcard = vcard,
                                                             indexdestination = False,
-                                                            updatedestinationctag = False,
                                                             ).run()
                             except:
                                 self.log_info("Could not add record %s" % (record,))

Modified: CalendarServer/trunk/twistedcaldav/index.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/index.py	2010-04-09 19:08:40 UTC (rev 5458)
+++ CalendarServer/trunk/twistedcaldav/index.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -1018,8 +1018,8 @@
         if do_commit:
             self._db_commit()
             
-        # This is a deferred but we can't defer at this point...
-        self.resource.bumpSyncToken(True)
+        # Need new sync-token
+        self.resource.initSyncToken()
 
 class IndexSchedule (CalendarIndex):
     """
@@ -1133,5 +1133,5 @@
         if do_commit:
             self._db_commit()
             
-        # This is a deferred but we can't defer at this point...
-        self.resource.bumpSyncToken(True)
+        # Need new sync-token
+        self.resource.initSyncToken()

Modified: CalendarServer/trunk/twistedcaldav/method/delete_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/delete_common.py	2010-04-09 19:08:40 UTC (rev 5458)
+++ CalendarServer/trunk/twistedcaldav/method/delete_common.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -316,13 +316,10 @@
                 yield delresource.quotaSizeAdjust(self.request, -old_size)
     
             if response == responsecode.NO_CONTENT:
+                newrevision = (yield parent.bumpSyncToken())
                 index = parent.index()
-                index.deleteResource(delresource.fp.basename())
+                index.deleteResource(delresource.fp.basename(), newrevision)    
     
-                # Change CTag on the parent addressbook collection
-                yield parent.updateCTag()
-    
-    
         except MemcacheLockTimeoutError:
             raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use on the server." % (deluri,)))
     
@@ -366,7 +363,7 @@
         if wasShared:
             yield delresource.downgradeFromShare(self.request)
 
-        yield delresource.updateCTag()
+        yield delresource.bumpSyncToken()
         more_responses = (yield self.deleteResource(delresource, deluri, parent))
         
         if isinstance(more_responses, MultiStatusResponse):
@@ -427,6 +424,7 @@
             
         elif isCalendarCollectionResource(self.resource):
             response = (yield self.deleteCalendar(self.resource, self.resource_uri, self.parent))
+
         elif isAddressBookCollectionResource(self.parent):
             response = (yield self.deleteAddressBookResource(self.resource, self.resource_uri, self.parent))
 

Modified: CalendarServer/trunk/twistedcaldav/method/mkcol.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/mkcol.py	2010-04-09 19:08:40 UTC (rev 5458)
+++ CalendarServer/trunk/twistedcaldav/method/mkcol.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -131,7 +131,7 @@
             for property in properties:
                 if isinstance(property, davxml.ResourceType):
                     if rtype:
-                        error = "Multiple {DAV:}resource-type properties in MKCOL request body: %s" % (mkcol,)
+                        error = "Multiple {DAV:}resourcetype properties in MKCOL request body: %s" % (mkcol,)
                         log.err(error)
                         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error))
                     else:
@@ -141,18 +141,18 @@
                             elif property.childrenOfType(carddavxml.AddressBook):
                                 rtype = "addressbook"
             if not rtype:
-                error = "No {DAV:}resource-type property in MKCOL request body: %s" % (mkcol,)
+                error = "No {DAV:}resourcetype property in MKCOL request body: %s" % (mkcol,)
                 log.err(error)
                 raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error))
             elif rtype not in ("calendar", "addressbook"):
-                error = "{DAV:}resource-type property in MKCOL request body not supported: %s" % (mkcol,)
+                error = "{DAV:}resourcetype property in MKCOL request body not supported: %s" % (mkcol,)
                 log.err(error)
                 raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error))
                 
             # Make sure feature is enabled
             if (rtype == "calendar" and not config.EnableCalDAV or
                 rtype == "addressbook" and not config.EnableCardDAV):
-                error = "{DAV:}resource-type property in MKCOL request body not supported: %s" % (mkcol,)
+                error = "{DAV:}resourcetype property in MKCOL request body not supported: %s" % (mkcol,)
                 log.err(error)
                 raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, error))
             

Modified: CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py	2010-04-09 19:08:40 UTC (rev 5458)
+++ CalendarServer/trunk/twistedcaldav/method/put_addressbook_common.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -179,7 +179,6 @@
         destination=None, destination_uri=None, destinationparent=None, destinationadbk=True,
         vcard=None,
         indexdestination = True,
-        updatedestinationctag = True,
    ):
         """
         Function that does common PUT/COPY/MOVE behavior.
@@ -232,13 +231,10 @@
         self.vcarddata = None
         self.deletesource = deletesource
         self.indexdestination = indexdestination
-        self.updatedestinationctag = updatedestinationctag
-        #self.isiTIP = isiTIP
-        #self.allowImplicitSchedule = allowImplicitSchedule
-        #self.internal_request = internal_request
         
         self.rollback = None
         self.access = None
+        self.newrevision = None
 
     def fullValidation(self):
         """
@@ -502,7 +498,8 @@
     def doSourceDelete(self):
         # Delete index for original item
         if self.sourceadbk:
-            self.source_index.deleteResource(self.source.fp.basename())
+            self.newrevision = (yield self.sourceparent.bumpSyncToken())
+            self.source_index.deleteResource(self.source.fp.basename(), self.newrevision)
             self.rollback.source_index_deleted = True
             log.debug("Source index removed %s" % (self.source.fp.path,))
 
@@ -511,14 +508,14 @@
         self.rollback.source_deleted = True
         log.debug("Source removed %s" % (self.source.fp.path,))
 
+        returnValue(None)
+
+    @inlineCallbacks
+    def doSourceQuotaCheck(self):
         # Update quota
         if self.sourcequota is not None:
             delete_size = 0 - self.old_source_size
             yield self.source.quotaSizeAdjust(self.request, delete_size)
-
-        # Change CTag on the parent vcard collection
-        if self.sourceadbk:
-            yield self.sourceparent.updateCTag()
   
         returnValue(None)
 
@@ -546,7 +543,7 @@
         
         # Add or update the index for this resource.
         try:
-            self.source_index.addResource(self.source.fp.basename(), self.vcard)
+            self.source_index.addResource(self.source.fp.basename(), self.vcard, self.newrevision)
         except TooManyInstancesError, ex:
             raise HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
@@ -569,7 +566,7 @@
         
         # Add or update the index for this resource.
         try:
-            self.destination_index.addResource(self.destination.fp.basename(), vcardtoindex)
+            self.destination_index.addResource(self.destination.fp.basename(), vcardtoindex, self.newrevision)
             log.debug("Destination indexed %s" % (self.destination.fp.path,))
         except (ValueError, TypeError), ex:
             log.err("Cannot index vcard resource: %s" % (ex,))
@@ -585,7 +582,7 @@
         
         # Delete index for original item
         if self.destinationadbk:
-            self.destination_index.deleteResource(self.destination.fp.basename())
+            self.destination_index.deleteResource(self.destination.fp.basename(), None)
             self.rollback.destination_index_deleted = True
             log.debug("Destination index removed %s" % (self.destination.fp.path,))
 
@@ -649,19 +646,20 @@
     
             # Index the new resource if storing to a vcard.
             if self.destinationadbk:
+                self.newrevision = (yield self.destinationparent.bumpSyncToken())
                 result = self.doDestinationIndex(self.vcard)
                 if result is not None:
                     self.rollback.Rollback()
                     returnValue(result)
     
+            # Delete the original source if needed.
+            if self.deletesource:
+                yield self.doSourceQuotaCheck()
+
             # Do quota check on destination
             if self.destquota is not None:
                 yield self.doDestinationQuotaCheck()
     
-            if self.destinationadbk and self.updatedestinationctag:
-                # Change CTag on the parent vcard collection
-                yield self.destinationparent.updateCTag()
-    
             # Can now commit changes and forget the rollback details
             self.rollback.Commit()
     

Modified: CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py	2010-04-09 19:08:40 UTC (rev 5458)
+++ CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -46,8 +46,8 @@
     """
     Generate a sync-collection REPORT.
     """
-    if not self.isPseudoCalendarCollection() or not config.EnableSyncReport:
-        log.err("sync-collection report is only allowed on calendar/inbox collection resources %s" % (self,))
+    if not self.isPseudoCalendarCollection() and not self.isAddressBookCollection() or not config.EnableSyncReport:
+        log.err("sync-collection report is only allowed on calendar/inbox/addressbook collection resources %s" % (self,))
         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, davxml.SupportedReport()))
    
     responses = []

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2010-04-09 19:08:40 UTC (rev 5458)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -436,6 +436,11 @@
                     # Remove share
                     yield self.downgradeFromShare(request)
                 returnValue(None)
+            else:
+                # resourcetype cannot be changed but we will allow it to be set to the same value
+                currentType = (yield self.resourceType(request))
+                if currentType == property:
+                    returnValue(None)
 
         result = (yield super(CalDAVResource, self).writeProperty(property, request))
         returnValue(result)
@@ -912,8 +917,8 @@
         if config.EnableCardDAV:
             result.append(davxml.Report(carddavxml.AddressBookQuery(),))
             result.append(davxml.Report(carddavxml.AddressBookMultiGet(),))
-        if self.isPseudoCalendarCollection() and config.EnableSyncReport:
-            # Only allowed on calendar/inbox collections
+        if (self.isPseudoCalendarCollection() or self.isAddressBookCollection()) and config.EnableSyncReport:
+            # Only allowed on calendar/inbox/addressbook collections
             result.append(davxml.Report(SyncCollection(),))
         return result
 

Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py	2010-04-09 19:08:40 UTC (rev 5458)
+++ CalendarServer/trunk/twistedcaldav/static.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -398,7 +398,7 @@
                 raise HTTPError(status)
 
             # Initialize CTag on the address book collection
-            d1 = self.updateCTag()
+            d1 = self.bumpSyncToken()
 
             # Create the index so its ready when the first PUTs come in
             d1.addCallback(lambda _: self.index().create())
@@ -507,7 +507,7 @@
         return changed, removed, current_token
 
     @inlineCallbacks
-    def bumpSyncToken(self, reset=False):
+    def bumpSyncToken(self):
         """
         Increment the sync-token which is also the ctag.
         
@@ -524,8 +524,6 @@
                 raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use on the server." % (self.uri,)))
 
             try:
-                if reset:
-                    raise ValueError
                 token = str(self.readDeadProperty(customxml.GETCTag))
                 caluuid, revision = token.split("#", 1)
                 revision = int(revision) + 1
@@ -542,6 +540,21 @@
         finally:
             yield lock.clean()
 
+    def initSyncToken(self):
+        """
+        Create a new sync-token which is also the ctag.
+        """
+        assert self.isCollection()
+        
+        # Initialise it
+        caluuid = uuid4()
+        revision = 1
+        token = "%s#%d" % (caluuid, revision,)
+        try:
+            self.writeDeadProperty(customxml.GETCTag(token))
+        except:
+            return fail(Failure())
+
     def updateCTag(self, token=None):
         assert self.isCollection()
         

Added: CalendarServer/trunk/twistedcaldav/test/test_vcardindex.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_vcardindex.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_vcardindex.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -0,0 +1,184 @@
+##
+# 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.
+##
+
+from twisted.internet import reactor
+from twisted.internet.task import deferLater
+
+from twistedcaldav.test.util import InMemoryMemcacheProtocol
+from twistedcaldav.vcard import Component
+from twistedcaldav.vcardindex import AddressBookIndex, MemcachedUIDReserver, ReservationError
+import twistedcaldav.test.util
+
+import os
+
+class SQLIndexTests (twistedcaldav.test.util.TestCase):
+    """
+    Test abstract SQL DB class
+    """
+
+    def setUp(self):
+        super(SQLIndexTests, self).setUp()
+        self.site.resource.isAddressBookCollection = lambda: True
+        self.db = AddressBookIndex(self.site.resource)
+
+
+    def test_reserve_uid_ok(self):
+        uid = "test-test-test"
+        d = self.db.isReservedUID(uid)
+        d.addCallback(self.assertFalse)
+        d.addCallback(lambda _: self.db.reserveUID(uid))
+        d.addCallback(lambda _: self.db.isReservedUID(uid))
+        d.addCallback(self.assertTrue)
+        d.addCallback(lambda _: self.db.unreserveUID(uid))
+        d.addCallback(lambda _: self.db.isReservedUID(uid))
+        d.addCallback(self.assertFalse)
+
+        return d
+
+
+    def test_reserve_uid_twice(self):
+        uid = "test-test-test"
+        d = self.db.reserveUID(uid)
+        d.addCallback(lambda _: self.db.isReservedUID(uid))
+        d.addCallback(self.assertTrue)
+        d.addCallback(lambda _:
+                      self.assertFailure(self.db.reserveUID(uid),
+                                         ReservationError))
+        return d
+
+
+    def test_unreserve_unreserved(self):
+        uid = "test-test-test"
+        return self.assertFailure(self.db.unreserveUID(uid),
+                                  ReservationError)
+
+
+    def test_reserve_uid_timeout(self):
+        # WARNING: This test is fundamentally flawed and will fail
+        # intermittently because it uses the real clock.
+        uid = "test-test-test"
+        from twistedcaldav.config import config
+        old_timeout = config.UIDReservationTimeOut
+        config.UIDReservationTimeOut = 1
+
+        def _finally():
+            config.UIDReservationTimeOut = old_timeout
+
+        d = self.db.isReservedUID(uid)
+        d.addCallback(self.assertFalse)
+        d.addCallback(lambda _: self.db.reserveUID(uid))
+        d.addCallback(lambda _: self.db.isReservedUID(uid))
+        d.addCallback(self.assertTrue)
+        d.addCallback(lambda _: deferLater(reactor, 2, lambda: None))
+        d.addCallback(lambda _: self.db.isReservedUID(uid))
+        d.addCallback(self.assertFalse)
+        self.addCleanup(_finally)
+
+        return d
+
+
+    def test_index(self):
+        data = (
+            (
+                "#1.1 Simple component",
+                "1.1",
+                """BEGIN:VCARD
+VERSION:3.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+UID:12345-67890-1.1
+FN:Cyrus Daboo
+N:Daboo;Cyrus
+EMAIL;TYPE=INTERNET,PREF:cyrus at example.com
+END:VCARD
+""",
+            ),
+        )
+
+        revision = 0
+        for description, name, vcard_txt in data:
+            revision += 1
+            calendar = Component.fromString(vcard_txt)
+            f = open(os.path.join(self.site.resource.fp.path, name), "w")
+            f.write(vcard_txt)
+            del f
+
+            self.db.addResource(name, calendar, revision)
+            self.assertTrue(self.db.resourceExists(name), msg=description)
+
+        self.db._db_recreate()
+        for description, name, vcard_txt in data:
+            self.assertTrue(self.db.resourceExists(name), msg=description)
+
+    def test_index_revisions(self):
+        data1 = """BEGIN:VCARD
+VERSION:3.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+UID:12345-67890-1-1.1
+FN:Cyrus Daboo
+N:Daboo;Cyrus
+EMAIL;TYPE=INTERNET,PREF:cyrus at example.com
+END:VCARD
+"""
+        data2 = """BEGIN:VCARD
+VERSION:3.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+UID:12345-67890-2-1.1
+FN:Wilfredo Sanchez
+N:Sanchez;Wilfredo
+EMAIL;TYPE=INTERNET,PREF:wsanchez at example.com
+END:VCARD
+"""
+        data3 = """BEGIN:VCARD
+VERSION:3.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+UID:12345-67890-3-1.1
+FN:Bruce Gaya
+N:Gaya;Bruce
+EMAIL;TYPE=INTERNET,PREF:bruce at example.com
+END:VCARD
+"""
+
+        vcard = Component.fromString(data1)
+        self.db.addResource("data1.vcf", vcard, 1)
+        vcard = Component.fromString(data2)
+        self.db.addResource("data2.vcf", vcard, 2)
+        vcard = Component.fromString(data3)
+        self.db.addResource("data3.vcf", vcard, 3)
+        self.db.deleteResource("data3.vcf", 4)
+
+        tests = (
+            (0, (["data1.vcf", "data2.vcf",], [],)),
+            (1, (["data2.vcf",], [],)),
+            (2, ([], [],)),
+            (3, ([], ["data3.vcf",],)),
+            (4, ([], [],)),
+            (5, ([], [],)),
+        )
+        
+        for revision, results in tests:
+            self.assertEquals(self.db.whatchanged(revision), results, "Mismatched results for whatchanged with revision %d" % (revision,))
+
+class MemcacheTests(SQLIndexTests):
+    def setUp(self):
+        super(MemcacheTests, self).setUp()
+        self.memcache = InMemoryMemcacheProtocol()
+        self.db.reserver = MemcachedUIDReserver(self.db, self.memcache)
+
+
+    def tearDown(self):
+        for _ignore_k, v in self.memcache._timeouts.iteritems():
+            if v.active():
+                v.cancel()

Modified: CalendarServer/trunk/twistedcaldav/vcardindex.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/vcardindex.py	2010-04-09 19:08:40 UTC (rev 5458)
+++ CalendarServer/trunk/twistedcaldav/vcardindex.py	2010-04-09 19:20:32 UTC (rev 5459)
@@ -38,6 +38,7 @@
 from twisted.internet.defer import maybeDeferred
 
 from twistedcaldav import carddavxml
+from twistedcaldav.index import SyncTokenValidException
 from twistedcaldav.query import addressbookquery
 from twistedcaldav.sql import AbstractSQLDatabase
 from twistedcaldav.sql import db_prefix
@@ -50,7 +51,7 @@
 log = Logger()
 
 db_basename = db_prefix + "sqlite"
-schema_version = "1"
+schema_version = "2"
 
 class ReservationError(LookupError):
     """
@@ -296,7 +297,7 @@
             if name is not None and self.resource.getChild(name_utf8) is None:
                 # Clean up
                 log.err("Stale resource record found for child %s with UID %s in %s" % (name, uid, self.resource))
-                self._delete_from_db(name, uid)
+                self._delete_from_db(name, uid, None)
                 self._db_commit()
             else:
                 resources.append(name_utf8)
@@ -328,7 +329,7 @@
 
         return uid
 
-    def addResource(self, name, vcard, fast=False):
+    def addResource(self, name, vcard, revision, fast=False):
         """
         Adding or updating an existing resource.
         To check for an update we attempt to get an existing UID
@@ -341,12 +342,12 @@
         """
         oldUID = self.resourceUIDForName(name)
         if oldUID is not None:
-            self._delete_from_db(name, oldUID)
-        self._add_to_db(name, vcard)
+            self._delete_from_db(name, oldUID, None)
+        self._add_to_db(name, vcard, revision)
         if not fast:
             self._db_commit()
 
-    def deleteResource(self, name):
+    def deleteResource(self, name, revision):
         """
         Remove this resource from the index.
         @param name: the name of the resource to add.
@@ -354,7 +355,7 @@
         """
         uid = self.resourceUIDForName(name)
         if uid is not None:
-            self._delete_from_db(name, uid)
+            self._delete_from_db(name, uid, revision)
             self._db_commit()
     
     def resourceExists(self, name):
@@ -381,6 +382,26 @@
         results = self._db_values_for_sql(statement, *names)
         return results
     
+    def whatchanged(self, revision):
+
+        results = [(name.encode("utf-8"), created, wasdeleted) for name, created, wasdeleted in self._db_execute("select NAME, CREATEDREVISION, WASDELETED from REVISIONS where REVISION > :1", revision)]
+        results.sort(key=lambda x:x[1])
+        
+        changed = []
+        deleted = []
+        for name, created, wasdeleted in results:
+            if name:
+                if wasdeleted == 'Y':
+                    # Don't report items that were created/deleted since the requested revision
+                    if created <= revision:
+                        deleted.append(name)
+                else:
+                    changed.append(name)
+            else:
+                raise SyncTokenValidException
+        
+        return changed, deleted,
+
     def searchValid(self, filter):
         if isinstance(filter, carddavxml.Filter):
             qualifiers = addressbookquery.sqladdressbookquery(filter)
@@ -418,7 +439,7 @@
             else:
                 log.err("vCard resource %s is missing from %s. Removing from index."
                         % (name, self.resource))
-                self.deleteResource(name)
+                self.deleteResource(name, None)
 
     def _db_version(self):
         """
@@ -461,6 +482,28 @@
         )
 
         #
+        # REVISIONS table tracks changes
+        #   NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key)
+        #   REVISION: revision number
+        #   WASDELETED: Y if revision deleted, N if added or changed
+        #
+        q.execute(
+            """
+            create table REVISIONS (
+                NAME            text unique,
+                REVISION        integer default 0,
+                CREATEDREVISION integer default 0,
+                WASDELETED      text(1) default "N"
+            )
+            """
+        )
+        q.execute(
+            """
+            create index REVISION on REVISIONS (REVISION)
+            """
+        )
+
+        #
         # RESERVED table tracks reserved UIDs
         #   UID: The UID being reserved
         #   TIME: When the reservation was made
@@ -504,7 +547,7 @@
                     log.err("Non-addressbook resource: %s" % (name,))
                 else:
                     #log.msg("Indexing resource: %s" % (name,))
-                    self.addResource(name, vcard, True)
+                    self.addResource(name, vcard, 0, True)
             finally:
                 stream.close()
         
@@ -512,8 +555,50 @@
         if do_commit:
             self._db_commit()
 
-    def _add_to_db(self, name, vcard, cursor = None):
+        # Need sync-token
+        self.resource.initSyncToken()
+
+    def _db_can_upgrade(self, old_version):
         """
+        Can we do an in-place upgrade
+        """
+        
+        # v2 is a minor change
+        return True
+
+    def _db_upgrade_data_tables(self, q, old_version):
+        """
+        Upgrade the data from an older version of the DB.
+        """
+
+        # When going to version 2+ all we need to do is add revision table and index
+        if old_version < 2:
+            q.execute(
+                """
+                create table REVISIONS (
+                    NAME            text unique,
+                    REVISION        integer default 0,
+                    CREATEDREVISION integer default 0,
+                    WASDELETED      text(1) default "N"
+                )
+                """
+            )
+            q.execute(
+                """
+                create index REVISION on REVISIONS (REVISION)
+                """
+            )
+            
+            self._db_execute(
+                """
+                insert into REVISIONS (NAME)
+                select NAME from RESOURCE
+                """
+            )
+                
+
+    def _add_to_db(self, name, vcard, revision, cursor = None):
+        """
         Records the given address book resource in the index with the given name.
         Resource names and UIDs must both be unique; only one resource name may
         be associated with any given UID and vice versa.
@@ -531,11 +616,29 @@
             values (:1, :2)
             """, name, uid,
         )
+
+        if revision is not None:
+            created = self._db_value_for_sql("select CREATEDREVISION from REVISIONS where NAME = :1", name)
+            if created is None:
+                created = revision
+            self._db_execute(
+                """
+                insert or replace into REVISIONS (NAME, REVISION, CREATEDREVISION, WASDELETED)
+                values (:1, :2, :3, :4)
+                """, name, revision, revision, 'N',
+            )
     
-    def _delete_from_db(self, name, uid):
+    def _delete_from_db(self, name, uid, revision):
         """
         Deletes the specified entry from all dbs.
         @param name: the name of the resource to delete.
         @param uid: the uid of the resource to delete.
         """
         self._db_execute("delete from RESOURCE where NAME = :1", name)
+        if revision is not None:
+            self._db_execute(
+                """
+                update REVISIONS SET REVISION = :1, WASDELETED = :2
+                where NAME = :3
+                """, revision, 'Y', name
+            )
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100409/88a96666/attachment-0001.html>


More information about the calendarserver-changes mailing list