Commit efe06091 authored by Jim Fulton's avatar Jim Fulton

Added an option to control whether .old files are kept when packing

file storages.

Also fixed a bug in handling the create option with blob support.
parent 6f70d777
...@@ -30,6 +30,8 @@ New Features ...@@ -30,6 +30,8 @@ New Features
- FileStorage now supports blobs directly. - FileStorage now supports blobs directly.
- You can now control whether FileStorages keep .old files when packing.
3.9.0a8 (2008-12-15) 3.9.0a8 (2008-12-15)
==================== ====================
......
...@@ -107,7 +107,8 @@ class FileStorage( ...@@ -107,7 +107,8 @@ class FileStorage(
_pack_is_in_progress = False _pack_is_in_progress = False
def __init__(self, file_name, create=False, read_only=False, stop=None, def __init__(self, file_name, create=False, read_only=False, stop=None,
quota=None, pack_gc=True, packer=None, blob_dir=None): quota=None, pack_gc=True, pack_keep_old=True, packer=None,
blob_dir=None):
if read_only: if read_only:
self._is_read_only = True self._is_read_only = True
...@@ -131,6 +132,7 @@ class FileStorage( ...@@ -131,6 +132,7 @@ class FileStorage(
self._file_name = file_name self._file_name = file_name
self._pack_gc = pack_gc self._pack_gc = pack_gc
self.pack_keep_old = pack_keep_old
if packer is not None: if packer is not None:
self.packer = packer self.packer = packer
...@@ -203,12 +205,16 @@ class FileStorage( ...@@ -203,12 +205,16 @@ class FileStorage(
self._quota = quota self._quota = quota
self.blob_dir = blob_dir
if blob_dir: if blob_dir:
self.blob_dir = os.path.abspath(blob_dir)
if create and os.path.exists(self.blob_dir):
ZODB.blob.remove_committed_dir(self.blob_dir)
self._blob_init(blob_dir) self._blob_init(blob_dir)
zope.interface.alsoProvides(self, zope.interface.alsoProvides(self,
ZODB.interfaces.IBlobStorageRestoreable) ZODB.interfaces.IBlobStorageRestoreable)
else: else:
self.blob_dir = None
self._blob_init_no_blobs() self._blob_init_no_blobs()
def copyTransactionsFrom(self, other): def copyTransactionsFrom(self, other):
...@@ -1085,6 +1091,8 @@ class FileStorage( ...@@ -1085,6 +1091,8 @@ class FileStorage(
# OK, we're beyond the point of no return # OK, we're beyond the point of no return
os.rename(self._file_name + '.pack', self._file_name) os.rename(self._file_name + '.pack', self._file_name)
if not self.pack_keep_old:
os.remove(oldpath)
self._file = open(self._file_name, 'r+b') self._file = open(self._file_name, 'r+b')
self._initIndex(index, self._tindex) self._initIndex(index, self._tindex)
self._pos = opos self._pos = opos
...@@ -1107,7 +1115,7 @@ class FileStorage( ...@@ -1107,7 +1115,7 @@ class FileStorage(
lblob_dir = len(self.blob_dir) lblob_dir = len(self.blob_dir)
fshelper = self.fshelper fshelper = self.fshelper
old = self.blob_dir+'.old' old = self.blob_dir+'.old'
os.mkdir(old, 0777) link_or_copy = ZODB.blob.link_or_copy
# Helper to clean up dirs left empty after moving things to old # Helper to clean up dirs left empty after moving things to old
def maybe_remove_empty_dir_containing(path): def maybe_remove_empty_dir_containing(path):
...@@ -1118,17 +1126,23 @@ class FileStorage( ...@@ -1118,17 +1126,23 @@ class FileStorage(
os.rmdir(path) os.rmdir(path)
maybe_remove_empty_dir_containing(path) maybe_remove_empty_dir_containing(path)
# Helper that moves a oid dir or revision file to the old dir. if self.pack_keep_old:
def move(path): # Helpers that move oid dir or revision file to the old dir.
dest = os.path.dirname(old+path[lblob_dir:]) os.mkdir(old, 0777)
if not os.path.exists(dest): link_or_copy(os.path.join(self.blob_dir, '.layout'),
os.makedirs(dest, 0700) os.path.join(old, '.layout'))
os.rename(path, old+path[lblob_dir:]) def handle_file(path):
maybe_remove_empty_dir_containing(path) dest = os.path.dirname(old+path[lblob_dir:])
if not os.path.exists(dest):
os.makedirs(dest, 0700)
os.rename(path, old+path[lblob_dir:])
handle_dir = handle_file
else:
# Helpers that remove an oid dir or revision file.
handle_file = ZODB.blob.remove_committed
handle_dir = ZODB.blob.remove_committed_dir
# Fist step: "remove" oids or revisions by moving them to .old # Fist step: move or remove oids or revisions
# (Later, when we add an option to not keep old files, we'll
# be able to simply remove.)
for line in open(os.path.join(self.blob_dir, '.removed')): for line in open(os.path.join(self.blob_dir, '.removed')):
line = line.strip().decode('hex') line = line.strip().decode('hex')
...@@ -1138,7 +1152,8 @@ class FileStorage( ...@@ -1138,7 +1152,8 @@ class FileStorage(
if not os.path.exists(path): if not os.path.exists(path):
# Hm, already gone. Odd. # Hm, already gone. Odd.
continue continue
move(path) handle_dir(path)
maybe_remove_empty_dir_containing(path)
continue continue
if len(line) != 16: if len(line) != 16:
...@@ -1149,10 +1164,16 @@ class FileStorage( ...@@ -1149,10 +1164,16 @@ class FileStorage(
if not os.path.exists(path): if not os.path.exists(path):
# Hm, already gone. Odd. # Hm, already gone. Odd.
continue continue
move(path) handle_file(path)
assert not os.path.exists(path)
maybe_remove_empty_dir_containing(path)
os.remove(os.path.join(self.blob_dir, '.removed'))
if not self.pack_keep_old:
return
# Second step, copy remaining files. # Second step, copy remaining files.
link_or_copy = ZODB.blob.link_or_copy
for path, dir_names, file_names in os.walk(self.blob_dir): for path, dir_names, file_names in os.walk(self.blob_dir):
for file_name in file_names: for file_name in file_names:
if not file_name.endswith('.blob'): if not file_name.endswith('.blob'):
......
...@@ -11,15 +11,93 @@ ...@@ -11,15 +11,93 @@
# FOR A PARTICULAR PURPOSE. # FOR A PARTICULAR PURPOSE.
# #
############################################################################## ##############################################################################
import unittest
from zope.testing import doctest from zope.testing import doctest
import os
import time
import transaction
import unittest
import ZODB.FileStorage
import ZODB.tests.util import ZODB.tests.util
def pack_keep_old():
"""Should a copy of the database be kept?
The pack_keep_old constructor argument controls whether a .old file (and .old directory for blobs is kept.)
>>> fs = ZODB.FileStorage.FileStorage('data.fs', blob_dir='blobs')
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> import ZODB.blob
>>> conn.root()[1] = ZODB.blob.Blob()
>>> conn.root()[1].open('w').write('some data')
>>> conn.root()[2] = ZODB.blob.Blob()
>>> conn.root()[2].open('w').write('some data')
>>> transaction.commit()
>>> conn.root()[1].open('w').write('some other data')
>>> del conn.root()[2]
>>> transaction.commit()
>>> old_size = os.stat('data.fs').st_size
>>> def get_blob_size(d):
... result = 0
... for path, dirs, file_names in os.walk(d):
... for file_name in file_names:
... result += os.stat(os.path.join(path, file_name)).st_size
... return result
>>> blob_size = get_blob_size('blobs')
>>> db.pack(time.time()+1)
>>> packed_size = os.stat('data.fs').st_size
>>> packed_size < old_size
True
>>> os.stat('data.fs.old').st_size == old_size
True
>>> packed_blob_size = get_blob_size('blobs')
>>> packed_blob_size < blob_size
True
>>> get_blob_size('blobs.old') == blob_size
True
>>> db.close()
>>> fs = ZODB.FileStorage.FileStorage('data.fs', blob_dir='blobs',
... create=True, pack_keep_old=False)
>>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> conn.root()[1] = ZODB.blob.Blob()
>>> conn.root()[1].open('w').write('some data')
>>> conn.root()[2] = ZODB.blob.Blob()
>>> conn.root()[2].open('w').write('some data')
>>> transaction.commit()
>>> conn.root()[1].open('w').write('some other data')
>>> del conn.root()[2]
>>> transaction.commit()
>>> db.pack(time.time()+1)
>>> os.stat('data.fs').st_size == packed_size
True
>>> os.path.exists('data.fs.old')
False
>>> get_blob_size('blobs') == packed_blob_size
True
>>> os.path.exists('blobs.old')
False
>>> db.close()
"""
def test_suite(): def test_suite():
return unittest.TestSuite(( return unittest.TestSuite((
doctest.DocFileSuite( doctest.DocFileSuite(
'zconfig.txt', 'zconfig.txt',
setUp=ZODB.tests.util.setUp, tearDown=ZODB.tests.util.tearDown, setUp=ZODB.tests.util.setUp, tearDown=ZODB.tests.util.tearDown,
), ),
doctest.DocTestSuite(
setUp=ZODB.tests.util.setUp, tearDown=ZODB.tests.util.tearDown,
),
)) ))
...@@ -39,15 +39,17 @@ blob-dir ...@@ -39,15 +39,17 @@ blob-dir
>>> os.path.basename(fs.blob_dir) >>> os.path.basename(fs.blob_dir)
'blobs' 'blobs'
>>> fs.close()
create create
Flag that indicates whether the storage should be truncated if Flag that indicates whether the storage should be truncated if
it already exists. it already exists.
To demonstrate this, we'll first write some dataL To demonstrate this, we'll first write some data:
>>> db = ZODB.DB('my.fs') # writes object 0 >>> db = ZODB.DB(fs)
>>> conn = db.open()
>>> import ZODB.blob, transaction
>>> conn.root()[1] = ZODB.blob.Blob()
>>> transaction.commit()
>>> db.close() >>> db.close()
Then reopen with the create option: Then reopen with the create option:
...@@ -55,6 +57,7 @@ create ...@@ -55,6 +57,7 @@ create
>>> fs = ZODB.config.storageFromString(""" >>> fs = ZODB.config.storageFromString("""
... <filestorage> ... <filestorage>
... path my.fs ... path my.fs
... blob-dir blobs
... create true ... create true
... </filestorage> ... </filestorage>
... """) ... """)
...@@ -66,6 +69,9 @@ create ...@@ -66,6 +69,9 @@ create
... ...
POSKeyError: 0x00 POSKeyError: 0x00
>>> sorted(os.listdir('blobs'))
['.layout', 'tmp']
>>> fs.close() >>> fs.close()
read-only read-only
...@@ -111,7 +117,7 @@ packer ...@@ -111,7 +117,7 @@ packer
some information about it's arguments: some information about it's arguments:
>>> def packer(storage, referencesf, stop, gc): >>> def packer(storage, referencesf, stop, gc):
... print referencesf, storage is fs, gc ... print referencesf, storage is fs, gc, storage.pack_keep_old
>>> ZODB.FileStorage.config_demo_printing_packer = packer >>> ZODB.FileStorage.config_demo_printing_packer = packer
>>> fs = ZODB.config.storageFromString(""" >>> fs = ZODB.config.storageFromString("""
...@@ -124,7 +130,7 @@ packer ...@@ -124,7 +130,7 @@ packer
>>> import time >>> import time
>>> db = ZODB.DB(fs) # writes object 0 >>> db = ZODB.DB(fs) # writes object 0
>>> fs.pack(time.time(), 42) >>> fs.pack(time.time(), 42)
42 True True 42 True True True
>>> fs.close() >>> fs.close()
...@@ -143,13 +149,29 @@ pack-gc ...@@ -143,13 +149,29 @@ pack-gc
... """) ... """)
>>> fs.pack(time.time(), 42) >>> fs.pack(time.time(), 42)
42 True False 42 True False True
Note that if we pass the gc option to pack, then this will Note that if we pass the gc option to pack, then this will
override the value set in the configuration: override the value set in the configuration:
>>> fs.pack(time.time(), 42, gc=True) >>> fs.pack(time.time(), 42, gc=True)
42 True True 42 True True True
>>> fs.close()
pack-keep-old
If false, then old files aren't kept when packing
>>> fs = ZODB.config.storageFromString("""
... <filestorage>
... path my.fs
... packer ZODB.FileStorage.config_demo_printing_packer
... pack-keep-old false
... </filestorage>
... """)
>>> fs.pack(time.time(), 42)
42 True True False
>>> fs.close() >>> fs.close()
......
...@@ -59,6 +59,12 @@ ...@@ -59,6 +59,12 @@
databases. databases.
</description> </description>
</key> </key>
<key name="pack-keep-old" datatype="boolean" default="true">
<description>
If true, a copy of the database before packing is kept in a
".old" file.
</description>
</key>
</sectiontype> </sectiontype>
<sectiontype name="mappingstorage" datatype=".MappingStorage" <sectiontype name="mappingstorage" datatype=".MappingStorage"
......
...@@ -11,9 +11,7 @@ ...@@ -11,9 +11,7 @@
# FOR A PARTICULAR PURPOSE # FOR A PARTICULAR PURPOSE
# #
############################################################################## ##############################################################################
"""Open database and storage from a configuration. """Open database and storage from a configuration."""
$Id$"""
import os import os
from cStringIO import StringIO from cStringIO import StringIO
...@@ -147,6 +145,7 @@ class FileStorage(BaseConfig): ...@@ -147,6 +145,7 @@ class FileStorage(BaseConfig):
read_only=self.config.read_only, read_only=self.config.read_only,
quota=self.config.quota, quota=self.config.quota,
pack_gc=self.config.pack_gc, pack_gc=self.config.pack_gc,
pack_keep_old=self.config.pack_keep_old,
blob_dir=self.config.blob_dir, blob_dir=self.config.blob_dir,
**options) **options)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment