[CalendarServer-changes] [10031] CalendarServer/branches/users/cdaboo/managed-attachments

source_changes at macosforge.org source_changes at macosforge.org
Wed Nov 14 14:15:21 PST 2012


Revision: 10031
          http://trac.calendarserver.org//changeset/10031
Author:   cdaboo at apple.com
Date:     2012-11-14 14:15:21 -0800 (Wed, 14 Nov 2012)
Log Message:
-----------
Further trimming down of the schema together with attachment update support.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/file.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/common.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_util.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_12_to_13.sql
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_tables.py

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py	2012-11-14 22:15:21 UTC (rev 10031)
@@ -1682,11 +1682,7 @@
 
 
     def displayName(self):
-        if self._newStoreObject is not None:
-            dispositionName = self._newStoreObject.dispositionName()
-            return dispositionName if dispositionName else self.name()
-        else:
-            return self._name
+        return self.name()
 
 
     @requiresPermissions(davxml.WriteContent())

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/file.py	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/file.py	2012-11-14 22:15:21 UTC (rev 10031)
@@ -50,7 +50,7 @@
 from txdav.caldav.icalendarstore import ICalendar, ICalendarObject
 from txdav.caldav.icalendarstore import ICalendarHome
 
-from txdav.caldav.datastore.index_file import Index as OldIndex,\
+from txdav.caldav.datastore.index_file import Index as OldIndex, \
     IndexSchedule as OldInboxIndex
 from txdav.caldav.datastore.util import (
     validateCalendarComponent, dropboxIDFromCalendarObject, CalendarObjectBase,
@@ -90,7 +90,6 @@
 
         self._childClass = Calendar
 
-
     createCalendarWithName = CommonHome.createChildWithName
     removeCalendarWithName = CommonHome.removeChildWithName
 
@@ -129,7 +128,7 @@
 
     @inlineCallbacks
     def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, type):
-        
+
         objectResources = (yield self.objectResourcesWithUID(uid, ("inbox",)))
         for objectResource in objectResources:
             if ok_object and objectResource._path == ok_object._path:
@@ -137,20 +136,22 @@
             matched_type = "schedule" if objectResource.isScheduleObject else "calendar"
             if type == "schedule" or matched_type == "schedule":
                 returnValue(True)
-            
+
         returnValue(False)
 
+
     @inlineCallbacks
     def getCalendarResourcesForUID(self, uid, allow_shared=False):
-        
+
         results = []
         objectResources = (yield self.objectResourcesWithUID(uid, ("inbox",)))
         for objectResource in objectResources:
             if allow_shared or objectResource._parentCollection.owned():
                 results.append(objectResource)
-            
+
         returnValue(results)
 
+
     @inlineCallbacks
     def calendarObjectWithDropboxID(self, dropboxID):
         """
@@ -186,18 +187,19 @@
         defaultCal = self.createCalendarWithName("calendar")
         props = defaultCal.properties()
         props[PropertyName(*ScheduleCalendarTransp.qname())] = ScheduleCalendarTransp(Opaque())
-        
+
         # Check whether components type must be separate
         if config.RestrictCalendarsToOneComponentType:
             defaultCal.setSupportedComponents("VEVENT")
-            
+
             # Default tasks
             defaultTasks = self.createCalendarWithName("tasks")
             props = defaultTasks.properties()
             defaultTasks.setSupportedComponents("VTODO")
-            
+
         self.createCalendarWithName("inbox")
 
+
     def ensureDefaultCalendarsExist(self):
         """
         Double check that we have calendars supporting at least VEVENT and VTODO,
@@ -222,10 +224,12 @@
                         newname = str(uuid.uuid4())
                     newcal = self.createCalendarWithName(newname)
                     newcal.setSupportedComponents(support_component)
-            
+
             _requireCalendarWithType("VEVENT", "calendar")
             _requireCalendarWithType("VTODO", "tasks")
 
+
+
 class Calendar(CommonHomeChild):
     """
     File-based implementation of L{ICalendar}.
@@ -270,7 +274,6 @@
         # TODO: implement me.
         raise NotImplementedError()
 
-
     ownerCalendarHome = CommonHomeChild.ownerHome
     viewerCalendarHome = CommonHomeChild.viewerHome
     calendarObjects = CommonHomeChild.objectResources
@@ -292,17 +295,19 @@
         Update the private property with the supported components. Technically this should only happen once
         on collection creation, but for migration we may need to change after the fact - hence a separate api.
         """
-        
+
         pname = PropertyName.fromElement(customxml.TwistedCalendarSupportedComponents)
         if supported_components:
             self.properties()[pname] = customxml.TwistedCalendarSupportedComponents.fromString(supported_components)
         elif pname in self.properties():
             del self.properties()[pname]
 
+
     def getSupportedComponents(self):
         result = str(self.properties().get(PropertyName.fromElement(customxml.TwistedCalendarSupportedComponents), ""))
         return result if result else None
 
+
     def isSupportedComponent(self, componentType):
         supported = self.getSupportedComponents()
         if supported:
@@ -310,6 +315,7 @@
         else:
             return True
 
+
     def initPropertyStore(self, props):
         # Setup peruser special properties
         props.setSpecialProperties(
@@ -323,61 +329,69 @@
             ),
         )
 
+
     def contentType(self):
         """
         The content type of Calendar objects is text/calendar.
         """
         return MimeType.fromString("text/calendar; charset=utf-8")
 
+
     def splitCollectionByComponentTypes(self):
         """
         If the calendar contains iCalendar data with different component types, then split it into separate collections
         each containing only one component type. When doing this make sure properties and sharing state are preserved
         on any new calendars created.
         """
-        
+
         # TODO: implement this for filestore
         pass
 
+
     def _countComponentTypes(self):
         """
         Count each component type in this calendar.
-        
-        @return: a C{tuple} of C{tuple} containing the component type name and count. 
+
+        @return: a C{tuple} of C{tuple} containing the component type name and count.
         """
 
         rows = self._index._oldIndex.componentTypeCounts()
         result = tuple([(componentType, componentCount) for componentType, componentCount in sorted(rows, key=lambda x:x[0])])
         return result
 
