[Zodb-checkins] SVN: ZODB/trunk/ - merged ctheune-blobszerocopy branch

Christian Theune ct at gocept.com
Thu Mar 8 17:15:07 EST 2007


Log message for revision 73079:
   - merged ctheune-blobszerocopy branch
   - added notice to history txt that zodb 3.8 now contains blobs :)
  

Changed:
  U   ZODB/trunk/HISTORY.txt
  A   ZODB/trunk/doc/HOWTO-Blobs-NFS.txt
  U   ZODB/trunk/src/ZEO/ClientStorage.py
  U   ZODB/trunk/src/ZEO/ServerStub.py
  U   ZODB/trunk/src/ZEO/StorageServer.py
  U   ZODB/trunk/src/ZEO/tests/testZEO.py
  U   ZODB/trunk/src/ZODB/Blobs/Blob.py
  U   ZODB/trunk/src/ZODB/Blobs/interfaces.py
  A   ZODB/trunk/src/ZODB/Blobs/tests/consume.txt
  U   ZODB/trunk/src/ZODB/Blobs/tests/test_doctests.py
  U   ZODB/trunk/src/ZODB/Blobs/tests/test_undo.py
  U   ZODB/trunk/src/ZODB/component.xml
  U   ZODB/trunk/src/ZODB/config.py

-=-
Modified: ZODB/trunk/HISTORY.txt
===================================================================
--- ZODB/trunk/HISTORY.txt	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/HISTORY.txt	2007-03-08 22:15:06 UTC (rev 73079)
@@ -3,6 +3,8 @@
 ==========================
 Release date: ???
 
+- Added support for Blobs. 
+
 BTrees
 ------
 

Copied: ZODB/trunk/doc/HOWTO-Blobs-NFS.txt (from rev 73078, ZODB/branches/ctheune-blobszerocopy/doc/HOWTO-Blobs-NFS.txt)

Modified: ZODB/trunk/src/ZEO/ClientStorage.py
===================================================================
--- ZODB/trunk/src/ZEO/ClientStorage.py	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZEO/ClientStorage.py	2007-03-08 22:15:06 UTC (rev 73079)
@@ -112,7 +112,7 @@
                  wait=None, wait_timeout=None,
                  read_only=0, read_only_fallback=0,
                  username='', password='', realm=None,
