瀏覽代碼

borg delete -a ARCH_GLOB, borg rdelete

Thomas Waldmann 2 年之前
父節點
當前提交
34b6248d75
共有 2 個文件被更改,包括 124 次插入118 次删除
  1. 104 99
      src/borg/archiver.py
  2. 20 19
      src/borg/testsuite/archiver.py

+ 104 - 99
src/borg/archiver.py

@@ -679,7 +679,7 @@ class Archiver:
             # now build files cache
             rc1 = self.do_create(self.parse_args([f'--repo={repo}', 'create', compression,
                                                   'borg-benchmark-crud2', path]))
-            rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud2']))
+            rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '-a', 'borg-benchmark-crud2']))
             assert rc1 == rc2 == 0
             # measure a no-change update (archive1 is still present)
             t_start = time.monotonic()
@@ -687,7 +687,7 @@ class Archiver:
                                                   'borg-benchmark-crud3', path]))
             t_end = time.monotonic()
             dt_update = t_end - t_start
-            rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud3']))
+            rc2 = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '-a', 'borg-benchmark-crud3']))
             assert rc1 == rc2 == 0
             # measure extraction (dry-run: without writing result to disk)
             t_start = time.monotonic()
@@ -698,7 +698,7 @@ class Archiver:
             assert rc == 0
             # measure archive deletion (of LAST present archive with the data)
             t_start = time.monotonic()
-            rc = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '--name=borg-benchmark-crud1']))
+            rc = self.do_delete(self.parse_args([f'--repo={repo}', 'delete', '-a', 'borg-benchmark-crud1']))
             t_end = time.monotonic()
             dt_delete = t_end - t_start
             assert rc == 0
@@ -1515,35 +1515,80 @@ class Archiver:
         return self.exit_code
 
     @with_repository(exclusive=True, manifest=False)
-    def do_delete(self, args, repository):
-        """Delete an existing repository or archives"""
-        archive_filter_specified = any((args.first, args.last, args.prefix is not None, args.glob_archives))
-        explicit_archives_specified = args.name or args.archives
+    def do_rdelete(self, args, repository):
+        """Delete a repository"""
         self.output_list = args.output_list
