|
@@ -2,6 +2,7 @@ from binascii import hexlify
|
|
|
from configparser import ConfigParser
|
|
|
import errno
|
|
|
import os
|
|
|
+import inspect
|
|
|
from io import StringIO
|
|
|
import random
|
|
|
import stat
|
|
@@ -17,7 +18,7 @@ from hashlib import sha256
|
|
|
import pytest
|
|
|
|
|
|
from .. import xattr
|
|
|
-from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP
|
|
|
+from ..archive import Archive, ChunkBuffer, ArchiveRecreater, CHUNK_MAX_EXP
|
|
|
from ..archiver import Archiver
|
|
|
from ..cache import Cache
|
|
|
from ..crypto import bytes_to_long, num_aes_blocks
|
|
@@ -235,9 +236,6 @@ class ArchiverTestCaseBase(BaseTestCase):
|
|
|
def create_src_archive(self, name):
|
|
|
self.cmd('create', self.repository_location + '::' + name, src_dir)
|
|
|
|
|
|
-
|
|
|
-class ArchiverTestCase(ArchiverTestCaseBase):
|
|
|
-
|
|
|
def create_regular_file(self, name, size=0, contents=None):
|
|
|
filename = os.path.join(self.input_path, name)
|
|
|
if not os.path.exists(os.path.dirname(filename)):
|
|
@@ -295,6 +293,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
|
|
have_root = False
|
|
|
return have_root
|
|
|
|
|
|
+
|
|
|
+class ArchiverTestCase(ArchiverTestCaseBase):
|
|
|
def test_basic_functionality(self):
|
|
|
have_root = self.create_test_files()
|
|
|
self.cmd('init', self.repository_location)
|
|
@@ -637,29 +637,56 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
|
|
self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2")
|
|
|
self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"])
|
|
|
|
|
|
- def test_exclude_caches(self):
|
|
|
+ def _create_test_caches(self):
|
|
|
self.cmd('init', self.repository_location)
|
|
|
self.create_regular_file('file1', size=1024 * 80)
|
|
|
self.create_regular_file('cache1/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
|
|
|
self.create_regular_file('cache2/CACHEDIR.TAG', contents=b'invalid signature')
|
|
|
- self.cmd('create', '--exclude-caches', self.repository_location + '::test', 'input')
|
|
|
+ os.mkdir('input/cache3')
|
|
|
+ os.link('input/cache1/CACHEDIR.TAG', 'input/cache3/CACHEDIR.TAG')
|
|
|
+
|
|
|
+ def _assert_test_caches(self):
|
|
|
with changedir('output'):
|
|
|
self.cmd('extract', self.repository_location + '::test')
|
|
|
self.assert_equal(sorted(os.listdir('output/input')), ['cache2', 'file1'])
|
|
|
self.assert_equal(sorted(os.listdir('output/input/cache2')), ['CACHEDIR.TAG'])
|
|
|
|
|
|
- def test_exclude_tagged(self):
|
|
|
+ def test_exclude_caches(self):
|
|
|
+ self._create_test_caches()
|
|
|
+ self.cmd('create', '--exclude-caches', self.repository_location + '::test', 'input')
|
|
|
+ self._assert_test_caches()
|
|
|
+
|
|
|
+ def test_recreate_exclude_caches(self):
|
|
|
+ self._create_test_caches()
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ self.cmd('recreate', '--exclude-caches', self.repository_location + '::test')
|
|
|
+ self._assert_test_caches()
|
|
|
+
|
|
|
+ def _create_test_tagged(self):
|
|
|
self.cmd('init', self.repository_location)
|
|
|
self.create_regular_file('file1', size=1024 * 80)
|
|
|
self.create_regular_file('tagged1/.NOBACKUP')
|
|
|
self.create_regular_file('tagged2/00-NOBACKUP')
|
|
|
self.create_regular_file('tagged3/.NOBACKUP/file2')
|
|
|
- self.cmd('create', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP', self.repository_location + '::test', 'input')
|
|
|
+
|
|
|
+ def _assert_test_tagged(self):
|
|
|
with changedir('output'):
|
|
|
self.cmd('extract', self.repository_location + '::test')
|
|
|
self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'tagged3'])
|
|
|
|
|
|
- def test_exclude_keep_tagged(self):
|
|
|
+ def test_exclude_tagged(self):
|
|
|
+ self._create_test_tagged()
|
|
|
+ self.cmd('create', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP', self.repository_location + '::test', 'input')
|
|
|
+ self._assert_test_tagged()
|
|
|
+
|
|
|
+ def test_recreate_exclude_tagged(self):
|
|
|
+ self._create_test_tagged()
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ self.cmd('recreate', '--exclude-if-present', '.NOBACKUP', '--exclude-if-present', '00-NOBACKUP',
|
|
|
+ self.repository_location + '::test')
|
|
|
+ self._assert_test_tagged()
|
|
|
+
|
|
|
+ def _create_test_keep_tagged(self):
|
|
|
self.cmd('init', self.repository_location)
|
|
|
self.create_regular_file('file0', size=1024)
|
|
|
self.create_regular_file('tagged1/.NOBACKUP1')
|
|
@@ -672,8 +699,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
|
|
self.create_regular_file('taggedall/.NOBACKUP2')
|
|
|
self.create_regular_file('taggedall/CACHEDIR.TAG', contents=b'Signature: 8a477f597d28d172789f06886806bc55 extra stuff')
|
|
|
self.create_regular_file('taggedall/file4', size=1024)
|
|
|
- self.cmd('create', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2',
|
|
|
- '--exclude-caches', '--keep-tag-files', self.repository_location + '::test', 'input')
|
|
|
+
|
|
|
+ def _assert_test_keep_tagged(self):
|
|
|
with changedir('output'):
|
|
|
self.cmd('extract', self.repository_location + '::test')
|
|
|
self.assert_equal(sorted(os.listdir('output/input')), ['file0', 'tagged1', 'tagged2', 'tagged3', 'taggedall'])
|
|
@@ -683,6 +710,19 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
|
|
self.assert_equal(sorted(os.listdir('output/input/taggedall')),
|
|
|
['.NOBACKUP1', '.NOBACKUP2', 'CACHEDIR.TAG', ])
|
|
|
|
|
|
+ def test_exclude_keep_tagged(self):
|
|
|
+ self._create_test_keep_tagged()
|
|
|
+ self.cmd('create', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2',
|
|
|
+ '--exclude-caches', '--keep-tag-files', self.repository_location + '::test', 'input')
|
|
|
+ self._assert_test_keep_tagged()
|
|
|
+
|
|
|
+ def test_recreate_exclude_keep_tagged(self):
|
|
|
+ self._create_test_keep_tagged()
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ self.cmd('recreate', '--exclude-if-present', '.NOBACKUP1', '--exclude-if-present', '.NOBACKUP2',
|
|
|
+ '--exclude-caches', '--keep-tag-files', self.repository_location + '::test')
|
|
|
+ self._assert_test_keep_tagged()
|
|
|
+
|
|
|
def test_path_normalization(self):
|
|
|
self.cmd('init', self.repository_location)
|
|
|
self.create_regular_file('dir1/dir2/file', size=1024 * 80)
|
|
@@ -880,6 +920,13 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
|
|
self.assert_in("U input/file1", output)
|
|
|
self.assert_in("x input/file2", output)
|
|
|
|
|
|
+ def test_create_delete_inbetween(self):
|
|
|
+ self.create_test_files()
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ self.cmd('create', self.repository_location + '::test1', 'input')
|
|
|
+ self.cmd('delete', self.repository_location + '::test1')
|
|
|
+ self.cmd('create', self.repository_location + '::test2', 'input')
|
|
|
+
|
|
|
def test_create_topical(self):
|
|
|
now = time.time()
|
|
|
self.create_regular_file('file1', size=1024 * 80)
|
|
@@ -1149,6 +1196,176 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
|
|
self.cmd('init', self.repository_location, exit_code=1)
|
|
|
assert not os.path.exists(self.repository_location)
|
|
|
|
|
|
+ def test_recreate_basic(self):
|
|
|
+ self.create_test_files()
|
|
|
+ self.create_regular_file('dir2/file3', size=1024 * 80)
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ archive = self.repository_location + '::test0'
|
|
|
+ self.cmd('create', archive, 'input')
|
|
|
+ self.cmd('recreate', archive, 'input/dir2', '-e', 'input/dir2/file3')
|
|
|
+ listing = self.cmd('list', '--short', archive)
|
|
|
+ assert 'file1' not in listing
|
|
|
+ assert 'dir2/file2' in listing
|
|
|
+ assert 'dir2/file3' not in listing
|
|
|
+
|
|
|
+ def test_recreate_subtree_hardlinks(self):
|
|
|
+ # This is essentially the same problem set as in test_extract_hardlinks
|
|
|
+ self._extract_hardlinks_setup()
|
|
|
+ self.cmd('create', self.repository_location + '::test2', 'input')
|
|
|
+ self.cmd('recreate', self.repository_location + '::test', 'input/dir1')
|
|
|
+ with changedir('output'):
|
|
|
+ self.cmd('extract', self.repository_location + '::test')
|
|
|
+ assert os.stat('input/dir1/hardlink').st_nlink == 2
|
|
|
+ assert os.stat('input/dir1/subdir/hardlink').st_nlink == 2
|
|
|
+ assert os.stat('input/dir1/aaaa').st_nlink == 2
|
|
|
+ assert os.stat('input/dir1/source2').st_nlink == 2
|
|
|
+ with changedir('output'):
|
|
|
+ self.cmd('extract', self.repository_location + '::test2')
|
|
|
+ assert os.stat('input/dir1/hardlink').st_nlink == 4
|
|
|
+
|
|
|
+ def test_recreate_rechunkify(self):
|
|
|
+ with open(os.path.join(self.input_path, 'large_file'), 'wb') as fd:
|
|
|
+ fd.write(b'a' * 250)
|
|
|
+ fd.write(b'b' * 250)
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ self.cmd('create', '--chunker-params', '7,9,8,128', self.repository_location + '::test1', 'input')
|
|
|
+ self.cmd('create', self.repository_location + '::test2', 'input', '--no-files-cache')
|
|
|
+ list = self.cmd('list', self.repository_location + '::test1', 'input/large_file',
|
|
|
+ '--format', '{num_chunks} {unique_chunks}')
|
|
|
+ num_chunks, unique_chunks = map(int, list.split(' '))
|
|
|
+ # test1 and test2 do not deduplicate
|
|
|
+ assert num_chunks == unique_chunks
|
|
|
+ self.cmd('recreate', self.repository_location, '--chunker-params', 'default')
|
|
|
+ # test1 and test2 do deduplicate after recreate
|
|
|
+ assert not int(self.cmd('list', self.repository_location + '::test1', 'input/large_file',
|
|
|
+ '--format', '{unique_chunks}'))
|
|
|
+
|
|
|
+ def test_recreate_recompress(self):
|
|
|
+ self.create_regular_file('compressible', size=10000)
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ list = self.cmd('list', self.repository_location + '::test', 'input/compressible',
|
|
|
+ '--format', '{size} {csize}')
|
|
|
+ size, csize = map(int, list.split(' '))
|
|
|
+ assert csize >= size
|
|
|
+ self.cmd('recreate', self.repository_location, '-C', 'lz4')
|
|
|
+ list = self.cmd('list', self.repository_location + '::test', 'input/compressible',
|
|
|
+ '--format', '{size} {csize}')
|
|
|
+ size, csize = map(int, list.split(' '))
|
|
|
+ assert csize < size
|
|
|
+
|
|
|
+ def test_recreate_dry_run(self):
|
|
|
+ self.create_regular_file('compressible', size=10000)
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ archives_before = self.cmd('list', self.repository_location + '::test')
|
|
|
+ self.cmd('recreate', self.repository_location, '-n', '-e', 'input/compressible')
|
|
|
+ archives_after = self.cmd('list', self.repository_location + '::test')
|
|
|
+ assert archives_after == archives_before
|
|
|
+
|
|
|
+ def _recreate_interrupt_patch(self, interrupt_after_n_1_files):
|
|
|
+ def interrupt(self, *args):
|
|
|
+ if interrupt_after_n_1_files:
|
|
|
+ self.interrupt = True
|
|
|
+ pi_save(self, *args)
|
|
|
+ else:
|
|
|
+ raise ArchiveRecreater.Interrupted
|
|
|
+
|
|
|
+ def process_item_patch(*args):
|
|
|
+ return pi_call.pop(0)(*args)
|
|
|
+
|
|
|
+ pi_save = ArchiveRecreater.process_item
|
|
|
+ pi_call = [pi_save] * interrupt_after_n_1_files + [interrupt]
|
|
|
+ return process_item_patch
|
|
|
+
|
|
|
+ def _test_recreate_interrupt(self, change_args, interrupt_early):
|
|
|
+ self.create_test_files()
|
|
|
+ self.create_regular_file('dir2/abcdef', size=1024 * 80)
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ process_files = 1
|
|
|
+ if interrupt_early:
|
|
|
+ process_files = 0
|
|
|
+ with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(process_files)):
|
|
|
+ self.cmd('recreate', '-sv', '--list', self.repository_location, 'input/dir2')
|
|
|
+ assert 'test.recreate' in self.cmd('list', self.repository_location)
|
|
|
+ if change_args:
|
|
|
+ with patch.object(sys, 'argv', sys.argv + ['non-forking tests don\'t use sys.argv']):
|
|
|
+ output = self.cmd('recreate', '-sv', '--list', '-pC', 'lz4', self.repository_location, 'input/dir2')
|
|
|
+ else:
|
|
|
+ output = self.cmd('recreate', '-sv', '--list', self.repository_location, 'input/dir2')
|
|
|
+ assert 'Found test.recreate, will resume' in output
|
|
|
+ assert change_args == ('Command line changed' in output)
|
|
|
+ if not interrupt_early:
|
|
|
+ assert 'Fast-forwarded to input/dir2/abcdef' in output
|
|
|
+ assert 'A input/dir2/abcdef' not in output
|
|
|
+ assert 'A input/dir2/file2' in output
|
|
|
+ archives = self.cmd('list', self.repository_location)
|
|
|
+ assert 'test.recreate' not in archives
|
|
|
+ assert 'test' in archives
|
|
|
+ files = self.cmd('list', self.repository_location + '::test')
|
|
|
+ assert 'dir2/file2' in files
|
|
|
+ assert 'dir2/abcdef' in files
|
|
|
+ assert 'file1' not in files
|
|
|
+
|
|
|
+ def test_recreate_interrupt(self):
|
|
|
+ self._test_recreate_interrupt(False, True)
|
|
|
+
|
|
|
+ def test_recreate_interrupt2(self):
|
|
|
+ self._test_recreate_interrupt(True, False)
|
|
|
+
|
|
|
+ def _test_recreate_chunker_interrupt_patch(self):
|
|
|
+ real_add_chunk = Cache.add_chunk
|
|
|
+
|
|
|
+ def add_chunk(*args, **kwargs):
|
|
|
+ frame = inspect.stack()[2]
|
|
|
+ try:
|
|
|
+ caller_self = frame[0].f_locals['self']
|
|
|
+ caller_self.interrupt = True
|
|
|
+ finally:
|
|
|
+ del frame
|
|
|
+ return real_add_chunk(*args, **kwargs)
|
|
|
+ return add_chunk
|
|
|
+
|
|
|
+ def test_recreate_rechunkify_interrupt(self):
|
|
|
+ self.create_regular_file('file1', size=1024 * 80)
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ archive_before = self.cmd('list', self.repository_location + '::test', '--format', '{sha512}')
|
|
|
+ with patch.object(Cache, 'add_chunk', self._test_recreate_chunker_interrupt_patch()):
|
|
|
+ self.cmd('recreate', '-p', '--chunker-params', '16,18,17,4095', self.repository_location)
|
|
|
+ assert 'test.recreate' in self.cmd('list', self.repository_location)
|
|
|
+ output = self.cmd('recreate', '-svp', '--debug', '--chunker-params', '16,18,17,4095', self.repository_location)
|
|
|
+ assert 'Found test.recreate, will resume' in output
|
|
|
+ assert 'Copied 1 chunks from a partially processed item' in output
|
|
|
+ archive_after = self.cmd('list', self.repository_location + '::test', '--format', '{sha512}')
|
|
|
+ assert archive_after == archive_before
|
|
|
+
|
|
|
+ def test_recreate_changed_source(self):
|
|
|
+ self.create_test_files()
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(1)):
|
|
|
+ self.cmd('recreate', self.repository_location, 'input/dir2')
|
|
|
+ assert 'test.recreate' in self.cmd('list', self.repository_location)
|
|
|
+ self.cmd('delete', self.repository_location + '::test')
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ output = self.cmd('recreate', self.repository_location, 'input/dir2')
|
|
|
+ assert 'Source archive changed, will discard test.recreate and start over' in output
|
|
|
+
|
|
|
+ def test_recreate_refuses_temporary(self):
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ self.cmd('recreate', self.repository_location + '::cba.recreate', exit_code=2)
|
|
|
+
|
|
|
+ def test_recreate_skips_nothing_to_do(self):
|
|
|
+ self.create_regular_file('file1', size=1024 * 80)
|
|
|
+ self.cmd('init', self.repository_location)
|
|
|
+ self.cmd('create', self.repository_location + '::test', 'input')
|
|
|
+ info_before = self.cmd('info', self.repository_location + '::test')
|
|
|
+ self.cmd('recreate', self.repository_location, '--chunker-params', 'default')
|
|
|
+ info_after = self.cmd('info', self.repository_location + '::test')
|
|
|
+ assert info_before == info_after # includes archive ID
|
|
|
+
|
|
|
|
|
|
@unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
|
|
|
class ArchiverTestCaseBinary(ArchiverTestCase):
|
|
@@ -1159,6 +1376,18 @@ class ArchiverTestCaseBinary(ArchiverTestCase):
|
|
|
def test_init_interrupt(self):
|
|
|
pass
|
|
|
|
|
|
+ @unittest.skip('patches objects')
|
|
|
+ def test_recreate_rechunkify_interrupt(self):
|
|
|
+ pass
|
|
|
+
|
|
|
+ @unittest.skip('patches objects')
|
|
|
+ def test_recreate_interrupt(self):
|
|
|
+ pass
|
|
|
+
|
|
|
+ @unittest.skip('patches objects')
|
|
|
+ def test_recreate_changed_source(self):
|
|
|
+ pass
|
|
|
+
|
|
|
|
|
|
class ArchiverCheckTestCase(ArchiverTestCaseBase):
|
|
|
|
|
@@ -1274,9 +1503,6 @@ class RemoteArchiverTestCase(ArchiverTestCase):
|
|
|
|
|
|
|
|
|
class DiffArchiverTestCase(ArchiverTestCaseBase):
|
|
|
- create_test_files = ArchiverTestCase.create_test_files
|
|
|
- create_regular_file = ArchiverTestCase.create_regular_file
|
|
|
-
|
|
|
def test_basic_functionality(self):
|
|
|
# Initialize test folder
|
|
|
self.create_test_files()
|