+
     def _splitComponentType(self, component):
         """
         Create a new calendar and move all components of the specified component type into the new one.
         Make sure properties and sharing state is preserved on the new calendar.
-        
+
         @param component: Component type to split out
         @type component: C{str}
         """
-        
+
         # TODO: implement this for filestore
         pass
 
+
     def _transferSharingDetails(self, newcalendar, component):
         """
         If the current calendar is shared, make the new calendar shared in the same way, but tweak the name.
         """
-        
+
         # TODO: implement this for filestore
         pass
-    
+
+
     def _transferCalendarObjects(self, newcalendar, component):
         """
         Move all calendar components of the specified type to the specified calendar.
         """
-        
+
         # TODO: implement this for filestore
         pass
 
+
+
 class CalendarObject(CommonObjectResource, CalendarObjectBase):
     """
     @ivar _path: The path of the .ics file on disk
@@ -389,7 +403,7 @@
     def __init__(self, name, calendar, metadata=None):
         super(CalendarObject, self).__init__(name, calendar)
         self._attachments = {}
-        
+
         if metadata is not None:
             self.accessMode = metadata.get("accessMode", "")
             self.isScheduleObject = metadata.get("isScheduleObject", False)
@@ -428,7 +442,7 @@
             if self._path.exists():
                 backup = hidden(self._path.temporarySibling())
                 self._path.moveTo(backup)
-            
+
             fh = self._path.open("w")
             try:
                 # FIXME: concurrency problem; if this write is interrupted
@@ -476,7 +490,7 @@
 
         if unfixed:
             self.log_error("Calendar data at %s had unfixable problems:\n  %s" % (self._path.path, "\n  ".join(unfixed),))
-        
+
         if fixed:
             self.log_error("Calendar data at %s had fixable problems:\n  %s" % (self._path.path, "\n  ".join(fixed),))
 
@@ -516,35 +530,41 @@
                     "File corruption detected (improper start) in file: %s"
                     % (self._path.path,)
                 )
-        
+
         self._objectText = text
         return text
 
+
     def uid(self):
         if not hasattr(self, "_uid"):
             self._uid = self.component().resourceUID()
         return self._uid
 
+
     def componentType(self):
         if not hasattr(self, "_componentType"):
             self._componentType = self.component().mainType()
         return self._componentType
 
+
     def organizer(self):
         return self.component().getOrganizer()
 
+
     def getMetadata(self):
         metadata = {}
-        metadata["accessMode"] = self.accessMode 
+        metadata["accessMode"] = self.accessMode
         metadata["isScheduleObject"] = self.isScheduleObject
         metadata["scheduleTag"] = self.scheduleTag
         metadata["scheduleEtags"] = self.scheduleEtags
         metadata["hasPrivateComment"] = self.hasPrivateComment
         return metadata
 
+
     def _get_accessMode(self):
         return str(self.properties().get(PropertyName.fromElement(customxml.TwistedCalendarAccessProperty), ""))
 
+
     def _set_accessMode(self, value):
         pname = PropertyName.fromElement(customxml.TwistedCalendarAccessProperty)
         if value:
@@ -564,6 +584,7 @@
             prop = str(prop) == "true"
         return prop
 
+
     def _set_isScheduleObject(self, value):
         pname = PropertyName.fromElement(customxml.TwistedSchedulingObjectResource)
         if value is not None:
@@ -576,6 +597,7 @@
     def _get_scheduleTag(self):
         return str(self.properties().get(PropertyName.fromElement(caldavxml.ScheduleTag), ""))
 
+
     def _set_scheduleTag(self, value):
         pname = PropertyName.fromElement(caldavxml.ScheduleTag)
         if value:
@@ -588,6 +610,7 @@
     def _get_scheduleEtags(self):
         return tuple([str(etag) for etag in self.properties().get(PropertyName.fromElement(customxml.TwistedScheduleMatchETags), customxml.TwistedScheduleMatchETags()).children])
 
+
     def _set_scheduleEtags(self, value):
         if value:
             etags = [davxml.GETETag.fromString(etag) for etag in value]
@@ -603,6 +626,7 @@
     def _get_hasPrivateComment(self):
         return PropertyName.fromElement(customxml.TwistedCalendarHasPrivateCommentsProperty) in self.properties()
 
+
     def _set_hasPrivateComment(self, value):
         pname = PropertyName.fromElement(customxml.TwistedCalendarHasPrivateCommentsProperty)
         if value:
@@ -612,6 +636,19 @@
 
     hasPrivateComment = property(_get_hasPrivateComment, _set_hasPrivateComment)
 
+
+    def addAttachment(self, pathpattern, rids, content_type, filename, stream):
+        raise NotImplementedError
+
+
+    def updateAttachment(self, pathpattern, managed_id, content_type, filename, stream):
+        raise NotImplementedError
+
+
+    def removeAttachment(self, rids, managed_id):
+        raise NotImplementedError
+
+
     @inlineCallbacks
     def createAttachmentWithName(self, name):
         """
@@ -707,6 +744,7 @@
             ),
         )
 
+
     # IDataStoreObject
     def contentType(self):
         """
@@ -718,7 +756,7 @@
 
 class AttachmentStorageTransport(StorageTransportBase):
 
-    def __init__(self, attachment, contentType):
+    def __init__(self, attachment, contentType, dispositionName):
         """
         Initialize this L{AttachmentStorageTransport} and open its file for
         writing.