-                 blob_dir=None):
+                 blob_dir=None, blob_cache_writable=False):
         """ClientStorage constructor.
 
         This is typically invoked from a custom_zodb.py file.
@@ -188,6 +188,10 @@
         blob_dir -- directory path for blob data.  'blob data' is data that
             is retrieved via the loadBlob API.
 
+        blob_cache_writable -- Flag whether the blob_dir is a writable shared
+        filesystem that should be used instead of transferring blob data over
+        zrpc.
+
         Note that the authentication protocol is defined by the server
         and is detected by the ClientStorage upon connecting (see
         testConnection() and doAuth() for details).
@@ -315,6 +319,8 @@
         self._lock = threading.Lock()
 
         # XXX need to check for POSIX-ness here
+        self.blob_dir = blob_dir
+        self.blob_cache_writable = blob_cache_writable
         if blob_dir is not None:
             self.fshelper = FilesystemHelper(blob_dir)
             self.fshelper.create()
@@ -892,6 +898,26 @@
     def storeBlob(self, oid, serial, data, blobfilename, version, txn):
         """Storage API: store a blob object."""
         serials = self.store(oid, serial, data, version, txn)
+        if self.blob_cache_writable:
+            self._storeBlob_shared(oid, serial, data, blobfilename, version, txn)
+        else:
+            self._storeBlob_copy(oid, serial, data, blobfilename, version, txn)
+        return serials
+
+    def _storeBlob_shared(self, oid, serial, data, filename, version, txn):
+        # First, move the blob into the blob directory
+        dir = self.fshelper.getPathForOID(oid)
+        if not os.path.exists(dir):
+            os.mkdir(dir)
+        fd, target = self.fshelper.blob_mkstemp(oid, serial)
+        os.close(fd)
+        os.rename(filename, target)
+        # Now tell the server where we put it
+        self._server.storeBlobShared(oid, serial, data,
+                                     os.path.basename(target), version, id(txn))
+
+    def _storeBlob_copy(self, oid, serial, data, blobfilename, version, txn):
+        """Version of storeBlob() that copies the data over the ZEO protocol."""
         blobfile = open(blobfilename, "rb")
         while True:
             chunk = blobfile.read(1<<16)
@@ -904,7 +930,6 @@
                 break
         blobfile.close()
         os.unlink(blobfilename)
-        return serials
 
     def _do_load_blob(self, oid, serial, version):
         """Do the actual loading from the RPC server."""
@@ -999,7 +1024,7 @@
 
     def getBlobLock(self):
         # indirection to support unit testing
-        return Lock()
+        return threading.Lock()
 
     def tpc_vote(self, txn):
         """Storage API: vote on a transaction."""

Modified: ZODB/trunk/src/ZEO/ServerStub.py
===================================================================
--- ZODB/trunk/src/ZEO/ServerStub.py	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZEO/ServerStub.py	2007-03-08 22:15:06 UTC (rev 73079)
@@ -226,6 +226,10 @@
     def storeBlob(self, oid, serial, chunk, version, id):
         self.rpc.callAsync('storeBlob', oid, serial, chunk, version, id)
 
+    def storeBlobShared(self, oid, serial, data, filename, version, id):
+        self.rpc.callAsync('storeBlobShared', oid, serial, data, filename, 
+                           version, id)
+
     ##
     # Start two-phase commit for a transaction
     # @param id id used by client to identify current transaction.  The

Modified: ZODB/trunk/src/ZEO/StorageServer.py
===================================================================
--- ZODB/trunk/src/ZEO/StorageServer.py	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZEO/StorageServer.py	2007-03-08 22:15:06 UTC (rev 73079)
@@ -482,12 +482,18 @@
         if key not in self.blob_transfer:
             tempname = mktemp()
             tempfile = open(tempname, "wb")
-            self.blob_transfer[key] = (tempname, tempfile)   # XXX Force close and remove them when Storage closes
+            # XXX Force close and remove them when Storage closes
+            self.blob_transfer[key] = (tempname, tempfile)
         else:
             tempname, tempfile = self.blob_transfer[key]
+        tempfile.write(chunk)
 
-        tempfile.write(chunk)
- 
+    def storeBlobShared(self, oid, serial, data, filename, version, id):
+        # Reconstruct the full path from the filename in the OID directory
+        filename = os.path.join(self.storage.fshelper.getPathForOID(oid),
+                                filename)
+        self.blob_log.append((oid, serial, data, filename, version))
+
     def loadBlob(self, oid, serial, version, offset):
         key = (oid, serial)
         if not key in self.blob_loads:

Modified: ZODB/trunk/src/ZEO/tests/testZEO.py
===================================================================
--- ZODB/trunk/src/ZEO/tests/testZEO.py	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZEO/tests/testZEO.py	2007-03-08 22:15:06 UTC (rev 73079)
@@ -133,6 +133,9 @@
 
     """Combine tests from various origins in one class."""
 
+    blob_cache_writable = False
+    blob_cache_dir = None
+
     def setUp(self):
         logger.info("setUp() %s", self.id())
         port = get_port()
@@ -142,10 +145,12 @@
         self._pids = [pid]
         self._servers = [adminaddr]
         self._conf_path = path
-        self.blob_cache_dir = tempfile.mkdtemp()  # This is the blob cache for ClientStorage
+        if not self.blob_cache_dir:
+            self.blob_cache_dir = tempfile.mkdtemp()  # This is the blob cache for ClientStorage
         self._storage = ClientStorage(zport, '1', cache_size=20000000,
                                       min_disconnect_poll=0.5, wait=1,
-                                      wait_timeout=60, blob_dir=self.blob_cache_dir)
+                                      wait_timeout=60, blob_dir=self.blob_cache_dir,
+                                      blob_cache_writable=self.blob_cache_writable)
         self._storage.registerDB(DummyDB(), None)
 
     def tearDown(self):
@@ -397,16 +402,14 @@
                 ConnectionInvalidationOnReconnect,
                ]
 
-class BlobAdaptedFileStorageTests(GenericTests):
-    """ZEO backed by a BlobStorage-adapted FileStorage."""
-    def setUp(self):
-        self.blobdir = tempfile.mkdtemp()  # This is the blob directory on the ZEO server
-        self.filestorage = tempfile.mktemp()
-        super(BlobAdaptedFileStorageTests, self).setUp()
+class CommonBlobTests:
 
     def tearDown(self):
         super(BlobAdaptedFileStorageTests, self).tearDown()
-        shutil.rmtree(self.blobdir)
+        if os.path.exists(self.blobdir):
+            # Might be gone already if the super() method deleted
+            # the shared directory. Don't worry.
+            shutil.rmtree(self.blobdir)
 
     def getConfig(self):
         return """
