diff --git a/src/borg/archive.py b/src/borg/archive.py index fc131d516..7a4d7e877 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1331,10 +1331,10 @@ class ArchiveRecreater: self.interrupt = False self.errors = False - def recreate(self, archive_name, comment=None): + def recreate(self, archive_name, comment=None, target_name=None): assert not self.is_temporary_archive(archive_name) archive = self.open_archive(archive_name) - target, resume_from = self.create_target_or_resume(archive) + target, resume_from = self.create_target_or_resume(archive, target_name) if self.exclude_if_present or self.exclude_caches: self.matcher_add_tagged_dirs(archive) if self.matcher.empty() and not self.recompress and not target.recreate_rechunkify and comment is None: @@ -1344,7 +1344,8 @@ class ArchiveRecreater: self.process_items(archive, target, resume_from) except self.Interrupted as e: return self.save(archive, target, completed=False, metadata=e.metadata) - return self.save(archive, target, comment) + replace_original = target_name is None + return self.save(archive, target, comment, replace_original=replace_original) def process_items(self, archive, target, resume_from=None): matcher = self.matcher @@ -1475,7 +1476,7 @@ class ArchiveRecreater: logger.debug('Copied %d chunks from a partially processed item', len(partial_chunks)) return partial_chunks - def save(self, archive, target, comment=None, completed=True, metadata=None): + def save(self, archive, target, comment=None, completed=True, metadata=None, replace_original=True): """Save target archive. If completed, replace source. If not, save temporary with additional 'metadata' dict.""" if self.dry_run: return completed @@ -1487,8 +1488,9 @@ class ArchiveRecreater: 'cmdline': archive.metadata[b'cmdline'], 'recreate_cmdline': sys.argv, }) - archive.delete(Statistics(), progress=self.progress) - target.rename(archive.name) + if replace_original: + archive.delete(Statistics(), progress=self.progress) + target.rename(archive.name) if self.stats: target.end = datetime.utcnow() log_multi(DASHES, @@ -1540,11 +1542,11 @@ class ArchiveRecreater: matcher.add(tag_files, True) matcher.add(tagged_dirs, False) - def create_target_or_resume(self, archive): + def create_target_or_resume(self, archive, target_name=None): """Create new target archive or resume from temporary archive, if it exists. Return archive, resume from path""" if self.dry_run: return self.FakeTargetArchive(), None - target_name = archive.name + '.recreate' + target_name = target_name or archive.name + '.recreate' resume = target_name in self.manifest.archives target, resume_from = None, None if resume: diff --git a/src/borg/archiver.py b/src/borg/archiver.py index b03b0a4c1..2e6c05f2c 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -969,8 +969,11 @@ class Archiver: if recreater.is_temporary_archive(name): self.print_error('Refusing to work on temporary archive of prior recreate: %s', name) return self.exit_code - recreater.recreate(name, args.comment) + recreater.recreate(name, args.comment, args.target) else: + if args.target is not None: + self.print_error('--target: Need to specify single archive') + return self.exit_code for archive in manifest.list_archive_infos(sort_by='ts'): name = archive.name if recreater.is_temporary_archive(name): @@ -2036,6 +2039,8 @@ class Archiver: archive that is built during the operation exists at the same time at ".recreate". The new archive will have a different archive ID. + With --target the original archive is not replaced, instead a new archive is created. + When rechunking space usage can be substantial, expect at least the entire deduplicated size of the archives using the previous chunker params. When recompressing approximately 1 % of the repository size or 512 MB @@ -2081,6 +2086,10 @@ class Archiver: help='keep tag files of excluded caches/directories') archive_group = subparser.add_argument_group('Archive options') + archive_group.add_argument('--target', dest='target', metavar='TARGET', default=None, + type=archivename_validator(), + help='create a new archive with the name ARCHIVE, do not replace existing archive ' + '(only applies for a single archive)') archive_group.add_argument('--comment', dest='comment', metavar='COMMENT', default=None, help='add a comment text to the archive') archive_group.add_argument('--timestamp', dest='timestamp', diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 950256be1..2df01b29e 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1522,6 +1522,28 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.cmd('init', self.repository_location, exit_code=1) assert not os.path.exists(self.repository_location) + def test_recreate_target_rc(self): + self.cmd('init', self.repository_location) + output = self.cmd('recreate', self.repository_location, '--target=asdf', exit_code=2) + assert 'Need to specify single archive' in output + + def test_recreate_target(self): + self.create_test_files() + self.cmd('init', self.repository_location) + archive = self.repository_location + '::test0' + self.cmd('create', archive, 'input') + original_archive = self.cmd('list', self.repository_location) + self.cmd('recreate', archive, 'input/dir2', '-e', 'input/dir2/file3', '--target=new-archive') + archives = self.cmd('list', self.repository_location) + assert original_archive in archives + assert 'new-archive' in archives + + archive = self.repository_location + '::new-archive' + 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_basic(self): self.create_test_files() self.create_regular_file('dir2/file3', size=1024 * 80)