Commit b94910cb authored by Nicolas Wavrant's avatar Nicolas Wavrant

repozo: recover ZODB in a temporary file then rename it

So if recovering a ZODB fails for any reason, it doesn't leave behind
a partial file which may be confused with the recovered file (as it
bears same name).
parent 949a4209
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
5.5.2 (unreleased) 5.5.2 (unreleased)
================== ==================
- Make repozo's recover mode atomic by recovering the backup in a
temporary file which is then moved to the expected output file.
- Add a new option to repozo in recover mode which allows to verify - Add a new option to repozo in recover mode which allows to verify
backups integrity on the fly. backups integrity on the fly.
......
...@@ -77,7 +77,8 @@ Options for -R/--recover: ...@@ -77,7 +77,8 @@ Options for -R/--recover:
--with-verification --with-verification
Verify on the fly the backup files on recovering. This option runs Verify on the fly the backup files on recovering. This option runs
the same checks as when repozo is run in -V/--verify mode, and the same checks as when repozo is run in -V/--verify mode, and
allows to verify and recover a backup in one single step. allows to verify and recover a backup in one single step. If a sanity
check fails, the partially recovered ZODB will be left in place.
Options for -V/--verify: Options for -V/--verify:
-Q / --quick -Q / --quick
...@@ -665,8 +666,14 @@ def do_recover(options): ...@@ -665,8 +666,14 @@ def do_recover(options):
log('Recovering file to stdout') log('Recovering file to stdout')
outfp = sys.stdout outfp = sys.stdout
else: else:
# Delete old ZODB before recovering backup as size of
# old ZODB + full partial file may be superior to free disk space
if os.path.exists(options.output):
log('Deleting old %s', options.output)
os.unlink(options.output)
log('Recovering file to %s', options.output) log('Recovering file to %s', options.output)
outfp = open(options.output, 'wb') temporary_output_file = options.output + '.part'
outfp = open(temporary_output_file, 'wb')
if options.withverify: if options.withverify:
datfile = os.path.splitext(repofiles[0])[0] + '.dat' datfile = os.path.splitext(repofiles[0])[0] + '.dat'
with open(datfile) as fp: with open(datfile) as fp:
...@@ -699,8 +706,6 @@ def do_recover(options): ...@@ -699,8 +706,6 @@ def do_recover(options):
else: else:
reposz, reposum = concat(repofiles, outfp) reposz, reposum = concat(repofiles, outfp)
log('Recovered %s bytes, md5: %s', reposz, reposum) log('Recovered %s bytes, md5: %s', reposz, reposum)
if outfp != sys.stdout:
outfp.close()
if options.output is not None: if options.output is not None:
last_base = os.path.splitext(repofiles[-1])[0] last_base = os.path.splitext(repofiles[-1])[0]
...@@ -712,6 +717,15 @@ def do_recover(options): ...@@ -712,6 +717,15 @@ def do_recover(options):
else: else:
log('No index file to restore: %s', source_index) log('No index file to restore: %s', source_index)
if outfp != sys.stdout:
outfp.close()
try:
os.rename(temporary_output_file, options.output)
except OSError:
log("ZODB has been fully recovered as %s, but it cannot be renamed into : %s",
temporary_output_file, options.output)
raise
def do_verify(options): def do_verify(options):
# Verify the sizes and checksums of all files mentioned in the .dat file # Verify the sizes and checksums of all files mentioned in the .dat file
......
...@@ -936,6 +936,7 @@ class Test_do_recover(OptionsTestBase, unittest.TestCase): ...@@ -936,6 +936,7 @@ class Test_do_recover(OptionsTestBase, unittest.TestCase):
'/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n' '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'
'/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec60\n') '/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec60\n')
self._callFUT(options) self._callFUT(options)
self.assertFalse(os.path.exists(output + '.part'))
self.assertEqual(_read_file(output), b'AAABBBB') self.assertEqual(_read_file(output), b'AAABBBB')
def test_w_incr_backup_with_verify_sum_inconsistent(self): def test_w_incr_backup_with_verify_sum_inconsistent(self):
...@@ -953,6 +954,7 @@ class Test_do_recover(OptionsTestBase, unittest.TestCase): ...@@ -953,6 +954,7 @@ class Test_do_recover(OptionsTestBase, unittest.TestCase):
'/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n' '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'
'/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec61\n') '/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec61\n')
self.assertRaises(VerificationFail, self._callFUT, options) self.assertRaises(VerificationFail, self._callFUT, options)
self.assertTrue(os.path.exists(output + '.part'))
def test_w_incr_backup_with_verify_size_inconsistent(self): def test_w_incr_backup_with_verify_size_inconsistent(self):
import tempfile import tempfile
...@@ -969,6 +971,7 @@ class Test_do_recover(OptionsTestBase, unittest.TestCase): ...@@ -969,6 +971,7 @@ class Test_do_recover(OptionsTestBase, unittest.TestCase):
'/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n' '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'
'/backup/2010-05-14-04-05-06.deltafs 3 8 f50881ced34c7d9e6bce100bf33dec60\n') '/backup/2010-05-14-04-05-06.deltafs 3 8 f50881ced34c7d9e6bce100bf33dec60\n')
self.assertRaises(VerificationFail, self._callFUT, options) self.assertRaises(VerificationFail, self._callFUT, options)
self.assertTrue(os.path.exists(output + '.part'))
class Test_do_verify(OptionsTestBase, unittest.TestCase): class Test_do_verify(OptionsTestBase, unittest.TestCase):
......
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