-        if archive_filter_specified and explicit_archives_specified:
-            self.print_error('Mixing archive filters and explicitly named archives is not supported.')
-            return self.exit_code
-        if archive_filter_specified or explicit_archives_specified:
-            return self._delete_archives(args, repository)
+        dry_run = args.dry_run
+        keep_security_info = args.keep_security_info
+
+        if not args.cache_only:
+            if args.forced == 0:  # without --force, we let the user see the archives list and confirm.
+                id = bin_to_hex(repository.id)
+                location = repository._location.canonical_path()
+                msg = []
+                try:
+                    manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)
+                    n_archives = len(manifest.archives)
+                    msg.append(f"You requested to completely DELETE the following repository "
+                               f"*including* {n_archives} archives it contains:")
+                except NoManifestError:
+                    n_archives = None
+                    msg.append("You requested to completely DELETE the following repository "
+                               "*including* all archives it may contain:")
+
+                msg.append(DASHES)
+                msg.append(f"Repository ID: {id}")
+                msg.append(f"Location: {location}")
+
+                if self.output_list:
+                    msg.append("")
+                    msg.append("Archives:")
+
+                    if n_archives is not None:
+                        if n_archives > 0:
+                            for archive_info in manifest.archives.list(sort_by=['ts']):
+                                msg.append(format_archive(archive_info))
+                        else:
+                            msg.append("This repository seems to not have any archives.")
+                    else:
+                        msg.append("This repository seems to have no manifest, so we can't "
+                                   "tell anything about its contents.")
+
+                msg.append(DASHES)
+                msg.append("Type 'YES' if you understand this and want to continue: ")
+                msg = '\n'.join(msg)
+                if not yes(msg, false_msg="Aborting.", invalid_msg='Invalid answer, aborting.', truish=('YES',),
+                           retry=False, env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'):
+                    self.exit_code = EXIT_ERROR
+                    return self.exit_code
+            if not dry_run:
+                repository.destroy()
+                logger.info("Repository deleted.")
+                if not keep_security_info:
+                    SecurityManager.destroy(repository)
+            else:
+                logger.info("Would delete repository.")
+                logger.info("Would %s security info." % ("keep" if keep_security_info else "delete"))
+        if not dry_run:
+            Cache.destroy(repository)
+            logger.info("Cache deleted.")
         else:
-            return self._delete_repository(args, repository)
+            logger.info("Would delete cache.")
+        return self.exit_code
 
-    def _delete_archives(self, args, repository):
+    @with_repository(exclusive=True, manifest=False)
+    def do_delete(self, args, repository):
         """Delete archives"""
+        self.output_list = args.output_list
         dry_run = args.dry_run
-
         manifest, key = Manifest.load(repository, (Manifest.Operation.DELETE,))
-
-        if args.name or args.archives:
-            archives = list(args.archives)
-            if args.name:
-                archives.insert(0, args.name)
-            archive_names = tuple(archives)
-        else:
-            args.consider_checkpoints = True
-            archive_names = tuple(x.name for x in manifest.archives.list_considering(args))
-            if not archive_names:
-                return self.exit_code
+        archive_names = tuple(x.name for x in manifest.archives.list_considering(args))
+        if not archive_names:
+            return self.exit_code
+        if args.glob_archives is None and args.first == 0 and args.last == 0:
+            self.print_error("Aborting: if you really want to delete all archives, please use -a '*' "
+                             "or just delete the whole repository (might be much faster).")
+            return EXIT_ERROR
 
         if args.forced == 2:
             deleted = False
@@ -1605,66 +1650,6 @@ class Archiver:
 
         return self.exit_code
 
-    def _delete_repository(self, args, repository):
-        """Delete a repository"""
-        dry_run = args.dry_run
-        keep_security_info = args.keep_security_info
-
-        if not args.cache_only:
-            if args.forced == 0:  # without --force, we let the user see the archives list and confirm.
-                id = bin_to_hex(repository.id)
-                location = repository._location.canonical_path()
-                msg = []
-                try:
-                    manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK)
-                    n_archives = len(manifest.archives)
-                    msg.append(f"You requested to completely DELETE the following repository "
-                               f"*including* {n_archives} archives it contains:")
-                except NoManifestError:
-                    n_archives = None
-                    msg.append("You requested to completely DELETE the following repository "
-                               "*including* all archives it may contain:")
-
-                msg.append(DASHES)
-                msg.append(f"Repository ID: {id}")
-                msg.append(f"Location: {location}")
-
-                if self.output_list:
-                    msg.append("")
-                    msg.append("Archives:")
-
-                    if n_archives is not None:
-                        if n_archives > 0:
-                            for archive_info in manifest.archives.list(sort_by=['ts']):
-                                msg.append(format_archive(archive_info))
-                        else:
-                            msg.append("This repository seems to not have any archives.")
-                    else:
-                        msg.append("This repository seems to have no manifest, so we can't "
-                                   "tell anything about its contents.")
-
-                msg.append(DASHES)
-                msg.append("Type 'YES' if you understand this and want to continue: ")
-                msg = '\n'.join(msg)
-                if not yes(msg, false_msg="Aborting.", invalid_msg='Invalid answer, aborting.', truish=('YES',),
-                           retry=False, env_var_override='BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'):
-                    self.exit_code = EXIT_ERROR
-                    return self.exit_code
-            if not dry_run:
-                repository.destroy()
-                logger.info("Repository deleted.")
-                if not keep_security_info:
-                    SecurityManager.destroy(repository)
-            else:
-                logger.info("Would delete repository.")
-                logger.info("Would %s security info." % ("keep" if keep_security_info else "delete"))
-        if not dry_run:
-            Cache.destroy(repository)
-            logger.info("Cache deleted.")
-        else:
-            logger.info("Would delete cache.")
-        return self.exit_code
-
     def do_mount(self, args):
         """Mount archive or an entire repository as a FUSE filesystem"""
         # Perform these checks before opening the repository and asking for a passphrase.
@@ -4062,18 +4047,42 @@ class Archiver:
         subparser.add_argument('output', metavar='OUTPUT', type=argparse.FileType('wb'),
                                help='Output file')
 
