From b14c050f690aa249291126a491da4193fedd77a0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 1 Sep 2024 02:07:32 +0200 Subject: [PATCH] rspace: manage reserved space in repository --- src/borg/archiver/__init__.py | 3 + src/borg/archiver/rcreate_cmd.py | 8 ++- src/borg/archiver/rspace_cmd.py | 110 +++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/borg/archiver/rspace_cmd.py diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 2279b90dd..e18fef3d0 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -86,6 +86,7 @@ from .rcreate_cmd import RCreateMixIn from .rinfo_cmd import RInfoMixIn from .rdelete_cmd import RDeleteMixIn from .rlist_cmd import RListMixIn +from .rspace_cmd import RSpaceMixIn from .serve_cmd import ServeMixIn from .tar_cmds import TarMixIn from .transfer_cmd import TransferMixIn @@ -115,6 +116,7 @@ class Archiver( RDeleteMixIn, RInfoMixIn, RListMixIn, + RSpaceMixIn, ServeMixIn, TarMixIn, TransferMixIn, @@ -351,6 +353,7 @@ class Archiver( self.build_parser_rlist(subparsers, common_parser, mid_common_parser) self.build_parser_recreate(subparsers, common_parser, mid_common_parser) self.build_parser_rename(subparsers, common_parser, mid_common_parser) + self.build_parser_rspace(subparsers, common_parser, mid_common_parser) self.build_parser_serve(subparsers, common_parser, mid_common_parser) self.build_parser_tar(subparsers, common_parser, mid_common_parser) self.build_parser_transfer(subparsers, common_parser, mid_common_parser) diff --git a/src/borg/archiver/rcreate_cmd.py b/src/borg/archiver/rcreate_cmd.py index 1693dd9a2..39597a848 100644 --- a/src/borg/archiver/rcreate_cmd.py +++ b/src/borg/archiver/rcreate_cmd.py @@ -48,8 +48,14 @@ class RCreateMixIn: " borg key export -r REPOSITORY encrypted-key-backup\n" " borg key export -r REPOSITORY --paper encrypted-key-backup.txt\n" " borg key export -r REPOSITORY --qr-html encrypted-key-backup.html\n" - "2. Write down the borg key passphrase and store it at safe place.\n" + "2. Write down the borg key passphrase and store it at safe place." ) + logger.warning( + "\n" + "Reserve some repository storage space now for emergencies like 'disk full'\n" + "by running:\n" + " borg rspace --reserve 1G" + ) def build_parser_rcreate(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog diff --git a/src/borg/archiver/rspace_cmd.py b/src/borg/archiver/rspace_cmd.py new file mode 100644 index 000000000..8352590d8 --- /dev/null +++ b/src/borg/archiver/rspace_cmd.py @@ -0,0 +1,110 @@ +import argparse +import math +import os + +from ._common import with_repository, Highlander +from ..constants import * # NOQA +from ..helpers import parse_file_size, format_file_size + +from ..logger import create_logger + +logger = create_logger() + + +class RSpaceMixIn: + @with_repository(lock=False, manifest=False) + def do_rspace(self, args, repository): + """Manage reserved space in repository""" + # we work without locking here because locks don't work with full disk. + if args.reserve_space > 0: + storage_space_reserve_object_size = 64 * 2**20 # 64 MiB per object + count = math.ceil(float(args.reserve_space) / storage_space_reserve_object_size) # round up + size = 0 + for i in range(count): + data = os.urandom(storage_space_reserve_object_size) # counter-act fs compression/dedup + repository.store_store(f"config/space-reserve.{i}", data) + size += len(data) + print(f"There is {format_file_size(size, iec=False)} reserved space in this repository now.") + elif args.free_space: + infos = repository.store_list("config") + size = 0 + for info in infos: + if info.name.startswith("space-reserve."): + size += info.size + repository.store_delete(f"config/{info.name}") + print(f"Freed {format_file_size(size, iec=False)} in repository.") + print("Now run borg prune or borg delete plus borg compact to free more space.") + print("After that, do not forget to reserve space again for next time!") + else: # print amount currently reserved + infos = repository.store_list("config") + size = 0 + for info in infos: + if info.name.startswith("space-reserve."): + size += info.size + print(f"There is {format_file_size(size, iec=False)} reserved space in this repository.") + print("In case you want to change the amount, use --free first to free all reserved space,") + print("then use --reserve with the desired amount.") + + def build_parser_rspace(self, subparsers, common_parser, mid_common_parser): + from ._common import process_epilog + + rspace_epilog = process_epilog( + """ + This command manages reserved space in a repository. + + Borg can not work in disk-full conditions (can not lock a repo and thus can + not run prune/delete or compact operations to free disk space). + + To avoid running into dead-end situations like that, you can put some objects + into a repository that take up some disk space. If you ever run into a + disk-full situation, you can free that space and then borg will be able to + run normally, so you can free more disk space by using prune/delete/compact. + After that, don't forget to reserve space again, in case you run into that + situation again at a later time. + + Examples:: + + # Create a new repository: + $ borg rcreate ... + # Reserve approx. 1GB of space for emergencies: + $ borg rspace --reserve 1G + + # Check amount of reserved space in the repository: + $ borg rspace + + # EMERGENCY! Free all reserved space to get things back to normal: + $ borg rspace --free + $ borg prune ... + $ borg delete ... + $ borg compact -v # only this actually frees space of deleted archives + $ borg rspace --reserve 1G # reserve space again for next time + + + Reserved space is always rounded up to use full reservation blocks of 64MiB. + """ + ) + subparser = subparsers.add_parser( + "rspace", + parents=[common_parser], + add_help=False, + description=self.do_rspace.__doc__, + epilog=rspace_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help="manage reserved space in a repository", + ) + subparser.set_defaults(func=self.do_rspace) + subparser.add_argument( + "--reserve", + metavar="SPACE", + dest="reserve_space", + default=0, + type=parse_file_size, + action=Highlander, + help="Amount of space to reserve (e.g. 100M, 1G). Default: 0.", + ) + subparser.add_argument( + "--free", + dest="free_space", + action="store_true", + help="Free all reserved space. Don't forget to reserve space later again.", + )