2
0
Marian Beermann 9 жил өмнө
parent
commit
93b1cf3453

+ 10 - 8
src/borg/archive.py

@@ -1331,10 +1331,10 @@ class ArchiveRecreater:
         self.interrupt = False
         self.interrupt = False
         self.errors = 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)
         assert not self.is_temporary_archive(archive_name)
         archive = self.open_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:
         if self.exclude_if_present or self.exclude_caches:
             self.matcher_add_tagged_dirs(archive)
             self.matcher_add_tagged_dirs(archive)
         if self.matcher.empty() and not self.recompress and not target.recreate_rechunkify and comment is None:
         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)
             self.process_items(archive, target, resume_from)
         except self.Interrupted as e:
         except self.Interrupted as e:
             return self.save(archive, target, completed=False, metadata=e.metadata)
             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):
     def process_items(self, archive, target, resume_from=None):
         matcher = self.matcher
         matcher = self.matcher
@@ -1475,7 +1476,7 @@ class ArchiveRecreater:
         logger.debug('Copied %d chunks from a partially processed item', len(partial_chunks))
         logger.debug('Copied %d chunks from a partially processed item', len(partial_chunks))
         return 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."""
         """Save target archive. If completed, replace source. If not, save temporary with additional 'metadata' dict."""
         if self.dry_run:
         if self.dry_run:
             return completed
             return completed
@@ -1487,8 +1488,9 @@ class ArchiveRecreater:
                 'cmdline': archive.metadata[b'cmdline'],
                 'cmdline': archive.metadata[b'cmdline'],
                 'recreate_cmdline': sys.argv,
                 '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:
             if self.stats:
                 target.end = datetime.utcnow()
                 target.end = datetime.utcnow()
                 log_multi(DASHES,
                 log_multi(DASHES,
@@ -1540,11 +1542,11 @@ class ArchiveRecreater:
         matcher.add(tag_files, True)
         matcher.add(tag_files, True)
         matcher.add(tagged_dirs, False)
         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"""
         """Create new target archive or resume from temporary archive, if it exists. Return archive, resume from path"""
         if self.dry_run:
         if self.dry_run:
             return self.FakeTargetArchive(), None
             return self.FakeTargetArchive(), None
-        target_name = archive.name + '.recreate'
+        target_name = target_name or archive.name + '.recreate'
         resume = target_name in self.manifest.archives
         resume = target_name in self.manifest.archives
         target, resume_from = None, None
         target, resume_from = None, None
         if resume:
         if resume:

+ 10 - 1
src/borg/archiver.py

@@ -969,8 +969,11 @@ class Archiver:
                 if recreater.is_temporary_archive(name):
                 if recreater.is_temporary_archive(name):
                     self.print_error('Refusing to work on temporary archive of prior recreate: %s', name)
                     self.print_error('Refusing to work on temporary archive of prior recreate: %s', name)
                     return self.exit_code
                     return self.exit_code
-                recreater.recreate(name, args.comment)
+                recreater.recreate(name, args.comment, args.target)
             else:
             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'):
                 for archive in manifest.list_archive_infos(sort_by='ts'):
                     name = archive.name
                     name = archive.name
                     if recreater.is_temporary_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
         archive that is built during the operation exists at the same time at
         "<ARCHIVE>.recreate". The new archive will have a different archive ID.
         "<ARCHIVE>.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
         When rechunking space usage can be substantial, expect at least the entire
         deduplicated size of the archives using the previous chunker params.
         deduplicated size of the archives using the previous chunker params.
         When recompressing approximately 1 % of the repository size or 512 MB
         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')
                                    help='keep tag files of excluded caches/directories')
 
 
         archive_group = subparser.add_argument_group('Archive options')
         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,
         archive_group.add_argument('--comment', dest='comment', metavar='COMMENT', default=None,
                                    help='add a comment text to the archive')
                                    help='add a comment text to the archive')
         archive_group.add_argument('--timestamp', dest='timestamp',
         archive_group.add_argument('--timestamp', dest='timestamp',

+ 22 - 0
src/borg/testsuite/archiver.py

@@ -1522,6 +1522,28 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             self.cmd('init', self.repository_location, exit_code=1)
             self.cmd('init', self.repository_location, exit_code=1)
         assert not os.path.exists(self.repository_location)
         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):
     def test_recreate_basic(self):
         self.create_test_files()
         self.create_test_files()
         self.create_regular_file('dir2/file3', size=1024 * 80)
         self.create_regular_file('dir2/file3', size=1024 * 80)