@@ -727,7 +765,7 @@
         @type attachment: L{Attachment}
         """
         super(AttachmentStorageTransport, self).__init__(
-            attachment, contentType)
+            attachment, contentType, dispositionName)
         self._path = self._attachment._path.temporarySibling()
         self._file = self._path.open("w")
 
@@ -795,8 +833,8 @@
         return propStoreClass(uid, lambda: self._path)
 
 
-    def store(self, contentType):
-        return AttachmentStorageTransport(self, contentType)
+    def store(self, contentType, dispositionName=None):
+        return AttachmentStorageTransport(self, contentType, dispositionName)
 
 
     def retrieve(self, protocol):
@@ -861,6 +899,7 @@
             yield calendarObject
 
 
+
 class Invites(object):
     #
     # OK, here's where we get ugly.

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py	2012-11-14 22:15:21 UTC (rev 10031)
@@ -14,8 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from twext.web2.stream import readStream
-from pycalendar.value import PyCalendarValue
 
 """
 SQL backend for CalDAV storage.
@@ -27,11 +25,18 @@
     "CalendarObject",
 ]
 
+from twext.enterprise.dal.syntax import Delete
+from twext.enterprise.dal.syntax import Insert
+from twext.enterprise.dal.syntax import Len
+from twext.enterprise.dal.syntax import Parameter
+from twext.enterprise.dal.syntax import Select, Count, ColumnSyntax
+from twext.enterprise.dal.syntax import Update
+from twext.enterprise.dal.syntax import utcNowSQL
 from twext.python.clsprop import classproperty
+from twext.python.filepath import CachingFilePath
 from twext.python.vcomponent import VComponent
-from txdav.xml.rfc2518 import ResourceType
 from twext.web2.http_headers import MimeType, generateContentType
-from twext.python.filepath import CachingFilePath
+from twext.web2.stream import readStream
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python import hashlib
@@ -46,10 +51,14 @@
 from twistedcaldav.memcacher import Memcacher
 
 from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.datastore.util import AttachmentRetrievalTransport
+from txdav.caldav.datastore.util import CalendarObjectBase
+from txdav.caldav.datastore.util import StorageTransportBase
 from txdav.caldav.datastore.util import validateCalendarComponent, \
     dropboxIDFromCalendarObject
 from txdav.caldav.icalendarstore import ICalendarHome, ICalendar, ICalendarObject, \
     IAttachment, AttachmentStoreFailed, AttachmentStoreValidManagedID
+from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE
 from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator, \
@@ -59,30 +68,17 @@
     _ATTACHMENTS_MODE_NONE, _ATTACHMENTS_MODE_READ, _ATTACHMENTS_MODE_WRITE, \
     CALENDAR_HOME_TABLE, CALENDAR_HOME_METADATA_TABLE, \
     CALENDAR_AND_CALENDAR_BIND, CALENDAR_OBJECT_REVISIONS_AND_BIND_TABLE, \
-    CALENDAR_OBJECT_AND_BIND_TABLE, _BIND_STATUS_INVITED, schema, \
-    _ATTACHMENT_STATUS_DROPBOX, _ATTACHMENT_STATUS_MANAGED
-from twext.enterprise.dal.syntax import Select, Count, ColumnSyntax
-from twext.enterprise.dal.syntax import Insert
-from twext.enterprise.dal.syntax import Update
-from twext.enterprise.dal.syntax import Delete
-from twext.enterprise.dal.syntax import Parameter
-from twext.enterprise.dal.syntax import utcNowSQL
-from twext.enterprise.dal.syntax import Len
-
-from txdav.caldav.datastore.util import CalendarObjectBase
-from txdav.caldav.icalendarstore import QuotaExceeded
-
-from txdav.caldav.datastore.util import StorageTransportBase
+    CALENDAR_OBJECT_AND_BIND_TABLE, _BIND_STATUS_INVITED, schema
 from txdav.common.icommondatastore import IndexedSearchException, \
     InternalDataStoreError, HomeChildNameAlreadyExistsError, \
     HomeChildNameNotAllowedError
+from txdav.xml.rfc2518 import ResourceType
 
 from pycalendar.datetime import PyCalendarDateTime
 from pycalendar.duration import PyCalendarDuration
 from pycalendar.timezone import PyCalendarTimezone
+from pycalendar.value import PyCalendarValue
 
-from txdav.caldav.datastore.util import AttachmentRetrievalTransport
-
 from zope.interface.declarations import implements
 
 import os
@@ -130,30 +126,12 @@
         ch = schema.CALENDAR_HOME
         cb = schema.CALENDAR_BIND
         cor = schema.CALENDAR_OBJECT_REVISIONS
-        at = schema.ATTACHMENT
         rp = schema.RESOURCE_PROPERTY
 
         # delete attachments corresponding to this home, also removing from disk
-        rows = (yield Select(
-            [at.STATUS, at.DROPBOX_ID, at.PATH, ],
-            From=at,
-            Where=(
-                at.CALENDAR_HOME_RESOURCE_ID == self._resourceID
-            ),
-        ).on(self._txn))
-        for status, dropboxID, path in rows:
-            attachment = Attachment._attachmentPathRoot(self._txn, status, dropboxID).child(path)
-            if attachment.exists():
-                yield attachment.remove()
+        yield Attachment.removedHome(self._txn, self._resourceID)
 
         yield Delete(
-            From=at,
-            Where=(
-                at.CALENDAR_HOME_RESOURCE_ID == self._resourceID
-            ),
-        ).on(self._txn)
-
-        yield Delete(
             From=cb,
             Where=cb.CALENDAR_HOME_RESOURCE_ID == self._resourceID
         ).on(self._txn)
@@ -1334,7 +1312,7 @@
             "MANAGED-ID": attachment.managedID(),
             "MTAG": attachment.md5(),
             "FMTTYPE": "%s/%s" % (attachment.contentType().mediaType, attachment.contentType().mediaSubtype),
-            "FILENAME": attachment.dispositionName(),
+            "FILENAME": attachment.name(),
             "SIZE": str(attachment.size()),
         }, valuetype=PyCalendarValue.VALUETYPE_URI)
         if rids is None:
@@ -1370,7 +1348,7 @@
 
             # We actually create a brand new attachment object for the update, but with the same managed-id. That way, other resources
             # referencing the old attachment data will still see that.
-            attachment = (yield self.createManagedAttachment(managed_id))
+            attachment = (yield self.updateManagedAttachment(managed_id, oldattachment))
             t = attachment.store(content_type, filename)
             yield readStream(stream, t.write)
         except Exception, e:
@@ -1386,7 +1364,7 @@
             "MANAGED-ID": attachment.managedID(),
             "MTAG": attachment.md5(),
             "FMTTYPE": "%s/%s" % (attachment.contentType().mediaType, attachment.contentType().mediaSubtype),
-            "FILENAME": attachment.dispositionName(),
+            "FILENAME": attachment.name(),
             "SIZE": str(attachment.size()),
         }, valuetype=PyCalendarValue.VALUETYPE_URI)
         calendar.replaceAllPropertiesWithParameterMatch(attach, "MANAGED-ID", managed_id)
@@ -1425,12 +1403,12 @@
 
 
     @inlineCallbacks
-    def createManagedAttachment(self, managedID=None):
+    def createManagedAttachment(self):
 
         # We need to know the resource_ID of the home collection of the owner
         # (not sharee) of this event
         sharerHomeID = (yield self._parentCollection.sharerHomeID())
-        managedID = str(uuid.uuid4()) if managedID is None else managedID
+        managedID = str(uuid.uuid4())
         returnValue((
             yield ManagedAttachment.create(
                 self._txn, managedID, sharerHomeID, self._resourceID,
@@ -1438,6 +1416,19 @@
         ))
 
 
+    @inlineCallbacks
+    def updateManagedAttachment(self, managedID, oldattachment):
+
+        # We need to know the resource_ID of the home collection of the owner
+        # (not sharee) of this event
+        sharerHomeID = (yield self._parentCollection.sharerHomeID())
+        returnValue((
+            yield ManagedAttachment.update(
+                self._txn, managedID, sharerHomeID, self._resourceID, oldattachment._attachmentID,
+            )
+        ))
+
+
     def attachmentWithManagedID(self, managed_id):
         return ManagedAttachment.load(self._txn, managed_id)
 
@@ -1587,25 +1578,12 @@
             raise QuotaExceeded()
 
         self._path.moveTo(self._attachment._path)
-        self._attachment._contentType = self._contentType
-        self._attachment._dispositionName = self._dispositionName
-        self._attachment._md5 = self._hash.hexdigest()
-        self._attachment._size = newSize
-        att = schema.ATTACHMENT
-        self._attachment._created, self._attachment._modified = map(
-            sqltime,
-            (yield Update(
-                {
-                    att.CONTENT_TYPE    : generateContentType(self._contentType),
-                    att.SIZE            : self._attachment._size,
-                    att.MD5             : self._attachment._md5,
-                    att.MODIFIED        : utcNowSQL,
-                    att.DISPLAYNAME     : self._dispositionName,
-                },
-                Where=(att.PATH == self._attachment.name()).And(
-                    att.DROPBOX_ID == self._attachment._dropboxID
-                ),
-                Return=(att.CREATED, att.MODIFIED)).on(self._txn))[0]
+
+        yield self._attachment.changed(
+            self._contentType,
+            self._dispositionName,
+            self._hash.hexdigest(),
+            newSize
         )
 
         if home:
@@ -1626,10 +1604,9 @@
 
     implements(IAttachment)
 
-    def __init__(self, txn, a_id, status, dropboxID, name, ownerHomeID=None, justCreated=False):
+    def __init__(self, txn, a_id, dropboxID, name, ownerHomeID=None, justCreated=False):
         self._txn = txn
         self._attachmentID = a_id
-        self._attachmentStatus = status
         self._ownerHomeID = ownerHomeID
         self._dropboxID = dropboxID
         self._contentType = None
@@ -1638,20 +1615,13 @@
         self._created = None
         self._modified = None
         self._name = name
-        self._dispositionName = None
         self._justCreated = justCreated
 
 
-    @classmethod
-    def _attachmentPathRoot(cls, txn, status, dropboxID):
-        attachmentRoot = txn._store.attachmentsPath
+    def _attachmentPathRoot(self):
+        return self._txn._store.attachmentsPath
 
-        # Use directory hashing scheme based on MD5 of dropboxID if using dropbox, else
-        # just use dropboxID as-is if managed (since we know it is a uuid in that case)
-        hasheduid = hashlib.md5(dropboxID).hexdigest() if status == _ATTACHMENT_STATUS_DROPBOX else dropboxID
-        return attachmentRoot.child(hasheduid[0:2]).child(hasheduid[2:4]).child(hasheduid)
 
-
     @inlineCallbacks
     def initFromStore(self):
         """
@@ -1668,7 +1638,6 @@
         rows = (yield Select(
             [
                 att.ATTACHMENT_ID,
-                att.STATUS,
                 att.DROPBOX_ID,
                 att.CALENDAR_HOME_RESOURCE_ID,
                 att.CONTENT_TYPE,
@@ -1677,7 +1646,6 @@
                 att.CREATED,
                 att.MODIFIED,
                 att.PATH,
-                att.DISPLAYNAME,
             ],
             From=att,
             Where=where
@@ -1688,7 +1656,6 @@
 
         row_iter = iter(rows[0])
         self._attachmentID = row_iter.next()
-        self._attachmentStatus = row_iter.next()
         self._dropboxID = row_iter.next()
         self._ownerHomeID = row_iter.next()
         self._contentType = MimeType.fromString(row_iter.next())
@@ -1697,7 +1664,6 @@
         self._created = sqltime(row_iter.next())
         self._modified = sqltime(row_iter.next())
         self._name = row_iter.next()
-        self._dispositionName = row_iter.next()
 
         returnValue(self)
 
@@ -1707,19 +1673,13 @@
 
 
     def isManaged(self):
-        return self._attachmentStatus == _ATTACHMENT_STATUS_MANAGED
+        return not self._dropboxID
 
 
     def name(self):
         return self._name
 
 
-    @property
-    def _path(self):
-        attachmentRoot = self._attachmentPathRoot(self._txn, self._attachmentStatus, self._dropboxID)
-        return attachmentRoot.child(self.name())
-
-
     def properties(self):
         pass # stub
 
@@ -1731,11 +1691,14 @@
     def retrieve(self, protocol):
         return AttachmentRetrievalTransport(self._path).start(protocol)
 
+
+    def changed(self, contentType, dispositionName, md5, size):
+        raise NotImplementedError
+
     _removeStatement = Delete(
         From=schema.ATTACHMENT,
-        Where=(schema.ATTACHMENT.DROPBOX_ID == Parameter("dropboxID")).And(
-            schema.ATTACHMENT.PATH == Parameter("path")
-        ))
+        Where=(schema.ATTACHMENT.ATTACHMENT_ID == Parameter("attachmentID"))
+    )
 
 
     @inlineCallbacks
@@ -1754,11 +1717,12 @@
 
     def removePaths(self):
         """
-        Remove the actual file and up to three parent directories if empty.
+        Remove the actual file and up to attachment parent directory if empty.
         """
         self._path.remove()
         parent = self._path.parent()
-        for _ignore in range(3):
+        toppath = self._attachmentPathRoot().path
+        while parent.path != toppath:
             if len(parent.listdir()) == 0:
                 parent.remove()
                 parent = parent.parent()
@@ -1772,10 +1736,59 @@
         for attachments that have failed to be created due to errors during
         storage.)
         """
-        return self._removeStatement.on(self._txn, dropboxID=self._dropboxID,
-                                        path=self._name)
+        return self._removeStatement.on(self._txn, attachmentID=self._attachmentID)
 
 
+    @classmethod
+    @inlineCallbacks
+    def removedHome(cls, txn, homeID):
+        """
+        A calendar home is being removed so all of its attachments must go too. When removing,
+        we don't care about quota adjustment as there will be no quota once the home is removed.
+
+        TODO: this needs to be transactional wrt the actual file deletes.
+        """
+        att = schema.ATTACHMENT
+        attco = schema.ATTACHMENT_CALENDAR_OBJECT
+
+        rows = (yield Select(
+            [att.ATTACHMENT_ID, att.DROPBOX_ID, ],
+            From=att,
+            Where=(
+                att.CALENDAR_HOME_RESOURCE_ID == homeID
+            ),
+        ).on(txn))
+
+        for attachmentID, dropboxID in rows:
+            if dropboxID:
+                attachment = DropBoxAttachment(txn, attachmentID, None, None)
+            else:
+                attachment = ManagedAttachment(txn, attachmentID, None, None)
+            attachment = (yield attachment.initFromStore())
+            if attachment._path.exists():
+                attachment.removePaths()
+
+        yield Delete(
+            From=attco,
+            Where=(
+                attco.ATTACHMENT_ID.In(Select(
+                    [att.ATTACHMENT_ID, ],
+                    From=att,
+                    Where=(
+                        att.CALENDAR_HOME_RESOURCE_ID == homeID
+                    ),
+                ))
+            ),
+        ).on(txn)
+
+        yield Delete(
+            From=att,
+            Where=(
+                att.CALENDAR_HOME_RESOURCE_ID == homeID
+            ),
+        ).on(txn)
+
+
     # IDataStoreObject
     def contentType(self):
         return self._contentType
@@ -1797,12 +1810,7 @@
         return self._modified
 
 
-    # IAttachment
-    def dispositionName(self):
-        return self._dispositionName
 
-
-
 class DropBoxAttachment(Attachment):
 
     @classmethod
@@ -1821,23 +1829,15 @@
         @type ownerHomeID: C{int}
         """
 
-        # File system paths need to exist
-        try:
-            cls._attachmentPathRoot(txn, _ATTACHMENT_STATUS_DROPBOX, dropboxID).makedirs()
-        except:
-            pass
-
         # Now create the DB entry
         att = schema.ATTACHMENT
         rows = (yield Insert({
             att.CALENDAR_HOME_RESOURCE_ID : ownerHomeID,
-            att.STATUS                    : _ATTACHMENT_STATUS_DROPBOX,
             att.DROPBOX_ID                : dropboxID,
             att.CONTENT_TYPE              : "",
             att.SIZE                      : 0,
             att.MD5                       : "",
             att.PATH                      : name,
-            att.DISPLAYNAME               : None,
         }, Return=(att.ATTACHMENT_ID, att.CREATED, att.MODIFIED)).on(txn))
 
         row_iter = iter(rows[0])
@@ -1845,28 +1845,66 @@
         created = sqltime(row_iter.next())
         modified = sqltime(row_iter.next())
 
-        attachment = cls(txn, a_id, _ATTACHMENT_STATUS_DROPBOX, dropboxID, name, ownerHomeID, True)
+        attachment = cls(txn, a_id, dropboxID, name, ownerHomeID, True)
         attachment._created = created
         attachment._modified = modified
+
+        # File system paths need to exist
+        try:
+            attachment._path.parent().makedirs()
+        except:
+            pass
+
         returnValue(attachment)
 
 
     @classmethod
     @inlineCallbacks
     def load(cls, txn, dropboxID, name):
-        attachment = cls(txn, None, _ATTACHMENT_STATUS_DROPBOX, dropboxID, name)
+        attachment = cls(txn, None, dropboxID, name)
         attachment = (yield attachment.initFromStore())
         returnValue(attachment)
 
 
+    @property
+    def _path(self):
+        # Use directory hashing scheme based on MD5 of dropboxID
+        hasheduid = hashlib.md5(self._dropboxID).hexdigest()
+        attachmentRoot = self._attachmentPathRoot().child(hasheduid[0:2]).child(hasheduid[2:4]).child(hasheduid)
+        return attachmentRoot.child(self.name())
 
+
+    @inlineCallbacks
+    def changed(self, contentType, dispositionName, md5, size):
+        """
+        Dropbox attachments never change their path - ignore dispositionName.
+        """
+
+        self._contentType = contentType
+        self._md5 = md5
+        self._size = size
+
+        att = schema.ATTACHMENT
+        self._created, self._modified = map(
+            sqltime,
+            (yield Update(
+                {
+                    att.CONTENT_TYPE    : generateContentType(self._contentType),
+                    att.SIZE            : self._size,
+                    att.MD5             : self._md5,
+                    att.MODIFIED        : utcNowSQL,
+                },
+                Where=(att.ATTACHMENT_ID == self._attachmentID),
+                Return=(att.CREATED, att.MODIFIED)).on(self._txn))[0]
+        )
+
+
+
 class ManagedAttachment(Attachment):
 
-    _PATH_NAME = "data"
-
     @classmethod
     @inlineCallbacks
-    def create(cls, txn, managedID, ownerHomeID, referencedBy):
+    def _create(cls, txn, managedID, ownerHomeID):
         """
         Create a new Attachment object.
 
@@ -1876,27 +1914,17 @@
         @type managedID: C{str}
         @param ownerHomeID: the resource-id of the home collection of the attachment owner
         @type ownerHomeID: C{int}
-        @param referencedBy: the resource-id of the calendar object referencing the attachment
-        @type referencedBy: C{int}
         """
 
-        # File system paths need to exist
-        try:
-            cls._attachmentPathRoot(txn, _ATTACHMENT_STATUS_MANAGED, managedID).makedirs()
-        except:
-            pass
-
         # Now create the DB entry
         att = schema.ATTACHMENT
         rows = (yield Insert({
             att.CALENDAR_HOME_RESOURCE_ID : ownerHomeID,
-            att.STATUS                    : _ATTACHMENT_STATUS_MANAGED,
-            att.DROPBOX_ID                : managedID,
+            att.DROPBOX_ID                : None,
             att.CONTENT_TYPE              : "",
             att.SIZE                      : 0,
             att.MD5                       : "",
-            att.PATH                      : cls._PATH_NAME,
-            att.DISPLAYNAME               : None,
+            att.PATH                      : "",
         }, Return=(att.ATTACHMENT_ID, att.CREATED, att.MODIFIED)).on(txn))
 
         row_iter = iter(rows[0])
@@ -1904,28 +1932,104 @@
         created = sqltime(row_iter.next())
         modified = sqltime(row_iter.next())
 
+        attachment = cls(txn, a_id, managedID, None, ownerHomeID, True)
+        attachment._managedID = managedID
+        attachment._created = created
+        attachment._modified = modified
+
+        # File system paths need to exist
+        try:
+            attachment._path.parent().makedirs()
+        except:
+            pass
+
+        returnValue(attachment)
+
+
+    @classmethod
+    @inlineCallbacks
+    def create(cls, txn, managedID, ownerHomeID, referencedBy):
+        """
+        Create a new Attachment object.
+
+        @param txn: The transaction to use
+        @type txn: L{CommonStoreTransaction}
+        @param managedID: the identifier for the attachment
+        @type managedID: C{str}
+        @param ownerHomeID: the resource-id of the home collection of the attachment owner
+        @type ownerHomeID: C{int}
+        @param referencedBy: the resource-id of the calendar object referencing the attachment
+        @type referencedBy: C{int}
+        """
+
+        # Now create the DB entry
+        attachment = (yield cls._create(txn, managedID, ownerHomeID))
+
         # Create the attachment<->calendar object relationship for managed attachments
         attco = schema.ATTACHMENT_CALENDAR_OBJECT
-        rows = (yield Insert({
-            attco.ATTACHMENT_ID               : a_id,
+        yield Insert({
+            attco.ATTACHMENT_ID               : attachment._attachmentID,
             attco.MANAGED_ID                  : managedID,
             attco.CALENDAR_OBJECT_RESOURCE_ID : referencedBy,
-        }).on(txn))
+        }).on(txn)
 
-        attachment = cls(txn, a_id, _ATTACHMENT_STATUS_MANAGED, managedID, cls._PATH_NAME, ownerHomeID, True)
-        attachment._managedID = managedID
-        attachment._created = created
-        attachment._modified = modified
         returnValue(attachment)
 
 
     @classmethod
     @inlineCallbacks
-    def load(cls, txn, managedID=None):
+    def update(cls, txn, managedID, ownerHomeID, referencedBy, oldAttachmentID):
+        """
+        Create a new Attachment object.
+
+        @param txn: The transaction to use
+        @type txn: L{CommonStoreTransaction}
+        @param managedID: the identifier for the attachment
+        @type managedID: C{str}
+        @param ownerHomeID: the resource-id of the home collection of the attachment owner
+        @type ownerHomeID: C{int}
+        @param referencedBy: the resource-id of the calendar object referencing the attachment
+        @type referencedBy: C{int}
+        @param oldAttachmentID: the attachment-id of the existing attachment being updated
+        @type oldAttachmentID: C{int}
+        """
+
+        # Now create the DB entry
+        attachment = (yield cls._create(txn, managedID, ownerHomeID))
+
+        # Update the attachment<->calendar object relationship for managed attachments
         attco = schema.ATTACHMENT_CALENDAR_OBJECT
+        yield Update(
+            {
+                attco.ATTACHMENT_ID    : attachment._attachmentID,
+            },
+            Where=(attco.MANAGED_ID == managedID).And(
+                attco.CALENDAR_OBJECT_RESOURCE_ID == referencedBy
+            ),
+        ).on(txn)
+
+        # Now check whether old attachmentID is still referenced - if not delete it
         rows = (yield Select(
             [attco.ATTACHMENT_ID, ],
             From=attco,
+            Where=(attco.ATTACHMENT_ID == oldAttachmentID),
+        ).on(txn))
+        aids = [row[0] for row in rows] if rows is not None else ()
+        if len(aids) == 0:
+            oldattachment = ManagedAttachment(txn, oldAttachmentID, None, None)
+            oldattachment = (yield oldattachment.initFromStore())
+            yield oldattachment.remove()
+
+        returnValue(attachment)
+
+
+    @classmethod
+    @inlineCallbacks
+    def load(cls, txn, managedID):
+        attco = schema.ATTACHMENT_CALENDAR_OBJECT
+        rows = (yield Select(
+            [attco.ATTACHMENT_ID, ],
+            From=attco,
             Where=(attco.MANAGED_ID == managedID),
         ).on(txn))
         aids = [row[0] for row in rows] if rows is not None else ()
@@ -1934,7 +2038,7 @@
         elif len(aids) != 1:
             raise AttachmentStoreValidManagedID
 
-        attachment = cls(txn, aids[0], _ATTACHMENT_STATUS_MANAGED, None, cls._PATH_NAME)
+        attachment = cls(txn, aids[0], None, None)
         attachment = (yield attachment.initFromStore())
         attachment._managedID = managedID
         returnValue(attachment)
@@ -1980,7 +2084,40 @@
         return self._managedID
 
 
+    @property
+    def _path(self):
+        # Use directory hashing scheme based on MD5 of attachmentID
+        hasheduid = hashlib.md5(str(self._attachmentID)).hexdigest()
+        return self._attachmentPathRoot().child(hasheduid[0:2]).child(hasheduid[2:4]).child(hasheduid)
+
+
     @inlineCallbacks
+    def changed(self, contentType, dispositionName, md5, size):
+        """
+        Always update name to current disposition name.
+        """
+
+        self._contentType = contentType
+        self._name = dispositionName
+        self._md5 = md5
+        self._size = size
+        att = schema.ATTACHMENT
+        self._created, self._modified = map(
+            sqltime,
+            (yield Update(
+                {
+                    att.CONTENT_TYPE    : generateContentType(self._contentType),
+                    att.SIZE            : self._size,
+                    att.MD5             : self._md5,
+                    att.MODIFIED        : utcNowSQL,
+                    att.PATH            : self._name,
+                },
+                Where=(att.ATTACHMENT_ID == self._attachmentID),
+                Return=(att.CREATED, att.MODIFIED)).on(self._txn))[0]
+        )
+
+
+    @inlineCallbacks
     def removeFromResource(self, resourceID):
 
         # Delete the reference

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/common.py	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/common.py	2012-11-14 22:15:21 UTC (rev 10031)
@@ -1946,7 +1946,7 @@
         attachment = yield obj.createAttachmentWithName(
             "new.attachment",
         )
-        t = attachment.store(MimeType("text", "x-fixture"))
+        t = attachment.store(MimeType("text", "x-fixture"), "")
         self.assertProvides(IAttachmentStorageTransport, t)
         t.write("new attachment")
         t.write(" text")
@@ -2007,7 +2007,7 @@
             created, once all the bytes have been stored.
         """
         att = yield obj.createAttachmentWithName(name)
-        t = att.store(mimeType)
+        t = att.store(mimeType, "")
         t.write(contents)
         yield t.loseConnection()
         returnValue(att)
@@ -2085,7 +2085,7 @@
         obj = yield self.calendarObjectUnderTest()
         name = 'a-fun-attachment'
         attachment = yield obj.createAttachmentWithName(name)
-        transport = attachment.store(MimeType("test", "x-something"))
+        transport = attachment.store(MimeType("test", "x-something"), "")
         peer = transport.getPeer()
         host = transport.getHost()
         self.assertIdentical(peer.attachment, attachment)
@@ -2104,7 +2104,7 @@
         """
         home = yield self.homeUnderTest()
         attachment = yield getit()
-        t = attachment.store(MimeType("text", "x-fixture"))
+        t = attachment.store(MimeType("text", "x-fixture"), "")
         sample = "all work and no play makes jack a dull boy"
         chunk = (sample * (home.quotaAllowedBytes() / len(sample)))
 
@@ -2141,7 +2141,7 @@
         create = lambda: obj.createAttachmentWithName("exists.attachment")
         get = lambda: obj.attachmentWithName("exists.attachment")
         attachment = yield create()
-        t = attachment.store(MimeType("text", "x-fixture"))
+        t = attachment.store(MimeType("text", "x-fixture"), "")
         sampleData = "a reasonably sized attachment"
         t.write(sampleData)
         yield t.loseConnection()
@@ -2204,7 +2204,7 @@
         attachment = yield obj.createAttachmentWithName(
             "new.attachment",
         )
-        t = attachment.store(MimeType("text", "plain"))
+        t = attachment.store(MimeType("text", "plain"), "")
         t.write("new attachment text")
         yield t.loseConnection()
         yield self.commit()

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_util.py	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_util.py	2012-11-14 22:15:21 UTC (rev 10031)
@@ -110,7 +110,7 @@
 END:VCALENDAR
 """)
 
-        self.assertEquals( (yield dropboxIDFromCalendarObject(resource)), "12345-67890.dropbox")
+        self.assertEquals((yield dropboxIDFromCalendarObject(resource)), "12345-67890.dropbox")
 
 
     @inlineCallbacks
@@ -305,10 +305,12 @@
                 return self._name
 
         for filename, result in test_files:
-            item = StorageTransportBase(FakeAttachment(filename), None)
+            item = StorageTransportBase(FakeAttachment(filename), None, None)
             self.assertEquals(item._contentType, result)
-            item = StorageTransportBase(FakeAttachment(filename), result)
+            self.assertEquals(item._dispositionName, None)
+            item = StorageTransportBase(FakeAttachment(filename), result, filename)
             self.assertEquals(item._contentType, result)
+            self.assertEquals(item._dispositionName, filename)
 
 
 

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py	2012-11-14 22:15:21 UTC (rev 10031)
@@ -553,10 +553,12 @@
     # New managed attachment APIs that supersede dropbox
     #
 
-    def addAttachment(rids, content_type, filename, stream):
+    def addAttachment(pathpattern, rids, content_type, filename, stream):
         """
         Add a managed attachment to the calendar data.
 
+        @param pathpattern: URI template for the attachment property value.
+        @type pathpattern: C{str}
         @param rids: set of RECURRENCE-ID values (not adjusted for UTC or TZID offset) to add the
             new attachment to. The server must create necessary overrides if none already exist.
         @type rids: C{iterable}
@@ -571,10 +573,12 @@
         """
 
 
-    def updateAttachment(managed_id, content_type, filename, stream):
+    def updateAttachment(pathpattern, managed_id, content_type, filename, stream):
         """
         Update an existing managed attachment in the calendar data.
 
+        @param pathpattern: URI template for the attachment property value.
+        @type pathpattern: C{str}
         @param managed_id: the identifier of the attachment to update.
         @type managed_id: C{str}
         @param content_type: content-type information for the attachment data.
@@ -738,11 +742,3 @@
             that the stream is complete to its C{connectionLost} method.
         @type protocol: L{IProtocol}
         """
-
-    def dispositionName():
-        """
-        The content-disposition filename for the attachment. Note that this is not necessarily the same as
-        the path name used to store the attachment.
-
-        @rtype: C{str}
-        """

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py	2012-11-14 22:15:21 UTC (rev 10031)
@@ -814,6 +814,7 @@
             a = ("-- Label: %s\n" % (self._label.replace("%", "%%"),) + a[0],) + a[1:]
         if self._store.logSQL:
             log.error("SQL: %r %r" % (a, kw,))
+        results = None
         try:
             results = (yield self._sqlTxn.execSQL(*a, **kw))
         finally:

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_schema/current.sql	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_schema/current.sql	2012-11-14 22:15:21 UTC (rev 10031)
@@ -302,6 +302,7 @@
 create index TRANSPARENCY_TIME_RANGE_INSTANCE_ID on
   TRANSPARENCY(TIME_RANGE_INSTANCE_ID);
 
+
 ----------------
 -- Attachment --
 ----------------
@@ -310,18 +311,14 @@
 
 create table ATTACHMENT (
   ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
-  STATUS                      integer default 0 not null,
   CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
-  DROPBOX_ID                  varchar(255)      not null,
+  DROPBOX_ID                  varchar(255),
   CONTENT_TYPE                varchar(255)      not null,
   SIZE                        integer           not null,
   MD5                         char(32)          not null,
   CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
   MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-  PATH                        varchar(1024)     not null,
-  DISPLAYNAME                 varchar(255),
-
-  unique(DROPBOX_ID, PATH) --implicit index
+  PATH                        varchar(1024)     not null
 );
 
 create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
@@ -337,17 +334,7 @@
   unique(MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
 );
 
--- Enumeration of attachment status
 
-create table ATTACHMENT_STATUS (
-  ID          integer     primary key,
-  DESCRIPTION varchar(16) not null unique
-);
-
-insert into ATTACHMENT_STATUS values (0, 'dropbox');
-insert into ATTACHMENT_STATUS values (1, 'managed');
-
-
 -----------------------
 -- Resource Property --
 -----------------------

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_12_to_13.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_12_to_13.sql	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_12_to_13.sql	2012-11-14 22:15:21 UTC (rev 10031)
@@ -26,28 +26,19 @@
 
 alter table ATTACHMENT
  drop constraint ATTACHMENT_PKEY,
- add column ATTACHMENT_ID integer primary key default nextval('ATTACHMENT_ID_SEQ'),
- add column DISPLAYNAME varchar(255),
- add column STATUS integer default 0 not null,
- add unique(DROPBOX_ID, PATH);
+ alter column DROPBOX_ID drop not null,
+ add column ATTACHMENT_ID integer primary key default nextval('ATTACHMENT_ID_SEQ');
 
 create table ATTACHMENT_CALENDAR_OBJECT (
   ATTACHMENT_ID                  integer      not null references ATTACHMENT on delete cascade,
   MANAGED_ID                     varchar(255) not null,
   CALENDAR_OBJECT_RESOURCE_ID    integer      not null references CALENDAR_OBJECT on delete cascade,
 
-  primary key(ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID) -- implicit index
+  primary key(ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+  unique(MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
 );
 
-create table ATTACHMENT_STATUS (
-  ID          integer     primary key,
-  DESCRIPTION varchar(16) not null unique
-);
 
-insert into ATTACHMENT_STATUS values (0, 'dropbox');
-insert into ATTACHMENT_STATUS values (1, 'managed');
-
-
 -- Now update the version
 -- No data upgrades
 update CALENDARSERVER set VALUE = '13' where NAME = 'VERSION';

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_tables.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_tables.py	2012-11-14 22:11:17 UTC (rev 10030)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql_tables.py	2012-11-14 22:15:21 UTC (rev 10031)
@@ -1,5 +1,5 @@
 # -*- test-case-name: txdav.common.datastore.test.test_sql_tables -*-
-# #
+##
 # Copyright (c) 2010-2012 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,7 @@
 # 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.
-# #
+##
 
 """
 SQL Table definitions.
@@ -174,16 +174,6 @@
 _BIND_MODE_DIRECT = _bindMode('direct')
 
 
-_attachmentStatus = _schemaConstants(
-    schema.ATTACHMENT_STATUS.DESCRIPTION,
-    schema.ATTACHMENT_STATUS.ID
-)
-
-
-_ATTACHMENT_STATUS_DROPBOX = _attachmentStatus('dropbox')
-_ATTACHMENT_STATUS_MANAGED = _attachmentStatus('managed')
-
-
 # Compatibility tables for string formatting:
 CALENDAR_HOME_TABLE = _S(schema.CALENDAR_HOME)
 CALENDAR_HOME_METADATA_TABLE = _S(schema.CALENDAR_HOME_METADATA)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121114/ef4a53ec/attachment-0001.html>


More information about the calendarserver-changes mailing list