-        # borg delete
-        delete_epilog = process_epilog("""
-        This command deletes an archive from the repository or the complete repository.
-
-        Important: When deleting archives, repository disk space is **not** freed until
-        you run ``borg compact``.
+        # borg rdelete
+        rdelete_epilog = process_epilog("""
+        This command deletes the complete repository.
 
         When you delete a complete repository, the security info and local cache for it
         (if any) are also deleted. Alternatively, you can delete just the local cache
         with the ``--cache-only`` option, or keep the security info with the
         ``--keep-security-info`` option.
 
+        Always first use ``--dry-run --list`` to see what would be deleted.
+        """)
+        subparser = subparsers.add_parser('rdelete', parents=[common_parser], add_help=False,
+                                          description=self.do_rdelete.__doc__,
+                                          epilog=rdelete_epilog,
+                                          formatter_class=argparse.RawDescriptionHelpFormatter,
+                                          help='delete repository')
+        subparser.set_defaults(func=self.do_rdelete)
+        subparser.add_argument('-n', '--dry-run', dest='dry_run', action='store_true',
+                               help='do not change repository')
+        subparser.add_argument('--list', dest='output_list', action='store_true',
+                               help='output verbose list of archives')
+        subparser.add_argument('--force', dest='forced', action='count', default=0,
+                               help='force deletion of corrupted archives, '
+                                    'use ``--force --force`` in case ``--force`` does not work.')
+        subparser.add_argument('--cache-only', dest='cache_only', action='store_true',
+                               help='delete only the local cache for the given repository')
+        subparser.add_argument('--keep-security-info', dest='keep_security_info', action='store_true',
+                               help='keep the local security info when deleting a repository')
+
+        # borg delete
+        delete_epilog = process_epilog("""
+        This command deletes archives from the repository.
+
+        Important: When deleting archives, repository disk space is **not** freed until
+        you run ``borg compact``.
+
         When in doubt, use ``--dry-run --list`` to see what would be deleted.
 
         When using ``--stats``, you will get some statistics about how much data was
@@ -4087,9 +4096,7 @@ class Archiver:
         (for more info on these patterns, see :ref:`borg_patterns`). Note that these
         two options are mutually exclusive.
 
-        To avoid accidentally deleting archives, especially when using glob patterns,
-        it might be helpful to use the ``--dry-run`` to test out the command without
-        actually making any changes to the repository.
+        Always first use ``--dry-run --list`` to see what would be deleted.
         """)
         subparser = subparsers.add_parser('delete', parents=[common_parser], add_help=False,
                                           description=self.do_delete.__doc__,
@@ -4101,6 +4108,8 @@ class Archiver:
                                help='do not change repository')
         subparser.add_argument('--list', dest='output_list', action='store_true',
                                help='output verbose list of archives')
+        subparser.add_argument('--consider-checkpoints', action='store_true', dest='consider_checkpoints',
+                               help='consider checkpoint archives for deletion (default: not considered).')
         subparser.add_argument('-s', '--stats', dest='stats', action='store_true',
                                help='print statistics for the deleted archive')
         subparser.add_argument('--cache-only', dest='cache_only', action='store_true',
@@ -4112,10 +4121,6 @@ class Archiver:
                                help='keep the local security info when deleting a repository')
         subparser.add_argument('--save-space', dest='save_space', action='store_true',
                                help='work slower, but using less space')
-        subparser.add_argument('--name', dest='name', metavar='NAME', type=NameSpec,
-                               help='specify the archive name')
-        subparser.add_argument('archives', metavar='ARCHIVE', nargs='*',
-                               help='archives to delete')
         define_archive_filters_group(subparser)
 
         # borg transfer

+ 20 - 19
src/borg/testsuite/archiver.py

@@ -710,7 +710,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=none')
         self._set_repository_id(self.repository_path, repository_id)
         self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only')
+        self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only')
         if self.FORK_DEFAULT:
             self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input', exit_code=EXIT_ERROR)
         else:
@@ -723,8 +723,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         os.environ['BORG_PASSPHRASE'] = 'passphrase'
         self.cmd(f'--repo={self.repository_location}_encrypted', 'init', '--encryption=repokey')
         self.cmd(f'--repo={self.repository_location}_encrypted', 'create', 'test', 'input')
-        self.cmd(f'--repo={self.repository_location}_unencrypted', 'delete', '--cache-only')
-        self.cmd(f'--repo={self.repository_location}_encrypted', 'delete', '--cache-only')
+        self.cmd(f'--repo={self.repository_location}_unencrypted', 'rdelete', '--cache-only')
+        self.cmd(f'--repo={self.repository_location}_encrypted', 'rdelete', '--cache-only')
         shutil.rmtree(self.repository_path + '_encrypted')
         os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
         if self.FORK_DEFAULT:
@@ -744,7 +744,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         with environment_variable(BORG_PASSPHRASE=''):
             self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')
             # Delete cache & security database, AKA switch to user perspective
-            self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only')
+            self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only')
             shutil.rmtree(self.get_security_dir())
         with environment_variable(BORG_PASSPHRASE=None):
             # This is the part were the user would be tricked, e.g. she assumes that BORG_PASSPHRASE
@@ -1276,7 +1276,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
     def test_create_no_cache_sync(self):
         self.create_test_files()
         self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only')
+        self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only')
         create_json = json.loads(self.cmd(f'--repo={self.repository_location}', 'create',
                                           '--no-cache-sync', '--json', '--error',
                                           'test', 'input'))  # ignore experimental warning
@@ -1284,7 +1284,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         create_stats = create_json['cache']['stats']
         info_stats = info_json['cache']['stats']
         assert create_stats == info_stats
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only')
+        self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only')
         self.cmd(f'--repo={self.repository_location}', 'create', '--no-cache-sync', 'test2', 'input')
         self.cmd(f'--repo={self.repository_location}', 'rinfo')
         self.cmd(f'--repo={self.repository_location}', 'check')
@@ -1601,9 +1601,9 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd(f'--repo={self.repository_location}', 'extract', 'test.2', '--dry-run')
         self.cmd(f'--repo={self.repository_location}', 'delete', '--prefix', 'another_')
         self.cmd(f'--repo={self.repository_location}', 'delete', '--last', '1')
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test')
+        self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test')
         self.cmd(f'--repo={self.repository_location}', 'extract', 'test.2', '--dry-run')
-        output = self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test.2', '--stats')
+        output = self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test.2', '--stats')
         self.assert_in('Deleted data:', output)
         # Make sure all data except the manifest has been deleted
         with Repository(self.repository_path) as repository:
@@ -1615,9 +1615,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd(f'--repo={self.repository_location}', 'create', 'test1', 'input')
         self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input')
         self.cmd(f'--repo={self.repository_location}', 'create', 'test3', 'input')
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test1', 'test2')
+        self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test1')
+        self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test2')
         self.cmd(f'--repo={self.repository_location}', 'extract', 'test3', '--dry-run')
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test3')
+        self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test3')
         assert not self.cmd(f'--repo={self.repository_location}', 'rlist')
 
     def test_delete_repo(self):
@@ -1627,10 +1628,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input')
         self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input')
         os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'no'
-        self.cmd(f'--repo={self.repository_location}', 'delete', exit_code=2)
+        self.cmd(f'--repo={self.repository_location}', 'rdelete', exit_code=2)
         assert os.path.exists(self.repository_path)
         os.environ['BORG_DELETE_I_KNOW_WHAT_I_AM_DOING'] = 'YES'
-        self.cmd(f'--repo={self.repository_location}', 'delete')
+        self.cmd(f'--repo={self.repository_location}', 'rdelete')
         # Make sure the repo is gone
         self.assertFalse(os.path.exists(self.repository_path))
 
@@ -1647,7 +1648,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             else:
                 assert False  # missed the file
             repository.commit(compact=False)
-        output = self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test', '--force')
+        output = self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test', '--force')
         self.assert_in('deleted archive was corrupted', output)
         self.cmd(f'--repo={self.repository_location}', 'check', '--repair')
         output = self.cmd(f'--repo={self.repository_location}', 'rlist')
@@ -1662,7 +1663,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             id = archive.metadata.items[0]
             repository.put(id, b'corrupted items metadata stream chunk')
             repository.commit(compact=False)
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test', '--force', '--force')
+        self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test', '--force', '--force')
         self.cmd(f'--repo={self.repository_location}', 'check', '--repair')
         output = self.cmd(f'--repo={self.repository_location}', 'rlist')
         self.assert_not_in('test', output)
@@ -1831,7 +1832,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
 
     def test_unknown_feature_on_cache_sync(self):
         self.cmd(f'--repo={self.repository_location}', 'init', '--encryption=repokey')
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only')
+        self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only')
         self.add_unknown_feature(Manifest.Operation.READ)
         self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'create', 'test', 'input'])
 
@@ -1861,10 +1862,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd(f'--repo={self.repository_location}', 'create', 'test', 'input')
         self.add_unknown_feature(Manifest.Operation.DELETE)
         # delete of an archive raises
-        self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'delete', '--name=test'])
+        self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'delete', '-a', 'test'])
         self.cmd_raises_unknown_feature([f'--repo={self.repository_location}', 'prune', '--keep-daily=3'])
         # delete of the whole repository ignores features
-        self.cmd(f'--repo={self.repository_location}', 'delete')
+        self.cmd(f'--repo={self.repository_location}', 'rdelete')
 
     @unittest.skipUnless(llfuse, 'llfuse not installed')
     def test_unknown_feature_on_mount(self):
@@ -2784,7 +2785,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         verify_uniqueness()
         self.cmd(f'--repo={self.repository_location}', 'create', 'test.2', 'input')
         verify_uniqueness()
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--name=test.2')
+        self.cmd(f'--repo={self.repository_location}', 'delete', '-a', 'test.2')
         verify_uniqueness()
 
     def test_aes_counter_uniqueness_keyfile(self):
@@ -4042,7 +4043,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase):
         self.cmd(f'--repo={self.repository_location}', 'create', 'test2', 'input')
 
         # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d
-        self.cmd(f'--repo={self.repository_location}', 'delete', '--cache-only')
+        self.cmd(f'--repo={self.repository_location}', 'rdelete', '--cache-only')
         self.cmd(f'--repo={self.repository_location}', 'rinfo', '--json')
 
         chunks_archive = os.path.join(self.cache_path, 'chunks.archive.d')