@@ -452,7 +455,7 @@
                                 tid_repr(revid) + BLOB_SUFFIX)
         self.assert_(os.path.exists(filename))
         self.assertEqual(somedata, open(filename).read())
-        
+
     def checkLoadBlob(self):
         from ZODB.Blobs.Blob import Blob
         from ZODB.tests.StorageTestBase import zodb_pickle, ZERO, \
@@ -481,7 +484,47 @@
             self._storage.tpc_abort(t)
             raise
 
+        filename = self._storage.loadBlob(oid, serial, version)
+        self.assertEquals(somedata, open(filename, 'rb').read())
 
+
+class BlobAdaptedFileStorageTests(GenericTests, CommonBlobTests):
+    """ZEO backed by a BlobStorage-adapted FileStorage."""
+
+    def setUp(self):
+        self.blobdir = tempfile.mkdtemp()  # This is the blob directory on the ZEO server
+        self.filestorage = tempfile.mktemp()
+        super(BlobAdaptedFileStorageTests, self).setUp()
+
+    def checkLoadBlobLocks(self):
+        from ZODB.Blobs.Blob import Blob
+        from ZODB.tests.StorageTestBase import zodb_pickle, ZERO, \
+             handle_serials
+        import transaction
+
+        version = ''
+        somedata = 'a' * 10
+
+        blob = Blob()
+        bd_fh = blob.open('w')
+        bd_fh.write(somedata)
+        bd_fh.close()
+        tfname = bd_fh.name
+        oid = self._storage.new_oid()
+        data = zodb_pickle(blob)
+
+        t = transaction.Transaction()
+        try:
+            self._storage.tpc_begin(t)
+            r1 = self._storage.storeBlob(oid, ZERO, data, tfname, '', t)
+            r2 = self._storage.tpc_vote(t)
+            serial = handle_serials(oid, r1, r2)
+            self._storage.tpc_finish(t)
+        except:
+            self._storage.tpc_abort(t)
+            raise
+
+
         class Dummy:
             def __init__(self):
                 self.acquired = 0
@@ -527,8 +570,18 @@
         self.assertEqual(thestatusdict.added, [(oid, serial)])
         self.assertEqual(thestatusdict.removed, [(oid, serial)])
 
+
+class BlobWritableCacheTests(GenericTests, CommonBlobTests):
+
+    def setUp(self):
+        self.blobdir = self.blob_cache_dir = tempfile.mkdtemp()
+        self.filestorage = tempfile.mktemp()
+        self.blob_cache_writable = True
+        super(BlobWritableCacheTests, self).setUp()
+
+
 test_classes = [FileStorageTests, MappingStorageTests,
-                BlobAdaptedFileStorageTests]
+                BlobAdaptedFileStorageTests, BlobWritableCacheTests]
 
 def test_suite():
     suite = unittest.TestSuite()

Modified: ZODB/trunk/src/ZODB/Blobs/Blob.py
===================================================================
--- ZODB/trunk/src/ZODB/Blobs/Blob.py	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZODB/Blobs/Blob.py	2007-03-08 22:15:06 UTC (rev 73079)
@@ -17,6 +17,7 @@
 __docformat__ = "reStructuredText"
 
 import os
+import sys
 import time
 import tempfile
 import logging
@@ -30,14 +31,22 @@
 import transaction.interfaces
 from persistent import Persistent
 
+if sys.platform == 'win32':
+    import win32file
 
 BLOB_SUFFIX = ".blob"
 
 
 class Blob(Persistent):
- 
+
     zope.interface.implements(IBlob)
 
+    # Binding this to an attribute allows overriding it in the unit tests
+    if sys.platform == 'win32':
+        _os_link = lambda src, dst: win32file.CreateHardLink(src, dst, None)
+    else:
+        _os_link = os.link
+
     _p_blob_readers = 0
     _p_blob_writers = 0
     _p_blob_uncommitted = None  # Filename of the uncommitted (dirty) data
@@ -56,39 +65,34 @@
 
     def open(self, mode="r"):
         """Returns a file(-like) object representing blob data."""
-
-        tempdir = os.environ.get('ZODB_BLOB_TEMPDIR', tempfile.gettempdir())
-
         result = None
 
         if (mode.startswith("r") or mode=="U"):
             if self._current_filename() is None:
-                raise BlobError, "Blob does not exist."
+                raise BlobError("Blob does not exist.")
 
             if self._p_blob_writers != 0:
-                raise BlobError, "Already opened for writing."
+                raise BlobError("Already opened for writing.")
 
             self._p_blob_readers += 1
             result = BlobFile(self._current_filename(), mode, self)
 
         elif mode.startswith("w"):
             if self._p_blob_readers != 0:
-                raise BlobError, "Already opened for reading."
+                raise BlobError("Already opened for reading.")
 
+            self._p_blob_writers += 1
             if self._p_blob_uncommitted is None:
-                self._p_blob_uncommitted = utils.mktemp(dir=tempdir)
-
-            self._p_blob_writers += 1
+                self._create_uncommitted_file()
             result = BlobFile(self._p_blob_uncommitted, mode, self)
 
         elif mode.startswith("a"):
             if self._p_blob_readers != 0:
-                raise BlobError, "Already opened for reading."
+                raise BlobError("Already opened for reading.")
 
             if self._p_blob_uncommitted is None:
                 # Create a new working copy
-                self._p_blob_uncommitted = utils.mktemp(dir=tempdir)
-                uncommitted = BlobFile(self._p_blob_uncommitted, mode, self)
+                uncommitted = BlobFile(self._create_uncommitted_file(), mode, self)
                 # NOTE: _p_blob data appears by virtue of Connection._setstate
                 utils.cp(file(self._p_blob_data), uncommitted)
                 uncommitted.seek(0)
@@ -100,51 +104,73 @@
             result = uncommitted
 
         else:
-            raise IOError, 'invalid mode: %s ' % mode
+            raise IOError('invalid mode: %s ' % mode)
 
         if result is not None:
-            # We join the transaction with our own data manager in order to be
-            # notified of commit/vote/abort events.  We do this because at
-            # transaction boundaries, we need to fix up _p_ reference counts
-            # that keep track of open readers and writers and close any
-            # writable filehandles we've opened.
-            if self._p_blob_manager is None:
-                # Blobs need to always participate in transactions.
-                if self._p_jar is not None:
-                    # If we are connected to a database, then we use the
-                    # transaction manager that belongs to this connection
-                    tm = self._p_jar.transaction_manager
-                else:
-                    # If we are not connected to a database, we check whether
-                    # we have been given an explicit transaction manager
-                    if self._p_blob_transaction:
-                        tm = self._p_blob_transaction
-                    else:
-                        # Otherwise we use the default
-                        # transaction manager as an educated guess.
-                        tm = transaction.manager
-                # Create our datamanager and join he current transaction.
-                dm = BlobDataManager(self, result, tm)
-                tm.get().join(dm)
-            else:
-                # Each blob data manager should manage only the one blob
-                # assigned to it.  Assert that this is the case and it is the
-                # correct blob
-                assert self._p_blob_manager.blob is self
-                self._p_blob_manager.register_fh(result)
+            self._setup_transaction_manager(result)
         return result
 
-    def openDetached(self):
+    def openDetached(self, class_=file):
         """Returns a file(-like) object in read mode that can be used
         outside of transaction boundaries.
 
         """
         if self._current_filename() is None:
-            raise BlobError, "Blob does not exist."
+            raise BlobError("Blob does not exist.")
         if self._p_blob_writers != 0:
-            raise BlobError, "Already opened for writing."
-        return file(self._current_filename(), "rb")
+            raise BlobError("Already opened for writing.")
+        # XXX this should increase the reader number and have a test !?!
+        return class_(self._current_filename(), "rb")
 
+    def consumeFile(self, filename):
+        """Will replace the current data of the blob with the file given under
+        filename.
+        """
+        if self._p_blob_writers != 0:
+            raise BlobError("Already opened for writing.")
+        if self._p_blob_readers != 0:
+            raise BlobError("Already opened for reading.")
+
+        previous_uncommitted = bool(self._p_blob_uncommitted)
+        if previous_uncommitted:
+            # If we have uncommitted data, we move it aside for now
+            # in case the consumption doesn't work.
+            target = self._p_blob_uncommitted
+            target_aside = target+".aside"
+            os.rename(target, target_aside)
+        else:
+            target = self._create_uncommitted_file()
+            # We need to unlink the freshly created target again
+            # to allow link() to do its job
+            os.unlink(target)
+
+        try:
+            self._os_link(filename, target)
+        except:
+            # Recover from the failed consumption: First remove the file, it
+            # might exist and mark the pointer to the uncommitted file.
+            self._p_blob_uncommitted = None
+            if os.path.exists(target):
+                os.unlink(target)
+
+            # If there was a file moved aside, bring it back including the pointer to
+            # the uncommitted file.
+            if previous_uncommitted:
+                os.rename(target_aside, target)
+                self._p_blob_uncommitted = target
+
+            # Re-raise the exception to make the application aware of it.
+            raise
+        else:
+            if previous_uncommitted:
+                # The relinking worked so we can remove the data that we had 
+                # set aside.
+                os.unlink(target_aside)
+
+            # We changed the blob state and have to make sure we join the
+            # transaction.
+            self._change()
+
     # utility methods
 
     def _current_filename(self):
