SVN: ZODB/trunk/src/ New Feature:

Jim Fulton <[email protected]>
Newsgroups gmane.comp.python.zope.zodb.cvs
Message-ID <20100517175417.3D31B941B7__31594.0942652107$1274118863$gmane$org@cvs.zope.org>
Log message for revision 112424:
  New Feature:
    The file-storage backup script, repoze, will now create a backup
    index file if an output file name is given via the --output/-o
    option.
  
  (Merged the tseaver-repozo_index branch.)
  

Changed:
  U   ZODB/trunk/src/CHANGES.txt
  U   ZODB/trunk/src/ZODB/scripts/repozo.py
  U   ZODB/trunk/src/ZODB/scripts/tests/test_repozo.py

-=-
Modified: ZODB/trunk/src/CHANGES.txt
===================================================================
--- ZODB/trunk/src/CHANGES.txt	2010-05-17 17:34:29 UTC (rev 112423)
+++ ZODB/trunk/src/CHANGES.txt	2010-05-17 17:54:16 UTC (rev 112424)
@@ -37,6 +37,10 @@
   new option (large_record_size/large-record-size) to control the
   record size at which the warning is issued.
 
+- The file-storage backup script, repoze, will now create a backup
+  index file if an output file name is given via the --output/-o
+  option.
+
 Bugs Fixed
 ----------
 

Modified: ZODB/trunk/src/ZODB/scripts/repozo.py
===================================================================
--- ZODB/trunk/src/ZODB/scripts/repozo.py	2010-05-17 17:34:29 UTC (rev 112423)
+++ ZODB/trunk/src/ZODB/scripts/repozo.py	2010-05-17 17:54:16 UTC (rev 112424)
@@ -5,7 +5,7 @@
 # Originally written by Anthony Baxter
 # Significantly modified by Barry Warsaw
 
-"""repozo.py -- incremental and full backups of a Data.fs file.
+"""repozo.py -- incremental and full backups of a Data.fs file and index.
 
 Usage: %(program)s [options]
 Where:
@@ -66,9 +66,13 @@
     --output=filename
         Write recovered ZODB to given file.  By default, the file is
         written to stdout.
