diff --git a/setup.cfg b/setup.cfg index 3b57bf2eb..4316af2a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -121,6 +121,7 @@ per_file_ignores = src/borg/archiver/config.py:F405,E722 src/borg/archiver/common.py:E501,F405 src/borg/archiver/debug.py:F405 + src/borg/archiver/delete.py:F405 src/borg/archiver/diff.py:F405 src/borg/archiver/help.py:E501,F405 src/borg/archiver/keys.py:F405 diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index c27c1f12f..e96c4048b 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -28,7 +28,7 @@ try: from .common import with_repository, with_archive, Highlander from .. import __version__ from .. import helpers - from ..archive import Archive, ArchiveRecreater, Statistics, is_special + from ..archive import Archive, ArchiveRecreater, is_special from ..archive import BackupError, BackupOSError, backup_io, OsOpen, stat_update_check from ..archive import FilesystemObjectProcessors, MetadataCollector, ChunksProcessor from ..cache import Cache @@ -39,7 +39,7 @@ try: from ..helpers import location_validator, archivename_validator, ChunkerParams, Location from ..helpers import NameSpec, CommentSpec, FilesCacheMode from ..helpers import BaseFormatter, ItemFormatter, ArchiveFormatter - from ..helpers import format_timedelta, format_file_size, format_archive + from ..helpers import format_timedelta, format_file_size from ..helpers import remove_surrogates, bin_to_hex, eval_escapes from ..helpers import timestamp from ..helpers import get_cache_dir, os_stat @@ -93,6 +93,7 @@ from .check import CheckMixIn from .compact import CompactMixIn from .config import ConfigMixIn from .debug import DebugMixIn +from .delete import DeleteMixIn from .diff import DiffMixIn from .help import HelpMixIn from .keys import KeysMixIn @@ -112,6 +113,7 @@ class Archiver( ConfigMixIn, CompactMixIn, DebugMixIn, + DeleteMixIn, DiffMixIn, TarMixIn, BenchmarkMixIn, @@ -697,83 +699,6 @@ class Archiver( pi.finish() return self.exit_code - @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,)) - 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 - logger_list = logging.getLogger("borg.output.list") - for i, archive_name in enumerate(archive_names, 1): - try: - current_archive = manifest.archives.pop(archive_name) - except KeyError: - self.exit_code = EXIT_WARNING - logger.warning(f"Archive {archive_name} not found ({i}/{len(archive_names)}).") - else: - deleted = True - if self.output_list: - msg = "Would delete: {} ({}/{})" if dry_run else "Deleted archive: {} ({}/{})" - logger_list.info(msg.format(format_archive(current_archive), i, len(archive_names))) - if dry_run: - logger.info("Finished dry-run.") - elif deleted: - manifest.write() - # note: might crash in compact() after committing the repo - repository.commit(compact=False) - logger.warning('Done. Run "borg check --repair" to clean up the mess.') - else: - logger.warning("Aborted.") - return self.exit_code - - stats = Statistics(iec=args.iec) - with Cache(repository, key, manifest, progress=args.progress, lock_wait=self.lock_wait, iec=args.iec) as cache: - msg_delete = "Would delete archive: {} ({}/{})" if dry_run else "Deleting archive: {} ({}/{})" - msg_not_found = "Archive {} not found ({}/{})." - logger_list = logging.getLogger("borg.output.list") - delete_count = 0 - for i, archive_name in enumerate(archive_names, 1): - try: - archive_info = manifest.archives[archive_name] - except KeyError: - logger.warning(msg_not_found.format(archive_name, i, len(archive_names))) - else: - if self.output_list: - logger_list.info(msg_delete.format(format_archive(archive_info), i, len(archive_names))) - - if not dry_run: - archive = Archive( - repository, - key, - manifest, - archive_name, - cache=cache, - consider_part_files=args.consider_part_files, - ) - archive.delete(stats, progress=args.progress, forced=args.forced) - delete_count += 1 - if delete_count > 0: - # only write/commit if we actually changed something, see #6060. - manifest.write() - repository.commit(compact=False, save_space=args.save_space) - cache.commit() - if args.stats: - log_multi(str(stats), logger=logging.getLogger("borg.output.stats")) - - return self.exit_code - @with_repository(compatibility=(Manifest.Operation.READ,)) def do_list(self, args, repository, manifest, key): """List archive contents""" @@ -1672,78 +1597,10 @@ class Archiver( self.build_parser_config(subparsers, common_parser, mid_common_parser) self.build_parser_debug(subparsers, common_parser, mid_common_parser) + self.build_parser_delete(subparsers, common_parser, mid_common_parser) self.build_parser_help(subparsers, common_parser, mid_common_parser, parser) self.build_parser_rdelete(subparsers, common_parser, mid_common_parser, parser) - # 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 - deleted - the "Deleted data" deduplicated size there is most interesting as - that is how much your repository will shrink. - Please note that the "All archives" stats refer to the state after deletion. - - You can delete multiple archives by specifying a matching shell pattern, - using the ``--glob-archives GLOB`` option (for more info on these patterns, - see :ref:`borg_patterns`). - - 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__, - epilog=delete_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help="delete archive", - ) - subparser.set_defaults(func=self.do_delete) - 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( - "--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", - help="delete only the local cache for the given repository", - ) - 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( - "--keep-security-info", - dest="keep_security_info", - action="store_true", - 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" - ) - define_archive_filters_group(subparser) - # borg extract extract_epilog = process_epilog( """ diff --git a/src/borg/archiver/delete.py b/src/borg/archiver/delete.py new file mode 100644 index 000000000..3983b5a51 --- /dev/null +++ b/src/borg/archiver/delete.py @@ -0,0 +1,163 @@ +import argparse +import logging + +from .common import with_repository +from ..archive import Archive, Statistics +from ..cache import Cache +from ..constants import * # NOQA +from ..helpers import Manifest +from ..helpers import log_multi, format_archive + +from ..logger import create_logger + +logger = create_logger() + + +class DeleteMixIn: + @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,)) + 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 + logger_list = logging.getLogger("borg.output.list") + for i, archive_name in enumerate(archive_names, 1): + try: + current_archive = manifest.archives.pop(archive_name) + except KeyError: + self.exit_code = EXIT_WARNING + logger.warning(f"Archive {archive_name} not found ({i}/{len(archive_names)}).") + else: + deleted = True + if self.output_list: + msg = "Would delete: {} ({}/{})" if dry_run else "Deleted archive: {} ({}/{})" + logger_list.info(msg.format(format_archive(current_archive), i, len(archive_names))) + if dry_run: + logger.info("Finished dry-run.") + elif deleted: + manifest.write() + # note: might crash in compact() after committing the repo + repository.commit(compact=False) + logger.warning('Done. Run "borg check --repair" to clean up the mess.') + else: + logger.warning("Aborted.") + return self.exit_code + + stats = Statistics(iec=args.iec) + with Cache(repository, key, manifest, progress=args.progress, lock_wait=self.lock_wait, iec=args.iec) as cache: + msg_delete = "Would delete archive: {} ({}/{})" if dry_run else "Deleting archive: {} ({}/{})" + msg_not_found = "Archive {} not found ({}/{})." + logger_list = logging.getLogger("borg.output.list") + delete_count = 0 + for i, archive_name in enumerate(archive_names, 1): + try: + archive_info = manifest.archives[archive_name] + except KeyError: + logger.warning(msg_not_found.format(archive_name, i, len(archive_names))) + else: + if self.output_list: + logger_list.info(msg_delete.format(format_archive(archive_info), i, len(archive_names))) + + if not dry_run: + archive = Archive( + repository, + key, + manifest, + archive_name, + cache=cache, + consider_part_files=args.consider_part_files, + ) + archive.delete(stats, progress=args.progress, forced=args.forced) + delete_count += 1 + if delete_count > 0: + # only write/commit if we actually changed something, see #6060. + manifest.write() + repository.commit(compact=False, save_space=args.save_space) + cache.commit() + if args.stats: + log_multi(str(stats), logger=logging.getLogger("borg.output.stats")) + + return self.exit_code + + def build_parser_delete(self, subparsers, common_parser, mid_common_parser): + from .common import process_epilog, define_archive_filters_group + + 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 + deleted - the "Deleted data" deduplicated size there is most interesting as + that is how much your repository will shrink. + Please note that the "All archives" stats refer to the state after deletion. + + You can delete multiple archives by specifying a matching shell pattern, + using the ``--glob-archives GLOB`` option (for more info on these patterns, + see :ref:`borg_patterns`). + + 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__, + epilog=delete_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help="delete archive", + ) + subparser.set_defaults(func=self.do_delete) + 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( + "--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", + help="delete only the local cache for the given repository", + ) + 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( + "--keep-security-info", + dest="keep_security_info", + action="store_true", + 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" + ) + define_archive_filters_group(subparser)