@@ -152,9 +178,46 @@
         # Connection._setstate
         return self._p_blob_uncommitted or self._p_blob_data
 
+    def _create_uncommitted_file(self):
+        assert self._p_blob_uncommitted is None, "Uncommitted file already exists."
+        tempdir = os.environ.get('ZODB_BLOB_TEMPDIR', tempfile.gettempdir())
+        self._p_blob_uncommitted = utils.mktemp(dir=tempdir)
+        return self._p_blob_uncommitted
+
     def _change(self):
         self._p_changed = 1
 
+    def _setup_transaction_manager(self, result):
+        # We join the transaction with our own data manager in order to be
+        # notified of commit/vote/abort events.  We do this because at
+        # transaction boundaries, we need to fix up _p_ reference counts
+        # that keep track of open readers and writers and close any
+        # writable filehandles we've opened.
+        if self._p_blob_manager is None:
+            # Blobs need to always participate in transactions.
+            if self._p_jar is not None:
+                # If we are connected to a database, then we use the
+                # transaction manager that belongs to this connection
+                tm = self._p_jar.transaction_manager
+            else:
+                # If we are not connected to a database, we check whether
+                # we have been given an explicit transaction manager
+                if self._p_blob_transaction:
+                    tm = self._p_blob_transaction
+                else:
+                    # Otherwise we use the default
+                    # transaction manager as an educated guess.
+                    tm = transaction.manager
+            # Create our datamanager and join he current transaction.
+            dm = BlobDataManager(self, result, tm)
+            tm.get().join(dm)
+        elif result:
+            # Each blob data manager should manage only the one blob
+            # assigned to it.  Assert that this is the case and it is the
+            # correct blob
+            assert self._p_blob_manager.blob is self
+            self._p_blob_manager.register_fh(result)
+
     # utility methods which should not cause the object's state to be
     # loaded if they are called while the object is a ghost.  Thus,
     # they are named with the _p_ convention and only operate against
@@ -171,7 +234,7 @@
         elif mode.startswith('w') or mode.startswith('a'):
             self._p_blob_writers = max(0, self._p_blob_writers - 1)
         else:
-            raise AssertionError, 'Unknown mode %s' % mode
+            raise AssertionError('Unknown mode %s' % mode)
 
     def _p_blob_refcounts(self):
         # used by unit tests

Modified: ZODB/trunk/src/ZODB/Blobs/interfaces.py
===================================================================
--- ZODB/trunk/src/ZODB/Blobs/interfaces.py	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZODB/Blobs/interfaces.py	2007-03-08 22:15:06 UTC (rev 73079)
@@ -1,6 +1,23 @@
+##############################################################################
+#
+# Copyright (c) 2005-2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Blob-related interfaces
 
+"""
+
 from zope.interface import Interface
 
+
 class IBlob(Interface):
     """A BLOB supports efficient handling of large data within ZODB."""
 
@@ -10,7 +27,7 @@
         mode: Mode to open the file with. Possible values: r,w,r+,a
         """
 
-    def openDetached():
+    def openDetached(class_=file):
         """Returns a file(-like) object in read mode that can be used
         outside of transaction boundaries.
 
@@ -19,12 +36,24 @@
 
         The handle is not attached to the blob and can be used outside of a
         transaction.
+
+        Optionally the class that should be used to open the file can be
+        specified. This can be used to e.g. use Zope's FileStreamIterator.
         """
 
-    # XXX need a method to initialize the blob from the storage
-    # this means a) setting the _p_blob_data filename and b) putting
-    # the current data in that file
+    def consumeFile(filename):
+        """Will replace the current data of the blob with the file given under
+        filename.
 
+        This method uses link-like semantics internally and has the requirement
+        that the file that is to be consumed lives on the same volume (or
+        mount/share) as the blob directory.
+
+        The blob must not be opened for reading or writing when consuming a 
+        file.
+        """
+
+
 class IBlobStorage(Interface):
     """A storage supporting BLOBs."""
 
@@ -39,4 +68,3 @@
 
         Raises POSKeyError if the blobfile cannot be found.
         """
-

Copied: ZODB/trunk/src/ZODB/Blobs/tests/consume.txt (from rev 73078, ZODB/branches/ctheune-blobszerocopy/src/ZODB/Blobs/tests/consume.txt)

Modified: ZODB/trunk/src/ZODB/Blobs/tests/test_doctests.py
===================================================================
--- ZODB/trunk/src/ZODB/Blobs/tests/test_doctests.py	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZODB/Blobs/tests/test_doctests.py	2007-03-08 22:15:06 UTC (rev 73079)
@@ -16,4 +16,4 @@
 
 def test_suite():
     return DocFileSuite("basic.txt",  "connection.txt", "transaction.txt",
-                        "packing.txt", "importexport.txt")
+                        "packing.txt", "importexport.txt", "consume.txt")

Modified: ZODB/trunk/src/ZODB/Blobs/tests/test_undo.py
===================================================================
--- ZODB/trunk/src/ZODB/Blobs/tests/test_undo.py	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZODB/Blobs/tests/test_undo.py	2007-03-08 22:15:06 UTC (rev 73079)
@@ -94,6 +94,47 @@
         self.assertEqual(blob.open('r').read(), 'this is state 1')
         transaction.abort()
 
+    def testUndoAfterConsumption(self):
+        base_storage = FileStorage(self.storagefile)
+        blob_storage = BlobStorage(self.blob_dir, base_storage)
+        database = DB(blob_storage)
+        connection = database.open()
+        root = connection.root()
+        transaction.begin()
+        to_consume = tempfile.NamedTemporaryFile()
+        to_consume.write('this is state 1')
+        to_consume.flush()
+
+        blob = Blob()
+        blob.consumeFile(to_consume.name)
+
+        root['blob'] = blob
+        transaction.commit()
+
+        transaction.begin()
+        blob = root['blob']
+        to_consume = tempfile.NamedTemporaryFile()
+        to_consume.write('this is state 2')
+        to_consume.flush()
+        blob.consumeFile(to_consume.name)
+        transaction.commit()
+
+        transaction.begin()
+        blob = root['blob']
+        self.assertEqual(blob.open('r').read(), 'this is state 2')
+        transaction.abort()
+
+        serial = base64.encodestring(blob_storage._tid)
+
+        transaction.begin()
+        blob_storage.undo(serial, blob_storage._transaction)
+        transaction.commit()
+
+        transaction.begin()
+        blob = root['blob']
+        self.assertEqual(blob.open('r').read(), 'this is state 1')
+        transaction.abort()
+
     def testRedo(self):
         base_storage = FileStorage(self.storagefile)
         blob_storage = BlobStorage(self.blob_dir, base_storage)

Modified: ZODB/trunk/src/ZODB/component.xml
===================================================================
--- ZODB/trunk/src/ZODB/component.xml	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZODB/component.xml	2007-03-08 22:15:06 UTC (rev 73079)
@@ -67,9 +67,18 @@
     <multikey name="server" datatype="socket-connection-address" required="yes"/>
     <key name="blob-dir" required="no">
       <description>
-        Path name to the blob storage directory.
+        Path name to the blob cache directory.
       </description>
     </key>
+    <key name="blob-cache-writable" required="no" default="no"
+        datatype="boolean">
+      <description>
+          Tells whether the cache is a shared writable directory
+          and that the ZEO protocol should not transfer the file
+          but only the filename when committing.
+      </description>
+    </key>
+
     <key name="storage" default="1">
       <description>
         The name of the storage that the client wants to use.  If the

Modified: ZODB/trunk/src/ZODB/config.py
===================================================================
--- ZODB/trunk/src/ZODB/config.py	2007-03-08 21:54:21 UTC (rev 73078)
+++ ZODB/trunk/src/ZODB/config.py	2007-03-08 22:15:06 UTC (rev 73079)
@@ -141,7 +141,7 @@
         base = self.config.base.open()
         return BlobStorage(self.config.blob_dir, base)
 
-        
+
 class ZEOClient(BaseConfig):
 
     def open(self):
@@ -152,6 +152,7 @@
         return ClientStorage(
             L,
             blob_dir=self.config.blob_dir,
+            blob_cache_writable=self.config.blob_cache_writable,
             storage=self.config.storage,
             cache_size=self.config.cache_size,
             name=self.config.name,



More information about the Zodb-checkins mailing list