+
+        Note:  for the stdout case, the index file will **not** be restored
+        automatically.
 """
 
 import os
+import shutil
 import sys
 try:
     # the hashlib package is available from Python 2.5
@@ -96,6 +100,11 @@
 class WouldOverwriteFiles(Exception):
     pass
 
+
+class NoFiles(Exception):
+    pass
+
+
 def usage(code, msg=''):
     outfp = sys.stderr
     if code == 0:
@@ -394,14 +403,14 @@
     # keep most recent full
     if not full:
         return
-    
+
     recentfull = full.pop(-1)
     deletable.remove(recentfull)
     root, ext = os.path.splitext(recentfull)
     dat = root + '.dat'
     if dat in deletable:
         deletable.remove(dat)
-    
+
     for fname in deletable:
         log('removing old backup file %s (and .dat)', fname)
         root, ext = os.path.splitext(fname)
@@ -412,6 +421,10 @@
         os.unlink(os.path.join(options.repository, fname))
 
 def do_full_backup(options):
+    options.full = True
+    dest = os.path.join(options.repository, gen_filename(options))
+    if os.path.exists(dest):
+        raise WouldOverwriteFiles('Cannot overwrite existing file: %s' % dest)
     # Find the file position of the last completed transaction.
     fs = FileStorage(options.file, read_only=True)
     # Note that the FileStorage ctor calls read_index() which scans the file
@@ -420,11 +433,12 @@
     # because we only want to copy stuff from the beginning of the file to the
     # last valid transaction record.
     pos = fs.getSize()
+    # Save the storage index into the repository
+    index_file = os.path.join(options.repository,
+                              gen_filename(options, '.index'))
+    log('writing index')
+    fs._index.save(pos, index_file)
     fs.close()
-    options.full = True
-    dest = os.path.join(options.repository, gen_filename(options))
-    if os.path.exists(dest):
-        raise WouldOverwriteFiles('Cannot overwrite existing file: %s' % dest)
     log('writing full backup: %s bytes to %s', pos, dest)
     sum = copyfile(options, dest, 0, pos)
     # Write the data file for this full backup
@@ -439,6 +453,10 @@
 
 
 def do_incremental_backup(options, reposz, repofiles):
+    options.full = False
+    dest = os.path.join(options.repository, gen_filename(options))
+    if os.path.exists(dest):
+        raise WouldOverwriteFiles('Cannot overwrite existing file: %s' % dest)
     # Find the file position of the last completed transaction.
     fs = FileStorage(options.file, read_only=True)
     # Note that the FileStorage ctor calls read_index() which scans the file
@@ -447,11 +465,11 @@
     # because we only want to copy stuff from the beginning of the file to the
     # last valid transaction record.
     pos = fs.getSize()
+    log('writing index')
+    index_file = os.path.join(options.repository,
+                              gen_filename(options, '.index'))
+    fs._index.save(pos, index_file)
     fs.close()
-    options.full = False
-    dest = os.path.join(options.repository, gen_filename(options))
-    if os.path.exists(dest):
-        raise WouldOverwriteFiles('Cannot overwrite existing file: %s' % dest)
     log('writing incremental: %s bytes to %s',  pos-reposz, dest)
     sum = copyfile(options, dest, reposz, pos - reposz)
     # The first file in repofiles points to the last full backup.  Use this to
@@ -552,10 +570,9 @@
     repofiles = find_files(options)
     if not repofiles:
         if options.date:
-            log('No files in repository before %s', options.date)
+            raise NoFiles('No files in repository before %s', options.date)
         else:
-            log('No files in repository')
-        return
+            raise NoFiles('No files in repository')
     if options.output is None:
         log('Recovering file to stdout')
         outfp = sys.stdout
@@ -567,7 +584,17 @@
         outfp.close()
     log('Recovered %s bytes, md5: %s', reposz, reposum)
 
+    if options.output is not None:
+        last_base = os.path.splitext(repofiles[-1])[0]
+        source_index = '%s.index' % last_base
+        target_index = '%s.index' % options.output
+        if os.path.exists(source_index):
+            log('Restoring index file %s to %s', source_index, target_index)
+            shutil.copyfile(source_index, target_index)
+        else:
+            log('No index file to restore: %s', source_index)
 
+
 def main(argv=None):
     if argv is None:
         argv = sys.argv[1:]
@@ -577,10 +604,14 @@
             do_backup(options)
         except WouldOverwriteFiles, e:
             print >> sys.stderr, str(e)
-            sys.exit(2)
+            sys.exit(1)
     else:
         assert options.mode == RECOVER
-        do_recover(options)
+        try:
+            do_recover(options)
+        except NoFiles, e:
+            print >> sys.stderr, str(e)
+            sys.exit(1)
 
 
 if __name__ == '__main__':

Modified: ZODB/trunk/src/ZODB/scripts/tests/test_repozo.py
===================================================================
--- ZODB/trunk/src/ZODB/scripts/tests/test_repozo.py	2010-05-17 17:34:29 UTC (rev 112423)
+++ ZODB/trunk/src/ZODB/scripts/tests/test_repozo.py	2010-05-17 17:54:16 UTC (rev 112424)
@@ -74,6 +74,7 @@
                     del tree[keys[0]]
         transaction.commit()
         self.pos = self.db.storage._pos
+        self.maxkey = self.db.storage._oid
         self.close()
 
 
@@ -326,6 +327,11 @@
         f.close()
         return fqn
 
+    def test_no_files(self):
+        options = self._makeOptions(date='2010-05-14-13-30-57')
+        found = self._callFUT(options)
+        self.assertEqual(found, [])
+
     def test_explicit_date(self):
         options = self._makeOptions(date='2010-05-14-13-30-57')
         files = []
@@ -525,7 +531,9 @@
         self.assertRaises(WouldOverwriteFiles, self._callFUT, options)
 
     def test_empty(self):
+        import struct
         from ZODB.scripts.repozo import gen_filename
+        from ZODB.fsIndex import fsIndex
         db = self._makeDB()
         options = self._makeOptions(file=db._file_name,
                                     gzip=False,
@@ -542,6 +550,15 @@
         self.assertEqual(open(datfile).read(),
                          '%s 0 %d %s\n' %
                             (target, len(original), md5(original).hexdigest()))
+        ndxfile = os.path.join(self._repository_directory,
+                               gen_filename(options, '.index'))
+        ndx_info = fsIndex.load(ndxfile)
+        self.assertEqual(ndx_info['pos'], len(original))
+        index = ndx_info['index']
+        pZero = struct.pack(">Q", 0)
+        pOne = struct.pack(">Q", 1)
+        self.assertEqual(index.minKey(), pZero)
+        self.assertEqual(index.maxKey(), pOne)
 
 
 class Test_do_incremental_backup(OptionsTestBase, unittest.TestCase):
@@ -576,7 +593,9 @@
                           self._callFUT, options, 0, repofiles)
 
     def test_no_changes(self):
+        import struct
         from ZODB.scripts.repozo import gen_filename
+        from ZODB.fsIndex import fsIndex
         db = self._makeDB()
         oldpos = db.pos
         options = self._makeOptions(file=db._file_name,
@@ -603,9 +622,20 @@
         self.assertEqual(open(datfile).read(),
                          '%s %d %d %s\n' %
                             (target, oldpos, oldpos, md5('').hexdigest()))
+        ndxfile = os.path.join(self._repository_directory,
+                               gen_filename(options, '.index'))
+        ndx_info = fsIndex.load(ndxfile)
+        self.assertEqual(ndx_info['pos'], oldpos)
+        index = ndx_info['index']
+        pZero = struct.pack(">Q", 0)
+        pOne = struct.pack(">Q", 1)
+        self.assertEqual(index.minKey(), pZero)
+        self.assertEqual(index.maxKey(), pOne)
 
     def test_w_changes(self):
+        import struct
         from ZODB.scripts.repozo import gen_filename
+        from ZODB.fsIndex import fsIndex
         db = self._makeDB()
         oldpos = db.pos
         options = self._makeOptions(file=db._file_name,
@@ -637,8 +667,109 @@
                          '%s %d %d %s\n' %
                             (target, oldpos, newpos,
                              md5(increment).hexdigest()))
+        ndxfile = os.path.join(self._repository_directory,
+                               gen_filename(options, '.index'))
+        ndx_info = fsIndex.load(ndxfile)
+        self.assertEqual(ndx_info['pos'], newpos)
+        index = ndx_info['index']
+        pZero = struct.pack(">Q", 0)
+        self.assertEqual(index.minKey(), pZero)
+        self.assertEqual(index.maxKey(), db.maxkey)
 
 
+class Test_do_recover(OptionsTestBase, unittest.TestCase):
+
+    def _callFUT(self, options):
+        from ZODB.scripts.repozo import do_recover
+        return do_recover(options)
+
+    def _makeFile(self, hour, min, sec, ext, text=None):
+        # call _makeOptions first!
+        name = '2010-05-14-%02d-%02d-%02d%s' % (hour, min, sec, ext)
+        if text is None:
+            text = name
+        fqn = os.path.join(self._repository_directory, name)
+        f = open(fqn, 'wb')
+        f.write(text)
+        f.flush()
+        f.close()
+        return fqn
+
+    def test_no_files(self):
+        from ZODB.scripts.repozo import NoFiles
+        options = self._makeOptions(date=None,
+                                    test_now=(2010, 5, 15, 13, 30, 57))
+        self.assertRaises(NoFiles, self._callFUT, options)
+
+    def test_no_files_before_explicit_date(self):
+        from ZODB.scripts.repozo import NoFiles
+        options = self._makeOptions(date='2010-05-13-13-30-57')
+        files = []
+        for h, m, s, e in [(2, 13, 14, '.fs'),
+                           (2, 13, 14, '.dat'),
+                           (3, 14, 15, '.deltafs'),
+                           (4, 14, 15, '.deltafs'),
+                           (5, 14, 15, '.deltafs'),
+                           (12, 13, 14, '.fs'),
+                           (12, 13, 14, '.dat'),
+                           (13, 14, 15, '.deltafs'),
+                           (14, 15, 16, '.deltafs'),
+                          ]:
+            files.append(self._makeFile(h, m, s, e))
+        self.assertRaises(NoFiles, self._callFUT, options)
+
+    def test_w_full_backup_latest_no_index(self):
+        import tempfile
+        dd = self._data_directory = tempfile.mkdtemp()
+        output = os.path.join(dd, 'Data.fs')
+        index = os.path.join(dd, 'Data.fs.index')
+        options = self._makeOptions(date='2010-05-15-13-30-57',
+                                    output=output)
+        self._makeFile(2, 3, 4, '.fs', 'AAA')
+        self._makeFile(4, 5, 6, '.fs', 'BBB')
+        self._callFUT(options)
+        self.assertEqual(open(output, 'rb').read(), 'BBB')
+
+    def test_w_full_backup_latest_index(self):
+        import tempfile
+        dd = self._data_directory = tempfile.mkdtemp()
+        output = os.path.join(dd, 'Data.fs')
+        index = os.path.join(dd, 'Data.fs.index')
+        options = self._makeOptions(date='2010-05-15-13-30-57',
+                                    output=output)
+        self._makeFile(2, 3, 4, '.fs', 'AAA')
+        self._makeFile(4, 5, 6, '.fs', 'BBB')
+        self._makeFile(4, 5, 6, '.index', 'CCC')
+        self._callFUT(options)
+        self.assertEqual(open(output, 'rb').read(), 'BBB')
+        self.assertEqual(open(index, 'rb').read(), 'CCC')
+
+    def test_w_incr_backup_latest_no_index(self):
+        import tempfile
+        dd = self._data_directory = tempfile.mkdtemp()
+        output = os.path.join(dd, 'Data.fs')
+        index = os.path.join(dd, 'Data.fs.index')
+        options = self._makeOptions(date='2010-05-15-13-30-57',
+                                    output=output)
+        self._makeFile(2, 3, 4, '.fs', 'AAA')
+        self._makeFile(4, 5, 6, '.deltafs', 'BBB')
+        self._callFUT(options)
+        self.assertEqual(open(output, 'rb').read(), 'AAABBB')
+
+    def test_w_incr_backup_latest_index(self):
+        import tempfile
+        dd = self._data_directory = tempfile.mkdtemp()
+        output = os.path.join(dd, 'Data.fs')
+        index = os.path.join(dd, 'Data.fs.index')
+        options = self._makeOptions(date='2010-05-15-13-30-57',
+                                    output=output)
+        self._makeFile(2, 3, 4, '.fs', 'AAA')
+        self._makeFile(4, 5, 6, '.deltafs', 'BBB')
+        self._makeFile(4, 5, 6, '.index', 'CCC')
+        self._callFUT(options)
+        self.assertEqual(open(output, 'rb').read(), 'AAABBB')
+        self.assertEqual(open(index, 'rb').read(), 'CCC')
+
 class MonteCarloTests(unittest.TestCase):
 
     layer = ZODB.tests.util.MininalTestLayer('repozo')
@@ -754,5 +885,9 @@
         unittest.makeSuite(Test_delete_old_backups),
         unittest.makeSuite(Test_do_full_backup),
         unittest.makeSuite(Test_do_incremental_backup),
+        #unittest.makeSuite(Test_do_backup),  #TODO
+        unittest.makeSuite(Test_do_recover),
+        # N.B.:  this test take forever to run (~40sec on a fast laptop),
+        # *and* it is non-deterministic.
         unittest.makeSuite(MonteCarloTests),
     ])
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.