From d30d5f4aecaa1b492d7decf85258deb1eff57016 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 4 Aug 2024 15:57:37 +0200 Subject: [PATCH 01/79] Repository3 / RemoteRepository3: implement a borgstore based repository Simplify the repository a lot: No repository transactions, no log-like appending, no append-only, no segments, just using a key/value store for the individual chunks. No locking yet. Also: mypy: ignore missing import there are no library stubs for borgstore yet, so mypy errors without that option. pyproject.toml: install borgstore directly from github There is no pypi release yet. use pip install -e . rather than python setup.py develop The latter is deprecated and had issues installing the "borgstore from github" dependency. --- .github/workflows/ci.yml | 3 +- pyproject.toml | 2 + src/borg/archive.py | 10 +- src/borg/archiver/__init__.py | 25 +- src/borg/archiver/_common.py | 29 +- src/borg/archiver/config_cmd.py | 177 --- src/borg/archiver/debug_cmd.py | 39 +- src/borg/archiver/rcompress_cmd.py | 11 +- src/borg/archiver/serve_cmd.py | 2 +- src/borg/archiver/version_cmd.py | 4 +- src/borg/cache.py | 16 +- src/borg/crypto/keymanager.py | 4 +- src/borg/fuse.py | 2 +- src/borg/helpers/misc.py | 2 +- src/borg/helpers/parseformat.py | 4 +- src/borg/manifest.py | 4 +- src/borg/remote.py | 4 +- src/borg/remote3.py | 1269 +++++++++++++++++ src/borg/repository3.py | 314 ++++ src/borg/testsuite/archiver/__init__.py | 20 +- .../testsuite/archiver/bypass_lock_option.py | 130 -- src/borg/testsuite/archiver/check_cmd.py | 8 +- src/borg/testsuite/archiver/checks.py | 24 +- src/borg/testsuite/archiver/config_cmd.py | 64 - src/borg/testsuite/archiver/corruption.py | 18 - src/borg/testsuite/archiver/create_cmd.py | 4 +- src/borg/testsuite/archiver/delete_cmd.py | 6 +- src/borg/testsuite/archiver/key_cmds.py | 14 +- src/borg/testsuite/archiver/rcompress_cmd.py | 4 +- src/borg/testsuite/archiver/rcreate_cmd.py | 29 - src/borg/testsuite/archiver/rename_cmd.py | 4 +- src/borg/testsuite/archiver/return_codes.py | 2 +- src/borg/testsuite/archiver/rinfo_cmd.py | 18 - src/borg/testsuite/cache.py | 6 +- src/borg/testsuite/repoobj.py | 4 +- src/borg/testsuite/repository3.py | 290 ++++ tox.ini | 2 +- 37 files changed, 1967 insertions(+), 601 deletions(-) delete mode 100644 src/borg/archiver/config_cmd.py create mode 100644 src/borg/remote3.py create mode 100644 src/borg/repository3.py delete mode 100644 src/borg/testsuite/archiver/bypass_lock_option.py delete mode 100644 src/borg/testsuite/archiver/config_cmd.py create mode 100644 src/borg/testsuite/repository3.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f91c3ed3..68fb3a506 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,8 +104,7 @@ jobs: pip install -r requirements.d/development.txt - name: Install borgbackup run: | - # pip install -e . - python setup.py -v develop + pip install -e . - name: run tox env env: XDISTN: "4" diff --git a/pyproject.toml b/pyproject.toml index 3003d9bbb..40c374cba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,8 @@ dependencies = [ "platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'", # for macOS: breaking changes in 3.0.0, "platformdirs >=2.6.0, <5.0.0; sys_platform != 'darwin'", # for others: 2.6+ works consistently. "argon2-cffi", + "borgstore", + ] [project.optional-dependencies] diff --git a/src/borg/archive.py b/src/borg/archive.py index fe19b4b6b..d45f4426e 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -51,7 +51,7 @@ from .item import Item, ArchiveItem, ItemDiff from .platform import acl_get, acl_set, set_flags, get_flags, swidth, hostname from .remote import cache_if_remote -from .repository import Repository, LIST_SCAN_LIMIT +from .repository3 import Repository3, LIST_SCAN_LIMIT from .repoobj import RepoObj has_link = hasattr(os, "link") @@ -1046,7 +1046,7 @@ class ChunksIndexError(Error): def fetch_async_response(wait=True): try: return self.repository.async_response(wait=wait) - except Repository.ObjectNotFound: + except Repository3.ObjectNotFound: nonlocal error # object not in repo - strange, but we wanted to delete it anyway. if forced == 0: @@ -1093,7 +1093,7 @@ def chunk_decref(id, size, stats): error = True if progress: pi.finish() - except (msgpack.UnpackException, Repository.ObjectNotFound): + except (msgpack.UnpackException, Repository3.ObjectNotFound): # items metadata corrupted if forced == 0: raise @@ -1887,7 +1887,7 @@ def init_chunks(self): # Explicitly set the initial usable hash table capacity to avoid performance issues # due to hash table "resonance". # Since reconstruction of archive items can add some new chunks, add 10 % headroom. - self.chunks = ChunkIndex(usable=len(self.repository) * 1.1) + self.chunks = ChunkIndex() marker = None while True: result = self.repository.list(limit=LIST_SCAN_LIMIT, marker=marker) @@ -1939,7 +1939,7 @@ def verify_data(self): chunk_id = chunk_ids_revd.pop(-1) # better efficiency try: encrypted_data = next(chunk_data_iter) - except (Repository.ObjectNotFound, IntegrityErrorBase) as err: + except (Repository3.ObjectNotFound, IntegrityErrorBase) as err: self.error_found = True errors += 1 logger.error("chunk %s: %s", bin_to_hex(chunk_id), err) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index f0de10529..1e1e11eed 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -36,7 +36,7 @@ from ..helpers import ErrorIgnoringTextIOWrapper from ..helpers import msgpack from ..helpers import sig_int - from ..remote import RemoteRepository + from ..remote3 import RemoteRepository3 from ..selftest import selftest except BaseException: # an unhandled exception in the try-block would cause the borg cli command to exit with rc 1 due to python's @@ -68,7 +68,6 @@ def get_func(args): from .benchmark_cmd import BenchmarkMixIn from .check_cmd import CheckMixIn from .compact_cmd import CompactMixIn -from .config_cmd import ConfigMixIn from .create_cmd import CreateMixIn from .debug_cmd import DebugMixIn from .delete_cmd import DeleteMixIn @@ -98,7 +97,6 @@ class Archiver( BenchmarkMixIn, CheckMixIn, CompactMixIn, - ConfigMixIn, CreateMixIn, DebugMixIn, DeleteMixIn, @@ -336,7 +334,6 @@ def build_parser(self): self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser) self.build_parser_check(subparsers, common_parser, mid_common_parser) self.build_parser_compact(subparsers, common_parser, mid_common_parser) - self.build_parser_config(subparsers, common_parser, mid_common_parser) self.build_parser_create(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) @@ -412,22 +409,6 @@ def parse_args(self, args=None): elif not args.paths_from_stdin: # need at least 1 path but args.paths may also be populated from patterns parser.error("Need at least one PATH argument.") - if not getattr(args, "lock", True): # Option --bypass-lock sets args.lock = False - bypass_allowed = { - self.do_check, - self.do_config, - self.do_diff, - self.do_export_tar, - self.do_extract, - self.do_info, - self.do_rinfo, - self.do_list, - self.do_rlist, - self.do_mount, - self.do_umount, - } - if func not in bypass_allowed: - raise Error("Not allowed to bypass locking mechanism for chosen command") # we can only have a complete knowledge of placeholder replacements we should do **after** arg parsing, # e.g. due to options like --timestamp that override the current time. # thus we have to initialize replace_placeholders here and process all args that need placeholder replacement. @@ -581,7 +562,7 @@ def sig_trace_handler(sig_no, stack): # pragma: no cover def format_tb(exc): qualname = type(exc).__qualname__ - remote = isinstance(exc, RemoteRepository.RPCError) + remote = isinstance(exc, RemoteRepository3.RPCError) if remote: prefix = "Borg server: " trace_back = "\n".join(prefix + line for line in exc.exception_full.splitlines()) @@ -659,7 +640,7 @@ def main(): # pragma: no cover tb_log_level = logging.ERROR if e.traceback else logging.DEBUG tb = format_tb(e) exit_code = e.exit_code - except RemoteRepository.RPCError as e: + except RemoteRepository3.RPCError as e: important = e.traceback msg = e.exception_full if important else e.get_message() msgid = e.exception_class diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 4a49de9b8..f205cf8ed 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -14,7 +14,9 @@ from ..manifest import Manifest, AI_HUMAN_SORT_KEYS from ..patterns import PatternMatcher from ..remote import RemoteRepository +from ..remote3 import RemoteRepository3 from ..repository import Repository +from ..repository3 import Repository3 from ..repoobj import RepoObj, RepoObj1 from ..patterns import ( ArgparsePatternAction, @@ -29,9 +31,10 @@ logger = create_logger(__name__) -def get_repository(location, *, create, exclusive, lock_wait, lock, append_only, make_parent_dirs, storage_quota, args): +def get_repository(location, *, create, exclusive, lock_wait, lock, append_only, make_parent_dirs, storage_quota, args, v1_or_v2): if location.proto in ("ssh", "socket"): - repository = RemoteRepository( + RemoteRepoCls = RemoteRepository if v1_or_v2 else RemoteRepository3 + repository = RemoteRepoCls( location, create=create, exclusive=exclusive, @@ -43,7 +46,8 @@ def get_repository(location, *, create, exclusive, lock_wait, lock, append_only, ) else: - repository = Repository( + RepoCls = Repository if v1_or_v2 else Repository3 + repository = RepoCls( location.path, create=create, exclusive=exclusive, @@ -98,8 +102,7 @@ def with_repository( decorator_name="with_repository", ) - # To process the `--bypass-lock` option if specified, we need to - # modify `lock` inside `wrapper`. Therefore we cannot use the + # We may need to modify `lock` inside `wrapper`. Therefore we cannot use the # `nonlocal` statement to access `lock` as modifications would also # affect the scope outside of `wrapper`. Subsequent calls would # only see the overwritten value of `lock`, not the original one. @@ -129,13 +132,15 @@ def wrapper(self, args, **kwargs): make_parent_dirs=make_parent_dirs, storage_quota=storage_quota, args=args, + v1_or_v2=False, ) with repository: - if repository.version not in (2,): + if repository.version not in (3,): raise Error( - "This borg version only accepts version 2 repos for -r/--repo. " - "You can use 'borg transfer' to copy archives from old to new repos." + f"This borg version only accepts version 3 repos for -r/--repo, " + f"but not version {repository.version}. " + f"You can use 'borg transfer' to copy archives from old to new repos." ) if manifest or cache: manifest_ = Manifest.load(repository, compatibility) @@ -195,6 +200,7 @@ def wrapper(self, args, **kwargs): make_parent_dirs=False, storage_quota=None, args=args, + v1_or_v2=True ) with repository: @@ -504,13 +510,6 @@ def define_common_options(add_common_option): action=Highlander, help="wait at most SECONDS for acquiring a repository/cache lock (default: %(default)d).", ) - add_common_option( - "--bypass-lock", - dest="lock", - action="store_false", - default=argparse.SUPPRESS, # only create args attribute if option is specified - help="Bypass locking mechanism", - ) add_common_option("--show-version", dest="show_version", action="store_true", help="show/log the borg version") add_common_option("--show-rc", dest="show_rc", action="store_true", help="show/log the return code (rc)") add_common_option( diff --git a/src/borg/archiver/config_cmd.py b/src/borg/archiver/config_cmd.py deleted file mode 100644 index f92baf4f3..000000000 --- a/src/borg/archiver/config_cmd.py +++ /dev/null @@ -1,177 +0,0 @@ -import argparse -import configparser - -from ._common import with_repository -from ..cache import Cache, assert_secure -from ..constants import * # NOQA -from ..helpers import Error, CommandError -from ..helpers import parse_file_size, hex_to_bin -from ..manifest import Manifest - -from ..logger import create_logger - -logger = create_logger() - - -class ConfigMixIn: - @with_repository(exclusive=True, manifest=False) - def do_config(self, args, repository): - """get, set, and delete values in a repository or cache config file""" - - def repo_validate(section, name, value=None, check_value=True): - if section not in ["repository"]: - raise ValueError("Invalid section") - if name in ["segments_per_dir", "last_segment_checked"]: - if check_value: - try: - int(value) - except ValueError: - raise ValueError("Invalid value") from None - elif name in ["max_segment_size", "additional_free_space", "storage_quota"]: - if check_value: - try: - parse_file_size(value) - except ValueError: - raise ValueError("Invalid value") from None - if name == "storage_quota": - if parse_file_size(value) < parse_file_size("10M"): - raise ValueError("Invalid value: storage_quota < 10M") - elif name == "max_segment_size": - if parse_file_size(value) >= MAX_SEGMENT_SIZE_LIMIT: - raise ValueError("Invalid value: max_segment_size >= %d" % MAX_SEGMENT_SIZE_LIMIT) - elif name in ["append_only"]: - if check_value and value not in ["0", "1"]: - raise ValueError("Invalid value") - elif name in ["id"]: - if check_value: - hex_to_bin(value, length=32) - else: - raise ValueError("Invalid name") - - def cache_validate(section, name, value=None, check_value=True): - if section not in ["cache"]: - raise ValueError("Invalid section") - # currently, we do not support setting anything in the cache via borg config. - raise ValueError("Invalid name") - - def list_config(config): - default_values = { - "version": "1", - "segments_per_dir": str(DEFAULT_SEGMENTS_PER_DIR), - "max_segment_size": str(MAX_SEGMENT_SIZE_LIMIT), - "additional_free_space": "0", - "storage_quota": repository.storage_quota, - "append_only": repository.append_only, - } - print("[repository]") - for key in [ - "version", - "segments_per_dir", - "max_segment_size", - "storage_quota", - "additional_free_space", - "append_only", - "id", - ]: - value = config.get("repository", key, fallback=False) - if value is None: - value = default_values.get(key) - if value is None: - raise Error("The repository config is missing the %s key which has no default value" % key) - print(f"{key} = {value}") - for key in ["last_segment_checked"]: - value = config.get("repository", key, fallback=None) - if value is None: - continue - print(f"{key} = {value}") - - if not args.list: - if args.name is None: - raise CommandError("No config key name was provided.") - try: - section, name = args.name.split(".") - except ValueError: - section = args.cache and "cache" or "repository" - name = args.name - - if args.cache: - manifest = Manifest.load(repository, (Manifest.Operation.WRITE,)) - assert_secure(repository, manifest, self.lock_wait) - cache = Cache(repository, manifest, lock_wait=self.lock_wait) - - try: - if args.cache: - cache.cache_config.load() - config = cache.cache_config._config - save = cache.cache_config.save - validate = cache_validate - else: - config = repository.config - save = lambda: repository.save_config(repository.path, repository.config) # noqa - validate = repo_validate - - if args.delete: - validate(section, name, check_value=False) - config.remove_option(section, name) - if len(config.options(section)) == 0: - config.remove_section(section) - save() - elif args.list: - list_config(config) - elif args.value: - validate(section, name, args.value) - if section not in config.sections(): - config.add_section(section) - config.set(section, name, args.value) - save() - else: - try: - print(config.get(section, name)) - except (configparser.NoOptionError, configparser.NoSectionError) as e: - raise Error(e) - finally: - if args.cache: - cache.close() - - def build_parser_config(self, subparsers, common_parser, mid_common_parser): - from ._common import process_epilog - - config_epilog = process_epilog( - """ - This command gets and sets options in a local repository or cache config file. - For security reasons, this command only works on local repositories. - - To delete a config value entirely, use ``--delete``. To list the values - of the configuration file or the default values, use ``--list``. To get an existing - key, pass only the key name. To set a key, pass both the key name and - the new value. Keys can be specified in the format "section.name" or - simply "name"; the section will default to "repository" and "cache" for - the repo and cache configs, respectively. - - - By default, borg config manipulates the repository config file. Using ``--cache`` - edits the repository cache's config file instead. - """ - ) - subparser = subparsers.add_parser( - "config", - parents=[common_parser], - add_help=False, - description=self.do_config.__doc__, - epilog=config_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help="get and set configuration values", - ) - subparser.set_defaults(func=self.do_config) - subparser.add_argument( - "-c", "--cache", dest="cache", action="store_true", help="get and set values from the repo cache" - ) - - group = subparser.add_mutually_exclusive_group() - group.add_argument( - "-d", "--delete", dest="delete", action="store_true", help="delete the key from the config file" - ) - group.add_argument("-l", "--list", dest="list", action="store_true", help="list the configuration of the repo") - - subparser.add_argument("name", metavar="NAME", nargs="?", help="name of config key") - subparser.add_argument("value", metavar="VALUE", nargs="?", help="new value for key") diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index fe9df81f4..89f121521 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -15,7 +15,8 @@ from ..helpers import CommandError, RTError from ..manifest import Manifest from ..platform import get_process_id -from ..repository import Repository, LIST_SCAN_LIMIT, TAG_PUT, TAG_DELETE, TAG_COMMIT +from ..repository import Repository, TAG_PUT, TAG_DELETE, TAG_COMMIT +from ..repository3 import Repository3, LIST_SCAN_LIMIT from ..repoobj import RepoObj from ._common import with_repository, Highlander @@ -330,7 +331,7 @@ def do_debug_delete_obj(self, args, repository): repository.delete(id) modified = True print("object %s deleted." % hex_id) - except Repository.ObjectNotFound: + except Repository3.ObjectNotFound: print("object %s not found." % hex_id) if modified: repository.commit(compact=False) @@ -351,23 +352,6 @@ def do_debug_refcount_obj(self, args, repository, manifest, cache): except KeyError: print("object %s not found [info from chunks cache]." % hex_id) - @with_repository(manifest=False, exclusive=True) - def do_debug_dump_hints(self, args, repository): - """dump repository hints""" - if not repository._active_txn: - repository.prepare_txn(repository.get_transaction_id()) - try: - hints = dict( - segments=repository.segments, - compact=repository.compact, - storage_quota_use=repository.storage_quota_use, - shadow_index={bin_to_hex(k): v for k, v in repository.shadow_index.items()}, - ) - with dash_open(args.path, "w") as fd: - json.dump(hints, fd, indent=4) - finally: - repository.rollback() - def do_debug_convert_profile(self, args): """convert Borg profile to Python profile""" import marshal @@ -689,23 +673,6 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): subparser.set_defaults(func=self.do_debug_refcount_obj) subparser.add_argument("ids", metavar="IDs", nargs="+", type=str, help="hex object ID(s) to show refcounts for") - debug_dump_hints_epilog = process_epilog( - """ - This command dumps the repository hints data. - """ - ) - subparser = debug_parsers.add_parser( - "dump-hints", - parents=[common_parser], - add_help=False, - description=self.do_debug_dump_hints.__doc__, - epilog=debug_dump_hints_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help="dump repo hints (debug)", - ) - subparser.set_defaults(func=self.do_debug_dump_hints) - subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into") - debug_convert_profile_epilog = process_epilog( """ Convert a Borg profile to a Python cProfile compatible profile. diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index 30706dcd6..bc096b3c3 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -24,14 +24,7 @@ def find_chunks(repository, repo_objs, stats, ctype, clevel, olevel): compr_keys = stats["compr_keys"] = set() compr_wanted = ctype, clevel, olevel state = None - chunks_count = len(repository) - chunks_limit = min(1000, max(100, chunks_count // 1000)) - pi = ProgressIndicatorPercent( - total=chunks_count, - msg="Searching for recompression candidates %3.1f%%", - step=0.1, - msgid="rcompress.find_chunks", - ) + chunks_limit = 1000 while True: chunk_ids, state = repository.scan(limit=chunks_limit, state=state) if not chunk_ids: @@ -44,8 +37,6 @@ def find_chunks(repository, repo_objs, stats, ctype, clevel, olevel): compr_keys.add(compr_found) stats[compr_found] += 1 stats["checked_count"] += 1 - pi.show(increase=1) - pi.finish() return recompress_ids diff --git a/src/borg/archiver/serve_cmd.py b/src/borg/archiver/serve_cmd.py index 8cc613c58..3c5165831 100644 --- a/src/borg/archiver/serve_cmd.py +++ b/src/borg/archiver/serve_cmd.py @@ -3,7 +3,7 @@ from ._common import Highlander from ..constants import * # NOQA from ..helpers import parse_storage_quota -from ..remote import RepositoryServer +from ..remote3 import RepositoryServer from ..logger import create_logger diff --git a/src/borg/archiver/version_cmd.py b/src/borg/archiver/version_cmd.py index 75593cbfb..03981b4d3 100644 --- a/src/borg/archiver/version_cmd.py +++ b/src/borg/archiver/version_cmd.py @@ -2,7 +2,7 @@ from .. import __version__ from ..constants import * # NOQA -from ..remote import RemoteRepository +from ..remote3 import RemoteRepository3 from ..logger import create_logger @@ -16,7 +16,7 @@ def do_version(self, args): client_version = parse_version(__version__) if args.location.proto in ("ssh", "socket"): - with RemoteRepository(args.location, lock=False, args=args) as repository: + with RemoteRepository3(args.location, lock=False, args=args) as repository: server_version = repository.server_version else: server_version = client_version diff --git a/src/borg/cache.py b/src/borg/cache.py index 88fe32902..ee88793f3 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -32,7 +32,7 @@ from .manifest import Manifest from .platform import SaveFile from .remote import cache_if_remote -from .repository import LIST_SCAN_LIMIT +from .repository3 import LIST_SCAN_LIMIT # note: cmtime might be either a ctime or a mtime timestamp, chunks is a list of ChunkListEntry FileCacheEntry = namedtuple("FileCacheEntry", "age inode size cmtime chunks") @@ -718,35 +718,27 @@ def add_chunk( return ChunkListEntry(id, size) def _load_chunks_from_repo(self): - # Explicitly set the initial usable hash table capacity to avoid performance issues - # due to hash table "resonance". - # Since we're creating an archive, add 10 % from the start. - num_chunks = len(self.repository) - chunks = ChunkIndex(usable=num_chunks * 1.1) - pi = ProgressIndicatorPercent( - total=num_chunks, msg="Downloading chunk list... %3.0f%%", msgid="cache.download_chunks" - ) + chunks = ChunkIndex() t0 = perf_counter() num_requests = 0 + num_chunks = 0 marker = None while True: result = self.repository.list(limit=LIST_SCAN_LIMIT, marker=marker) num_requests += 1 if not result: break - pi.show(increase=len(result)) marker = result[-1] # All chunks from the repository have a refcount of MAX_VALUE, which is sticky, # therefore we can't/won't delete them. Chunks we added ourselves in this transaction # (e.g. checkpoint archives) are tracked correctly. init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) for id_ in result: + num_chunks += 1 chunks[id_] = init_entry - assert len(chunks) == num_chunks # LocalCache does not contain the manifest, either. del chunks[self.manifest.MANIFEST_ID] duration = perf_counter() - t0 or 0.01 - pi.finish() logger.debug( "Cache: downloaded %d chunk IDs in %.2f s (%d requests), ~%s/s", num_chunks, diff --git a/src/borg/crypto/keymanager.py b/src/borg/crypto/keymanager.py index d8d25893d..c2105ec5b 100644 --- a/src/borg/crypto/keymanager.py +++ b/src/borg/crypto/keymanager.py @@ -5,7 +5,7 @@ from ..helpers import Error, yes, bin_to_hex, hex_to_bin, dash_open from ..manifest import Manifest, NoManifestError -from ..repository import Repository +from ..repository3 import Repository3 from ..repoobj import RepoObj @@ -50,7 +50,7 @@ def __init__(self, repository): try: manifest_chunk = self.repository.get(Manifest.MANIFEST_ID) - except Repository.ObjectNotFound: + except Repository3.ObjectNotFound: raise NoManifestError manifest_data = RepoObj.extract_crypted_data(manifest_chunk) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 92f145874..dd77f4016 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -46,7 +46,7 @@ def async_wrapper(fn): from .item import Item from .platform import uid2user, gid2group from .platformflags import is_darwin -from .remote import RemoteRepository +from .remote import RemoteRepository # TODO 3 def fuse_main(): diff --git a/src/borg/helpers/misc.py b/src/borg/helpers/misc.py index 1f687a0bb..6028b93a3 100644 --- a/src/borg/helpers/misc.py +++ b/src/borg/helpers/misc.py @@ -2,7 +2,7 @@ import io import os import os.path -import platform +import platform # python stdlib import - if this fails, check that cwd != src/borg/ import sys from collections import deque from itertools import islice diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index c69889b18..f1d95cad0 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1182,11 +1182,13 @@ def ellipsis_truncate(msg, space): class BorgJsonEncoder(json.JSONEncoder): def default(self, o): from ..repository import Repository + from ..repository3 import Repository3 from ..remote import RemoteRepository + from ..remote3 import RemoteRepository3 from ..archive import Archive from ..cache import LocalCache, AdHocCache, AdHocWithFilesCache - if isinstance(o, Repository) or isinstance(o, RemoteRepository): + if isinstance(o, (Repository, Repository3)) or isinstance(o, (RemoteRepository, RemoteRepository3)): return {"id": bin_to_hex(o.id), "location": o._location.canonical_path()} if isinstance(o, Archive): return o.info() diff --git a/src/borg/manifest.py b/src/borg/manifest.py index 9b23cb63c..cdc1b99fb 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -246,11 +246,11 @@ def last_timestamp(self): def load(cls, repository, operations, key=None, *, ro_cls=RepoObj): from .item import ManifestItem from .crypto.key import key_factory - from .repository import Repository + from .repository3 import Repository3 try: cdata = repository.get(cls.MANIFEST_ID) - except Repository.ObjectNotFound: + except Repository3.ObjectNotFound: raise NoManifestError if not key: key = key_factory(repository, cdata, ro_cls=ro_cls) diff --git a/src/borg/remote.py b/src/borg/remote.py index 924b36ad7..e035224d7 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -640,6 +640,7 @@ def __init__( exclusive=exclusive, append_only=append_only, make_parent_dirs=make_parent_dirs, + v1_or_v2=True, # make remote use Repository, not Repository3 ) info = self.info() self.version = info["version"] @@ -939,9 +940,10 @@ def handle_error(unpacked): since=parse_version("1.0.0"), append_only={"since": parse_version("1.0.7"), "previously": False}, make_parent_dirs={"since": parse_version("1.1.9"), "previously": False}, + v1_or_v2={"since": parse_version("2.0.0b8"), "previously": True}, # TODO fix version ) def open( - self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False, make_parent_dirs=False + self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False, make_parent_dirs=False, v1_or_v2=False ): """actual remoting is done via self.call in the @api decorator""" diff --git a/src/borg/remote3.py b/src/borg/remote3.py new file mode 100644 index 000000000..85687035d --- /dev/null +++ b/src/borg/remote3.py @@ -0,0 +1,1269 @@ +import atexit +import errno +import functools +import inspect +import logging +import os +import queue +import select +import shlex +import shutil +import socket +import struct +import sys +import tempfile +import textwrap +import time +import traceback +from subprocess import Popen, PIPE + +import borg.logger +from . import __version__ +from .compress import Compressor +from .constants import * # NOQA +from .helpers import Error, ErrorWithTraceback, IntegrityError +from .helpers import bin_to_hex +from .helpers import get_limited_unpacker +from .helpers import replace_placeholders +from .helpers import sysinfo +from .helpers import format_file_size +from .helpers import safe_unlink +from .helpers import prepare_subprocess_env, ignore_sigint +from .helpers import get_socket_filename +from .locking import LockTimeout, NotLocked, NotMyLock, LockFailed +from .logger import create_logger, borg_serve_log_queue +from .helpers import msgpack +from .repository import Repository +from .repository3 import Repository3 +from .version import parse_version, format_version +from .checksums import xxh64 +from .helpers.datastruct import EfficientCollectionQueue + +logger = create_logger(__name__) + +BORG_VERSION = parse_version(__version__) +MSGID, MSG, ARGS, RESULT, LOG = "i", "m", "a", "r", "l" + +MAX_INFLIGHT = 100 + +RATELIMIT_PERIOD = 0.1 + + +def os_write(fd, data): + """os.write wrapper so we do not lose data for partial writes.""" + # TODO: this issue is fixed in cygwin since at least 2.8.0, remove this + # wrapper / workaround when this version is considered ancient. + # This is happening frequently on cygwin due to its small pipe buffer size of only 64kiB + # and also due to its different blocking pipe behaviour compared to Linux/*BSD. + # Neither Linux nor *BSD ever do partial writes on blocking pipes, unless interrupted by a + # signal, in which case serve() would terminate. + amount = remaining = len(data) + while remaining: + count = os.write(fd, data) + remaining -= count + if not remaining: + break + data = data[count:] + time.sleep(count * 1e-09) + return amount + + +class ConnectionClosed(Error): + """Connection closed by remote host""" + + exit_mcode = 80 + + +class ConnectionClosedWithHint(ConnectionClosed): + """Connection closed by remote host. {}""" + + exit_mcode = 81 + + +class PathNotAllowed(Error): + """Repository path not allowed: {}""" + + exit_mcode = 83 + + +class InvalidRPCMethod(Error): + """RPC method {} is not valid""" + + exit_mcode = 82 + + +class UnexpectedRPCDataFormatFromClient(Error): + """Borg {}: Got unexpected RPC data format from client.""" + + exit_mcode = 85 + + +class UnexpectedRPCDataFormatFromServer(Error): + """Got unexpected RPC data format from server:\n{}""" + + exit_mcode = 86 + + def __init__(self, data): + try: + data = data.decode()[:128] + except UnicodeDecodeError: + data = data[:128] + data = ["%02X" % byte for byte in data] + data = textwrap.fill(" ".join(data), 16 * 3) + super().__init__(data) + + +class ConnectionBrokenWithHint(Error): + """Connection to remote host is broken. {}""" + + exit_mcode = 87 + + +# Protocol compatibility: +# In general the server is responsible for rejecting too old clients and the client it responsible for rejecting +# too old servers. This ensures that the knowledge what is compatible is always held by the newer component. +# +# For the client the return of the negotiate method is a dict which includes the server version. +# +# All method calls on the remote repository object must be allowlisted in RepositoryServer.rpc_methods and have api +# stubs in RemoteRepository*. The @api decorator on these stubs is used to set server version requirements. +# +# Method parameters are identified only by name and never by position. Unknown parameters are ignored by the server. +# If a new parameter is important and may not be ignored, on the client a parameter specific version requirement needs +# to be added. +# When parameters are removed, they need to be preserved as defaulted parameters on the client stubs so that older +# servers still get compatible input. + + +class RepositoryServer: # pragma: no cover + _rpc_methods = ( + "__len__", + "check", + "commit", + "delete", + "destroy", + "flags", + "flags_many", + "get", + "list", + "scan", + "negotiate", + "open", + "close", + "info", + "put", + "rollback", + "save_key", + "load_key", + "break_lock", + "inject_exception", + ) + + _rpc_methods3 = ( + "__len__", + "check", + "commit", + "delete", + "destroy", + "get", + "list", + "scan", + "negotiate", + "open", + "close", + "info", + "put", + "save_key", + "load_key", + "break_lock", + "inject_exception", + ) + + def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota, use_socket): + self.repository = None + self.RepoCls = None + self.rpc_methods = ("open", "close", "negotiate") + self.restrict_to_paths = restrict_to_paths + self.restrict_to_repositories = restrict_to_repositories + # This flag is parsed from the serve command line via Archiver.do_serve, + # i.e. it reflects local system policy and generally ranks higher than + # whatever the client wants, except when initializing a new repository + # (see RepositoryServer.open below). + self.append_only = append_only + self.storage_quota = storage_quota + self.client_version = None # we update this after client sends version information + if use_socket is False: + self.socket_path = None + elif use_socket is True: # --socket + self.socket_path = get_socket_filename() + else: # --socket=/some/path + self.socket_path = use_socket + + def filter_args(self, f, kwargs): + """Remove unknown named parameters from call, because client did (implicitly) say it's ok.""" + known = set(inspect.signature(f).parameters) + return {name: kwargs[name] for name in kwargs if name in known} + + def send_queued_log(self): + while True: + try: + # lr_dict contents see BorgQueueHandler + lr_dict = borg_serve_log_queue.get_nowait() + except queue.Empty: + break + else: + msg = msgpack.packb({LOG: lr_dict}) + os_write(self.stdout_fd, msg) + + def serve(self): + def inner_serve(): + os.set_blocking(self.stdin_fd, False) + assert not os.get_blocking(self.stdin_fd) + os.set_blocking(self.stdout_fd, True) + assert os.get_blocking(self.stdout_fd) + + unpacker = get_limited_unpacker("server") + shutdown_serve = False + while True: + # before processing any new RPCs, send out all pending log output + self.send_queued_log() + + if shutdown_serve: + # shutdown wanted! get out of here after sending all log output. + assert self.repository is None + return + + # process new RPCs + r, w, es = select.select([self.stdin_fd], [], [], 10) + if r: + data = os.read(self.stdin_fd, BUFSIZE) + if not data: + shutdown_serve = True + continue + unpacker.feed(data) + for unpacked in unpacker: + if isinstance(unpacked, dict): + msgid = unpacked[MSGID] + method = unpacked[MSG] + args = unpacked[ARGS] + else: + if self.repository is not None: + self.repository.close() + raise UnexpectedRPCDataFormatFromClient(__version__) + try: + # logger.debug(f"{type(self)} method: {type(self.repository)}.{method}") + if method not in self.rpc_methods: + raise InvalidRPCMethod(method) + try: + f = getattr(self, method) + except AttributeError: + f = getattr(self.repository, method) + args = self.filter_args(f, args) + res = f(**args) + except BaseException as e: + # logger.exception(e) + ex_short = traceback.format_exception_only(e.__class__, e) + ex_full = traceback.format_exception(*sys.exc_info()) + ex_trace = True + if isinstance(e, Error): + ex_short = [e.get_message()] + ex_trace = e.traceback + if isinstance(e, (self.RepoCls.DoesNotExist, self.RepoCls.AlreadyExists, PathNotAllowed)): + # These exceptions are reconstructed on the client end in RemoteRepository*.call_many(), + # and will be handled just like locally raised exceptions. Suppress the remote traceback + # for these, except ErrorWithTraceback, which should always display a traceback. + pass + else: + logging.debug("\n".join(ex_full)) + + sys_info = sysinfo() + try: + msg = msgpack.packb( + { + MSGID: msgid, + "exception_class": e.__class__.__name__, + "exception_args": e.args, + "exception_full": ex_full, + "exception_short": ex_short, + "exception_trace": ex_trace, + "sysinfo": sys_info, + } + ) + except TypeError: + msg = msgpack.packb( + { + MSGID: msgid, + "exception_class": e.__class__.__name__, + "exception_args": [ + x if isinstance(x, (str, bytes, int)) else None for x in e.args + ], + "exception_full": ex_full, + "exception_short": ex_short, + "exception_trace": ex_trace, + "sysinfo": sys_info, + } + ) + os_write(self.stdout_fd, msg) + else: + os_write(self.stdout_fd, msgpack.packb({MSGID: msgid, RESULT: res})) + if es: + shutdown_serve = True + continue + + if self.socket_path: # server for socket:// connections + try: + # remove any left-over socket file + os.unlink(self.socket_path) + except OSError: + if os.path.exists(self.socket_path): + raise + sock_dir = os.path.dirname(self.socket_path) + os.makedirs(sock_dir, exist_ok=True) + if self.socket_path.endswith(".sock"): + pid_file = self.socket_path.replace(".sock", ".pid") + else: + pid_file = self.socket_path + ".pid" + pid = os.getpid() + with open(pid_file, "w") as f: + f.write(str(pid)) + atexit.register(functools.partial(os.remove, pid_file)) + atexit.register(functools.partial(os.remove, self.socket_path)) + sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM) + sock.bind(self.socket_path) # this creates the socket file in the fs + sock.listen(0) # no backlog + os.chmod(self.socket_path, mode=0o0770) # group members may use the socket, too. + print(f"borg serve: PID {pid}, listening on socket {self.socket_path} ...", file=sys.stderr) + + while True: + connection, client_address = sock.accept() + print(f"Accepted a connection on socket {self.socket_path} ...", file=sys.stderr) + self.stdin_fd = connection.makefile("rb").fileno() + self.stdout_fd = connection.makefile("wb").fileno() + inner_serve() + print(f"Finished with connection on socket {self.socket_path} .", file=sys.stderr) + else: # server for one ssh:// connection + self.stdin_fd = sys.stdin.fileno() + self.stdout_fd = sys.stdout.fileno() + inner_serve() + + def negotiate(self, client_data): + if isinstance(client_data, dict): + self.client_version = client_data["client_version"] + else: + self.client_version = BORG_VERSION # seems to be newer than current version (no known old format) + + # not a known old format, send newest negotiate this version knows + return {"server_version": BORG_VERSION} + + def _resolve_path(self, path): + if isinstance(path, bytes): + path = os.fsdecode(path) + if path.startswith("/~/"): # /~/x = path x relative to own home dir + home_dir = os.environ.get("HOME") or os.path.expanduser("~%s" % os.environ.get("USER", "")) + path = os.path.join(home_dir, path[3:]) + elif path.startswith("/./"): # /./x = path x relative to cwd + path = path[3:] + return os.path.realpath(path) + + def open( + self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False, make_parent_dirs=False, v1_or_v2=False + ): + self.RepoCls = Repository if v1_or_v2 else Repository3 + self.rpc_methods = self._rpc_methods if v1_or_v2 else self._rpc_methods3 + logging.debug("Resolving repository path %r", path) + path = self._resolve_path(path) + logging.debug("Resolved repository path to %r", path) + path_with_sep = os.path.join(path, "") # make sure there is a trailing slash (os.sep) + if self.restrict_to_paths: + # if --restrict-to-path P is given, we make sure that we only operate in/below path P. + # for the prefix check, it is important that the compared paths both have trailing slashes, + # so that a path /foobar will NOT be accepted with --restrict-to-path /foo option. + for restrict_to_path in self.restrict_to_paths: + restrict_to_path_with_sep = os.path.join(os.path.realpath(restrict_to_path), "") # trailing slash + if path_with_sep.startswith(restrict_to_path_with_sep): + break + else: + raise PathNotAllowed(path) + if self.restrict_to_repositories: + for restrict_to_repository in self.restrict_to_repositories: + restrict_to_repository_with_sep = os.path.join(os.path.realpath(restrict_to_repository), "") + if restrict_to_repository_with_sep == path_with_sep: + break + else: + raise PathNotAllowed(path) + # "borg init" on "borg serve --append-only" (=self.append_only) does not create an append only repo, + # while "borg init --append-only" (=append_only) does, regardless of the --append-only (self.append_only) + # flag for serve. + append_only = (not create and self.append_only) or append_only + self.repository = self.RepoCls( + path, + create, + lock_wait=lock_wait, + lock=lock, + append_only=append_only, + storage_quota=self.storage_quota, + exclusive=exclusive, + make_parent_dirs=make_parent_dirs, + send_log_cb=self.send_queued_log, + ) + self.repository.__enter__() # clean exit handled by serve() method + return self.repository.id + + def close(self): + if self.repository is not None: + self.repository.__exit__(None, None, None) + self.repository = None + borg.logger.flush_logging() + self.send_queued_log() + + def inject_exception(self, kind): + s1 = "test string" + s2 = "test string2" + if kind == "DoesNotExist": + raise self.RepoCls.DoesNotExist(s1) + elif kind == "AlreadyExists": + raise self.RepoCls.AlreadyExists(s1) + elif kind == "CheckNeeded": + raise self.RepoCls.CheckNeeded(s1) + elif kind == "IntegrityError": + raise IntegrityError(s1) + elif kind == "PathNotAllowed": + raise PathNotAllowed("foo") + elif kind == "ObjectNotFound": + raise self.RepoCls.ObjectNotFound(s1, s2) + elif kind == "InvalidRPCMethod": + raise InvalidRPCMethod(s1) + elif kind == "divide": + 0 // 0 + + +class SleepingBandwidthLimiter: + def __init__(self, limit): + if limit: + self.ratelimit = int(limit * RATELIMIT_PERIOD) + self.ratelimit_last = time.monotonic() + self.ratelimit_quota = self.ratelimit + else: + self.ratelimit = None + + def write(self, fd, to_send): + if self.ratelimit: + now = time.monotonic() + if self.ratelimit_last + RATELIMIT_PERIOD <= now: + self.ratelimit_quota += self.ratelimit + if self.ratelimit_quota > 2 * self.ratelimit: + self.ratelimit_quota = 2 * self.ratelimit + self.ratelimit_last = now + if self.ratelimit_quota == 0: + tosleep = self.ratelimit_last + RATELIMIT_PERIOD - now + time.sleep(tosleep) + self.ratelimit_quota += self.ratelimit + self.ratelimit_last = time.monotonic() + if len(to_send) > self.ratelimit_quota: + to_send = to_send[: self.ratelimit_quota] + try: + written = os.write(fd, to_send) + except BrokenPipeError: + raise ConnectionBrokenWithHint("Broken Pipe") from None + if self.ratelimit: + self.ratelimit_quota -= written + return written + + +def api(*, since, **kwargs_decorator): + """Check version requirements and use self.call to do the remote method call. + + specifies the version in which borg introduced this method. + Calling this method when connected to an older version will fail without transmitting anything to the server. + + Further kwargs can be used to encode version specific restrictions: + + is the value resulting in the behaviour before introducing the new parameter. + If a previous hardcoded behaviour is parameterized in a version, this allows calls that use the previously + hardcoded behaviour to pass through and generates an error if another behaviour is requested by the client. + E.g. when 'append_only' was introduced in 1.0.7 the previous behaviour was what now is append_only=False. + Thus @api(..., append_only={'since': parse_version('1.0.7'), 'previously': False}) allows calls + with append_only=False for all version but rejects calls using append_only=True on versions older than 1.0.7. + + is a flag to set the behaviour if an old version is called the new way. + If set to True, the method is called without the (not yet supported) parameter (this should be done if that is the + more desirable behaviour). If False, an exception is generated. + E.g. before 'threshold' was introduced in 1.2.0a8, a hardcoded threshold of 0.1 was used in commit(). + """ + + def decorator(f): + @functools.wraps(f) + def do_rpc(self, *args, **kwargs): + sig = inspect.signature(f) + bound_args = sig.bind(self, *args, **kwargs) + named = {} # Arguments for the remote process + extra = {} # Arguments for the local process + for name, param in sig.parameters.items(): + if name == "self": + continue + if name in bound_args.arguments: + if name == "wait": + extra[name] = bound_args.arguments[name] + else: + named[name] = bound_args.arguments[name] + else: + if param.default is not param.empty: + named[name] = param.default + + if self.server_version < since: + raise self.RPCServerOutdated(f.__name__, format_version(since)) + + for name, restriction in kwargs_decorator.items(): + if restriction["since"] <= self.server_version: + continue + if "previously" in restriction and named[name] == restriction["previously"]: + continue + if restriction.get("dontcare", False): + continue + + raise self.RPCServerOutdated( + f"{f.__name__} {name}={named[name]!s}", format_version(restriction["since"]) + ) + + return self.call(f.__name__, named, **extra) + + return do_rpc + + return decorator + + +class RemoteRepository3: + extra_test_args = [] # type: ignore + + class RPCError(Exception): + def __init__(self, unpacked): + # unpacked has keys: 'exception_args', 'exception_full', 'exception_short', 'sysinfo' + self.unpacked = unpacked + + def get_message(self): + return "\n".join(self.unpacked["exception_short"]) + + @property + def traceback(self): + return self.unpacked.get("exception_trace", True) + + @property + def exception_class(self): + return self.unpacked["exception_class"] + + @property + def exception_full(self): + return "\n".join(self.unpacked["exception_full"]) + + @property + def sysinfo(self): + return self.unpacked["sysinfo"] + + class RPCServerOutdated(Error): + """Borg server is too old for {}. Required version {}""" + + exit_mcode = 84 + + @property + def method(self): + return self.args[0] + + @property + def required_version(self): + return self.args[1] + + def __init__( + self, + location, + create=False, + exclusive=False, + lock_wait=None, + lock=True, + append_only=False, + make_parent_dirs=False, + args=None, + ): + self.location = self._location = location + self.preload_ids = [] + self.msgid = 0 + self.rx_bytes = 0 + self.tx_bytes = 0 + self.to_send = EfficientCollectionQueue(1024 * 1024, bytes) + self.stdin_fd = self.stdout_fd = self.stderr_fd = None + self.stderr_received = b"" # incomplete stderr line bytes received (no \n yet) + self.chunkid_to_msgids = {} + self.ignore_responses = set() + self.responses = {} + self.async_responses = {} + self.shutdown_time = None + self.ratelimit = SleepingBandwidthLimiter(args.upload_ratelimit * 1024 if args and args.upload_ratelimit else 0) + self.upload_buffer_size_limit = args.upload_buffer * 1024 * 1024 if args and args.upload_buffer else 0 + self.unpacker = get_limited_unpacker("client") + self.server_version = None # we update this after server sends its version + self.p = self.sock = None + self._args = args + if self.location.proto == "ssh": + testing = location.host == "__testsuite__" + # when testing, we invoke and talk to a borg process directly (no ssh). + # when not testing, we invoke the system-installed ssh binary to talk to a remote borg. + env = prepare_subprocess_env(system=not testing) + borg_cmd = self.borg_cmd(args, testing) + if not testing: + borg_cmd = self.ssh_cmd(location) + borg_cmd + logger.debug("SSH command line: %s", borg_cmd) + # we do not want the ssh getting killed by Ctrl-C/SIGINT because it is needed for clean shutdown of borg. + # borg's SIGINT handler tries to write a checkpoint and requires the remote repo connection. + self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, preexec_fn=ignore_sigint) + self.stdin_fd = self.p.stdin.fileno() + self.stdout_fd = self.p.stdout.fileno() + self.stderr_fd = self.p.stderr.fileno() + self.r_fds = [self.stdout_fd, self.stderr_fd] + self.x_fds = [self.stdin_fd, self.stdout_fd, self.stderr_fd] + elif self.location.proto == "socket": + if args.use_socket is False or args.use_socket is True: # nothing or --socket + socket_path = get_socket_filename() + else: # --socket=/some/path + socket_path = args.use_socket + self.sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM) + try: + self.sock.connect(socket_path) # note: socket_path length is rather limited. + except FileNotFoundError: + self.sock = None + raise Error(f"The socket file {socket_path} does not exist.") + except ConnectionRefusedError: + self.sock = None + raise Error(f"There is no borg serve running for the socket file {socket_path}.") + self.stdin_fd = self.sock.makefile("wb").fileno() + self.stdout_fd = self.sock.makefile("rb").fileno() + self.stderr_fd = None + self.r_fds = [self.stdout_fd] + self.x_fds = [self.stdin_fd, self.stdout_fd] + else: + raise Error(f"Unsupported protocol {location.proto}") + + os.set_blocking(self.stdin_fd, False) + assert not os.get_blocking(self.stdin_fd) + os.set_blocking(self.stdout_fd, False) + assert not os.get_blocking(self.stdout_fd) + if self.stderr_fd is not None: + os.set_blocking(self.stderr_fd, False) + assert not os.get_blocking(self.stderr_fd) + + try: + try: + version = self.call("negotiate", {"client_data": {"client_version": BORG_VERSION}}) + except ConnectionClosed: + raise ConnectionClosedWithHint("Is borg working on the server?") from None + if isinstance(version, dict): + self.server_version = version["server_version"] + else: + raise Exception("Server insisted on using unsupported protocol version %s" % version) + + self.id = self.open( + path=self.location.path, + create=create, + lock_wait=lock_wait, + lock=lock, + exclusive=exclusive, + append_only=append_only, + make_parent_dirs=make_parent_dirs, + ) + info = self.info() + self.version = info["version"] + self.append_only = info["append_only"] + + except Exception: + self.close() + raise + + def __del__(self): + if len(self.responses): + logging.debug("still %d cached responses left in RemoteRepository3" % (len(self.responses),)) + if self.p or self.sock: + self.close() + assert False, "cleanup happened in Repository3.__del__" + + def __repr__(self): + return f"<{self.__class__.__name__} {self.location.canonical_path()}>" + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + try: + if exc_type is not None: + self.shutdown_time = time.monotonic() + 30 + finally: + # in any case, we want to close the repo cleanly. + logger.debug( + "RemoteRepository3: %s bytes sent, %s bytes received, %d messages sent", + format_file_size(self.tx_bytes), + format_file_size(self.rx_bytes), + self.msgid, + ) + self.close() + + @property + def id_str(self): + return bin_to_hex(self.id) + + def borg_cmd(self, args, testing): + """return a borg serve command line""" + # give some args/options to 'borg serve' process as they were given to us + opts = [] + if args is not None: + root_logger = logging.getLogger() + if root_logger.isEnabledFor(logging.DEBUG): + opts.append("--debug") + elif root_logger.isEnabledFor(logging.INFO): + opts.append("--info") + elif root_logger.isEnabledFor(logging.WARNING): + pass # warning is default + elif root_logger.isEnabledFor(logging.ERROR): + opts.append("--error") + elif root_logger.isEnabledFor(logging.CRITICAL): + opts.append("--critical") + else: + raise ValueError("log level missing, fix this code") + + # Tell the remote server about debug topics it may need to consider. + # Note that debug topics are usable for "spew" or "trace" logs which would + # be too plentiful to transfer for normal use, so the server doesn't send + # them unless explicitly enabled. + # + # Needless to say, if you do --debug-topic=repository.compaction, for example, + # with a 1.0.x server it won't work, because the server does not recognize the + # option. + # + # This is not considered a problem, since this is a debugging feature that + # should not be used for regular use. + for topic in args.debug_topics: + if "." not in topic: + topic = "borg.debug." + topic + if "repository" in topic: + opts.append("--debug-topic=%s" % topic) + + if "storage_quota" in args and args.storage_quota: + opts.append("--storage-quota=%s" % args.storage_quota) + env_vars = [] + if testing: + return env_vars + [sys.executable, "-m", "borg", "serve"] + opts + self.extra_test_args + else: # pragma: no cover + remote_path = args.remote_path or os.environ.get("BORG_REMOTE_PATH", "borg") + remote_path = replace_placeholders(remote_path) + return env_vars + [remote_path, "serve"] + opts + + def ssh_cmd(self, location): + """return a ssh command line that can be prefixed to a borg command line""" + rsh = self._args.rsh or os.environ.get("BORG_RSH", "ssh") + args = shlex.split(rsh) + if location.port: + args += ["-p", str(location.port)] + if location.user: + args.append(f"{location.user}@{location.host}") + else: + args.append("%s" % location.host) + return args + + def call(self, cmd, args, **kw): + for resp in self.call_many(cmd, [args], **kw): + return resp + + def call_many(self, cmd, calls, wait=True, is_preloaded=False, async_wait=True): + if not calls and cmd != "async_responses": + return + + def send_buffer(): + if self.to_send: + try: + written = self.ratelimit.write(self.stdin_fd, self.to_send.peek_front()) + self.tx_bytes += written + self.to_send.pop_front(written) + except OSError as e: + # io.write might raise EAGAIN even though select indicates + # that the fd should be writable. + # EWOULDBLOCK is added for defensive programming sake. + if e.errno not in [errno.EAGAIN, errno.EWOULDBLOCK]: + raise + + def pop_preload_msgid(chunkid): + msgid = self.chunkid_to_msgids[chunkid].pop(0) + if not self.chunkid_to_msgids[chunkid]: + del self.chunkid_to_msgids[chunkid] + return msgid + + def handle_error(unpacked): + if "exception_class" not in unpacked: + return + + error = unpacked["exception_class"] + args = unpacked["exception_args"] + + if error == "Error": + raise Error(args[0]) + elif error == "ErrorWithTraceback": + raise ErrorWithTraceback(args[0]) + elif error == "DoesNotExist": + raise Repository3.DoesNotExist(self.location.processed) + elif error == "AlreadyExists": + raise Repository3.AlreadyExists(self.location.processed) + elif error == "CheckNeeded": + raise Repository3.CheckNeeded(self.location.processed) + elif error == "IntegrityError": + raise IntegrityError(args[0]) + elif error == "PathNotAllowed": + raise PathNotAllowed(args[0]) + elif error == "PathPermissionDenied": + raise Repository3.PathPermissionDenied(args[0]) + elif error == "ParentPathDoesNotExist": + raise Repository3.ParentPathDoesNotExist(args[0]) + elif error == "ObjectNotFound": + raise Repository3.ObjectNotFound(args[0], self.location.processed) + elif error == "InvalidRPCMethod": + raise InvalidRPCMethod(args[0]) + elif error == "LockTimeout": + raise LockTimeout(args[0]) + elif error == "LockFailed": + raise LockFailed(args[0], args[1]) + elif error == "NotLocked": + raise NotLocked(args[0]) + elif error == "NotMyLock": + raise NotMyLock(args[0]) + else: + raise self.RPCError(unpacked) + + calls = list(calls) + waiting_for = [] + maximum_to_send = 0 if wait else self.upload_buffer_size_limit + send_buffer() # Try to send data, as some cases (async_response) will never try to send data otherwise. + while wait or calls: + if self.shutdown_time and time.monotonic() > self.shutdown_time: + # we are shutting this RemoteRepository3 down already, make sure we do not waste + # a lot of time in case a lot of async stuff is coming in or remote is gone or slow. + logger.debug( + "shutdown_time reached, shutting down with %d waiting_for and %d async_responses.", + len(waiting_for), + len(self.async_responses), + ) + return + while waiting_for: + try: + unpacked = self.responses.pop(waiting_for[0]) + waiting_for.pop(0) + handle_error(unpacked) + yield unpacked[RESULT] + if not waiting_for and not calls: + return + except KeyError: + break + if cmd == "async_responses": + while True: + try: + msgid, unpacked = self.async_responses.popitem() + except KeyError: + # there is nothing left what we already have received + if async_wait and self.ignore_responses: + # but do not return if we shall wait and there is something left to wait for: + break + else: + return + else: + handle_error(unpacked) + yield unpacked[RESULT] + if self.to_send or ((calls or self.preload_ids) and len(waiting_for) < MAX_INFLIGHT): + w_fds = [self.stdin_fd] + else: + w_fds = [] + r, w, x = select.select(self.r_fds, w_fds, self.x_fds, 1) + if x: + raise Exception("FD exception occurred") + for fd in r: + if fd is self.stdout_fd: + data = os.read(fd, BUFSIZE) + if not data: + raise ConnectionClosed() + self.rx_bytes += len(data) + self.unpacker.feed(data) + for unpacked in self.unpacker: + if not isinstance(unpacked, dict): + raise UnexpectedRPCDataFormatFromServer(data) + + lr_dict = unpacked.get(LOG) + if lr_dict is not None: + # Re-emit remote log messages locally. + _logger = logging.getLogger(lr_dict["name"]) + if _logger.isEnabledFor(lr_dict["level"]): + _logger.handle(logging.LogRecord(**lr_dict)) + continue + + msgid = unpacked[MSGID] + if msgid in self.ignore_responses: + self.ignore_responses.remove(msgid) + # async methods never return values, but may raise exceptions. + if "exception_class" in unpacked: + self.async_responses[msgid] = unpacked + else: + # we currently do not have async result values except "None", + # so we do not add them into async_responses. + if unpacked[RESULT] is not None: + self.async_responses[msgid] = unpacked + else: + self.responses[msgid] = unpacked + elif fd is self.stderr_fd: + data = os.read(fd, 32768) + if not data: + raise ConnectionClosed() + self.rx_bytes += len(data) + # deal with incomplete lines (may appear due to block buffering) + if self.stderr_received: + data = self.stderr_received + data + self.stderr_received = b"" + lines = data.splitlines(keepends=True) + if lines and not lines[-1].endswith((b"\r", b"\n")): + self.stderr_received = lines.pop() + # now we have complete lines in and any partial line in self.stderr_received. + _logger = logging.getLogger() + for line in lines: + # borg serve (remote/server side) should not emit stuff on stderr, + # but e.g. the ssh process (local/client side) might output errors there. + assert line.endswith((b"\r", b"\n")) + # something came in on stderr, log it to not lose it. + # decode late, avoid partial utf-8 sequences. + _logger.warning("stderr: " + line.decode().strip()) + if w: + while ( + (len(self.to_send) <= maximum_to_send) + and (calls or self.preload_ids) + and len(waiting_for) < MAX_INFLIGHT + ): + if calls: + if is_preloaded: + assert cmd == "get", "is_preload is only supported for 'get'" + if calls[0]["id"] in self.chunkid_to_msgids: + waiting_for.append(pop_preload_msgid(calls.pop(0)["id"])) + else: + args = calls.pop(0) + if cmd == "get" and args["id"] in self.chunkid_to_msgids: + waiting_for.append(pop_preload_msgid(args["id"])) + else: + self.msgid += 1 + waiting_for.append(self.msgid) + self.to_send.push_back(msgpack.packb({MSGID: self.msgid, MSG: cmd, ARGS: args})) + if not self.to_send and self.preload_ids: + chunk_id = self.preload_ids.pop(0) + args = {"id": chunk_id} + self.msgid += 1 + self.chunkid_to_msgids.setdefault(chunk_id, []).append(self.msgid) + self.to_send.push_back(msgpack.packb({MSGID: self.msgid, MSG: "get", ARGS: args})) + + send_buffer() + self.ignore_responses |= set(waiting_for) # we lose order here + + @api( + since=parse_version("1.0.0"), + append_only={"since": parse_version("1.0.7"), "previously": False}, + make_parent_dirs={"since": parse_version("1.1.9"), "previously": False}, + v1_or_v2={"since": parse_version("2.0.0b8"), "previously": True}, # TODO fix version + ) + def open( + self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False, make_parent_dirs=False, v1_or_v2=False + ): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0a3")) + def info(self): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("1.0.0"), max_duration={"since": parse_version("1.2.0a4"), "previously": 0}) + def check(self, repair=False, max_duration=0): + """actual remoting is done via self.call in the @api decorator""" + + @api( + since=parse_version("1.0.0"), + compact={"since": parse_version("1.2.0a0"), "previously": True, "dontcare": True}, + threshold={"since": parse_version("1.2.0a8"), "previously": 0.1, "dontcare": True}, + ) + def commit(self, compact=True, threshold=0.1): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("1.0.0")) + def rollback(self): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("1.0.0")) + def destroy(self): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("1.0.0")) + def __len__(self): + """actual remoting is done via self.call in the @api decorator""" + + @api( + since=parse_version("1.0.0"), + mask={"since": parse_version("2.0.0b2"), "previously": 0}, + value={"since": parse_version("2.0.0b2"), "previously": 0}, + ) + def list(self, limit=None, marker=None, mask=0, value=0): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b3")) + def scan(self, limit=None, state=None): + """actual remoting is done via self.call in the @api decorator""" + + def get(self, id, read_data=True): + for resp in self.get_many([id], read_data=read_data): + return resp + + def get_many(self, ids, read_data=True, is_preloaded=False): + yield from self.call_many("get", [{"id": id, "read_data": read_data} for id in ids], is_preloaded=is_preloaded) + + @api(since=parse_version("1.0.0")) + def put(self, id, data, wait=True): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("1.0.0")) + def delete(self, id, wait=True): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("1.0.0")) + def save_key(self, keydata): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("1.0.0")) + def load_key(self): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("1.0.0")) + def break_lock(self): + """actual remoting is done via self.call in the @api decorator""" + + def close(self): + if self.p or self.sock: + self.call("close", {}, wait=True) + if self.p: + self.p.stdin.close() + self.p.stdout.close() + self.p.wait() + self.p = None + if self.sock: + try: + self.sock.shutdown(socket.SHUT_RDWR) + except OSError as e: + if e.errno != errno.ENOTCONN: + raise + self.sock.close() + self.sock = None + + def async_response(self, wait=True): + for resp in self.call_many("async_responses", calls=[], wait=True, async_wait=wait): + return resp + + def preload(self, ids): + self.preload_ids += ids + + +class RepositoryNoCache: + """A not caching Repository wrapper, passes through to repository. + + Just to have same API (including the context manager) as RepositoryCache. + + *transform* is a callable taking two arguments, key and raw repository data. + The return value is returned from get()/get_many(). By default, the raw + repository data is returned. + """ + + def __init__(self, repository, transform=None): + self.repository = repository + self.transform = transform or (lambda key, data: data) + + def close(self): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def get(self, key, read_data=True): + return next(self.get_many([key], read_data=read_data, cache=False)) + + def get_many(self, keys, read_data=True, cache=True): + for key, data in zip(keys, self.repository.get_many(keys, read_data=read_data)): + yield self.transform(key, data) + + def log_instrumentation(self): + pass + + +class RepositoryCache(RepositoryNoCache): + """ + A caching Repository wrapper. + + Caches Repository GET operations locally. + + *pack* and *unpack* complement *transform* of the base class. + *pack* receives the output of *transform* and should return bytes, + which are stored in the cache. *unpack* receives these bytes and + should return the initial data (as returned by *transform*). + """ + + def __init__(self, repository, pack=None, unpack=None, transform=None): + super().__init__(repository, transform) + self.pack = pack or (lambda data: data) + self.unpack = unpack or (lambda data: data) + self.cache = set() + self.basedir = tempfile.mkdtemp(prefix="borg-cache-") + self.query_size_limit() + self.size = 0 + # Instrumentation + self.hits = 0 + self.misses = 0 + self.slow_misses = 0 + self.slow_lat = 0.0 + self.evictions = 0 + self.enospc = 0 + + def query_size_limit(self): + available_space = shutil.disk_usage(self.basedir).free + self.size_limit = int(min(available_space * 0.25, 2**31)) + + def prefixed_key(self, key, complete): + # just prefix another byte telling whether this key refers to a complete chunk + # or a without-data-metadata-only chunk (see also read_data param). + prefix = b"\x01" if complete else b"\x00" + return prefix + key + + def key_filename(self, key): + return os.path.join(self.basedir, bin_to_hex(key)) + + def backoff(self): + self.query_size_limit() + target_size = int(0.9 * self.size_limit) + while self.size > target_size and self.cache: + key = self.cache.pop() + file = self.key_filename(key) + self.size -= os.stat(file).st_size + os.unlink(file) + self.evictions += 1 + + def add_entry(self, key, data, cache, complete): + transformed = self.transform(key, data) + if not cache: + return transformed + packed = self.pack(transformed) + pkey = self.prefixed_key(key, complete=complete) + file = self.key_filename(pkey) + try: + with open(file, "wb") as fd: + fd.write(packed) + except OSError as os_error: + try: + safe_unlink(file) + except FileNotFoundError: + pass # open() could have failed as well + if os_error.errno == errno.ENOSPC: + self.enospc += 1 + self.backoff() + else: + raise + else: + self.size += len(packed) + self.cache.add(pkey) + if self.size > self.size_limit: + self.backoff() + return transformed + + def log_instrumentation(self): + logger.debug( + "RepositoryCache: current items %d, size %s / %s, %d hits, %d misses, %d slow misses (+%.1fs), " + "%d evictions, %d ENOSPC hit", + len(self.cache), + format_file_size(self.size), + format_file_size(self.size_limit), + self.hits, + self.misses, + self.slow_misses, + self.slow_lat, + self.evictions, + self.enospc, + ) + + def close(self): + self.log_instrumentation() + self.cache.clear() + shutil.rmtree(self.basedir) + + def get_many(self, keys, read_data=True, cache=True): + # It could use different cache keys depending on read_data and cache full vs. meta-only chunks. + unknown_keys = [key for key in keys if self.prefixed_key(key, complete=read_data) not in self.cache] + repository_iterator = zip(unknown_keys, self.repository.get_many(unknown_keys, read_data=read_data)) + for key in keys: + pkey = self.prefixed_key(key, complete=read_data) + if pkey in self.cache: + file = self.key_filename(pkey) + with open(file, "rb") as fd: + self.hits += 1 + yield self.unpack(fd.read()) + else: + for key_, data in repository_iterator: + if key_ == key: + transformed = self.add_entry(key, data, cache, complete=read_data) + self.misses += 1 + yield transformed + break + else: + # slow path: eviction during this get_many removed this key from the cache + t0 = time.perf_counter() + data = self.repository.get(key, read_data=read_data) + self.slow_lat += time.perf_counter() - t0 + transformed = self.add_entry(key, data, cache, complete=read_data) + self.slow_misses += 1 + yield transformed + # Consume any pending requests + for _ in repository_iterator: + pass + + +def cache_if_remote(repository, *, decrypted_cache=False, pack=None, unpack=None, transform=None, force_cache=False): + """ + Return a Repository(No)Cache for *repository*. + + If *decrypted_cache* is a repo_objs object, then get and get_many will return a tuple + (csize, plaintext) instead of the actual data in the repository. The cache will + store decrypted data, which increases CPU efficiency (by avoiding repeatedly decrypting + and more importantly MAC and ID checking cached objects). + Internally, objects are compressed with LZ4. + """ + if decrypted_cache and (pack or unpack or transform): + raise ValueError("decrypted_cache and pack/unpack/transform are incompatible") + elif decrypted_cache: + repo_objs = decrypted_cache + # 32 bit csize, 64 bit (8 byte) xxh64, 1 byte ctype, 1 byte clevel + cache_struct = struct.Struct("=I8sBB") + compressor = Compressor("lz4") + + def pack(data): + csize, decrypted = data + meta, compressed = compressor.compress({}, decrypted) + return cache_struct.pack(csize, xxh64(compressed), meta["ctype"], meta["clevel"]) + compressed + + def unpack(data): + data = memoryview(data) + csize, checksum, ctype, clevel = cache_struct.unpack(data[: cache_struct.size]) + compressed = data[cache_struct.size :] + if checksum != xxh64(compressed): + raise IntegrityError("detected corrupted data in metadata cache") + meta = dict(ctype=ctype, clevel=clevel, csize=len(compressed)) + _, decrypted = compressor.decompress(meta, compressed) + return csize, decrypted + + def transform(id_, data): + meta, decrypted = repo_objs.parse(id_, data, ro_type=ROBJ_DONTCARE) + csize = meta.get("csize", len(data)) + return csize, decrypted + + if isinstance(repository, RemoteRepository3) or force_cache: + return RepositoryCache(repository, pack, unpack, transform) + else: + return RepositoryNoCache(repository, transform) diff --git a/src/borg/repository3.py b/src/borg/repository3.py new file mode 100644 index 000000000..3edee5c4b --- /dev/null +++ b/src/borg/repository3.py @@ -0,0 +1,314 @@ +import os + +from borgstore.store import Store +from borgstore.store import ObjectNotFound as StoreObjectNotFound + +from .constants import * # NOQA +from .helpers import Error, ErrorWithTraceback, IntegrityError +from .helpers import Location +from .helpers import bin_to_hex, hex_to_bin +from .logger import create_logger +from .repoobj import RepoObj + +logger = create_logger(__name__) + + +class Repository3: + """borgstore based key value store""" + + class AlreadyExists(Error): + """A repository already exists at {}.""" + + exit_mcode = 10 + + class CheckNeeded(ErrorWithTraceback): + """Inconsistency detected. Please run "borg check {}".""" + + exit_mcode = 12 + + class DoesNotExist(Error): + """Repository {} does not exist.""" + + exit_mcode = 13 + + class InsufficientFreeSpaceError(Error): + """Insufficient free space to complete transaction (required: {}, available: {}).""" + + exit_mcode = 14 + + class InvalidRepository(Error): + """{} is not a valid repository. Check repo config.""" + + exit_mcode = 15 + + class InvalidRepositoryConfig(Error): + """{} does not have a valid configuration. Check repo config [{}].""" + + exit_mcode = 16 + + class ObjectNotFound(ErrorWithTraceback): + """Object with key {} not found in repository {}.""" + + exit_mcode = 17 + + def __init__(self, id, repo): + if isinstance(id, bytes): + id = bin_to_hex(id) + super().__init__(id, repo) + + class ParentPathDoesNotExist(Error): + """The parent path of the repo directory [{}] does not exist.""" + + exit_mcode = 18 + + class PathAlreadyExists(Error): + """There is already something at {}.""" + + exit_mcode = 19 + + class StorageQuotaExceeded(Error): + """The storage quota ({}) has been exceeded ({}). Try deleting some archives.""" + + exit_mcode = 20 + + class PathPermissionDenied(Error): + """Permission denied to {}.""" + + exit_mcode = 21 + + def __init__( + self, + path, + create=False, + exclusive=False, + lock_wait=None, + lock=True, + append_only=False, + storage_quota=None, + make_parent_dirs=False, + send_log_cb=None, + ): + self.path = os.path.abspath(path) + url = "file://%s" % self.path + # use a Store with flat config storage and 2-levels-nested data storage + self.store = Store(url, levels={"config/": [0], "data/": [2]}) + self._location = Location(url) + self.version = None + # long-running repository methods which emit log or progress output are responsible for calling + # the ._send_log method periodically to get log and progress output transferred to the borg client + # in a timely manner, in case we have a RemoteRepository. + # for local repositories ._send_log can be called also (it will just do nothing in that case). + self._send_log = send_log_cb or (lambda: None) + self.do_create = create + self.created = False + self.acceptable_repo_versions = (3, ) + self.opened = False + self.append_only = append_only # XXX not implemented / not implementable + self.storage_quota = storage_quota # XXX not implemented + self.storage_quota_use = 0 # XXX not implemented + + def __repr__(self): + return f"<{self.__class__.__name__} {self.path}>" + + def __enter__(self): + if self.do_create: + self.do_create = False + self.create() + self.created = True + self.open() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + @property + def id_str(self): + return bin_to_hex(self.id) + + def create(self): + """Create a new empty repository""" + self.store.create() + self.store.open() + self.store.store("config/readme", REPOSITORY_README.encode()) + self.version = 3 + self.store.store("config/version", str(self.version).encode()) + self.store.store("config/id", bin_to_hex(os.urandom(32)).encode()) + self.store.close() + + def _set_id(self, id): + # for testing: change the id of an existing repository + assert self.opened + assert isinstance(id, bytes) and len(id) == 32 + self.id = id + self.store.store("config/id", bin_to_hex(id).encode()) + + def save_key(self, keydata): + # note: saving an empty key means that there is no repokey anymore + self.store.store("keys/repokey", keydata) + + def load_key(self): + keydata = self.store.load("keys/repokey") + # note: if we return an empty string, it means there is no repo key + return keydata + + def destroy(self): + """Destroy the repository""" + self.close() + self.store.destroy() + + def open(self): + self.store.open() + readme = self.store.load("config/readme").decode() + if readme != REPOSITORY_README: + raise self.InvalidRepository(self.path) + self.version = int(self.store.load("config/version").decode()) + if self.version not in self.acceptable_repo_versions: + self.close() + raise self.InvalidRepositoryConfig( + self.path, "repository version %d is not supported by this borg version" % self.version + ) + self.id = hex_to_bin(self.store.load("config/id").decode(), length=32) + self.opened = True + + def close(self): + if self.opened: + self.store.close() + self.opened = False + + def info(self): + """return some infos about the repo (must be opened first)""" + info = dict(id=self.id, version=self.version, storage_quota_use=self.storage_quota_use, storage_quota=self.storage_quota, append_only=self.append_only) + return info + + def commit(self, compact=True, threshold=0.1): + pass + + def check(self, repair=False, max_duration=0): + """Check repository consistency + + This method verifies all segment checksums and makes sure + the index is consistent with the data stored in the segments. + """ + mode = "full" + logger.info("Starting repository check") + # XXX TODO + logger.info("Finished %s repository check, no problems found.", mode) + return True + + def scan_low_level(self, segment=None, offset=None): + raise NotImplementedError + + def __len__(self): + raise NotImplementedError + + def __contains__(self, id): + raise NotImplementedError + + def list(self, limit=None, marker=None, mask=0, value=0): + """ + list IDs starting from after id - in index (pseudo-random) order. + + if mask and value are given, only return IDs where flags & mask == value (default: all IDs). + """ + infos = self.store.list("data") # XXX we can only get the full list from the store + ids = [hex_to_bin(info.name) for info in infos] + if marker is not None: + idx = ids.index(marker) + ids = ids[idx + 1:] + if limit is not None: + return ids[:limit] + return ids + + + def scan(self, limit=None, state=None): + """ + list (the next) chunk IDs from the repository. + + state can either be None (initially, when starting to scan) or the object + returned from a previous scan call (meaning "continue scanning"). + + returns: list of chunk ids, state + """ + # we only have store.list() anyway, so just call .list() from here. + ids = self.list(limit=limit, marker=state) + state = ids[-1] if ids else None + return ids, state + + def get(self, id, read_data=True): + id_hex = bin_to_hex(id) + key = "data/" + id_hex + try: + if read_data: + # read everything + return self.store.load(key) + else: + # RepoObj layout supports separately encrypted metadata and data. + # We return enough bytes so the client can decrypt the metadata. + meta_len_size = RepoObj.meta_len_hdr.size + extra_len = 1024 - meta_len_size # load a bit more, 1024b, reduces round trips + obj = self.store.load(key, size=meta_len_size + extra_len) + meta_len = obj[0:meta_len_size] + if len(meta_len) != meta_len_size: + raise IntegrityError( + f"Object too small [id {id_hex}]: expected {meta_len_size}, got {len(meta_len)} bytes" + ) + ml = RepoObj.meta_len_hdr.unpack(meta_len)[0] + if ml > extra_len: + # we did not get enough, need to load more, but not all. + # this should be rare, as chunk metadata is rather small usually. + obj = self.store.load(key, size=meta_len_size + ml) + meta = obj[meta_len_size:meta_len_size + ml] + if len(meta) != ml: + raise IntegrityError( + f"Object too small [id {id_hex}]: expected {ml}, got {len(meta)} bytes" + ) + return meta_len + meta + except StoreObjectNotFound: + raise self.ObjectNotFound(id, self.path) from None + + + def get_many(self, ids, read_data=True, is_preloaded=False): + for id_ in ids: + yield self.get(id_, read_data=read_data) + + def put(self, id, data, wait=True): + """put a repo object + + Note: when doing calls with wait=False this gets async and caller must + deal with async results / exceptions later. + """ + data_size = len(data) + if data_size > MAX_DATA_SIZE: + raise IntegrityError(f"More than allowed put data [{data_size} > {MAX_DATA_SIZE}]") + + key = "data/" + bin_to_hex(id) + self.store.store(key, data) + + def delete(self, id, wait=True): + """delete a repo object + + Note: when doing calls with wait=False this gets async and caller must + deal with async results / exceptions later. + """ + key = "data/" + bin_to_hex(id) + try: + self.store.delete(key) + except StoreObjectNotFound: + raise self.ObjectNotFound(id, self.path) from None + + def async_response(self, wait=True): + """Get one async result (only applies to remote repositories). + + async commands (== calls with wait=False, e.g. delete and put) have no results, + but may raise exceptions. These async exceptions must get collected later via + async_response() calls. Repeat the call until it returns None. + The previous calls might either return one (non-None) result or raise an exception. + If wait=True is given and there are outstanding responses, it will wait for them + to arrive. With wait=False, it will only return already received responses. + """ + + def preload(self, ids): + """Preload objects (only applies to remote repositories)""" + + def break_lock(self): + pass diff --git a/src/borg/testsuite/archiver/__init__.py b/src/borg/testsuite/archiver/__init__.py index 9d7a5db42..b8da0ea1d 100644 --- a/src/borg/testsuite/archiver/__init__.py +++ b/src/borg/testsuite/archiver/__init__.py @@ -27,8 +27,8 @@ from ...logger import flush_logging from ...manifest import Manifest from ...platform import get_flags -from ...remote import RemoteRepository -from ...repository import Repository +from ...remote3 import RemoteRepository3 +from ...repository3 import Repository3 from .. import has_lchflags, is_utime_fully_supported, have_fuse_mtime_ns, st_mtime_ns_round, no_selinux from .. import changedir from .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported @@ -169,7 +169,7 @@ def create_src_archive(archiver, name, ts=None): def open_archive(repo_path, name): - repository = Repository(repo_path, exclusive=True) + repository = Repository3(repo_path, exclusive=True) with repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) archive = Archive(manifest, name) @@ -178,9 +178,9 @@ def open_archive(repo_path, name): def open_repository(archiver): if archiver.get_kind() == "remote": - return RemoteRepository(Location(archiver.repository_location)) + return RemoteRepository3(Location(archiver.repository_location)) else: - return Repository(archiver.repository_path, exclusive=True) + return Repository3(archiver.repository_path, exclusive=True) def create_regular_file(input_path, name, size=0, contents=None): @@ -256,17 +256,13 @@ def create_test_files(input_path, create_hardlinks=True): def _extract_repository_id(repo_path): - with Repository(repo_path) as repository: + with Repository3(repo_path) as repository: return repository.id def _set_repository_id(repo_path, id): - config = ConfigParser(interpolation=None) - config.read(os.path.join(repo_path, "config")) - config.set("repository", "id", bin_to_hex(id)) - with open(os.path.join(repo_path, "config"), "w") as fd: - config.write(fd) - with Repository(repo_path) as repository: + with Repository3(repo_path) as repository: + repository._set_id(id) return repository.id diff --git a/src/borg/testsuite/archiver/bypass_lock_option.py b/src/borg/testsuite/archiver/bypass_lock_option.py deleted file mode 100644 index 8ddeb6762..000000000 --- a/src/borg/testsuite/archiver/bypass_lock_option.py +++ /dev/null @@ -1,130 +0,0 @@ -import pytest - -from ...constants import * # NOQA -from ...helpers import EXIT_ERROR -from ...locking import LockFailed -from ...remote import RemoteRepository -from .. import llfuse -from . import cmd, create_src_archive, RK_ENCRYPTION, read_only, fuse_mount - - -def test_readonly_check(archiver): - cmd(archiver, "rcreate", RK_ENCRYPTION) - create_src_archive(archiver, "test") - - with read_only(archiver.repository_path): - # verify that command normally doesn't work with read-only repo - if archiver.FORK_DEFAULT: - cmd(archiver, "check", "--verify-data", exit_code=EXIT_ERROR) - else: - with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - cmd(archiver, "check", "--verify-data") - if isinstance(excinfo.value, RemoteRepository.RPCError): - assert excinfo.value.exception_class == "LockFailed" - # verify that command works with read-only repo when using --bypass-lock - cmd(archiver, "check", "--verify-data", "--bypass-lock") - - -def test_readonly_diff(archiver): - cmd(archiver, "rcreate", RK_ENCRYPTION) - create_src_archive(archiver, "a") - create_src_archive(archiver, "b") - - with read_only(archiver.repository_path): - # verify that command normally doesn't work with read-only repo - if archiver.FORK_DEFAULT: - cmd(archiver, "diff", "a", "b", exit_code=EXIT_ERROR) - else: - with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - cmd(archiver, "diff", "a", "b") - if isinstance(excinfo.value, RemoteRepository.RPCError): - assert excinfo.value.exception_class == "LockFailed" - # verify that command works with read-only repo when using --bypass-lock - cmd(archiver, "diff", "a", "b", "--bypass-lock") - - -def test_readonly_export_tar(archiver): - cmd(archiver, "rcreate", RK_ENCRYPTION) - create_src_archive(archiver, "test") - - with read_only(archiver.repository_path): - # verify that command normally doesn't work with read-only repo - if archiver.FORK_DEFAULT: - cmd(archiver, "export-tar", "test", "test.tar", exit_code=EXIT_ERROR) - else: - with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - cmd(archiver, "export-tar", "test", "test.tar") - if isinstance(excinfo.value, RemoteRepository.RPCError): - assert excinfo.value.exception_class == "LockFailed" - # verify that command works with read-only repo when using --bypass-lock - cmd(archiver, "export-tar", "test", "test.tar", "--bypass-lock") - - -def test_readonly_extract(archiver): - cmd(archiver, "rcreate", RK_ENCRYPTION) - create_src_archive(archiver, "test") - - with read_only(archiver.repository_path): - # verify that command normally doesn't work with read-only repo - if archiver.FORK_DEFAULT: - cmd(archiver, "extract", "test", exit_code=EXIT_ERROR) - else: - with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - cmd(archiver, "extract", "test") - if isinstance(excinfo.value, RemoteRepository.RPCError): - assert excinfo.value.exception_class == "LockFailed" - # verify that command works with read-only repo when using --bypass-lock - cmd(archiver, "extract", "test", "--bypass-lock") - - -def test_readonly_info(archiver): - cmd(archiver, "rcreate", RK_ENCRYPTION) - create_src_archive(archiver, "test") - with read_only(archiver.repository_path): - # verify that command normally doesn't work with read-only repo - if archiver.FORK_DEFAULT: - cmd(archiver, "rinfo", exit_code=EXIT_ERROR) - else: - with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - cmd(archiver, "rinfo") - if isinstance(excinfo.value, RemoteRepository.RPCError): - assert excinfo.value.exception_class == "LockFailed" - # verify that command works with read-only repo when using --bypass-lock - cmd(archiver, "rinfo", "--bypass-lock") - - -def test_readonly_list(archiver): - cmd(archiver, "rcreate", RK_ENCRYPTION) - create_src_archive(archiver, "test") - with read_only(archiver.repository_path): - # verify that command normally doesn't work with read-only repo - if archiver.FORK_DEFAULT: - cmd(archiver, "rlist", exit_code=EXIT_ERROR) - else: - with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - cmd(archiver, "rlist") - if isinstance(excinfo.value, RemoteRepository.RPCError): - assert excinfo.value.exception_class == "LockFailed" - # verify that command works with read-only repo when using --bypass-lock - cmd(archiver, "rlist", "--bypass-lock") - - -@pytest.mark.skipif(not llfuse, reason="llfuse not installed") -def test_readonly_mount(archiver): - cmd(archiver, "rcreate", RK_ENCRYPTION) - create_src_archive(archiver, "test") - with read_only(archiver.repository_path): - # verify that command normally doesn't work with read-only repo - if archiver.FORK_DEFAULT: - with fuse_mount(archiver, exit_code=EXIT_ERROR): - pass - else: - with pytest.raises((LockFailed, RemoteRepository.RPCError)) as excinfo: - # self.fuse_mount always assumes fork=True, so for this test we have to set fork=False manually - with fuse_mount(archiver, fork=False): - pass - if isinstance(excinfo.value, RemoteRepository.RPCError): - assert excinfo.value.exception_class == "LockFailed" - # verify that command works with read-only repo when using --bypass-lock - with fuse_mount(archiver, None, "--bypass-lock"): - pass diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index 87fd10ab3..2727a5781 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -8,7 +8,7 @@ from ...constants import * # NOQA from ...helpers import bin_to_hex, msgpack from ...manifest import Manifest -from ...repository import Repository +from ...repository3 import Repository3 from . import cmd, src_file, create_src_archive, open_archive, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA @@ -28,12 +28,10 @@ def test_check_usage(archivers, request): output = cmd(archiver, "check", "-v", "--progress", exit_code=0) assert "Starting repository check" in output assert "Starting archive consistency check" in output - assert "Checking segments" in output output = cmd(archiver, "check", "-v", "--repository-only", exit_code=0) assert "Starting repository check" in output assert "Starting archive consistency check" not in output - assert "Checking segments" not in output output = cmd(archiver, "check", "-v", "--archives-only", exit_code=0) assert "Starting repository check" not in output @@ -348,7 +346,7 @@ def test_extra_chunks(archivers, request): pytest.skip("only works locally") check_cmd_setup(archiver) cmd(archiver, "check", exit_code=0) - with Repository(archiver.repository_location, exclusive=True) as repository: + with Repository3(archiver.repository_location, exclusive=True) as repository: repository.put(b"01234567890123456789012345678901", b"xxxx") repository.commit(compact=False) output = cmd(archiver, "check", "-v", exit_code=0) # orphans are not considered warnings anymore @@ -391,7 +389,7 @@ def test_empty_repository(archivers, request): if archiver.get_kind() == "remote": pytest.skip("only works locally") check_cmd_setup(archiver) - with Repository(archiver.repository_location, exclusive=True) as repository: + with Repository3(archiver.repository_location, exclusive=True) as repository: for id_ in repository.list(): repository.delete(id_) repository.commit(compact=False) diff --git a/src/borg/testsuite/archiver/checks.py b/src/borg/testsuite/archiver/checks.py index a9324fbdf..8f5dd144a 100644 --- a/src/borg/testsuite/archiver/checks.py +++ b/src/borg/testsuite/archiver/checks.py @@ -9,8 +9,8 @@ from ...helpers import Location, get_security_dir, bin_to_hex from ...helpers import EXIT_ERROR from ...manifest import Manifest, MandatoryFeatureUnsupported -from ...remote import RemoteRepository, PathNotAllowed -from ...repository import Repository +from ...remote3 import RemoteRepository3, PathNotAllowed +from ...repository3 import Repository3 from .. import llfuse from .. import changedir from . import cmd, _extract_repository_id, open_repository, check_cache, create_test_files @@ -25,7 +25,7 @@ def get_security_directory(repo_path): def add_unknown_feature(repo_path, operation): - with Repository(repo_path, exclusive=True) as repository: + with Repository3(repo_path, exclusive=True) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) manifest.config["feature_flags"] = {operation.value: {"mandatory": ["unknown-feature"]}} manifest.write() @@ -272,7 +272,7 @@ def test_unknown_mandatory_feature_in_cache(archivers, request): remote_repo = archiver.get_kind() == "remote" print(cmd(archiver, "rcreate", RK_ENCRYPTION)) - with Repository(archiver.repository_path, exclusive=True) as repository: + with Repository3(archiver.repository_path, exclusive=True) as repository: if remote_repo: repository._location = Location(archiver.repository_location) manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -299,7 +299,7 @@ def wipe_wrapper(*args): if is_localcache: assert called - with Repository(archiver.repository_path, exclusive=True) as repository: + with Repository3(archiver.repository_path, exclusive=True) as repository: if remote_repo: repository._location = Location(archiver.repository_location) manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -346,26 +346,26 @@ def test_env_use_chunks_archive(archivers, request, monkeypatch): def test_remote_repo_restrict_to_path(remote_archiver): original_location, repo_path = remote_archiver.repository_location, remote_archiver.repository_path # restricted to repo directory itself: - with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-path", repo_path]): + with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-path", repo_path]): cmd(remote_archiver, "rcreate", RK_ENCRYPTION) # restricted to repo directory itself, fail for other directories with same prefix: - with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-path", repo_path]): + with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-path", repo_path]): with pytest.raises(PathNotAllowed): remote_archiver.repository_location = original_location + "_0" cmd(remote_archiver, "rcreate", RK_ENCRYPTION) # restricted to a completely different path: - with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-path", "/foo"]): + with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-path", "/foo"]): with pytest.raises(PathNotAllowed): remote_archiver.repository_location = original_location + "_1" cmd(remote_archiver, "rcreate", RK_ENCRYPTION) path_prefix = os.path.dirname(repo_path) # restrict to repo directory's parent directory: - with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-path", path_prefix]): + with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-path", path_prefix]): remote_archiver.repository_location = original_location + "_2" cmd(remote_archiver, "rcreate", RK_ENCRYPTION) # restrict to repo directory's parent directory and another directory: with patch.object( - RemoteRepository, "extra_test_args", ["--restrict-to-path", "/foo", "--restrict-to-path", path_prefix] + RemoteRepository3, "extra_test_args", ["--restrict-to-path", "/foo", "--restrict-to-path", path_prefix] ): remote_archiver.repository_location = original_location + "_3" cmd(remote_archiver, "rcreate", RK_ENCRYPTION) @@ -374,10 +374,10 @@ def test_remote_repo_restrict_to_path(remote_archiver): def test_remote_repo_restrict_to_repository(remote_archiver): repo_path = remote_archiver.repository_path # restricted to repo directory itself: - with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-repository", repo_path]): + with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-repository", repo_path]): cmd(remote_archiver, "rcreate", RK_ENCRYPTION) parent_path = os.path.join(repo_path, "..") - with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-repository", parent_path]): + with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-repository", parent_path]): with pytest.raises(PathNotAllowed): cmd(remote_archiver, "rcreate", RK_ENCRYPTION) diff --git a/src/borg/testsuite/archiver/config_cmd.py b/src/borg/testsuite/archiver/config_cmd.py deleted file mode 100644 index fa89df241..000000000 --- a/src/borg/testsuite/archiver/config_cmd.py +++ /dev/null @@ -1,64 +0,0 @@ -import os -import pytest - -from ...constants import * # NOQA -from . import RK_ENCRYPTION, create_test_files, cmd, generate_archiver_tests -from ...helpers import CommandError, Error - -pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,binary") # NOQA - - -def test_config(archivers, request): - archiver = request.getfixturevalue(archivers) - create_test_files(archiver.input_path) - os.unlink("input/flagfile") - cmd(archiver, "rcreate", RK_ENCRYPTION) - output = cmd(archiver, "config", "--list") - assert "[repository]" in output - assert "version" in output - assert "segments_per_dir" in output - assert "storage_quota" in output - assert "append_only" in output - assert "additional_free_space" in output - assert "id" in output - assert "last_segment_checked" not in output - - if archiver.FORK_DEFAULT: - output = cmd(archiver, "config", "last_segment_checked", exit_code=2) - assert "No option " in output - else: - with pytest.raises(Error): - cmd(archiver, "config", "last_segment_checked") - - cmd(archiver, "config", "last_segment_checked", "123") - output = cmd(archiver, "config", "last_segment_checked") - assert output == "123" + os.linesep - output = cmd(archiver, "config", "--list") - assert "last_segment_checked" in output - cmd(archiver, "config", "--delete", "last_segment_checked") - - for cfg_key, cfg_value in [("additional_free_space", "2G"), ("repository.append_only", "1")]: - output = cmd(archiver, "config", cfg_key) - assert output == "0" + os.linesep - cmd(archiver, "config", cfg_key, cfg_value) - output = cmd(archiver, "config", cfg_key) - assert output == cfg_value + os.linesep - cmd(archiver, "config", "--delete", cfg_key) - if archiver.FORK_DEFAULT: - cmd(archiver, "config", cfg_key, exit_code=2) - else: - with pytest.raises(Error): - cmd(archiver, "config", cfg_key) - - cmd(archiver, "config", "--list", "--delete", exit_code=2) - if archiver.FORK_DEFAULT: - expected_ec = CommandError().exit_code - cmd(archiver, "config", exit_code=expected_ec) - else: - with pytest.raises(CommandError): - cmd(archiver, "config") - if archiver.FORK_DEFAULT: - cmd(archiver, "config", "invalid-option", exit_code=2) - else: - with pytest.raises(Error): - cmd(archiver, "config", "invalid-option") diff --git a/src/borg/testsuite/archiver/corruption.py b/src/borg/testsuite/archiver/corruption.py index 65804eaca..3df1789d1 100644 --- a/src/borg/testsuite/archiver/corruption.py +++ b/src/borg/testsuite/archiver/corruption.py @@ -13,24 +13,6 @@ from ...cache import LocalCache -def test_check_corrupted_repository(archiver): - cmd(archiver, "rcreate", RK_ENCRYPTION) - create_src_archive(archiver, "test") - cmd(archiver, "extract", "test", "--dry-run") - cmd(archiver, "check") - - name = sorted(os.listdir(os.path.join(archiver.tmpdir, "repository", "data", "0")), reverse=True)[1] - with open(os.path.join(archiver.tmpdir, "repository", "data", "0", name), "r+b") as fd: - fd.seek(100) - fd.write(b"XXXX") - - if archiver.FORK_DEFAULT: - cmd(archiver, "check", exit_code=1) - else: - with pytest.raises(Error): - cmd(archiver, "check") - - def corrupt_archiver(archiver): create_test_files(archiver.input_path) cmd(archiver, "rcreate", RK_ENCRYPTION) diff --git a/src/borg/testsuite/archiver/create_cmd.py b/src/borg/testsuite/archiver/create_cmd.py index 72e8bc97d..4f740abfa 100644 --- a/src/borg/testsuite/archiver/create_cmd.py +++ b/src/borg/testsuite/archiver/create_cmd.py @@ -16,7 +16,7 @@ from ...constants import * # NOQA from ...manifest import Manifest from ...platform import is_cygwin, is_win32, is_darwin -from ...repository import Repository +from ...repository3 import Repository3 from ...helpers import CommandError, BackupPermissionError from .. import has_lchflags from .. import changedir @@ -668,7 +668,7 @@ def test_create_dry_run(archivers, request): cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "create", "--dry-run", "test", "input") # Make sure no archive has been created - with Repository(archiver.repository_path) as repository: + with Repository3(archiver.repository_path) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) assert len(manifest.archives) == 0 diff --git a/src/borg/testsuite/archiver/delete_cmd.py b/src/borg/testsuite/archiver/delete_cmd.py index 25c35e931..e931cc588 100644 --- a/src/borg/testsuite/archiver/delete_cmd.py +++ b/src/borg/testsuite/archiver/delete_cmd.py @@ -1,7 +1,7 @@ from ...archive import Archive from ...constants import * # NOQA from ...manifest import Manifest -from ...repository import Repository +from ...repository3 import Repository3 from . import cmd, create_regular_file, src_file, create_src_archive, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA @@ -47,7 +47,7 @@ def test_delete_force(archivers, request): archiver = request.getfixturevalue(archivers) cmd(archiver, "rcreate", "--encryption=none") create_src_archive(archiver, "test") - with Repository(archiver.repository_path, exclusive=True) as repository: + with Repository3(archiver.repository_path, exclusive=True) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) archive = Archive(manifest, "test") for item in archive.iter_items(): @@ -69,7 +69,7 @@ def test_delete_double_force(archivers, request): archiver = request.getfixturevalue(archivers) cmd(archiver, "rcreate", "--encryption=none") create_src_archive(archiver, "test") - with Repository(archiver.repository_path, exclusive=True) as repository: + with Repository3(archiver.repository_path, exclusive=True) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) archive = Archive(manifest, "test") id = archive.metadata.items[0] diff --git a/src/borg/testsuite/archiver/key_cmds.py b/src/borg/testsuite/archiver/key_cmds.py index ef00e007e..32a56fdd6 100644 --- a/src/borg/testsuite/archiver/key_cmds.py +++ b/src/borg/testsuite/archiver/key_cmds.py @@ -9,7 +9,7 @@ from ...helpers import CommandError from ...helpers import bin_to_hex, hex_to_bin from ...helpers import msgpack -from ...repository import Repository +from ...repository3 import Repository3 from .. import key from . import RK_ENCRYPTION, KF_ENCRYPTION, cmd, _extract_repository_id, _set_repository_id, generate_archiver_tests @@ -129,7 +129,7 @@ def test_key_export_repokey(archivers, request): assert export_contents.startswith("BORG_KEY " + bin_to_hex(repo_id) + "\n") - with Repository(archiver.repository_path) as repository: + with Repository3(archiver.repository_path) as repository: repo_key = AESOCBRepoKey(repository) repo_key.load(None, Passphrase.env_passphrase()) @@ -138,12 +138,12 @@ def test_key_export_repokey(archivers, request): assert repo_key.crypt_key == backup_key.crypt_key - with Repository(archiver.repository_path) as repository: + with Repository3(archiver.repository_path) as repository: repository.save_key(b"") cmd(archiver, "key", "import", export_file) - with Repository(archiver.repository_path) as repository: + with Repository3(archiver.repository_path) as repository: repo_key2 = AESOCBRepoKey(repository) repo_key2.load(None, Passphrase.env_passphrase()) @@ -302,7 +302,7 @@ def test_init_defaults_to_argon2(archivers, request): """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401""" archiver = request.getfixturevalue(archivers) cmd(archiver, "rcreate", RK_ENCRYPTION) - with Repository(archiver.repository_path) as repository: + with Repository3(archiver.repository_path) as repository: key = msgpack.unpackb(binascii.a2b_base64(repository.load_key())) assert key["algorithm"] == "argon2 chacha20-poly1305" @@ -313,7 +313,7 @@ def test_change_passphrase_does_not_change_algorithm_argon2(archivers, request): os.environ["BORG_NEW_PASSPHRASE"] = "newpassphrase" cmd(archiver, "key", "change-passphrase") - with Repository(archiver.repository_path) as repository: + with Repository3(archiver.repository_path) as repository: key = msgpack.unpackb(binascii.a2b_base64(repository.load_key())) assert key["algorithm"] == "argon2 chacha20-poly1305" @@ -323,6 +323,6 @@ def test_change_location_does_not_change_algorithm_argon2(archivers, request): cmd(archiver, "rcreate", KF_ENCRYPTION) cmd(archiver, "key", "change-location", "repokey") - with Repository(archiver.repository_path) as repository: + with Repository3(archiver.repository_path) as repository: key = msgpack.unpackb(binascii.a2b_base64(repository.load_key())) assert key["algorithm"] == "argon2 chacha20-poly1305" diff --git a/src/borg/testsuite/archiver/rcompress_cmd.py b/src/borg/testsuite/archiver/rcompress_cmd.py index 4635f05ac..680f1a427 100644 --- a/src/borg/testsuite/archiver/rcompress_cmd.py +++ b/src/borg/testsuite/archiver/rcompress_cmd.py @@ -1,7 +1,7 @@ import os from ...constants import * # NOQA -from ...repository import Repository +from ...repository3 import Repository3 from ...manifest import Manifest from ...compress import ZSTD, ZLIB, LZ4, CNONE from ...helpers import bin_to_hex @@ -12,7 +12,7 @@ def test_rcompress(archiver): def check_compression(ctype, clevel, olevel): """check if all the chunks in the repo are compressed/obfuscated like expected""" - repository = Repository(archiver.repository_path, exclusive=True) + repository = Repository3(archiver.repository_path, exclusive=True) with repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) state = None diff --git a/src/borg/testsuite/archiver/rcreate_cmd.py b/src/borg/testsuite/archiver/rcreate_cmd.py index b027ca1a8..0569d747d 100644 --- a/src/borg/testsuite/archiver/rcreate_cmd.py +++ b/src/borg/testsuite/archiver/rcreate_cmd.py @@ -6,28 +6,11 @@ from ...helpers.errors import Error, CancelledByUser from ...constants import * # NOQA from ...crypto.key import FlexiKey -from ...repository import Repository from . import cmd, generate_archiver_tests, RK_ENCRYPTION, KF_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA -def test_rcreate_parent_dirs(archivers, request): - archiver = request.getfixturevalue(archivers) - if archiver.EXE: - pytest.skip("does not raise Exception, but sets rc==2") - remote_repo = archiver.get_kind() == "remote" - parent_path = os.path.join(archiver.tmpdir, "parent1", "parent2") - repository_path = os.path.join(parent_path, "repository") - archiver.repository_location = ("ssh://__testsuite__" + repository_path) if remote_repo else repository_path - with pytest.raises(Repository.ParentPathDoesNotExist): - # normal borg rcreate does NOT create missing parent dirs - cmd(archiver, "rcreate", "--encryption=none") - # but if told so, it does: - cmd(archiver, "rcreate", "--encryption=none", "--make-parent-dirs") - assert os.path.exists(parent_path) - - def test_rcreate_interrupt(archivers, request): archiver = request.getfixturevalue(archivers) if archiver.EXE: @@ -51,18 +34,6 @@ def test_rcreate_requires_encryption_option(archivers, request): cmd(archiver, "rcreate", exit_code=2) -def test_rcreate_nested_repositories(archivers, request): - archiver = request.getfixturevalue(archivers) - cmd(archiver, "rcreate", RK_ENCRYPTION) - archiver.repository_location += "/nested" - if archiver.FORK_DEFAULT: - expected_ec = Repository.AlreadyExists().exit_code - cmd(archiver, "rcreate", RK_ENCRYPTION, exit_code=expected_ec) - else: - with pytest.raises(Repository.AlreadyExists): - cmd(archiver, "rcreate", RK_ENCRYPTION) - - def test_rcreate_refuse_to_overwrite_keyfile(archivers, request, monkeypatch): # BORG_KEY_FILE=something borg rcreate should quit if "something" already exists. # See: https://github.com/borgbackup/borg/pull/6046 diff --git a/src/borg/testsuite/archiver/rename_cmd.py b/src/borg/testsuite/archiver/rename_cmd.py index 5a1b65c0a..7a1637733 100644 --- a/src/borg/testsuite/archiver/rename_cmd.py +++ b/src/borg/testsuite/archiver/rename_cmd.py @@ -1,6 +1,6 @@ from ...constants import * # NOQA from ...manifest import Manifest -from ...repository import Repository +from ...repository3 import Repository3 from . import cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA @@ -21,7 +21,7 @@ def test_rename(archivers, request): cmd(archiver, "extract", "test.3", "--dry-run") cmd(archiver, "extract", "test.4", "--dry-run") # Make sure both archives have been renamed - with Repository(archiver.repository_path) as repository: + with Repository3(archiver.repository_path) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) assert len(manifest.archives) == 2 assert "test.3" in manifest.archives diff --git a/src/borg/testsuite/archiver/return_codes.py b/src/borg/testsuite/archiver/return_codes.py index 9c23f7995..3825904a4 100644 --- a/src/borg/testsuite/archiver/return_codes.py +++ b/src/borg/testsuite/archiver/return_codes.py @@ -5,7 +5,7 @@ def test_return_codes(cmd_fixture, tmpdir): - repo = tmpdir.mkdir("repo") + repo = tmpdir / "repo" # borg creates the directory input = tmpdir.mkdir("input") output = tmpdir.mkdir("output") input.join("test_file").write("content") diff --git a/src/borg/testsuite/archiver/rinfo_cmd.py b/src/borg/testsuite/archiver/rinfo_cmd.py index bf2b14c52..269c08326 100644 --- a/src/borg/testsuite/archiver/rinfo_cmd.py +++ b/src/borg/testsuite/archiver/rinfo_cmd.py @@ -35,21 +35,3 @@ def test_info_json(archivers, request): stats = cache["stats"] assert all(isinstance(o, int) for o in stats.values()) assert all(key in stats for key in ("total_chunks", "total_size", "total_unique_chunks", "unique_size")) - - -def test_info_on_repository_with_storage_quota(archivers, request): - archiver = request.getfixturevalue(archivers) - create_regular_file(archiver.input_path, "file1", contents=randbytes(1000 * 1000)) - cmd(archiver, "rcreate", RK_ENCRYPTION, "--storage-quota=1G") - cmd(archiver, "create", "test", "input") - info_repo = cmd(archiver, "rinfo") - assert "Storage quota: 1.00 MB used out of 1.00 GB" in info_repo - - -def test_info_on_repository_without_storage_quota(archivers, request): - archiver = request.getfixturevalue(archivers) - create_regular_file(archiver.input_path, "file1", contents=randbytes(1000 * 1000)) - cmd(archiver, "rcreate", RK_ENCRYPTION) - cmd(archiver, "create", "test", "input") - info_repo = cmd(archiver, "rinfo") - assert "Storage quota: 1.00 MB used" in info_repo diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index 60cb870e3..c232c84b2 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -12,7 +12,7 @@ from ..crypto.key import AESOCBRepoKey from ..hashindex import ChunkIndex, CacheSynchronizer from ..manifest import Manifest -from ..repository import Repository +from ..repository3 import Repository3 class TestCacheSynchronizer: @@ -164,7 +164,7 @@ class TestAdHocCache: @pytest.fixture def repository(self, tmpdir): self.repository_location = os.path.join(str(tmpdir), "repository") - with Repository(self.repository_location, exclusive=True, create=True) as repository: + with Repository3(self.repository_location, exclusive=True, create=True) as repository: repository.put(H(1), b"1234") repository.put(Manifest.MANIFEST_ID, b"5678") yield repository @@ -201,7 +201,7 @@ def test_deletes_chunks_during_lifetime(self, cache, repository): assert cache.seen_chunk(H(5)) == 1 cache.chunk_decref(H(5), 1, Statistics()) assert not cache.seen_chunk(H(5)) - with pytest.raises(Repository.ObjectNotFound): + with pytest.raises(Repository3.ObjectNotFound): repository.get(H(5)) def test_files_cache(self, cache): diff --git a/src/borg/testsuite/repoobj.py b/src/borg/testsuite/repoobj.py index 44c364d81..f34fa07d0 100644 --- a/src/borg/testsuite/repoobj.py +++ b/src/borg/testsuite/repoobj.py @@ -3,14 +3,14 @@ from ..constants import ROBJ_FILE_STREAM, ROBJ_MANIFEST, ROBJ_ARCHIVE_META from ..crypto.key import PlaintextKey from ..helpers.errors import IntegrityError -from ..repository import Repository +from ..repository3 import Repository3 from ..repoobj import RepoObj, RepoObj1 from ..compress import LZ4 @pytest.fixture def repository(tmpdir): - return Repository(tmpdir, create=True) + return Repository3(tmpdir, create=True) @pytest.fixture diff --git a/src/borg/testsuite/repository3.py b/src/borg/testsuite/repository3.py new file mode 100644 index 000000000..533d18723 --- /dev/null +++ b/src/borg/testsuite/repository3.py @@ -0,0 +1,290 @@ +import logging +import os +import sys +from typing import Optional + +import pytest + +from ..helpers import Location +from ..helpers import IntegrityError +from ..platformflags import is_win32 +from ..remote3 import RemoteRepository3, InvalidRPCMethod, PathNotAllowed +from ..repository3 import Repository3, MAX_DATA_SIZE +from ..repoobj import RepoObj +from .hashindex import H + + +@pytest.fixture() +def repository(tmp_path): + repository_location = os.fspath(tmp_path / "repository") + yield Repository3(repository_location, exclusive=True, create=True) + + +@pytest.fixture() +def remote_repository(tmp_path): + if is_win32: + pytest.skip("Remote repository does not yet work on Windows.") + repository_location = Location("ssh://__testsuite__" + os.fspath(tmp_path / "repository")) + yield RemoteRepository3(repository_location, exclusive=True, create=True) + + +def pytest_generate_tests(metafunc): + # Generates tests that run on both local and remote repos + if "repo_fixtures" in metafunc.fixturenames: + metafunc.parametrize("repo_fixtures", ["repository", "remote_repository"]) + + +def get_repository_from_fixture(repo_fixtures, request): + # returns the repo object from the fixture for tests that run on both local and remote repos + return request.getfixturevalue(repo_fixtures) + + +def reopen(repository, exclusive: Optional[bool] = True, create=False): + if isinstance(repository, Repository3): + if repository.opened: + raise RuntimeError("Repo must be closed before a reopen. Cannot support nested repository contexts.") + return Repository3(repository.path, exclusive=exclusive, create=create) + + if isinstance(repository, RemoteRepository3): + if repository.p is not None or repository.sock is not None: + raise RuntimeError("Remote repo must be closed before a reopen. Cannot support nested repository contexts.") + return RemoteRepository3(repository.location, exclusive=exclusive, create=create) + + raise TypeError( + f"Invalid argument type. Expected 'Repository3' or 'RemoteRepository3', received '{type(repository).__name__}'." + ) + + +def fchunk(data, meta=b""): + # format chunk: create a raw chunk that has valid RepoObj layout, but does not use encryption or compression. + meta_len = RepoObj.meta_len_hdr.pack(len(meta)) + assert isinstance(data, bytes) + chunk = meta_len + meta + data + return chunk + + +def pchunk(chunk): + # parse chunk: parse data and meta from a raw chunk made by fchunk + meta_len_size = RepoObj.meta_len_hdr.size + meta_len = chunk[:meta_len_size] + meta_len = RepoObj.meta_len_hdr.unpack(meta_len)[0] + meta = chunk[meta_len_size : meta_len_size + meta_len] + data = chunk[meta_len_size + meta_len :] + return data, meta + + +def pdchunk(chunk): + # parse only data from a raw chunk made by fchunk + return pchunk(chunk)[0] + + +def test_basic_operations(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + for x in range(100): + repository.put(H(x), fchunk(b"SOMEDATA")) + key50 = H(50) + assert pdchunk(repository.get(key50)) == b"SOMEDATA" + repository.delete(key50) + with pytest.raises(Repository3.ObjectNotFound): + repository.get(key50) + with reopen(repository) as repository: + with pytest.raises(Repository3.ObjectNotFound): + repository.get(key50) + for x in range(100): + if x == 50: + continue + assert pdchunk(repository.get(H(x))) == b"SOMEDATA" + + +def test_read_data(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + meta, data = b"meta", b"data" + meta_len = RepoObj.meta_len_hdr.pack(len(meta)) + chunk_complete = meta_len + meta + data + chunk_short = meta_len + meta + repository.put(H(0), chunk_complete) + assert repository.get(H(0)) == chunk_complete + assert repository.get(H(0), read_data=True) == chunk_complete + assert repository.get(H(0), read_data=False) == chunk_short + + +def test_consistency(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repository.put(H(0), fchunk(b"foo")) + assert pdchunk(repository.get(H(0))) == b"foo" + repository.put(H(0), fchunk(b"foo2")) + assert pdchunk(repository.get(H(0))) == b"foo2" + repository.put(H(0), fchunk(b"bar")) + assert pdchunk(repository.get(H(0))) == b"bar" + repository.delete(H(0)) + with pytest.raises(Repository3.ObjectNotFound): + repository.get(H(0)) + + +def test_list(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + for x in range(100): + repository.put(H(x), fchunk(b"SOMEDATA")) + repo_list = repository.list() + assert len(repo_list) == 100 + first_half = repository.list(limit=50) + assert len(first_half) == 50 + assert first_half == repo_list[:50] + second_half = repository.list(marker=first_half[-1]) + assert len(second_half) == 50 + assert second_half == repo_list[50:] + assert len(repository.list(limit=50)) == 50 + + +def test_scan(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + for x in range(100): + repository.put(H(x), fchunk(b"SOMEDATA")) + ids, _ = repository.scan() + assert len(ids) == 100 + first_half, state = repository.scan(limit=50) + assert len(first_half) == 50 + assert first_half == ids[:50] + second_half, _ = repository.scan(state=state) + assert len(second_half) == 50 + assert second_half == ids[50:] + + +def test_max_data_size(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + max_data = b"x" * (MAX_DATA_SIZE - RepoObj.meta_len_hdr.size) + repository.put(H(0), fchunk(max_data)) + assert pdchunk(repository.get(H(0))) == max_data + with pytest.raises(IntegrityError): + repository.put(H(1), fchunk(max_data + b"x")) + + +def check(repository, repo_path, repair=False, status=True): + assert repository.check(repair=repair) == status + # Make sure no tmp files are left behind + tmp_files = [name for name in os.listdir(repo_path) if "tmp" in name] + assert tmp_files == [], "Found tmp files" + + +def _get_mock_args(): + class MockArgs: + remote_path = "borg" + umask = 0o077 + debug_topics = [] + rsh = None + + def __contains__(self, item): + # to behave like argparse.Namespace + return hasattr(self, item) + + return MockArgs() + + +def test_remote_invalid_rpc(remote_repository): + with remote_repository: + with pytest.raises(InvalidRPCMethod): + remote_repository.call("__init__", {}) + + +def test_remote_rpc_exception_transport(remote_repository): + with remote_repository: + s1 = "test string" + + try: + remote_repository.call("inject_exception", {"kind": "DoesNotExist"}) + except Repository3.DoesNotExist as e: + assert len(e.args) == 1 + assert e.args[0] == remote_repository.location.processed + + try: + remote_repository.call("inject_exception", {"kind": "AlreadyExists"}) + except Repository3.AlreadyExists as e: + assert len(e.args) == 1 + assert e.args[0] == remote_repository.location.processed + + try: + remote_repository.call("inject_exception", {"kind": "CheckNeeded"}) + except Repository3.CheckNeeded as e: + assert len(e.args) == 1 + assert e.args[0] == remote_repository.location.processed + + try: + remote_repository.call("inject_exception", {"kind": "IntegrityError"}) + except IntegrityError as e: + assert len(e.args) == 1 + assert e.args[0] == s1 + + try: + remote_repository.call("inject_exception", {"kind": "PathNotAllowed"}) + except PathNotAllowed as e: + assert len(e.args) == 1 + assert e.args[0] == "foo" + + try: + remote_repository.call("inject_exception", {"kind": "ObjectNotFound"}) + except Repository3.ObjectNotFound as e: + assert len(e.args) == 2 + assert e.args[0] == s1 + assert e.args[1] == remote_repository.location.processed + + try: + remote_repository.call("inject_exception", {"kind": "InvalidRPCMethod"}) + except InvalidRPCMethod as e: + assert len(e.args) == 1 + assert e.args[0] == s1 + + try: + remote_repository.call("inject_exception", {"kind": "divide"}) + except RemoteRepository3.RPCError as e: + assert e.unpacked + assert e.get_message() == "ZeroDivisionError: integer division or modulo by zero\n" + assert e.exception_class == "ZeroDivisionError" + assert len(e.exception_full) > 0 + + +def test_remote_ssh_cmd(remote_repository): + with remote_repository: + args = _get_mock_args() + remote_repository._args = args + assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "example.com"] + assert remote_repository.ssh_cmd(Location("ssh://user@example.com/foo")) == ["ssh", "user@example.com"] + assert remote_repository.ssh_cmd(Location("ssh://user@example.com:1234/foo")) == [ + "ssh", + "-p", + "1234", + "user@example.com", + ] + os.environ["BORG_RSH"] = "ssh --foo" + assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "--foo", "example.com"] + + +def test_remote_borg_cmd(remote_repository): + with remote_repository: + assert remote_repository.borg_cmd(None, testing=True) == [sys.executable, "-m", "borg", "serve"] + args = _get_mock_args() + # XXX without next line we get spurious test fails when using pytest-xdist, root cause unknown: + logging.getLogger().setLevel(logging.INFO) + # note: test logger is on info log level, so --info gets added automagically + assert remote_repository.borg_cmd(args, testing=False) == ["borg", "serve", "--info"] + args.remote_path = "borg-0.28.2" + assert remote_repository.borg_cmd(args, testing=False) == ["borg-0.28.2", "serve", "--info"] + args.debug_topics = ["something_client_side", "repository_compaction"] + assert remote_repository.borg_cmd(args, testing=False) == [ + "borg-0.28.2", + "serve", + "--info", + "--debug-topic=borg.debug.repository_compaction", + ] + args = _get_mock_args() + args.storage_quota = 0 + assert remote_repository.borg_cmd(args, testing=False) == ["borg", "serve", "--info"] + args.storage_quota = 314159265 + assert remote_repository.borg_cmd(args, testing=False) == [ + "borg", + "serve", + "--info", + "--storage-quota=314159265", + ] + args.rsh = "ssh -i foo" + remote_repository._args = args + assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "-i", "foo", "example.com"] diff --git a/tox.ini b/tox.ini index 10f3e7f30..402186dcd 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ deps = pytest mypy pkgconfig -commands = mypy +commands = mypy --ignore-missing-imports [testenv:docs] changedir = docs From d95cacd6248266c886a18301510bc2be6f034baa Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 7 Aug 2024 14:46:51 +0200 Subject: [PATCH 02/79] implement Repository3.check It uses xxh64 hashes of the meta and data parts to verify their validity. On a server with borg, this can be done server-side without the borg key. The new RepoObj header has meta_size, data_size, meta_hash and data_hash. --- src/borg/repoobj.py | 40 +++++++------ src/borg/repository.py | 25 ++++----- src/borg/repository3.py | 71 +++++++++++++++++------- src/borg/testsuite/archiver/check_cmd.py | 14 +++-- src/borg/testsuite/repository.py | 25 +++++---- src/borg/testsuite/repository3.py | 23 ++++---- 6 files changed, 118 insertions(+), 80 deletions(-) diff --git a/src/borg/repoobj.py b/src/borg/repoobj.py index 3fb2534ad..55208a457 100644 --- a/src/borg/repoobj.py +++ b/src/borg/repoobj.py @@ -1,6 +1,8 @@ +from collections import namedtuple from struct import Struct from .constants import * # NOQA +from .checksums import xxh64 from .helpers import msgpack, workarounds from .helpers.errors import IntegrityError from .compress import Compressor, LZ4_COMPRESSOR, get_compressor @@ -10,14 +12,17 @@ class RepoObj: - meta_len_hdr = Struct(" bytes: # used for crypto type detection - offs = cls.meta_len_hdr.size - meta_len = cls.meta_len_hdr.unpack(data[:offs])[0] - return data[offs + meta_len :] + hdr_size = cls.obj_header.size + hdr = cls.ObjHeader(*cls.obj_header.unpack(data[:hdr_size])) + return data[hdr_size + hdr.meta_size:] def __init__(self, key): self.key = key @@ -61,8 +66,9 @@ def format( data_encrypted = self.key.encrypt(id, data_compressed) meta_packed = msgpack.packb(meta) meta_encrypted = self.key.encrypt(id, meta_packed) - hdr = self.meta_len_hdr.pack(len(meta_encrypted)) - return hdr + meta_encrypted + data_encrypted + hdr = self.ObjHeader(len(meta_encrypted), len(data_encrypted), xxh64(meta_encrypted), xxh64(data_encrypted)) + hdr_packed = self.obj_header.pack(*hdr) + return hdr_packed + meta_encrypted + data_encrypted def parse_meta(self, id: bytes, cdata: bytes, ro_type: str) -> dict: # when calling parse_meta, enough cdata needs to be supplied to contain completely the @@ -71,11 +77,10 @@ def parse_meta(self, id: bytes, cdata: bytes, ro_type: str) -> dict: assert isinstance(cdata, bytes) assert isinstance(ro_type, str) obj = memoryview(cdata) - offs = self.meta_len_hdr.size - hdr = obj[:offs] - len_meta_encrypted = self.meta_len_hdr.unpack(hdr)[0] - assert offs + len_meta_encrypted <= len(obj) - meta_encrypted = obj[offs : offs + len_meta_encrypted] + hdr_size = self.obj_header.size + hdr = self.ObjHeader(*self.obj_header.unpack(obj[:hdr_size])) + assert hdr_size + hdr.meta_size <= len(obj) + meta_encrypted = obj[hdr_size:hdr_size + hdr.meta_size] meta_packed = self.key.decrypt(id, meta_encrypted) meta = msgpack.unpackb(meta_packed) if ro_type != ROBJ_DONTCARE and meta["type"] != ro_type: @@ -100,17 +105,16 @@ def parse( assert isinstance(id, bytes) assert isinstance(cdata, bytes) obj = memoryview(cdata) - offs = self.meta_len_hdr.size - hdr = obj[:offs] - len_meta_encrypted = self.meta_len_hdr.unpack(hdr)[0] - assert offs + len_meta_encrypted <= len(obj) - meta_encrypted = obj[offs : offs + len_meta_encrypted] - offs += len_meta_encrypted + hdr_size = self.obj_header.size + hdr = self.ObjHeader(*self.obj_header.unpack(obj[:hdr_size])) + assert hdr_size + hdr.meta_size <= len(obj) + meta_encrypted = obj[hdr_size : hdr_size + hdr.meta_size] meta_packed = self.key.decrypt(id, meta_encrypted) meta_compressed = msgpack.unpackb(meta_packed) # means: before adding more metadata in decompress block if ro_type != ROBJ_DONTCARE and meta_compressed["type"] != ro_type: raise IntegrityError(f"ro_type expected: {ro_type} got: {meta_compressed['type']}") - data_encrypted = obj[offs:] + assert hdr_size + hdr.meta_size + hdr.data_size <= len(obj) + data_encrypted = obj[hdr_size + hdr.meta_size:hdr_size + hdr.meta_size + hdr.data_size] data_compressed = self.key.decrypt(id, data_encrypted) # does not include the type/level bytes if decompress: ctype = meta_compressed["ctype"] diff --git a/src/borg/repository.py b/src/borg/repository.py index 079fbc21e..74591619d 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1837,25 +1837,24 @@ def check_crc32(wanted, header, *data): # supporting separately encrypted metadata and data. # In this case, we return enough bytes so the client can decrypt the metadata # and seek over the rest (over the encrypted data). - meta_len_size = RepoObj.meta_len_hdr.size - meta_len = fd.read(meta_len_size) - length -= meta_len_size - if len(meta_len) != meta_len_size: + hdr_size = RepoObj.obj_header.size + hdr = fd.read(hdr_size) + length -= hdr_size + if len(hdr) != hdr_size: raise IntegrityError( f"Segment entry meta length short read [segment {segment}, offset {offset}]: " - f"expected {meta_len_size}, got {len(meta_len)} bytes" + f"expected {hdr_size}, got {len(hdr)} bytes" ) - ml = RepoObj.meta_len_hdr.unpack(meta_len)[0] - meta = fd.read(ml) - length -= ml - if len(meta) != ml: + meta_size = RepoObj.obj_header.unpack(hdr)[0] + meta = fd.read(meta_size) + length -= meta_size + if len(meta) != meta_size: raise IntegrityError( f"Segment entry meta short read [segment {segment}, offset {offset}]: " - f"expected {ml}, got {len(meta)} bytes" + f"expected {meta_size}, got {len(meta)} bytes" ) - data = meta_len + meta # shortened chunk - enough so the client can decrypt the metadata - # we do not have a checksum for this data, but the client's AEAD crypto will check it. - # in any case, we see over the remainder of the chunk + data = hdr + meta # shortened chunk - enough so the client can decrypt the metadata + # in any case, we seek over the remainder of the chunk oldpos = fd.tell() seeked = fd.seek(length, os.SEEK_CUR) - oldpos if seeked != length: diff --git a/src/borg/repository3.py b/src/borg/repository3.py index 3edee5c4b..ab6fb986c 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -3,6 +3,7 @@ from borgstore.store import Store from borgstore.store import ObjectNotFound as StoreObjectNotFound +from .checksums import xxh64 from .constants import * # NOQA from .helpers import Error, ErrorWithTraceback, IntegrityError from .helpers import Location @@ -184,16 +185,46 @@ def commit(self, compact=True, threshold=0.1): pass def check(self, repair=False, max_duration=0): - """Check repository consistency + """Check repository consistency""" + def log_error(msg): + nonlocal obj_corrupted + obj_corrupted = True + logger.error(f"Repo object {info.name} is corrupted: {msg}") - This method verifies all segment checksums and makes sure - the index is consistent with the data stored in the segments. - """ + # TODO: implement repair, progress indicator, partial checks, ... mode = "full" logger.info("Starting repository check") - # XXX TODO - logger.info("Finished %s repository check, no problems found.", mode) - return True + objs_checked = objs_errors = 0 + infos = self.store.list("data") + for info in infos: + obj_corrupted = False + key = "data/%s" % info.name + obj = self.store.load(key) + hdr_size = RepoObj.obj_header.size + obj_size = len(obj) + if obj_size >= hdr_size: + hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size])) + meta = obj[hdr_size:hdr_size+hdr.meta_size] + if hdr.meta_size != len(meta): + log_error("metadata size incorrect.") + elif hdr.meta_hash != xxh64(meta): + log_error("metadata does not match checksum.") + data = obj[hdr_size+hdr.meta_size:hdr_size+hdr.meta_size+hdr.data_size] + if hdr.data_size != len(data): + log_error("data size incorrect.") + elif hdr.data_hash != xxh64(data): + log_error("data does not match checksum.") + else: + log_error("too small.") + objs_checked += 1 + if obj_corrupted: + objs_errors += 1 + logger.info(f"Checked {objs_checked} repository objects, {objs_errors} errors.") + if objs_errors == 0: + logger.info("Finished %s repository check, no problems found.", mode) + else: + logger.error("Finished %s repository check, errors found.", mode) + return objs_errors == 0 or repair def scan_low_level(self, segment=None, offset=None): raise NotImplementedError @@ -244,25 +275,25 @@ def get(self, id, read_data=True): else: # RepoObj layout supports separately encrypted metadata and data. # We return enough bytes so the client can decrypt the metadata. - meta_len_size = RepoObj.meta_len_hdr.size - extra_len = 1024 - meta_len_size # load a bit more, 1024b, reduces round trips - obj = self.store.load(key, size=meta_len_size + extra_len) - meta_len = obj[0:meta_len_size] - if len(meta_len) != meta_len_size: + hdr_size = RepoObj.obj_header.size + extra_size = 1024 - hdr_size # load a bit more, 1024b, reduces round trips + obj = self.store.load(key, size=hdr_size + extra_size) + hdr = obj[0:hdr_size] + if len(hdr) != hdr_size: raise IntegrityError( - f"Object too small [id {id_hex}]: expected {meta_len_size}, got {len(meta_len)} bytes" + f"Object too small [id {id_hex}]: expected {hdr_size}, got {len(hdr)} bytes" ) - ml = RepoObj.meta_len_hdr.unpack(meta_len)[0] - if ml > extra_len: + meta_size = RepoObj.obj_header.unpack(hdr)[0] + if meta_size > extra_size: # we did not get enough, need to load more, but not all. # this should be rare, as chunk metadata is rather small usually. - obj = self.store.load(key, size=meta_len_size + ml) - meta = obj[meta_len_size:meta_len_size + ml] - if len(meta) != ml: + obj = self.store.load(key, size=hdr_size + meta_size) + meta = obj[hdr_size:hdr_size + meta_size] + if len(meta) != meta_size: raise IntegrityError( - f"Object too small [id {id_hex}]: expected {ml}, got {len(meta)} bytes" + f"Object too small [id {id_hex}]: expected {meta_size}, got {len(meta)} bytes" ) - return meta_len + meta + return hdr + meta except StoreObjectNotFound: raise self.ObjectNotFound(id, self.path) from None diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index 2727a5781..c393c238b 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -9,6 +9,7 @@ from ...helpers import bin_to_hex, msgpack from ...manifest import Manifest from ...repository3 import Repository3 +from ..repository3 import fchunk from . import cmd, src_file, create_src_archive, open_archive, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA @@ -207,7 +208,7 @@ def test_corrupted_manifest(archivers, request): archive, repository = open_archive(archiver.repository_path, "archive1") with repository: manifest = repository.get(Manifest.MANIFEST_ID) - corrupted_manifest = manifest + b"corrupted!" + corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] repository.put(Manifest.MANIFEST_ID, corrupted_manifest) repository.commit(compact=False) cmd(archiver, "check", exit_code=1) @@ -257,7 +258,7 @@ def test_manifest_rebuild_corrupted_chunk(archivers, request): archive, repository = open_archive(archiver.repository_path, "archive1") with repository: manifest = repository.get(Manifest.MANIFEST_ID) - corrupted_manifest = manifest + b"corrupted!" + corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] repository.put(Manifest.MANIFEST_ID, corrupted_manifest) chunk = repository.get(archive.id) corrupted_chunk = chunk + b"corrupted!" @@ -276,7 +277,7 @@ def test_manifest_rebuild_duplicate_archive(archivers, request): repo_objs = archive.repo_objs with repository: manifest = repository.get(Manifest.MANIFEST_ID) - corrupted_manifest = manifest + b"corrupted!" + corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] repository.put(Manifest.MANIFEST_ID, corrupted_manifest) archive_dict = { "command_line": "", @@ -307,7 +308,7 @@ def test_spoofed_archive(archivers, request): with repository: # attacker would corrupt or delete the manifest to trigger a rebuild of it: manifest = repository.get(Manifest.MANIFEST_ID) - corrupted_manifest = manifest + b"corrupted!" + corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] repository.put(Manifest.MANIFEST_ID, corrupted_manifest) archive_dict = { "command_line": "", @@ -347,7 +348,8 @@ def test_extra_chunks(archivers, request): check_cmd_setup(archiver) cmd(archiver, "check", exit_code=0) with Repository3(archiver.repository_location, exclusive=True) as repository: - repository.put(b"01234567890123456789012345678901", b"xxxx") + chunk = fchunk(b"xxxx") + repository.put(b"01234567890123456789012345678901", chunk) repository.commit(compact=False) output = cmd(archiver, "check", "-v", exit_code=0) # orphans are not considered warnings anymore assert "1 orphaned (unused) objects found." in output @@ -374,7 +376,7 @@ def test_verify_data(archivers, request, init_args): repository.put(chunk.id, data) break repository.commit(compact=False) - cmd(archiver, "check", exit_code=0) + cmd(archiver, "check", exit_code=1) output = cmd(archiver, "check", "--verify-data", exit_code=1) assert bin_to_hex(chunk.id) + ", integrity error" in output diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index ca60b53b8..985fdb1b4 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -6,6 +6,7 @@ import pytest +from ..checksums import xxh64 from ..hashindex import NSIndex from ..helpers import Location from ..helpers import IntegrityError @@ -73,19 +74,19 @@ def get_path(repository): def fchunk(data, meta=b""): # create a raw chunk that has valid RepoObj layout, but does not use encryption or compression. - meta_len = RepoObj.meta_len_hdr.pack(len(meta)) + hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data)) assert isinstance(data, bytes) - chunk = meta_len + meta + data + chunk = hdr + meta + data return chunk def pchunk(chunk): # parse data and meta from a raw chunk made by fchunk - meta_len_size = RepoObj.meta_len_hdr.size - meta_len = chunk[:meta_len_size] - meta_len = RepoObj.meta_len_hdr.unpack(meta_len)[0] - meta = chunk[meta_len_size : meta_len_size + meta_len] - data = chunk[meta_len_size + meta_len :] + hdr_size = RepoObj.obj_header.size + hdr = chunk[:hdr_size] + meta_size, data_size = RepoObj.obj_header.unpack(hdr)[0:2] + meta = chunk[hdr_size : hdr_size + meta_size] + data = chunk[hdr_size + meta_size : hdr_size + meta_size + data_size] return data, meta @@ -148,9 +149,9 @@ def test_multiple_transactions(repo_fixtures, request): def test_read_data(repo_fixtures, request): with get_repository_from_fixture(repo_fixtures, request) as repository: meta, data = b"meta", b"data" - meta_len = RepoObj.meta_len_hdr.pack(len(meta)) - chunk_complete = meta_len + meta + data - chunk_short = meta_len + meta + hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data)) + chunk_complete = hdr + meta + data + chunk_short = hdr + meta repository.put(H(0), chunk_complete) repository.commit(compact=False) assert repository.get(H(0)) == chunk_complete @@ -273,7 +274,7 @@ def test_scan_modify(repo_fixtures, request): def test_max_data_size(repo_fixtures, request): with get_repository_from_fixture(repo_fixtures, request) as repository: - max_data = b"x" * (MAX_DATA_SIZE - RepoObj.meta_len_hdr.size) + max_data = b"x" * (MAX_DATA_SIZE - RepoObj.obj_header.size) repository.put(H(0), fchunk(max_data)) assert pdchunk(repository.get(H(0))) == max_data with pytest.raises(IntegrityError): @@ -706,7 +707,7 @@ def test_exceed_quota(repository): repository.commit(compact=False) assert repository.storage_quota_use == len(ch1) + len(ch2) + (41 + 8) * 2 # check ch2!? with reopen(repository) as repository: - repository.storage_quota = 150 + repository.storage_quota = 161 # open new transaction; hints and thus quota data is not loaded unless needed. repository.put(H(1), ch1) # we have 2 puts for H(1) here and not yet compacted. diff --git a/src/borg/testsuite/repository3.py b/src/borg/testsuite/repository3.py index 533d18723..859a7e443 100644 --- a/src/borg/testsuite/repository3.py +++ b/src/borg/testsuite/repository3.py @@ -5,6 +5,7 @@ import pytest +from ..checksums import xxh64 from ..helpers import Location from ..helpers import IntegrityError from ..platformflags import is_win32 @@ -57,19 +58,19 @@ def reopen(repository, exclusive: Optional[bool] = True, create=False): def fchunk(data, meta=b""): # format chunk: create a raw chunk that has valid RepoObj layout, but does not use encryption or compression. - meta_len = RepoObj.meta_len_hdr.pack(len(meta)) + hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data)) assert isinstance(data, bytes) - chunk = meta_len + meta + data + chunk = hdr + meta + data return chunk def pchunk(chunk): # parse chunk: parse data and meta from a raw chunk made by fchunk - meta_len_size = RepoObj.meta_len_hdr.size - meta_len = chunk[:meta_len_size] - meta_len = RepoObj.meta_len_hdr.unpack(meta_len)[0] - meta = chunk[meta_len_size : meta_len_size + meta_len] - data = chunk[meta_len_size + meta_len :] + hdr_size = RepoObj.obj_header.size + hdr = chunk[:hdr_size] + meta_size, data_size = RepoObj.obj_header.unpack(hdr)[0:2] + meta = chunk[hdr_size : hdr_size + meta_size] + data = chunk[hdr_size + meta_size : hdr_size + meta_size + data_size] return data, meta @@ -99,9 +100,9 @@ def test_basic_operations(repo_fixtures, request): def test_read_data(repo_fixtures, request): with get_repository_from_fixture(repo_fixtures, request) as repository: meta, data = b"meta", b"data" - meta_len = RepoObj.meta_len_hdr.pack(len(meta)) - chunk_complete = meta_len + meta + data - chunk_short = meta_len + meta + hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data)) + chunk_complete = hdr + meta + data + chunk_short = hdr + meta repository.put(H(0), chunk_complete) assert repository.get(H(0)) == chunk_complete assert repository.get(H(0), read_data=True) == chunk_complete @@ -152,7 +153,7 @@ def test_scan(repo_fixtures, request): def test_max_data_size(repo_fixtures, request): with get_repository_from_fixture(repo_fixtures, request) as repository: - max_data = b"x" * (MAX_DATA_SIZE - RepoObj.meta_len_hdr.size) + max_data = b"x" * (MAX_DATA_SIZE - RepoObj.obj_header.size) repository.put(H(0), fchunk(max_data)) assert pdchunk(repository.get(H(0))) == max_data with pytest.raises(IntegrityError): From c740fd718b63682e1dd0dd67b7bb65a85b787236 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 9 Aug 2024 01:41:13 +0200 Subject: [PATCH 03/79] transfer: fix upgrades from borg 1.x by adding a --from-borg1 option borg transfer is primarily a general purpose archive transfer function from borg2 to related borg2 repos. but for upgrades from borg 1.x, we also need to support: - rcreate with a borg 1.x "other repo" - transfer with a borg 1.x "other repo" --- src/borg/archiver/_common.py | 10 ++-- src/borg/archiver/rcreate_cmd.py | 11 ++++ src/borg/archiver/transfer_cmd.py | 57 +++++++++++++-------- src/borg/testsuite/archiver/transfer_cmd.py | 4 +- 4 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index f205cf8ed..441ec3207 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -190,6 +190,8 @@ def wrapper(self, args, **kwargs): if not location.valid: # nothing to do return method(self, args, **kwargs) + v1_or_v2 = getattr(args, "v1_or_v2", False) + repository = get_repository( location, create=False, @@ -200,12 +202,14 @@ def wrapper(self, args, **kwargs): make_parent_dirs=False, storage_quota=None, args=args, - v1_or_v2=True + v1_or_v2=v1_or_v2, ) with repository: - if repository.version not in (1, 2): - raise Error("This borg version only accepts version 1 or 2 repos for --other-repo.") + acceptable_versions = (1, 2) if v1_or_v2 else (3,) + if repository.version not in acceptable_versions: + raise Error( + f"This borg version only accepts version {' or '.join(acceptable_versions)} repos for --other-repo.") kwargs["other_repository"] = repository if manifest or cache: manifest_ = Manifest.load( diff --git a/src/borg/archiver/rcreate_cmd.py b/src/borg/archiver/rcreate_cmd.py index b4a66645d..d9353b157 100644 --- a/src/borg/archiver/rcreate_cmd.py +++ b/src/borg/archiver/rcreate_cmd.py @@ -172,6 +172,14 @@ def build_parser_rcreate(self, subparsers, common_parser, mid_common_parser): keys to manage. Creating related repositories is useful e.g. if you want to use ``borg transfer`` later. + + Creating a related repository for data migration from borg 1.2 or 1.4 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + You can use ``borg rcreate --other-repo ORIG_REPO --from-borg1 ...`` to create a related + repository that uses the same secret key material as the given other/original repository. + + Then use ``borg transfer --other-repo ORIG_REPO --from-borg1 ...`` to transfer the archives. """ ) subparser = subparsers.add_parser( @@ -193,6 +201,9 @@ def build_parser_rcreate(self, subparsers, common_parser, mid_common_parser): action=Highlander, help="reuse the key material from the other repository", ) + subparser.add_argument( + "--from-borg1", dest="v1_or_v2", action="store_true", help="other repository is borg 1.x" + ) subparser.add_argument( "-e", "--encryption", diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index 1ba8ed3c8..773347681 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -61,10 +61,15 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non from .. import upgrade as upgrade_mod + v1_or_v2 = getattr(args, "v1_or_v2", False) + upgrader = args.upgrader + if upgrader == "NoOp" and v1_or_v2: + upgrader = "From12To20" + try: - UpgraderCls = getattr(upgrade_mod, f"Upgrader{args.upgrader}") + UpgraderCls = getattr(upgrade_mod, f"Upgrader{upgrader}") except AttributeError: - raise Error(f"No such upgrader: {args.upgrader}") + raise Error(f"No such upgrader: {upgrader}") if UpgraderCls is not upgrade_mod.UpgraderFrom12To20 and other_manifest.repository.version == 1: raise Error("To transfer from a borg 1.x repo, you need to use: --upgrader=From12To20") @@ -188,32 +193,41 @@ def build_parser_transfer(self, subparsers, common_parser, mid_common_parser): If you want to globally change compression while transferring archives to the DST_REPO, give ``--compress=WANTED_COMPRESSION --recompress=always``. - Suggested use for general purpose archive transfer (not repo upgrades):: - - # create a related DST_REPO (reusing key material from SRC_REPO), so that - # chunking and chunk id generation will work in the same way as before. - borg --repo=DST_REPO rcreate --other-repo=SRC_REPO --encryption=DST_ENC - - # transfer archives from SRC_REPO to DST_REPO - borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check what it would do - borg --repo=DST_REPO transfer --other-repo=SRC_REPO # do it! - borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check! anything left? - The default is to transfer all archives, including checkpoint archives. You could use the misc. archive filter options to limit which archives it will transfer, e.g. using the ``-a`` option. This is recommended for big repositories with multiple data sets to keep the runtime per invocation lower. - For repository upgrades (e.g. from a borg 1.2 repo to a related borg 2.0 repo), usage is - quite similar to the above:: + General purpose archive transfer + ++++++++++++++++++++++++++++++++ - # fast: compress metadata with zstd,3, but keep data chunks compressed as they are: - borg --repo=DST_REPO transfer --other-repo=SRC_REPO --upgrader=From12To20 \\ - --compress=zstd,3 --recompress=never + Transfer borg2 archives into a related other borg2 repository:: - # compress metadata and recompress data with zstd,3 - borg --repo=DST_REPO transfer --other-repo=SRC_REPO --upgrader=From12To20 \\ + # create a related DST_REPO (reusing key material from SRC_REPO), so that + # chunking and chunk id generation will work in the same way as before. + borg --repo=DST_REPO rcreate --encryption=DST_ENC --other-repo=SRC_REPO + + # transfer archives from SRC_REPO to DST_REPO + borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check what it would do + borg --repo=DST_REPO transfer --other-repo=SRC_REPO # do it! + borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check! anything left? + + + Data migration / upgrade from borg 1.x + ++++++++++++++++++++++++++++++++++++++ + + To migrate your borg 1.x archives into a related, new borg2 repository, usage is quite similar + to the above, but you need the ``--from-borg1`` option:: + + borg --repo=DST_REPO rcreate --encryption=DST_ENC --other-repo=SRC_REPO --from-borg1 + + # to continue using lz4 compression as you did in SRC_REPO: + borg --repo=DST_REPO transfer --other-repo=SRC_REPO --from-borg1 \\ + --compress=lz4 --recompress=never + + # alternatively, to recompress everything to zstd,3: + borg --repo=DST_REPO transfer --other-repo=SRC_REPO --from-borg1 \\ --compress=zstd,3 --recompress=always @@ -241,6 +255,9 @@ def build_parser_transfer(self, subparsers, common_parser, mid_common_parser): action=Highlander, help="transfer archives from the other repository", ) + subparser.add_argument( + "--from-borg1", dest="v1_or_v2", action="store_true", help="other repository is borg 1.x" + ) subparser.add_argument( "--upgrader", metavar="UPGRADER", diff --git a/src/borg/testsuite/archiver/transfer_cmd.py b/src/borg/testsuite/archiver/transfer_cmd.py index 52d9025c0..75fb551e8 100644 --- a/src/borg/testsuite/archiver/transfer_cmd.py +++ b/src/borg/testsuite/archiver/transfer_cmd.py @@ -75,8 +75,8 @@ def convert_tz(local_naive, tzoffset, tzinfo): assert os.environ.get("BORG_PASSPHRASE") == "waytooeasyonlyfortests" os.environ["BORG_TESTONLY_WEAKEN_KDF"] = "0" # must use the strong kdf here or it can't decrypt the key - cmd(archiver, "rcreate", RK_ENCRYPTION, other_repo1) - cmd(archiver, "transfer", other_repo1, "--upgrader=From12To20") + cmd(archiver, "rcreate", RK_ENCRYPTION, other_repo1, "--from-borg1") + cmd(archiver, "transfer", other_repo1, "--from-borg1") cmd(archiver, "check") # check list of archives / manifest From 72d0caeb6b87d5718b95aa28875303d982712310 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 10 Aug 2024 02:33:30 +0200 Subject: [PATCH 04/79] locking3: store-based repo locking Features: - exclusive and non-exclusive locks - acquire timeout - lock auto-expiry (after 30mins of inactivity), lock refresh - use tz-aware datetimes (in utc timezone) in locks Also: - document lock acquisition rules in the src - increased default BORG_LOCK_WAIT to 10s - better document with-lock test Stale locks are ignored and automatically deleted. Default: stale == 30 Minutes old. lock.refresh() can be called frequently to avoid that an acquired lock becomes stale. It does not do much if the last real refresh was recently. After stale/2 time it checks and refreshes the locks in the store. Update the repository3 code to call refresh frequently: - get/put/list/scan - inside check loop --- src/borg/archiver/_common.py | 2 +- src/borg/archiver/lock_cmds.py | 23 --- src/borg/locking3.py | 222 +++++++++++++++++++++++ src/borg/remote3.py | 2 +- src/borg/repository3.py | 30 ++- src/borg/testsuite/archiver/lock_cmds.py | 35 +++- src/borg/testsuite/locking3.py | 90 +++++++++ 7 files changed, 369 insertions(+), 35 deletions(-) create mode 100644 src/borg/locking3.py create mode 100644 src/borg/testsuite/locking3.py diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 441ec3207..5636af1a1 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -510,7 +510,7 @@ def define_common_options(add_common_option): metavar="SECONDS", dest="lock_wait", type=int, - default=int(os.environ.get("BORG_LOCK_WAIT", 1)), + default=int(os.environ.get("BORG_LOCK_WAIT", 10)), action=Highlander, help="wait at most SECONDS for acquiring a repository/cache lock (default: %(default)d).", ) diff --git a/src/borg/archiver/lock_cmds.py b/src/borg/archiver/lock_cmds.py index d0cf026ee..a8839b631 100644 --- a/src/borg/archiver/lock_cmds.py +++ b/src/borg/archiver/lock_cmds.py @@ -5,7 +5,6 @@ from ..cache import Cache from ..constants import * # NOQA from ..helpers import prepare_subprocess_env, set_ec, CommandError -from ..manifest import Manifest from ..logger import create_logger @@ -16,20 +15,6 @@ class LocksMixIn: @with_repository(manifest=False, exclusive=True) def do_with_lock(self, args, repository): """run a user specified command with the repository lock held""" - # for a new server, this will immediately take an exclusive lock. - # to support old servers, that do not have "exclusive" arg in open() - # RPC API, we also do it the old way: - # re-write manifest to start a repository transaction - this causes a - # lock upgrade to exclusive for remote (and also for local) repositories. - # by using manifest=False in the decorator, we avoid having to require - # the encryption key (and can operate just with encrypted data). - data = repository.get(Manifest.MANIFEST_ID) - repository.put(Manifest.MANIFEST_ID, data) - # usually, a 0 byte (open for writing) segment file would be visible in the filesystem here. - # we write and close this file, to rather have a valid segment file on disk, before invoking the subprocess. - # we can only do this for local repositories (with .io), though: - if hasattr(repository, "io"): - repository.io.close_segment() env = prepare_subprocess_env(system=True) try: # we exit with the return code we get from the subprocess @@ -37,14 +22,6 @@ def do_with_lock(self, args, repository): set_ec(rc) except (FileNotFoundError, OSError, ValueError) as e: raise CommandError(f"Error while trying to run '{args.command}': {e}") - finally: - # we need to commit the "no change" operation we did to the manifest - # because it created a new segment file in the repository. if we would - # roll back, the same file would be later used otherwise (for other content). - # that would be bad if somebody uses rsync with ignore-existing (or - # any other mechanism relying on existing segment data not changing). - # see issue #1867. - repository.commit(compact=False) @with_repository(lock=False, manifest=False) def do_break_lock(self, args, repository): diff --git a/src/borg/locking3.py b/src/borg/locking3.py new file mode 100644 index 000000000..92922aec3 --- /dev/null +++ b/src/borg/locking3.py @@ -0,0 +1,222 @@ +import datetime +import json +import random +import time + +from borgstore.store import ObjectNotFound + +from . import platform +from .checksums import xxh64 +from .helpers import Error, ErrorWithTraceback, bin_to_hex +from .logger import create_logger + +logger = create_logger(__name__) + + +class LockError(Error): + """Failed to acquire the lock {}.""" + + exit_mcode = 70 + + +class LockErrorT(ErrorWithTraceback): + """Failed to acquire the lock {}.""" + + exit_mcode = 71 + + +class LockFailed(LockErrorT): + """Failed to create/acquire the lock {} ({}).""" + + exit_mcode = 72 + + +class LockTimeout(LockError): + """Failed to create/acquire the lock {} (timeout).""" + + exit_mcode = 73 + + +class NotLocked(LockErrorT): + """Failed to release the lock {} (was not locked).""" + + exit_mcode = 74 + + +class NotMyLock(LockErrorT): + """Failed to release the lock {} (was/is locked, but not by me).""" + + exit_mcode = 75 + + +class Lock: + """ + A Lock for a resource that can be accessed in a shared or exclusive way. + Typically, write access to a resource needs an exclusive lock (1 writer, + no one is allowed reading) and read access to a resource needs a shared + lock (multiple readers are allowed). + + If possible, try to use the contextmanager here like:: + + with Lock(...) as lock: + ... + + This makes sure the lock is released again if the block is left, no + matter how (e.g. if an exception occurred). + """ + + def __init__(self, store, exclusive=False, sleep=None, timeout=1.0, stale=30*60, id=None): + self.store = store + self.is_exclusive = exclusive + self.sleep = sleep + self.timeout = timeout + self.race_recheck_delay = 0.01 # local: 0.01, network/slow remote: >= 1.0 + self.other_locks_go_away_delay = 0.1 # local: 0.1, network/slow remote: >= 1.0 + self.retry_delay_min = 1.0 + self.retry_delay_max = 5.0 + self.stale_td = datetime.timedelta(seconds=stale) # ignore/delete it if older + self.refresh_td = datetime.timedelta(seconds=stale//2) # don't refresh it if younger + self.last_refresh_dt = None + self.id = id or platform.get_process_id() + assert len(self.id) == 3 + + def __enter__(self): + return self.acquire() + + def __exit__(self, *exc): + self.release() + + def __repr__(self): + return f"<{self.__class__.__name__}: {self.id!r}>" + + def _create_lock(self, *, exclusive=None): + assert exclusive is not None + now = datetime.datetime.now(datetime.timezone.utc) + timestamp = now.isoformat(timespec="milliseconds") + lock = dict(exclusive=exclusive, hostid=self.id[0], processid=self.id[1], threadid=self.id[2], time=timestamp) + value = json.dumps(lock).encode("utf-8") + key = bin_to_hex(xxh64(value)) + self.store.store(f"locks/{key}", value) + self.last_refresh_dt = now + return key + + def _delete_lock(self, key, *, ignore_not_found=False): + try: + self.store.delete(f"locks/{key}") + except ObjectNotFound: + if not ignore_not_found: + raise + + def _get_locks(self): + now = datetime.datetime.now(datetime.timezone.utc) + locks = {} + try: + infos = list(self.store.list("locks")) + except ObjectNotFound: + return {} + for info in infos: + key = info.name + content = self.store.load(f"locks/{key}") + lock = json.loads(content.decode("utf-8")) + dt = datetime.datetime.fromisoformat(lock["time"]) + stale = dt < now - self.stale_td + if stale: + # ignore it and delete it (even if it is not from us) + self._delete_lock(key, ignore_not_found=True) + else: + lock["key"] = key + lock["dt"] = dt + locks[key] = lock + return locks + + def _find_locks(self, *, only_exclusive=False, only_mine=False): + locks = self._get_locks() + found_locks = [] + for key in locks: + lock = locks[key] + if (not only_exclusive or lock["exclusive"]) and (not only_mine or (lock["hostid"], lock["processid"], lock["threadid"]) == self.id): + found_locks.append(lock) + return found_locks + + def acquire(self): + # goal + # for exclusive lock: there must be only 1 exclusive lock and no other (exclusive or non-exclusive) locks. + # for non-exclusive lock: there can be multiple n-e locks, but there must not exist an exclusive lock. + started = time.monotonic() + while time.monotonic() - started < self.timeout: + exclusive_locks = self._find_locks(only_exclusive=True) + if len(exclusive_locks) == 0: + # looks like there are no exclusive locks, create our lock. + key = self._create_lock(exclusive=self.is_exclusive) + # obviously we have a race condition here: other client(s) might have created exclusive + # lock(s) at the same time in parallel. thus we have to check again. + time.sleep(self.race_recheck_delay) # give other clients time to notice our exclusive lock, stop creating theirs + exclusive_locks = self._find_locks(only_exclusive=True) + if self.is_exclusive: + if len(exclusive_locks) == 1 and exclusive_locks[0]["key"] == key: + # success, we are the only exclusive lock! wait until the non-exclusive locks go away: + while time.monotonic() - started < self.timeout: + locks = self._find_locks(only_exclusive=False) + if len(locks) == 1 and locks[0]["key"] == key: + # success, we are alone! + return self + time.sleep(self.other_locks_go_away_delay) + break # timeout + else: + # take back our lock as some other client(s) also created exclusive lock(s). + self._delete_lock(key, ignore_not_found=True) + else: # not is_exclusive + if len(exclusive_locks) == 0: + # success, noone else created an exclusive lock meanwhile! + # We don't care for other non-exclusive locks. + return self + else: + # take back our lock as some other client(s) also created exclusive lock(s). + self._delete_lock(key, ignore_not_found=True) + # wait a random bit before retrying + time.sleep(self.retry_delay_min + (self.retry_delay_max - self.retry_delay_min) * random.random()) + # timeout + raise LockFailed(str(self.store), "timeout") + + def release(self): + locks = self._find_locks(only_mine=True) + if not locks: + raise NotLocked(str(self.store)) + assert len(locks) == 1 + self._delete_lock(locks[0]["key"], ignore_not_found=True) + self.last_refresh_dt = None + + def got_exclusive_lock(self): + locks = self._find_locks(only_mine=True, only_exclusive=True) + return len(locks) == 1 + + def break_lock(self): + """break ALL locks (not just ours)""" + locks = self._get_locks() + for key in locks: + self._delete_lock(key, ignore_not_found=True) + self.last_refresh_dt = None + + def migrate_lock(self, old_id, new_id): + """migrate the lock ownership from old_id to new_id""" + assert self.id == old_id + assert len(new_id) == 3 + old_locks = self._find_locks(only_mine=True) + assert len(old_locks) == 1 + self.id = new_id + self._create_lock(exclusive=old_locks[0]["exclusive"]) + self._delete_lock(old_locks[0]["key"]) + now = datetime.datetime.now(datetime.timezone.utc) + self.last_refresh_dt = now + + def refresh(self): + """refresh the lock - call this frequently, but not later than every seconds""" + now = datetime.datetime.now(datetime.timezone.utc) + if self.last_refresh_dt is not None and now > self.last_refresh_dt + self.refresh_td: + old_locks = self._find_locks(only_mine=True) + assert len(old_locks) == 1 + old_lock = old_locks[0] + if old_lock["dt"] < now - self.refresh_td: + self._create_lock(exclusive=old_lock["exclusive"]) + self._delete_lock(old_lock["key"]) + self.last_refresh_dt = now diff --git a/src/borg/remote3.py b/src/borg/remote3.py index 85687035d..3b27fd355 100644 --- a/src/borg/remote3.py +++ b/src/borg/remote3.py @@ -577,7 +577,7 @@ def __init__( location, create=False, exclusive=False, - lock_wait=None, + lock_wait=1.0, lock=True, append_only=False, make_parent_dirs=False, diff --git a/src/borg/repository3.py b/src/borg/repository3.py index ab6fb986c..d21422a63 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -8,6 +8,7 @@ from .helpers import Error, ErrorWithTraceback, IntegrityError from .helpers import Location from .helpers import bin_to_hex, hex_to_bin +from .locking3 import Lock from .logger import create_logger from .repoobj import RepoObj @@ -82,7 +83,7 @@ def __init__( path, create=False, exclusive=False, - lock_wait=None, + lock_wait=1.0, lock=True, append_only=False, storage_quota=None, @@ -107,6 +108,10 @@ def __init__( self.append_only = append_only # XXX not implemented / not implementable self.storage_quota = storage_quota # XXX not implemented self.storage_quota_use = 0 # XXX not implemented + self.lock = None + self.do_lock = lock + self.lock_wait = lock_wait + self.exclusive = exclusive def __repr__(self): return f"<{self.__class__.__name__} {self.path}>" @@ -116,7 +121,7 @@ def __enter__(self): self.do_create = False self.create() self.created = True - self.open() + self.open(exclusive=bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock) return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -143,6 +148,10 @@ def _set_id(self, id): self.id = id self.store.store("config/id", bin_to_hex(id).encode()) + def _lock_refresh(self): + if self.lock is not None: + self.lock.refresh() + def save_key(self, keydata): # note: saving an empty key means that there is no repokey anymore self.store.store("keys/repokey", keydata) @@ -157,8 +166,13 @@ def destroy(self): self.close() self.store.destroy() - def open(self): + def open(self, *, exclusive, lock_wait=None, lock=True): + assert lock_wait is not None self.store.open() + if lock: + self.lock = Lock(self.store, exclusive, timeout=lock_wait).acquire() + else: + self.lock = None readme = self.store.load("config/readme").decode() if readme != REPOSITORY_README: raise self.InvalidRepository(self.path) @@ -173,6 +187,9 @@ def open(self): def close(self): if self.opened: + if self.lock: + self.lock.release() + self.lock = None self.store.close() self.opened = False @@ -197,6 +214,7 @@ def log_error(msg): objs_checked = objs_errors = 0 infos = self.store.list("data") for info in infos: + self._lock_refresh() obj_corrupted = False key = "data/%s" % info.name obj = self.store.load(key) @@ -241,6 +259,7 @@ def list(self, limit=None, marker=None, mask=0, value=0): if mask and value are given, only return IDs where flags & mask == value (default: all IDs). """ + self._lock_refresh() infos = self.store.list("data") # XXX we can only get the full list from the store ids = [hex_to_bin(info.name) for info in infos] if marker is not None: @@ -266,6 +285,7 @@ def scan(self, limit=None, state=None): return ids, state def get(self, id, read_data=True): + self._lock_refresh() id_hex = bin_to_hex(id) key = "data/" + id_hex try: @@ -308,6 +328,7 @@ def put(self, id, data, wait=True): Note: when doing calls with wait=False this gets async and caller must deal with async results / exceptions later. """ + self._lock_refresh() data_size = len(data) if data_size > MAX_DATA_SIZE: raise IntegrityError(f"More than allowed put data [{data_size} > {MAX_DATA_SIZE}]") @@ -321,6 +342,7 @@ def delete(self, id, wait=True): Note: when doing calls with wait=False this gets async and caller must deal with async results / exceptions later. """ + self._lock_refresh() key = "data/" + bin_to_hex(id) try: self.store.delete(key) @@ -342,4 +364,4 @@ def preload(self, ids): """Preload objects (only applies to remote repositories)""" def break_lock(self): - pass + Lock(self.store).break_lock() diff --git a/src/borg/testsuite/archiver/lock_cmds.py b/src/borg/testsuite/archiver/lock_cmds.py index 20cfbd7da..8fce39bd9 100644 --- a/src/borg/testsuite/archiver/lock_cmds.py +++ b/src/borg/testsuite/archiver/lock_cmds.py @@ -1,4 +1,6 @@ import os +import subprocess +import time from ...constants import * # NOQA from . import cmd, generate_archiver_tests, RK_ENCRYPTION @@ -13,12 +15,33 @@ def test_break_lock(archivers, request): cmd(archiver, "break-lock") -def test_with_lock(archivers, request): - archiver = request.getfixturevalue(archivers) - cmd(archiver, "rcreate", RK_ENCRYPTION) - lock_path = os.path.join(archiver.repository_path, "lock.exclusive") - command = "python3", "-c", 'import os, sys; sys.exit(42 if os.path.exists("%s") else 23)' % lock_path - cmd(archiver, "with-lock", *command, fork=True, exit_code=42) +def test_with_lock(tmp_path): + repo_path = tmp_path / "repo" + env = os.environ.copy() + env["BORG_REPO"] = "file://" + str(repo_path) + command0 = "python3", "-m", "borg", "rcreate", "--encryption=none" + # timings must be adjusted so that command1 keeps running while command2 tries to get the lock, + # so that lock acquisition for command2 fails as the test expects it. + lock_wait, execution_time, startup_wait = 2, 4, 1 + assert lock_wait < execution_time - startup_wait + command1 = "python3", "-c", f'import time; print("first command - acquires the lock"); time.sleep({execution_time})' + command2 = "python3", "-c", 'print("second command - should never get executed")' + borgwl = "python3", "-m", "borg", "with-lock", f"--lock-wait={lock_wait}" + popen_options = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env) + subprocess.run(command0, env=env, check=True, text=True, capture_output=True) + assert repo_path.exists() + with subprocess.Popen([*borgwl, *command1], **popen_options) as p1: + time.sleep(startup_wait) # wait until p1 is running + # now try to get another lock on the same repository: + with subprocess.Popen([*borgwl, *command2], **popen_options) as p2: + out, err_out = p2.communicate() + assert "second command" not in out # command2 is "locked out" + assert "Failed to create/acquire the lock" in err_out + assert p2.returncode == 72 # LockTimeout: could not acquire the lock, p1 already has it + out, err_out = p1.communicate() + assert "first command" in out # command1 was executed and had the lock + assert not err_out + assert p1.returncode == 0 def test_with_lock_non_existent_command(archivers, request): diff --git a/src/borg/testsuite/locking3.py b/src/borg/testsuite/locking3.py new file mode 100644 index 000000000..f3976fa83 --- /dev/null +++ b/src/borg/testsuite/locking3.py @@ -0,0 +1,90 @@ +import time + +import pytest + +from borgstore.store import Store + +from ..locking3 import ( + Lock, + LockFailed, + NotLocked, +) + +ID1 = "foo", 1, 1 +ID2 = "bar", 2, 2 + + +@pytest.fixture() +def lockstore(tmpdir): + store = Store("file://" + str(tmpdir / "lockstore")) + store.create() + with store: + yield store + store.destroy() + + +class TestLock: + def test_cm(self, lockstore): + with Lock(lockstore, exclusive=True, id=ID1) as lock: + assert lock.got_exclusive_lock() + with Lock(lockstore, exclusive=False, id=ID1) as lock: + assert not lock.got_exclusive_lock() + + def test_got_exclusive_lock(self, lockstore): + lock = Lock(lockstore, exclusive=True, id=ID1) + assert not lock.got_exclusive_lock() + lock.acquire() + assert lock.got_exclusive_lock() + lock.release() + assert not lock.got_exclusive_lock() + + def test_exclusive_lock(self, lockstore): + # there must not be 2 exclusive locks + with Lock(lockstore, exclusive=True, id=ID1): + with pytest.raises(LockFailed): + Lock(lockstore, exclusive=True, id=ID2).acquire() + # acquiring an exclusive lock will time out if the non-exclusive does not go away + with Lock(lockstore, exclusive=False, id=ID1): + with pytest.raises(LockFailed): + Lock(lockstore, exclusive=True, id=ID2).acquire() + + def test_double_nonexclusive_lock_succeeds(self, lockstore): + with Lock(lockstore, exclusive=False, id=ID1): + with Lock(lockstore, exclusive=False, id=ID2): + pass + + def test_not_locked(self, lockstore): + lock = Lock(lockstore, exclusive=True, id=ID1) + with pytest.raises(NotLocked): + lock.release() + lock = Lock(lockstore, exclusive=False, id=ID1) + with pytest.raises(NotLocked): + lock.release() + + def test_break_lock(self, lockstore): + lock = Lock(lockstore, exclusive=True, id=ID1).acquire() + lock.break_lock() + with Lock(lockstore, exclusive=True, id=ID2): + pass + with Lock(lockstore, exclusive=True, id=ID1): + pass + + def test_lock_refresh_stale_removal(self, lockstore): + # stale after 2s, refreshable after 1s + lock = Lock(lockstore, exclusive=True, id=ID1, stale=2) + lock.acquire() + lock_keys_a00 = set(lock._get_locks()) + time.sleep(0.5) + lock.refresh() # shouldn't change locks, existing lock too young + lock_keys_a05 = set(lock._get_locks()) + time.sleep(0.6) + lock.refresh() # that should refresh the lock! + lock_keys_b00 = set(lock._get_locks()) + time.sleep(2.1) + lock_keys_b21 = set(lock._get_locks()) # now the lock should be stale & gone. + assert lock_keys_a00 == lock_keys_a05 # was too young, no refresh done + assert len(lock_keys_a00) == 1 + assert lock_keys_a00 != lock_keys_b00 # refresh done, new lock has different key + assert len(lock_keys_b00) == 1 + assert len(lock_keys_b21) == 0 # stale lock was ignored + assert len(list(lock.store.list("locks"))) == 0 # stale lock was removed from store From 8b9c052acc0c03f591a4213788d8f577d9dcb78c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Aug 2024 16:46:51 +0200 Subject: [PATCH 05/79] manifest: store archives separately one-by-one into archives/* repository: - api/rpc support for get/put manifest - api/rpc support to access the store --- src/borg/archive.py | 21 ++++-- src/borg/archiver/compact_cmd.py | 13 ++-- src/borg/archiver/debug_cmd.py | 3 +- src/borg/archiver/rcompress_cmd.py | 12 ++-- src/borg/cache.py | 6 +- src/borg/crypto/keymanager.py | 9 ++- src/borg/helpers/parseformat.py | 2 +- src/borg/manifest.py | 63 ++++++++++++++--- src/borg/remote.py | 10 +++ src/borg/remote3.py | 30 +++++++++ src/borg/repository.py | 11 ++- src/borg/repository3.py | 86 +++++++++++++++++------- src/borg/testsuite/archiver/check_cmd.py | 12 ++++ src/borg/testsuite/cache.py | 1 - 14 files changed, 220 insertions(+), 59 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index d45f4426e..3f8c7c914 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -51,6 +51,7 @@ from .item import Item, ArchiveItem, ItemDiff from .platform import acl_get, acl_set, set_flags, get_flags, swidth, hostname from .remote import cache_if_remote +from .remote3 import RemoteRepository3 from .repository3 import Repository3, LIST_SCAN_LIMIT from .repoobj import RepoObj @@ -1852,14 +1853,14 @@ def check( self.repair = repair self.repository = repository self.init_chunks() - if not self.chunks: + if not isinstance(repository, (Repository3, RemoteRepository3)) and not self.chunks: logger.error("Repository contains no apparent data at all, cannot continue check/repair.") return False self.key = self.make_key(repository) self.repo_objs = RepoObj(self.key) if verify_data: self.verify_data() - if Manifest.MANIFEST_ID not in self.chunks: + if not isinstance(repository, (Repository3, RemoteRepository3)) and Manifest.MANIFEST_ID not in self.chunks: logger.error("Repository manifest not found!") self.error_found = True self.manifest = self.rebuild_manifest() @@ -1869,7 +1870,8 @@ def check( except IntegrityErrorBase as exc: logger.error("Repository manifest is corrupted: %s", exc) self.error_found = True - del self.chunks[Manifest.MANIFEST_ID] + if not isinstance(repository, (Repository3, RemoteRepository3)): + del self.chunks[Manifest.MANIFEST_ID] self.manifest = self.rebuild_manifest() self.rebuild_refcounts( match=match, first=first, last=last, sort_by=sort_by, older=older, oldest=oldest, newer=newer, newest=newest @@ -1900,6 +1902,16 @@ def init_chunks(self): def make_key(self, repository): attempt = 0 + + # try the manifest first! + attempt += 1 + cdata = repository.get_manifest() + try: + return key_factory(repository, cdata) + except UnsupportedPayloadError: + # we get here, if the cdata we got has a corrupted key type byte + pass # ignore it, just continue trying + for chunkid, _ in self.chunks.iteritems(): attempt += 1 if attempt > 999: @@ -2070,7 +2082,8 @@ def rebuild_refcounts( Missing and/or incorrect data is repaired when detected """ # Exclude the manifest from chunks (manifest entry might be already deleted from self.chunks) - self.chunks.pop(Manifest.MANIFEST_ID, None) + if not isinstance(self.repository, (Repository3, RemoteRepository3)): + self.chunks.pop(Manifest.MANIFEST_ID, None) def mark_as_possibly_superseded(id_): if self.chunks.get(id_, ChunkIndexEntry(0, 0)).refcount == 0: diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index d5847741a..ea3093ec5 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -3,6 +3,8 @@ from ._common import with_repository, Highlander from ..constants import * # NOQA from ..manifest import Manifest +from ..repository3 import Repository3 +from ..remote3 import RemoteRepository3 from ..logger import create_logger @@ -13,11 +15,12 @@ class CompactMixIn: @with_repository(manifest=False, exclusive=True) def do_compact(self, args, repository): """compact segment files in the repository""" - # see the comment in do_with_lock about why we do it like this: - data = repository.get(Manifest.MANIFEST_ID) - repository.put(Manifest.MANIFEST_ID, data) - threshold = args.threshold / 100 - repository.commit(compact=True, threshold=threshold) + if not isinstance(repository, (Repository3, RemoteRepository3)): + # see the comment in do_with_lock about why we do it like this: + data = repository.get(Manifest.MANIFEST_ID) + repository.put(Manifest.MANIFEST_ID, data) + threshold = args.threshold / 100 + repository.commit(compact=True, threshold=threshold) def build_parser_compact(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index 89f121521..d3e03a364 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -100,7 +100,8 @@ def output(fd): def do_debug_dump_manifest(self, args, repository, manifest): """dump decoded repository manifest""" repo_objs = manifest.repo_objs - _, data = repo_objs.parse(manifest.MANIFEST_ID, repository.get(manifest.MANIFEST_ID), ro_type=ROBJ_MANIFEST) + cdata = repository.get_manifest() + _, data = repo_objs.parse(manifest.MANIFEST_ID, cdata, ro_type=ROBJ_MANIFEST) meta = prepare_dump_dict(msgpack.unpackb(data, object_hook=StableDict)) diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index bc096b3c3..c8558f7ee 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -5,7 +5,8 @@ from ..constants import * # NOQA from ..compress import CompressionSpec, ObfuscateSize, Auto, COMPRESSOR_TABLE from ..helpers import sig_int, ProgressIndicatorPercent, Error - +from ..repository3 import Repository3 +from ..remote3 import RemoteRepository3 from ..manifest import Manifest from ..logger import create_logger @@ -120,10 +121,11 @@ def checkpoint_func(): chunks_limit = min(1000, max(100, recompress_candidate_count // 1000)) uncommitted_chunks = 0 - # start a new transaction - data = repository.get(Manifest.MANIFEST_ID) - repository.put(Manifest.MANIFEST_ID, data) - uncommitted_chunks += 1 + if not isinstance(repository, (Repository3, RemoteRepository3)): + # start a new transaction + data = repository.get(Manifest.MANIFEST_ID) + repository.put(Manifest.MANIFEST_ID, data) + uncommitted_chunks += 1 pi = ProgressIndicatorPercent( total=len(recompress_ids), msg="Recompressing %3.1f%%", step=0.1, msgid="rcompress.process_chunks" diff --git a/src/borg/cache.py b/src/borg/cache.py index ee88793f3..31c4df0b3 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -32,7 +32,8 @@ from .manifest import Manifest from .platform import SaveFile from .remote import cache_if_remote -from .repository3 import LIST_SCAN_LIMIT +from .remote3 import RemoteRepository3 +from .repository3 import LIST_SCAN_LIMIT, Repository3 # note: cmtime might be either a ctime or a mtime timestamp, chunks is a list of ChunkListEntry FileCacheEntry = namedtuple("FileCacheEntry", "age inode size cmtime chunks") @@ -737,7 +738,8 @@ def _load_chunks_from_repo(self): num_chunks += 1 chunks[id_] = init_entry # LocalCache does not contain the manifest, either. - del chunks[self.manifest.MANIFEST_ID] + if not isinstance(self.repository, (Repository3, RemoteRepository3)): + del chunks[self.manifest.MANIFEST_ID] duration = perf_counter() - t0 or 0.01 logger.debug( "Cache: downloaded %d chunk IDs in %.2f s (%d requests), ~%s/s", diff --git a/src/borg/crypto/keymanager.py b/src/borg/crypto/keymanager.py index c2105ec5b..fe5050f55 100644 --- a/src/borg/crypto/keymanager.py +++ b/src/borg/crypto/keymanager.py @@ -3,9 +3,12 @@ import textwrap from hashlib import sha256 +from borgstore.store import ObjectNotFound as StoreObjectNotFound + from ..helpers import Error, yes, bin_to_hex, hex_to_bin, dash_open from ..manifest import Manifest, NoManifestError from ..repository3 import Repository3 +from ..repository import Repository from ..repoobj import RepoObj @@ -48,11 +51,7 @@ def __init__(self, repository): self.keyblob = None self.keyblob_storage = None - try: - manifest_chunk = self.repository.get(Manifest.MANIFEST_ID) - except Repository3.ObjectNotFound: - raise NoManifestError - + manifest_chunk = repository.get_manifest() manifest_data = RepoObj.extract_crypted_data(manifest_chunk) key = identify_key(manifest_data) self.keyblob_storage = key.STORAGE diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index f1d95cad0..774ec8b4d 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1188,7 +1188,7 @@ def default(self, o): from ..archive import Archive from ..cache import LocalCache, AdHocCache, AdHocWithFilesCache - if isinstance(o, (Repository, Repository3)) or isinstance(o, (RemoteRepository, RemoteRepository3)): + if isinstance(o, (Repository, RemoteRepository)) or isinstance(o, (Repository3, RemoteRepository3)): return {"id": bin_to_hex(o.id), "location": o._location.canonical_path()} if isinstance(o, Archive): return o.info() diff --git a/src/borg/manifest.py b/src/borg/manifest.py index cdc1b99fb..afe6d89af 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -5,13 +5,14 @@ from operator import attrgetter from collections.abc import Sequence -from .logger import create_logger +from borgstore.store import ObjectNotFound, ItemInfo +from .logger import create_logger logger = create_logger() from .constants import * # NOQA from .helpers.datastruct import StableDict -from .helpers.parseformat import bin_to_hex +from .helpers.parseformat import bin_to_hex, hex_to_bin from .helpers.time import parse_timestamp, calculate_relative_offset, archive_ts_now from .helpers.errors import Error from .patterns import get_regex_from_pattern @@ -246,12 +247,10 @@ def last_timestamp(self): def load(cls, repository, operations, key=None, *, ro_cls=RepoObj): from .item import ManifestItem from .crypto.key import key_factory + from .remote3 import RemoteRepository3 from .repository3 import Repository3 - try: - cdata = repository.get(cls.MANIFEST_ID) - except Repository3.ObjectNotFound: - raise NoManifestError + cdata = repository.get_manifest() if not key: key = key_factory(repository, cdata, ro_cls=ro_cls) manifest = cls(key, repository, ro_cls=ro_cls) @@ -261,7 +260,24 @@ def load(cls, repository, operations, key=None, *, ro_cls=RepoObj): manifest.id = manifest.repo_objs.id_hash(data) if m.get("version") not in (1, 2): raise ValueError("Invalid manifest version") - manifest.archives.set_raw_dict(m.archives) + + if isinstance(repository, (Repository3, RemoteRepository3)): + from .helpers import msgpack + archives = {} + try: + infos = list(repository.store_list("archives")) + except ObjectNotFound: + infos = [] + for info in infos: + info = ItemInfo(*info) # RPC does not give us a NamedTuple + value = repository.store_load(f"archives/{info.name}") + _, value = manifest.repo_objs.parse(hex_to_bin(info.name), value, ro_type=ROBJ_MANIFEST) + archive = msgpack.unpackb(value) + archives[archive["name"]] = dict(id=archive["id"], time=archive["time"]) + manifest.archives.set_raw_dict(archives) + else: + manifest.archives.set_raw_dict(m.archives) + manifest.timestamp = m.get("timestamp") manifest.config = m.config # valid item keys are whatever is known in the repo or every key we know @@ -298,6 +314,8 @@ def get_all_mandatory_features(self): def write(self): from .item import ManifestItem + from .remote3 import RemoteRepository3 + from .repository3 import Repository3 # self.timestamp needs to be strictly monotonically increasing. Clocks often are not set correctly if self.timestamp is None: @@ -312,12 +330,39 @@ def write(self): assert all(len(name) <= 255 for name in self.archives) assert len(self.item_keys) <= 100 self.config["item_keys"] = tuple(sorted(self.item_keys)) + + if isinstance(self.repository, (Repository3, RemoteRepository3)): + valid_keys = set() + for name, info in self.archives.get_raw_dict().items(): + archive = dict(name=name, id=info["id"], time=info["time"]) + value = self.key.pack_metadata(archive) + id = self.repo_objs.id_hash(value) + key = bin_to_hex(id) + value = self.repo_objs.format(id, {}, value, ro_type=ROBJ_MANIFEST) + self.repository.store_store(f"archives/{key}", value) + valid_keys.add(key) + # now, delete all other keys in archives/ which are not in valid keys / in the manifest anymore. + # TODO: this is a dirty hack to simulate the old manifest behaviour closely, but also means + # keeping its problems, like read-modify-write behaviour requiring an exclusive lock. + try: + infos = list(self.repository.store_list("archives")) + except ObjectNotFound: + infos = [] + for info in infos: + info = ItemInfo(*info) # RPC does not give us a NamedTuple + if info.name not in valid_keys: + self.repository.store_delete(f"archives/{info.name}") + manifest_archives = {} + else: + manifest_archives = StableDict(self.archives.get_raw_dict()) + manifest = ManifestItem( version=2, - archives=StableDict(self.archives.get_raw_dict()), + archives=manifest_archives, timestamp=self.timestamp, config=StableDict(self.config), ) data = self.key.pack_metadata(manifest.as_dict()) self.id = self.repo_objs.id_hash(data) - self.repository.put(self.MANIFEST_ID, self.repo_objs.format(self.MANIFEST_ID, {}, data, ro_type=ROBJ_MANIFEST)) + robj = self.repo_objs.format(self.MANIFEST_ID, {}, data, ro_type=ROBJ_MANIFEST) + self.repository.put_manifest(robj) diff --git a/src/borg/remote.py b/src/borg/remote.py index e035224d7..65d81b163 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -156,6 +156,8 @@ class RepositoryServer: # pragma: no cover "load_key", "break_lock", "inject_exception", + "get_manifest", + "put_manifest", ) def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota, use_socket): @@ -1046,6 +1048,14 @@ def async_response(self, wait=True): def preload(self, ids): self.preload_ids += ids + @api(since=parse_version("2.0.0b8")) + def get_manifest(self): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b8")) + def put_manifest(self, data): + """actual remoting is done via self.call in the @api decorator""" + class RepositoryNoCache: """A not caching Repository wrapper, passes through to repository. diff --git a/src/borg/remote3.py b/src/borg/remote3.py index 3b27fd355..25087adc8 100644 --- a/src/borg/remote3.py +++ b/src/borg/remote3.py @@ -177,6 +177,12 @@ class RepositoryServer: # pragma: no cover "load_key", "break_lock", "inject_exception", + "get_manifest", + "put_manifest", + "store_list", + "store_load", + "store_store", + "store_delete", ) def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota, use_socket): @@ -1061,6 +1067,30 @@ def async_response(self, wait=True): def preload(self, ids): self.preload_ids += ids + @api(since=parse_version("2.0.0b8")) + def get_manifest(self): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b8")) + def put_manifest(self, data): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b8")) + def store_list(self, name): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b8")) + def store_load(self, name): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b8")) + def store_store(self, name, value): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b8")) + def store_delete(self, name): + """actual remoting is done via self.call in the @api decorator""" + class RepositoryNoCache: """A not caching Repository wrapper, passes through to repository. diff --git a/src/borg/repository.py b/src/borg/repository.py index 74591619d..9f2b7509c 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -23,7 +23,7 @@ from .helpers.lrucache import LRUCache from .locking import Lock, LockError, LockErrorT from .logger import create_logger -from .manifest import Manifest +from .manifest import Manifest, NoManifestError from .platform import SaveFile, SyncFile, sync_dir, safe_fadvise from .repoobj import RepoObj from .checksums import crc32, StreamingXXH64 @@ -1396,6 +1396,15 @@ def async_response(self, wait=True): def preload(self, ids): """Preload objects (only applies to remote repositories)""" + def get_manifest(self): + try: + return self.get(Manifest.MANIFEST_ID) + except self.ObjectNotFound: + raise NoManifestError + + def put_manifest(self, data): + return self.put(Manifest.MANIFEST_ID, data) + class LoggedIO: class SegmentFull(Exception): diff --git a/src/borg/repository3.py b/src/borg/repository3.py index d21422a63..3b8e15b9f 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -10,6 +10,7 @@ from .helpers import bin_to_hex, hex_to_bin from .locking3 import Lock from .logger import create_logger +from .manifest import NoManifestError from .repoobj import RepoObj logger = create_logger(__name__) @@ -213,30 +214,38 @@ def log_error(msg): logger.info("Starting repository check") objs_checked = objs_errors = 0 infos = self.store.list("data") - for info in infos: - self._lock_refresh() - obj_corrupted = False - key = "data/%s" % info.name - obj = self.store.load(key) - hdr_size = RepoObj.obj_header.size - obj_size = len(obj) - if obj_size >= hdr_size: - hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size])) - meta = obj[hdr_size:hdr_size+hdr.meta_size] - if hdr.meta_size != len(meta): - log_error("metadata size incorrect.") - elif hdr.meta_hash != xxh64(meta): - log_error("metadata does not match checksum.") - data = obj[hdr_size+hdr.meta_size:hdr_size+hdr.meta_size+hdr.data_size] - if hdr.data_size != len(data): - log_error("data size incorrect.") - elif hdr.data_hash != xxh64(data): - log_error("data does not match checksum.") - else: - log_error("too small.") - objs_checked += 1 - if obj_corrupted: - objs_errors += 1 + try: + for info in infos: + self._lock_refresh() + obj_corrupted = False + key = "data/%s" % info.name + try: + obj = self.store.load(key) + except StoreObjectNotFound: + # looks like object vanished since store.list(), ignore that. + continue + hdr_size = RepoObj.obj_header.size + obj_size = len(obj) + if obj_size >= hdr_size: + hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size])) + meta = obj[hdr_size:hdr_size+hdr.meta_size] + if hdr.meta_size != len(meta): + log_error("metadata size incorrect.") + elif hdr.meta_hash != xxh64(meta): + log_error("metadata does not match checksum.") + data = obj[hdr_size+hdr.meta_size:hdr_size+hdr.meta_size+hdr.data_size] + if hdr.data_size != len(data): + log_error("data size incorrect.") + elif hdr.data_hash != xxh64(data): + log_error("data does not match checksum.") + else: + log_error("too small.") + objs_checked += 1 + if obj_corrupted: + objs_errors += 1 + except StoreObjectNotFound: + # it can be that there is no "data/" at all, then it crashes when iterating infos. + pass logger.info(f"Checked {objs_checked} repository objects, {objs_errors} errors.") if objs_errors == 0: logger.info("Finished %s repository check, no problems found.", mode) @@ -261,7 +270,10 @@ def list(self, limit=None, marker=None, mask=0, value=0): """ self._lock_refresh() infos = self.store.list("data") # XXX we can only get the full list from the store - ids = [hex_to_bin(info.name) for info in infos] + try: + ids = [hex_to_bin(info.name) for info in infos] + except StoreObjectNotFound: + ids = [] if marker is not None: idx = ids.index(marker) ids = ids[idx + 1:] @@ -365,3 +377,27 @@ def preload(self, ids): def break_lock(self): Lock(self.store).break_lock() + + def get_manifest(self): + try: + return self.store.load("config/manifest") + except StoreObjectNotFound: + raise NoManifestError + + def put_manifest(self, data): + return self.store.store("config/manifest", data) + + def store_list(self, name): + try: + return list(self.store.list(name)) + except StoreObjectNotFound: + return [] + + def store_load(self, name): + return self.store.load(name) + + def store_store(self, name, value): + return self.store.store(name, value) + + def store_delete(self, name): + return self.store.delete(name) diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index c393c238b..7085a5fe7 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -192,6 +192,8 @@ def test_missing_manifest(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") + if isinstance(repository, Repository3): + pytest.skip("Test not adapted to Repository3") with repository: repository.delete(Manifest.MANIFEST_ID) repository.commit(compact=False) @@ -206,6 +208,8 @@ def test_corrupted_manifest(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") + if isinstance(repository, Repository3): + pytest.skip("Test not adapted to Repository3") with repository: manifest = repository.get(Manifest.MANIFEST_ID) corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] @@ -222,6 +226,8 @@ def test_spoofed_manifest(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") + if isinstance(repository, Repository3): + pytest.skip("Test not adapted to Repository3") with repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) cdata = manifest.repo_objs.format( @@ -256,6 +262,8 @@ def test_manifest_rebuild_corrupted_chunk(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") + if isinstance(repository, Repository3): + pytest.skip("Test not adapted to Repository3") with repository: manifest = repository.get(Manifest.MANIFEST_ID) corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] @@ -274,6 +282,8 @@ def test_manifest_rebuild_duplicate_archive(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") + if isinstance(repository, Repository3): + pytest.skip("Test not adapted to Repository3") repo_objs = archive.repo_objs with repository: manifest = repository.get(Manifest.MANIFEST_ID) @@ -304,6 +314,8 @@ def test_spoofed_archive(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") + if isinstance(repository, Repository3): + pytest.skip("Test not adapted to Repository3") repo_objs = archive.repo_objs with repository: # attacker would corrupt or delete the manifest to trigger a rebuild of it: diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index c232c84b2..f9de6ccc7 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -166,7 +166,6 @@ def repository(self, tmpdir): self.repository_location = os.path.join(str(tmpdir), "repository") with Repository3(self.repository_location, exclusive=True, create=True) as repository: repository.put(H(1), b"1234") - repository.put(Manifest.MANIFEST_ID, b"5678") yield repository @pytest.fixture From b637542dcf65d3073553217df8c3ef53e1635ed2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Aug 2024 23:37:57 +0200 Subject: [PATCH 06/79] repository3/manifest: tests reenabled, fixes --- src/borg/archive.py | 21 ++++++++----- src/borg/remote3.py | 3 ++ src/borg/testsuite/archiver/check_cmd.py | 38 ++++++++++-------------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 3f8c7c914..4cf937bf7 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -52,7 +52,7 @@ from .platform import acl_get, acl_set, set_flags, get_flags, swidth, hostname from .remote import cache_if_remote from .remote3 import RemoteRepository3 -from .repository3 import Repository3, LIST_SCAN_LIMIT +from .repository3 import Repository3, LIST_SCAN_LIMIT, NoManifestError from .repoobj import RepoObj has_link = hasattr(os, "link") @@ -1860,8 +1860,9 @@ def check( self.repo_objs = RepoObj(self.key) if verify_data: self.verify_data() - if not isinstance(repository, (Repository3, RemoteRepository3)) and Manifest.MANIFEST_ID not in self.chunks: - logger.error("Repository manifest not found!") + try: + repository.get_manifest() + except NoManifestError: self.error_found = True self.manifest = self.rebuild_manifest() else: @@ -1905,12 +1906,16 @@ def make_key(self, repository): # try the manifest first! attempt += 1 - cdata = repository.get_manifest() try: - return key_factory(repository, cdata) - except UnsupportedPayloadError: - # we get here, if the cdata we got has a corrupted key type byte - pass # ignore it, just continue trying + cdata = repository.get_manifest() + except NoManifestError: + pass + else: + try: + return key_factory(repository, cdata) + except UnsupportedPayloadError: + # we get here, if the cdata we got has a corrupted key type byte + pass # ignore it, just continue trying for chunkid, _ in self.chunks.iteritems(): attempt += 1 diff --git a/src/borg/remote3.py b/src/borg/remote3.py index 25087adc8..1345a4e0b 100644 --- a/src/borg/remote3.py +++ b/src/borg/remote3.py @@ -32,6 +32,7 @@ from .helpers import get_socket_filename from .locking import LockTimeout, NotLocked, NotMyLock, LockFailed from .logger import create_logger, borg_serve_log_queue +from .manifest import NoManifestError from .helpers import msgpack from .repository import Repository from .repository3 import Repository3 @@ -835,6 +836,8 @@ def handle_error(unpacked): raise NotLocked(args[0]) elif error == "NotMyLock": raise NotMyLock(args[0]) + elif error == "NoManifestError": + raise NoManifestError else: raise self.RPCError(unpacked) diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index 7085a5fe7..d68998820 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -8,6 +8,7 @@ from ...constants import * # NOQA from ...helpers import bin_to_hex, msgpack from ...manifest import Manifest +from ...remote3 import RemoteRepository3 from ...repository3 import Repository3 from ..repository3 import fchunk from . import cmd, src_file, create_src_archive, open_archive, generate_archiver_tests, RK_ENCRYPTION @@ -192,11 +193,12 @@ def test_missing_manifest(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") - if isinstance(repository, Repository3): - pytest.skip("Test not adapted to Repository3") with repository: - repository.delete(Manifest.MANIFEST_ID) - repository.commit(compact=False) + if isinstance(repository, (Repository3, RemoteRepository3)): + repository.store_delete("config/manifest") + else: + repository.delete(Manifest.MANIFEST_ID) + repository.commit(compact=False) cmd(archiver, "check", exit_code=1) output = cmd(archiver, "check", "-v", "--repair", exit_code=0) assert "archive1" in output @@ -208,12 +210,10 @@ def test_corrupted_manifest(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") - if isinstance(repository, Repository3): - pytest.skip("Test not adapted to Repository3") with repository: - manifest = repository.get(Manifest.MANIFEST_ID) + manifest = repository.get_manifest() corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] - repository.put(Manifest.MANIFEST_ID, corrupted_manifest) + repository.put_manifest(corrupted_manifest) repository.commit(compact=False) cmd(archiver, "check", exit_code=1) output = cmd(archiver, "check", "-v", "--repair", exit_code=0) @@ -226,8 +226,6 @@ def test_spoofed_manifest(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") - if isinstance(repository, Repository3): - pytest.skip("Test not adapted to Repository3") with repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) cdata = manifest.repo_objs.format( @@ -247,7 +245,7 @@ def test_spoofed_manifest(archivers, request): ) # maybe a repo-side attacker could manage to move the fake manifest file chunk over to the manifest ID. # we simulate this here by directly writing the fake manifest data to the manifest ID. - repository.put(Manifest.MANIFEST_ID, cdata) + repository.put_manifest(cdata) repository.commit(compact=False) # borg should notice that the manifest has the wrong ro_type. cmd(archiver, "check", exit_code=1) @@ -262,12 +260,10 @@ def test_manifest_rebuild_corrupted_chunk(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") - if isinstance(repository, Repository3): - pytest.skip("Test not adapted to Repository3") with repository: - manifest = repository.get(Manifest.MANIFEST_ID) + manifest = repository.get_manifest() corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] - repository.put(Manifest.MANIFEST_ID, corrupted_manifest) + repository.put_manifest(corrupted_manifest) chunk = repository.get(archive.id) corrupted_chunk = chunk + b"corrupted!" repository.put(archive.id, corrupted_chunk) @@ -282,13 +278,11 @@ def test_manifest_rebuild_duplicate_archive(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") - if isinstance(repository, Repository3): - pytest.skip("Test not adapted to Repository3") repo_objs = archive.repo_objs with repository: - manifest = repository.get(Manifest.MANIFEST_ID) + manifest = repository.get_manifest() corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] - repository.put(Manifest.MANIFEST_ID, corrupted_manifest) + repository.put_manifest(corrupted_manifest) archive_dict = { "command_line": "", "item_ptrs": [], @@ -314,14 +308,12 @@ def test_spoofed_archive(archivers, request): archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") - if isinstance(repository, Repository3): - pytest.skip("Test not adapted to Repository3") repo_objs = archive.repo_objs with repository: # attacker would corrupt or delete the manifest to trigger a rebuild of it: - manifest = repository.get(Manifest.MANIFEST_ID) + manifest = repository.get_manifest() corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] - repository.put(Manifest.MANIFEST_ID, corrupted_manifest) + repository.put_manifest(corrupted_manifest) archive_dict = { "command_line": "", "item_ptrs": [], From c292ee25c59dd43d526a8fe7eec69d668d353838 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Aug 2024 23:57:26 +0200 Subject: [PATCH 07/79] rcompress: use get/put_manifest --- src/borg/archiver/rcompress_cmd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index c8558f7ee..7b27b83bc 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -123,8 +123,8 @@ def checkpoint_func(): if not isinstance(repository, (Repository3, RemoteRepository3)): # start a new transaction - data = repository.get(Manifest.MANIFEST_ID) - repository.put(Manifest.MANIFEST_ID, data) + data = repository.get_manifest() + repository.put_manifest(data) uncommitted_chunks += 1 pi = ProgressIndicatorPercent( From 8c2cbdbb553c40689974eabe69689fbfa978b74c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 12 Aug 2024 23:58:30 +0200 Subject: [PATCH 08/79] compact: remove "borg compact", not needed any more All chunks are separate objects in borgstore. --- src/borg/archiver/__init__.py | 3 -- src/borg/archiver/compact_cmd.py | 67 -------------------------------- 2 files changed, 70 deletions(-) delete mode 100644 src/borg/archiver/compact_cmd.py diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 1e1e11eed..7b92419f3 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -67,7 +67,6 @@ def get_func(args): from .benchmark_cmd import BenchmarkMixIn from .check_cmd import CheckMixIn -from .compact_cmd import CompactMixIn from .create_cmd import CreateMixIn from .debug_cmd import DebugMixIn from .delete_cmd import DeleteMixIn @@ -96,7 +95,6 @@ def get_func(args): class Archiver( BenchmarkMixIn, CheckMixIn, - CompactMixIn, CreateMixIn, DebugMixIn, DeleteMixIn, @@ -333,7 +331,6 @@ def build_parser(self): self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser) self.build_parser_check(subparsers, common_parser, mid_common_parser) - self.build_parser_compact(subparsers, common_parser, mid_common_parser) self.build_parser_create(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) diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py deleted file mode 100644 index ea3093ec5..000000000 --- a/src/borg/archiver/compact_cmd.py +++ /dev/null @@ -1,67 +0,0 @@ -import argparse - -from ._common import with_repository, Highlander -from ..constants import * # NOQA -from ..manifest import Manifest -from ..repository3 import Repository3 -from ..remote3 import RemoteRepository3 - -from ..logger import create_logger - -logger = create_logger() - - -class CompactMixIn: - @with_repository(manifest=False, exclusive=True) - def do_compact(self, args, repository): - """compact segment files in the repository""" - if not isinstance(repository, (Repository3, RemoteRepository3)): - # see the comment in do_with_lock about why we do it like this: - data = repository.get(Manifest.MANIFEST_ID) - repository.put(Manifest.MANIFEST_ID, data) - threshold = args.threshold / 100 - repository.commit(compact=True, threshold=threshold) - - def build_parser_compact(self, subparsers, common_parser, mid_common_parser): - from ._common import process_epilog - - compact_epilog = process_epilog( - """ - This command frees repository space by compacting segments. - - Use this regularly to avoid running out of space - you do not need to use this - after each borg command though. It is especially useful after deleting archives, - because only compaction will really free repository space. - - borg compact does not need a key, so it is possible to invoke it from the - client or also from the server. - - Depending on the amount of segments that need compaction, it may take a while, - so consider using the ``--progress`` option. - - A segment is compacted if the amount of saved space is above the percentage value - given by the ``--threshold`` option. If omitted, a threshold of 10% is used. - When using ``--verbose``, borg will output an estimate of the freed space. - - See :ref:`separate_compaction` in Additional Notes for more details. - """ - ) - subparser = subparsers.add_parser( - "compact", - parents=[common_parser], - add_help=False, - description=self.do_compact.__doc__, - epilog=compact_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help="compact segment files / free space in repo", - ) - subparser.set_defaults(func=self.do_compact) - subparser.add_argument( - "--threshold", - metavar="PERCENT", - dest="threshold", - type=int, - default=10, - action=Highlander, - help="set minimum threshold for saved space in PERCENT (Default: 10)", - ) From 8ef517161d8eb0fd4a534e887603763cd83aadce Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Aug 2024 16:51:16 +0200 Subject: [PATCH 09/79] compact: reimplement "borg compact" as garbage collection It also outputs some statistics and warns about missing/reappeared chunks. --- src/borg/archiver/__init__.py | 3 + src/borg/archiver/compact_cmd.py | 161 +++++++++++++++++++++ src/borg/testsuite/archiver/compact_cmd.py | 44 ++++++ 3 files changed, 208 insertions(+) create mode 100644 src/borg/archiver/compact_cmd.py create mode 100644 src/borg/testsuite/archiver/compact_cmd.py diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 7b92419f3..1e1e11eed 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -67,6 +67,7 @@ def get_func(args): from .benchmark_cmd import BenchmarkMixIn from .check_cmd import CheckMixIn +from .compact_cmd import CompactMixIn from .create_cmd import CreateMixIn from .debug_cmd import DebugMixIn from .delete_cmd import DeleteMixIn @@ -95,6 +96,7 @@ def get_func(args): class Archiver( BenchmarkMixIn, CheckMixIn, + CompactMixIn, CreateMixIn, DebugMixIn, DeleteMixIn, @@ -331,6 +333,7 @@ def build_parser(self): self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser) self.build_parser_check(subparsers, common_parser, mid_common_parser) + self.build_parser_compact(subparsers, common_parser, mid_common_parser) self.build_parser_create(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) diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py new file mode 100644 index 000000000..43d26be24 --- /dev/null +++ b/src/borg/archiver/compact_cmd.py @@ -0,0 +1,161 @@ +import argparse +from typing import Tuple, Dict + +from ._common import with_repository +from ..archive import Archive +from ..constants import * # NOQA +from ..helpers import set_ec, EXIT_WARNING, EXIT_ERROR, format_file_size +from ..helpers import ProgressIndicatorPercent +from ..manifest import Manifest +from ..remote3 import RemoteRepository3 +from ..repository3 import Repository3 + +from ..logger import create_logger +logger = create_logger() + + +class ArchiveGarbageCollector: + def __init__(self, repository, manifest): + self.repository = repository + assert isinstance(repository, (Repository3, RemoteRepository3)) + self.manifest = manifest + self.repository_chunks = None # what we have in the repository + self.used_chunks = None # what archives currently reference + self.wanted_chunks = None # chunks that would be nice to have for next borg check --repair + self.total_files = None # overall number of source files written to all archives in this repo + self.total_size = None # overall size of source file content data written to all archives + self.archives_count = None # number of archives (including checkpoint archives) + + def garbage_collect(self): + """Removes unused chunks from a repository.""" + logger.info("Starting compaction / garbage collection...") + logger.info("Getting object IDs present in the repository...") + self.repository_chunks = self.get_repository_chunks() + logger.info("Computing object IDs used by archives...") + self.used_chunks, self.wanted_chunks, self.total_files, self.total_size, self.archives_count = self.analyze_archives() + self.report_and_delete() + logger.info("Finished compaction / garbage collection...") + + def get_repository_chunks(self) -> Dict[bytes, int]: + """Build a dict id -> size of all chunks present in the repository""" + repository_chunks = {} + marker = None + while True: + result = self.repository.list(limit=LIST_SCAN_LIMIT, marker=marker) + if not result: + break + marker = result[-1] + for chunk_id in result: + repository_chunks[chunk_id] = 0 # plaintext size unknown + return repository_chunks + + def analyze_archives(self) -> Tuple[Dict[bytes, int], Dict[bytes, int], int, int, int]: + """Iterate over all items in all archives, create the dicts id -> size of all used/wanted chunks.""" + used_chunks = {} # chunks referenced by item.chunks + wanted_chunks = {} # additional "wanted" chunks seen in item.chunks_healthy + archive_infos = self.manifest.archives.list(consider_checkpoints=True) + num_archives = len(archive_infos) + pi = ProgressIndicatorPercent( + total=num_archives, msg="Computing used/wanted chunks %3.1f%%", step=0.1, msgid="compact.analyze_archives" + ) + total_size, total_files = 0, 0 + for i, info in enumerate(archive_infos): + pi.show(i) + logger.info(f"Analyzing archive {info.name} ({i + 1}/{num_archives})") + archive = Archive(self.manifest, info.name) + # archive metadata size unknown, but usually small/irrelevant: + used_chunks[archive.id] = 0 + for id in archive.metadata.item_ptrs: + used_chunks[id] = 0 + for id in archive.metadata.items: + used_chunks[id] = 0 + # archive items content data: + for item in archive.iter_items(): + total_files += 1 # every fs object counts, not just regular files + if "chunks" in item: + for id, size in item.chunks: + total_size += size # original, uncompressed file content size + used_chunks[id] = size + if "chunks_healthy" in item: + # we also consider the chunks_healthy chunks as referenced - do not throw away + # anything that borg check --repair might still need. + for id, size in item.chunks_healthy: + if id not in used_chunks: + wanted_chunks[id] = size + pi.finish() + return used_chunks, wanted_chunks, total_files, total_size, num_archives + + def report_and_delete(self): + run_repair = " Run borg check --repair!" + + missing_new = set(self.used_chunks) - set(self.repository_chunks) + if missing_new: + logger.error(f"Repository has {len(missing_new)} new missing objects." + run_repair) + set_ec(EXIT_ERROR) + + missing_known = set(self.wanted_chunks) - set(self.repository_chunks) + if missing_known: + logger.warning(f"Repository has {len(missing_known)} known missing objects.") + set_ec(EXIT_WARNING) + + missing_found = set(self.wanted_chunks) & set(self.repository_chunks) + if missing_found: + logger.warning(f"{len(missing_found)} previously missing objects re-appeared!" + run_repair) + set_ec(EXIT_WARNING) + + referenced_chunks = set(self.used_chunks) | set(self.wanted_chunks) + unused = set(self.repository_chunks) - referenced_chunks + logger.info(f"Repository has {len(unused)} objects to delete.") + if unused: + logger.info(f"Deleting {len(unused)} unused objects...") + pi = ProgressIndicatorPercent( + total=len(unused), msg="Deleting unused objects %3.1f%%", step=0.1, + msgid="compact.report_and_delete" + ) + for i, id in enumerate(unused): + pi.show(i) + self.repository.delete(id) + del self.repository_chunks[id] + pi.finish() + + count = len(self.repository_chunks) + logger.info(f"Repository has {count} objects now.") + + logger.info(f"Overall statistics, considering all {self.archives_count} archives in this repository:") + logger.info(f"Source files count (before deduplication): {self.total_files}") + logger.info(f"Source files size (before deduplication): {format_file_size(self.total_size, precision=0)}") + dsize = sum(self.used_chunks[id] for id in self.repository_chunks) + logger.info(f"Deduplicated size (before compression, encryption): {format_file_size(dsize, precision=0)}") + + +class CompactMixIn: + @with_repository(exclusive=True, compatibility=(Manifest.Operation.DELETE,)) + def do_compact(self, args, repository, manifest): + """Collect garbage in repository""" + ArchiveGarbageCollector(repository, manifest).garbage_collect() + + def build_parser_compact(self, subparsers, common_parser, mid_common_parser): + from ._common import process_epilog + + compact_epilog = process_epilog( + """ + Free repository space by deleting unused chunks. + + borg compact analyzes all existing archives to find out which chunks are + actually used. There might be unused chunks resulting from borg delete or prune, + which can be removed to free space in the repository. + + Differently than borg 1.x, borg2's compact needs the borg key if the repo is + encrypted. + """ + ) + subparser = subparsers.add_parser( + "compact", + parents=[common_parser], + add_help=False, + description=self.do_compact.__doc__, + epilog=compact_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help="compact repository", + ) + subparser.set_defaults(func=self.do_compact) diff --git a/src/borg/testsuite/archiver/compact_cmd.py b/src/borg/testsuite/archiver/compact_cmd.py new file mode 100644 index 000000000..c1dc3fcb7 --- /dev/null +++ b/src/borg/testsuite/archiver/compact_cmd.py @@ -0,0 +1,44 @@ +from ...constants import * # NOQA +from . import cmd, create_src_archive, generate_archiver_tests, RK_ENCRYPTION + +pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA + + +def test_compact_empty_repository(archivers, request): + archiver = request.getfixturevalue(archivers) + + cmd(archiver, "rcreate", RK_ENCRYPTION) + + output = cmd(archiver, "compact", "-v", exit_code=0) + assert "Starting compaction" in output + assert "Repository has 0 objects now." in output + assert "Finished compaction" in output + + +def test_compact_after_deleting_all_archives(archivers, request): + archiver = request.getfixturevalue(archivers) + + cmd(archiver, "rcreate", RK_ENCRYPTION) + create_src_archive(archiver, "archive") + cmd(archiver, "delete", "-a", "archive", exit_code=0) + + output = cmd(archiver, "compact", "-v", exit_code=0) + assert "Starting compaction" in output + assert "Deleting " in output + assert "Repository has 0 objects now." in output + assert "Finished compaction" in output + + +def test_compact_after_deleting_some_archives(archivers, request): + archiver = request.getfixturevalue(archivers) + + cmd(archiver, "rcreate", RK_ENCRYPTION) + create_src_archive(archiver, "archive1") + create_src_archive(archiver, "archive2") + cmd(archiver, "delete", "-a", "archive1", exit_code=0) + + output = cmd(archiver, "compact", "-v", exit_code=0) + assert "Starting compaction" in output + assert "Deleting " in output + assert "Repository has 0 objects now, using approx. 0 B." not in output + assert "Finished compaction" in output From 17ea1181554bd82fe74d5e37b7bb4bded2c98e39 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Aug 2024 22:52:14 +0200 Subject: [PATCH 10/79] check: remove orphan chunks detection/cleanup This is now done in borg compact, so borg check does not need to care. --- src/borg/archive.py | 20 -------------------- src/borg/testsuite/archiver/check_cmd.py | 7 +------ 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 4cf937bf7..31b1f0575 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1877,7 +1877,6 @@ def check( self.rebuild_refcounts( match=match, first=first, last=last, sort_by=sort_by, older=older, oldest=oldest, newer=newer, newest=newest ) - self.orphan_chunks_check() self.finish() if self.error_found: logger.error("Archive consistency check complete, problems found.") @@ -2346,25 +2345,6 @@ def valid_item(obj): self.manifest.archives[info.name] = (new_archive_id, info.ts) pi.finish() - def orphan_chunks_check(self): - if self.check_all: - unused = {id_ for id_, entry in self.chunks.iteritems() if entry.refcount == 0} - orphaned = unused - self.possibly_superseded - if orphaned: - logger.info(f"{len(orphaned)} orphaned (unused) objects found.") - for chunk_id in orphaned: - logger.debug(f"chunk {bin_to_hex(chunk_id)} is orphaned.") - # To support working with AdHocCache or AdHocWithFilesCache, we do not set self.error_found = True. - if self.repair and unused: - logger.info( - "Deleting %d orphaned and %d superseded objects..." % (len(orphaned), len(self.possibly_superseded)) - ) - for id_ in unused: - self.repository.delete(id_) - logger.info("Finished deleting orphaned/superseded objects.") - else: - logger.info("Orphaned objects check skipped (needs all archives checked).") - def finish(self): if self.repair: logger.info("Writing Manifest.") diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index d68998820..14683682c 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -355,12 +355,7 @@ def test_extra_chunks(archivers, request): chunk = fchunk(b"xxxx") repository.put(b"01234567890123456789012345678901", chunk) repository.commit(compact=False) - output = cmd(archiver, "check", "-v", exit_code=0) # orphans are not considered warnings anymore - assert "1 orphaned (unused) objects found." in output - cmd(archiver, "check", "--repair", exit_code=0) - output = cmd(archiver, "check", "-v", exit_code=0) - assert "orphaned (unused) objects found." not in output - cmd(archiver, "extract", "archive1", "--dry-run", exit_code=0) + cmd(archiver, "check", "-v", exit_code=0) # check does not deal with orphans anymore @pytest.mark.parametrize("init_args", [["--encryption=repokey-aes-ocb"], ["--encryption", "none"]]) From 4c052cd65d6f9e98ab6cae45ae15849e59dea686 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Aug 2024 18:58:32 +0200 Subject: [PATCH 11/79] delete: just remove archive from manifest, let borg compact clean up later. much faster and easier now, similar to what borg delete --force --force used to do. considering that speed, no need for checkpointing anymore. --stats does not work that way, thus it was removed. borg compact now shows some stats. --- src/borg/archive.py | 83 +--------------- src/borg/archiver/delete_cmd.py | 114 +++++----------------- src/borg/archiver/prune_cmd.py | 49 ++-------- src/borg/testsuite/archiver/delete_cmd.py | 57 +---------- 4 files changed, 41 insertions(+), 262 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 31b1f0575..326b3cbf8 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1038,84 +1038,11 @@ def rename(self, name): self.set_meta("name", name) del self.manifest.archives[oldname] - def delete(self, stats, progress=False, forced=False): - class ChunksIndexError(Error): - """Chunk ID {} missing from chunks index, corrupted chunks index - aborting transaction.""" - - exception_ignored = object() - - def fetch_async_response(wait=True): - try: - return self.repository.async_response(wait=wait) - except Repository3.ObjectNotFound: - nonlocal error - # object not in repo - strange, but we wanted to delete it anyway. - if forced == 0: - raise - error = True - return exception_ignored # must not return None here - - def chunk_decref(id, size, stats): - try: - self.cache.chunk_decref(id, size, stats, wait=False) - except KeyError: - nonlocal error - if forced == 0: - cid = bin_to_hex(id) - raise ChunksIndexError(cid) - error = True - else: - fetch_async_response(wait=False) - - error = False - try: - unpacker = msgpack.Unpacker(use_list=False) - items_ids = self.metadata.items - pi = ProgressIndicatorPercent( - total=len(items_ids), msg="Decrementing references %3.0f%%", msgid="archive.delete" - ) - for i, (items_id, data) in enumerate(zip(items_ids, self.repository.get_many(items_ids))): - if progress: - pi.show(i) - _, data = self.repo_objs.parse(items_id, data, ro_type=ROBJ_ARCHIVE_STREAM) - unpacker.feed(data) - chunk_decref(items_id, 1, stats) - try: - for item in unpacker: - item = Item(internal_dict=item) - if "chunks" in item: - for chunk_id, size in item.chunks: - chunk_decref(chunk_id, size, stats) - except (TypeError, ValueError): - # if items metadata spans multiple chunks and one chunk got dropped somehow, - # it could be that unpacker yields bad types - if forced == 0: - raise - error = True - if progress: - pi.finish() - except (msgpack.UnpackException, Repository3.ObjectNotFound): - # items metadata corrupted - if forced == 0: - raise - error = True - - # delete the blocks that store all the references that end up being loaded into metadata.items: - for id in self.metadata.item_ptrs: - chunk_decref(id, 1, stats) - - # in forced delete mode, we try hard to delete at least the manifest entry, - # if possible also the archive superblock, even if processing the items raises - # some harmless exception. - chunk_decref(self.id, 1, stats) + def delete(self): + # quick and dirty: we just nuke the archive from the archives list - that will + # potentially orphan all chunks previously referenced by the archive, except the ones also + # referenced by other archives. In the end, "borg compact" will clean up and free space. del self.manifest.archives[self.name] - while fetch_async_response(wait=True) is not None: - # we did async deletes, process outstanding results (== exceptions), - # so there is nothing pending when we return and our caller wants to commit. - pass - if error: - logger.warning("forced deletion succeeded, but the deleted archive was corrupted.") - logger.warning("borg check --repair is required to free all space.") @staticmethod def compare_archives_iter( @@ -2501,7 +2428,7 @@ def save(self, archive, target, comment=None, replace_original=True): target.save(comment=comment, timestamp=self.timestamp, additional_metadata=additional_metadata) if replace_original: - archive.delete(Statistics(), progress=self.progress) + archive.delete() target.rename(archive.name) if self.stats: target.start = _start diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index 7095cda90..275607299 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -1,11 +1,9 @@ import argparse import logging -from ._common import with_repository, Highlander -from ..archive import Archive, Statistics -from ..cache import Cache +from ._common import with_repository from ..constants import * # NOQA -from ..helpers import log_multi, format_archive, sig_int, CommandError, Error +from ..helpers import format_archive, CommandError from ..manifest import Manifest from ..logger import create_logger @@ -29,67 +27,30 @@ def do_delete(self, args, repository): "or just delete the whole repository (might be much faster)." ) - 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.print_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) - self.print_warning('Done. Run "borg check --repair" to clean up the mess.', wc=None) + deleted = False + logger_list = logging.getLogger("borg.output.list") + for i, archive_name in enumerate(archive_names, 1): + try: + # this does NOT use Archive.delete, so this code hopefully even works in cases a corrupt archive + # would make the code in class Archive crash, so the user can at least get rid of such archives. + current_archive = manifest.archives.pop(archive_name) + except KeyError: + self.print_warning(f"Archive {archive_name} not found ({i}/{len(archive_names)}).") else: - self.print_warning("Aborted.", wc=None) - return + 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() + repository.commit(compact=False) + self.print_warning('Done. Run "borg compact" to free space.', wc=None) + else: + self.print_warning("Aborted.", wc=None) + return - stats = Statistics(iec=args.iec) - with Cache(repository, manifest, progress=args.progress, lock_wait=self.lock_wait, iec=args.iec) as cache: - - def checkpoint_func(): - manifest.write() - repository.commit(compact=False) - cache.commit() - - msg_delete = "Would delete archive: {} ({}/{})" if dry_run else "Deleting archive: {} ({}/{})" - msg_not_found = "Archive {} not found ({}/{})." - logger_list = logging.getLogger("borg.output.list") - uncommitted_deletes = 0 - for i, archive_name in enumerate(archive_names, 1): - if sig_int and sig_int.action_done(): - break - try: - archive_info = manifest.archives[archive_name] - except KeyError: - self.print_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(manifest, archive_name, cache=cache) - archive.delete(stats, progress=args.progress, forced=args.forced) - checkpointed = self.maybe_checkpoint( - checkpoint_func=checkpoint_func, checkpoint_interval=args.checkpoint_interval - ) - uncommitted_deletes = 0 if checkpointed else (uncommitted_deletes + 1) - if sig_int: - # Ctrl-C / SIGINT: do not checkpoint (commit) again, we already have a checkpoint in this case. - raise Error("Got Ctrl-C / SIGINT.") - elif uncommitted_deletes > 0: - checkpoint_func() - if args.stats: - log_multi(str(stats), logger=logging.getLogger("borg.output.stats")) def build_parser_delete(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog, define_archive_filters_group @@ -103,16 +64,9 @@ def build_parser_delete(self, subparsers, common_parser, mid_common_parser): 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 pattern, using the ``--match-archives PATTERN`` 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( @@ -135,24 +89,4 @@ def build_parser_delete(self, subparsers, common_parser, mid_common_parser): 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( - "--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( - "-c", - "--checkpoint-interval", - metavar="SECONDS", - dest="checkpoint_interval", - type=int, - default=1800, - action=Highlander, - help="write checkpoint every SECONDS seconds (Default: 1800)", - ) define_archive_filters_group(subparser) diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index e8121993a..47443d079 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -7,10 +7,10 @@ import re from ._common import with_repository, Highlander -from ..archive import Archive, Statistics +from ..archive import Archive from ..cache import Cache from ..constants import * # NOQA -from ..helpers import ArchiveFormatter, interval, sig_int, log_multi, ProgressIndicatorPercent, CommandError, Error +from ..helpers import ArchiveFormatter, interval, sig_int, ProgressIndicatorPercent, CommandError, Error from ..manifest import Manifest from ..logger import create_logger @@ -127,14 +127,7 @@ def do_prune(self, args, repository, manifest): keep += prune_split(archives, rule, num, kept_because) to_delete = (set(archives) | checkpoints) - (set(keep) | set(keep_checkpoints)) - stats = Statistics(iec=args.iec) with Cache(repository, manifest, lock_wait=self.lock_wait, iec=args.iec) as cache: - - def checkpoint_func(): - manifest.write() - repository.commit(compact=False) - cache.commit() - list_logger = logging.getLogger("borg.output.list") # set up counters for the progress display to_delete_len = len(to_delete) @@ -152,11 +145,8 @@ def checkpoint_func(): archives_deleted += 1 log_message = "Pruning archive (%d/%d):" % (archives_deleted, to_delete_len) archive = Archive(manifest, archive.name, cache) - archive.delete(stats, forced=args.forced) - checkpointed = self.maybe_checkpoint( - checkpoint_func=checkpoint_func, checkpoint_interval=args.checkpoint_interval - ) - uncommitted_deletes = 0 if checkpointed else (uncommitted_deletes + 1) + archive.delete() + uncommitted_deletes += 1 else: if is_checkpoint(archive.name): log_message = "Keeping checkpoint archive:" @@ -172,12 +162,11 @@ def checkpoint_func(): list_logger.info(f"{log_message:<44} {formatter.format_item(archive, jsonline=False)}") pi.finish() if sig_int: - # Ctrl-C / SIGINT: do not checkpoint (commit) again, we already have a checkpoint in this case. raise Error("Got Ctrl-C / SIGINT.") elif uncommitted_deletes > 0: - checkpoint_func() - if args.stats: - log_multi(str(stats), logger=logging.getLogger("borg.output.stats")) + manifest.write() + repository.commit(compact=False) + cache.commit() def build_parser_prune(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog @@ -235,11 +224,6 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser): keep the last N archives under the assumption that you do not create more than one backup archive in the same second). - 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 pruning. - You can influence how the ``--list`` output is formatted by using the ``--short`` option (less wide output) or by giving a custom format using ``--format`` (see the ``borg rlist`` description for more details about the format string). @@ -256,15 +240,6 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser): ) subparser.set_defaults(func=self.do_prune) subparser.add_argument("-n", "--dry-run", dest="dry_run", action="store_true", help="do not change repository") - subparser.add_argument( - "--force", - dest="forced", - action="store_true", - help="force pruning of corrupted archives, " "use ``--force --force`` in case ``--force`` does not work.", - ) - subparser.add_argument( - "-s", "--stats", dest="stats", action="store_true", help="print statistics for the deleted archive" - ) subparser.add_argument( "--list", dest="output_list", action="store_true", help="output verbose list of archives it keeps/prunes" ) @@ -353,13 +328,3 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser): help="number of yearly archives to keep", ) define_archive_filters_group(subparser, sort_by=False, first_last=False) - subparser.add_argument( - "-c", - "--checkpoint-interval", - metavar="SECONDS", - dest="checkpoint_interval", - type=int, - default=1800, - action=Highlander, - help="write checkpoint every SECONDS seconds (Default: 1800)", - ) diff --git a/src/borg/testsuite/archiver/delete_cmd.py b/src/borg/testsuite/archiver/delete_cmd.py index e931cc588..f29324b4b 100644 --- a/src/borg/testsuite/archiver/delete_cmd.py +++ b/src/borg/testsuite/archiver/delete_cmd.py @@ -1,13 +1,10 @@ -from ...archive import Archive from ...constants import * # NOQA -from ...manifest import Manifest -from ...repository3 import Repository3 -from . import cmd, create_regular_file, src_file, create_src_archive, generate_archiver_tests, RK_ENCRYPTION +from . import cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA -def test_delete(archivers, request): +def test_delete_options(archivers, request): archiver = request.getfixturevalue(archivers) create_regular_file(archiver.input_path, "file1", size=1024 * 80) create_regular_file(archiver.input_path, "dir2/file2", size=1024 * 80) @@ -17,14 +14,11 @@ def test_delete(archivers, request): cmd(archiver, "create", "test.3", "input") cmd(archiver, "create", "another_test.1", "input") cmd(archiver, "create", "another_test.2", "input") - cmd(archiver, "extract", "test", "--dry-run") - cmd(archiver, "extract", "test.2", "--dry-run") cmd(archiver, "delete", "--match-archives", "sh:another_*") - cmd(archiver, "delete", "--last", "1") + cmd(archiver, "delete", "--last", "1") # test.3 cmd(archiver, "delete", "-a", "test") - cmd(archiver, "extract", "test.2", "--dry-run") - output = cmd(archiver, "delete", "-a", "test.2", "--stats") - assert "Original size: -" in output # negative size == deleted data + cmd(archiver, "extract", "test.2", "--dry-run") # still there? + cmd(archiver, "delete", "-a", "test.2") output = cmd(archiver, "rlist") assert output == "" # no archives left! @@ -35,47 +29,6 @@ def test_delete_multiple(archivers, request): cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "create", "test1", "input") cmd(archiver, "create", "test2", "input") - cmd(archiver, "create", "test3", "input") cmd(archiver, "delete", "-a", "test1") cmd(archiver, "delete", "-a", "test2") - cmd(archiver, "extract", "test3", "--dry-run") - cmd(archiver, "delete", "-a", "test3") assert not cmd(archiver, "rlist") - - -def test_delete_force(archivers, request): - archiver = request.getfixturevalue(archivers) - cmd(archiver, "rcreate", "--encryption=none") - create_src_archive(archiver, "test") - with Repository3(archiver.repository_path, exclusive=True) as repository: - manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - archive = Archive(manifest, "test") - for item in archive.iter_items(): - if item.path.endswith(src_file): - repository.delete(item.chunks[-1].id) - break - else: - assert False # missed the file - repository.commit(compact=False) - output = cmd(archiver, "delete", "-a", "test", "--force") - assert "deleted archive was corrupted" in output - - cmd(archiver, "check", "--repair") - output = cmd(archiver, "rlist") - assert "test" not in output - - -def test_delete_double_force(archivers, request): - archiver = request.getfixturevalue(archivers) - cmd(archiver, "rcreate", "--encryption=none") - create_src_archive(archiver, "test") - with Repository3(archiver.repository_path, exclusive=True) as repository: - manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - archive = Archive(manifest, "test") - id = archive.metadata.items[0] - repository.put(id, b"corrupted items metadata stream chunk") - repository.commit(compact=False) - cmd(archiver, "delete", "-a", "test", "--force", "--force") - cmd(archiver, "check", "--repair") - output = cmd(archiver, "rlist") - assert "test" not in output From d6a70f48f24ab8c459d2ebd5d910ea949087b2d8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Aug 2024 21:54:35 +0200 Subject: [PATCH 12/79] remove LocalCache Note: this is the default cache implementation in borg 1.x, it worked well, but there were some issues: - if the local chunks cache got out of sync with the repository, it needed an expensive rebuild from the infos in all archives. - to optimize that, a local chunks.archive.d cache was used to speed that up, but at the price of quite significant space needs. AdhocCacheWithFiles replaced this with a non-persistent chunks cache, requesting all chunkids from the repository to initialize a simplified non-persistent chunks index, that does not do real refcounting and also initially does not have size information for pre-existing chunks. We want to move away from precise refcounting, LocalCache needs to die. --- docs/changes_1.x.rst | 2 +- docs/faq.rst | 32 -- docs/internals/data-structures.rst | 9 - docs/usage/general/environment.rst.inc | 3 - src/borg/archiver/create_cmd.py | 14 - src/borg/cache.py | 483 +------------------- src/borg/helpers/parseformat.py | 4 +- src/borg/testsuite/archiver/__init__.py | 31 -- src/borg/testsuite/archiver/checks.py | 66 +-- src/borg/testsuite/archiver/corruption.py | 77 +--- src/borg/testsuite/archiver/create_cmd.py | 2 +- src/borg/testsuite/archiver/debug_cmds.py | 8 +- src/borg/testsuite/archiver/recreate_cmd.py | 25 +- src/borg/testsuite/conftest.py | 25 +- 14 files changed, 36 insertions(+), 745 deletions(-) diff --git a/docs/changes_1.x.rst b/docs/changes_1.x.rst index 3366a90b8..714726c1c 100644 --- a/docs/changes_1.x.rst +++ b/docs/changes_1.x.rst @@ -3469,7 +3469,7 @@ Other changes: - archiver tests: add check_cache tool - lints refcounts - fixed cache sync performance regression from 1.1.0b1 onwards, #1940 -- syncing the cache without chunks.archive.d (see :ref:`disable_archive_chunks`) +- syncing the cache without chunks.archive.d now avoids any merges and is thus faster, #1940 - borg check --verify-data: faster due to linear on-disk-order scan - borg debug-xxx commands removed, we use "debug xxx" subcommands now, #1627 diff --git a/docs/faq.rst b/docs/faq.rst index 0daa226ca..edae228ac 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -704,38 +704,6 @@ serialized way in a single script, you need to give them ``--lock-wait N`` (with being a bit more than the time the server needs to terminate broken down connections and release the lock). -.. _disable_archive_chunks: - -The borg cache eats way too much disk space, what can I do? ------------------------------------------------------------ - -This may especially happen if borg needs to rebuild the local "chunks" index - -either because it was removed, or because it was not coherent with the -repository state any more (e.g. because another borg instance changed the -repository). - -To optimize this rebuild process, borg caches per-archive information in the -``chunks.archive.d/`` directory. It won't help the first time it happens, but it -will make the subsequent rebuilds faster (because it needs to transfer less data -from the repository). While being faster, the cache needs quite some disk space, -which might be unwanted. - -You can disable the cached archive chunk indexes by setting the environment -variable ``BORG_USE_CHUNKS_ARCHIVE`` to ``no``. - -This has some pros and cons, though: - -- much less disk space needs for ~/.cache/borg. -- chunk cache resyncs will be slower as it will have to transfer chunk usage - metadata for all archives from the repository (which might be slow if your - repo connection is slow) and it will also have to build the hashtables from - that data. - chunk cache resyncs happen e.g. if your repo was written to by another - machine (if you share same backup repo between multiple machines) or if - your local chunks cache was lost somehow. - -The long term plan to improve this is called "borgception", see :issue:`474`. - Can I back up my root partition (/) with Borg? ---------------------------------------------- diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 8d1562ff2..7237306bc 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -1134,7 +1134,6 @@ The *digests* key contains a mapping of part names to their digests. Integrity data is generally stored by the upper layers, introduced below. An exception is the DetachedIntegrityCheckedFile, which automatically writes and reads it from a ".integrity" file next to the data file. -It is used for archive chunks indexes in chunks.archive.d. Upper layer ~~~~~~~~~~~ @@ -1182,14 +1181,6 @@ easy to tell whether the checksums concern the current state of the cache. Integrity errors are fatal in these files, terminating the program, and are not automatically corrected at this time. -.. rubric:: chunks.archive.d - -Indices in chunks.archive.d are not transacted and use DetachedIntegrityCheckedFile, -which writes the integrity data to a separate ".integrity" file. - -Integrity errors result in deleting the affected index and rebuilding it. -This logs a warning and increases the exit code to WARNING (1). - .. _integrity_repo: .. rubric:: Repository index and hints diff --git a/docs/usage/general/environment.rst.inc b/docs/usage/general/environment.rst.inc index cd89f8d50..907cd28a3 100644 --- a/docs/usage/general/environment.rst.inc +++ b/docs/usage/general/environment.rst.inc @@ -88,9 +88,6 @@ General: BORG_CACHE_IMPL Choose the implementation for the clientside cache, choose one of: - - ``local``: uses a persistent chunks cache and keeps it in a perfect state (precise refcounts and - sizes), requiring a potentially resource expensive cache sync in multi-client scenarios. - Also has a persistent files cache. - ``adhoc``: builds a non-persistent chunks cache by querying the repo. Chunks cache contents are somewhat sloppy for already existing chunks, concerning their refcount ("infinite") and size (0). No files cache (slow, will chunk all input files). DEPRECATED. diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 40160f641..9930bc290 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -224,8 +224,6 @@ def create_inner(archive, cache, fso): manifest, progress=args.progress, lock_wait=self.lock_wait, - no_cache_sync_permitted=args.no_cache_sync, - no_cache_sync_forced=args.no_cache_sync_forced, prefer_adhoc_cache=args.prefer_adhoc_cache, cache_mode=args.files_cache_mode, iec=args.iec, @@ -799,18 +797,6 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): help="only display items with the given status characters (see description)", ) subparser.add_argument("--json", action="store_true", help="output stats as JSON. Implies ``--stats``.") - subparser.add_argument( - "--no-cache-sync", - dest="no_cache_sync", - action="store_true", - help="experimental: do not synchronize the chunks cache.", - ) - subparser.add_argument( - "--no-cache-sync-forced", - dest="no_cache_sync_forced", - action="store_true", - help="experimental: do not synchronize the chunks cache (forced).", - ) subparser.add_argument( "--prefer-adhoc-cache", dest="prefer_adhoc_cache", diff --git a/src/borg/cache.py b/src/borg/cache.py index 31c4df0b3..0fc38283f 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -12,26 +12,22 @@ files_cache_logger = create_logger("borg.debug.files_cache") from .constants import CACHE_README, FILES_CACHE_MODE_DISABLED, ROBJ_FILE_STREAM -from .hashindex import ChunkIndex, ChunkIndexEntry, CacheSynchronizer +from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import Error from .helpers import get_cache_dir, get_security_dir -from .helpers import bin_to_hex, hex_to_bin, parse_stringified_list +from .helpers import hex_to_bin, parse_stringified_list from .helpers import format_file_size from .helpers import safe_ns from .helpers import yes -from .helpers import remove_surrogates -from .helpers import ProgressIndicatorPercent, ProgressIndicatorMessage -from .helpers import set_ec, EXIT_WARNING -from .helpers import safe_unlink +from .helpers import ProgressIndicatorMessage from .helpers import msgpack from .helpers.msgpack import int_to_timestamp, timestamp_to_int -from .item import ArchiveItem, ChunkListEntry +from .item import ChunkListEntry from .crypto.key import PlaintextKey -from .crypto.file_integrity import IntegrityCheckedFile, DetachedIntegrityCheckedFile, FileIntegrityError +from .crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError from .locking import Lock from .manifest import Manifest from .platform import SaveFile -from .remote import cache_if_remote from .remote3 import RemoteRepository3 from .repository3 import LIST_SCAN_LIMIT, Repository3 @@ -355,24 +351,10 @@ def __new__( warn_if_unencrypted=True, progress=False, lock_wait=None, - no_cache_sync_permitted=False, - no_cache_sync_forced=False, prefer_adhoc_cache=False, cache_mode=FILES_CACHE_MODE_DISABLED, iec=False, ): - def local(): - return LocalCache( - manifest=manifest, - path=path, - sync=sync, - warn_if_unencrypted=warn_if_unencrypted, - progress=progress, - iec=iec, - lock_wait=lock_wait, - cache_mode=cache_mode, - ) - def adhocwithfiles(): return AdHocWithFilesCache( manifest=manifest, @@ -389,38 +371,14 @@ def adhoc(): impl = get_cache_impl() if impl != "cli": - methods = dict(local=local, adhocwithfiles=adhocwithfiles, adhoc=adhoc) + methods = dict(adhocwithfiles=adhocwithfiles, adhoc=adhoc) try: method = methods[impl] except KeyError: raise RuntimeError("Unknown BORG_CACHE_IMPL value: %s" % impl) return method() - if no_cache_sync_forced: - return adhoc() if prefer_adhoc_cache else adhocwithfiles() - - if not no_cache_sync_permitted: - return local() - - # no cache sync may be permitted, but if the local cache is in sync it'd be stupid to invalidate - # it by needlessly using the AdHocCache or the AdHocWithFilesCache. - # Check if the local cache exists and is in sync. - - cache_config = CacheConfig(repository, path, lock_wait) - if cache_config.exists(): - with cache_config: - cache_in_sync = cache_config.manifest_id == manifest.id - # Don't nest cache locks - if cache_in_sync: - # Local cache is in sync, use it - logger.debug("Cache: choosing local cache (in sync)") - return local() - if prefer_adhoc_cache: # adhoc cache, without files cache - logger.debug("Cache: choosing AdHocCache (local cache does not exist or is not in sync)") - return adhoc() - else: - logger.debug("Cache: choosing AdHocWithFilesCache (local cache does not exist or is not in sync)") - return adhocwithfiles() + return adhoc() if prefer_adhoc_cache else adhocwithfiles() class CacheStatsMixin: @@ -671,15 +629,7 @@ def seen_chunk(self, id, size=None): entry = self.chunks.get(id, ChunkIndexEntry(0, None)) if entry.refcount and size is not None: assert isinstance(entry.size, int) - if entry.size: - # LocalCache: has existing size information and uses *size* to make an effort at detecting collisions. - if size != entry.size: - # we already have a chunk with that id, but different size. - # this is either a hash collision (unlikely) or corruption or a bug. - raise Exception( - "chunk has same id [%r], but different size (stored: %d new: %d)!" % (id, entry.size, size) - ) - else: + if not entry.size: # AdHocWithFilesCache / AdHocCache: # Here *size* is used to update the chunk's size information, which will be zero for existing chunks. self.chunks[id] = entry._replace(size=size) @@ -737,7 +687,7 @@ def _load_chunks_from_repo(self): for id_ in result: num_chunks += 1 chunks[id_] = init_entry - # LocalCache does not contain the manifest, either. + # Cache does not contain the manifest. if not isinstance(self.repository, (Repository3, RemoteRepository3)): del chunks[self.manifest.MANIFEST_ID] duration = perf_counter() - t0 or 0.01 @@ -753,413 +703,6 @@ def _load_chunks_from_repo(self): return chunks -class LocalCache(CacheStatsMixin, FilesCacheMixin, ChunksMixin): - """ - Persistent, local (client-side) cache. - """ - - def __init__( - self, - manifest, - path=None, - sync=True, - warn_if_unencrypted=True, - progress=False, - lock_wait=None, - cache_mode=FILES_CACHE_MODE_DISABLED, - iec=False, - ): - """ - :param warn_if_unencrypted: print warning if accessing unknown unencrypted repository - :param lock_wait: timeout for lock acquisition (int [s] or None [wait forever]) - :param sync: do :meth:`.sync` - :param cache_mode: what shall be compared in the file stat infos vs. cached stat infos comparison - """ - CacheStatsMixin.__init__(self, iec=iec) - FilesCacheMixin.__init__(self, cache_mode) - assert isinstance(manifest, Manifest) - self.manifest = manifest - self.repository = manifest.repository - self.key = manifest.key - self.repo_objs = manifest.repo_objs - self.progress = progress - self._txn_active = False - self.do_cache = os.environ.get("BORG_USE_CHUNKS_ARCHIVE", "yes").lower() in ["yes", "1", "true"] - - self.path = cache_dir(self.repository, path) - self.security_manager = SecurityManager(self.repository) - self.cache_config = CacheConfig(self.repository, self.path, lock_wait) - - # Warn user before sending data to a never seen before unencrypted repository - if not os.path.exists(self.path): - self.security_manager.assert_access_unknown(warn_if_unencrypted, manifest, self.key) - self.create() - - try: - self.open() - except (FileNotFoundError, FileIntegrityError): - self.wipe_cache() - self.open() - - try: - self.security_manager.assert_secure(manifest, self.key) - - if not self.check_cache_compatibility(): - self.wipe_cache() - - self.update_compatibility() - - if sync and self.manifest.id != self.cache_config.manifest_id: - self.sync() - self.commit() - except: # noqa - self.close() - raise - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - def create(self): - """Create a new empty cache at `self.path`""" - os.makedirs(self.path) - with open(os.path.join(self.path, "README"), "w") as fd: - fd.write(CACHE_README) - self.cache_config.create() - ChunkIndex().write(os.path.join(self.path, "chunks")) - os.makedirs(os.path.join(self.path, "chunks.archive.d")) - self._create_empty_files_cache(self.path) - - def _do_open(self): - self.cache_config.load() - with IntegrityCheckedFile( - path=os.path.join(self.path, "chunks"), - write=False, - integrity_data=self.cache_config.integrity.get("chunks"), - ) as fd: - self.chunks = ChunkIndex.read(fd) - self._read_files_cache() - - def open(self): - if not os.path.isdir(self.path): - raise Exception("%s Does not look like a Borg cache" % self.path) - self.cache_config.open() - self.rollback() - - def close(self): - if self.cache_config is not None: - self.cache_config.close() - self.cache_config = None - - def begin_txn(self): - # Initialize transaction snapshot - pi = ProgressIndicatorMessage(msgid="cache.begin_transaction") - txn_dir = os.path.join(self.path, "txn.tmp") - os.mkdir(txn_dir) - pi.output("Initializing cache transaction: Reading config") - shutil.copy(os.path.join(self.path, "config"), txn_dir) - pi.output("Initializing cache transaction: Reading chunks") - shutil.copy(os.path.join(self.path, "chunks"), txn_dir) - pi.output("Initializing cache transaction: Reading files") - try: - shutil.copy(os.path.join(self.path, self.files_cache_name()), txn_dir) - except FileNotFoundError: - self._create_empty_files_cache(txn_dir) - os.replace(txn_dir, os.path.join(self.path, "txn.active")) - self._txn_active = True - pi.finish() - - def commit(self): - """Commit transaction""" - if not self._txn_active: - return - self.security_manager.save(self.manifest, self.key) - pi = ProgressIndicatorMessage(msgid="cache.commit") - if self.files is not None: - pi.output("Saving files cache") - integrity_data = self._write_files_cache() - self.cache_config.integrity[self.files_cache_name()] = integrity_data - pi.output("Saving chunks cache") - with IntegrityCheckedFile(path=os.path.join(self.path, "chunks"), write=True) as fd: - self.chunks.write(fd) - self.cache_config.integrity["chunks"] = fd.integrity_data - pi.output("Saving cache config") - self.cache_config.save(self.manifest) - os.replace(os.path.join(self.path, "txn.active"), os.path.join(self.path, "txn.tmp")) - shutil.rmtree(os.path.join(self.path, "txn.tmp")) - self._txn_active = False - pi.finish() - - def rollback(self): - """Roll back partial and aborted transactions""" - # Remove partial transaction - if os.path.exists(os.path.join(self.path, "txn.tmp")): - shutil.rmtree(os.path.join(self.path, "txn.tmp")) - # Roll back active transaction - txn_dir = os.path.join(self.path, "txn.active") - if os.path.exists(txn_dir): - shutil.copy(os.path.join(txn_dir, "config"), self.path) - shutil.copy(os.path.join(txn_dir, "chunks"), self.path) - shutil.copy(os.path.join(txn_dir, self.discover_files_cache_name(txn_dir)), self.path) - txn_tmp = os.path.join(self.path, "txn.tmp") - os.replace(txn_dir, txn_tmp) - if os.path.exists(txn_tmp): - shutil.rmtree(txn_tmp) - self._txn_active = False - self._do_open() - - def sync(self): - """Re-synchronize chunks cache with repository. - - Maintains a directory with known backup archive indexes, so it only - needs to fetch infos from repo and build a chunk index once per backup - archive. - If out of sync, missing archive indexes get added, outdated indexes - get removed and a new master chunks index is built by merging all - archive indexes. - """ - archive_path = os.path.join(self.path, "chunks.archive.d") - # Instrumentation - processed_item_metadata_bytes = 0 - processed_item_metadata_chunks = 0 - compact_chunks_archive_saved_space = 0 - - def mkpath(id, suffix=""): - id_hex = bin_to_hex(id) - path = os.path.join(archive_path, id_hex + suffix) - return path - - def cached_archives(): - if self.do_cache: - fns = os.listdir(archive_path) - # filenames with 64 hex digits == 256bit, - # or compact indices which are 64 hex digits + ".compact" - return {hex_to_bin(fn) for fn in fns if len(fn) == 64} | { - hex_to_bin(fn[:64]) for fn in fns if len(fn) == 72 and fn.endswith(".compact") - } - else: - return set() - - def repo_archives(): - return {info.id for info in self.manifest.archives.list()} - - def cleanup_outdated(ids): - for id in ids: - cleanup_cached_archive(id) - - def cleanup_cached_archive(id, cleanup_compact=True): - try: - os.unlink(mkpath(id)) - os.unlink(mkpath(id) + ".integrity") - except FileNotFoundError: - pass - if not cleanup_compact: - return - try: - os.unlink(mkpath(id, suffix=".compact")) - os.unlink(mkpath(id, suffix=".compact") + ".integrity") - except FileNotFoundError: - pass - - def fetch_and_build_idx(archive_id, decrypted_repository, chunk_idx): - nonlocal processed_item_metadata_bytes - nonlocal processed_item_metadata_chunks - csize, data = decrypted_repository.get(archive_id) - chunk_idx.add(archive_id, 1, len(data)) - archive = self.key.unpack_archive(data) - archive = ArchiveItem(internal_dict=archive) - if archive.version not in (1, 2): # legacy - raise Exception("Unknown archive metadata version") - if archive.version == 1: - items = archive.items - elif archive.version == 2: - items = [] - for chunk_id, (csize, data) in zip(archive.item_ptrs, decrypted_repository.get_many(archive.item_ptrs)): - chunk_idx.add(chunk_id, 1, len(data)) - ids = msgpack.unpackb(data) - items.extend(ids) - sync = CacheSynchronizer(chunk_idx) - for item_id, (csize, data) in zip(items, decrypted_repository.get_many(items)): - chunk_idx.add(item_id, 1, len(data)) - processed_item_metadata_bytes += len(data) - processed_item_metadata_chunks += 1 - sync.feed(data) - if self.do_cache: - write_archive_index(archive_id, chunk_idx) - - def write_archive_index(archive_id, chunk_idx): - nonlocal compact_chunks_archive_saved_space - compact_chunks_archive_saved_space += chunk_idx.compact() - fn = mkpath(archive_id, suffix=".compact") - fn_tmp = mkpath(archive_id, suffix=".tmp") - try: - with DetachedIntegrityCheckedFile( - path=fn_tmp, write=True, filename=bin_to_hex(archive_id) + ".compact" - ) as fd: - chunk_idx.write(fd) - except Exception: - safe_unlink(fn_tmp) - else: - os.replace(fn_tmp, fn) - - def read_archive_index(archive_id, archive_name): - archive_chunk_idx_path = mkpath(archive_id) - logger.info("Reading cached archive chunk index for %s", archive_name) - try: - try: - # Attempt to load compact index first - with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path + ".compact", write=False) as fd: - archive_chunk_idx = ChunkIndex.read(fd, permit_compact=True) - # In case a non-compact index exists, delete it. - cleanup_cached_archive(archive_id, cleanup_compact=False) - # Compact index read - return index, no conversion necessary (below). - return archive_chunk_idx - except FileNotFoundError: - # No compact index found, load non-compact index, and convert below. - with DetachedIntegrityCheckedFile(path=archive_chunk_idx_path, write=False) as fd: - archive_chunk_idx = ChunkIndex.read(fd) - except FileIntegrityError as fie: - logger.error("Cached archive chunk index of %s is corrupted: %s", archive_name, fie) - # Delete corrupted index, set warning. A new index must be build. - cleanup_cached_archive(archive_id) - set_ec(EXIT_WARNING) - return None - - # Convert to compact index. Delete the existing index first. - logger.debug("Found non-compact index for %s, converting to compact.", archive_name) - cleanup_cached_archive(archive_id) - write_archive_index(archive_id, archive_chunk_idx) - return archive_chunk_idx - - def get_archive_ids_to_names(archive_ids): - # Pass once over all archives and build a mapping from ids to names. - # The easier approach, doing a similar loop for each archive, has - # square complexity and does about a dozen million functions calls - # with 1100 archives (which takes 30s CPU seconds _alone_). - archive_names = {} - for info in self.manifest.archives.list(): - if info.id in archive_ids: - archive_names[info.id] = info.name - assert len(archive_names) == len(archive_ids) - return archive_names - - def create_master_idx(chunk_idx): - logger.debug("Synchronizing chunks index...") - cached_ids = cached_archives() - archive_ids = repo_archives() - logger.info( - "Cached archive chunk indexes: %d fresh, %d stale, %d need fetching.", - len(archive_ids & cached_ids), - len(cached_ids - archive_ids), - len(archive_ids - cached_ids), - ) - # deallocates old hashindex, creates empty hashindex: - chunk_idx.clear() - cleanup_outdated(cached_ids - archive_ids) - # Explicitly set the usable initial hash table capacity to avoid performance issues - # due to hash table "resonance". - master_index_capacity = len(self.repository) - if archive_ids: - chunk_idx = None if not self.do_cache else ChunkIndex(usable=master_index_capacity) - pi = ProgressIndicatorPercent( - total=len(archive_ids), - step=0.1, - msg="%3.0f%% Syncing chunks index. Processing archive %s.", - msgid="cache.sync", - ) - archive_ids_to_names = get_archive_ids_to_names(archive_ids) - for archive_id, archive_name in archive_ids_to_names.items(): - pi.show(info=[remove_surrogates(archive_name)]) # legacy. borg2 always has pure unicode arch names. - if self.do_cache: - if archive_id in cached_ids: - archive_chunk_idx = read_archive_index(archive_id, archive_name) - if archive_chunk_idx is None: - cached_ids.remove(archive_id) - if archive_id not in cached_ids: - # Do not make this an else branch; the FileIntegrityError exception handler - # above can remove *archive_id* from *cached_ids*. - logger.info("Fetching and building archive index for %s.", archive_name) - archive_chunk_idx = ChunkIndex() - fetch_and_build_idx(archive_id, decrypted_repository, archive_chunk_idx) - logger.debug("Merging into master chunks index.") - chunk_idx.merge(archive_chunk_idx) - else: - chunk_idx = chunk_idx or ChunkIndex(usable=master_index_capacity) - logger.info("Fetching archive index for %s.", archive_name) - fetch_and_build_idx(archive_id, decrypted_repository, chunk_idx) - pi.finish() - logger.debug( - "Chunks index sync: processed %s (%d chunks) of metadata.", - format_file_size(processed_item_metadata_bytes), - processed_item_metadata_chunks, - ) - logger.debug( - "Chunks index sync: compact chunks.archive.d storage saved %s bytes.", - format_file_size(compact_chunks_archive_saved_space), - ) - logger.debug("Chunks index sync done.") - return chunk_idx - - # The cache can be used by a command that e.g. only checks against Manifest.Operation.WRITE, - # which does not have to include all flags from Manifest.Operation.READ. - # Since the sync will attempt to read archives, check compatibility with Manifest.Operation.READ. - self.manifest.check_repository_compatibility((Manifest.Operation.READ,)) - - self.begin_txn() - with cache_if_remote(self.repository, decrypted_cache=self.repo_objs) as decrypted_repository: - self.chunks = create_master_idx(self.chunks) - - def check_cache_compatibility(self): - my_features = Manifest.SUPPORTED_REPO_FEATURES - if self.cache_config.ignored_features & my_features: - # The cache might not contain references of chunks that need a feature that is mandatory for some operation - # and which this version supports. To avoid corruption while executing that operation force rebuild. - return False - if not self.cache_config.mandatory_features <= my_features: - # The cache was build with consideration to at least one feature that this version does not understand. - # This client might misinterpret the cache. Thus force a rebuild. - return False - return True - - def wipe_cache(self): - logger.warning("Discarding incompatible or corrupted cache and forcing a cache rebuild") - archive_path = os.path.join(self.path, "chunks.archive.d") - if os.path.isdir(archive_path): - shutil.rmtree(os.path.join(self.path, "chunks.archive.d")) - os.makedirs(os.path.join(self.path, "chunks.archive.d")) - self.chunks = ChunkIndex() - with IntegrityCheckedFile(path=os.path.join(self.path, "chunks"), write=True) as fd: - self.chunks.write(fd) - self.cache_config.integrity["chunks"] = fd.integrity_data - integrity_data = self._create_empty_files_cache(self.path) - self.cache_config.integrity[self.files_cache_name()] = integrity_data - self.cache_config.manifest_id = "" - self.cache_config._config.set("cache", "manifest", "") - if not self.cache_config._config.has_section("integrity"): - self.cache_config._config.add_section("integrity") - for file, integrity_data in self.cache_config.integrity.items(): - self.cache_config._config.set("integrity", file, integrity_data) - # This is needed to pass the integrity check later on inside CacheConfig.load() - self.cache_config._config.set("integrity", "manifest", "") - - self.cache_config.ignored_features = set() - self.cache_config.mandatory_features = set() - with SaveFile(self.cache_config.config_path) as fd: - self.cache_config._config.write(fd) - - def update_compatibility(self): - operation_to_features_map = self.manifest.get_all_mandatory_features() - my_features = Manifest.SUPPORTED_REPO_FEATURES - repo_features = set() - for operation, features in operation_to_features_map.items(): - repo_features.update(features) - - self.cache_config.ignored_features.update(repo_features - my_features) - self.cache_config.mandatory_features.update(repo_features & my_features) - - class AdHocWithFilesCache(CacheStatsMixin, FilesCacheMixin, ChunksMixin): """ Like AdHocCache, but with a files cache. @@ -1326,10 +869,10 @@ class AdHocCache(CacheStatsMixin, ChunksMixin): """ Ad-hoc, non-persistent cache. - Compared to the standard LocalCache the AdHocCache does not maintain accurate reference count, - nor does it provide a files cache (which would require persistence). Chunks that were not added - during the current AdHocCache lifetime won't have correct size set (0 bytes) and will - have an infinite reference count (MAX_VALUE). + The AdHocCache does not maintain accurate reference count, nor does it provide a files cache + (which would require persistence). + Chunks that were not added during the current AdHocCache lifetime won't have correct size set + (0 bytes) and will have an infinite reference count (MAX_VALUE). """ str_format = """\ diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 774ec8b4d..aabb5e288 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1186,13 +1186,13 @@ def default(self, o): from ..remote import RemoteRepository from ..remote3 import RemoteRepository3 from ..archive import Archive - from ..cache import LocalCache, AdHocCache, AdHocWithFilesCache + from ..cache import AdHocCache, AdHocWithFilesCache if isinstance(o, (Repository, RemoteRepository)) or isinstance(o, (Repository3, RemoteRepository3)): return {"id": bin_to_hex(o.id), "location": o._location.canonical_path()} if isinstance(o, Archive): return o.info() - if isinstance(o, (LocalCache, AdHocWithFilesCache)): + if isinstance(o, (AdHocWithFilesCache, )): return {"path": o.path, "stats": o.stats()} if isinstance(o, AdHocCache): return {"stats": o.stats()} diff --git a/src/borg/testsuite/archiver/__init__.py b/src/borg/testsuite/archiver/__init__.py index b8da0ea1d..7f0d63d25 100644 --- a/src/borg/testsuite/archiver/__init__.py +++ b/src/borg/testsuite/archiver/__init__.py @@ -8,7 +8,6 @@ import sys import tempfile import time -from configparser import ConfigParser from contextlib import contextmanager from datetime import datetime from io import BytesIO, StringIO @@ -18,11 +17,9 @@ from ... import xattr, platform from ...archive import Archive from ...archiver import Archiver, PURE_PYTHON_MSGPACK_WARNING -from ...cache import Cache, LocalCache from ...constants import * # NOQA from ...helpers import Location, umount from ...helpers import EXIT_SUCCESS -from ...helpers import bin_to_hex from ...helpers import init_ec_warnings from ...logger import flush_logging from ...manifest import Manifest @@ -344,34 +341,6 @@ def _assert_test_keep_tagged(archiver): assert sorted(os.listdir("output/input/taggedall")), [".NOBACKUP1", ".NOBACKUP2", CACHE_TAG_NAME] -def check_cache(archiver): - # First run a regular borg check - cmd(archiver, "check") - # Then check that the cache on disk matches exactly what's in the repo. - with open_repository(archiver) as repository: - manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - with Cache(repository, manifest, sync=False) as cache: - original_chunks = cache.chunks - # the LocalCache implementation has an on-disk chunks cache, - # but AdHocWithFilesCache and AdHocCache don't have persistent chunks cache. - persistent = isinstance(cache, LocalCache) - Cache.destroy(repository) - with Cache(repository, manifest) as cache: - correct_chunks = cache.chunks - if not persistent: - # there is no point in doing the checks - return - assert original_chunks is not correct_chunks - seen = set() - for id, (refcount, size) in correct_chunks.iteritems(): - o_refcount, o_size = original_chunks[id] - assert refcount == o_refcount - assert size == o_size - seen.add(id) - for id, (refcount, size) in original_chunks.iteritems(): - assert id in seen - - @contextmanager def assert_creates_file(path): assert not os.path.exists(path), f"{path} should not exist" diff --git a/src/borg/testsuite/archiver/checks.py b/src/borg/testsuite/archiver/checks.py index 8f5dd144a..3eb61e14d 100644 --- a/src/borg/testsuite/archiver/checks.py +++ b/src/borg/testsuite/archiver/checks.py @@ -4,7 +4,7 @@ import pytest -from ...cache import Cache, LocalCache, get_cache_impl +from ...cache import Cache from ...constants import * # NOQA from ...helpers import Location, get_security_dir, bin_to_hex from ...helpers import EXIT_ERROR @@ -13,7 +13,7 @@ from ...repository3 import Repository3 from .. import llfuse from .. import changedir -from . import cmd, _extract_repository_id, open_repository, check_cache, create_test_files +from . import cmd, _extract_repository_id, create_test_files from . import _set_repository_id, create_regular_file, assert_creates_file, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote") # NOQA @@ -204,17 +204,6 @@ def test_unknown_feature_on_create(archivers, request): cmd_raises_unknown_feature(archiver, ["create", "test", "input"]) -@pytest.mark.skipif(get_cache_impl() in ("adhocwithfiles", "adhoc"), reason="only works with LocalCache") -def test_unknown_feature_on_cache_sync(archivers, request): - # LocalCache.sync checks repo compat - archiver = request.getfixturevalue(archivers) - cmd(archiver, "rcreate", RK_ENCRYPTION) - # delete the cache to trigger a cache sync later in borg create - cmd(archiver, "rdelete", "--cache-only") - add_unknown_feature(archiver.repository_path, Manifest.Operation.READ) - cmd_raises_unknown_feature(archiver, ["create", "test", "input"]) - - def test_unknown_feature_on_change_passphrase(archivers, request): archiver = request.getfixturevalue(archivers) print(cmd(archiver, "rcreate", RK_ENCRYPTION)) @@ -266,7 +255,6 @@ def test_unknown_feature_on_mount(archivers, request): cmd_raises_unknown_feature(archiver, ["mount", mountpoint]) -@pytest.mark.allow_cache_wipe def test_unknown_mandatory_feature_in_cache(archivers, request): archiver = request.getfixturevalue(archivers) remote_repo = archiver.get_kind() == "remote" @@ -277,27 +265,12 @@ def test_unknown_mandatory_feature_in_cache(archivers, request): repository._location = Location(archiver.repository_location) manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) with Cache(repository, manifest) as cache: - is_localcache = isinstance(cache, LocalCache) cache.begin_txn() cache.cache_config.mandatory_features = {"unknown-feature"} cache.commit() if archiver.FORK_DEFAULT: cmd(archiver, "create", "test", "input") - else: - called = False - wipe_cache_safe = LocalCache.wipe_cache - - def wipe_wrapper(*args): - nonlocal called - called = True - wipe_cache_safe(*args) - - with patch.object(LocalCache, "wipe_cache", wipe_wrapper): - cmd(archiver, "create", "test", "input") - - if is_localcache: - assert called with Repository3(archiver.repository_path, exclusive=True) as repository: if remote_repo: @@ -307,41 +280,6 @@ def wipe_wrapper(*args): assert cache.cache_config.mandatory_features == set() -def test_check_cache(archivers, request): - archiver = request.getfixturevalue(archivers) - cmd(archiver, "rcreate", RK_ENCRYPTION) - cmd(archiver, "create", "test", "input") - with open_repository(archiver) as repository: - manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - with Cache(repository, manifest, sync=False) as cache: - cache.begin_txn() - cache.chunks.incref(list(cache.chunks.iteritems())[0][0]) - cache.commit() - persistent = isinstance(cache, LocalCache) - if not persistent: - pytest.skip("check_cache is pointless if we do not have a persistent chunks cache") - with pytest.raises(AssertionError): - check_cache(archiver) - - -@pytest.mark.skipif(get_cache_impl() in ("adhocwithfiles", "adhoc"), reason="only works with LocalCache") -def test_env_use_chunks_archive(archivers, request, monkeypatch): - archiver = request.getfixturevalue(archivers) - create_test_files(archiver.input_path) - monkeypatch.setenv("BORG_USE_CHUNKS_ARCHIVE", "no") - cmd(archiver, "rcreate", RK_ENCRYPTION) - repository_id = bin_to_hex(_extract_repository_id(archiver.repository_path)) - cache_path = os.path.join(archiver.cache_path, repository_id) - cmd(archiver, "create", "test", "input") - assert os.path.exists(cache_path) - assert os.path.exists(os.path.join(cache_path, "chunks.archive.d")) - assert len(os.listdir(os.path.join(cache_path, "chunks.archive.d"))) == 0 - cmd(archiver, "rdelete", "--cache-only") - monkeypatch.setenv("BORG_USE_CHUNKS_ARCHIVE", "yes") - cmd(archiver, "create", "test2", "input") - assert len(os.listdir(os.path.join(cache_path, "chunks.archive.d"))) > 0 - - # Begin Remote Tests def test_remote_repo_restrict_to_path(remote_archiver): original_location, repo_path = remote_archiver.repository_location, remote_archiver.repository_path diff --git a/src/borg/testsuite/archiver/corruption.py b/src/borg/testsuite/archiver/corruption.py index 3df1789d1..7bd4c55f7 100644 --- a/src/borg/testsuite/archiver/corruption.py +++ b/src/borg/testsuite/archiver/corruption.py @@ -2,15 +2,12 @@ import json import os from configparser import ConfigParser -from unittest.mock import patch import pytest from ...constants import * # NOQA -from ...helpers import bin_to_hex, Error -from . import cmd, create_src_archive, create_test_files, RK_ENCRYPTION -from ...hashindex import ChunkIndex -from ...cache import LocalCache +from ...helpers import bin_to_hex +from . import cmd, create_test_files, RK_ENCRYPTION def corrupt_archiver(archiver): @@ -27,40 +24,6 @@ def corrupt(file, amount=1): fd.write(corrupted) -@pytest.mark.allow_cache_wipe -def test_cache_chunks(archiver): - corrupt_archiver(archiver) - if archiver.cache_path is None: - pytest.skip("no cache path for this kind of Cache implementation") - - create_src_archive(archiver, "test") - chunks_path = os.path.join(archiver.cache_path, "chunks") - if not os.path.exists(chunks_path): - pytest.skip("no persistent chunks index for this kind of Cache implementation") - - chunks_before_corruption = set(ChunkIndex(path=chunks_path).iteritems()) - - corrupt(chunks_path) - - assert not archiver.FORK_DEFAULT # test does not support forking - - chunks_in_memory = None - sync_chunks = LocalCache.sync - - def sync_wrapper(cache): - nonlocal chunks_in_memory - sync_chunks(cache) - chunks_in_memory = set(cache.chunks.iteritems()) - - with patch.object(LocalCache, "sync", sync_wrapper): - out = cmd(archiver, "rinfo") - - assert chunks_in_memory == chunks_before_corruption - assert "forcing a cache rebuild" in out - chunks_after_repair = set(ChunkIndex(path=chunks_path).iteritems()) - assert chunks_after_repair == chunks_before_corruption - - def test_cache_files(archiver): corrupt_archiver(archiver) if archiver.cache_path is None: @@ -73,42 +36,6 @@ def test_cache_files(archiver): assert "files cache is corrupted" in out -def test_chunks_archive(archiver): - corrupt_archiver(archiver) - if archiver.cache_path is None: - pytest.skip("no cache path for this kind of Cache implementation") - - cmd(archiver, "create", "test1", "input") - # Find ID of test1, so we can corrupt it later :) - target_id = cmd(archiver, "rlist", "--format={id}{NL}").strip() - cmd(archiver, "create", "test2", "input") - - # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d - cmd(archiver, "rdelete", "--cache-only") - cmd(archiver, "rinfo", "--json") - - chunks_archive = os.path.join(archiver.cache_path, "chunks.archive.d") - if not os.path.exists(chunks_archive): - pytest.skip("Only LocalCache has a per-archive chunks index cache.") - assert len(os.listdir(chunks_archive)) == 4 # two archives, one chunks cache and one .integrity file each - - corrupt(os.path.join(chunks_archive, target_id + ".compact")) - - # Trigger cache sync by changing the manifest ID in the cache config - config_path = os.path.join(archiver.cache_path, "config") - config = ConfigParser(interpolation=None) - config.read(config_path) - config.set("cache", "manifest", bin_to_hex(bytes(32))) - with open(config_path, "w") as fd: - config.write(fd) - - # Cache sync notices corrupted archive chunks, but automatically recovers. - out = cmd(archiver, "create", "-v", "test3", "input", exit_code=1) - assert "Reading cached archive chunk index for test1" in out - assert "Cached archive chunk index of test1 is corrupted" in out - assert "Fetching and building archive index for test1" in out - - def test_old_version_interfered(archiver): corrupt_archiver(archiver) if archiver.cache_path is None: diff --git a/src/borg/testsuite/archiver/create_cmd.py b/src/borg/testsuite/archiver/create_cmd.py index 4f740abfa..0181888d8 100644 --- a/src/borg/testsuite/archiver/create_cmd.py +++ b/src/borg/testsuite/archiver/create_cmd.py @@ -550,7 +550,7 @@ def test_create_pattern_intermediate_folders_first(archivers, request): assert out_list.index("d x/b") < out_list.index("- x/b/foo_b") -@pytest.mark.skipif(get_cache_impl() in ("adhocwithfiles", "local"), reason="only works with AdHocCache") +@pytest.mark.skipif(get_cache_impl() != "adhoc", reason="only works with AdHocCache") def test_create_no_cache_sync_adhoc(archivers, request): # TODO: add test for AdHocWithFilesCache archiver = request.getfixturevalue(archivers) create_test_files(archiver.input_path) diff --git a/src/borg/testsuite/archiver/debug_cmds.py b/src/borg/testsuite/archiver/debug_cmds.py index 3923871a5..2e105fedd 100644 --- a/src/borg/testsuite/archiver/debug_cmds.py +++ b/src/borg/testsuite/archiver/debug_cmds.py @@ -168,12 +168,8 @@ def test_debug_refcount_obj(archivers, request): create_json = json.loads(cmd(archiver, "create", "--json", "test", "input")) archive_id = create_json["archive"]["id"] output = cmd(archiver, "debug", "refcount-obj", archive_id).strip() - # LocalCache does precise refcounting, so we'll get 1 reference for the archive. - # AdHocCache or AdHocWithFilesCache doesn't, we'll get ChunkIndex.MAX_VALUE as refcount. - assert ( - output == f"object {archive_id} has 1 referrers [info from chunks cache]." - or output == f"object {archive_id} has 4294966271 referrers [info from chunks cache]." - ) + # AdHocCache or AdHocWithFilesCache don't do precise refcounting, we'll get ChunkIndex.MAX_VALUE as refcount. + assert output == f"object {archive_id} has 4294966271 referrers [info from chunks cache]." # Invalid IDs do not abort or return an error output = cmd(archiver, "debug", "refcount-obj", "124", "xyza").strip() diff --git a/src/borg/testsuite/archiver/recreate_cmd.py b/src/borg/testsuite/archiver/recreate_cmd.py index b21a73fc8..d9c119961 100644 --- a/src/borg/testsuite/archiver/recreate_cmd.py +++ b/src/borg/testsuite/archiver/recreate_cmd.py @@ -16,7 +16,6 @@ _assert_test_keep_tagged, _extract_hardlinks_setup, generate_archiver_tests, - check_cache, cmd, create_regular_file, create_test_files, @@ -96,12 +95,12 @@ def test_recreate_target(archivers, request): archiver = request.getfixturevalue(archivers) create_test_files(archiver.input_path) cmd(archiver, "rcreate", RK_ENCRYPTION) - check_cache(archiver) + cmd(archiver, "check") cmd(archiver, "create", "test0", "input") - check_cache(archiver) + cmd(archiver, "check") original_archive = cmd(archiver, "rlist") cmd(archiver, "recreate", "test0", "input/dir2", "-e", "input/dir2/file3", "--target=new-archive") - check_cache(archiver) + cmd(archiver, "check") archives = cmd(archiver, "rlist") assert original_archive in archives @@ -120,7 +119,7 @@ def test_recreate_basic(archivers, request): cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "create", "test0", "input") cmd(archiver, "recreate", "test0", "input/dir2", "-e", "input/dir2/file3") - check_cache(archiver) + cmd(archiver, "check") listing = cmd(archiver, "list", "test0", "--short") assert "file1" not in listing assert "dir2/file2" in listing @@ -134,7 +133,7 @@ def test_recreate_subtree_hardlinks(archivers, request): _extract_hardlinks_setup(archiver) cmd(archiver, "create", "test2", "input") cmd(archiver, "recreate", "-a", "test", "input/dir1") - check_cache(archiver) + cmd(archiver, "check") with changedir("output"): cmd(archiver, "extract", "test") assert os.stat("input/dir1/hardlink").st_nlink == 2 @@ -159,7 +158,7 @@ def test_recreate_rechunkify(archivers, request): # right now, the file is chunked differently assert num_chunks1 != num_chunks2 cmd(archiver, "recreate", "--chunker-params", "default") - check_cache(archiver) + cmd(archiver, "check") num_chunks1 = int(cmd(archiver, "list", "test1", "input/large_file", "--format", "{num_chunks}")) num_chunks2 = int(cmd(archiver, "list", "test2", "input/large_file", "--format", "{num_chunks}")) # now the files are chunked in the same way @@ -220,7 +219,7 @@ def test_recreate_dry_run(archivers, request): cmd(archiver, "create", "test", "input") archives_before = cmd(archiver, "list", "test") cmd(archiver, "recreate", "-n", "-e", "input/compressible") - check_cache(archiver) + cmd(archiver, "check") archives_after = cmd(archiver, "list", "test") assert archives_after == archives_before @@ -232,7 +231,7 @@ def test_recreate_skips_nothing_to_do(archivers, request): cmd(archiver, "create", "test", "input") info_before = cmd(archiver, "info", "-a", "test") cmd(archiver, "recreate", "--chunker-params", "default") - check_cache(archiver) + cmd(archiver, "check") info_after = cmd(archiver, "info", "-a", "test") assert info_before == info_after # includes archive ID @@ -248,22 +247,22 @@ def test_recreate_list_output(archivers, request): cmd(archiver, "create", "test", "input") output = cmd(archiver, "recreate", "-a", "test", "--list", "--info", "-e", "input/file2") - check_cache(archiver) + cmd(archiver, "check") assert "input/file1" in output assert "- input/file2" in output output = cmd(archiver, "recreate", "-a", "test", "--list", "-e", "input/file3") - check_cache(archiver) + cmd(archiver, "check") assert "input/file1" in output assert "- input/file3" in output output = cmd(archiver, "recreate", "-a", "test", "-e", "input/file4") - check_cache(archiver) + cmd(archiver, "check") assert "input/file1" not in output assert "- input/file4" not in output output = cmd(archiver, "recreate", "-a", "test", "--info", "-e", "input/file5") - check_cache(archiver) + cmd(archiver, "check") assert "input/file1" not in output assert "- input/file5" not in output diff --git a/src/borg/testsuite/conftest.py b/src/borg/testsuite/conftest.py index 4708d9170..42bc4f0ed 100644 --- a/src/borg/testsuite/conftest.py +++ b/src/borg/testsuite/conftest.py @@ -9,8 +9,7 @@ pytest.register_assert_rewrite("borg.testsuite") -import borg.cache # noqa: E402 -from borg.archiver import Archiver +from borg.archiver import Archiver # noqa: E402 from borg.logger import setup_logging # noqa: E402 # Ensure that the loggers exist for all tests @@ -56,28 +55,6 @@ def pytest_report_header(config, start_path): return output -class DefaultPatches: - def __init__(self, request): - self.org_cache_wipe_cache = borg.cache.LocalCache.wipe_cache - - def wipe_should_not_be_called(*a, **kw): - raise AssertionError( - "Cache wipe was triggered, if this is part of the test add " "@pytest.mark.allow_cache_wipe" - ) - - if "allow_cache_wipe" not in request.keywords: - borg.cache.LocalCache.wipe_cache = wipe_should_not_be_called - request.addfinalizer(self.undo) - - def undo(self): - borg.cache.LocalCache.wipe_cache = self.org_cache_wipe_cache - - -@pytest.fixture(autouse=True) -def default_patches(request): - return DefaultPatches(request) - - @pytest.fixture() def set_env_variables(): os.environ["BORG_CHECK_I_KNOW_WHAT_I_AM_DOING"] = "YES" From 7a93890602cb3b1434f46076d83299a57de8a643 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Aug 2024 22:16:47 +0200 Subject: [PATCH 13/79] archive.calc_stats: remove unique size computation --- src/borg/archive.py | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 326b3cbf8..39ba8dcba 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -29,7 +29,7 @@ from .crypto.low_level import IntegrityError as IntegrityErrorBase from .helpers import BackupError, BackupRaceConditionError from .helpers import BackupOSError, BackupPermissionError, BackupFileNotFoundError, BackupIOError -from .hashindex import ChunkIndex, ChunkIndexEntry, CacheSynchronizer +from .hashindex import ChunkIndex, ChunkIndexEntry from .helpers import HardLinkManager from .helpers import ChunkIteratorFileWrapper, open_item from .helpers import Error, IntegrityError, set_ec @@ -711,34 +711,8 @@ def save(self, name=None, comment=None, timestamp=None, stats=None, additional_m return metadata def calc_stats(self, cache, want_unique=True): - if not want_unique: - unique_size = 0 - else: - - def add(id): - entry = cache.chunks[id] - archive_index.add(id, 1, entry.size) - - archive_index = ChunkIndex() - sync = CacheSynchronizer(archive_index) - add(self.id) - # we must escape any % char in the archive name, because we use it in a format string, see #6500 - arch_name_escd = self.name.replace("%", "%%") - pi = ProgressIndicatorPercent( - total=len(self.metadata.items), - msg="Calculating statistics for archive %s ... %%3.0f%%%%" % arch_name_escd, - msgid="archive.calc_stats", - ) - for id, chunk in zip(self.metadata.items, self.repository.get_many(self.metadata.items)): - pi.show(increase=1) - add(id) - _, data = self.repo_objs.parse(id, chunk, ro_type=ROBJ_ARCHIVE_STREAM) - sync.feed(data) - unique_size = archive_index.stats_against(cache.chunks)[1] - pi.finish() - stats = Statistics(iec=self.iec) - stats.usize = unique_size + stats.usize = 0 # this is expensive to compute stats.nfiles = self.metadata.nfiles stats.osize = self.metadata.size return stats From 0306ba9a63f01248e18c5eb7b67a48fef3adcae1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Aug 2024 22:39:43 +0200 Subject: [PATCH 14/79] get rid of the CacheSynchronizer Lots of low-level code written back then to optimize runtime of some functions. We'll solve this differently by doing less stats, esp. if it is expensive to compute. --- scripts/fuzz-cache-sync/HOWTO | 10 - scripts/fuzz-cache-sync/main.c | 33 -- .../fuzz-cache-sync/testcase_dir/test_simple | Bin 147 -> 0 bytes src/borg/cache_sync/cache_sync.c | 138 ------ src/borg/cache_sync/sysdep.h | 194 -------- src/borg/cache_sync/unpack.h | 416 ------------------ src/borg/cache_sync/unpack_define.h | 95 ---- src/borg/cache_sync/unpack_template.h | 365 --------------- src/borg/hashindex.pyx | 53 +-- src/borg/testsuite/cache.py | 147 +------ 10 files changed, 2 insertions(+), 1449 deletions(-) delete mode 100644 scripts/fuzz-cache-sync/HOWTO delete mode 100644 scripts/fuzz-cache-sync/main.c delete mode 100644 scripts/fuzz-cache-sync/testcase_dir/test_simple delete mode 100644 src/borg/cache_sync/cache_sync.c delete mode 100644 src/borg/cache_sync/sysdep.h delete mode 100644 src/borg/cache_sync/unpack.h delete mode 100644 src/borg/cache_sync/unpack_define.h delete mode 100644 src/borg/cache_sync/unpack_template.h diff --git a/scripts/fuzz-cache-sync/HOWTO b/scripts/fuzz-cache-sync/HOWTO deleted file mode 100644 index ae144b287..000000000 --- a/scripts/fuzz-cache-sync/HOWTO +++ /dev/null @@ -1,10 +0,0 @@ -- Install AFL and the requirements for LLVM mode (see docs) -- Compile the fuzzing target, e.g. - - AFL_HARDEN=1 afl-clang-fast main.c -o fuzz-target -O3 - - (other options, like using ASan or MSan are possible as well) -- Add additional test cases to testcase_dir -- Run afl, easiest (but inefficient) way; - - afl-fuzz -i testcase_dir -o findings_dir ./fuzz-target diff --git a/scripts/fuzz-cache-sync/main.c b/scripts/fuzz-cache-sync/main.c deleted file mode 100644 index c65dd272d..000000000 --- a/scripts/fuzz-cache-sync/main.c +++ /dev/null @@ -1,33 +0,0 @@ - -#define BORG_NO_PYTHON - -#include "../../src/borg/_hashindex.c" -#include "../../src/borg/cache_sync/cache_sync.c" - -#define BUFSZ 32768 - -int main() { - char buf[BUFSZ]; - int len, ret; - CacheSyncCtx *ctx; - HashIndex *idx; - - /* capacity, key size, value size */ - idx = hashindex_init(0, 32, 12); - ctx = cache_sync_init(idx); - - while (1) { - len = read(0, buf, BUFSZ); - if (!len) { - break; - } - ret = cache_sync_feed(ctx, buf, len); - if(!ret && cache_sync_error(ctx)) { - fprintf(stderr, "error: %s\n", cache_sync_error(ctx)); - return 1; - } - } - hashindex_free(idx); - cache_sync_free(ctx); - return 0; -} diff --git a/scripts/fuzz-cache-sync/testcase_dir/test_simple b/scripts/fuzz-cache-sync/testcase_dir/test_simple deleted file mode 100644 index d5f6670c154abeb45b981330307e6f4ac7e5e3ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147 zcmaFAfA9PKd(-msfn-u*5tyt3k}Q`NL%3pkKwfEaDo83hqcktO7?}ZN0}+FQ0e)b} L$V8+{BPM15ZpAyL diff --git a/src/borg/cache_sync/cache_sync.c b/src/borg/cache_sync/cache_sync.c deleted file mode 100644 index eff765d49..000000000 --- a/src/borg/cache_sync/cache_sync.c +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Borg cache synchronizer, - * high level interface. - * - * These routines parse msgpacked item metadata and update a HashIndex - * with all chunks that are referenced from the items. - * - * This file only contains some initialization and buffer management. - * - * The parser is split in two parts, somewhat similar to lexer/parser combinations: - * - * unpack_template.h munches msgpack and calls a specific callback for each object - * encountered (e.g. beginning of a map, an integer, a string, a map item etc.). - * - * unpack.h implements these callbacks and uses another state machine to - * extract chunk references from it. - */ - -#include "unpack.h" - -typedef struct { - unpack_context ctx; - - char *buf; - size_t head; - size_t tail; - size_t size; -} CacheSyncCtx; - -static CacheSyncCtx * -cache_sync_init(HashIndex *chunks) -{ - CacheSyncCtx *ctx; - if (!(ctx = (CacheSyncCtx*)malloc(sizeof(CacheSyncCtx)))) { - return NULL; - } - - unpack_init(&ctx->ctx); - /* needs to be set only once */ - ctx->ctx.user.chunks = chunks; - ctx->ctx.user.totals.size = 0; - ctx->ctx.user.totals.num_files = 0; - ctx->buf = NULL; - ctx->head = 0; - ctx->tail = 0; - ctx->size = 0; - - return ctx; -} - -static void -cache_sync_free(CacheSyncCtx *ctx) -{ - if(ctx->buf) { - free(ctx->buf); - } - free(ctx); -} - -static const char * -cache_sync_error(const CacheSyncCtx *ctx) -{ - return ctx->ctx.user.last_error; -} - -static uint64_t -cache_sync_num_files_totals(const CacheSyncCtx *ctx) -{ - return ctx->ctx.user.totals.num_files; -} - -static uint64_t -cache_sync_size_totals(const CacheSyncCtx *ctx) -{ - return ctx->ctx.user.totals.size; -} - -/** - * feed data to the cache synchronizer - * 0 = abort, 1 = continue - * abort is a regular condition, check cache_sync_error - */ -static int -cache_sync_feed(CacheSyncCtx *ctx, void *data, uint32_t length) -{ - size_t new_size; - int ret; - char *new_buf; - - if(ctx->tail + length > ctx->size) { - if((ctx->tail - ctx->head) + length <= ctx->size) { - /* | XXXXX| -> move data in buffer backwards -> |XXXXX | */ - memmove(ctx->buf, ctx->buf + ctx->head, ctx->tail - ctx->head); - ctx->tail -= ctx->head; - ctx->head = 0; - } else { - /* must expand buffer to fit all data */ - new_size = (ctx->tail - ctx->head) + length; - new_buf = (char*) malloc(new_size); - if(!new_buf) { - ctx->ctx.user.last_error = "cache_sync_feed: unable to allocate buffer"; - return 0; - } - if(ctx->buf) { - memcpy(new_buf, ctx->buf + ctx->head, ctx->tail - ctx->head); - free(ctx->buf); - } - ctx->buf = new_buf; - ctx->tail -= ctx->head; - ctx->head = 0; - ctx->size = new_size; - } - } - - memcpy(ctx->buf + ctx->tail, data, length); - ctx->tail += length; - - while(1) { - if(ctx->head >= ctx->tail) { - return 1; /* request more bytes */ - } - - ret = unpack_execute(&ctx->ctx, ctx->buf, ctx->tail, &ctx->head); - if(ret == 1) { - unpack_init(&ctx->ctx); - continue; - } else if(ret == 0) { - return 1; - } else { - if(!ctx->ctx.user.last_error) { - ctx->ctx.user.last_error = "Unknown error"; - } - return 0; - } - } - /* unreachable */ - return 1; -} diff --git a/src/borg/cache_sync/sysdep.h b/src/borg/cache_sync/sysdep.h deleted file mode 100644 index e4ce7850f..000000000 --- a/src/borg/cache_sync/sysdep.h +++ /dev/null @@ -1,194 +0,0 @@ -/* - * MessagePack system dependencies - * - * Copyright (C) 2008-2010 FURUHASHI Sadayuki - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef MSGPACK_SYSDEP_H__ -#define MSGPACK_SYSDEP_H__ - -#include -#include -#if defined(_MSC_VER) && _MSC_VER < 1600 -typedef __int8 int8_t; -typedef unsigned __int8 uint8_t; -typedef __int16 int16_t; -typedef unsigned __int16 uint16_t; -typedef __int32 int32_t; -typedef unsigned __int32 uint32_t; -typedef __int64 int64_t; -typedef unsigned __int64 uint64_t; -#elif defined(_MSC_VER) // && _MSC_VER >= 1600 -#include -#else -#include -#include -#endif - -#ifdef _WIN32 -#define _msgpack_atomic_counter_header -typedef long _msgpack_atomic_counter_t; -#define _msgpack_sync_decr_and_fetch(ptr) InterlockedDecrement(ptr) -#define _msgpack_sync_incr_and_fetch(ptr) InterlockedIncrement(ptr) -#elif defined(__GNUC__) && ((__GNUC__*10 + __GNUC_MINOR__) < 41) -#define _msgpack_atomic_counter_header "gcc_atomic.h" -#else -typedef unsigned int _msgpack_atomic_counter_t; -#define _msgpack_sync_decr_and_fetch(ptr) __sync_sub_and_fetch(ptr, 1) -#define _msgpack_sync_incr_and_fetch(ptr) __sync_add_and_fetch(ptr, 1) -#endif - -#ifdef _WIN32 - -#ifdef __cplusplus -/* numeric_limits::min,max */ -#ifdef max -#undef max -#endif -#ifdef min -#undef min -#endif -#endif - -#else -#include /* __BYTE_ORDER */ -#endif - -#if !defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__) -#if __BYTE_ORDER == __LITTLE_ENDIAN -#define __LITTLE_ENDIAN__ -#elif __BYTE_ORDER == __BIG_ENDIAN -#define __BIG_ENDIAN__ -#elif _WIN32 -#define __LITTLE_ENDIAN__ -#endif -#endif - - -#ifdef __LITTLE_ENDIAN__ - -#ifdef _WIN32 -# if defined(ntohs) -# define _msgpack_be16(x) ntohs(x) -# elif defined(_byteswap_ushort) || (defined(_MSC_VER) && _MSC_VER >= 1400) -# define _msgpack_be16(x) ((uint16_t)_byteswap_ushort((unsigned short)x)) -# else -# define _msgpack_be16(x) ( \ - ((((uint16_t)x) << 8) ) | \ - ((((uint16_t)x) >> 8) ) ) -# endif -#else -# define _msgpack_be16(x) ntohs(x) -#endif - -#ifdef _WIN32 -# if defined(ntohl) -# define _msgpack_be32(x) ntohl(x) -# elif defined(_byteswap_ulong) || (defined(_MSC_VER) && _MSC_VER >= 1400) -# define _msgpack_be32(x) ((uint32_t)_byteswap_ulong((unsigned long)x)) -# else -# define _msgpack_be32(x) \ - ( ((((uint32_t)x) << 24) ) | \ - ((((uint32_t)x) << 8) & 0x00ff0000U ) | \ - ((((uint32_t)x) >> 8) & 0x0000ff00U ) | \ - ((((uint32_t)x) >> 24) ) ) -# endif -#else -# define _msgpack_be32(x) ntohl(x) -#endif - -#if defined(_byteswap_uint64) || (defined(_MSC_VER) && _MSC_VER >= 1400) -# define _msgpack_be64(x) (_byteswap_uint64(x)) -#elif defined(bswap_64) -# define _msgpack_be64(x) bswap_64(x) -#elif defined(__DARWIN_OSSwapInt64) -# define _msgpack_be64(x) __DARWIN_OSSwapInt64(x) -#else -#define _msgpack_be64(x) \ - ( ((((uint64_t)x) << 56) ) | \ - ((((uint64_t)x) << 40) & 0x00ff000000000000ULL ) | \ - ((((uint64_t)x) << 24) & 0x0000ff0000000000ULL ) | \ - ((((uint64_t)x) << 8) & 0x000000ff00000000ULL ) | \ - ((((uint64_t)x) >> 8) & 0x00000000ff000000ULL ) | \ - ((((uint64_t)x) >> 24) & 0x0000000000ff0000ULL ) | \ - ((((uint64_t)x) >> 40) & 0x000000000000ff00ULL ) | \ - ((((uint64_t)x) >> 56) ) ) -#endif - -#define _msgpack_load16(cast, from) ((cast)( \ - (((uint16_t)((uint8_t*)(from))[0]) << 8) | \ - (((uint16_t)((uint8_t*)(from))[1]) ) )) - -#define _msgpack_load32(cast, from) ((cast)( \ - (((uint32_t)((uint8_t*)(from))[0]) << 24) | \ - (((uint32_t)((uint8_t*)(from))[1]) << 16) | \ - (((uint32_t)((uint8_t*)(from))[2]) << 8) | \ - (((uint32_t)((uint8_t*)(from))[3]) ) )) - -#define _msgpack_load64(cast, from) ((cast)( \ - (((uint64_t)((uint8_t*)(from))[0]) << 56) | \ - (((uint64_t)((uint8_t*)(from))[1]) << 48) | \ - (((uint64_t)((uint8_t*)(from))[2]) << 40) | \ - (((uint64_t)((uint8_t*)(from))[3]) << 32) | \ - (((uint64_t)((uint8_t*)(from))[4]) << 24) | \ - (((uint64_t)((uint8_t*)(from))[5]) << 16) | \ - (((uint64_t)((uint8_t*)(from))[6]) << 8) | \ - (((uint64_t)((uint8_t*)(from))[7]) ) )) - -#else - -#define _msgpack_be16(x) (x) -#define _msgpack_be32(x) (x) -#define _msgpack_be64(x) (x) - -#define _msgpack_load16(cast, from) ((cast)( \ - (((uint16_t)((uint8_t*)from)[0]) << 8) | \ - (((uint16_t)((uint8_t*)from)[1]) ) )) - -#define _msgpack_load32(cast, from) ((cast)( \ - (((uint32_t)((uint8_t*)from)[0]) << 24) | \ - (((uint32_t)((uint8_t*)from)[1]) << 16) | \ - (((uint32_t)((uint8_t*)from)[2]) << 8) | \ - (((uint32_t)((uint8_t*)from)[3]) ) )) - -#define _msgpack_load64(cast, from) ((cast)( \ - (((uint64_t)((uint8_t*)from)[0]) << 56) | \ - (((uint64_t)((uint8_t*)from)[1]) << 48) | \ - (((uint64_t)((uint8_t*)from)[2]) << 40) | \ - (((uint64_t)((uint8_t*)from)[3]) << 32) | \ - (((uint64_t)((uint8_t*)from)[4]) << 24) | \ - (((uint64_t)((uint8_t*)from)[5]) << 16) | \ - (((uint64_t)((uint8_t*)from)[6]) << 8) | \ - (((uint64_t)((uint8_t*)from)[7]) ) )) -#endif - - -#define _msgpack_store16(to, num) \ - do { uint16_t val = _msgpack_be16(num); memcpy(to, &val, 2); } while(0) -#define _msgpack_store32(to, num) \ - do { uint32_t val = _msgpack_be32(num); memcpy(to, &val, 4); } while(0) -#define _msgpack_store64(to, num) \ - do { uint64_t val = _msgpack_be64(num); memcpy(to, &val, 8); } while(0) - -/* -#define _msgpack_load16(cast, from) \ - ({ cast val; memcpy(&val, (char*)from, 2); _msgpack_be16(val); }) -#define _msgpack_load32(cast, from) \ - ({ cast val; memcpy(&val, (char*)from, 4); _msgpack_be32(val); }) -#define _msgpack_load64(cast, from) \ - ({ cast val; memcpy(&val, (char*)from, 8); _msgpack_be64(val); }) -*/ - - -#endif /* msgpack/sysdep.h */ diff --git a/src/borg/cache_sync/unpack.h b/src/borg/cache_sync/unpack.h deleted file mode 100644 index ad898655f..000000000 --- a/src/borg/cache_sync/unpack.h +++ /dev/null @@ -1,416 +0,0 @@ -/* - * Borg cache synchronizer, - * based on a MessagePack for Python unpacking routine - * - * Copyright (C) 2009 Naoki INADA - * Copyright (c) 2017 Marian Beermann - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * This limits the depth of the structures we can unpack, i.e. how many containers - * are nestable. - */ -#define MSGPACK_EMBED_STACK_SIZE (16) -#include "unpack_define.h" - -// 2**32 - 1025 -#define _MAX_VALUE ( (uint32_t) 4294966271UL ) - -#define MIN(x, y) ((x) < (y) ? (x): (y)) - -#ifdef DEBUG -#define SET_LAST_ERROR(msg) \ - fprintf(stderr, "cache_sync parse error: %s\n", (msg)); \ - u->last_error = (msg); -#else -#define SET_LAST_ERROR(msg) \ - u->last_error = (msg); -#endif - -typedef struct unpack_user { - /* Item.chunks is at the top level; we don't care about anything else, - * only need to track the current level to navigate arbitrary and unknown structure. - * To discern keys from everything else on the top level we use expect_map_item_end. - */ - int level; - - const char *last_error; - - HashIndex *chunks; - - /* - * We don't care about most stuff. This flag tells us whether we're at the chunks structure, - * meaning: - * {'foo': 'bar', 'chunks': [...], 'stuff': ... } - * ^-HERE-^ - */ - int inside_chunks; - - /* does this item have a chunks list in it? */ - int has_chunks; - - enum { - /* the next thing is a map key at the Item root level, - * and it might be e.g. the "chunks" key we're looking for */ - expect_map_key, - - /* blocking state to expect_map_key - * { 'stuff': , 'chunks': [ - * emk -> emie -> -> -> -> emk ecb eeboce - * (nested containers are tracked via level) - * emk=expect_map_key, emie=expect_map_item_end, ecb=expect_chunks_begin, - * eeboce=expect_entry_begin_or_chunks_end - */ - expect_map_item_end, - - /* next thing must be the chunks array (array) */ - expect_chunks_begin, - - /* next thing must either be another CLE (array) or end of Item.chunks (array_end) */ - expect_entry_begin_or_chunks_end, - - /* - * processing ChunkListEntry tuple: - * expect_key, expect_size, expect_entry_end - */ - /* next thing must be the key (raw, l=32) */ - expect_key, - /* next thing must be the size (int) */ - expect_size, - /* next thing must be the end of the CLE (array_end) */ - expect_entry_end, - - expect_item_begin - } expect; - - /* collect values here for current chunklist entry */ - struct { - unsigned char key[32]; - uint32_t size; - } current; - - /* summing up chunks sizes here within a single item */ - struct { - uint64_t size; - } item; - - /* total sizes and files count coming from all files */ - struct { - uint64_t size, num_files; - } totals; - -} unpack_user; - -struct unpack_context; -typedef struct unpack_context unpack_context; -typedef int (*execute_fn)(unpack_context *ctx, const char* data, size_t len, size_t* off); - -#define UNEXPECTED(what) \ - if(u->inside_chunks || u->expect == expect_map_key) { \ - SET_LAST_ERROR("Unexpected object: " what); \ - return -1; \ - } - -static inline void unpack_init_user_state(unpack_user *u) -{ - u->last_error = NULL; - u->level = 0; - u->inside_chunks = 0; - u->expect = expect_item_begin; -} - -static inline int unpack_callback_uint64(unpack_user* u, int64_t d) -{ - switch(u->expect) { - case expect_size: - u->current.size = d; - u->expect = expect_entry_end; - break; - default: - UNEXPECTED("integer"); - } - return 0; -} - -static inline int unpack_callback_uint32(unpack_user* u, uint32_t d) -{ - return unpack_callback_uint64(u, d); -} - -static inline int unpack_callback_uint16(unpack_user* u, uint16_t d) -{ - return unpack_callback_uint64(u, d); -} - -static inline int unpack_callback_uint8(unpack_user* u, uint8_t d) -{ - return unpack_callback_uint64(u, d); -} - -static inline int unpack_callback_int64(unpack_user* u, uint64_t d) -{ - return unpack_callback_uint64(u, d); -} - -static inline int unpack_callback_int32(unpack_user* u, int32_t d) -{ - return unpack_callback_uint64(u, d); -} - -static inline int unpack_callback_int16(unpack_user* u, int16_t d) -{ - return unpack_callback_uint64(u, d); -} - -static inline int unpack_callback_int8(unpack_user* u, int8_t d) -{ - return unpack_callback_uint64(u, d); -} - -/* Ain't got anything to do with those floats */ -static inline int unpack_callback_double(unpack_user* u, double d) -{ - (void)d; - UNEXPECTED("double"); - return 0; -} - -static inline int unpack_callback_float(unpack_user* u, float d) -{ - (void)d; - UNEXPECTED("float"); - return 0; -} - -/* nil/true/false — I/don't/care */ -static inline int unpack_callback_nil(unpack_user* u) -{ - UNEXPECTED("nil"); - return 0; -} - -static inline int unpack_callback_true(unpack_user* u) -{ - UNEXPECTED("true"); - return 0; -} - -static inline int unpack_callback_false(unpack_user* u) -{ - UNEXPECTED("false"); - return 0; -} - -static inline int unpack_callback_array(unpack_user* u, unsigned int n) -{ - switch(u->expect) { - case expect_chunks_begin: - /* b'chunks': [ - * ^ */ - u->expect = expect_entry_begin_or_chunks_end; - break; - case expect_entry_begin_or_chunks_end: - /* b'chunks': [ ( - * ^ */ - if(n != 2) { - SET_LAST_ERROR("Invalid chunk list entry length"); - return -1; - } - u->expect = expect_key; - break; - default: - if(u->inside_chunks) { - SET_LAST_ERROR("Unexpected array start"); - return -1; - } else { - u->level++; - return 0; - } - } - return 0; -} - -static inline int unpack_callback_array_item(unpack_user* u, unsigned int current) -{ - (void)u; (void)current; - return 0; -} - -static inline int unpack_callback_array_end(unpack_user* u) -{ - uint32_t *cache_entry; - uint32_t cache_values[3]; - uint64_t refcount; - - switch(u->expect) { - case expect_entry_end: - /* b'chunks': [ ( b'1234...', 123, 345 ) - * ^ */ - cache_entry = (uint32_t*) hashindex_get(u->chunks, u->current.key); - if(cache_entry) { - refcount = _le32toh(cache_entry[0]); - if(refcount > _MAX_VALUE) { - SET_LAST_ERROR("invalid reference count"); - return -1; - } - refcount += 1; - cache_entry[0] = _htole32(MIN(refcount, _MAX_VALUE)); - } else { - /* refcount, size */ - cache_values[0] = _htole32(1); - cache_values[1] = _htole32(u->current.size); - if(!hashindex_set(u->chunks, u->current.key, cache_values)) { - SET_LAST_ERROR("hashindex_set failed"); - return -1; - } - } - u->item.size += u->current.size; - u->expect = expect_entry_begin_or_chunks_end; - break; - case expect_entry_begin_or_chunks_end: - /* b'chunks': [ ] - * ^ */ - /* end of Item.chunks */ - u->inside_chunks = 0; - u->expect = expect_map_item_end; - break; - default: - if(u->inside_chunks) { - SET_LAST_ERROR("Invalid state transition (unexpected array end)"); - return -1; - } else { - u->level--; - return 0; - } - } - return 0; -} - -static inline int unpack_callback_map(unpack_user* u, unsigned int n) -{ - (void)n; - - if(u->level == 0) { - if(u->expect != expect_item_begin) { - SET_LAST_ERROR("Invalid state transition"); /* unreachable */ - return -1; - } - /* This begins a new Item */ - u->expect = expect_map_key; - u->has_chunks = 0; - u->item.size = 0; - } - - if(u->inside_chunks) { - UNEXPECTED("map"); - } - - u->level++; - - return 0; -} - -static inline int unpack_callback_map_item(unpack_user* u, unsigned int current) -{ - (void)u; (void)current; - - if(u->level == 1) { - switch(u->expect) { - case expect_map_item_end: - u->expect = expect_map_key; - break; - default: - SET_LAST_ERROR("Unexpected map item"); - return -1; - } - } - return 0; -} - -static inline int unpack_callback_map_end(unpack_user* u) -{ - u->level--; - if(u->inside_chunks) { - SET_LAST_ERROR("Unexpected map end"); - return -1; - } - if(u->level == 0) { - /* This ends processing of an Item */ - if(u->has_chunks) { - u->totals.num_files += 1; - u->totals.size += u->item.size; - } - } - return 0; -} - -static inline int unpack_callback_raw(unpack_user* u, const char* b, const char* p, unsigned int length) -{ - /* raw = what Borg uses for text stuff */ - /* Note: p points to an internal buffer which contains l bytes. */ - (void)b; - - switch(u->expect) { - case expect_map_key: - if(length == 6 && !memcmp("chunks", p, 6)) { - u->expect = expect_chunks_begin; - u->inside_chunks = 1; - u->has_chunks = 1; - } else { - u->expect = expect_map_item_end; - } - break; - default: - if(u->inside_chunks) { - SET_LAST_ERROR("Unexpected raw in chunks structure"); - return -1; - } - } - return 0; -} - -static inline int unpack_callback_bin(unpack_user* u, const char* b, const char* p, unsigned int length) -{ - /* bin = what Borg uses for binary stuff */ - /* Note: p points to an internal buffer which contains l bytes. */ - (void)b; - - switch(u->expect) { - case expect_key: - if(length != 32) { - SET_LAST_ERROR("Incorrect key length"); - return -1; - } - memcpy(u->current.key, p, 32); - u->expect = expect_size; - break; - default: - if(u->inside_chunks) { - SET_LAST_ERROR("Unexpected bytes in chunks structure"); - return -1; - } - } - return 0; -} - -static inline int unpack_callback_ext(unpack_user* u, const char* base, const char* pos, - unsigned int length) -{ - (void)u; (void)base; (void)pos; (void)length; - UNEXPECTED("ext"); - return 0; -} - -#include "unpack_template.h" diff --git a/src/borg/cache_sync/unpack_define.h b/src/borg/cache_sync/unpack_define.h deleted file mode 100644 index 10c910861..000000000 --- a/src/borg/cache_sync/unpack_define.h +++ /dev/null @@ -1,95 +0,0 @@ -/* - * MessagePack unpacking routine template - * - * Copyright (C) 2008-2010 FURUHASHI Sadayuki - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef MSGPACK_UNPACK_DEFINE_H__ -#define MSGPACK_UNPACK_DEFINE_H__ - -#include "sysdep.h" -#include -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - - -#ifndef MSGPACK_EMBED_STACK_SIZE -#define MSGPACK_EMBED_STACK_SIZE 32 -#endif - - -// CS is first byte & 0x1f -typedef enum { - CS_HEADER = 0x00, // nil - - //CS_ = 0x01, - //CS_ = 0x02, // false - //CS_ = 0x03, // true - - CS_BIN_8 = 0x04, - CS_BIN_16 = 0x05, - CS_BIN_32 = 0x06, - - CS_EXT_8 = 0x07, - CS_EXT_16 = 0x08, - CS_EXT_32 = 0x09, - - CS_FLOAT = 0x0a, - CS_DOUBLE = 0x0b, - CS_UINT_8 = 0x0c, - CS_UINT_16 = 0x0d, - CS_UINT_32 = 0x0e, - CS_UINT_64 = 0x0f, - CS_INT_8 = 0x10, - CS_INT_16 = 0x11, - CS_INT_32 = 0x12, - CS_INT_64 = 0x13, - - //CS_FIXEXT1 = 0x14, - //CS_FIXEXT2 = 0x15, - //CS_FIXEXT4 = 0x16, - //CS_FIXEXT8 = 0x17, - //CS_FIXEXT16 = 0x18, - - CS_RAW_8 = 0x19, - CS_RAW_16 = 0x1a, - CS_RAW_32 = 0x1b, - CS_ARRAY_16 = 0x1c, - CS_ARRAY_32 = 0x1d, - CS_MAP_16 = 0x1e, - CS_MAP_32 = 0x1f, - - ACS_RAW_VALUE, - ACS_BIN_VALUE, - ACS_EXT_VALUE, -} msgpack_unpack_state; - - -typedef enum { - CT_ARRAY_ITEM, - CT_MAP_KEY, - CT_MAP_VALUE, -} msgpack_container_type; - - -#ifdef __cplusplus -} -#endif - -#endif /* msgpack/unpack_define.h */ diff --git a/src/borg/cache_sync/unpack_template.h b/src/borg/cache_sync/unpack_template.h deleted file mode 100644 index 39f9f3314..000000000 --- a/src/borg/cache_sync/unpack_template.h +++ /dev/null @@ -1,365 +0,0 @@ -/* - * MessagePack unpacking routine template - * - * Copyright (C) 2008-2010 FURUHASHI Sadayuki - * Copyright (c) 2017 Marian Beermann - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * - * This has been slightly adapted from the vanilla msgpack-{c, python} version. - * Since cache_sync does not intend to build an output data structure, - * msgpack_unpack_object and all of its uses was removed. - */ - -#ifndef USE_CASE_RANGE -#if !defined(_MSC_VER) -#define USE_CASE_RANGE -#endif -#endif - -typedef struct unpack_stack { - size_t size; - size_t count; - unsigned int ct; -} unpack_stack; - -struct unpack_context { - unpack_user user; - unsigned int cs; - unsigned int trail; - unsigned int top; - unpack_stack stack[MSGPACK_EMBED_STACK_SIZE]; -}; - -static inline void unpack_init(unpack_context* ctx) -{ - ctx->cs = CS_HEADER; - ctx->trail = 0; - ctx->top = 0; - unpack_init_user_state(&ctx->user); -} - -#define construct 1 - -static inline int unpack_execute(unpack_context* ctx, const char* data, size_t len, size_t* off) -{ - const unsigned char* p = (unsigned char*)data + *off; - const unsigned char* const pe = (unsigned char*)data + len; - const void* n = NULL; - - unsigned int trail = ctx->trail; - unsigned int cs = ctx->cs; - unsigned int top = ctx->top; - unpack_stack* stack = ctx->stack; - unpack_user* user = &ctx->user; - - unpack_stack* c = NULL; - - int ret; - - assert(len >= *off); - -#define construct_cb(name) \ - construct && unpack_callback ## name - -#define push_simple_value(func) \ - if(construct_cb(func)(user) < 0) { goto _failed; } \ - goto _push -#define push_fixed_value(func, arg) \ - if(construct_cb(func)(user, arg) < 0) { goto _failed; } \ - goto _push -#define push_variable_value(func, base, pos, len) \ - if(construct_cb(func)(user, \ - (const char*)base, (const char*)pos, len) < 0) { goto _failed; } \ - goto _push - -#define again_fixed_trail(_cs, trail_len) \ - trail = trail_len; \ - cs = _cs; \ - goto _fixed_trail_again -#define again_fixed_trail_if_zero(_cs, trail_len, ifzero) \ - trail = trail_len; \ - if(trail == 0) { goto ifzero; } \ - cs = _cs; \ - goto _fixed_trail_again - -#define start_container(func, count_, ct_) \ - if(top >= MSGPACK_EMBED_STACK_SIZE) { goto _failed; } /* FIXME */ \ - if(construct_cb(func)(user, count_) < 0) { goto _failed; } \ - if((count_) == 0) { \ - if (construct_cb(func##_end)(user) < 0) { goto _failed; } \ - goto _push; } \ - stack[top].ct = ct_; \ - stack[top].size = count_; \ - stack[top].count = 0; \ - ++top; \ - goto _header_again - -#define NEXT_CS(p) ((unsigned int)*p & 0x1f) - -#ifdef USE_CASE_RANGE -#define SWITCH_RANGE_BEGIN switch(*p) { -#define SWITCH_RANGE(FROM, TO) case FROM ... TO: -#define SWITCH_RANGE_DEFAULT default: -#define SWITCH_RANGE_END } -#else -#define SWITCH_RANGE_BEGIN { if(0) { -#define SWITCH_RANGE(FROM, TO) } else if(FROM <= *p && *p <= TO) { -#define SWITCH_RANGE_DEFAULT } else { -#define SWITCH_RANGE_END } } -#endif - - if(p == pe) { goto _out; } - do { - switch(cs) { - case CS_HEADER: - SWITCH_RANGE_BEGIN - SWITCH_RANGE(0x00, 0x7f) // Positive Fixnum - push_fixed_value(_uint8, *(uint8_t*)p); - SWITCH_RANGE(0xe0, 0xff) // Negative Fixnum - push_fixed_value(_int8, *(int8_t*)p); - SWITCH_RANGE(0xc0, 0xdf) // Variable - switch(*p) { - case 0xc0: // nil - push_simple_value(_nil); - //case 0xc1: // never used - case 0xc2: // false - push_simple_value(_false); - case 0xc3: // true - push_simple_value(_true); - case 0xc4: // bin 8 - again_fixed_trail(NEXT_CS(p), 1); - case 0xc5: // bin 16 - again_fixed_trail(NEXT_CS(p), 2); - case 0xc6: // bin 32 - again_fixed_trail(NEXT_CS(p), 4); - case 0xc7: // ext 8 - again_fixed_trail(NEXT_CS(p), 1); - case 0xc8: // ext 16 - again_fixed_trail(NEXT_CS(p), 2); - case 0xc9: // ext 32 - again_fixed_trail(NEXT_CS(p), 4); - case 0xca: // float - case 0xcb: // double - case 0xcc: // unsigned int 8 - case 0xcd: // unsigned int 16 - case 0xce: // unsigned int 32 - case 0xcf: // unsigned int 64 - case 0xd0: // signed int 8 - case 0xd1: // signed int 16 - case 0xd2: // signed int 32 - case 0xd3: // signed int 64 - again_fixed_trail(NEXT_CS(p), 1 << (((unsigned int)*p) & 0x03)); - case 0xd4: // fixext 1 - case 0xd5: // fixext 2 - case 0xd6: // fixext 4 - case 0xd7: // fixext 8 - again_fixed_trail_if_zero(ACS_EXT_VALUE, - (1 << (((unsigned int)*p) & 0x03))+1, - _ext_zero); - case 0xd8: // fixext 16 - again_fixed_trail_if_zero(ACS_EXT_VALUE, 16+1, _ext_zero); - case 0xd9: // str 8 - again_fixed_trail(NEXT_CS(p), 1); - case 0xda: // raw 16 - case 0xdb: // raw 32 - case 0xdc: // array 16 - case 0xdd: // array 32 - case 0xde: // map 16 - case 0xdf: // map 32 - again_fixed_trail(NEXT_CS(p), 2 << (((unsigned int)*p) & 0x01)); - default: - goto _failed; - } - SWITCH_RANGE(0xa0, 0xbf) // FixRaw - again_fixed_trail_if_zero(ACS_RAW_VALUE, ((unsigned int)*p & 0x1f), _raw_zero); - SWITCH_RANGE(0x90, 0x9f) // FixArray - start_container(_array, ((unsigned int)*p) & 0x0f, CT_ARRAY_ITEM); - SWITCH_RANGE(0x80, 0x8f) // FixMap - start_container(_map, ((unsigned int)*p) & 0x0f, CT_MAP_KEY); - - SWITCH_RANGE_DEFAULT - goto _failed; - SWITCH_RANGE_END - // end CS_HEADER - - - _fixed_trail_again: - ++p; // fallthrough - - default: - if((size_t)(pe - p) < trail) { goto _out; } - n = p; p += trail - 1; - switch(cs) { - case CS_EXT_8: - again_fixed_trail_if_zero(ACS_EXT_VALUE, *(uint8_t*)n+1, _ext_zero); - case CS_EXT_16: - again_fixed_trail_if_zero(ACS_EXT_VALUE, - _msgpack_load16(uint16_t,n)+1, - _ext_zero); - case CS_EXT_32: - again_fixed_trail_if_zero(ACS_EXT_VALUE, - _msgpack_load32(uint32_t,n)+1, - _ext_zero); - case CS_FLOAT: { - union { uint32_t i; float f; } mem; - mem.i = _msgpack_load32(uint32_t,n); - push_fixed_value(_float, mem.f); } - case CS_DOUBLE: { - union { uint64_t i; double f; } mem; - mem.i = _msgpack_load64(uint64_t,n); -#if defined(__arm__) && !(__ARM_EABI__) // arm-oabi - // https://github.com/msgpack/msgpack-perl/pull/1 - mem.i = (mem.i & 0xFFFFFFFFUL) << 32UL | (mem.i >> 32UL); -#endif - push_fixed_value(_double, mem.f); } - case CS_UINT_8: - push_fixed_value(_uint8, *(uint8_t*)n); - case CS_UINT_16: - push_fixed_value(_uint16, _msgpack_load16(uint16_t,n)); - case CS_UINT_32: - push_fixed_value(_uint32, _msgpack_load32(uint32_t,n)); - case CS_UINT_64: - push_fixed_value(_uint64, _msgpack_load64(uint64_t,n)); - - case CS_INT_8: - push_fixed_value(_int8, *(int8_t*)n); - case CS_INT_16: - push_fixed_value(_int16, _msgpack_load16(int16_t,n)); - case CS_INT_32: - push_fixed_value(_int32, _msgpack_load32(int32_t,n)); - case CS_INT_64: - push_fixed_value(_int64, _msgpack_load64(int64_t,n)); - - case CS_BIN_8: - again_fixed_trail_if_zero(ACS_BIN_VALUE, *(uint8_t*)n, _bin_zero); - case CS_BIN_16: - again_fixed_trail_if_zero(ACS_BIN_VALUE, _msgpack_load16(uint16_t,n), _bin_zero); - case CS_BIN_32: - again_fixed_trail_if_zero(ACS_BIN_VALUE, _msgpack_load32(uint32_t,n), _bin_zero); - case ACS_BIN_VALUE: - _bin_zero: - push_variable_value(_bin, data, n, trail); - - case CS_RAW_8: - again_fixed_trail_if_zero(ACS_RAW_VALUE, *(uint8_t*)n, _raw_zero); - case CS_RAW_16: - again_fixed_trail_if_zero(ACS_RAW_VALUE, _msgpack_load16(uint16_t,n), _raw_zero); - case CS_RAW_32: - again_fixed_trail_if_zero(ACS_RAW_VALUE, _msgpack_load32(uint32_t,n), _raw_zero); - case ACS_RAW_VALUE: - _raw_zero: - push_variable_value(_raw, data, n, trail); - - case ACS_EXT_VALUE: - _ext_zero: - push_variable_value(_ext, data, n, trail); - - case CS_ARRAY_16: - start_container(_array, _msgpack_load16(uint16_t,n), CT_ARRAY_ITEM); - case CS_ARRAY_32: - /* FIXME security guard */ - start_container(_array, _msgpack_load32(uint32_t,n), CT_ARRAY_ITEM); - - case CS_MAP_16: - start_container(_map, _msgpack_load16(uint16_t,n), CT_MAP_KEY); - case CS_MAP_32: - /* FIXME security guard */ - start_container(_map, _msgpack_load32(uint32_t,n), CT_MAP_KEY); - - default: - goto _failed; - } - } - -_push: - if(top == 0) { goto _finish; } - c = &stack[top-1]; - switch(c->ct) { - case CT_ARRAY_ITEM: - if(construct_cb(_array_item)(user, c->count) < 0) { goto _failed; } - if(++c->count == c->size) { - if (construct_cb(_array_end)(user) < 0) { goto _failed; } - --top; - /*printf("stack pop %d\n", top);*/ - goto _push; - } - goto _header_again; - case CT_MAP_KEY: - c->ct = CT_MAP_VALUE; - goto _header_again; - case CT_MAP_VALUE: - if(construct_cb(_map_item)(user, c->count) < 0) { goto _failed; } - if(++c->count == c->size) { - if (construct_cb(_map_end)(user) < 0) { goto _failed; } - --top; - /*printf("stack pop %d\n", top);*/ - goto _push; - } - c->ct = CT_MAP_KEY; - goto _header_again; - - default: - goto _failed; - } - -_header_again: - cs = CS_HEADER; - ++p; - } while(p != pe); - goto _out; - - -_finish: - if (!construct) - unpack_callback_nil(user); - ++p; - ret = 1; - /* printf("-- finish --\n"); */ - goto _end; - -_failed: - /* printf("** FAILED **\n"); */ - ret = -1; - goto _end; - -_out: - ret = 0; - goto _end; - -_end: - ctx->cs = cs; - ctx->trail = trail; - ctx->top = top; - *off = p - (const unsigned char*)data; - - return ret; -#undef construct_cb -} - -#undef SWITCH_RANGE_BEGIN -#undef SWITCH_RANGE -#undef SWITCH_RANGE_DEFAULT -#undef SWITCH_RANGE_END -#undef push_simple_value -#undef push_fixed_value -#undef push_variable_value -#undef again_fixed_trail -#undef again_fixed_trail_if_zero -#undef start_container -#undef construct - -#undef NEXT_CS - -/* vim: set ts=4 sw=4 sts=4 expandtab */ diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 207227035..94149105c 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -34,19 +34,7 @@ cdef extern from "_hashindex.c": double HASH_MAX_LOAD -cdef extern from "cache_sync/cache_sync.c": - ctypedef struct CacheSyncCtx: - pass - - CacheSyncCtx *cache_sync_init(HashIndex *chunks) - const char *cache_sync_error(const CacheSyncCtx *ctx) - uint64_t cache_sync_num_files_totals(const CacheSyncCtx *ctx) - uint64_t cache_sync_size_totals(const CacheSyncCtx *ctx) - int cache_sync_feed(CacheSyncCtx *ctx, void *data, uint32_t length) - void cache_sync_free(CacheSyncCtx *ctx) - - uint32_t _MAX_VALUE - +_MAX_VALUE = 4294966271UL # 2**32 - 1025 cdef _NoDefault = object() @@ -592,42 +580,3 @@ cdef class ChunkKeyIterator: cdef uint32_t refcount = _le32toh(value[0]) assert refcount <= _MAX_VALUE, "invalid reference count" return (self.key)[:self.key_size], ChunkIndexEntry(refcount, _le32toh(value[1])) - - -cdef Py_buffer ro_buffer(object data) except *: - cdef Py_buffer view - PyObject_GetBuffer(data, &view, PyBUF_SIMPLE) - return view - - -cdef class CacheSynchronizer: - cdef ChunkIndex chunks - cdef CacheSyncCtx *sync - - def __cinit__(self, chunks): - self.chunks = chunks - self.sync = cache_sync_init(self.chunks.index) - if not self.sync: - raise Exception('cache_sync_init failed') - - def __dealloc__(self): - if self.sync: - cache_sync_free(self.sync) - - def feed(self, chunk): - cdef Py_buffer chunk_buf = ro_buffer(chunk) - cdef int rc - rc = cache_sync_feed(self.sync, chunk_buf.buf, chunk_buf.len) - PyBuffer_Release(&chunk_buf) - if not rc: - error = cache_sync_error(self.sync) - if error != NULL: - raise ValueError('cache_sync_feed failed: ' + error.decode('ascii')) - - @property - def num_files_totals(self): - return cache_sync_num_files_totals(self.sync) - - @property - def size_totals(self): - return cache_sync_size_totals(self.sync) diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index f9de6ccc7..28846d0ad 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -10,156 +10,11 @@ from ..archive import Statistics from ..cache import AdHocCache from ..crypto.key import AESOCBRepoKey -from ..hashindex import ChunkIndex, CacheSynchronizer +from ..hashindex import ChunkIndex from ..manifest import Manifest from ..repository3 import Repository3 -class TestCacheSynchronizer: - @pytest.fixture - def index(self): - return ChunkIndex() - - @pytest.fixture - def sync(self, index): - return CacheSynchronizer(index) - - def test_no_chunks(self, index, sync): - data = packb({"foo": "bar", "baz": 1234, "bar": 5678, "user": "chunks", "chunks": []}) - sync.feed(data) - assert not len(index) - - def test_simple(self, index, sync): - data = packb({"foo": "bar", "baz": 1234, "bar": 5678, "user": "chunks", "chunks": [(H(1), 1), (H(2), 2)]}) - sync.feed(data) - assert len(index) == 2 - assert index[H(1)] == (1, 1) - assert index[H(2)] == (1, 2) - - def test_multiple(self, index, sync): - data = packb({"foo": "bar", "baz": 1234, "bar": 5678, "user": "chunks", "chunks": [(H(1), 1), (H(2), 2)]}) - data += packb({"xattrs": {"security.foo": "bar", "chunks": "123456"}, "stuff": [(1, 2, 3)]}) - data += packb( - { - "xattrs": {"security.foo": "bar", "chunks": "123456"}, - "chunks": [(H(1), 1), (H(2), 2)], - "stuff": [(1, 2, 3)], - } - ) - data += packb({"chunks": [(H(3), 1)]}) - data += packb({"chunks": [(H(1), 1)]}) - - part1 = data[:70] - part2 = data[70:120] - part3 = data[120:] - sync.feed(part1) - sync.feed(part2) - sync.feed(part3) - assert len(index) == 3 - assert index[H(1)] == (3, 1) - assert index[H(2)] == (2, 2) - assert index[H(3)] == (1, 1) - - @pytest.mark.parametrize( - "elem,error", - ( - ({1: 2}, "Unexpected object: map"), - ( - bytes(213), - ["Unexpected bytes in chunks structure", "Incorrect key length"], # structure 2/3 - ), # structure 3/3 - (1, "Unexpected object: integer"), - (1.0, "Unexpected object: double"), - (True, "Unexpected object: true"), - (False, "Unexpected object: false"), - (None, "Unexpected object: nil"), - ), - ids=["map", "bytes", "int", "double", "true", "false", "none"], - ) - @pytest.mark.parametrize( - "structure", - (lambda elem: {"chunks": elem}, lambda elem: {"chunks": [elem]}, lambda elem: {"chunks": [(elem, 1)]}), - ) - def test_corrupted(self, sync, structure, elem, error): - packed = packb(structure(elem)) - with pytest.raises(ValueError) as excinfo: - sync.feed(packed) - if isinstance(error, str): - error = [error] - possible_errors = ["cache_sync_feed failed: " + error for error in error] - assert str(excinfo.value) in possible_errors - - @pytest.mark.parametrize( - "data,error", - ( - # Incorrect tuple length - ({"chunks": [(bytes(32), 2, 3, 4)]}, "Invalid chunk list entry length"), - ({"chunks": [(bytes(32),)]}, "Invalid chunk list entry length"), - # Incorrect types - ({"chunks": [(1, 2)]}, "Unexpected object: integer"), - ({"chunks": [(1, bytes(32))]}, "Unexpected object: integer"), - ({"chunks": [(bytes(32), 1.0)]}, "Unexpected object: double"), - ), - ) - def test_corrupted_ancillary(self, index, sync, data, error): - packed = packb(data) - with pytest.raises(ValueError) as excinfo: - sync.feed(packed) - assert str(excinfo.value) == "cache_sync_feed failed: " + error - - def make_index_with_refcount(self, refcount): - index_data = io.BytesIO() - index_data.write(b"BORG2IDX") - # version - index_data.write((2).to_bytes(4, "little")) - # num_entries - index_data.write((1).to_bytes(4, "little")) - # num_buckets - index_data.write((1).to_bytes(4, "little")) - # num_empty - index_data.write((0).to_bytes(4, "little")) - # key_size - index_data.write((32).to_bytes(4, "little")) - # value_size - index_data.write((3 * 4).to_bytes(4, "little")) - # reserved - index_data.write(bytes(1024 - 32)) - - index_data.write(H(0)) - index_data.write(refcount.to_bytes(4, "little")) - index_data.write((1234).to_bytes(4, "little")) - index_data.write((5678).to_bytes(4, "little")) - - index_data.seek(0) - index = ChunkIndex.read(index_data) - return index - - def test_corrupted_refcount(self): - index = self.make_index_with_refcount(ChunkIndex.MAX_VALUE + 1) - sync = CacheSynchronizer(index) - data = packb({"chunks": [(H(0), 1)]}) - with pytest.raises(ValueError) as excinfo: - sync.feed(data) - assert str(excinfo.value) == "cache_sync_feed failed: invalid reference count" - - def test_refcount_max_value(self): - index = self.make_index_with_refcount(ChunkIndex.MAX_VALUE) - sync = CacheSynchronizer(index) - data = packb({"chunks": [(H(0), 1)]}) - sync.feed(data) - assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234) - - def test_refcount_one_below_max_value(self): - index = self.make_index_with_refcount(ChunkIndex.MAX_VALUE - 1) - sync = CacheSynchronizer(index) - data = packb({"chunks": [(H(0), 1)]}) - sync.feed(data) - # Incremented to maximum - assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234) - sync.feed(data) - assert index[H(0)] == (ChunkIndex.MAX_VALUE, 1234) - - class TestAdHocCache: @pytest.fixture def repository(self, tmpdir): From fc6d459875244399f6e95166cc8aab98c302edb5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Aug 2024 01:46:55 +0200 Subject: [PATCH 15/79] cache: replace .stats() by a dummy Dummy returns all-zero stats from that call. Problem was that these values can't be computed from the chunks cache anymore. No correct refcounts, often no size information. Also removed hashindex.ChunkIndex.summarize (previously used by the above mentioned .stats() call) and .stats_against (unused) for same reason. --- src/borg/cache.py | 17 +--------- src/borg/hashindex.pyx | 59 --------------------------------- src/borg/selftest.py | 2 +- src/borg/testsuite/hashindex.py | 12 ------- 4 files changed, 2 insertions(+), 88 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 0fc38283f..7018cd001 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -398,22 +398,7 @@ def __str__(self): Summary = namedtuple("Summary", ["total_size", "unique_size", "total_unique_chunks", "total_chunks"]) def stats(self): - from .archive import Archive - - if isinstance(self, AdHocCache) and getattr(self, "chunks", None) is None: - self.chunks = self._load_chunks_from_repo() # AdHocCache usually only has .chunks after begin_txn. - - # XXX: this should really be moved down to `hashindex.pyx` - total_size, unique_size, total_unique_chunks, total_chunks = self.chunks.summarize() - # since borg 1.2 we have new archive metadata telling the total size per archive, - # so we can just sum up all archives to get the "all archives" stats: - total_size = 0 - for archive_name in self.manifest.archives: - archive = Archive(self.manifest, archive_name) - stats = archive.calc_stats(self, want_unique=False) - total_size += stats.osize - stats = self.Summary(total_size, unique_size, total_unique_chunks, total_chunks)._asdict() - return stats + return self.Summary(0, 0, 0, 0)._asdict() # dummy to not cause crash with current code def format_tuple(self): stats = self.stats() diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 94149105c..3e2757fec 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -463,65 +463,6 @@ cdef class ChunkIndex(IndexBase): iter.key = key - self.key_size return iter - def summarize(self): - cdef uint64_t size = 0, unique_size = 0, chunks = 0, unique_chunks = 0 - cdef uint32_t *values - cdef uint32_t refcount - cdef unsigned char *key = NULL - - while True: - key = hashindex_next_key(self.index, key) - if not key: - break - unique_chunks += 1 - values = (key + self.key_size) - refcount = _le32toh(values[0]) - assert refcount <= _MAX_VALUE, "invalid reference count" - chunks += refcount - unique_size += _le32toh(values[1]) - size += _le32toh(values[1]) * _le32toh(values[0]) - - return size, unique_size, unique_chunks, chunks - - def stats_against(self, ChunkIndex master_index): - """ - Calculate chunk statistics of this index against *master_index*. - - A chunk is counted as unique if the number of references - in this index matches the number of references in *master_index*. - - This index must be a subset of *master_index*. - - Return the same statistics tuple as summarize: - size, unique_size, unique_chunks, chunks. - """ - cdef uint64_t size = 0, unique_size = 0, chunks = 0, unique_chunks = 0 - cdef uint32_t our_refcount, chunk_size - cdef const uint32_t *our_values - cdef const uint32_t *master_values - cdef const unsigned char *key = NULL - cdef HashIndex *master = master_index.index - - while True: - key = hashindex_next_key(self.index, key) - if not key: - break - our_values = (key + self.key_size) - master_values = hashindex_get(master, key) - if not master_values: - raise ValueError('stats_against: key contained in self but not in master_index.') - our_refcount = _le32toh(our_values[0]) - chunk_size = _le32toh(master_values[1]) - - chunks += our_refcount - size += chunk_size * our_refcount - if our_values[0] == master_values[0]: - # our refcount equals the master's refcount, so this chunk is unique to us - unique_chunks += 1 - unique_size += chunk_size - - return size, unique_size, unique_chunks, chunks - def add(self, key, refs, size): assert len(key) == self.key_size cdef uint32_t[2] data diff --git a/src/borg/selftest.py b/src/borg/selftest.py index 8f6b693bb..53415fde1 100644 --- a/src/borg/selftest.py +++ b/src/borg/selftest.py @@ -33,7 +33,7 @@ ChunkerTestCase, ] -SELFTEST_COUNT = 33 +SELFTEST_COUNT = 32 class SelfTestResult(TestResult): diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 54e56e5f3..19a04b90e 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -141,18 +141,6 @@ def test_chunkindex_merge(self): assert idx1[H(3)] == (3, 300) assert idx1[H(4)] == (6, 400) - def test_chunkindex_summarize(self): - idx = ChunkIndex() - idx[H(1)] = 1, 1000 - idx[H(2)] = 2, 2000 - idx[H(3)] = 3, 3000 - - size, unique_size, unique_chunks, chunks = idx.summarize() - assert size == 1000 + 2 * 2000 + 3 * 3000 - assert unique_size == 1000 + 2000 + 3000 - assert chunks == 1 + 2 + 3 - assert unique_chunks == 3 - def test_flags(self): idx = NSIndex() key = H(0) From dcde48490e92cb742e86b8025685eb924d2f81f0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Aug 2024 02:11:17 +0200 Subject: [PATCH 16/79] remove CacheStatsMixin --- src/borg/archiver/rinfo_cmd.py | 1 - src/borg/cache.py | 39 ++---------------------- src/borg/helpers/parseformat.py | 4 +-- src/borg/testsuite/archiver/rinfo_cmd.py | 8 +---- 4 files changed, 5 insertions(+), 47 deletions(-) diff --git a/src/borg/archiver/rinfo_cmd.py b/src/borg/archiver/rinfo_cmd.py index bba038117..1500a8fa9 100644 --- a/src/borg/archiver/rinfo_cmd.py +++ b/src/borg/archiver/rinfo_cmd.py @@ -64,7 +64,6 @@ def do_rinfo(self, args, repository, manifest, cache): output += "Security dir: {security_dir}\n".format(**info) print(output) - print(str(cache)) def build_parser_rinfo(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog diff --git a/src/borg/cache.py b/src/borg/cache.py index 7018cd001..30e9b8f18 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -381,32 +381,6 @@ def adhoc(): return adhoc() if prefer_adhoc_cache else adhocwithfiles() -class CacheStatsMixin: - str_format = """\ -Original size: {0.total_size} -Deduplicated size: {0.unique_size} -Unique chunks: {0.total_unique_chunks} -Total chunks: {0.total_chunks} -""" - - def __init__(self, iec=False): - self.iec = iec - - def __str__(self): - return self.str_format.format(self.format_tuple()) - - Summary = namedtuple("Summary", ["total_size", "unique_size", "total_unique_chunks", "total_chunks"]) - - def stats(self): - return self.Summary(0, 0, 0, 0)._asdict() # dummy to not cause crash with current code - - def format_tuple(self): - stats = self.stats() - for field in ["total_size", "unique_size"]: - stats[field] = format_file_size(stats[field], iec=self.iec) - return self.Summary(**stats) - - class FilesCacheMixin: """ Massively accelerate processing of unchanged files by caching their chunks list. @@ -688,7 +662,7 @@ def _load_chunks_from_repo(self): return chunks -class AdHocWithFilesCache(CacheStatsMixin, FilesCacheMixin, ChunksMixin): +class AdHocWithFilesCache(FilesCacheMixin, ChunksMixin): """ Like AdHocCache, but with a files cache. """ @@ -708,7 +682,6 @@ def __init__( :param lock_wait: timeout for lock acquisition (int [s] or None [wait forever]) :param cache_mode: what shall be compared in the file stat infos vs. cached stat infos comparison """ - CacheStatsMixin.__init__(self, iec=iec) FilesCacheMixin.__init__(self, cache_mode) assert isinstance(manifest, Manifest) self.manifest = manifest @@ -850,7 +823,7 @@ def update_compatibility(self): self.cache_config.mandatory_features.update(repo_features & my_features) -class AdHocCache(CacheStatsMixin, ChunksMixin): +class AdHocCache(ChunksMixin): """ Ad-hoc, non-persistent cache. @@ -859,15 +832,7 @@ class AdHocCache(CacheStatsMixin, ChunksMixin): Chunks that were not added during the current AdHocCache lifetime won't have correct size set (0 bytes) and will have an infinite reference count (MAX_VALUE). """ - - str_format = """\ -All archives: unknown unknown unknown - - Unique chunks Total chunks -Chunk index: {0.total_unique_chunks:20d} unknown""" - def __init__(self, manifest, warn_if_unencrypted=True, lock_wait=None, iec=False): - CacheStatsMixin.__init__(self, iec=iec) assert isinstance(manifest, Manifest) self.manifest = manifest self.repository = manifest.repository diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index aabb5e288..cd67fd912 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1193,9 +1193,9 @@ def default(self, o): if isinstance(o, Archive): return o.info() if isinstance(o, (AdHocWithFilesCache, )): - return {"path": o.path, "stats": o.stats()} + return {"path": o.path} if isinstance(o, AdHocCache): - return {"stats": o.stats()} + return {} if callable(getattr(o, "to_json", None)): return o.to_json() return super().default(o) diff --git a/src/borg/testsuite/archiver/rinfo_cmd.py b/src/borg/testsuite/archiver/rinfo_cmd.py index 269c08326..d08606a22 100644 --- a/src/borg/testsuite/archiver/rinfo_cmd.py +++ b/src/borg/testsuite/archiver/rinfo_cmd.py @@ -1,5 +1,4 @@ import json -from random import randbytes from ...constants import * # NOQA from . import checkts, cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION @@ -13,7 +12,7 @@ def test_info(archivers, request): cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "create", "test", "input") info_repo = cmd(archiver, "rinfo") - assert "Original size:" in info_repo + assert "Repository ID:" in info_repo def test_info_json(archivers, request): @@ -30,8 +29,3 @@ def test_info_json(archivers, request): checkts(repository["last_modified"]) assert info_repo["encryption"]["mode"] == RK_ENCRYPTION[13:] assert "keyfile" not in info_repo["encryption"] - - cache = info_repo["cache"] - stats = cache["stats"] - assert all(isinstance(o, int) for o in stats.values()) - assert all(key in stats for key in ("total_chunks", "total_size", "total_unique_chunks", "unique_size")) From d59306f48b559245ea964401ce35bdd6a2f3f022 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Aug 2024 02:14:33 +0200 Subject: [PATCH 17/79] rinfo: remove size stats related docs, not shown any more --- src/borg/archiver/rinfo_cmd.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/borg/archiver/rinfo_cmd.py b/src/borg/archiver/rinfo_cmd.py index 1500a8fa9..91a7627f3 100644 --- a/src/borg/archiver/rinfo_cmd.py +++ b/src/borg/archiver/rinfo_cmd.py @@ -71,15 +71,6 @@ def build_parser_rinfo(self, subparsers, common_parser, mid_common_parser): rinfo_epilog = process_epilog( """ This command displays detailed information about the repository. - - Please note that the deduplicated sizes of the individual archives do not add - up to the deduplicated size of the repository ("all archives"), because the two - are meaning different things: - - This archive / deduplicated size = amount of data stored ONLY for this archive - = unique chunks of this archive. - All archives / deduplicated size = amount of data stored in the repo - = all chunks in the repository. """ ) subparser = subparsers.add_parser( From 1231c961fb558a85909f8a31499843309ed25398 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Aug 2024 16:29:17 +0200 Subject: [PATCH 18/79] blacken the code --- src/borg/archiver/_common.py | 7 +++++-- src/borg/archiver/compact_cmd.py | 12 +++++++++--- src/borg/archiver/delete_cmd.py | 1 - src/borg/cache.py | 1 + src/borg/helpers/parseformat.py | 2 +- src/borg/locking3.py | 12 ++++++++---- src/borg/manifest.py | 7 +++---- src/borg/remote.py | 10 +++++++++- src/borg/remote3.py | 20 ++++++++++++++++++-- src/borg/repoobj.py | 6 +++--- src/borg/repository3.py | 29 +++++++++++++++-------------- src/borg/testsuite/locking3.py | 6 +----- 12 files changed, 73 insertions(+), 40 deletions(-) diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 5636af1a1..6c04f1667 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -31,7 +31,9 @@ logger = create_logger(__name__) -def get_repository(location, *, create, exclusive, lock_wait, lock, append_only, make_parent_dirs, storage_quota, args, v1_or_v2): +def get_repository( + location, *, create, exclusive, lock_wait, lock, append_only, make_parent_dirs, storage_quota, args, v1_or_v2 +): if location.proto in ("ssh", "socket"): RemoteRepoCls = RemoteRepository if v1_or_v2 else RemoteRepository3 repository = RemoteRepoCls( @@ -209,7 +211,8 @@ def wrapper(self, args, **kwargs): acceptable_versions = (1, 2) if v1_or_v2 else (3,) if repository.version not in acceptable_versions: raise Error( - f"This borg version only accepts version {' or '.join(acceptable_versions)} repos for --other-repo.") + f"This borg version only accepts version {' or '.join(acceptable_versions)} repos for --other-repo." + ) kwargs["other_repository"] = repository if manifest or cache: manifest_ = Manifest.load( diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index 43d26be24..a546624db 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -11,6 +11,7 @@ from ..repository3 import Repository3 from ..logger import create_logger + logger = create_logger() @@ -32,7 +33,13 @@ def garbage_collect(self): logger.info("Getting object IDs present in the repository...") self.repository_chunks = self.get_repository_chunks() logger.info("Computing object IDs used by archives...") - self.used_chunks, self.wanted_chunks, self.total_files, self.total_size, self.archives_count = self.analyze_archives() + ( + self.used_chunks, + self.wanted_chunks, + self.total_files, + self.total_size, + self.archives_count, + ) = self.analyze_archives() self.report_and_delete() logger.info("Finished compaction / garbage collection...") @@ -109,8 +116,7 @@ def report_and_delete(self): if unused: logger.info(f"Deleting {len(unused)} unused objects...") pi = ProgressIndicatorPercent( - total=len(unused), msg="Deleting unused objects %3.1f%%", step=0.1, - msgid="compact.report_and_delete" + total=len(unused), msg="Deleting unused objects %3.1f%%", step=0.1, msgid="compact.report_and_delete" ) for i, id in enumerate(unused): pi.show(i) diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index 275607299..434204c47 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -51,7 +51,6 @@ def do_delete(self, args, repository): self.print_warning("Aborted.", wc=None) return - def build_parser_delete(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog, define_archive_filters_group diff --git a/src/borg/cache.py b/src/borg/cache.py index 30e9b8f18..3e7f113f6 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -832,6 +832,7 @@ class AdHocCache(ChunksMixin): Chunks that were not added during the current AdHocCache lifetime won't have correct size set (0 bytes) and will have an infinite reference count (MAX_VALUE). """ + def __init__(self, manifest, warn_if_unencrypted=True, lock_wait=None, iec=False): assert isinstance(manifest, Manifest) self.manifest = manifest diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index cd67fd912..ea0590836 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1192,7 +1192,7 @@ def default(self, o): return {"id": bin_to_hex(o.id), "location": o._location.canonical_path()} if isinstance(o, Archive): return o.info() - if isinstance(o, (AdHocWithFilesCache, )): + if isinstance(o, (AdHocWithFilesCache,)): return {"path": o.path} if isinstance(o, AdHocCache): return {} diff --git a/src/borg/locking3.py b/src/borg/locking3.py index 92922aec3..091dbbf44 100644 --- a/src/borg/locking3.py +++ b/src/borg/locking3.py @@ -65,7 +65,7 @@ class Lock: matter how (e.g. if an exception occurred). """ - def __init__(self, store, exclusive=False, sleep=None, timeout=1.0, stale=30*60, id=None): + def __init__(self, store, exclusive=False, sleep=None, timeout=1.0, stale=30 * 60, id=None): self.store = store self.is_exclusive = exclusive self.sleep = sleep @@ -75,7 +75,7 @@ def __init__(self, store, exclusive=False, sleep=None, timeout=1.0, stale=30*60, self.retry_delay_min = 1.0 self.retry_delay_max = 5.0 self.stale_td = datetime.timedelta(seconds=stale) # ignore/delete it if older - self.refresh_td = datetime.timedelta(seconds=stale//2) # don't refresh it if younger + self.refresh_td = datetime.timedelta(seconds=stale // 2) # don't refresh it if younger self.last_refresh_dt = None self.id = id or platform.get_process_id() assert len(self.id) == 3 @@ -134,7 +134,9 @@ def _find_locks(self, *, only_exclusive=False, only_mine=False): found_locks = [] for key in locks: lock = locks[key] - if (not only_exclusive or lock["exclusive"]) and (not only_mine or (lock["hostid"], lock["processid"], lock["threadid"]) == self.id): + if (not only_exclusive or lock["exclusive"]) and ( + not only_mine or (lock["hostid"], lock["processid"], lock["threadid"]) == self.id + ): found_locks.append(lock) return found_locks @@ -150,7 +152,9 @@ def acquire(self): key = self._create_lock(exclusive=self.is_exclusive) # obviously we have a race condition here: other client(s) might have created exclusive # lock(s) at the same time in parallel. thus we have to check again. - time.sleep(self.race_recheck_delay) # give other clients time to notice our exclusive lock, stop creating theirs + time.sleep( + self.race_recheck_delay + ) # give other clients time to notice our exclusive lock, stop creating theirs exclusive_locks = self._find_locks(only_exclusive=True) if self.is_exclusive: if len(exclusive_locks) == 1 and exclusive_locks[0]["key"] == key: diff --git a/src/borg/manifest.py b/src/borg/manifest.py index afe6d89af..4c4664ffb 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -8,6 +8,7 @@ from borgstore.store import ObjectNotFound, ItemInfo from .logger import create_logger + logger = create_logger() from .constants import * # NOQA @@ -263,6 +264,7 @@ def load(cls, repository, operations, key=None, *, ro_cls=RepoObj): if isinstance(repository, (Repository3, RemoteRepository3)): from .helpers import msgpack + archives = {} try: infos = list(repository.store_list("archives")) @@ -357,10 +359,7 @@ def write(self): manifest_archives = StableDict(self.archives.get_raw_dict()) manifest = ManifestItem( - version=2, - archives=manifest_archives, - timestamp=self.timestamp, - config=StableDict(self.config), + version=2, archives=manifest_archives, timestamp=self.timestamp, config=StableDict(self.config) ) data = self.key.pack_metadata(manifest.as_dict()) self.id = self.repo_objs.id_hash(data) diff --git a/src/borg/remote.py b/src/borg/remote.py index 65d81b163..fc3bf1384 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -945,7 +945,15 @@ def handle_error(unpacked): v1_or_v2={"since": parse_version("2.0.0b8"), "previously": True}, # TODO fix version ) def open( - self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False, make_parent_dirs=False, v1_or_v2=False + self, + path, + create=False, + lock_wait=None, + lock=True, + exclusive=False, + append_only=False, + make_parent_dirs=False, + v1_or_v2=False, ): """actual remoting is done via self.call in the @api decorator""" diff --git a/src/borg/remote3.py b/src/borg/remote3.py index 1345a4e0b..e87a3dbfe 100644 --- a/src/borg/remote3.py +++ b/src/borg/remote3.py @@ -373,7 +373,15 @@ def _resolve_path(self, path): return os.path.realpath(path) def open( - self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False, make_parent_dirs=False, v1_or_v2=False + self, + path, + create=False, + lock_wait=None, + lock=True, + exclusive=None, + append_only=False, + make_parent_dirs=False, + v1_or_v2=False, ): self.RepoCls = Repository if v1_or_v2 else Repository3 self.rpc_methods = self._rpc_methods if v1_or_v2 else self._rpc_methods3 @@ -975,7 +983,15 @@ def handle_error(unpacked): v1_or_v2={"since": parse_version("2.0.0b8"), "previously": True}, # TODO fix version ) def open( - self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False, make_parent_dirs=False, v1_or_v2=False + self, + path, + create=False, + lock_wait=None, + lock=True, + exclusive=False, + append_only=False, + make_parent_dirs=False, + v1_or_v2=False, ): """actual remoting is done via self.call in the @api decorator""" diff --git a/src/borg/repoobj.py b/src/borg/repoobj.py index 55208a457..64c054325 100644 --- a/src/borg/repoobj.py +++ b/src/borg/repoobj.py @@ -22,7 +22,7 @@ def extract_crypted_data(cls, data: bytes) -> bytes: # used for crypto type detection hdr_size = cls.obj_header.size hdr = cls.ObjHeader(*cls.obj_header.unpack(data[:hdr_size])) - return data[hdr_size + hdr.meta_size:] + return data[hdr_size + hdr.meta_size :] def __init__(self, key): self.key = key @@ -80,7 +80,7 @@ def parse_meta(self, id: bytes, cdata: bytes, ro_type: str) -> dict: hdr_size = self.obj_header.size hdr = self.ObjHeader(*self.obj_header.unpack(obj[:hdr_size])) assert hdr_size + hdr.meta_size <= len(obj) - meta_encrypted = obj[hdr_size:hdr_size + hdr.meta_size] + meta_encrypted = obj[hdr_size : hdr_size + hdr.meta_size] meta_packed = self.key.decrypt(id, meta_encrypted) meta = msgpack.unpackb(meta_packed) if ro_type != ROBJ_DONTCARE and meta["type"] != ro_type: @@ -114,7 +114,7 @@ def parse( if ro_type != ROBJ_DONTCARE and meta_compressed["type"] != ro_type: raise IntegrityError(f"ro_type expected: {ro_type} got: {meta_compressed['type']}") assert hdr_size + hdr.meta_size + hdr.data_size <= len(obj) - data_encrypted = obj[hdr_size + hdr.meta_size:hdr_size + hdr.meta_size + hdr.data_size] + data_encrypted = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size] data_compressed = self.key.decrypt(id, data_encrypted) # does not include the type/level bytes if decompress: ctype = meta_compressed["ctype"] diff --git a/src/borg/repository3.py b/src/borg/repository3.py index 3b8e15b9f..3aa9fe3eb 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -104,7 +104,7 @@ def __init__( self._send_log = send_log_cb or (lambda: None) self.do_create = create self.created = False - self.acceptable_repo_versions = (3, ) + self.acceptable_repo_versions = (3,) self.opened = False self.append_only = append_only # XXX not implemented / not implementable self.storage_quota = storage_quota # XXX not implemented @@ -196,7 +196,13 @@ def close(self): def info(self): """return some infos about the repo (must be opened first)""" - info = dict(id=self.id, version=self.version, storage_quota_use=self.storage_quota_use, storage_quota=self.storage_quota, append_only=self.append_only) + info = dict( + id=self.id, + version=self.version, + storage_quota_use=self.storage_quota_use, + storage_quota=self.storage_quota, + append_only=self.append_only, + ) return info def commit(self, compact=True, threshold=0.1): @@ -204,6 +210,7 @@ def commit(self, compact=True, threshold=0.1): def check(self, repair=False, max_duration=0): """Check repository consistency""" + def log_error(msg): nonlocal obj_corrupted obj_corrupted = True @@ -228,12 +235,12 @@ def log_error(msg): obj_size = len(obj) if obj_size >= hdr_size: hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size])) - meta = obj[hdr_size:hdr_size+hdr.meta_size] + meta = obj[hdr_size : hdr_size + hdr.meta_size] if hdr.meta_size != len(meta): log_error("metadata size incorrect.") elif hdr.meta_hash != xxh64(meta): log_error("metadata does not match checksum.") - data = obj[hdr_size+hdr.meta_size:hdr_size+hdr.meta_size+hdr.data_size] + data = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size] if hdr.data_size != len(data): log_error("data size incorrect.") elif hdr.data_hash != xxh64(data): @@ -276,12 +283,11 @@ def list(self, limit=None, marker=None, mask=0, value=0): ids = [] if marker is not None: idx = ids.index(marker) - ids = ids[idx + 1:] + ids = ids[idx + 1 :] if limit is not None: return ids[:limit] return ids - def scan(self, limit=None, state=None): """ list (the next) chunk IDs from the repository. @@ -312,24 +318,19 @@ def get(self, id, read_data=True): obj = self.store.load(key, size=hdr_size + extra_size) hdr = obj[0:hdr_size] if len(hdr) != hdr_size: - raise IntegrityError( - f"Object too small [id {id_hex}]: expected {hdr_size}, got {len(hdr)} bytes" - ) + raise IntegrityError(f"Object too small [id {id_hex}]: expected {hdr_size}, got {len(hdr)} bytes") meta_size = RepoObj.obj_header.unpack(hdr)[0] if meta_size > extra_size: # we did not get enough, need to load more, but not all. # this should be rare, as chunk metadata is rather small usually. obj = self.store.load(key, size=hdr_size + meta_size) - meta = obj[hdr_size:hdr_size + meta_size] + meta = obj[hdr_size : hdr_size + meta_size] if len(meta) != meta_size: - raise IntegrityError( - f"Object too small [id {id_hex}]: expected {meta_size}, got {len(meta)} bytes" - ) + raise IntegrityError(f"Object too small [id {id_hex}]: expected {meta_size}, got {len(meta)} bytes") return hdr + meta except StoreObjectNotFound: raise self.ObjectNotFound(id, self.path) from None - def get_many(self, ids, read_data=True, is_preloaded=False): for id_ in ids: yield self.get(id_, read_data=read_data) diff --git a/src/borg/testsuite/locking3.py b/src/borg/testsuite/locking3.py index f3976fa83..7a50d3dec 100644 --- a/src/borg/testsuite/locking3.py +++ b/src/borg/testsuite/locking3.py @@ -4,11 +4,7 @@ from borgstore.store import Store -from ..locking3 import ( - Lock, - LockFailed, - NotLocked, -) +from ..locking3 import Lock, LockFailed, NotLocked ID1 = "foo", 1, 1 ID2 = "bar", 2, 2 From 3e7a4cd814a73acdbf95a26524ddc50a54f092c0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 14 Aug 2024 16:35:29 +0200 Subject: [PATCH 19/79] make ruff happy --- src/borg/archiver/_common.py | 4 ++-- src/borg/crypto/keymanager.py | 5 ----- src/borg/testsuite/cache.py | 3 --- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 6c04f1667..a6d3e57ac 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -1,4 +1,3 @@ -import argparse import functools import os import textwrap @@ -211,7 +210,8 @@ def wrapper(self, args, **kwargs): acceptable_versions = (1, 2) if v1_or_v2 else (3,) if repository.version not in acceptable_versions: raise Error( - f"This borg version only accepts version {' or '.join(acceptable_versions)} repos for --other-repo." + f"This borg version only accepts version {' or '.join(acceptable_versions)} " + f"repos for --other-repo." ) kwargs["other_repository"] = repository if manifest or cache: diff --git a/src/borg/crypto/keymanager.py b/src/borg/crypto/keymanager.py index fe5050f55..63335c445 100644 --- a/src/borg/crypto/keymanager.py +++ b/src/borg/crypto/keymanager.py @@ -3,12 +3,7 @@ import textwrap from hashlib import sha256 -from borgstore.store import ObjectNotFound as StoreObjectNotFound - from ..helpers import Error, yes, bin_to_hex, hex_to_bin, dash_open -from ..manifest import Manifest, NoManifestError -from ..repository3 import Repository3 -from ..repository import Repository from ..repoobj import RepoObj diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index 28846d0ad..b9959687b 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -1,8 +1,5 @@ -import io import os.path -from ..helpers.msgpack import packb - import pytest from .hashindex import H From cb9ff3b49028ac0c59d18cc66e8b590f1e844a47 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 15 Aug 2024 01:10:51 +0200 Subject: [PATCH 20/79] fuse/mount code and test fixes --- src/borg/fuse.py | 4 ++-- src/borg/repository3.py | 5 +++++ src/borg/testsuite/archiver/mount_cmds.py | 2 +- src/borg/testsuite/locking3.py | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/borg/fuse.py b/src/borg/fuse.py index dd77f4016..22dd4a4e1 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -46,7 +46,7 @@ def async_wrapper(fn): from .item import Item from .platform import uid2user, gid2group from .platformflags import is_darwin -from .remote import RemoteRepository # TODO 3 +from .remote3 import RemoteRepository3 def fuse_main(): @@ -546,7 +546,7 @@ def pop_option(options, key, present, not_present, wanted_type, int_base=0): self._create_filesystem() llfuse.init(self, mountpoint, options) if not foreground: - if isinstance(self.repository_uncached, RemoteRepository): + if isinstance(self.repository_uncached, RemoteRepository3): daemonize() else: with daemonizing() as (old_id, new_id): diff --git a/src/borg/repository3.py b/src/borg/repository3.py index 3aa9fe3eb..eb1827cf4 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -379,6 +379,11 @@ def preload(self, ids): def break_lock(self): Lock(self.store).break_lock() + def migrate_lock(self, old_id, new_id): + # note: only needed for local repos + if self.lock is not None: + self.lock.migrate_lock(old_id, new_id) + def get_manifest(self): try: return self.store.load("config/manifest") diff --git a/src/borg/testsuite/archiver/mount_cmds.py b/src/borg/testsuite/archiver/mount_cmds.py index 136eb145f..0f05e92f2 100644 --- a/src/borg/testsuite/archiver/mount_cmds.py +++ b/src/borg/testsuite/archiver/mount_cmds.py @@ -7,7 +7,7 @@ from ... import xattr, platform from ...constants import * # NOQA -from ...locking import Lock +from ...locking3 import Lock from ...helpers import flags_noatime, flags_normal from .. import has_lchflags, llfuse from .. import changedir, no_selinux, same_ts_ns diff --git a/src/borg/testsuite/locking3.py b/src/borg/testsuite/locking3.py index 7a50d3dec..b9c4d3697 100644 --- a/src/borg/testsuite/locking3.py +++ b/src/borg/testsuite/locking3.py @@ -84,3 +84,17 @@ def test_lock_refresh_stale_removal(self, lockstore): assert len(lock_keys_b00) == 1 assert len(lock_keys_b21) == 0 # stale lock was ignored assert len(list(lock.store.list("locks"))) == 0 # stale lock was removed from store + + def test_migrate_lock(self, lockstore): + old_id, new_id = ID1, ID2 + assert old_id[1] != new_id[1] # different PIDs (like when doing daemonize()) + lock = Lock(lockstore, id=old_id).acquire() + old_locks = lock._find_locks(only_mine=True) + assert lock.id == old_id # lock is for old id / PID + lock.migrate_lock(old_id, new_id) # fix the lock + assert lock.id == new_id # lock corresponds to the new id / PID + new_locks = lock._find_locks(only_mine=True) + assert old_locks != new_locks + assert len(old_locks) == len(new_locks) == 1 + assert old_locks[0]["hostid"] == old_id[0] + assert new_locks[0]["hostid"] == new_id[0] From bfbf3ba7aa748117e78822e4b947aefd4e4b0452 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 17 Aug 2024 12:06:29 +0200 Subject: [PATCH 21/79] repository3.check: implement --repair Tests were a bit tricky as there is validation on 2 layers now: - repository3 does an xxh64 check, finds most corruptions already - on the archives level, borg also does an even stronger cryptographic check --- src/borg/repository3.py | 62 ++++++++++++++-------- src/borg/testsuite/archiver/check_cmd.py | 65 +++++++++++++++++++++--- 2 files changed, 101 insertions(+), 26 deletions(-) diff --git a/src/borg/repository3.py b/src/borg/repository3.py index eb1827cf4..fe05ff57f 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -216,7 +216,26 @@ def log_error(msg): obj_corrupted = True logger.error(f"Repo object {info.name} is corrupted: {msg}") - # TODO: implement repair, progress indicator, partial checks, ... + def check_object(obj): + """Check if obj looks valid.""" + hdr_size = RepoObj.obj_header.size + obj_size = len(obj) + if obj_size >= hdr_size: + hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size])) + meta = obj[hdr_size : hdr_size + hdr.meta_size] + if hdr.meta_size != len(meta): + log_error("metadata size incorrect.") + elif hdr.meta_hash != xxh64(meta): + log_error("metadata does not match checksum.") + data = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size] + if hdr.data_size != len(data): + log_error("data size incorrect.") + elif hdr.data_hash != xxh64(data): + log_error("data does not match checksum.") + else: + log_error("too small.") + + # TODO: progress indicator, partial checks, ... mode = "full" logger.info("Starting repository check") objs_checked = objs_errors = 0 @@ -224,40 +243,43 @@ def log_error(msg): try: for info in infos: self._lock_refresh() - obj_corrupted = False key = "data/%s" % info.name try: obj = self.store.load(key) except StoreObjectNotFound: # looks like object vanished since store.list(), ignore that. continue - hdr_size = RepoObj.obj_header.size - obj_size = len(obj) - if obj_size >= hdr_size: - hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size])) - meta = obj[hdr_size : hdr_size + hdr.meta_size] - if hdr.meta_size != len(meta): - log_error("metadata size incorrect.") - elif hdr.meta_hash != xxh64(meta): - log_error("metadata does not match checksum.") - data = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size] - if hdr.data_size != len(data): - log_error("data size incorrect.") - elif hdr.data_hash != xxh64(data): - log_error("data does not match checksum.") - else: - log_error("too small.") + obj_corrupted = False + check_object(obj) objs_checked += 1 if obj_corrupted: objs_errors += 1 + if repair: + # if it is corrupted, we can't do much except getting rid of it. + # but let's just retry loading it, in case the error goes away. + try: + obj = self.store.load(key) + except StoreObjectNotFound: + log_error("existing object vanished.") + else: + obj_corrupted = False + check_object(obj) + if obj_corrupted: + log_error("reloading did not help, deleting it!") + self.store.delete(key) + else: + log_error("reloading did help, inconsistent behaviour detected!") except StoreObjectNotFound: # it can be that there is no "data/" at all, then it crashes when iterating infos. pass logger.info(f"Checked {objs_checked} repository objects, {objs_errors} errors.") if objs_errors == 0: - logger.info("Finished %s repository check, no problems found.", mode) + logger.info(f"Finished {mode} repository check, no problems found.") else: - logger.error("Finished %s repository check, errors found.", mode) + if repair: + logger.info(f"Finished {mode} repository check, errors found and repaired.") + else: + logger.error(f"Finished {mode} repository check, errors found.") return objs_errors == 0 or repair def scan_low_level(self, segment=None, offset=None): diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index 14683682c..3b9344076 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -360,6 +360,55 @@ def test_extra_chunks(archivers, request): @pytest.mark.parametrize("init_args", [["--encryption=repokey-aes-ocb"], ["--encryption", "none"]]) def test_verify_data(archivers, request, init_args): + archiver = request.getfixturevalue(archivers) + if archiver.get_kind() != "local": + pytest.skip("only works locally, patches objects") + + # it's tricky to test the cryptographic data verification, because usually already the + # repository-level xxh64 hash fails to verify. So we use a fake one that doesn't. + # note: it only works like tested here for a highly engineered data corruption attack, + # because with accidental corruption, usually already the xxh64 low-level check fails. + def fake_xxh64(data, seed=0): + return b"fakefake" + + import borg.repoobj + import borg.repository3 + + with patch.object(borg.repoobj, "xxh64", fake_xxh64), patch.object(borg.repository3, "xxh64", fake_xxh64): + check_cmd_setup(archiver) + shutil.rmtree(archiver.repository_path) + cmd(archiver, "rcreate", *init_args) + create_src_archive(archiver, "archive1") + archive, repository = open_archive(archiver.repository_path, "archive1") + with repository: + for item in archive.iter_items(): + if item.path.endswith(src_file): + chunk = item.chunks[-1] + data = repository.get(chunk.id) + data = data[0:123] + b"x" + data[123:] + repository.put(chunk.id, data) + break + repository.commit(compact=False) + + # the normal archives check does not read file content data. + cmd(archiver, "check", "--archives-only", exit_code=0) + # but with --verify-data, it does and notices the issue. + output = cmd(archiver, "check", "--archives-only", "--verify-data", exit_code=1) + assert f"{bin_to_hex(chunk.id)}, integrity error" in output + + # repair (heal is tested in another test) + output = cmd(archiver, "check", "--repair", "--verify-data", exit_code=0) + assert f"{bin_to_hex(chunk.id)}, integrity error" in output + assert f"{src_file}: New missing file chunk detected" in output + + # run with --verify-data again, all fine now (file was patched with a replacement chunk). + cmd(archiver, "check", "--archives-only", "--verify-data", exit_code=0) + + +@pytest.mark.parametrize("init_args", [["--encryption=repokey-aes-ocb"], ["--encryption", "none"]]) +def test_corrupted_file_chunk(archivers, request, init_args): + ## similar to test_verify_data, but here we let the low level repository-only checks discover the issue. + archiver = request.getfixturevalue(archivers) check_cmd_setup(archiver) shutil.rmtree(archiver.repository_path) @@ -371,19 +420,23 @@ def test_verify_data(archivers, request, init_args): if item.path.endswith(src_file): chunk = item.chunks[-1] data = repository.get(chunk.id) - data = data[0:100] + b"x" + data[101:] + data = data[0:123] + b"x" + data[123:] repository.put(chunk.id, data) break repository.commit(compact=False) - cmd(archiver, "check", exit_code=1) - output = cmd(archiver, "check", "--verify-data", exit_code=1) - assert bin_to_hex(chunk.id) + ", integrity error" in output + + # the normal check checks all repository objects and the xxh64 checksum fails. + output = cmd(archiver, "check", "--repository-only", exit_code=1) + assert f"{bin_to_hex(chunk.id)} is corrupted: data does not match checksum." in output # repair (heal is tested in another test) - output = cmd(archiver, "check", "--repair", "--verify-data", exit_code=0) - assert bin_to_hex(chunk.id) + ", integrity error" in output + output = cmd(archiver, "check", "--repair", exit_code=0) + assert f"{bin_to_hex(chunk.id)} is corrupted: data does not match checksum." in output assert f"{src_file}: New missing file chunk detected" in output + # run normal check again, all fine now (file was patched with a replacement chunk). + cmd(archiver, "check", "--repository-only", exit_code=0) + def test_empty_repository(archivers, request): archiver = request.getfixturevalue(archivers) From 1189fc3495b560a0a3322bf245d88e1d90bd1ad6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 17 Aug 2024 16:41:14 +0200 Subject: [PATCH 22/79] debug dump-repo-objs: remove --ghost This was used for an implementation detail of the borg 1.x repository code, dumping uncommitted objects. Not needed any more. Also remove local repository method scan_low_level, it was only used by --ghost. --- src/borg/archiver/debug_cmd.py | 75 ++++++++-------------------------- src/borg/repository.py | 25 ------------ src/borg/repository3.py | 3 -- 3 files changed, 16 insertions(+), 87 deletions(-) diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index d3e03a364..d513224db 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -11,11 +11,11 @@ from ..helpers import bin_to_hex, hex_to_bin, prepare_dump_dict from ..helpers import dash_open from ..helpers import StableDict -from ..helpers import positive_int_validator, archivename_validator +from ..helpers import archivename_validator from ..helpers import CommandError, RTError from ..manifest import Manifest from ..platform import get_process_id -from ..repository import Repository, TAG_PUT, TAG_DELETE, TAG_COMMIT +from ..repository import Repository from ..repository3 import Repository3, LIST_SCAN_LIMIT from ..repoobj import RepoObj @@ -127,40 +127,21 @@ def decrypt_dump(i, id, cdata, tag=None, segment=None, offset=None): with open(filename, "wb") as fd: fd.write(data) - if args.ghost: - # dump ghosty stuff from segment files: not yet committed objects, deleted / superseded objects, commit tags - - # set up the key without depending on a manifest obj - for id, cdata, tag, segment, offset in repository.scan_low_level(): - if tag == TAG_PUT: - key = key_factory(repository, cdata) - repo_objs = RepoObj(key) - break - i = 0 - for id, cdata, tag, segment, offset in repository.scan_low_level(segment=args.segment, offset=args.offset): - if tag == TAG_PUT: - decrypt_dump(i, id, cdata, tag="put", segment=segment, offset=offset) - elif tag == TAG_DELETE: - decrypt_dump(i, id, None, tag="del", segment=segment, offset=offset) - elif tag == TAG_COMMIT: - decrypt_dump(i, None, None, tag="commit", segment=segment, offset=offset) + # set up the key without depending on a manifest obj + ids = repository.list(limit=1, marker=None) + cdata = repository.get(ids[0]) + key = key_factory(repository, cdata) + repo_objs = RepoObj(key) + state = None + i = 0 + while True: + ids, state = repository.scan(limit=LIST_SCAN_LIMIT, state=state) # must use on-disk order scanning here + if not ids: + break + for id in ids: + cdata = repository.get(id) + decrypt_dump(i, id, cdata) i += 1 - else: - # set up the key without depending on a manifest obj - ids = repository.list(limit=1, marker=None) - cdata = repository.get(ids[0]) - key = key_factory(repository, cdata) - repo_objs = RepoObj(key) - state = None - i = 0 - while True: - ids, state = repository.scan(limit=LIST_SCAN_LIMIT, state=state) # must use on-disk order scanning here - if not ids: - break - for id in ids: - cdata = repository.get(id) - decrypt_dump(i, id, cdata) - i += 1 print("Done.") @with_repository(manifest=False) @@ -469,30 +450,6 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): help="dump repo objects (debug)", ) subparser.set_defaults(func=self.do_debug_dump_repo_objs) - subparser.add_argument( - "--ghost", - dest="ghost", - action="store_true", - help="dump all segment file contents, including deleted/uncommitted objects and commits.", - ) - subparser.add_argument( - "--segment", - metavar="SEG", - dest="segment", - type=positive_int_validator, - default=None, - action=Highlander, - help="used together with --ghost: limit processing to given segment.", - ) - subparser.add_argument( - "--offset", - metavar="OFFS", - dest="offset", - type=positive_int_validator, - default=None, - action=Highlander, - help="used together with --ghost: limit processing to given offset.", - ) debug_search_repo_objs_epilog = process_epilog( """ diff --git a/src/borg/repository.py b/src/borg/repository.py index 9f2b7509c..65d4cb477 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1186,31 +1186,6 @@ def report_error(msg, *args): logger.info("Finished %s repository check, no problems found.", mode) return not error_found or repair - def scan_low_level(self, segment=None, offset=None): - """Very low level scan over all segment file entries. - - It does NOT care about what's committed and what not. - It does NOT care whether an object might be deleted or superseded later. - It just yields anything it finds in the segment files. - - This is intended as a last-resort way to get access to all repo contents of damaged repos, - when there is uncommitted, but valuable data in there... - - When segment or segment+offset is given, limit processing to this location only. - """ - for current_segment, filename in self.io.segment_iterator(start_segment=segment, end_segment=segment): - try: - for tag, key, current_offset, _, data in self.io.iter_objects( - segment=current_segment, offset=offset or 0 - ): - if offset is not None and current_offset > offset: - break - yield key, data, tag, current_segment, current_offset - except IntegrityError as err: - logger.error( - "Segment %d (%s) has IntegrityError(s) [%s] - skipping." % (current_segment, filename, str(err)) - ) - def _rollback(self, *, cleanup): if cleanup: self.io.cleanup(self.io.get_segments_transaction_id()) diff --git a/src/borg/repository3.py b/src/borg/repository3.py index fe05ff57f..78a98225d 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -282,9 +282,6 @@ def check_object(obj): logger.error(f"Finished {mode} repository check, errors found.") return objs_errors == 0 or repair - def scan_low_level(self, segment=None, offset=None): - raise NotImplementedError - def __len__(self): raise NotImplementedError From 60edc8255fa6db23b9c88ea2cce459757094415b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 18 Aug 2024 14:17:01 +0200 Subject: [PATCH 23/79] repository/repository3: remove .scan method This was an implementation specific "in on-disk order" list method that made sense with borg 1.x log-like segment files only. But we now store objects separately, so there is no "in on-disk order" anymore. --- src/borg/archive.py | 5 +- src/borg/archiver/debug_cmd.py | 24 +++---- src/borg/archiver/rcompress_cmd.py | 5 +- src/borg/constants.py | 2 +- src/borg/remote.py | 5 -- src/borg/remote3.py | 6 -- src/borg/repository.py | 68 ++------------------ src/borg/repository3.py | 14 ---- src/borg/testsuite/archiver/debug_cmds.py | 2 +- src/borg/testsuite/archiver/rcompress_cmd.py | 5 +- src/borg/testsuite/repository.py | 50 -------------- src/borg/testsuite/repository3.py | 14 ---- 12 files changed, 26 insertions(+), 174 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 39ba8dcba..44def4fe7 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1843,11 +1843,12 @@ def verify_data(self): pi = ProgressIndicatorPercent( total=chunks_count_index, msg="Verifying data %6.2f%%", step=0.01, msgid="check.verify_data" ) - state = None + marker = None while True: - chunk_ids, state = self.repository.scan(limit=100, state=state) + chunk_ids = self.repository.list(limit=100, marker=marker) if not chunk_ids: break + marker = chunk_ids[-1] chunks_count_segments += len(chunk_ids) chunk_data_iter = self.repository.get_many(chunk_ids) chunk_ids_revd = list(reversed(chunk_ids)) diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index d513224db..82df95b35 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -110,19 +110,15 @@ def do_debug_dump_manifest(self, args, repository, manifest): @with_repository(manifest=False) def do_debug_dump_repo_objs(self, args, repository): - """dump (decrypted, decompressed) repo objects, repo index MUST be current/correct""" + """dump (decrypted, decompressed) repo objects""" from ..crypto.key import key_factory - def decrypt_dump(i, id, cdata, tag=None, segment=None, offset=None): + def decrypt_dump(id, cdata): if cdata is not None: _, data = repo_objs.parse(id, cdata, ro_type=ROBJ_DONTCARE) else: _, data = {}, b"" - tag_str = "" if tag is None else "_" + tag - segment_str = "_" + str(segment) if segment is not None else "" - offset_str = "_" + str(offset) if offset is not None else "" - id_str = "_" + bin_to_hex(id) if id is not None else "" - filename = "%08d%s%s%s%s.obj" % (i, segment_str, offset_str, tag_str, id_str) + filename = f"{bin_to_hex(id)}.obj" print("Dumping", filename) with open(filename, "wb") as fd: fd.write(data) @@ -132,16 +128,15 @@ def decrypt_dump(i, id, cdata, tag=None, segment=None, offset=None): cdata = repository.get(ids[0]) key = key_factory(repository, cdata) repo_objs = RepoObj(key) - state = None - i = 0 + marker = None while True: - ids, state = repository.scan(limit=LIST_SCAN_LIMIT, state=state) # must use on-disk order scanning here + ids = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) if not ids: break + marker = ids[-1] for id in ids: cdata = repository.get(id) - decrypt_dump(i, id, cdata) - i += 1 + decrypt_dump(id, cdata) print("Done.") @with_repository(manifest=False) @@ -179,14 +174,15 @@ def print_finding(info, wanted, data, offset): key = key_factory(repository, cdata) repo_objs = RepoObj(key) - state = None + marker = None last_data = b"" last_id = None i = 0 while True: - ids, state = repository.scan(limit=LIST_SCAN_LIMIT, state=state) # must use on-disk order scanning here + ids = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) if not ids: break + marker = ids[-1] for id in ids: cdata = repository.get(id) _, data = repo_objs.parse(id, cdata, ro_type=ROBJ_DONTCARE) diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index 7b27b83bc..e9c36cfec 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -24,12 +24,13 @@ def find_chunks(repository, repo_objs, stats, ctype, clevel, olevel): recompress_ids = [] compr_keys = stats["compr_keys"] = set() compr_wanted = ctype, clevel, olevel - state = None + marker = None chunks_limit = 1000 while True: - chunk_ids, state = repository.scan(limit=chunks_limit, state=state) + chunk_ids = repository.list(limit=chunks_limit, marker=marker) if not chunk_ids: break + marker = chunk_ids[-1] for id, chunk_no_data in zip(chunk_ids, repository.get_many(chunk_ids, read_data=False)): meta = repo_objs.parse_meta(id, chunk_no_data, ro_type=ROBJ_DONTCARE) compr_found = meta["ctype"], meta["clevel"], meta.get("olevel", -1) diff --git a/src/borg/constants.py b/src/borg/constants.py index 76e63f792..0511f62de 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -78,7 +78,7 @@ # MAX_DATA_SIZE or it will trigger the check for that. MAX_ARCHIVES = 400000 -# repo.list() / .scan() result count limit the borg client uses +# repo.list() result count limit the borg client uses LIST_SCAN_LIMIT = 100000 FD_MAX_AGE = 4 * 60 # 4 minutes diff --git a/src/borg/remote.py b/src/borg/remote.py index fc3bf1384..5cf591bed 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -145,7 +145,6 @@ class RepositoryServer: # pragma: no cover "flags_many", "get", "list", - "scan", "negotiate", "open", "close", @@ -993,10 +992,6 @@ def __len__(self): def list(self, limit=None, marker=None, mask=0, value=0): """actual remoting is done via self.call in the @api decorator""" - @api(since=parse_version("2.0.0b3")) - def scan(self, limit=None, state=None): - """actual remoting is done via self.call in the @api decorator""" - @api(since=parse_version("2.0.0b2")) def flags(self, id, mask=0xFFFFFFFF, value=None): """actual remoting is done via self.call in the @api decorator""" diff --git a/src/borg/remote3.py b/src/borg/remote3.py index e87a3dbfe..b3c08ca48 100644 --- a/src/borg/remote3.py +++ b/src/borg/remote3.py @@ -147,7 +147,6 @@ class RepositoryServer: # pragma: no cover "flags_many", "get", "list", - "scan", "negotiate", "open", "close", @@ -168,7 +167,6 @@ class RepositoryServer: # pragma: no cover "destroy", "get", "list", - "scan", "negotiate", "open", "close", @@ -1031,10 +1029,6 @@ def __len__(self): def list(self, limit=None, marker=None, mask=0, value=0): """actual remoting is done via self.call in the @api decorator""" - @api(since=parse_version("2.0.0b3")) - def scan(self, limit=None, state=None): - """actual remoting is done via self.call in the @api decorator""" - def get(self, id, read_data=True): for resp in self.get_many([id], read_data=read_data): return resp diff --git a/src/borg/repository.py b/src/borg/repository.py index 65d4cb477..334f33e3c 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1217,61 +1217,6 @@ def list(self, limit=None, marker=None, mask=0, value=0): self.index = self.open_index(self.get_transaction_id()) return [id_ for id_, _ in islice(self.index.iteritems(marker=marker, mask=mask, value=value), limit)] - def scan(self, limit=None, state=None): - """ - list (the next) chunk IDs from the repository - in on-disk order, so that a client - fetching data in this order does linear reads and reuses stuff from disk cache. - - state can either be None (initially, when starting to scan) or the object - returned from a previous scan call (meaning "continue scanning"). - - returns: list of chunk ids, state - - We rely on repository.check() has run already (either now or some time before) and that: - - - if we are called from a borg check command, self.index is a valid, fresh, in-sync repo index. - - if we are called from elsewhere, either self.index or the on-disk index is valid and in-sync. - - the repository segments are valid (no CRC errors). - if we encounter CRC errors in segment entry headers, rest of segment is skipped. - """ - if limit is not None and limit < 1: - raise ValueError("please use limit > 0 or limit = None") - transaction_id = self.get_transaction_id() - if not self.index: - self.index = self.open_index(transaction_id) - # smallest valid seg is 0, smallest valid offs is 8 - start_segment, start_offset, end_segment = state if state is not None else (0, 0, transaction_id) - ids, segment, offset = [], 0, 0 - # we only scan up to end_segment == transaction_id to scan only **committed** chunks, - # avoiding scanning into newly written chunks. - for segment, filename in self.io.segment_iterator(start_segment, end_segment): - # the start_offset we potentially got from state is only valid for the start_segment we also got - # from there. in case the segment file vanished meanwhile, the segment_iterator might never - # return a segment/filename corresponding to the start_segment and we must start from offset 0 then. - start_offset = start_offset if segment == start_segment else 0 - obj_iterator = self.io.iter_objects(segment, start_offset, read_data=False) - while True: - try: - tag, id, offset, size, _ = next(obj_iterator) - except (StopIteration, IntegrityError): - # either end-of-segment or an error - we can not seek to objects at - # higher offsets than one that has an error in the header fields. - break - if start_offset > 0: - # we are using a state != None and it points to the last object we have already - # returned in the previous scan() call - thus, we need to skip this one object. - # also, for the next segment, we need to start at offset 0. - start_offset = 0 - continue - if tag in (TAG_PUT2, TAG_PUT): - in_index = self.index.get(id) - if in_index and (in_index.segment, in_index.offset) == (segment, offset): - # we have found an existing and current object - ids.append(id) - if len(ids) == limit: - return ids, (segment, offset, end_segment) - return ids, (segment, offset, end_segment) - def flags(self, id, mask=0xFFFFFFFF, value=None): """ query and optionally set flags @@ -1625,7 +1570,7 @@ def get_segment_magic(self, segment): fd.seek(0) return fd.read(MAGIC_LEN) - def iter_objects(self, segment, offset=0, read_data=True): + def iter_objects(self, segment, read_data=True): """ Return object iterator for *segment*. @@ -1634,14 +1579,11 @@ def iter_objects(self, segment, offset=0, read_data=True): The iterator returns five-tuples of (tag, key, offset, size, data). """ fd = self.get_fd(segment) + offset = 0 fd.seek(offset) - if offset == 0: - # we are touching this segment for the first time, check the MAGIC. - # Repository.scan() calls us with segment > 0 when it continues an ongoing iteration - # from a marker position - but then we have checked the magic before already. - if fd.read(MAGIC_LEN) != MAGIC: - raise IntegrityError(f"Invalid segment magic [segment {segment}, offset {0}]") - offset = MAGIC_LEN + if fd.read(MAGIC_LEN) != MAGIC: + raise IntegrityError(f"Invalid segment magic [segment {segment}, offset {offset}]") + offset = MAGIC_LEN header = fd.read(self.header_fmt.size) while header: size, tag, key, data = self._read( diff --git a/src/borg/repository3.py b/src/borg/repository3.py index 78a98225d..4b00361b2 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -307,20 +307,6 @@ def list(self, limit=None, marker=None, mask=0, value=0): return ids[:limit] return ids - def scan(self, limit=None, state=None): - """ - list (the next) chunk IDs from the repository. - - state can either be None (initially, when starting to scan) or the object - returned from a previous scan call (meaning "continue scanning"). - - returns: list of chunk ids, state - """ - # we only have store.list() anyway, so just call .list() from here. - ids = self.list(limit=limit, marker=state) - state = ids[-1] if ids else None - return ids, state - def get(self, id, read_data=True): self._lock_refresh() id_hex = bin_to_hex(id) diff --git a/src/borg/testsuite/archiver/debug_cmds.py b/src/borg/testsuite/archiver/debug_cmds.py index 2e105fedd..4bf66bc7e 100644 --- a/src/borg/testsuite/archiver/debug_cmds.py +++ b/src/borg/testsuite/archiver/debug_cmds.py @@ -45,7 +45,7 @@ def test_debug_dump_repo_objs(archivers, request): with changedir("output"): output = cmd(archiver, "debug", "dump-repo-objs") output_dir = sorted(os.listdir("output")) - assert len(output_dir) > 0 and output_dir[0].startswith("00000000_") + assert len(output_dir) > 0 assert "Done." in output diff --git a/src/borg/testsuite/archiver/rcompress_cmd.py b/src/borg/testsuite/archiver/rcompress_cmd.py index 680f1a427..12325c81a 100644 --- a/src/borg/testsuite/archiver/rcompress_cmd.py +++ b/src/borg/testsuite/archiver/rcompress_cmd.py @@ -15,11 +15,12 @@ def check_compression(ctype, clevel, olevel): repository = Repository3(archiver.repository_path, exclusive=True) with repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - state = None + marker = None while True: - ids, state = repository.scan(limit=LIST_SCAN_LIMIT, state=state) + ids = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) if not ids: break + marker = ids[-1] for id in ids: chunk = repository.get(id, read_data=True) meta, data = manifest.repo_objs.parse( diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 985fdb1b4..5eeab725e 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -222,56 +222,6 @@ def test_list(repo_fixtures, request): assert len(repository.list(limit=50)) == 50 -def test_scan(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - for x in range(100): - repository.put(H(x), fchunk(b"SOMEDATA")) - repository.commit(compact=False) - ids, _ = repository.scan() - assert len(ids) == 100 - first_half, state = repository.scan(limit=50) - assert len(first_half) == 50 - assert first_half == ids[:50] - second_half, _ = repository.scan(state=state) - assert len(second_half) == 50 - assert second_half == ids[50:] - # check result order == on-disk order (which is hash order) - for x in range(100): - assert ids[x] == H(x) - - -def test_scan_modify(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - for x in range(100): - repository.put(H(x), fchunk(b"ORIGINAL")) - repository.commit(compact=False) - # now we scan, read and modify chunks at the same time - count = 0 - ids, _ = repository.scan() - for id in ids: - # scan results are in same order as we put the chunks into the repo (into the segment file) - assert id == H(count) - chunk = repository.get(id) - # check that we **only** get data that was committed when we started scanning - # and that we do not run into the new data we put into the repo. - assert pdchunk(chunk) == b"ORIGINAL" - count += 1 - repository.put(id, fchunk(b"MODIFIED")) - assert count == 100 - repository.commit() - - # now we have committed all the modified chunks, and **only** must get the modified ones. - count = 0 - ids, _ = repository.scan() - for id in ids: - # scan results are in same order as we put the chunks into the repo (into the segment file) - assert id == H(count) - chunk = repository.get(id) - assert pdchunk(chunk) == b"MODIFIED" - count += 1 - assert count == 100 - - def test_max_data_size(repo_fixtures, request): with get_repository_from_fixture(repo_fixtures, request) as repository: max_data = b"x" * (MAX_DATA_SIZE - RepoObj.obj_header.size) diff --git a/src/borg/testsuite/repository3.py b/src/borg/testsuite/repository3.py index 859a7e443..3f34299b5 100644 --- a/src/borg/testsuite/repository3.py +++ b/src/borg/testsuite/repository3.py @@ -137,20 +137,6 @@ def test_list(repo_fixtures, request): assert len(repository.list(limit=50)) == 50 -def test_scan(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - for x in range(100): - repository.put(H(x), fchunk(b"SOMEDATA")) - ids, _ = repository.scan() - assert len(ids) == 100 - first_half, state = repository.scan(limit=50) - assert len(first_half) == 50 - assert first_half == ids[:50] - second_half, _ = repository.scan(state=state) - assert len(second_half) == 50 - assert second_half == ids[50:] - - def test_max_data_size(repo_fixtures, request): with get_repository_from_fixture(repo_fixtures, request) as repository: max_data = b"x" * (MAX_DATA_SIZE - RepoObj.obj_header.size) From 6605f588cf7d9faac369ad00964b8cb486af16dd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 18 Aug 2024 17:55:41 +0200 Subject: [PATCH 24/79] remove the repository.flags call / feature this heavily depended on having a repository index where the flags get stored. we don't have that with borgstore. --- src/borg/hashindex.pyx | 54 +++++------------------ src/borg/remote.py | 18 +------- src/borg/remote3.py | 10 +---- src/borg/repository.py | 22 +-------- src/borg/repository3.py | 6 +-- src/borg/selftest.py | 2 +- src/borg/testsuite/hashindex.py | 76 +++----------------------------- src/borg/testsuite/repository.py | 53 ---------------------- 8 files changed, 27 insertions(+), 214 deletions(-) diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 3e2757fec..4f0560523 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -196,7 +196,7 @@ NSIndexEntry = namedtuple('NSIndexEntry', 'segment offset size') cdef class NSIndex(IndexBase): - value_size = 16 + value_size = 12 def __getitem__(self, key): assert len(key) == self.key_size @@ -209,13 +209,13 @@ cdef class NSIndex(IndexBase): def __setitem__(self, key, value): assert len(key) == self.key_size - cdef uint32_t[4] data + cdef uint32_t[3] data + assert len(value) == len(data) cdef uint32_t segment = value[0] assert segment <= _MAX_VALUE, "maximum number of segments reached" data[0] = _htole32(segment) data[1] = _htole32(value[1]) data[2] = _htole32(value[2]) - data[3] = 0 # init flags to all cleared if not hashindex_set(self.index, key, data): raise Exception('hashindex_set failed') @@ -228,12 +228,10 @@ cdef class NSIndex(IndexBase): assert segment <= _MAX_VALUE, "maximum number of segments reached" return data != NULL - def iteritems(self, marker=None, mask=0, value=0): + def iteritems(self, marker=None): """iterate over all items or optionally only over items having specific flag values""" cdef const unsigned char *key - assert isinstance(mask, int) - assert isinstance(value, int) - iter = NSKeyIterator(self.key_size, mask, value) + iter = NSKeyIterator(self.key_size) iter.idx = self iter.index = self.index if marker: @@ -243,20 +241,6 @@ cdef class NSIndex(IndexBase): iter.key = key - self.key_size return iter - def flags(self, key, mask=0xFFFFFFFF, value=None): - """query and optionally set flags""" - assert len(key) == self.key_size - assert isinstance(mask, int) - data = hashindex_get(self.index, key) - if not data: - raise KeyError(key) - flags = _le32toh(data[3]) - if isinstance(value, int): - new_flags = flags & ~mask # clear masked bits - new_flags |= value & mask # set value bits - data[3] = _htole32(new_flags) - return flags & mask # always return previous flags value - cdef class NSKeyIterator: cdef NSIndex idx @@ -264,15 +248,10 @@ cdef class NSKeyIterator: cdef const unsigned char *key cdef int key_size cdef int exhausted - cdef unsigned int flag_mask - cdef unsigned int flag_value - def __cinit__(self, key_size, mask, value): + def __cinit__(self, key_size): self.key = NULL self.key_size = key_size - # note: mask and value both default to 0, so they will match all entries - self.flag_mask = _htole32(mask) - self.flag_value = _htole32(value) self.exhausted = 0 def __iter__(self): @@ -282,16 +261,11 @@ cdef class NSKeyIterator: cdef uint32_t *value if self.exhausted: raise StopIteration - while True: - self.key = hashindex_next_key(self.index, self.key) - if not self.key: - self.exhausted = 1 - raise StopIteration - value = (self.key + self.key_size) - if value[3] & self.flag_mask == self.flag_value: - # we found a matching entry! - break - + self.key = hashindex_next_key(self.index, self.key) + if not self.key: + self.exhausted = 1 + raise StopIteration + value = (self.key + self.key_size) cdef uint32_t segment = _le32toh(value[0]) assert segment <= _MAX_VALUE, "maximum number of segments reached" return ((self.key)[:self.key_size], @@ -331,9 +305,8 @@ cdef class NSIndex1(IndexBase): # legacy borg 1.x assert segment <= _MAX_VALUE, "maximum number of segments reached" return data != NULL - def iteritems(self, marker=None, mask=0, value=0): + def iteritems(self, marker=None): cdef const unsigned char *key - assert mask == 0 and value == 0, "using mask/value is not supported for old index" iter = NSKeyIterator1(self.key_size) iter.idx = self iter.index = self.index @@ -344,9 +317,6 @@ cdef class NSIndex1(IndexBase): # legacy borg 1.x iter.key = key - self.key_size return iter - def flags(self, key, mask=0xFFFFFFFF, value=None): - raise NotImplemented("calling .flags() is not supported for old index") - cdef class NSKeyIterator1: # legacy borg 1.x cdef NSIndex1 idx diff --git a/src/borg/remote.py b/src/borg/remote.py index 5cf591bed..b1bd0eafe 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -141,8 +141,6 @@ class RepositoryServer: # pragma: no cover "commit", "delete", "destroy", - "flags", - "flags_many", "get", "list", "negotiate", @@ -984,20 +982,8 @@ def destroy(self): def __len__(self): """actual remoting is done via self.call in the @api decorator""" - @api( - since=parse_version("1.0.0"), - mask={"since": parse_version("2.0.0b2"), "previously": 0}, - value={"since": parse_version("2.0.0b2"), "previously": 0}, - ) - def list(self, limit=None, marker=None, mask=0, value=0): - """actual remoting is done via self.call in the @api decorator""" - - @api(since=parse_version("2.0.0b2")) - def flags(self, id, mask=0xFFFFFFFF, value=None): - """actual remoting is done via self.call in the @api decorator""" - - @api(since=parse_version("2.0.0b2")) - def flags_many(self, ids, mask=0xFFFFFFFF, value=None): + @api(since=parse_version("1.0.0")) + def list(self, limit=None, marker=None): """actual remoting is done via self.call in the @api decorator""" def get(self, id, read_data=True): diff --git a/src/borg/remote3.py b/src/borg/remote3.py index b3c08ca48..ef37a0886 100644 --- a/src/borg/remote3.py +++ b/src/borg/remote3.py @@ -143,8 +143,6 @@ class RepositoryServer: # pragma: no cover "commit", "delete", "destroy", - "flags", - "flags_many", "get", "list", "negotiate", @@ -1021,12 +1019,8 @@ def destroy(self): def __len__(self): """actual remoting is done via self.call in the @api decorator""" - @api( - since=parse_version("1.0.0"), - mask={"since": parse_version("2.0.0b2"), "previously": 0}, - value={"since": parse_version("2.0.0b2"), "previously": 0}, - ) - def list(self, limit=None, marker=None, mask=0, value=0): + @api(since=parse_version("1.0.0")) + def list(self, limit=None, marker=None): """actual remoting is done via self.call in the @api decorator""" def get(self, id, read_data=True): diff --git a/src/borg/repository.py b/src/borg/repository.py index 334f33e3c..60bc4fd8c 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1207,31 +1207,13 @@ def __contains__(self, id): self.index = self.open_index(self.get_transaction_id()) return id in self.index - def list(self, limit=None, marker=None, mask=0, value=0): + def list(self, limit=None, marker=None): """ list IDs starting from after id - in index (pseudo-random) order. - - if mask and value are given, only return IDs where flags & mask == value (default: all IDs). """ if not self.index: self.index = self.open_index(self.get_transaction_id()) - return [id_ for id_, _ in islice(self.index.iteritems(marker=marker, mask=mask, value=value), limit)] - - def flags(self, id, mask=0xFFFFFFFF, value=None): - """ - query and optionally set flags - - :param id: id (key) of object - :param mask: bitmask for flags (default: operate on all 32 bits) - :param value: value to set masked bits to (default: do not change any flags) - :return: (previous) flags value (only masked bits) - """ - if not self.index: - self.index = self.open_index(self.get_transaction_id()) - return self.index.flags(id, mask, value) - - def flags_many(self, ids, mask=0xFFFFFFFF, value=None): - return [self.flags(id_, mask, value) for id_ in ids] + return [id_ for id_, _ in islice(self.index.iteritems(marker=marker), limit)] def get(self, id, read_data=True): if not self.index: diff --git a/src/borg/repository3.py b/src/borg/repository3.py index 4b00361b2..811408be0 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -288,11 +288,9 @@ def __len__(self): def __contains__(self, id): raise NotImplementedError - def list(self, limit=None, marker=None, mask=0, value=0): + def list(self, limit=None, marker=None): """ - list IDs starting from after id - in index (pseudo-random) order. - - if mask and value are given, only return IDs where flags & mask == value (default: all IDs). + list IDs starting from after id . """ self._lock_refresh() infos = self.store.list("data") # XXX we can only get the full list from the store diff --git a/src/borg/selftest.py b/src/borg/selftest.py index 53415fde1..e410d0a33 100644 --- a/src/borg/selftest.py +++ b/src/borg/selftest.py @@ -33,7 +33,7 @@ ChunkerTestCase, ] -SELFTEST_COUNT = 32 +SELFTEST_COUNT = 30 class SelfTestResult(TestResult): diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 19a04b90e..34c3c9456 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -86,7 +86,7 @@ def _generic_test(self, cls, make_value, sha): def test_nsindex(self): self._generic_test( - NSIndex, lambda x: (x, x, x), "0d7880dbe02b64f03c471e60e193a1333879b4f23105768b10c9222accfeac5e" + NSIndex, lambda x: (x, x, x), "640b909cf07884cc11fdf5431ffc27dee399770ceadecce31dffecd130a311a3" ) def test_chunkindex(self): @@ -102,7 +102,7 @@ def test_resize(self): initial_size = os.path.getsize(filepath) self.assert_equal(len(idx), 0) for x in range(n): - idx[H(x)] = x, x, x, x + idx[H(x)] = x, x, x idx.write(filepath) assert initial_size < os.path.getsize(filepath) for x in range(n): @@ -114,7 +114,7 @@ def test_resize(self): def test_iteritems(self): idx = NSIndex() for x in range(100): - idx[H(x)] = x, x, x, x + idx[H(x)] = x, x, x iterator = idx.iteritems() all = list(iterator) self.assert_equal(len(all), 100) @@ -141,70 +141,6 @@ def test_chunkindex_merge(self): assert idx1[H(3)] == (3, 300) assert idx1[H(4)] == (6, 400) - def test_flags(self): - idx = NSIndex() - key = H(0) - self.assert_raises(KeyError, idx.flags, key, 0) - idx[key] = 0, 0, 0 # create entry - # check bit 0 and 1, should be both 0 after entry creation - self.assert_equal(idx.flags(key, mask=3), 0) - # set bit 0 - idx.flags(key, mask=1, value=1) - self.assert_equal(idx.flags(key, mask=1), 1) - # set bit 1 - idx.flags(key, mask=2, value=2) - self.assert_equal(idx.flags(key, mask=2), 2) - # check both bit 0 and 1, both should be set - self.assert_equal(idx.flags(key, mask=3), 3) - # clear bit 1 - idx.flags(key, mask=2, value=0) - self.assert_equal(idx.flags(key, mask=2), 0) - # clear bit 0 - idx.flags(key, mask=1, value=0) - self.assert_equal(idx.flags(key, mask=1), 0) - # check both bit 0 and 1, both should be cleared - self.assert_equal(idx.flags(key, mask=3), 0) - - def test_flags_iteritems(self): - idx = NSIndex() - keys_flagged0 = {H(i) for i in (1, 2, 3, 42)} - keys_flagged1 = {H(i) for i in (11, 12, 13, 142)} - keys_flagged2 = {H(i) for i in (21, 22, 23, 242)} - keys_flagged3 = {H(i) for i in (31, 32, 33, 342)} - for key in keys_flagged0: - idx[key] = 0, 0, 0 # create entry - idx.flags(key, mask=3, value=0) # not really necessary, unflagged is default - for key in keys_flagged1: - idx[key] = 0, 0, 0 # create entry - idx.flags(key, mask=3, value=1) - for key in keys_flagged2: - idx[key] = 0, 0, 0 # create entry - idx.flags(key, mask=3, value=2) - for key in keys_flagged3: - idx[key] = 0, 0, 0 # create entry - idx.flags(key, mask=3, value=3) - # check if we can iterate over all items - k_all = {k for k, v in idx.iteritems()} - self.assert_equal(k_all, keys_flagged0 | keys_flagged1 | keys_flagged2 | keys_flagged3) - # check if we can iterate over the flagged0 items - k0 = {k for k, v in idx.iteritems(mask=3, value=0)} - self.assert_equal(k0, keys_flagged0) - # check if we can iterate over the flagged1 items - k1 = {k for k, v in idx.iteritems(mask=3, value=1)} - self.assert_equal(k1, keys_flagged1) - # check if we can iterate over the flagged2 items - k1 = {k for k, v in idx.iteritems(mask=3, value=2)} - self.assert_equal(k1, keys_flagged2) - # check if we can iterate over the flagged3 items - k1 = {k for k, v in idx.iteritems(mask=3, value=3)} - self.assert_equal(k1, keys_flagged3) - # check if we can iterate over the flagged1 + flagged3 items - k1 = {k for k, v in idx.iteritems(mask=1, value=1)} - self.assert_equal(k1, keys_flagged1 | keys_flagged3) - # check if we can iterate over the flagged0 + flagged2 items - k1 = {k for k, v in idx.iteritems(mask=1, value=0)} - self.assert_equal(k1, keys_flagged0 | keys_flagged2) - class HashIndexExtraTestCase(BaseTestCase): """These tests are separate because they should not become part of the selftest.""" @@ -553,9 +489,9 @@ class NSIndexTestCase(BaseTestCase): def test_nsindex_segment_limit(self): idx = NSIndex() with self.assert_raises(AssertionError): - idx[H(1)] = NSIndex.MAX_VALUE + 1, 0, 0, 0 + idx[H(1)] = NSIndex.MAX_VALUE + 1, 0, 0 assert H(1) not in idx - idx[H(2)] = NSIndex.MAX_VALUE, 0, 0, 0 + idx[H(2)] = NSIndex.MAX_VALUE, 0, 0 assert H(2) in idx @@ -583,7 +519,7 @@ def HH(x, y, z): for y in range(700): # stay below max load not to trigger resize idx[HH(0, y, 0)] = (0, y, 0) - assert idx.size() == 1024 + 1031 * 48 # header + 1031 buckets + assert idx.size() == 1024 + 1031 * 44 # header + 1031 buckets # delete lots of the collisions, creating lots of tombstones for y in range(400): # stay above min load not to trigger resize diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 5eeab725e..3ad620bd5 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -231,59 +231,6 @@ def test_max_data_size(repo_fixtures, request): repository.put(H(1), fchunk(max_data + b"x")) -def test_set_flags(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - id = H(0) - repository.put(id, fchunk(b"")) - assert repository.flags(id) == 0x00000000 # init == all zero - repository.flags(id, mask=0x00000001, value=0x00000001) - assert repository.flags(id) == 0x00000001 - repository.flags(id, mask=0x00000002, value=0x00000002) - assert repository.flags(id) == 0x00000003 - repository.flags(id, mask=0x00000001, value=0x00000000) - assert repository.flags(id) == 0x00000002 - repository.flags(id, mask=0x00000002, value=0x00000000) - assert repository.flags(id) == 0x00000000 - - -def test_get_flags(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - id = H(0) - repository.put(id, fchunk(b"")) - assert repository.flags(id) == 0x00000000 # init == all zero - repository.flags(id, mask=0xC0000003, value=0x80000001) - assert repository.flags(id, mask=0x00000001) == 0x00000001 - assert repository.flags(id, mask=0x00000002) == 0x00000000 - assert repository.flags(id, mask=0x40000008) == 0x00000000 - assert repository.flags(id, mask=0x80000000) == 0x80000000 - - -def test_flags_many(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - ids_flagged = [H(0), H(1)] - ids_default_flags = [H(2), H(3)] - [repository.put(id, fchunk(b"")) for id in ids_flagged + ids_default_flags] - repository.flags_many(ids_flagged, mask=0xFFFFFFFF, value=0xDEADBEEF) - assert list(repository.flags_many(ids_default_flags)) == [0x00000000, 0x00000000] - assert list(repository.flags_many(ids_flagged)) == [0xDEADBEEF, 0xDEADBEEF] - assert list(repository.flags_many(ids_flagged, mask=0xFFFF0000)) == [0xDEAD0000, 0xDEAD0000] - assert list(repository.flags_many(ids_flagged, mask=0x0000FFFF)) == [0x0000BEEF, 0x0000BEEF] - - -def test_flags_persistence(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repository.put(H(0), fchunk(b"default")) - repository.put(H(1), fchunk(b"one one zero")) - # we do not set flags for H(0), so we can later check their default state. - repository.flags(H(1), mask=0x00000007, value=0x00000006) - repository.commit(compact=False) - with reopen(repository) as repository: - # we query all flags to check if the initial flags were all zero and - # only the ones we explicitly set to one are as expected. - assert repository.flags(H(0), mask=0xFFFFFFFF) == 0x00000000 - assert repository.flags(H(1), mask=0xFFFFFFFF) == 0x00000006 - - def _assert_sparse(repository): # the superseded 123456... PUT assert repository.compact[0] == 41 + 8 + len(fchunk(b"123456789")) From 68e64adb9f70c66011e1df153fe45237dd5ac429 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Mon, 19 Aug 2024 12:01:09 +0200 Subject: [PATCH 25/79] cache: add log msg to _load_chunks_from_repo For big repos, this might take a while, so at least have messages on debug level. --- src/borg/cache.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 3e7f113f6..5fea4c0c9 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -628,6 +628,7 @@ def add_chunk( return ChunkListEntry(id, size) def _load_chunks_from_repo(self): + logger.debug("Cache: querying the chunk IDs list from the repo...") chunks = ChunkIndex() t0 = perf_counter() num_requests = 0 @@ -651,7 +652,7 @@ def _load_chunks_from_repo(self): del chunks[self.manifest.MANIFEST_ID] duration = perf_counter() - t0 or 0.01 logger.debug( - "Cache: downloaded %d chunk IDs in %.2f s (%d requests), ~%s/s", + "Cache: queried %d chunk IDs in %.2f s (%d requests), ~%s/s", num_chunks, duration, num_requests, From 5c325e32543eb8c1023f76452de604bb1d04c683 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 20 Aug 2024 13:59:07 +0200 Subject: [PATCH 26/79] repository3.list: more efficient implementation --- src/borg/repository3.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/borg/repository3.py b/src/borg/repository3.py index 811408be0..5cd481c31 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -293,16 +293,25 @@ def list(self, limit=None, marker=None): list IDs starting from after id . """ self._lock_refresh() - infos = self.store.list("data") # XXX we can only get the full list from the store - try: - ids = [hex_to_bin(info.name) for info in infos] - except StoreObjectNotFound: - ids = [] - if marker is not None: - idx = ids.index(marker) - ids = ids[idx + 1 :] - if limit is not None: - return ids[:limit] + collect = True if marker is None else False + ids = [] + infos = self.store.list("data") # generator yielding ItemInfos + while True: + try: + info = next(infos) + except StoreObjectNotFound: + break # can happen e.g. if "data" does not exist, pointless to continue in that case + except StopIteration: + break + else: + id = hex_to_bin(info.name) + if collect: + ids.append(id) + if len(ids) == limit: + break + elif id == marker: + collect = True + # note: do not collect the marker id return ids def get(self, id, read_data=True): From c2890efdd1606eeb8a46505495aa0e87f6e49ba4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 20 Aug 2024 15:51:22 +0200 Subject: [PATCH 27/79] docs: update the repository filesystem docs In the end, it will all depend on the borgstore backend that will be used, so we better point to the borgstore project for details. --- docs/usage/general/file-systems.rst.inc | 53 ++++++++++++++----------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/docs/usage/general/file-systems.rst.inc b/docs/usage/general/file-systems.rst.inc index 1fbe47246..d53eb96bf 100644 --- a/docs/usage/general/file-systems.rst.inc +++ b/docs/usage/general/file-systems.rst.inc @@ -1,30 +1,37 @@ File systems ~~~~~~~~~~~~ -We strongly recommend against using Borg (or any other database-like -software) on non-journaling file systems like FAT, since it is not -possible to assume any consistency in case of power failures (or a -sudden disconnect of an external drive or similar failures). +We recommend using a reliable, scalable journaling filesystem for the +repository, e.g. zfs, btrfs, ext4, apfs. -While Borg uses a data store that is resilient against these failures -when used on journaling file systems, it is not possible to guarantee -this with some hardware -- independent of the software used. We don't -know a list of affected hardware. +Borg now uses the ``borgstore`` package to implement the key/value store it +uses for the repository. -If you are suspicious whether your Borg repository is still consistent -and readable after one of the failures mentioned above occurred, run -``borg check --verify-data`` to make sure it is consistent. +It currently uses the ``file:`` Store (posixfs backend) either with a local +directory or via ssh and a remote ``borg serve`` agent using borgstore on the +remote side. -.. rubric:: Requirements for Borg repository file systems +This means that it will store each chunk into a separate filesystem file +(for more details, see the ``borgstore`` project). -- Long file names -- At least three directory levels with short names -- Typically, file sizes up to a few hundred MB. - Large repositories may require large files (>2 GB). -- Up to 1000 files per directory. -- rename(2) / MoveFile(Ex) should work as specified, i.e. on the same file system - it should be a move (not a copy) operation, and in case of a directory - it should fail if the destination exists and is not an empty directory, - since this is used for locking. -- Also hardlinks are used for more safe and secure file updating (e.g. of the repo - config file), but the code tries to work also if hardlinks are not supported. +This has some pros and cons (compared to legacy borg 1.x's segment files): + +Pros: + +- Simplicity and better maintainability of the borg code. +- Sometimes faster, less I/O, better scalability: e.g. borg compact can just + remove unused chunks by deleting a single file and does not need to read + and re-write segment files to free space. +- In future, easier to adapt to other kinds of storage: + borgstore's backends are quite simple to implement. + A ``sftp:`` backend already exists, cloud storage might be easy to add. +- Parallel repository access with less locking is easier to implement. + +Cons: + +- The repository filesystem will have to deal with a big amount of files (there + are provisions in borgstore against having too many files in a single directory + by using a nested directory structure). +- Bigger fs space usage overhead (will depend on allocation block size - modern + filesystems like zfs are rather clever here using a variable block size). +- Sometimes slower, due to less sequential / more random access operations. From 5e3f2c04d578cba7291417c798fda863e8ad36c8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 22 Aug 2024 17:52:23 +0200 Subject: [PATCH 28/79] remove archive checkpointing borg1 needed this due to its transactional / rollback behaviour: if there was uncommitted stuff in the repo, next repo opening automatically rolled back to last commit. thus we needed checkpoint archives to reference chunks and commit the repo. borg2 does not do that anymore, unused chunks are only removed when the user invokes borg compact. thus, if a borg create gets interrupted, the user can just run borg create again and it will find some chunks are already in the repo, making progress even if borg create gets frequently interrupted. --- docs/deployment/automated-local.rst | 2 +- docs/faq.rst | 43 ++------ src/borg/archive.py | 116 +--------------------- src/borg/archiver/__init__.py | 16 --- src/borg/archiver/compact_cmd.py | 4 +- src/borg/archiver/create_cmd.py | 37 +------ src/borg/archiver/delete_cmd.py | 6 -- src/borg/archiver/info_cmd.py | 1 - src/borg/archiver/mount_cmds.py | 6 -- src/borg/archiver/prune_cmd.py | 39 ++------ src/borg/archiver/rcompress_cmd.py | 35 +------ src/borg/archiver/recreate_cmd.py | 21 ---- src/borg/archiver/rlist_cmd.py | 6 -- src/borg/archiver/tar_cmds.py | 30 +----- src/borg/archiver/transfer_cmd.py | 3 +- src/borg/cache.py | 2 +- src/borg/helpers/process.py | 3 +- src/borg/manifest.py | 10 +- src/borg/remote.py | 1 - src/borg/remote3.py | 1 - src/borg/testsuite/archiver/create_cmd.py | 26 +---- src/borg/testsuite/archiver/prune_cmd.py | 29 +----- src/borg/testsuite/archiver/rlist_cmd.py | 20 ---- src/borg/testsuite/cache.py | 1 - src/borg/testsuite/shellpattern.py | 6 +- 25 files changed, 44 insertions(+), 420 deletions(-) diff --git a/docs/deployment/automated-local.rst b/docs/deployment/automated-local.rst index d34a70a7f..dbc871511 100644 --- a/docs/deployment/automated-local.rst +++ b/docs/deployment/automated-local.rst @@ -105,7 +105,7 @@ modify it to suit your needs (e.g. more backup sets, dumping databases etc.). # # Options for borg create - BORG_OPTS="--stats --one-file-system --compression lz4 --checkpoint-interval 86400" + BORG_OPTS="--stats --one-file-system --compression lz4" # Set BORG_PASSPHRASE or BORG_PASSCOMMAND somewhere around here, using export, # if encryption is used. diff --git a/docs/faq.rst b/docs/faq.rst index edae228ac..1ceea731c 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -124,23 +124,14 @@ Are there other known limitations? remove files which are in the destination, but not in the archive. See :issue:`4598` for a workaround and more details. -.. _checkpoints_parts: +.. _interrupted_backup: If a backup stops mid-way, does the already-backed-up data stay there? ---------------------------------------------------------------------- -Yes, Borg supports resuming backups. - -During a backup, a special checkpoint archive named ``.checkpoint`` -is saved at every checkpoint interval (the default value for this is 30 -minutes) containing all the data backed-up until that point. - -This checkpoint archive is a valid archive, but it is only a partial backup -(not all files that you wanted to back up are contained in it and the last file -in it might be a partial file). Having it in the repo until a successful, full -backup is completed is useful because it references all the transmitted chunks up -to the checkpoint. This means that in case of an interruption, you only need to -retransfer the data since the last checkpoint. +Yes, the data transferred into the repo stays there - just avoid running +``borg compact`` before you completed the backup, because that would remove +unused chunks. If a backup was interrupted, you normally do not need to do anything special, just invoke ``borg create`` as you always do. If the repository is still locked, @@ -150,24 +141,14 @@ include the current datetime), it does not matter. Borg always does full single-pass backups, so it will start again from the beginning - but it will be much faster, because some of the data was -already stored into the repo (and is still referenced by the checkpoint -archive), so it does not need to get transmitted and stored again. - -Once your backup has finished successfully, you can delete all -``.checkpoint`` archives. If you run ``borg prune``, it will -also care for deleting unneeded checkpoints. - -Note: the checkpointing mechanism may create a partial (truncated) last file -in a checkpoint archive named ``.borg_part``. Such partial files -won't be contained in the final archive. -This is done so that checkpoints work cleanly and promptly while a big -file is being processed. +already stored into the repo, so it does not need to get transmitted and stored +again. How can I back up huge file(s) over a unstable connection? ---------------------------------------------------------- -Yes. For more details, see :ref:`checkpoints_parts`. +Yes. For more details, see :ref:`interrupted_backup`. How can I restore huge file(s) over an unstable connection? ----------------------------------------------------------- @@ -794,10 +775,9 @@ If you feel your Borg backup is too slow somehow, here is what you can do: - Don't use any expensive compression. The default is lz4 and super fast. Uncompressed is often slower than lz4. - Just wait. You can also interrupt it and start it again as often as you like, - it will converge against a valid "completed" state (see ``--checkpoint-interval``, - maybe use the default, but in any case don't make it too short). It is starting + it will converge against a valid "completed" state. It is starting from the beginning each time, but it is still faster then as it does not store - data into the repo which it already has there from last checkpoint. + data into the repo which it already has there. - If you don’t need additional file attributes, you can disable them with ``--noflags``, ``--noacls``, ``--noxattrs``. This can lead to noticeable performance improvements when your backup consists of many small files. @@ -1127,11 +1107,6 @@ conditions, but generally this should be avoided. If your backup disk is already full when Borg starts a write command like `borg create`, it will abort immediately and the repository will stay as-is. -If you run a backup that stops due to a disk running full, Borg will roll back, -delete the new segment file and thus freeing disk space automatically. There -may be a checkpoint archive left that has been saved before the disk got full. -You can keep it to speed up the next backup or delete it to get back more disk -space. Miscellaneous ############# diff --git a/src/borg/archive.py b/src/borg/archive.py index 44def4fe7..38d132fc2 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -44,7 +44,6 @@ from .helpers import os_open, flags_normal, flags_dir from .helpers import os_stat from .helpers import msgpack -from .helpers import sig_int from .helpers.lrucache import LRUCache from .manifest import Manifest from .patterns import PathPrefixPattern, FnmatchPattern, IECommand @@ -357,18 +356,6 @@ def flush(self, flush=False): def is_full(self): return self.buffer.tell() > self.BUFFER_SIZE - def save_chunks_state(self): - # as we only append to self.chunks, remembering the current length is good enough - self.saved_chunks_len = len(self.chunks) - - def restore_chunks_state(self): - scl = self.saved_chunks_len - assert scl is not None, "forgot to call save_chunks_state?" - tail_chunks = self.chunks[scl:] - del self.chunks[scl:] - self.saved_chunks_len = None - return tail_chunks - class CacheChunkBuffer(ChunkBuffer): def __init__(self, cache, key, stats, chunker_params=ITEMS_CHUNKER_PARAMS): @@ -509,12 +496,6 @@ def __init__( self.items_buffer = CacheChunkBuffer(self.cache, self.key, self.stats) if name in manifest.archives: raise self.AlreadyExists(name) - i = 0 - while True: - self.checkpoint_name = "{}.checkpoint{}".format(name, i and (".%d" % i) or "") - if self.checkpoint_name not in manifest.archives: - break - i += 1 else: info = self.manifest.archives.get(name) if info is None: @@ -629,32 +610,6 @@ def add_item(self, item, show_progress=True, stats=None): stats.show_progress(item=item, dt=0.2) self.items_buffer.add(item) - def prepare_checkpoint(self): - # we need to flush the archive metadata stream to repo chunks, so that - # we have the metadata stream chunks WITHOUT the part file item we add later. - # The part file item will then get into its own metadata stream chunk, which we - # can easily NOT include into the next checkpoint or the final archive. - self.items_buffer.flush(flush=True) - # remember the current state of self.chunks, which corresponds to the flushed chunks - self.items_buffer.save_chunks_state() - - def write_checkpoint(self): - metadata = self.save(self.checkpoint_name) - # that .save() has committed the repo. - # at next commit, we won't need this checkpoint archive any more because we will then - # have either a newer checkpoint archive or the final archive. - # so we can already remove it here, the next .save() will then commit this cleanup. - # remove its manifest entry, remove its ArchiveItem chunk, remove its item_ptrs chunks: - del self.manifest.archives[self.checkpoint_name] - self.cache.chunk_decref(self.id, 1, self.stats) - for id in metadata.item_ptrs: - self.cache.chunk_decref(id, 1, self.stats) - # also get rid of that part item, we do not want to have it in next checkpoint or final archive - tail_chunks = self.items_buffer.restore_chunks_state() - # tail_chunks contain the tail of the archive items metadata stream, not needed for next commit. - for id in tail_chunks: - self.cache.chunk_decref(id, 1, self.stats) # TODO can we have real size here? - def save(self, name=None, comment=None, timestamp=None, stats=None, additional_metadata=None): name = name or self.name if name in self.manifest.archives: @@ -1163,60 +1118,11 @@ def cached_hash(chunk, id_hash): class ChunksProcessor: # Processes an iterator of chunks for an Item - def __init__( - self, - *, - key, - cache, - add_item, - prepare_checkpoint, - write_checkpoint, - checkpoint_interval, - checkpoint_volume, - rechunkify, - ): + def __init__(self, *, key, cache, add_item, rechunkify): self.key = key self.cache = cache self.add_item = add_item - self.prepare_checkpoint = prepare_checkpoint - self.write_checkpoint = write_checkpoint self.rechunkify = rechunkify - # time interval based checkpointing - self.checkpoint_interval = checkpoint_interval - self.last_checkpoint = time.monotonic() - # file content volume based checkpointing - self.checkpoint_volume = checkpoint_volume - self.current_volume = 0 - self.last_volume_checkpoint = 0 - - def write_part_file(self, item): - self.prepare_checkpoint() - item = Item(internal_dict=item.as_dict()) - # for borg recreate, we already have a size member in the source item (giving the total file size), - # but we consider only a part of the file here, thus we must recompute the size from the chunks: - item.get_size(memorize=True, from_chunks=True) - item.path += ".borg_part" - self.add_item(item, show_progress=False) - self.write_checkpoint() - - def maybe_checkpoint(self, item): - checkpoint_done = False - sig_int_triggered = sig_int and sig_int.action_triggered() - if ( - sig_int_triggered - or (self.checkpoint_interval and time.monotonic() - self.last_checkpoint > self.checkpoint_interval) - or (self.checkpoint_volume and self.current_volume - self.last_volume_checkpoint >= self.checkpoint_volume) - ): - if sig_int_triggered: - logger.info("checkpoint requested: starting checkpoint creation...") - self.write_part_file(item) - checkpoint_done = True - self.last_checkpoint = time.monotonic() - self.last_volume_checkpoint = self.current_volume - if sig_int_triggered: - sig_int.action_completed() - logger.info("checkpoint requested: finished checkpoint creation!") - return checkpoint_done # whether a checkpoint archive was created def process_file_chunks(self, item, cache, stats, show_progress, chunk_iter, chunk_processor=None): if not chunk_processor: @@ -1237,16 +1143,13 @@ def chunk_processor(chunk): for chunk in chunk_iter: chunk_entry = chunk_processor(chunk) item.chunks.append(chunk_entry) - self.current_volume += chunk_entry[1] if show_progress: stats.show_progress(item=item, dt=0.2) - self.maybe_checkpoint(item) class FilesystemObjectProcessors: # When ported to threading, then this doesn't need chunker, cache, key any more. - # write_checkpoint should then be in the item buffer, - # and process_file becomes a callback passed to __init__. + # process_file becomes a callback passed to __init__. def __init__( self, @@ -2195,7 +2098,7 @@ def valid_item(obj): if last and len(archive_infos) < last: logger.warning("--last %d archives: only found %d archives", last, len(archive_infos)) else: - archive_infos = self.manifest.archives.list(sort_by=sort_by, consider_checkpoints=True) + archive_infos = self.manifest.archives.list(sort_by=sort_by) num_archives = len(archive_infos) pi = ProgressIndicatorPercent( @@ -2279,8 +2182,6 @@ def __init__( progress=False, file_status_printer=None, timestamp=None, - checkpoint_interval=1800, - checkpoint_volume=0, ): self.manifest = manifest self.repository = manifest.repository @@ -2305,8 +2206,6 @@ def __init__( self.stats = stats self.progress = progress self.print_file_status = file_status_printer or (lambda *args: None) - self.checkpoint_interval = None if dry_run else checkpoint_interval - self.checkpoint_volume = None if dry_run else checkpoint_volume def recreate(self, archive_name, comment=None, target_name=None): assert not self.is_temporary_archive(archive_name) @@ -2452,14 +2351,7 @@ def create_target(self, archive, target_name=None): "Rechunking archive from %s to %s", source_chunker_params or "(unknown)", target.chunker_params ) target.process_file_chunks = ChunksProcessor( - cache=self.cache, - key=self.key, - add_item=target.add_item, - prepare_checkpoint=target.prepare_checkpoint, - write_checkpoint=target.write_checkpoint, - checkpoint_interval=self.checkpoint_interval, - checkpoint_volume=self.checkpoint_volume, - rechunkify=target.recreate_rechunkify, + cache=self.cache, key=self.key, add_item=target.add_item, rechunkify=target.recreate_rechunkify ).process_file_chunks target.chunker = get_chunker(*target.chunker_params, seed=self.key.chunk_seed, sparse=False) return target diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 1e1e11eed..f4c7993f6 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -14,7 +14,6 @@ import os import shlex import signal - import time from datetime import datetime, timezone from ..logger import create_logger, setup_logging @@ -124,7 +123,6 @@ class Archiver( def __init__(self, lock_wait=None, prog=None): self.lock_wait = lock_wait self.prog = prog - self.last_checkpoint = time.monotonic() def print_warning(self, msg, *args, **kw): warning_code = kw.get("wc", EXIT_WARNING) # note: wc=None can be used to not influence exit code @@ -455,20 +453,6 @@ def _setup_topic_debugging(self, args): logger.debug("Enabling debug topic %s", topic) logging.getLogger(topic).setLevel("DEBUG") - def maybe_checkpoint(self, *, checkpoint_func, checkpoint_interval): - checkpointed = False - sig_int_triggered = sig_int and sig_int.action_triggered() - if sig_int_triggered or checkpoint_interval and time.monotonic() - self.last_checkpoint > checkpoint_interval: - if sig_int_triggered: - logger.info("checkpoint requested: starting checkpoint creation...") - checkpoint_func() - checkpointed = True - self.last_checkpoint = time.monotonic() - if sig_int_triggered: - sig_int.action_completed() - logger.info("checkpoint requested: finished checkpoint creation!") - return checkpointed - def run(self, args): os.umask(args.umask) # early, before opening files self.lock_wait = args.lock_wait diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index a546624db..bc435b820 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -25,7 +25,7 @@ def __init__(self, repository, manifest): self.wanted_chunks = None # chunks that would be nice to have for next borg check --repair self.total_files = None # overall number of source files written to all archives in this repo self.total_size = None # overall size of source file content data written to all archives - self.archives_count = None # number of archives (including checkpoint archives) + self.archives_count = None # number of archives def garbage_collect(self): """Removes unused chunks from a repository.""" @@ -60,7 +60,7 @@ def analyze_archives(self) -> Tuple[Dict[bytes, int], Dict[bytes, int], int, int """Iterate over all items in all archives, create the dicts id -> size of all used/wanted chunks.""" used_chunks = {} # chunks referenced by item.chunks wanted_chunks = {} # additional "wanted" chunks seen in item.chunks_healthy - archive_infos = self.manifest.archives.list(consider_checkpoints=True) + archive_infos = self.manifest.archives.list() num_archives = len(archive_infos) pi = ProgressIndicatorPercent( total=num_archives, msg="Computing used/wanted chunks %3.1f%%", step=0.1, msgid="compact.analyze_archives" diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 9930bc290..1d8bb0600 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -196,8 +196,7 @@ def create_inner(archive, cache, fso): archive.stats.rx_bytes = getattr(repository, "rx_bytes", 0) archive.stats.tx_bytes = getattr(repository, "tx_bytes", 0) if sig_int: - # do not save the archive if the user ctrl-c-ed - it is valid, but incomplete. - # we already have a checkpoint archive in this case. + # do not save the archive if the user ctrl-c-ed. raise Error("Got Ctrl-C / SIGINT.") else: archive.save(comment=args.comment, timestamp=args.timestamp) @@ -252,16 +251,7 @@ def create_inner(archive, cache, fso): numeric_ids=args.numeric_ids, nobirthtime=args.nobirthtime, ) - cp = ChunksProcessor( - cache=cache, - key=key, - add_item=archive.add_item, - prepare_checkpoint=archive.prepare_checkpoint, - write_checkpoint=archive.write_checkpoint, - checkpoint_interval=args.checkpoint_interval, - checkpoint_volume=args.checkpoint_volume, - rechunkify=False, - ) + cp = ChunksProcessor(cache=cache, key=key, add_item=archive.add_item, rechunkify=False) fso = FilesystemObjectProcessors( metadata_collector=metadata_collector, cache=cache, @@ -585,9 +575,7 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): The archive will consume almost no disk space for files or parts of files that have already been stored in other archives. - The archive name needs to be unique. It must not end in '.checkpoint' or - '.checkpoint.N' (with N being a number), because these names are used for - checkpoints and treated in special ways. + The archive name needs to be unique. In the archive name, you may use the following placeholders: {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. @@ -942,25 +930,6 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): help="manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, " "(+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.", ) - archive_group.add_argument( - "-c", - "--checkpoint-interval", - metavar="SECONDS", - dest="checkpoint_interval", - type=int, - default=1800, - action=Highlander, - help="write checkpoint every SECONDS seconds (Default: 1800)", - ) - archive_group.add_argument( - "--checkpoint-volume", - metavar="BYTES", - dest="checkpoint_volume", - type=int, - default=0, - action=Highlander, - help="write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing)", - ) archive_group.add_argument( "--chunker-params", metavar="PARAMS", diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index 434204c47..abac453ef 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -82,10 +82,4 @@ def build_parser_delete(self, subparsers, common_parser, mid_common_parser): 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).", - ) define_archive_filters_group(subparser) diff --git a/src/borg/archiver/info_cmd.py b/src/borg/archiver/info_cmd.py index 78ccf105d..763323f22 100644 --- a/src/borg/archiver/info_cmd.py +++ b/src/borg/archiver/info_cmd.py @@ -18,7 +18,6 @@ class InfoMixIn: def do_info(self, args, repository, manifest, cache): """Show archive details such as disk space used""" - args.consider_checkpoints = True archive_names = tuple(x.name for x in manifest.archives.list_considering(args)) output_data = [] diff --git a/src/borg/archiver/mount_cmds.py b/src/borg/archiver/mount_cmds.py index 15eade7a2..3d80090bc 100644 --- a/src/borg/archiver/mount_cmds.py +++ b/src/borg/archiver/mount_cmds.py @@ -158,12 +158,6 @@ def _define_borg_mount(self, parser): from ._common import define_exclusion_group, define_archive_filters_group parser.set_defaults(func=self.do_mount) - parser.add_argument( - "--consider-checkpoints", - action="store_true", - dest="consider_checkpoints", - help="Show checkpoint archives in the repository contents list (default: hidden).", - ) parser.add_argument("mountpoint", metavar="MOUNTPOINT", type=str, help="where to mount filesystem") parser.add_argument( "-f", "--foreground", dest="foreground", action="store_true", help="stay in foreground, do not daemonize" diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index 47443d079..a50d23a0f 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -4,7 +4,6 @@ import logging from operator import attrgetter import os -import re from ._common import with_repository, Highlander from ..archive import Archive @@ -91,25 +90,7 @@ def do_prune(self, args, repository, manifest): format = os.environ.get("BORG_PRUNE_FORMAT", "{archive:<36} {time} [{id}]") formatter = ArchiveFormatter(format, repository, manifest, manifest.key, iec=args.iec) - checkpoint_re = r"\.checkpoint(\.\d+)?" - archives_checkpoints = manifest.archives.list( - match=args.match_archives, - consider_checkpoints=True, - match_end=r"(%s)?\Z" % checkpoint_re, - sort_by=["ts"], - reverse=True, - ) - is_checkpoint = re.compile(r"(%s)\Z" % checkpoint_re).search - checkpoints = [arch for arch in archives_checkpoints if is_checkpoint(arch.name)] - # keep the latest checkpoint, if there is no later non-checkpoint archive - if archives_checkpoints and checkpoints and archives_checkpoints[0] is checkpoints[0]: - keep_checkpoints = checkpoints[:1] - else: - keep_checkpoints = [] - checkpoints = set(checkpoints) - # ignore all checkpoint archives to avoid keeping one (which is an incomplete backup) - # that is newer than a successfully completed backup - and killing the successful backup. - archives = [arch for arch in archives_checkpoints if arch not in checkpoints] + archives = manifest.archives.list(match=args.match_archives, sort_by=["ts"], reverse=True) keep = [] # collect the rule responsible for the keeping of each archive in this dict # keys are archive ids, values are a tuple @@ -126,7 +107,7 @@ def do_prune(self, args, repository, manifest): if num is not None: keep += prune_split(archives, rule, num, kept_because) - to_delete = (set(archives) | checkpoints) - (set(keep) | set(keep_checkpoints)) + to_delete = set(archives) - set(keep) with Cache(repository, manifest, lock_wait=self.lock_wait, iec=args.iec) as cache: list_logger = logging.getLogger("borg.output.list") # set up counters for the progress display @@ -134,7 +115,7 @@ def do_prune(self, args, repository, manifest): archives_deleted = 0 uncommitted_deletes = 0 pi = ProgressIndicatorPercent(total=len(to_delete), msg="Pruning archives %3.0f%%", msgid="prune") - for archive in archives_checkpoints: + for archive in archives: if sig_int and sig_int.action_done(): break if archive in to_delete: @@ -148,12 +129,9 @@ def do_prune(self, args, repository, manifest): archive.delete() uncommitted_deletes += 1 else: - if is_checkpoint(archive.name): - log_message = "Keeping checkpoint archive:" - else: - log_message = "Keeping archive (rule: {rule} #{num}):".format( - rule=kept_because[archive.id][0], num=kept_because[archive.id][1] - ) + log_message = "Keeping archive (rule: {rule} #{num}):".format( + rule=kept_because[archive.id][0], num=kept_because[archive.id][1] + ) if ( args.output_list or (args.list_pruned and archive in to_delete) @@ -184,11 +162,6 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser): `GFS `_ (Grandfather-father-son) backup rotation scheme. - Also, prune automatically removes checkpoint archives (incomplete archives left - behind by interrupted backup runs) except if the checkpoint is the latest - archive (and thus still needed). Checkpoint archives are not considered when - comparing archive counts against the retention limits (``--keep-X``). - If you use --match-archives (-a), then only archives that match the pattern are considered for deletion and only those archives count towards the totals specified by the rules. diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index e9c36cfec..1b1246e3e 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -110,23 +110,16 @@ def get_csettings(c): repo_objs = manifest.repo_objs ctype, clevel, olevel = get_csettings(repo_objs.compressor) # desired compression set by --compression - def checkpoint_func(): - while repository.async_response(wait=True) is not None: - pass - repository.commit(compact=True) - stats_find = defaultdict(int) stats_process = defaultdict(int) recompress_ids = find_chunks(repository, repo_objs, stats_find, ctype, clevel, olevel) recompress_candidate_count = len(recompress_ids) chunks_limit = min(1000, max(100, recompress_candidate_count // 1000)) - uncommitted_chunks = 0 if not isinstance(repository, (Repository3, RemoteRepository3)): # start a new transaction data = repository.get_manifest() repository.put_manifest(data) - uncommitted_chunks += 1 pi = ProgressIndicatorPercent( total=len(recompress_ids), msg="Recompressing %3.1f%%", step=0.1, msgid="rcompress.process_chunks" @@ -137,16 +130,14 @@ def checkpoint_func(): ids, recompress_ids = recompress_ids[:chunks_limit], recompress_ids[chunks_limit:] process_chunks(repository, repo_objs, stats_process, ids, olevel) pi.show(increase=len(ids)) - checkpointed = self.maybe_checkpoint( - checkpoint_func=checkpoint_func, checkpoint_interval=args.checkpoint_interval - ) - uncommitted_chunks = 0 if checkpointed else (uncommitted_chunks + len(ids)) pi.finish() if sig_int: - # Ctrl-C / SIGINT: do not checkpoint (commit) again, we already have a checkpoint in this case. + # Ctrl-C / SIGINT: do not commit raise Error("Got Ctrl-C / SIGINT.") - elif uncommitted_chunks > 0: - checkpoint_func() + else: + while repository.async_response(wait=True) is not None: + pass + repository.commit(compact=True) if args.stats: print() print("Recompression stats:") @@ -188,11 +179,6 @@ def build_parser_rcompress(self, subparsers, common_parser, mid_common_parser): Please note that the outcome might not always be the desired compression type/level - if no compression gives a shorter output, that might be chosen. - Every ``--checkpoint-interval``, progress is committed to the repository and - the repository is compacted (this is to keep temporary repo space usage in bounds). - A lower checkpoint interval means lower temporary repo space usage, but also - slower progress due to higher overhead (and vice versa). - Please note that this command can not work in low (or zero) free disk space conditions. @@ -228,14 +214,3 @@ def build_parser_rcompress(self, subparsers, common_parser, mid_common_parser): ) subparser.add_argument("-s", "--stats", dest="stats", action="store_true", help="print statistics") - - subparser.add_argument( - "-c", - "--checkpoint-interval", - metavar="SECONDS", - dest="checkpoint_interval", - type=int, - default=1800, - action=Highlander, - help="write checkpoint every SECONDS seconds (Default: 1800)", - ) diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index 9ba12579a..47ae1b3d4 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -34,8 +34,6 @@ def do_recreate(self, args, repository, manifest, cache): progress=args.progress, stats=args.stats, file_status_printer=self.print_file_status, - checkpoint_interval=args.checkpoint_interval, - checkpoint_volume=args.checkpoint_volume, dry_run=args.dry_run, timestamp=args.timestamp, ) @@ -142,25 +140,6 @@ def build_parser_recreate(self, subparsers, common_parser, mid_common_parser): help="create a new archive with the name ARCHIVE, do not replace existing archive " "(only applies for a single archive)", ) - archive_group.add_argument( - "-c", - "--checkpoint-interval", - dest="checkpoint_interval", - type=int, - default=1800, - action=Highlander, - metavar="SECONDS", - help="write checkpoint every SECONDS seconds (Default: 1800)", - ) - archive_group.add_argument( - "--checkpoint-volume", - metavar="BYTES", - dest="checkpoint_volume", - type=int, - default=0, - action=Highlander, - help="write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing)", - ) archive_group.add_argument( "--comment", metavar="COMMENT", diff --git a/src/borg/archiver/rlist_cmd.py b/src/borg/archiver/rlist_cmd.py index 0ef621f14..8d1b24c35 100644 --- a/src/borg/archiver/rlist_cmd.py +++ b/src/borg/archiver/rlist_cmd.py @@ -92,12 +92,6 @@ def build_parser_rlist(self, subparsers, common_parser, mid_common_parser): help="list repository contents", ) subparser.set_defaults(func=self.do_rlist) - subparser.add_argument( - "--consider-checkpoints", - action="store_true", - dest="consider_checkpoints", - help="Show checkpoint archives in the repository contents list (default: hidden).", - ) subparser.add_argument( "--short", dest="short", action="store_true", help="only print the archive names, nothing else" ) diff --git a/src/borg/archiver/tar_cmds.py b/src/borg/archiver/tar_cmds.py index c68d37c2a..a01a21688 100644 --- a/src/borg/archiver/tar_cmds.py +++ b/src/borg/archiver/tar_cmds.py @@ -269,16 +269,7 @@ def _import_tar(self, args, repository, manifest, key, cache, tarstream): start_monotonic=t0_monotonic, log_json=args.log_json, ) - cp = ChunksProcessor( - cache=cache, - key=key, - add_item=archive.add_item, - prepare_checkpoint=archive.prepare_checkpoint, - write_checkpoint=archive.write_checkpoint, - checkpoint_interval=args.checkpoint_interval, - checkpoint_volume=args.checkpoint_volume, - rechunkify=False, - ) + cp = ChunksProcessor(cache=cache, key=key, add_item=archive.add_item, rechunkify=False) tfo = TarfileObjectProcessors( cache=cache, key=key, @@ -524,25 +515,6 @@ def build_parser_tar(self, subparsers, common_parser, mid_common_parser): help="manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, " "(+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory.", ) - archive_group.add_argument( - "-c", - "--checkpoint-interval", - dest="checkpoint_interval", - type=int, - default=1800, - action=Highlander, - metavar="SECONDS", - help="write checkpoint every SECONDS seconds (Default: 1800)", - ) - archive_group.add_argument( - "--checkpoint-volume", - metavar="BYTES", - dest="checkpoint_volume", - type=int, - default=0, - action=Highlander, - help="write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing)", - ) archive_group.add_argument( "--chunker-params", dest="chunker_params", diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index 773347681..e740908f9 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -33,7 +33,6 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non ) dry_run = args.dry_run - args.consider_checkpoints = True archive_names = tuple(x.name for x in other_manifest.archives.list_considering(args)) if not archive_names: return @@ -193,7 +192,7 @@ def build_parser_transfer(self, subparsers, common_parser, mid_common_parser): If you want to globally change compression while transferring archives to the DST_REPO, give ``--compress=WANTED_COMPRESSION --recompress=always``. - The default is to transfer all archives, including checkpoint archives. + The default is to transfer all archives. You could use the misc. archive filter options to limit which archives it will transfer, e.g. using the ``-a`` option. This is recommended for big diff --git a/src/borg/cache.py b/src/borg/cache.py index 5fea4c0c9..81bdfab7b 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -642,7 +642,7 @@ def _load_chunks_from_repo(self): marker = result[-1] # All chunks from the repository have a refcount of MAX_VALUE, which is sticky, # therefore we can't/won't delete them. Chunks we added ourselves in this transaction - # (e.g. checkpoint archives) are tracked correctly. + # are tracked correctly. init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) for id_ in result: num_chunks += 1 diff --git a/src/borg/helpers/process.py b/src/borg/helpers/process.py index 7f20089f4..4149b7eda 100644 --- a/src/borg/helpers/process.py +++ b/src/borg/helpers/process.py @@ -244,8 +244,7 @@ def __exit__(self, exception_type, exception_value, traceback): self.ctx = None -# global flag which might trigger some special behaviour on first ctrl-c / SIGINT, -# e.g. if this is interrupting "borg create", it shall try to create a checkpoint. +# global flag which might trigger some special behaviour on first ctrl-c / SIGINT. sig_int = SigIntManager() diff --git a/src/borg/manifest.py b/src/borg/manifest.py index 4c4664ffb..29240baaa 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -110,7 +110,6 @@ def __delitem__(self, name): def list( self, *, - consider_checkpoints=True, match=None, match_end=r"\Z", sort_by=(), @@ -149,8 +148,6 @@ def list( if any([oldest, newest, older, newer]): archives = filter_archives_by_date(archives, oldest=oldest, newest=newest, newer=newer, older=older) - if not consider_checkpoints: - archives = [x for x in archives if ".checkpoint" not in x.name] for sortkey in reversed(sort_by): archives.sort(key=attrgetter(sortkey)) if first: @@ -163,18 +160,15 @@ def list( def list_considering(self, args): """ - get a list of archives, considering --first/last/prefix/match-archives/sort/consider-checkpoints cmdline args + get a list of archives, considering --first/last/prefix/match-archives/sort cmdline args """ name = getattr(args, "name", None) - consider_checkpoints = getattr(args, "consider_checkpoints", None) if name is not None: raise Error( - "Giving a specific name is incompatible with options --first, --last, " - "-a / --match-archives, and --consider-checkpoints." + "Giving a specific name is incompatible with options --first, --last " "and -a / --match-archives." ) return self.list( sort_by=args.sort_by.split(","), - consider_checkpoints=consider_checkpoints, match=args.match_archives, first=getattr(args, "first", None), last=getattr(args, "last", None), diff --git a/src/borg/remote.py b/src/borg/remote.py index b1bd0eafe..8d6f861fb 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -584,7 +584,6 @@ def __init__( borg_cmd = self.ssh_cmd(location) + borg_cmd logger.debug("SSH command line: %s", borg_cmd) # we do not want the ssh getting killed by Ctrl-C/SIGINT because it is needed for clean shutdown of borg. - # borg's SIGINT handler tries to write a checkpoint and requires the remote repo connection. self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, preexec_fn=ignore_sigint) self.stdin_fd = self.p.stdin.fileno() self.stdout_fd = self.p.stdout.fileno() diff --git a/src/borg/remote3.py b/src/borg/remote3.py index ef37a0886..793257c35 100644 --- a/src/borg/remote3.py +++ b/src/borg/remote3.py @@ -623,7 +623,6 @@ def __init__( borg_cmd = self.ssh_cmd(location) + borg_cmd logger.debug("SSH command line: %s", borg_cmd) # we do not want the ssh getting killed by Ctrl-C/SIGINT because it is needed for clean shutdown of borg. - # borg's SIGINT handler tries to write a checkpoint and requires the remote repo connection. self.p = Popen(borg_cmd, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, preexec_fn=ignore_sigint) self.stdin_fd = self.p.stdin.fileno() self.stdout_fd = self.p.stdout.fileno() diff --git a/src/borg/testsuite/archiver/create_cmd.py b/src/borg/testsuite/archiver/create_cmd.py index 0181888d8..a210fd2ec 100644 --- a/src/borg/testsuite/archiver/create_cmd.py +++ b/src/borg/testsuite/archiver/create_cmd.py @@ -236,33 +236,9 @@ def test_create_stdin(archivers, request): assert extracted_data == input_data -def test_create_stdin_checkpointing(archivers, request): - archiver = request.getfixturevalue(archivers) - chunk_size = 1000 # fixed chunker with this size, also volume based checkpointing after that volume - cmd(archiver, "rcreate", RK_ENCRYPTION) - input_data = b"X" * (chunk_size * 2 - 1) # one full and one partial chunk - cmd( - archiver, - "create", - f"--chunker-params=fixed,{chunk_size}", - f"--checkpoint-volume={chunk_size}", - "test", - "-", - input=input_data, - ) - # repo looking good overall? checks for rc == 0. - cmd(archiver, "check", "--debug") - # verify that there are no part files in final archive - out = cmd(archiver, "list", "test") - assert "stdin.borg_part" not in out - # verify full file - out = cmd(archiver, "extract", "test", "stdin", "--stdout", binary_output=True) - assert out == input_data - - def test_create_erroneous_file(archivers, request): archiver = request.getfixturevalue(archivers) - chunk_size = 1000 # fixed chunker with this size, also volume based checkpointing after that volume + chunk_size = 1000 # fixed chunker with this size create_regular_file(archiver.input_path, os.path.join(archiver.input_path, "file1"), size=chunk_size * 2) create_regular_file(archiver.input_path, os.path.join(archiver.input_path, "file2"), size=chunk_size * 2) create_regular_file(archiver.input_path, os.path.join(archiver.input_path, "file3"), size=chunk_size * 2) diff --git a/src/borg/testsuite/archiver/prune_cmd.py b/src/borg/testsuite/archiver/prune_cmd.py index e22a03d57..a61eec8e3 100644 --- a/src/borg/testsuite/archiver/prune_cmd.py +++ b/src/borg/testsuite/archiver/prune_cmd.py @@ -23,39 +23,18 @@ def test_prune_repository(archivers, request): cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "create", "test1", src_dir) cmd(archiver, "create", "test2", src_dir) - # these are not really a checkpoints, but they look like some: - cmd(archiver, "create", "test3.checkpoint", src_dir) - cmd(archiver, "create", "test3.checkpoint.1", src_dir) - cmd(archiver, "create", "test4.checkpoint", src_dir) output = cmd(archiver, "prune", "--list", "--dry-run", "--keep-daily=1") assert re.search(r"Would prune:\s+test1", output) - # must keep the latest non-checkpoint archive: + # must keep the latest archive: assert re.search(r"Keeping archive \(rule: daily #1\):\s+test2", output) - # must keep the latest checkpoint archive: - assert re.search(r"Keeping checkpoint archive:\s+test4.checkpoint", output) - output = cmd(archiver, "rlist", "--consider-checkpoints") + output = cmd(archiver, "rlist") assert "test1" in output assert "test2" in output - assert "test3.checkpoint" in output - assert "test3.checkpoint.1" in output - assert "test4.checkpoint" in output cmd(archiver, "prune", "--keep-daily=1") - output = cmd(archiver, "rlist", "--consider-checkpoints") + output = cmd(archiver, "rlist") assert "test1" not in output - # the latest non-checkpoint archive must be still there: + # the latest archive must be still there: assert "test2" in output - # only the latest checkpoint archive must still be there: - assert "test3.checkpoint" not in output - assert "test3.checkpoint.1" not in output - assert "test4.checkpoint" in output - # now we supersede the latest checkpoint by a successful backup: - cmd(archiver, "create", "test5", src_dir) - cmd(archiver, "prune", "--keep-daily=2") - output = cmd(archiver, "rlist", "--consider-checkpoints") - # all checkpoints should be gone now: - assert "checkpoint" not in output - # the latest archive must be still there - assert "test5" in output # This test must match docs/misc/prune-example.txt diff --git a/src/borg/testsuite/archiver/rlist_cmd.py b/src/borg/testsuite/archiver/rlist_cmd.py index 5c5c48ec5..a86b7d4d2 100644 --- a/src/borg/testsuite/archiver/rlist_cmd.py +++ b/src/borg/testsuite/archiver/rlist_cmd.py @@ -80,26 +80,6 @@ def test_date_matching(archivers, request): assert "archive3" not in output -def test_rlist_consider_checkpoints(archivers, request): - archiver = request.getfixturevalue(archivers) - - cmd(archiver, "rcreate", RK_ENCRYPTION) - cmd(archiver, "create", "test1", src_dir) - # these are not really a checkpoints, but they look like some: - cmd(archiver, "create", "test2.checkpoint", src_dir) - cmd(archiver, "create", "test3.checkpoint.1", src_dir) - - output = cmd(archiver, "rlist") - assert "test1" in output - assert "test2.checkpoint" not in output - assert "test3.checkpoint.1" not in output - - output = cmd(archiver, "rlist", "--consider-checkpoints") - assert "test1" in output - assert "test2.checkpoint" in output - assert "test3.checkpoint.1" in output - - def test_rlist_json(archivers, request): archiver = request.getfixturevalue(archivers) create_regular_file(archiver.input_path, "file1", size=1024 * 80) diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index b9959687b..9a6a6bcb3 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -47,7 +47,6 @@ def test_seen_chunk_add_chunk_size(self, cache): assert cache.add_chunk(H(1), {}, b"5678", stats=Statistics()) == (H(1), 4) def test_deletes_chunks_during_lifetime(self, cache, repository): - """E.g. checkpoint archives""" cache.add_chunk(H(5), {}, b"1010", stats=Statistics()) assert cache.seen_chunk(H(5)) == 1 cache.chunk_decref(H(5), 1, Statistics()) diff --git a/src/borg/testsuite/shellpattern.py b/src/borg/testsuite/shellpattern.py index e8b1acd1a..7b89cfd6b 100644 --- a/src/borg/testsuite/shellpattern.py +++ b/src/borg/testsuite/shellpattern.py @@ -124,9 +124,9 @@ def test_mismatch(path, patterns): def test_match_end(): regex = shellpattern.translate("*-home") # default is match_end == string end assert re.match(regex, "2017-07-03-home") - assert not re.match(regex, "2017-07-03-home.checkpoint") + assert not re.match(regex, "2017-07-03-home.xxx") - match_end = r"(%s)?\Z" % r"\.checkpoint(\.\d+)?" # with/without checkpoint ending + match_end = r"(\.xxx)?\Z" # with/without .xxx ending regex = shellpattern.translate("*-home", match_end=match_end) assert re.match(regex, "2017-07-03-home") - assert re.match(regex, "2017-07-03-home.checkpoint") + assert re.match(regex, "2017-07-03-home.xxx") From e23231b2c422b69b9ba3d01bbe1c1cdbb6854aa5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 22 Aug 2024 19:11:29 +0200 Subject: [PATCH 29/79] remove Repository3.commit didn't do anything anyway in this implementation. --- src/borg/archive.py | 2 -- src/borg/archiver/debug_cmd.py | 5 ----- src/borg/archiver/delete_cmd.py | 1 - src/borg/archiver/key_cmds.py | 1 - src/borg/archiver/prune_cmd.py | 1 - src/borg/archiver/rcompress_cmd.py | 1 - src/borg/archiver/rcreate_cmd.py | 1 - src/borg/archiver/recreate_cmd.py | 1 - src/borg/archiver/rename_cmd.py | 1 - src/borg/remote3.py | 1 - src/borg/repository3.py | 3 --- src/borg/testsuite/archiver/check_cmd.py | 13 ------------- src/borg/testsuite/archiver/checks.py | 1 - src/borg/testsuite/archiver/mount_cmds.py | 1 - 14 files changed, 33 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 38d132fc2..3550962df 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -661,7 +661,6 @@ def save(self, name=None, comment=None, timestamp=None, stats=None, additional_m pass self.manifest.archives[name] = (self.id, metadata.time) self.manifest.write() - self.repository.commit(compact=False) self.cache.commit() return metadata @@ -2155,7 +2154,6 @@ def finish(self): logger.info("Writing Manifest.") self.manifest.write() logger.info("Committing repo.") - self.repository.commit(compact=False) class ArchiveRecreater: diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index 82df95b35..670d4e979 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -293,12 +293,10 @@ def do_debug_put_obj(self, args, repository): repository.put(id, data) print("object %s put." % hex_id) - repository.commit(compact=False) @with_repository(manifest=False, exclusive=True) def do_debug_delete_obj(self, args, repository): """delete the objects with the given IDs from the repo""" - modified = False for hex_id in args.ids: try: id = hex_to_bin(hex_id, length=32) @@ -307,12 +305,9 @@ def do_debug_delete_obj(self, args, repository): else: try: repository.delete(id) - modified = True print("object %s deleted." % hex_id) except Repository3.ObjectNotFound: print("object %s not found." % hex_id) - if modified: - repository.commit(compact=False) print("Done.") @with_repository(manifest=False, exclusive=True, cache=True, compatibility=Manifest.NO_OPERATION_CHECK) diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index abac453ef..127e7184c 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -45,7 +45,6 @@ def do_delete(self, args, repository): logger.info("Finished dry-run.") elif deleted: manifest.write() - repository.commit(compact=False) self.print_warning('Done. Run "borg compact" to free space.', wc=None) else: self.print_warning("Aborted.", wc=None) diff --git a/src/borg/archiver/key_cmds.py b/src/borg/archiver/key_cmds.py index 1a7b4769a..f2e5c12d9 100644 --- a/src/borg/archiver/key_cmds.py +++ b/src/borg/archiver/key_cmds.py @@ -73,7 +73,6 @@ def do_change_location(self, args, repository, manifest, cache): manifest.key = key_new manifest.repo_objs.key = key_new manifest.write() - repository.commit(compact=False) # we need to rewrite cache config and security key-type info, # so that the cached key-type will match the repo key-type. diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index a50d23a0f..6c7ebf3ff 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -143,7 +143,6 @@ def do_prune(self, args, repository, manifest): raise Error("Got Ctrl-C / SIGINT.") elif uncommitted_deletes > 0: manifest.write() - repository.commit(compact=False) cache.commit() def build_parser_prune(self, subparsers, common_parser, mid_common_parser): diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index 1b1246e3e..c58a4fcab 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -137,7 +137,6 @@ def get_csettings(c): else: while repository.async_response(wait=True) is not None: pass - repository.commit(compact=True) if args.stats: print() print("Recompression stats:") diff --git a/src/borg/archiver/rcreate_cmd.py b/src/borg/archiver/rcreate_cmd.py index d9353b157..43ee578ce 100644 --- a/src/borg/archiver/rcreate_cmd.py +++ b/src/borg/archiver/rcreate_cmd.py @@ -32,7 +32,6 @@ def do_rcreate(self, args, repository, *, other_repository=None, other_manifest= manifest = Manifest(key, repository) manifest.key = key manifest.write() - repository.commit(compact=False) with Cache(repository, manifest, warn_if_unencrypted=False): pass if key.NAME != "plaintext": diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index 47ae1b3d4..90260efab 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -49,7 +49,6 @@ def do_recreate(self, args, repository, manifest, cache): logger.info("Skipped archive %s: Nothing to do. Archive was not processed.", name) if not args.dry_run: manifest.write() - repository.commit(compact=False) cache.commit() def build_parser_recreate(self, subparsers, common_parser, mid_common_parser): diff --git a/src/borg/archiver/rename_cmd.py b/src/borg/archiver/rename_cmd.py index 67dff512b..39b36571a 100644 --- a/src/borg/archiver/rename_cmd.py +++ b/src/borg/archiver/rename_cmd.py @@ -17,7 +17,6 @@ def do_rename(self, args, repository, manifest, cache, archive): """Rename an existing archive""" archive.rename(args.newname) manifest.write() - repository.commit(compact=False) cache.commit() def build_parser_rename(self, subparsers, common_parser, mid_common_parser): diff --git a/src/borg/remote3.py b/src/borg/remote3.py index 793257c35..cefc1d98c 100644 --- a/src/borg/remote3.py +++ b/src/borg/remote3.py @@ -160,7 +160,6 @@ class RepositoryServer: # pragma: no cover _rpc_methods3 = ( "__len__", "check", - "commit", "delete", "destroy", "get", diff --git a/src/borg/repository3.py b/src/borg/repository3.py index 5cd481c31..998c76ad1 100644 --- a/src/borg/repository3.py +++ b/src/borg/repository3.py @@ -205,9 +205,6 @@ def info(self): ) return info - def commit(self, compact=True, threshold=0.1): - pass - def check(self, repair=False, max_duration=0): """Check repository consistency""" diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index 3b9344076..973cb0a91 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -105,7 +105,6 @@ def test_missing_file_chunk(archivers, request): break else: pytest.fail("should not happen") # convert 'fail' - repository.commit(compact=False) cmd(archiver, "check", exit_code=1) output = cmd(archiver, "check", "--repair", exit_code=0) @@ -171,7 +170,6 @@ def test_missing_archive_item_chunk(archivers, request): archive, repository = open_archive(archiver.repository_path, "archive1") with repository: repository.delete(archive.metadata.items[0]) - repository.commit(compact=False) cmd(archiver, "check", exit_code=1) cmd(archiver, "check", "--repair", exit_code=0) cmd(archiver, "check", exit_code=0) @@ -183,7 +181,6 @@ def test_missing_archive_metadata(archivers, request): archive, repository = open_archive(archiver.repository_path, "archive1") with repository: repository.delete(archive.id) - repository.commit(compact=False) cmd(archiver, "check", exit_code=1) cmd(archiver, "check", "--repair", exit_code=0) cmd(archiver, "check", exit_code=0) @@ -198,7 +195,6 @@ def test_missing_manifest(archivers, request): repository.store_delete("config/manifest") else: repository.delete(Manifest.MANIFEST_ID) - repository.commit(compact=False) cmd(archiver, "check", exit_code=1) output = cmd(archiver, "check", "-v", "--repair", exit_code=0) assert "archive1" in output @@ -214,7 +210,6 @@ def test_corrupted_manifest(archivers, request): manifest = repository.get_manifest() corrupted_manifest = manifest[:123] + b"corrupted!" + manifest[123:] repository.put_manifest(corrupted_manifest) - repository.commit(compact=False) cmd(archiver, "check", exit_code=1) output = cmd(archiver, "check", "-v", "--repair", exit_code=0) assert "archive1" in output @@ -246,7 +241,6 @@ def test_spoofed_manifest(archivers, request): # maybe a repo-side attacker could manage to move the fake manifest file chunk over to the manifest ID. # we simulate this here by directly writing the fake manifest data to the manifest ID. repository.put_manifest(cdata) - repository.commit(compact=False) # borg should notice that the manifest has the wrong ro_type. cmd(archiver, "check", exit_code=1) # borg check --repair should remove the corrupted manifest and rebuild a new one. @@ -267,7 +261,6 @@ def test_manifest_rebuild_corrupted_chunk(archivers, request): chunk = repository.get(archive.id) corrupted_chunk = chunk + b"corrupted!" repository.put(archive.id, corrupted_chunk) - repository.commit(compact=False) cmd(archiver, "check", exit_code=1) output = cmd(archiver, "check", "-v", "--repair", exit_code=0) assert "archive2" in output @@ -295,7 +288,6 @@ def test_manifest_rebuild_duplicate_archive(archivers, request): archive = repo_objs.key.pack_metadata(archive_dict) archive_id = repo_objs.id_hash(archive) repository.put(archive_id, repo_objs.format(archive_id, {}, archive, ro_type=ROBJ_ARCHIVE_META)) - repository.commit(compact=False) cmd(archiver, "check", exit_code=1) cmd(archiver, "check", "--repair", exit_code=0) output = cmd(archiver, "rlist") @@ -336,7 +328,6 @@ def test_spoofed_archive(archivers, request): ro_type=ROBJ_FILE_STREAM, # a real archive is stored with ROBJ_ARCHIVE_META ), ) - repository.commit(compact=False) cmd(archiver, "check", exit_code=1) cmd(archiver, "check", "--repair", "--debug", exit_code=0) output = cmd(archiver, "rlist") @@ -354,7 +345,6 @@ def test_extra_chunks(archivers, request): with Repository3(archiver.repository_location, exclusive=True) as repository: chunk = fchunk(b"xxxx") repository.put(b"01234567890123456789012345678901", chunk) - repository.commit(compact=False) cmd(archiver, "check", "-v", exit_code=0) # check does not deal with orphans anymore @@ -388,7 +378,6 @@ def fake_xxh64(data, seed=0): data = data[0:123] + b"x" + data[123:] repository.put(chunk.id, data) break - repository.commit(compact=False) # the normal archives check does not read file content data. cmd(archiver, "check", "--archives-only", exit_code=0) @@ -423,7 +412,6 @@ def test_corrupted_file_chunk(archivers, request, init_args): data = data[0:123] + b"x" + data[123:] repository.put(chunk.id, data) break - repository.commit(compact=False) # the normal check checks all repository objects and the xxh64 checksum fails. output = cmd(archiver, "check", "--repository-only", exit_code=1) @@ -446,5 +434,4 @@ def test_empty_repository(archivers, request): with Repository3(archiver.repository_location, exclusive=True) as repository: for id_ in repository.list(): repository.delete(id_) - repository.commit(compact=False) cmd(archiver, "check", exit_code=1) diff --git a/src/borg/testsuite/archiver/checks.py b/src/borg/testsuite/archiver/checks.py index 3eb61e14d..df8a88002 100644 --- a/src/borg/testsuite/archiver/checks.py +++ b/src/borg/testsuite/archiver/checks.py @@ -29,7 +29,6 @@ def add_unknown_feature(repo_path, operation): manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) manifest.config["feature_flags"] = {operation.value: {"mandatory": ["unknown-feature"]}} manifest.write() - repository.commit(compact=False) def cmd_raises_unknown_feature(archiver, args): diff --git a/src/borg/testsuite/archiver/mount_cmds.py b/src/borg/testsuite/archiver/mount_cmds.py index 0f05e92f2..6b1a0df05 100644 --- a/src/borg/testsuite/archiver/mount_cmds.py +++ b/src/borg/testsuite/archiver/mount_cmds.py @@ -213,7 +213,6 @@ def test_fuse_allow_damaged_files(archivers, request): break else: assert False # missed the file - repository.commit(compact=False) cmd(archiver, "check", "--repair", exit_code=0) mountpoint = os.path.join(archiver.tmpdir, "mountpoint") From d9f24def6a74a0a4542bfac60babc6bd84852128 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 22 Aug 2024 19:25:44 +0200 Subject: [PATCH 30/79] remove unused remote.RepositoryServer --- src/borg/remote.py | 301 +-------------------------------------------- 1 file changed, 1 insertion(+), 300 deletions(-) diff --git a/src/borg/remote.py b/src/borg/remote.py index 8d6f861fb..594cc4a8a 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -1,10 +1,8 @@ -import atexit import errno import functools import inspect import logging import os -import queue import select import shlex import shutil @@ -14,10 +12,8 @@ import tempfile import textwrap import time -import traceback from subprocess import Popen, PIPE -import borg.logger from . import __version__ from .compress import Compressor from .constants import * # NOQA @@ -25,13 +21,12 @@ from .helpers import bin_to_hex from .helpers import get_limited_unpacker from .helpers import replace_placeholders -from .helpers import sysinfo from .helpers import format_file_size from .helpers import safe_unlink from .helpers import prepare_subprocess_env, ignore_sigint from .helpers import get_socket_filename from .locking import LockTimeout, NotLocked, NotMyLock, LockFailed -from .logger import create_logger, borg_serve_log_queue +from .logger import create_logger from .helpers import msgpack from .repository import Repository from .version import parse_version, format_version @@ -48,25 +43,6 @@ RATELIMIT_PERIOD = 0.1 -def os_write(fd, data): - """os.write wrapper so we do not lose data for partial writes.""" - # TODO: this issue is fixed in cygwin since at least 2.8.0, remove this - # wrapper / workaround when this version is considered ancient. - # This is happening frequently on cygwin due to its small pipe buffer size of only 64kiB - # and also due to its different blocking pipe behaviour compared to Linux/*BSD. - # Neither Linux nor *BSD ever do partial writes on blocking pipes, unless interrupted by a - # signal, in which case serve() would terminate. - amount = remaining = len(data) - while remaining: - count = os.write(fd, data) - remaining -= count - if not remaining: - break - data = data[count:] - time.sleep(count * 1e-09) - return amount - - class ConnectionClosed(Error): """Connection closed by remote host""" @@ -134,281 +110,6 @@ class ConnectionBrokenWithHint(Error): # servers still get compatible input. -class RepositoryServer: # pragma: no cover - rpc_methods = ( - "__len__", - "check", - "commit", - "delete", - "destroy", - "get", - "list", - "negotiate", - "open", - "close", - "info", - "put", - "rollback", - "save_key", - "load_key", - "break_lock", - "inject_exception", - "get_manifest", - "put_manifest", - ) - - def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota, use_socket): - self.repository = None - self.restrict_to_paths = restrict_to_paths - self.restrict_to_repositories = restrict_to_repositories - # This flag is parsed from the serve command line via Archiver.do_serve, - # i.e. it reflects local system policy and generally ranks higher than - # whatever the client wants, except when initializing a new repository - # (see RepositoryServer.open below). - self.append_only = append_only - self.storage_quota = storage_quota - self.client_version = None # we update this after client sends version information - if use_socket is False: - self.socket_path = None - elif use_socket is True: # --socket - self.socket_path = get_socket_filename() - else: # --socket=/some/path - self.socket_path = use_socket - - def filter_args(self, f, kwargs): - """Remove unknown named parameters from call, because client did (implicitly) say it's ok.""" - known = set(inspect.signature(f).parameters) - return {name: kwargs[name] for name in kwargs if name in known} - - def send_queued_log(self): - while True: - try: - # lr_dict contents see BorgQueueHandler - lr_dict = borg_serve_log_queue.get_nowait() - except queue.Empty: - break - else: - msg = msgpack.packb({LOG: lr_dict}) - os_write(self.stdout_fd, msg) - - def serve(self): - def inner_serve(): - os.set_blocking(self.stdin_fd, False) - assert not os.get_blocking(self.stdin_fd) - os.set_blocking(self.stdout_fd, True) - assert os.get_blocking(self.stdout_fd) - - unpacker = get_limited_unpacker("server") - shutdown_serve = False - while True: - # before processing any new RPCs, send out all pending log output - self.send_queued_log() - - if shutdown_serve: - # shutdown wanted! get out of here after sending all log output. - assert self.repository is None - return - - # process new RPCs - r, w, es = select.select([self.stdin_fd], [], [], 10) - if r: - data = os.read(self.stdin_fd, BUFSIZE) - if not data: - shutdown_serve = True - continue - unpacker.feed(data) - for unpacked in unpacker: - if isinstance(unpacked, dict): - msgid = unpacked[MSGID] - method = unpacked[MSG] - args = unpacked[ARGS] - else: - if self.repository is not None: - self.repository.close() - raise UnexpectedRPCDataFormatFromClient(__version__) - try: - if method not in self.rpc_methods: - raise InvalidRPCMethod(method) - try: - f = getattr(self, method) - except AttributeError: - f = getattr(self.repository, method) - args = self.filter_args(f, args) - res = f(**args) - except BaseException as e: - ex_short = traceback.format_exception_only(e.__class__, e) - ex_full = traceback.format_exception(*sys.exc_info()) - ex_trace = True - if isinstance(e, Error): - ex_short = [e.get_message()] - ex_trace = e.traceback - if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)): - # These exceptions are reconstructed on the client end in RemoteRepository.call_many(), - # and will be handled just like locally raised exceptions. Suppress the remote traceback - # for these, except ErrorWithTraceback, which should always display a traceback. - pass - else: - logging.debug("\n".join(ex_full)) - - sys_info = sysinfo() - try: - msg = msgpack.packb( - { - MSGID: msgid, - "exception_class": e.__class__.__name__, - "exception_args": e.args, - "exception_full": ex_full, - "exception_short": ex_short, - "exception_trace": ex_trace, - "sysinfo": sys_info, - } - ) - except TypeError: - msg = msgpack.packb( - { - MSGID: msgid, - "exception_class": e.__class__.__name__, - "exception_args": [ - x if isinstance(x, (str, bytes, int)) else None for x in e.args - ], - "exception_full": ex_full, - "exception_short": ex_short, - "exception_trace": ex_trace, - "sysinfo": sys_info, - } - ) - os_write(self.stdout_fd, msg) - else: - os_write(self.stdout_fd, msgpack.packb({MSGID: msgid, RESULT: res})) - if es: - shutdown_serve = True - continue - - if self.socket_path: # server for socket:// connections - try: - # remove any left-over socket file - os.unlink(self.socket_path) - except OSError: - if os.path.exists(self.socket_path): - raise - sock_dir = os.path.dirname(self.socket_path) - os.makedirs(sock_dir, exist_ok=True) - if self.socket_path.endswith(".sock"): - pid_file = self.socket_path.replace(".sock", ".pid") - else: - pid_file = self.socket_path + ".pid" - pid = os.getpid() - with open(pid_file, "w") as f: - f.write(str(pid)) - atexit.register(functools.partial(os.remove, pid_file)) - atexit.register(functools.partial(os.remove, self.socket_path)) - sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM) - sock.bind(self.socket_path) # this creates the socket file in the fs - sock.listen(0) # no backlog - os.chmod(self.socket_path, mode=0o0770) # group members may use the socket, too. - print(f"borg serve: PID {pid}, listening on socket {self.socket_path} ...", file=sys.stderr) - - while True: - connection, client_address = sock.accept() - print(f"Accepted a connection on socket {self.socket_path} ...", file=sys.stderr) - self.stdin_fd = connection.makefile("rb").fileno() - self.stdout_fd = connection.makefile("wb").fileno() - inner_serve() - print(f"Finished with connection on socket {self.socket_path} .", file=sys.stderr) - else: # server for one ssh:// connection - self.stdin_fd = sys.stdin.fileno() - self.stdout_fd = sys.stdout.fileno() - inner_serve() - - def negotiate(self, client_data): - if isinstance(client_data, dict): - self.client_version = client_data["client_version"] - else: - self.client_version = BORG_VERSION # seems to be newer than current version (no known old format) - - # not a known old format, send newest negotiate this version knows - return {"server_version": BORG_VERSION} - - def _resolve_path(self, path): - if isinstance(path, bytes): - path = os.fsdecode(path) - if path.startswith("/~/"): # /~/x = path x relative to own home dir - home_dir = os.environ.get("HOME") or os.path.expanduser("~%s" % os.environ.get("USER", "")) - path = os.path.join(home_dir, path[3:]) - elif path.startswith("/./"): # /./x = path x relative to cwd - path = path[3:] - return os.path.realpath(path) - - def open( - self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False, make_parent_dirs=False - ): - logging.debug("Resolving repository path %r", path) - path = self._resolve_path(path) - logging.debug("Resolved repository path to %r", path) - path_with_sep = os.path.join(path, "") # make sure there is a trailing slash (os.sep) - if self.restrict_to_paths: - # if --restrict-to-path P is given, we make sure that we only operate in/below path P. - # for the prefix check, it is important that the compared paths both have trailing slashes, - # so that a path /foobar will NOT be accepted with --restrict-to-path /foo option. - for restrict_to_path in self.restrict_to_paths: - restrict_to_path_with_sep = os.path.join(os.path.realpath(restrict_to_path), "") # trailing slash - if path_with_sep.startswith(restrict_to_path_with_sep): - break - else: - raise PathNotAllowed(path) - if self.restrict_to_repositories: - for restrict_to_repository in self.restrict_to_repositories: - restrict_to_repository_with_sep = os.path.join(os.path.realpath(restrict_to_repository), "") - if restrict_to_repository_with_sep == path_with_sep: - break - else: - raise PathNotAllowed(path) - # "borg init" on "borg serve --append-only" (=self.append_only) does not create an append only repo, - # while "borg init --append-only" (=append_only) does, regardless of the --append-only (self.append_only) - # flag for serve. - append_only = (not create and self.append_only) or append_only - self.repository = Repository( - path, - create, - lock_wait=lock_wait, - lock=lock, - append_only=append_only, - storage_quota=self.storage_quota, - exclusive=exclusive, - make_parent_dirs=make_parent_dirs, - send_log_cb=self.send_queued_log, - ) - self.repository.__enter__() # clean exit handled by serve() method - return self.repository.id - - def close(self): - if self.repository is not None: - self.repository.__exit__(None, None, None) - self.repository = None - borg.logger.flush_logging() - self.send_queued_log() - - def inject_exception(self, kind): - s1 = "test string" - s2 = "test string2" - if kind == "DoesNotExist": - raise Repository.DoesNotExist(s1) - elif kind == "AlreadyExists": - raise Repository.AlreadyExists(s1) - elif kind == "CheckNeeded": - raise Repository.CheckNeeded(s1) - elif kind == "IntegrityError": - raise IntegrityError(s1) - elif kind == "PathNotAllowed": - raise PathNotAllowed("foo") - elif kind == "ObjectNotFound": - raise Repository.ObjectNotFound(s1, s2) - elif kind == "InvalidRPCMethod": - raise InvalidRPCMethod(s1) - elif kind == "divide": - 0 // 0 - - class SleepingBandwidthLimiter: def __init__(self, limit): if limit: From 2be98c773b9fd9b472c98f04ecff9c51d98623b6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Aug 2024 14:22:23 +0200 Subject: [PATCH 31/79] check: update comment / help --- src/borg/archiver/check_cmd.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/borg/archiver/check_cmd.py b/src/borg/archiver/check_cmd.py index b3ca070a9..ed4dc3927 100644 --- a/src/borg/archiver/check_cmd.py +++ b/src/borg/archiver/check_cmd.py @@ -38,9 +38,7 @@ def do_check(self, args, repository): if args.repair and args.max_duration: raise CommandError("--repair does not allow --max-duration argument.") if args.max_duration and not args.repo_only: - # when doing a partial repo check, we can only check crc32 checksums in segment files, - # we can't build a fresh repo index in memory to verify the on-disk index against it. - # thus, we should not do an archives check based on a unknown-quality on-disk repo index. + # when doing a partial repo check, we can only check xxh64 hashes in repository files. # also, there is no max_duration support in the archives check code anyway. raise CommandError("--repository-only is required for --max-duration support.") if not args.archives_only: @@ -72,8 +70,8 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser): It consists of two major steps: 1. Checking the consistency of the repository itself. This includes checking - the segment magic headers, and both the metadata and data of all objects in - the segments. The read data is checked by size and CRC. Bit rot and other + the file magic headers, and both the metadata and data of all objects in + the repository. The read data is checked by size and hash. Bit rot and other types of accidental damage can be detected this way. Running the repository check can be split into multiple partial checks using ``--max-duration``. When checking a remote repository, please note that the checks run on the @@ -108,13 +106,12 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser): **Warning:** Please note that partial repository checks (i.e. running it with ``--max-duration``) can only perform non-cryptographic checksum checks on the - segment files. A full repository check (i.e. without ``--max-duration``) can - also do a repository index check. Enabling partial repository checks excepts - archive checks for the same reason. Therefore partial checks may be useful with - very large repositories only where a full check would take too long. + repository files. Enabling partial repository checks excepts archive checks + for the same reason. Therefore partial checks may be useful with very large + repositories only where a full check would take too long. The ``--verify-data`` option will perform a full integrity verification (as - opposed to checking the CRC32 of the segment) of data, which means reading the + opposed to checking just the xxh64) of data, which means reading the data from the repository, decrypting and decompressing it. It is a complete cryptographic verification and hence very time consuming, but will detect any accidental and malicious corruption. Tamper-resistance is only guaranteed for @@ -151,17 +148,15 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser): In practice, repair mode hooks into both the repository and archive checks: - 1. When checking the repository's consistency, repair mode will try to recover - as many objects from segments with integrity errors as possible, and ensure - that the index is consistent with the data stored in the segments. + 1. When checking the repository's consistency, repair mode removes corrupted + objects from the repository after it did a 2nd try to read them correctly. 2. When checking the consistency and correctness of archives, repair mode might remove whole archives from the manifest if their archive metadata chunk is corrupt or lost. On a chunk level (i.e. the contents of files), repair mode will replace corrupt or lost chunks with a same-size replacement chunk of zeroes. If a previously zeroed chunk reappears, repair mode will restore - this lost chunk using the new chunk. Lastly, repair mode will also delete - orphaned chunks (e.g. caused by read errors while creating the archive). + this lost chunk using the new chunk. Most steps taken by repair mode have a one-time effect on the repository, like removing a lost archive from the repository. However, replacing a corrupt or From 20c180c3177d96a0e85745075e0aa1b7c5c56d32 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Aug 2024 14:39:17 +0200 Subject: [PATCH 32/79] debug: remove refcount-obj command borg doesn't do precise refcounting anymore, so this is pretty useless. --- src/borg/archiver/debug_cmd.py | 32 ----------------------- src/borg/testsuite/archiver/debug_cmds.py | 18 ------------- 2 files changed, 50 deletions(-) diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index 670d4e979..a967d40c5 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -310,21 +310,6 @@ def do_debug_delete_obj(self, args, repository): print("object %s not found." % hex_id) print("Done.") - @with_repository(manifest=False, exclusive=True, cache=True, compatibility=Manifest.NO_OPERATION_CHECK) - def do_debug_refcount_obj(self, args, repository, manifest, cache): - """display refcounts for the objects with the given IDs""" - for hex_id in args.ids: - try: - id = hex_to_bin(hex_id, length=32) - except ValueError: - print("object id %s is invalid." % hex_id) - else: - try: - refcount = cache.chunks[id][0] - print("object %s has %d referrers [info from chunks cache]." % (hex_id, refcount)) - except KeyError: - print("object %s not found [info from chunks cache]." % hex_id) - def do_debug_convert_profile(self, args): """convert Borg profile to Python profile""" import marshal @@ -605,23 +590,6 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): "ids", metavar="IDs", nargs="+", type=str, help="hex object ID(s) to delete from the repo" ) - debug_refcount_obj_epilog = process_epilog( - """ - This command displays the reference count for objects from the repository. - """ - ) - subparser = debug_parsers.add_parser( - "refcount-obj", - parents=[common_parser], - add_help=False, - description=self.do_debug_refcount_obj.__doc__, - epilog=debug_refcount_obj_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, - help="show refcount for object from repository (debug)", - ) - subparser.set_defaults(func=self.do_debug_refcount_obj) - subparser.add_argument("ids", metavar="IDs", nargs="+", type=str, help="hex object ID(s) to show refcounts for") - debug_convert_profile_epilog = process_epilog( """ Convert a Borg profile to a Python cProfile compatible profile. diff --git a/src/borg/testsuite/archiver/debug_cmds.py b/src/borg/testsuite/archiver/debug_cmds.py index 4bf66bc7e..d8b571f66 100644 --- a/src/borg/testsuite/archiver/debug_cmds.py +++ b/src/borg/testsuite/archiver/debug_cmds.py @@ -158,24 +158,6 @@ def test_debug_dump_archive(archivers, request): assert "_items" in result -def test_debug_refcount_obj(archivers, request): - archiver = request.getfixturevalue(archivers) - cmd(archiver, "rcreate", RK_ENCRYPTION) - output = cmd(archiver, "debug", "refcount-obj", "0" * 64).strip() - info = "object 0000000000000000000000000000000000000000000000000000000000000000 not found [info from chunks cache]." - assert output == info - - create_json = json.loads(cmd(archiver, "create", "--json", "test", "input")) - archive_id = create_json["archive"]["id"] - output = cmd(archiver, "debug", "refcount-obj", archive_id).strip() - # AdHocCache or AdHocWithFilesCache don't do precise refcounting, we'll get ChunkIndex.MAX_VALUE as refcount. - assert output == f"object {archive_id} has 4294966271 referrers [info from chunks cache]." - - # Invalid IDs do not abort or return an error - output = cmd(archiver, "debug", "refcount-obj", "124", "xyza").strip() - assert output == f"object id 124 is invalid.{os.linesep}object id xyza is invalid." - - def test_debug_info(archivers, request): archiver = request.getfixturevalue(archivers) output = cmd(archiver, "debug", "info") From c5023da729c3b2ba9c1d8172a2adbb6dd9549584 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Aug 2024 14:42:37 +0200 Subject: [PATCH 33/79] transfer: rather talk of presence than refcount --- src/borg/archiver/transfer_cmd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index e740908f9..780513e55 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -100,8 +100,8 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non if "chunks" in item: chunks = [] for chunk_id, size in item.chunks: - refcount = cache.seen_chunk(chunk_id, size) - if refcount == 0: # target repo does not yet have this chunk + chunk_present = cache.seen_chunk(chunk_id, size) != 0 + if not chunk_present: # target repo does not yet have this chunk if not dry_run: cdata = other_repository.get(chunk_id) if args.recompress == "never": From 0b85b1a8bde322e0d5f05ed9854a2834f4be49a3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Aug 2024 14:52:07 +0200 Subject: [PATCH 34/79] parseformat: remove dsize and unique_chunks placeholder We don't have precise refcounts, thus we can't compute these. --- src/borg/helpers/parseformat.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index ea0590836..275f42b23 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -11,7 +11,7 @@ import stat import uuid from typing import Dict, Set, Tuple, ClassVar, Any, TYPE_CHECKING, Literal -from collections import Counter, OrderedDict +from collections import OrderedDict from datetime import datetime, timezone from functools import partial from string import Formatter @@ -837,9 +837,7 @@ class ItemFormatter(BaseFormatter): "flags": "file flags", "extra": 'prepends {target} with " -> " for soft links and " link to " for hard links', "size": "file size", - "dsize": "deduplicated size", "num_chunks": "number of chunks in this file", - "unique_chunks": "number of unique chunks in this file", "mtime": "file modification time", "ctime": "file change time", "atime": "file access time", @@ -853,14 +851,14 @@ class ItemFormatter(BaseFormatter): } KEY_GROUPS = ( ("type", "mode", "uid", "gid", "user", "group", "path", "target", "hlid", "flags"), - ("size", "dsize", "num_chunks", "unique_chunks"), + ("size", "num_chunks"), ("mtime", "ctime", "atime", "isomtime", "isoctime", "isoatime"), tuple(sorted(hash_algorithms)), ("archiveid", "archivename", "extra"), ("health",), ) - KEYS_REQUIRING_CACHE = ("dsize", "unique_chunks") + KEYS_REQUIRING_CACHE = () @classmethod def format_needs_cache(cls, format): @@ -878,9 +876,7 @@ def __init__(self, archive, format): self.format_keys = {f[1] for f in Formatter().parse(format)} self.call_keys = { "size": self.calculate_size, - "dsize": partial(self.sum_unique_chunks_metadata, lambda chunk: chunk.size), "num_chunks": self.calculate_num_chunks, - "unique_chunks": partial(self.sum_unique_chunks_metadata, lambda chunk: 1), "isomtime": partial(self.format_iso_time, "mtime"), "isoctime": partial(self.format_iso_time, "ctime"), "isoatime": partial(self.format_iso_time, "atime"), @@ -925,20 +921,6 @@ def get_item_data(self, item, jsonline=False): item_data[key] = self.call_keys[key](item) return item_data - def sum_unique_chunks_metadata(self, metadata_func, item): - """ - sum unique chunks metadata, a unique chunk is a chunk which is referenced globally as often as it is in the - item - - item: The item to sum its unique chunks' metadata - metadata_func: A function that takes a parameter of type ChunkIndexEntry and returns a number, used to return - the metadata needed from the chunk - """ - chunk_index = self.archive.cache.chunks - chunks = item.get("chunks", []) - chunks_counter = Counter(c.id for c in chunks) - return sum(metadata_func(c) for c in chunks if chunk_index[c.id].refcount == chunks_counter[c.id]) - def calculate_num_chunks(self, item): return len(item.get("chunks", [])) From 8455c950036ccd2db6ac0e34ea00014d5de1ec2e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Aug 2024 15:02:22 +0200 Subject: [PATCH 35/79] info: do not output deduplicated_size No precise refcounting, can't compute that inexpensively. --- src/borg/archive.py | 1 - src/borg/archiver/info_cmd.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 3550962df..f511d410f 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -123,7 +123,6 @@ def __repr__(self): def as_dict(self): return { "original_size": FileSize(self.osize, iec=self.iec), - "deduplicated_size": FileSize(self.usize, iec=self.iec), "nfiles": self.nfiles, "hashing_time": self.hashing_time, "chunking_time": self.chunking_time, diff --git a/src/borg/archiver/info_cmd.py b/src/borg/archiver/info_cmd.py index 763323f22..3de4d5a36 100644 --- a/src/borg/archiver/info_cmd.py +++ b/src/borg/archiver/info_cmd.py @@ -43,7 +43,6 @@ def do_info(self, args, repository, manifest, cache): Command line: {command_line} Number of files: {stats[nfiles]} Original size: {stats[original_size]} - Deduplicated size: {stats[deduplicated_size]} """ ) .strip() From 15e759c5064138cf9740bfaa3dba2f31634f5349 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Aug 2024 15:06:24 +0200 Subject: [PATCH 36/79] rcompress: fix help and comments no "on-disk order" anymore. --- src/borg/archiver/rcompress_cmd.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index c58a4fcab..c938378ad 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -16,11 +16,6 @@ def find_chunks(repository, repo_objs, stats, ctype, clevel, olevel): """find chunks that need processing (usually: recompression).""" - # to do it this way is maybe not obvious, thus keeping the essential design criteria here: - # - determine the chunk ids at one point in time (== do a **full** scan in one go) **before** - # writing to the repo (and especially before doing a compaction, which moves segment files around) - # - get the chunk ids in **on-disk order** (so we can efficiently compact while processing the chunks) - # - only put the ids into the list that actually need recompression (keeps it a little shorter in some cases) recompress_ids = [] compr_keys = stats["compr_keys"] = set() compr_wanted = ctype, clevel, olevel @@ -169,9 +164,8 @@ def build_parser_rcompress(self, subparsers, common_parser, mid_common_parser): """ Repository (re-)compression (and/or re-obfuscation). - Reads all chunks in the repository (in on-disk order, this is important for - compaction) and recompresses them if they are not already using the compression - type/level and obfuscation level given via ``--compression``. + Reads all chunks in the repository and recompresses them if they are not already + using the compression type/level and obfuscation level given via ``--compression``. If the outcome of the chunk processing indicates a change in compression type/level or obfuscation level, the processed chunk is written to the repository. From 84bd2b20d5430c7f6ee74f24d58e79ef8fb4f8be Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Aug 2024 15:14:59 +0200 Subject: [PATCH 37/79] rcreate: refer to borgstore rather than filesystem directory --- src/borg/archiver/rcreate_cmd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver/rcreate_cmd.py b/src/borg/archiver/rcreate_cmd.py index 43ee578ce..1693dd9a2 100644 --- a/src/borg/archiver/rcreate_cmd.py +++ b/src/borg/archiver/rcreate_cmd.py @@ -56,8 +56,8 @@ def build_parser_rcreate(self, subparsers, common_parser, mid_common_parser): rcreate_epilog = process_epilog( """ - This command creates a new, empty repository. A repository is a filesystem - directory containing the deduplicated data from zero or more archives. + This command creates a new, empty repository. A repository is a ``borgstore`` store + containing the deduplicated data from zero or more archives. Encryption mode TLDR ++++++++++++++++++++ From 05739aaa6596e98185f51e7ac0e774e59b6141fe Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 23 Aug 2024 19:00:48 +0200 Subject: [PATCH 38/79] refactor: rename repository/locking classes/modules Repository -> LegacyRepository RemoteRepository -> LegacyRemoteRepository borg.repository -> borg.legacyrepository borg.remote -> borg.legacyremote Repository3 -> Repository RemoteRepository3 -> RemoteRepository borg.repository3 -> borg.repository borg.remote3 -> borg.remote borg.locking -> borg.fslocking borg.locking3 -> borg.storelocking --- src/borg/archive.py | 13 +- src/borg/archiver/__init__.py | 6 +- src/borg/archiver/_common.py | 8 +- src/borg/archiver/compact_cmd.py | 6 +- src/borg/archiver/debug_cmd.py | 5 +- src/borg/archiver/rcompress_cmd.py | 6 +- src/borg/archiver/serve_cmd.py | 2 +- src/borg/archiver/version_cmd.py | 4 +- src/borg/cache.py | 8 +- src/borg/{locking.py => fslocking.py} | 0 src/borg/fuse.py | 4 +- src/borg/helpers/fs.py | 2 +- src/borg/helpers/parseformat.py | 6 +- src/borg/{remote3.py => legacyremote.py} | 395 +--- src/borg/legacyrepository.py | 1824 ++++++++++++++++ src/borg/manifest.py | 12 +- src/borg/remote.py | 371 +++- src/borg/repository.py | 1846 ++--------------- src/borg/repository3.py | 418 ---- src/borg/{locking3.py => storelocking.py} | 0 src/borg/testsuite/archiver/__init__.py | 14 +- src/borg/testsuite/archiver/check_cmd.py | 16 +- src/borg/testsuite/archiver/checks.py | 24 +- src/borg/testsuite/archiver/create_cmd.py | 4 +- src/borg/testsuite/archiver/key_cmds.py | 14 +- src/borg/testsuite/archiver/mount_cmds.py | 2 +- src/borg/testsuite/archiver/rcompress_cmd.py | 4 +- src/borg/testsuite/archiver/rename_cmd.py | 4 +- src/borg/testsuite/cache.py | 6 +- .../testsuite/{locking.py => fslocking.py} | 2 +- src/borg/testsuite/legacyrepository.py | 1115 ++++++++++ src/borg/testsuite/platform.py | 2 +- src/borg/testsuite/repoobj.py | 4 +- src/borg/testsuite/repository.py | 845 +------- src/borg/testsuite/repository3.py | 277 --- .../{locking3.py => storelocking.py} | 2 +- 36 files changed, 3635 insertions(+), 3636 deletions(-) rename src/borg/{locking.py => fslocking.py} (100%) rename src/borg/{remote3.py => legacyremote.py} (69%) create mode 100644 src/borg/legacyrepository.py delete mode 100644 src/borg/repository3.py rename src/borg/{locking3.py => storelocking.py} (100%) rename src/borg/testsuite/{locking.py => fslocking.py} (99%) create mode 100644 src/borg/testsuite/legacyrepository.py delete mode 100644 src/borg/testsuite/repository3.py rename src/borg/testsuite/{locking3.py => storelocking.py} (98%) diff --git a/src/borg/archive.py b/src/borg/archive.py index f511d410f..4160ab249 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -49,9 +49,8 @@ from .patterns import PathPrefixPattern, FnmatchPattern, IECommand from .item import Item, ArchiveItem, ItemDiff from .platform import acl_get, acl_set, set_flags, get_flags, swidth, hostname -from .remote import cache_if_remote -from .remote3 import RemoteRepository3 -from .repository3 import Repository3, LIST_SCAN_LIMIT, NoManifestError +from .remote import RemoteRepository, cache_if_remote +from .repository import Repository, LIST_SCAN_LIMIT, NoManifestError from .repoobj import RepoObj has_link = hasattr(os, "link") @@ -1655,7 +1654,7 @@ def check( self.repair = repair self.repository = repository self.init_chunks() - if not isinstance(repository, (Repository3, RemoteRepository3)) and not self.chunks: + if not isinstance(repository, (Repository, RemoteRepository)) and not self.chunks: logger.error("Repository contains no apparent data at all, cannot continue check/repair.") return False self.key = self.make_key(repository) @@ -1673,7 +1672,7 @@ def check( except IntegrityErrorBase as exc: logger.error("Repository manifest is corrupted: %s", exc) self.error_found = True - if not isinstance(repository, (Repository3, RemoteRepository3)): + if not isinstance(repository, (Repository, RemoteRepository)): del self.chunks[Manifest.MANIFEST_ID] self.manifest = self.rebuild_manifest() self.rebuild_refcounts( @@ -1758,7 +1757,7 @@ def verify_data(self): chunk_id = chunk_ids_revd.pop(-1) # better efficiency try: encrypted_data = next(chunk_data_iter) - except (Repository3.ObjectNotFound, IntegrityErrorBase) as err: + except (Repository.ObjectNotFound, IntegrityErrorBase) as err: self.error_found = True errors += 1 logger.error("chunk %s: %s", bin_to_hex(chunk_id), err) @@ -1889,7 +1888,7 @@ def rebuild_refcounts( Missing and/or incorrect data is repaired when detected """ # Exclude the manifest from chunks (manifest entry might be already deleted from self.chunks) - if not isinstance(self.repository, (Repository3, RemoteRepository3)): + if not isinstance(self.repository, (Repository, RemoteRepository)): self.chunks.pop(Manifest.MANIFEST_ID, None) def mark_as_possibly_superseded(id_): diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index f4c7993f6..e1c8512ab 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -35,7 +35,7 @@ from ..helpers import ErrorIgnoringTextIOWrapper from ..helpers import msgpack from ..helpers import sig_int - from ..remote3 import RemoteRepository3 + from ..remote import RemoteRepository from ..selftest import selftest except BaseException: # an unhandled exception in the try-block would cause the borg cli command to exit with rc 1 due to python's @@ -546,7 +546,7 @@ def sig_trace_handler(sig_no, stack): # pragma: no cover def format_tb(exc): qualname = type(exc).__qualname__ - remote = isinstance(exc, RemoteRepository3.RPCError) + remote = isinstance(exc, RemoteRepository.RPCError) if remote: prefix = "Borg server: " trace_back = "\n".join(prefix + line for line in exc.exception_full.splitlines()) @@ -624,7 +624,7 @@ def main(): # pragma: no cover tb_log_level = logging.ERROR if e.traceback else logging.DEBUG tb = format_tb(e) exit_code = e.exit_code - except RemoteRepository3.RPCError as e: + except RemoteRepository.RPCError as e: important = e.traceback msg = e.exception_full if important else e.get_message() msgid = e.exception_class diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index a6d3e57ac..7cb4b24f0 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -12,10 +12,10 @@ from ..helpers.nanorst import rst_to_terminal from ..manifest import Manifest, AI_HUMAN_SORT_KEYS from ..patterns import PatternMatcher +from ..legacyremote import LegacyRemoteRepository from ..remote import RemoteRepository -from ..remote3 import RemoteRepository3 +from ..legacyrepository import LegacyRepository from ..repository import Repository -from ..repository3 import Repository3 from ..repoobj import RepoObj, RepoObj1 from ..patterns import ( ArgparsePatternAction, @@ -34,7 +34,7 @@ def get_repository( location, *, create, exclusive, lock_wait, lock, append_only, make_parent_dirs, storage_quota, args, v1_or_v2 ): if location.proto in ("ssh", "socket"): - RemoteRepoCls = RemoteRepository if v1_or_v2 else RemoteRepository3 + RemoteRepoCls = LegacyRemoteRepository if v1_or_v2 else RemoteRepository repository = RemoteRepoCls( location, create=create, @@ -47,7 +47,7 @@ def get_repository( ) else: - RepoCls = Repository if v1_or_v2 else Repository3 + RepoCls = LegacyRepository if v1_or_v2 else Repository repository = RepoCls( location.path, create=create, diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index bc435b820..d9c912448 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -7,8 +7,8 @@ from ..helpers import set_ec, EXIT_WARNING, EXIT_ERROR, format_file_size from ..helpers import ProgressIndicatorPercent from ..manifest import Manifest -from ..remote3 import RemoteRepository3 -from ..repository3 import Repository3 +from ..remote import RemoteRepository +from ..repository import Repository from ..logger import create_logger @@ -18,7 +18,7 @@ class ArchiveGarbageCollector: def __init__(self, repository, manifest): self.repository = repository - assert isinstance(repository, (Repository3, RemoteRepository3)) + assert isinstance(repository, (Repository, RemoteRepository)) self.manifest = manifest self.repository_chunks = None # what we have in the repository self.used_chunks = None # what archives currently reference diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index a967d40c5..9ad4beb8e 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -15,8 +15,7 @@ from ..helpers import CommandError, RTError from ..manifest import Manifest from ..platform import get_process_id -from ..repository import Repository -from ..repository3 import Repository3, LIST_SCAN_LIMIT +from ..repository import Repository, LIST_SCAN_LIMIT from ..repoobj import RepoObj from ._common import with_repository, Highlander @@ -306,7 +305,7 @@ def do_debug_delete_obj(self, args, repository): try: repository.delete(id) print("object %s deleted." % hex_id) - except Repository3.ObjectNotFound: + except Repository.ObjectNotFound: print("object %s not found." % hex_id) print("Done.") diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index c938378ad..4df544e67 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -5,8 +5,8 @@ from ..constants import * # NOQA from ..compress import CompressionSpec, ObfuscateSize, Auto, COMPRESSOR_TABLE from ..helpers import sig_int, ProgressIndicatorPercent, Error -from ..repository3 import Repository3 -from ..remote3 import RemoteRepository3 +from ..repository import Repository +from ..remote import RemoteRepository from ..manifest import Manifest from ..logger import create_logger @@ -111,7 +111,7 @@ def get_csettings(c): recompress_candidate_count = len(recompress_ids) chunks_limit = min(1000, max(100, recompress_candidate_count // 1000)) - if not isinstance(repository, (Repository3, RemoteRepository3)): + if not isinstance(repository, (Repository, RemoteRepository)): # start a new transaction data = repository.get_manifest() repository.put_manifest(data) diff --git a/src/borg/archiver/serve_cmd.py b/src/borg/archiver/serve_cmd.py index 3c5165831..8cc613c58 100644 --- a/src/borg/archiver/serve_cmd.py +++ b/src/borg/archiver/serve_cmd.py @@ -3,7 +3,7 @@ from ._common import Highlander from ..constants import * # NOQA from ..helpers import parse_storage_quota -from ..remote3 import RepositoryServer +from ..remote import RepositoryServer from ..logger import create_logger diff --git a/src/borg/archiver/version_cmd.py b/src/borg/archiver/version_cmd.py index 03981b4d3..75593cbfb 100644 --- a/src/borg/archiver/version_cmd.py +++ b/src/borg/archiver/version_cmd.py @@ -2,7 +2,7 @@ from .. import __version__ from ..constants import * # NOQA -from ..remote3 import RemoteRepository3 +from ..remote import RemoteRepository from ..logger import create_logger @@ -16,7 +16,7 @@ def do_version(self, args): client_version = parse_version(__version__) if args.location.proto in ("ssh", "socket"): - with RemoteRepository3(args.location, lock=False, args=args) as repository: + with RemoteRepository(args.location, lock=False, args=args) as repository: server_version = repository.server_version else: server_version = client_version diff --git a/src/borg/cache.py b/src/borg/cache.py index 81bdfab7b..d2cfe268d 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -25,11 +25,11 @@ from .item import ChunkListEntry from .crypto.key import PlaintextKey from .crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError -from .locking import Lock +from .fslocking import Lock from .manifest import Manifest from .platform import SaveFile -from .remote3 import RemoteRepository3 -from .repository3 import LIST_SCAN_LIMIT, Repository3 +from .remote import RemoteRepository +from .repository import LIST_SCAN_LIMIT, Repository # note: cmtime might be either a ctime or a mtime timestamp, chunks is a list of ChunkListEntry FileCacheEntry = namedtuple("FileCacheEntry", "age inode size cmtime chunks") @@ -648,7 +648,7 @@ def _load_chunks_from_repo(self): num_chunks += 1 chunks[id_] = init_entry # Cache does not contain the manifest. - if not isinstance(self.repository, (Repository3, RemoteRepository3)): + if not isinstance(self.repository, (Repository, RemoteRepository)): del chunks[self.manifest.MANIFEST_ID] duration = perf_counter() - t0 or 0.01 logger.debug( diff --git a/src/borg/locking.py b/src/borg/fslocking.py similarity index 100% rename from src/borg/locking.py rename to src/borg/fslocking.py diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 22dd4a4e1..92f145874 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -46,7 +46,7 @@ def async_wrapper(fn): from .item import Item from .platform import uid2user, gid2group from .platformflags import is_darwin -from .remote3 import RemoteRepository3 +from .remote import RemoteRepository def fuse_main(): @@ -546,7 +546,7 @@ def pop_option(options, key, present, not_present, wanted_type, int_base=0): self._create_filesystem() llfuse.init(self, mountpoint, options) if not foreground: - if isinstance(self.repository_uncached, RemoteRepository3): + if isinstance(self.repository_uncached, RemoteRepository): daemonize() else: with daemonizing() as (old_id, new_id): diff --git a/src/borg/helpers/fs.py b/src/borg/helpers/fs.py index f422fc25a..b3c602f90 100644 --- a/src/borg/helpers/fs.py +++ b/src/borg/helpers/fs.py @@ -416,7 +416,7 @@ def safe_unlink(path): Use this when deleting potentially large files when recovering from a VFS error such as ENOSPC. It can help a full file system recover. Refer to the "File system interaction" section - in repository.py for further explanations. + in legacyrepository.py for further explanations. """ try: os.unlink(path) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 275f42b23..3b3af0dbb 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1163,14 +1163,14 @@ def ellipsis_truncate(msg, space): class BorgJsonEncoder(json.JSONEncoder): def default(self, o): + from ..legacyrepository import LegacyRepository from ..repository import Repository - from ..repository3 import Repository3 + from ..legacyremote import LegacyRemoteRepository from ..remote import RemoteRepository - from ..remote3 import RemoteRepository3 from ..archive import Archive from ..cache import AdHocCache, AdHocWithFilesCache - if isinstance(o, (Repository, RemoteRepository)) or isinstance(o, (Repository3, RemoteRepository3)): + if isinstance(o, (LegacyRepository, LegacyRemoteRepository)) or isinstance(o, (Repository, RemoteRepository)): return {"id": bin_to_hex(o.id), "location": o._location.canonical_path()} if isinstance(o, Archive): return o.info() diff --git a/src/borg/remote3.py b/src/borg/legacyremote.py similarity index 69% rename from src/borg/remote3.py rename to src/borg/legacyremote.py index cefc1d98c..496bd01e4 100644 --- a/src/borg/remote3.py +++ b/src/borg/legacyremote.py @@ -1,10 +1,8 @@ -import atexit import errno import functools import inspect import logging import os -import queue import select import shlex import shutil @@ -14,10 +12,8 @@ import tempfile import textwrap import time -import traceback from subprocess import Popen, PIPE -import borg.logger from . import __version__ from .compress import Compressor from .constants import * # NOQA @@ -25,17 +21,14 @@ from .helpers import bin_to_hex from .helpers import get_limited_unpacker from .helpers import replace_placeholders -from .helpers import sysinfo from .helpers import format_file_size from .helpers import safe_unlink from .helpers import prepare_subprocess_env, ignore_sigint from .helpers import get_socket_filename -from .locking import LockTimeout, NotLocked, NotMyLock, LockFailed -from .logger import create_logger, borg_serve_log_queue -from .manifest import NoManifestError +from .fslocking import LockTimeout, NotLocked, NotMyLock, LockFailed +from .logger import create_logger from .helpers import msgpack -from .repository import Repository -from .repository3 import Repository3 +from .legacyrepository import LegacyRepository from .version import parse_version, format_version from .checksums import xxh64 from .helpers.datastruct import EfficientCollectionQueue @@ -50,25 +43,6 @@ RATELIMIT_PERIOD = 0.1 -def os_write(fd, data): - """os.write wrapper so we do not lose data for partial writes.""" - # TODO: this issue is fixed in cygwin since at least 2.8.0, remove this - # wrapper / workaround when this version is considered ancient. - # This is happening frequently on cygwin due to its small pipe buffer size of only 64kiB - # and also due to its different blocking pipe behaviour compared to Linux/*BSD. - # Neither Linux nor *BSD ever do partial writes on blocking pipes, unless interrupted by a - # signal, in which case serve() would terminate. - amount = remaining = len(data) - while remaining: - count = os.write(fd, data) - remaining -= count - if not remaining: - break - data = data[count:] - time.sleep(count * 1e-09) - return amount - - class ConnectionClosed(Error): """Connection closed by remote host""" @@ -127,7 +101,7 @@ class ConnectionBrokenWithHint(Error): # For the client the return of the negotiate method is a dict which includes the server version. # # All method calls on the remote repository object must be allowlisted in RepositoryServer.rpc_methods and have api -# stubs in RemoteRepository*. The @api decorator on these stubs is used to set server version requirements. +# stubs in LegacyRemoteRepository. The @api decorator on these stubs is used to set server version requirements. # # Method parameters are identified only by name and never by position. Unknown parameters are ignored by the server. # If a new parameter is important and may not be ignored, on the client a parameter specific version requirement needs @@ -136,317 +110,6 @@ class ConnectionBrokenWithHint(Error): # servers still get compatible input. -class RepositoryServer: # pragma: no cover - _rpc_methods = ( - "__len__", - "check", - "commit", - "delete", - "destroy", - "get", - "list", - "negotiate", - "open", - "close", - "info", - "put", - "rollback", - "save_key", - "load_key", - "break_lock", - "inject_exception", - ) - - _rpc_methods3 = ( - "__len__", - "check", - "delete", - "destroy", - "get", - "list", - "negotiate", - "open", - "close", - "info", - "put", - "save_key", - "load_key", - "break_lock", - "inject_exception", - "get_manifest", - "put_manifest", - "store_list", - "store_load", - "store_store", - "store_delete", - ) - - def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota, use_socket): - self.repository = None - self.RepoCls = None - self.rpc_methods = ("open", "close", "negotiate") - self.restrict_to_paths = restrict_to_paths - self.restrict_to_repositories = restrict_to_repositories - # This flag is parsed from the serve command line via Archiver.do_serve, - # i.e. it reflects local system policy and generally ranks higher than - # whatever the client wants, except when initializing a new repository - # (see RepositoryServer.open below). - self.append_only = append_only - self.storage_quota = storage_quota - self.client_version = None # we update this after client sends version information - if use_socket is False: - self.socket_path = None - elif use_socket is True: # --socket - self.socket_path = get_socket_filename() - else: # --socket=/some/path - self.socket_path = use_socket - - def filter_args(self, f, kwargs): - """Remove unknown named parameters from call, because client did (implicitly) say it's ok.""" - known = set(inspect.signature(f).parameters) - return {name: kwargs[name] for name in kwargs if name in known} - - def send_queued_log(self): - while True: - try: - # lr_dict contents see BorgQueueHandler - lr_dict = borg_serve_log_queue.get_nowait() - except queue.Empty: - break - else: - msg = msgpack.packb({LOG: lr_dict}) - os_write(self.stdout_fd, msg) - - def serve(self): - def inner_serve(): - os.set_blocking(self.stdin_fd, False) - assert not os.get_blocking(self.stdin_fd) - os.set_blocking(self.stdout_fd, True) - assert os.get_blocking(self.stdout_fd) - - unpacker = get_limited_unpacker("server") - shutdown_serve = False - while True: - # before processing any new RPCs, send out all pending log output - self.send_queued_log() - - if shutdown_serve: - # shutdown wanted! get out of here after sending all log output. - assert self.repository is None - return - - # process new RPCs - r, w, es = select.select([self.stdin_fd], [], [], 10) - if r: - data = os.read(self.stdin_fd, BUFSIZE) - if not data: - shutdown_serve = True - continue - unpacker.feed(data) - for unpacked in unpacker: - if isinstance(unpacked, dict): - msgid = unpacked[MSGID] - method = unpacked[MSG] - args = unpacked[ARGS] - else: - if self.repository is not None: - self.repository.close() - raise UnexpectedRPCDataFormatFromClient(__version__) - try: - # logger.debug(f"{type(self)} method: {type(self.repository)}.{method}") - if method not in self.rpc_methods: - raise InvalidRPCMethod(method) - try: - f = getattr(self, method) - except AttributeError: - f = getattr(self.repository, method) - args = self.filter_args(f, args) - res = f(**args) - except BaseException as e: - # logger.exception(e) - ex_short = traceback.format_exception_only(e.__class__, e) - ex_full = traceback.format_exception(*sys.exc_info()) - ex_trace = True - if isinstance(e, Error): - ex_short = [e.get_message()] - ex_trace = e.traceback - if isinstance(e, (self.RepoCls.DoesNotExist, self.RepoCls.AlreadyExists, PathNotAllowed)): - # These exceptions are reconstructed on the client end in RemoteRepository*.call_many(), - # and will be handled just like locally raised exceptions. Suppress the remote traceback - # for these, except ErrorWithTraceback, which should always display a traceback. - pass - else: - logging.debug("\n".join(ex_full)) - - sys_info = sysinfo() - try: - msg = msgpack.packb( - { - MSGID: msgid, - "exception_class": e.__class__.__name__, - "exception_args": e.args, - "exception_full": ex_full, - "exception_short": ex_short, - "exception_trace": ex_trace, - "sysinfo": sys_info, - } - ) - except TypeError: - msg = msgpack.packb( - { - MSGID: msgid, - "exception_class": e.__class__.__name__, - "exception_args": [ - x if isinstance(x, (str, bytes, int)) else None for x in e.args - ], - "exception_full": ex_full, - "exception_short": ex_short, - "exception_trace": ex_trace, - "sysinfo": sys_info, - } - ) - os_write(self.stdout_fd, msg) - else: - os_write(self.stdout_fd, msgpack.packb({MSGID: msgid, RESULT: res})) - if es: - shutdown_serve = True - continue - - if self.socket_path: # server for socket:// connections - try: - # remove any left-over socket file - os.unlink(self.socket_path) - except OSError: - if os.path.exists(self.socket_path): - raise - sock_dir = os.path.dirname(self.socket_path) - os.makedirs(sock_dir, exist_ok=True) - if self.socket_path.endswith(".sock"): - pid_file = self.socket_path.replace(".sock", ".pid") - else: - pid_file = self.socket_path + ".pid" - pid = os.getpid() - with open(pid_file, "w") as f: - f.write(str(pid)) - atexit.register(functools.partial(os.remove, pid_file)) - atexit.register(functools.partial(os.remove, self.socket_path)) - sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM) - sock.bind(self.socket_path) # this creates the socket file in the fs - sock.listen(0) # no backlog - os.chmod(self.socket_path, mode=0o0770) # group members may use the socket, too. - print(f"borg serve: PID {pid}, listening on socket {self.socket_path} ...", file=sys.stderr) - - while True: - connection, client_address = sock.accept() - print(f"Accepted a connection on socket {self.socket_path} ...", file=sys.stderr) - self.stdin_fd = connection.makefile("rb").fileno() - self.stdout_fd = connection.makefile("wb").fileno() - inner_serve() - print(f"Finished with connection on socket {self.socket_path} .", file=sys.stderr) - else: # server for one ssh:// connection - self.stdin_fd = sys.stdin.fileno() - self.stdout_fd = sys.stdout.fileno() - inner_serve() - - def negotiate(self, client_data): - if isinstance(client_data, dict): - self.client_version = client_data["client_version"] - else: - self.client_version = BORG_VERSION # seems to be newer than current version (no known old format) - - # not a known old format, send newest negotiate this version knows - return {"server_version": BORG_VERSION} - - def _resolve_path(self, path): - if isinstance(path, bytes): - path = os.fsdecode(path) - if path.startswith("/~/"): # /~/x = path x relative to own home dir - home_dir = os.environ.get("HOME") or os.path.expanduser("~%s" % os.environ.get("USER", "")) - path = os.path.join(home_dir, path[3:]) - elif path.startswith("/./"): # /./x = path x relative to cwd - path = path[3:] - return os.path.realpath(path) - - def open( - self, - path, - create=False, - lock_wait=None, - lock=True, - exclusive=None, - append_only=False, - make_parent_dirs=False, - v1_or_v2=False, - ): - self.RepoCls = Repository if v1_or_v2 else Repository3 - self.rpc_methods = self._rpc_methods if v1_or_v2 else self._rpc_methods3 - logging.debug("Resolving repository path %r", path) - path = self._resolve_path(path) - logging.debug("Resolved repository path to %r", path) - path_with_sep = os.path.join(path, "") # make sure there is a trailing slash (os.sep) - if self.restrict_to_paths: - # if --restrict-to-path P is given, we make sure that we only operate in/below path P. - # for the prefix check, it is important that the compared paths both have trailing slashes, - # so that a path /foobar will NOT be accepted with --restrict-to-path /foo option. - for restrict_to_path in self.restrict_to_paths: - restrict_to_path_with_sep = os.path.join(os.path.realpath(restrict_to_path), "") # trailing slash - if path_with_sep.startswith(restrict_to_path_with_sep): - break - else: - raise PathNotAllowed(path) - if self.restrict_to_repositories: - for restrict_to_repository in self.restrict_to_repositories: - restrict_to_repository_with_sep = os.path.join(os.path.realpath(restrict_to_repository), "") - if restrict_to_repository_with_sep == path_with_sep: - break - else: - raise PathNotAllowed(path) - # "borg init" on "borg serve --append-only" (=self.append_only) does not create an append only repo, - # while "borg init --append-only" (=append_only) does, regardless of the --append-only (self.append_only) - # flag for serve. - append_only = (not create and self.append_only) or append_only - self.repository = self.RepoCls( - path, - create, - lock_wait=lock_wait, - lock=lock, - append_only=append_only, - storage_quota=self.storage_quota, - exclusive=exclusive, - make_parent_dirs=make_parent_dirs, - send_log_cb=self.send_queued_log, - ) - self.repository.__enter__() # clean exit handled by serve() method - return self.repository.id - - def close(self): - if self.repository is not None: - self.repository.__exit__(None, None, None) - self.repository = None - borg.logger.flush_logging() - self.send_queued_log() - - def inject_exception(self, kind): - s1 = "test string" - s2 = "test string2" - if kind == "DoesNotExist": - raise self.RepoCls.DoesNotExist(s1) - elif kind == "AlreadyExists": - raise self.RepoCls.AlreadyExists(s1) - elif kind == "CheckNeeded": - raise self.RepoCls.CheckNeeded(s1) - elif kind == "IntegrityError": - raise IntegrityError(s1) - elif kind == "PathNotAllowed": - raise PathNotAllowed("foo") - elif kind == "ObjectNotFound": - raise self.RepoCls.ObjectNotFound(s1, s2) - elif kind == "InvalidRPCMethod": - raise InvalidRPCMethod(s1) - elif kind == "divide": - 0 // 0 - - class SleepingBandwidthLimiter: def __init__(self, limit): if limit: @@ -542,7 +205,7 @@ def do_rpc(self, *args, **kwargs): return decorator -class RemoteRepository3: +class LegacyRemoteRepository: extra_test_args = [] # type: ignore class RPCError(Exception): @@ -587,7 +250,7 @@ def __init__( location, create=False, exclusive=False, - lock_wait=1.0, + lock_wait=None, lock=True, append_only=False, make_parent_dirs=False, @@ -676,6 +339,7 @@ def __init__( exclusive=exclusive, append_only=append_only, make_parent_dirs=make_parent_dirs, + v1_or_v2=True, # make remote use LegacyRepository ) info = self.info() self.version = info["version"] @@ -687,10 +351,10 @@ def __init__( def __del__(self): if len(self.responses): - logging.debug("still %d cached responses left in RemoteRepository3" % (len(self.responses),)) + logging.debug("still %d cached responses left in LegacyRemoteRepository" % (len(self.responses),)) if self.p or self.sock: self.close() - assert False, "cleanup happened in Repository3.__del__" + assert False, "cleanup happened in LegacyRemoteRepository.__del__" def __repr__(self): return f"<{self.__class__.__name__} {self.location.canonical_path()}>" @@ -702,10 +366,13 @@ def __exit__(self, exc_type, exc_val, exc_tb): try: if exc_type is not None: self.shutdown_time = time.monotonic() + 30 + self.rollback() finally: - # in any case, we want to close the repo cleanly. + # in any case, we want to close the repo cleanly, even if the + # rollback can not succeed (e.g. because the connection was + # already closed) and raised another exception: logger.debug( - "RemoteRepository3: %s bytes sent, %s bytes received, %d messages sent", + "LegacyRemoteRepository: %s bytes sent, %s bytes received, %d messages sent", format_file_size(self.tx_bytes), format_file_size(self.rx_bytes), self.msgid, @@ -813,21 +480,21 @@ def handle_error(unpacked): elif error == "ErrorWithTraceback": raise ErrorWithTraceback(args[0]) elif error == "DoesNotExist": - raise Repository3.DoesNotExist(self.location.processed) + raise LegacyRepository.DoesNotExist(self.location.processed) elif error == "AlreadyExists": - raise Repository3.AlreadyExists(self.location.processed) + raise LegacyRepository.AlreadyExists(self.location.processed) elif error == "CheckNeeded": - raise Repository3.CheckNeeded(self.location.processed) + raise LegacyRepository.CheckNeeded(self.location.processed) elif error == "IntegrityError": raise IntegrityError(args[0]) elif error == "PathNotAllowed": raise PathNotAllowed(args[0]) elif error == "PathPermissionDenied": - raise Repository3.PathPermissionDenied(args[0]) + raise LegacyRepository.PathPermissionDenied(args[0]) elif error == "ParentPathDoesNotExist": - raise Repository3.ParentPathDoesNotExist(args[0]) + raise LegacyRepository.ParentPathDoesNotExist(args[0]) elif error == "ObjectNotFound": - raise Repository3.ObjectNotFound(args[0], self.location.processed) + raise LegacyRepository.ObjectNotFound(args[0], self.location.processed) elif error == "InvalidRPCMethod": raise InvalidRPCMethod(args[0]) elif error == "LockTimeout": @@ -838,8 +505,6 @@ def handle_error(unpacked): raise NotLocked(args[0]) elif error == "NotMyLock": raise NotMyLock(args[0]) - elif error == "NoManifestError": - raise NoManifestError else: raise self.RPCError(unpacked) @@ -849,7 +514,7 @@ def handle_error(unpacked): send_buffer() # Try to send data, as some cases (async_response) will never try to send data otherwise. while wait or calls: if self.shutdown_time and time.monotonic() > self.shutdown_time: - # we are shutting this RemoteRepository3 down already, make sure we do not waste + # we are shutting this LegacyRemoteRepository down already, make sure we do not waste # a lot of time in case a lot of async stuff is coming in or remote is gone or slow. logger.debug( "shutdown_time reached, shutting down with %d waiting_for and %d async_responses.", @@ -1080,22 +745,6 @@ def get_manifest(self): def put_manifest(self, data): """actual remoting is done via self.call in the @api decorator""" - @api(since=parse_version("2.0.0b8")) - def store_list(self, name): - """actual remoting is done via self.call in the @api decorator""" - - @api(since=parse_version("2.0.0b8")) - def store_load(self, name): - """actual remoting is done via self.call in the @api decorator""" - - @api(since=parse_version("2.0.0b8")) - def store_store(self, name, value): - """actual remoting is done via self.call in the @api decorator""" - - @api(since=parse_version("2.0.0b8")) - def store_delete(self, name): - """actual remoting is done via self.call in the @api decorator""" - class RepositoryNoCache: """A not caching Repository wrapper, passes through to repository. @@ -1298,7 +947,7 @@ def transform(id_, data): csize = meta.get("csize", len(data)) return csize, decrypted - if isinstance(repository, RemoteRepository3) or force_cache: + if isinstance(repository, LegacyRemoteRepository) or force_cache: return RepositoryCache(repository, pack, unpack, transform) else: return RepositoryNoCache(repository, transform) diff --git a/src/borg/legacyrepository.py b/src/borg/legacyrepository.py new file mode 100644 index 000000000..eebf83bcb --- /dev/null +++ b/src/borg/legacyrepository.py @@ -0,0 +1,1824 @@ +import errno +import mmap +import os +import shutil +import stat +import struct +import time +from collections import defaultdict +from configparser import ConfigParser +from datetime import datetime, timezone +from functools import partial +from itertools import islice +from typing import Callable, DefaultDict + +from .constants import * # NOQA +from .hashindex import NSIndexEntry, NSIndex, NSIndex1, hashindex_variant +from .helpers import Error, ErrorWithTraceback, IntegrityError, format_file_size, parse_file_size +from .helpers import Location +from .helpers import ProgressIndicatorPercent +from .helpers import bin_to_hex, hex_to_bin +from .helpers import secure_erase, safe_unlink +from .helpers import msgpack +from .helpers.lrucache import LRUCache +from .fslocking import Lock, LockError, LockErrorT +from .logger import create_logger +from .manifest import Manifest, NoManifestError +from .platform import SaveFile, SyncFile, sync_dir, safe_fadvise +from .repoobj import RepoObj +from .checksums import crc32, StreamingXXH64 +from .crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError + +logger = create_logger(__name__) + +MAGIC = b"BORG_SEG" +MAGIC_LEN = len(MAGIC) + +TAG_PUT = 0 +TAG_DELETE = 1 +TAG_COMMIT = 2 +TAG_PUT2 = 3 + +# Highest ID usable as TAG_* value +# +# Code may expect not to find any tags exceeding this value. In particular, +# in order to speed up `borg check --repair`, any tag greater than MAX_TAG_ID +# is assumed to be corrupted. When increasing this value, in order to add more +# tags, keep in mind that old versions of Borg accessing a new repository +# may not be able to handle the new tags. +MAX_TAG_ID = 15 + +FreeSpace: Callable[[], DefaultDict] = partial(defaultdict, int) + + +def header_size(tag): + if tag == TAG_PUT2: + size = LoggedIO.HEADER_ID_SIZE + LoggedIO.ENTRY_HASH_SIZE + elif tag == TAG_PUT or tag == TAG_DELETE: + size = LoggedIO.HEADER_ID_SIZE + elif tag == TAG_COMMIT: + size = LoggedIO.header_fmt.size + else: + raise ValueError(f"unsupported tag: {tag!r}") + return size + + +class LegacyRepository: + """ + Filesystem based transactional key value store + + Transactionality is achieved by using a log (aka journal) to record changes. The log is a series of numbered files + called segments. Each segment is a series of log entries. The segment number together with the offset of each + entry relative to its segment start establishes an ordering of the log entries. This is the "definition" of + time for the purposes of the log. + + Log entries are either PUT, DELETE or COMMIT. + + A COMMIT is always the final log entry in a segment and marks all data from the beginning of the log until the + segment ending with the COMMIT as committed and consistent. The segment number of a segment ending with a COMMIT + is called the transaction ID of that commit, and a segment ending with a COMMIT is called committed. + + When reading from a repository it is first checked whether the last segment is committed. If it is not, then + all segments after the last committed segment are deleted; they contain log entries whose consistency is not + established by a COMMIT. + + Note that the COMMIT can't establish consistency by itself, but only manages to do so with proper support from + the platform (including the hardware). See platform.base.SyncFile for details. + + A PUT inserts a key-value pair. The value is stored in the log entry, hence the repository implements + full data logging, meaning that all data is consistent, not just metadata (which is common in file systems). + + A DELETE marks a key as deleted. + + For a given key only the last entry regarding the key, which is called current (all other entries are called + superseded), is relevant: If there is no entry or the last entry is a DELETE then the key does not exist. + Otherwise the last PUT defines the value of the key. + + By superseding a PUT (with either another PUT or a DELETE) the log entry becomes obsolete. A segment containing + such obsolete entries is called sparse, while a segment containing no such entries is called compact. + + Sparse segments can be compacted and thereby disk space freed. This destroys the transaction for which the + superseded entries where current. + + On disk layout: + + dir/README + dir/config + dir/data// + dir/index.X + dir/hints.X + + File system interaction + ----------------------- + + LoggedIO generally tries to rely on common behaviours across transactional file systems. + + Segments that are deleted are truncated first, which avoids problems if the FS needs to + allocate space to delete the dirent of the segment. This mostly affects CoW file systems, + traditional journaling file systems have a fairly good grip on this problem. + + Note that deletion, i.e. unlink(2), is atomic on every file system that uses inode reference + counts, which includes pretty much all of them. To remove a dirent the inodes refcount has + to be decreased, but you can't decrease the refcount before removing the dirent nor can you + decrease the refcount after removing the dirent. File systems solve this with a lock, + and by ensuring it all stays within the same FS transaction. + + Truncation is generally not atomic in itself, and combining truncate(2) and unlink(2) is of + course never guaranteed to be atomic. Truncation in a classic extent-based FS is done in + roughly two phases, first the extents are removed then the inode is updated. (In practice + this is of course way more complex). + + LoggedIO gracefully handles truncate/unlink splits as long as the truncate resulted in + a zero length file. Zero length segments are considered not to exist, while LoggedIO.cleanup() + will still get rid of them. + """ + + class AlreadyExists(Error): + """A repository already exists at {}.""" + + exit_mcode = 10 + + class CheckNeeded(ErrorWithTraceback): + """Inconsistency detected. Please run "borg check {}".""" + + exit_mcode = 12 + + class DoesNotExist(Error): + """Repository {} does not exist.""" + + exit_mcode = 13 + + class InsufficientFreeSpaceError(Error): + """Insufficient free space to complete transaction (required: {}, available: {}).""" + + exit_mcode = 14 + + class InvalidRepository(Error): + """{} is not a valid repository. Check repo config.""" + + exit_mcode = 15 + + class InvalidRepositoryConfig(Error): + """{} does not have a valid configuration. Check repo config [{}].""" + + exit_mcode = 16 + + class ObjectNotFound(ErrorWithTraceback): + """Object with key {} not found in repository {}.""" + + exit_mcode = 17 + + def __init__(self, id, repo): + if isinstance(id, bytes): + id = bin_to_hex(id) + super().__init__(id, repo) + + class ParentPathDoesNotExist(Error): + """The parent path of the repo directory [{}] does not exist.""" + + exit_mcode = 18 + + class PathAlreadyExists(Error): + """There is already something at {}.""" + + exit_mcode = 19 + + class StorageQuotaExceeded(Error): + """The storage quota ({}) has been exceeded ({}). Try deleting some archives.""" + + exit_mcode = 20 + + class PathPermissionDenied(Error): + """Permission denied to {}.""" + + exit_mcode = 21 + + def __init__( + self, + path, + create=False, + exclusive=False, + lock_wait=None, + lock=True, + append_only=False, + storage_quota=None, + make_parent_dirs=False, + send_log_cb=None, + ): + self.path = os.path.abspath(path) + self._location = Location("file://%s" % self.path) + self.version = None + # long-running repository methods which emit log or progress output are responsible for calling + # the ._send_log method periodically to get log and progress output transferred to the borg client + # in a timely manner, in case we have a LegacyRemoteRepository. + # for local repositories ._send_log can be called also (it will just do nothing in that case). + self._send_log = send_log_cb or (lambda: None) + self.io = None # type: LoggedIO + self.lock = None + self.index = None + # This is an index of shadowed log entries during this transaction. Consider the following sequence: + # segment_n PUT A, segment_x DELETE A + # After the "DELETE A" in segment_x the shadow index will contain "A -> [n]". + # .delete() is updating this index, it is persisted into "hints" file and is later used by .compact_segments(). + # We need the entries in the shadow_index to not accidentally drop the "DELETE A" when we compact segment_x + # only (and we do not compact segment_n), because DELETE A is still needed then because PUT A will be still + # there. Otherwise chunk A would reappear although it was previously deleted. + self.shadow_index = {} + self._active_txn = False + self.lock_wait = lock_wait + self.do_lock = lock + self.do_create = create + self.created = False + self.exclusive = exclusive + self.append_only = append_only + self.storage_quota = storage_quota + self.storage_quota_use = 0 + self.transaction_doomed = None + self.make_parent_dirs = make_parent_dirs + # v2 is the default repo version for borg 2.0 + # v1 repos must only be used in a read-only way, e.g. for + # --other-repo=V1_REPO with borg init and borg transfer! + self.acceptable_repo_versions = (1, 2) + + def __del__(self): + if self.lock: + self.close() + assert False, "cleanup happened in Repository.__del__" + + def __repr__(self): + return f"<{self.__class__.__name__} {self.path}>" + + def __enter__(self): + if self.do_create: + self.do_create = False + self.create(self.path) + self.created = True + self.open(self.path, bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None: + no_space_left_on_device = exc_type is OSError and exc_val.errno == errno.ENOSPC + # The ENOSPC could have originated somewhere else besides the Repository. The cleanup is always safe, unless + # EIO or FS corruption ensues, which is why we specifically check for ENOSPC. + if self._active_txn and no_space_left_on_device: + logger.warning("No space left on device, cleaning up partial transaction to free space.") + cleanup = True + else: + cleanup = False + self._rollback(cleanup=cleanup) + self.close() + + @property + def id_str(self): + return bin_to_hex(self.id) + + @staticmethod + def is_repository(path): + """Check whether there is already a Borg repository at *path*.""" + try: + # Use binary mode to avoid troubles if a README contains some stuff not in our locale + with open(os.path.join(path, "README"), "rb") as fd: + # Read only the first ~100 bytes (if any), in case some README file we stumble upon is large. + readme_head = fd.read(100) + # The first comparison captures our current variant (REPOSITORY_README), the second comparison + # is an older variant of the README file (used by 1.0.x). + return b"Borg Backup repository" in readme_head or b"Borg repository" in readme_head + except OSError: + # Ignore FileNotFound, PermissionError, ... + return False + + def check_can_create_repository(self, path): + """ + Raise an exception if a repository already exists at *path* or any parent directory. + + Checking parent directories is done for two reasons: + (1) It's just a weird thing to do, and usually not intended. A Borg using the "parent" repository + may be confused, or we may accidentally put stuff into the "data/" or "data//" directories. + (2) When implementing repository quotas (which we currently don't), it's important to prohibit + folks from creating quota-free repositories. Since no one can create a repository within another + repository, user's can only use the quota'd repository, when their --restrict-to-path points + at the user's repository. + """ + try: + st = os.stat(path) + except FileNotFoundError: + pass # nothing there! + except PermissionError: + raise self.PathPermissionDenied(path) from None + else: + # there is something already there! + if self.is_repository(path): + raise self.AlreadyExists(path) + if not stat.S_ISDIR(st.st_mode): + raise self.PathAlreadyExists(path) + try: + files = os.listdir(path) + except PermissionError: + raise self.PathPermissionDenied(path) from None + else: + if files: # a dir, but not empty + raise self.PathAlreadyExists(path) + else: # an empty directory is acceptable for us. + pass + + while True: + # Check all parent directories for Borg's repository README + previous_path = path + # Thus, path = previous_path/.. + path = os.path.abspath(os.path.join(previous_path, os.pardir)) + if path == previous_path: + # We reached the root of the directory hierarchy (/.. = / and C:\.. = C:\). + break + if self.is_repository(path): + raise self.AlreadyExists(path) + + def create(self, path): + """Create a new empty repository at `path`""" + self.check_can_create_repository(path) + if self.make_parent_dirs: + parent_path = os.path.join(path, os.pardir) + os.makedirs(parent_path, exist_ok=True) + if not os.path.exists(path): + try: + os.mkdir(path) + except FileNotFoundError as err: + raise self.ParentPathDoesNotExist(path) from err + with open(os.path.join(path, "README"), "w") as fd: + fd.write(REPOSITORY_README) + os.mkdir(os.path.join(path, "data")) + config = ConfigParser(interpolation=None) + config.add_section("repository") + self.version = 2 + config.set("repository", "version", str(self.version)) + config.set("repository", "segments_per_dir", str(DEFAULT_SEGMENTS_PER_DIR)) + config.set("repository", "max_segment_size", str(DEFAULT_MAX_SEGMENT_SIZE)) + config.set("repository", "append_only", str(int(self.append_only))) + if self.storage_quota: + config.set("repository", "storage_quota", str(self.storage_quota)) + else: + config.set("repository", "storage_quota", "0") + config.set("repository", "additional_free_space", "0") + config.set("repository", "id", bin_to_hex(os.urandom(32))) + self.save_config(path, config) + + def save_config(self, path, config): + config_path = os.path.join(path, "config") + old_config_path = os.path.join(path, "config.old") + + if os.path.isfile(old_config_path): + logger.warning("Old config file not securely erased on previous config update") + secure_erase(old_config_path, avoid_collateral_damage=True) + + if os.path.isfile(config_path): + link_error_msg = ( + "Failed to erase old repository config file securely (hardlinks not supported). " + "Old repokey data, if any, might persist on physical storage." + ) + try: + os.link(config_path, old_config_path) + except OSError as e: + if e.errno in (errno.EMLINK, errno.ENOSYS, errno.EPERM, errno.EACCES, errno.ENOTSUP, errno.EIO): + logger.warning(link_error_msg) + else: + raise + except AttributeError: + # some python ports have no os.link, see #4901 + logger.warning(link_error_msg) + + try: + with SaveFile(config_path) as fd: + config.write(fd) + except PermissionError as e: + # error is only a problem if we even had a lock + if self.do_lock: + raise + logger.warning( + "%s: Failed writing to '%s'. This is expected when working on " + "read-only repositories." % (e.strerror, e.filename) + ) + + if os.path.isfile(old_config_path): + secure_erase(old_config_path, avoid_collateral_damage=True) + + def save_key(self, keydata): + assert self.config + keydata = keydata.decode("utf-8") # remote repo: msgpack issue #99, getting bytes + # note: saving an empty key means that there is no repokey any more + self.config.set("repository", "key", keydata) + self.save_config(self.path, self.config) + + def load_key(self): + keydata = self.config.get("repository", "key", fallback="").strip() + # note: if we return an empty string, it means there is no repo key + return keydata.encode("utf-8") # remote repo: msgpack issue #99, returning bytes + + def destroy(self): + """Destroy the repository at `self.path`""" + if self.append_only: + raise ValueError(self.path + " is in append-only mode") + self.close() + os.remove(os.path.join(self.path, "config")) # kill config first + shutil.rmtree(self.path) + + def get_index_transaction_id(self): + indices = sorted( + int(fn[6:]) + for fn in os.listdir(self.path) + if fn.startswith("index.") and fn[6:].isdigit() and os.stat(os.path.join(self.path, fn)).st_size != 0 + ) + if indices: + return indices[-1] + else: + return None + + def check_transaction(self): + index_transaction_id = self.get_index_transaction_id() + segments_transaction_id = self.io.get_segments_transaction_id() + if index_transaction_id is not None and segments_transaction_id is None: + # we have a transaction id from the index, but we did not find *any* + # commit in the segment files (thus no segments transaction id). + # this can happen if a lot of segment files are lost, e.g. due to a + # filesystem or hardware malfunction. it means we have no identifiable + # valid (committed) state of the repo which we could use. + msg = '%s" - although likely this is "beyond repair' % self.path # dirty hack + raise self.CheckNeeded(msg) + # Attempt to rebuild index automatically if we crashed between commit + # tag write and index save. + if index_transaction_id != segments_transaction_id: + if index_transaction_id is not None and index_transaction_id > segments_transaction_id: + replay_from = None + else: + replay_from = index_transaction_id + self.replay_segments(replay_from, segments_transaction_id) + + def get_transaction_id(self): + self.check_transaction() + return self.get_index_transaction_id() + + def break_lock(self): + Lock(os.path.join(self.path, "lock")).break_lock() + + def migrate_lock(self, old_id, new_id): + # note: only needed for local repos + if self.lock is not None: + self.lock.migrate_lock(old_id, new_id) + + def open(self, path, exclusive, lock_wait=None, lock=True): + self.path = path + try: + st = os.stat(path) + except FileNotFoundError: + raise self.DoesNotExist(path) + if not stat.S_ISDIR(st.st_mode): + raise self.InvalidRepository(path) + if lock: + self.lock = Lock(os.path.join(path, "lock"), exclusive, timeout=lock_wait).acquire() + else: + self.lock = None + self.config = ConfigParser(interpolation=None) + try: + with open(os.path.join(self.path, "config")) as fd: + self.config.read_file(fd) + except FileNotFoundError: + self.close() + raise self.InvalidRepository(self.path) + if "repository" not in self.config.sections(): + self.close() + raise self.InvalidRepositoryConfig(path, "no repository section found") + self.version = self.config.getint("repository", "version") + if self.version not in self.acceptable_repo_versions: + self.close() + raise self.InvalidRepositoryConfig( + path, "repository version %d is not supported by this borg version" % self.version + ) + self.max_segment_size = parse_file_size(self.config.get("repository", "max_segment_size")) + if self.max_segment_size >= MAX_SEGMENT_SIZE_LIMIT: + self.close() + raise self.InvalidRepositoryConfig(path, "max_segment_size >= %d" % MAX_SEGMENT_SIZE_LIMIT) # issue 3592 + self.segments_per_dir = self.config.getint("repository", "segments_per_dir") + self.additional_free_space = parse_file_size(self.config.get("repository", "additional_free_space", fallback=0)) + # append_only can be set in the constructor + # it shouldn't be overridden (True -> False) here + self.append_only = self.append_only or self.config.getboolean("repository", "append_only", fallback=False) + if self.storage_quota is None: + # self.storage_quota is None => no explicit storage_quota was specified, use repository setting. + self.storage_quota = parse_file_size(self.config.get("repository", "storage_quota", fallback=0)) + self.id = hex_to_bin(self.config.get("repository", "id").strip(), length=32) + self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir) + + def _load_hints(self): + if (transaction_id := self.get_transaction_id()) is None: + # self is a fresh repo, so transaction_id is None and there is no hints file + return + hints = self._unpack_hints(transaction_id) + self.version = hints["version"] + self.storage_quota_use = hints["storage_quota_use"] + self.shadow_index = hints["shadow_index"] + + def info(self): + """return some infos about the repo (must be opened first)""" + info = dict(id=self.id, version=self.version, append_only=self.append_only) + self._load_hints() + info["storage_quota"] = self.storage_quota + info["storage_quota_use"] = self.storage_quota_use + return info + + def close(self): + if self.lock: + if self.io: + self.io.close() + self.io = None + self.lock.release() + self.lock = None + + def commit(self, compact=True, threshold=0.1): + """Commit transaction""" + if self.transaction_doomed: + exception = self.transaction_doomed + self.rollback() + raise exception + self.check_free_space() + segment = self.io.write_commit() + self.segments.setdefault(segment, 0) + self.compact[segment] += LoggedIO.header_fmt.size + if compact and not self.append_only: + self.compact_segments(threshold) + self.write_index() + self.rollback() + + def _read_integrity(self, transaction_id, key): + integrity_file = "integrity.%d" % transaction_id + integrity_path = os.path.join(self.path, integrity_file) + try: + with open(integrity_path, "rb") as fd: + integrity = msgpack.unpack(fd) + except FileNotFoundError: + return + if integrity.get("version") != 2: + logger.warning("Unknown integrity data version %r in %s", integrity.get("version"), integrity_file) + return + return integrity[key] + + def open_index(self, transaction_id, auto_recover=True): + if transaction_id is None: + return NSIndex() + index_path = os.path.join(self.path, "index.%d" % transaction_id) + variant = hashindex_variant(index_path) + integrity_data = self._read_integrity(transaction_id, "index") + try: + with IntegrityCheckedFile(index_path, write=False, integrity_data=integrity_data) as fd: + if variant == 2: + return NSIndex.read(fd) + if variant == 1: # legacy + return NSIndex1.read(fd) + except (ValueError, OSError, FileIntegrityError) as exc: + logger.warning("Repository index missing or corrupted, trying to recover from: %s", exc) + os.unlink(index_path) + if not auto_recover: + raise + self.prepare_txn(self.get_transaction_id()) + # don't leave an open transaction around + self.commit(compact=False) + return self.open_index(self.get_transaction_id()) + + def _unpack_hints(self, transaction_id): + hints_path = os.path.join(self.path, "hints.%d" % transaction_id) + integrity_data = self._read_integrity(transaction_id, "hints") + with IntegrityCheckedFile(hints_path, write=False, integrity_data=integrity_data) as fd: + return msgpack.unpack(fd) + + def prepare_txn(self, transaction_id, do_cleanup=True): + self._active_txn = True + if self.do_lock and not self.lock.got_exclusive_lock(): + if self.exclusive is not None: + # self.exclusive is either True or False, thus a new client is active here. + # if it is False and we get here, the caller did not use exclusive=True although + # it is needed for a write operation. if it is True and we get here, something else + # went very wrong, because we should have an exclusive lock, but we don't. + raise AssertionError("bug in code, exclusive lock should exist here") + # if we are here, this is an old client talking to a new server (expecting lock upgrade). + # or we are replaying segments and might need a lock upgrade for that. + try: + self.lock.upgrade() + except (LockError, LockErrorT): + # if upgrading the lock to exclusive fails, we do not have an + # active transaction. this is important for "serve" mode, where + # the repository instance lives on - even if exceptions happened. + self._active_txn = False + raise + if not self.index or transaction_id is None: + try: + self.index = self.open_index(transaction_id, auto_recover=False) + except (ValueError, OSError, FileIntegrityError) as exc: + logger.warning("Checking repository transaction due to previous error: %s", exc) + self.check_transaction() + self.index = self.open_index(transaction_id, auto_recover=False) + if transaction_id is None: + self.segments = {} # XXX bad name: usage_count_of_segment_x = self.segments[x] + self.compact = FreeSpace() # XXX bad name: freeable_space_of_segment_x = self.compact[x] + self.storage_quota_use = 0 + self.shadow_index.clear() + else: + if do_cleanup: + self.io.cleanup(transaction_id) + hints_path = os.path.join(self.path, "hints.%d" % transaction_id) + index_path = os.path.join(self.path, "index.%d" % transaction_id) + try: + hints = self._unpack_hints(transaction_id) + except (msgpack.UnpackException, FileNotFoundError, FileIntegrityError) as e: + logger.warning("Repository hints file missing or corrupted, trying to recover: %s", e) + if not isinstance(e, FileNotFoundError): + os.unlink(hints_path) + # index must exist at this point + os.unlink(index_path) + self.check_transaction() + self.prepare_txn(transaction_id) + return + if hints["version"] == 1: + logger.debug("Upgrading from v1 hints.%d", transaction_id) + self.segments = hints["segments"] + self.compact = FreeSpace() + self.storage_quota_use = 0 + self.shadow_index = {} + for segment in sorted(hints["compact"]): + logger.debug("Rebuilding sparse info for segment %d", segment) + self._rebuild_sparse(segment) + logger.debug("Upgrade to v2 hints complete") + elif hints["version"] != 2: + raise ValueError("Unknown hints file version: %d" % hints["version"]) + else: + self.segments = hints["segments"] + self.compact = FreeSpace(hints["compact"]) + self.storage_quota_use = hints.get("storage_quota_use", 0) + self.shadow_index = hints.get("shadow_index", {}) + # Drop uncommitted segments in the shadow index + for key, shadowed_segments in self.shadow_index.items(): + for segment in list(shadowed_segments): + if segment > transaction_id: + shadowed_segments.remove(segment) + + def write_index(self): + def flush_and_sync(fd): + fd.flush() + os.fsync(fd.fileno()) + + def rename_tmp(file): + os.replace(file + ".tmp", file) + + hints = { + "version": 2, + "segments": self.segments, + "compact": self.compact, + "storage_quota_use": self.storage_quota_use, + "shadow_index": self.shadow_index, + } + integrity = { + # Integrity version started at 2, the current hints version. + # Thus, integrity version == hints version, for now. + "version": 2 + } + transaction_id = self.io.get_segments_transaction_id() + assert transaction_id is not None + + # Log transaction in append-only mode + if self.append_only: + with open(os.path.join(self.path, "transactions"), "a") as log: + print( + "transaction %d, UTC time %s" + % (transaction_id, datetime.now(tz=timezone.utc).isoformat(timespec="microseconds")), + file=log, + ) + + # Write hints file + hints_name = "hints.%d" % transaction_id + hints_file = os.path.join(self.path, hints_name) + with IntegrityCheckedFile(hints_file + ".tmp", filename=hints_name, write=True) as fd: + msgpack.pack(hints, fd) + flush_and_sync(fd) + integrity["hints"] = fd.integrity_data + + # Write repository index + index_name = "index.%d" % transaction_id + index_file = os.path.join(self.path, index_name) + with IntegrityCheckedFile(index_file + ".tmp", filename=index_name, write=True) as fd: + # XXX: Consider using SyncFile for index write-outs. + self.index.write(fd) + flush_and_sync(fd) + integrity["index"] = fd.integrity_data + + # Write integrity file, containing checksums of the hints and index files + integrity_name = "integrity.%d" % transaction_id + integrity_file = os.path.join(self.path, integrity_name) + with open(integrity_file + ".tmp", "wb") as fd: + msgpack.pack(integrity, fd) + flush_and_sync(fd) + + # Rename the integrity file first + rename_tmp(integrity_file) + sync_dir(self.path) + # Rename the others after the integrity file is hypothetically on disk + rename_tmp(hints_file) + rename_tmp(index_file) + sync_dir(self.path) + + # Remove old auxiliary files + current = ".%d" % transaction_id + for name in os.listdir(self.path): + if not name.startswith(("index.", "hints.", "integrity.")): + continue + if name.endswith(current): + continue + os.unlink(os.path.join(self.path, name)) + self.index = None + + def check_free_space(self): + """Pre-commit check for sufficient free space necessary to perform the commit.""" + # As a baseline we take four times the current (on-disk) index size. + # At this point the index may only be updated by compaction, which won't resize it. + # We still apply a factor of four so that a later, separate invocation can free space + # (journaling all deletes for all chunks is one index size) or still make minor additions + # (which may grow the index up to twice its current size). + # Note that in a subsequent operation the committed index is still on-disk, therefore we + # arrive at index_size * (1 + 2 + 1). + # In that order: journaled deletes (1), hashtable growth (2), persisted index (1). + required_free_space = self.index.size() * 4 + + # Conservatively estimate hints file size: + # 10 bytes for each segment-refcount pair, 10 bytes for each segment-space pair + # Assume maximum of 5 bytes per integer. Segment numbers will usually be packed more densely (1-3 bytes), + # as will refcounts and free space integers. For 5 MiB segments this estimate is good to ~20 PB repo size. + # Add a generous 4K to account for constant format overhead. + hints_size = len(self.segments) * 10 + len(self.compact) * 10 + 4096 + required_free_space += hints_size + + required_free_space += self.additional_free_space + if not self.append_only: + full_segment_size = self.max_segment_size + MAX_OBJECT_SIZE + if len(self.compact) < 10: + # This is mostly for the test suite to avoid overestimated free space needs. This can be annoying + # if TMP is a small-ish tmpfs. + compact_working_space = 0 + for segment, free in self.compact.items(): + try: + compact_working_space += self.io.segment_size(segment) - free + except FileNotFoundError: + # looks like self.compact is referring to a nonexistent segment file, ignore it. + pass + logger.debug("check_free_space: Few segments, not requiring a full free segment") + compact_working_space = min(compact_working_space, full_segment_size) + logger.debug( + "check_free_space: Calculated working space for compact as %d bytes", compact_working_space + ) + required_free_space += compact_working_space + else: + # Keep one full worst-case segment free in non-append-only mode + required_free_space += full_segment_size + + try: + free_space = shutil.disk_usage(self.path).free + except OSError as os_error: + logger.warning("Failed to check free space before committing: " + str(os_error)) + return + logger.debug(f"check_free_space: Required bytes {required_free_space}, free bytes {free_space}") + if free_space < required_free_space: + if self.created: + logger.error("Not enough free space to initialize repository at this location.") + self.destroy() + else: + self._rollback(cleanup=True) + formatted_required = format_file_size(required_free_space) + formatted_free = format_file_size(free_space) + raise self.InsufficientFreeSpaceError(formatted_required, formatted_free) + + def compact_segments(self, threshold): + """Compact sparse segments by copying data into new segments""" + if not self.compact: + logger.debug("Nothing to do: compact empty") + return + quota_use_before = self.storage_quota_use + index_transaction_id = self.get_index_transaction_id() + segments = self.segments + unused = [] # list of segments, that are not used anymore + + def complete_xfer(intermediate=True): + # complete the current transfer (when some target segment is full) + nonlocal unused + # commit the new, compact, used segments + segment = self.io.write_commit(intermediate=intermediate) + self.segments.setdefault(segment, 0) + self.compact[segment] += LoggedIO.header_fmt.size + logger.debug( + "complete_xfer: Wrote %scommit at segment %d", "intermediate " if intermediate else "", segment + ) + # get rid of the old, sparse, unused segments. free space. + for segment in unused: + logger.debug("complete_xfer: Deleting unused segment %d", segment) + count = self.segments.pop(segment) + assert count == 0, "Corrupted segment reference count - corrupted index or hints" + self.io.delete_segment(segment) + del self.compact[segment] + unused = [] + + logger.debug("Compaction started (threshold is %i%%).", threshold * 100) + pi = ProgressIndicatorPercent( + total=len(self.compact), msg="Compacting segments %3.0f%%", step=1, msgid="repository.compact_segments" + ) + for segment, freeable_space in sorted(self.compact.items()): + if not self.io.segment_exists(segment): + logger.warning("Segment %d not found, but listed in compaction data", segment) + del self.compact[segment] + pi.show() + self._send_log() + continue + segment_size = self.io.segment_size(segment) + freeable_ratio = 1.0 * freeable_space / segment_size + # we want to compact if: + # - we can free a considerable relative amount of space (freeable_ratio over some threshold) + if not (freeable_ratio > threshold): + logger.debug( + "Not compacting segment %d (maybe freeable: %2.2f%% [%d bytes])", + segment, + freeable_ratio * 100.0, + freeable_space, + ) + pi.show() + self._send_log() + continue + segments.setdefault(segment, 0) + logger.debug( + "Compacting segment %d with usage count %d (maybe freeable: %2.2f%% [%d bytes])", + segment, + segments[segment], + freeable_ratio * 100.0, + freeable_space, + ) + for tag, key, offset, _, data in self.io.iter_objects(segment): + if tag == TAG_COMMIT: + continue + in_index = self.index.get(key) + is_index_object = in_index and (in_index.segment, in_index.offset) == (segment, offset) + if tag in (TAG_PUT2, TAG_PUT) and is_index_object: + try: + new_segment, offset = self.io.write_put(key, data, raise_full=True) + except LoggedIO.SegmentFull: + complete_xfer() + new_segment, offset = self.io.write_put(key, data) + self.index[key] = NSIndexEntry(new_segment, offset, len(data)) + segments.setdefault(new_segment, 0) + segments[new_segment] += 1 + segments[segment] -= 1 + if tag == TAG_PUT: + # old tag is PUT, but new will be PUT2 and use a bit more storage + self.storage_quota_use += self.io.ENTRY_HASH_SIZE + elif tag in (TAG_PUT2, TAG_PUT) and not is_index_object: + # If this is a PUT shadowed by a later tag, then it will be gone when this segment is deleted after + # this loop. Therefore it is removed from the shadow index. + try: + self.shadow_index[key].remove(segment) + except (KeyError, ValueError): + # do not remove entry with empty shadowed_segments list here, + # it is needed for shadowed_put_exists code (see below)! + pass + self.storage_quota_use -= header_size(tag) + len(data) + elif tag == TAG_DELETE and not in_index: + # If the shadow index doesn't contain this key, then we can't say if there's a shadowed older tag, + # therefore we do not drop the delete, but write it to a current segment. + key_not_in_shadow_index = key not in self.shadow_index + # If the key is in the shadow index and there is any segment with an older PUT of this + # key, we have a shadowed put. + shadowed_put_exists = key_not_in_shadow_index or any( + shadowed < segment for shadowed in self.shadow_index[key] + ) + delete_is_not_stable = index_transaction_id is None or segment > index_transaction_id + + if shadowed_put_exists or delete_is_not_stable: + # (introduced in 6425d16aa84be1eaaf88) + # This is needed to avoid object un-deletion if we crash between the commit and the deletion + # of old segments in complete_xfer(). + # + # However, this only happens if the crash also affects the FS to the effect that file deletions + # did not materialize consistently after journal recovery. If they always materialize in-order + # then this is not a problem, because the old segment containing a deleted object would be + # deleted before the segment containing the delete. + # + # Consider the following series of operations if we would not do this, i.e. this entire if: + # would be removed. + # Columns are segments, lines are different keys (line 1 = some key, line 2 = some other key) + # Legend: P=TAG_PUT/TAG_PUT2, D=TAG_DELETE, c=commit, i=index is written for latest commit + # + # Segment | 1 | 2 | 3 + # --------+-------+-----+------ + # Key 1 | P | D | + # Key 2 | P | | P + # commits | c i | c | c i + # --------+-------+-----+------ + # ^- compact_segments starts + # ^- complete_xfer commits, after that complete_xfer deletes + # segments 1 and 2 (and then the index would be written). + # + # Now we crash. But only segment 2 gets deleted, while segment 1 is still around. Now key 1 + # is suddenly undeleted (because the delete in segment 2 is now missing). + # Again, note the requirement here. We delete these in the correct order that this doesn't + # happen, and only if the FS materialization of these deletes is reordered or parts dropped + # this can happen. + # In this case it doesn't cause outright corruption, 'just' an index count mismatch, which + # will be fixed by borg-check --repair. + # + # Note that in this check the index state is the proxy for a "most definitely settled" + # repository state, i.e. the assumption is that *all* operations on segments <= index state + # are completed and stable. + try: + new_segment, size = self.io.write_delete(key, raise_full=True) + except LoggedIO.SegmentFull: + complete_xfer() + new_segment, size = self.io.write_delete(key) + self.compact[new_segment] += size + segments.setdefault(new_segment, 0) + else: + logger.debug( + "Dropping DEL for id %s - seg %d, iti %r, knisi %r, spe %r, dins %r, si %r", + bin_to_hex(key), + segment, + index_transaction_id, + key_not_in_shadow_index, + shadowed_put_exists, + delete_is_not_stable, + self.shadow_index.get(key), + ) + # we did not keep the delete tag for key (see if-branch) + if not self.shadow_index[key]: + # shadowed segments list is empty -> remove it + del self.shadow_index[key] + assert segments[segment] == 0, "Corrupted segment reference count - corrupted index or hints" + unused.append(segment) + pi.show() + self._send_log() + pi.finish() + self._send_log() + complete_xfer(intermediate=False) + self.io.clear_empty_dirs() + quota_use_after = self.storage_quota_use + logger.info("Compaction freed about %s repository space.", format_file_size(quota_use_before - quota_use_after)) + logger.debug("Compaction completed.") + + def replay_segments(self, index_transaction_id, segments_transaction_id): + # fake an old client, so that in case we do not have an exclusive lock yet, prepare_txn will upgrade the lock: + remember_exclusive = self.exclusive + self.exclusive = None + self.prepare_txn(index_transaction_id, do_cleanup=False) + try: + segment_count = sum(1 for _ in self.io.segment_iterator()) + pi = ProgressIndicatorPercent( + total=segment_count, msg="Replaying segments %3.0f%%", msgid="repository.replay_segments" + ) + for i, (segment, filename) in enumerate(self.io.segment_iterator()): + pi.show(i) + self._send_log() + if index_transaction_id is not None and segment <= index_transaction_id: + continue + if segment > segments_transaction_id: + break + objects = self.io.iter_objects(segment) + self._update_index(segment, objects) + pi.finish() + self._send_log() + self.write_index() + finally: + self.exclusive = remember_exclusive + self.rollback() + + def _update_index(self, segment, objects, report=None): + """some code shared between replay_segments and check""" + self.segments[segment] = 0 + for tag, key, offset, size, _ in objects: + if tag in (TAG_PUT2, TAG_PUT): + try: + # If this PUT supersedes an older PUT, mark the old segment for compaction and count the free space + in_index = self.index[key] + self.compact[in_index.segment] += header_size(tag) + size + self.segments[in_index.segment] -= 1 + self.shadow_index.setdefault(key, []).append(in_index.segment) + except KeyError: + pass + self.index[key] = NSIndexEntry(segment, offset, size) + self.segments[segment] += 1 + self.storage_quota_use += header_size(tag) + size + elif tag == TAG_DELETE: + try: + # if the deleted PUT is not in the index, there is nothing to clean up + in_index = self.index.pop(key) + except KeyError: + pass + else: + if self.io.segment_exists(in_index.segment): + # the old index is not necessarily valid for this transaction (e.g. compaction); if the segment + # is already gone, then it was already compacted. + self.segments[in_index.segment] -= 1 + self.compact[in_index.segment] += header_size(tag) + in_index.size + self.shadow_index.setdefault(key, []).append(in_index.segment) + elif tag == TAG_COMMIT: + continue + else: + msg = f"Unexpected tag {tag} in segment {segment}" + if report is None: + raise self.CheckNeeded(msg) + else: + report(msg) + if self.segments[segment] == 0: + self.compact[segment] = self.io.segment_size(segment) + + def _rebuild_sparse(self, segment): + """Rebuild sparse bytes count for a single segment relative to the current index.""" + try: + segment_size = self.io.segment_size(segment) + except FileNotFoundError: + # segment does not exist any more, remove it from the mappings. + # note: no need to self.compact.pop(segment), as we start from empty mapping. + self.segments.pop(segment) + return + + if self.segments[segment] == 0: + self.compact[segment] = segment_size + return + + self.compact[segment] = 0 + for tag, key, offset, size, _ in self.io.iter_objects(segment, read_data=False): + if tag in (TAG_PUT2, TAG_PUT): + in_index = self.index.get(key) + if not in_index or (in_index.segment, in_index.offset) != (segment, offset): + # This PUT is superseded later. + self.compact[segment] += header_size(tag) + size + elif tag == TAG_DELETE: + # The outcome of the DELETE has been recorded in the PUT branch already. + self.compact[segment] += header_size(tag) + size + + def check(self, repair=False, max_duration=0): + """Check repository consistency + + This method verifies all segment checksums and makes sure + the index is consistent with the data stored in the segments. + """ + if self.append_only and repair: + raise ValueError(self.path + " is in append-only mode") + error_found = False + + def report_error(msg, *args): + nonlocal error_found + error_found = True + logger.error(msg, *args) + + logger.info("Starting repository check") + assert not self._active_txn + try: + transaction_id = self.get_transaction_id() + current_index = self.open_index(transaction_id) + logger.debug("Read committed index of transaction %d", transaction_id) + except Exception as exc: + transaction_id = self.io.get_segments_transaction_id() + current_index = None + logger.debug("Failed to read committed index (%s)", exc) + if transaction_id is None: + logger.debug("No segments transaction found") + transaction_id = self.get_index_transaction_id() + if transaction_id is None: + logger.debug("No index transaction found, trying latest segment") + transaction_id = self.io.get_latest_segment() + if transaction_id is None: + report_error("This repository contains no valid data.") + return False + if repair: + self.io.cleanup(transaction_id) + segments_transaction_id = self.io.get_segments_transaction_id() + logger.debug("Segment transaction is %s", segments_transaction_id) + logger.debug("Determined transaction is %s", transaction_id) + self.prepare_txn(None) # self.index, self.compact, self.segments, self.shadow_index all empty now! + segment_count = sum(1 for _ in self.io.segment_iterator()) + logger.debug("Found %d segments", segment_count) + + partial = bool(max_duration) + assert not (repair and partial) + mode = "partial" if partial else "full" + if partial: + # continue a past partial check (if any) or start one from beginning + last_segment_checked = self.config.getint("repository", "last_segment_checked", fallback=-1) + logger.info("Skipping to segments >= %d", last_segment_checked + 1) + else: + # start from the beginning and also forget about any potential past partial checks + last_segment_checked = -1 + self.config.remove_option("repository", "last_segment_checked") + self.save_config(self.path, self.config) + t_start = time.monotonic() + pi = ProgressIndicatorPercent( + total=segment_count, msg="Checking segments %3.1f%%", step=0.1, msgid="repository.check" + ) + segment = -1 # avoid uninitialized variable if there are no segment files at all + for i, (segment, filename) in enumerate(self.io.segment_iterator()): + pi.show(i) + self._send_log() + if segment <= last_segment_checked: + continue + if segment > transaction_id: + continue + logger.debug("Checking segment file %s...", filename) + try: + objects = list(self.io.iter_objects(segment)) + except IntegrityError as err: + report_error(str(err)) + objects = [] + if repair: + self.io.recover_segment(segment, filename) + objects = list(self.io.iter_objects(segment)) + if not partial: + self._update_index(segment, objects, report_error) + if partial and time.monotonic() > t_start + max_duration: + logger.info("Finished partial segment check, last segment checked is %d", segment) + self.config.set("repository", "last_segment_checked", str(segment)) + self.save_config(self.path, self.config) + break + else: + logger.info("Finished segment check at segment %d", segment) + self.config.remove_option("repository", "last_segment_checked") + self.save_config(self.path, self.config) + + pi.finish() + self._send_log() + # self.index, self.segments, self.compact now reflect the state of the segment files up to . + # We might need to add a commit tag if no committed segment is found. + if repair and segments_transaction_id is None: + report_error(f"Adding commit tag to segment {transaction_id}") + self.io.segment = transaction_id + 1 + self.io.write_commit() + if not partial: + logger.info("Starting repository index check") + if current_index and not repair: + # current_index = "as found on disk" + # self.index = "as rebuilt in-memory from segments" + if len(current_index) != len(self.index): + report_error("Index object count mismatch.") + report_error("committed index: %d objects", len(current_index)) + report_error("rebuilt index: %d objects", len(self.index)) + else: + logger.info("Index object count match.") + line_format = "ID: %-64s rebuilt index: %-16s committed index: %-16s" + not_found = "" + for key, value in self.index.iteritems(): + current_value = current_index.get(key, not_found) + if current_value != value: + report_error(line_format, bin_to_hex(key), value, current_value) + self._send_log() + for key, current_value in current_index.iteritems(): + if key in self.index: + continue + value = self.index.get(key, not_found) + if current_value != value: + report_error(line_format, bin_to_hex(key), value, current_value) + self._send_log() + if repair: + self.write_index() + self.rollback() + if error_found: + if repair: + logger.info("Finished %s repository check, errors found and repaired.", mode) + else: + logger.error("Finished %s repository check, errors found.", mode) + else: + logger.info("Finished %s repository check, no problems found.", mode) + return not error_found or repair + + def _rollback(self, *, cleanup): + if cleanup: + self.io.cleanup(self.io.get_segments_transaction_id()) + self.index = None + self._active_txn = False + self.transaction_doomed = None + + def rollback(self): + # note: when used in remote mode, this is time limited, see LegacyRemoteRepository.shutdown_time. + self._rollback(cleanup=False) + + def __len__(self): + if not self.index: + self.index = self.open_index(self.get_transaction_id()) + return len(self.index) + + def __contains__(self, id): + if not self.index: + self.index = self.open_index(self.get_transaction_id()) + return id in self.index + + def list(self, limit=None, marker=None): + """ + list IDs starting from after id - in index (pseudo-random) order. + """ + if not self.index: + self.index = self.open_index(self.get_transaction_id()) + return [id_ for id_, _ in islice(self.index.iteritems(marker=marker), limit)] + + def get(self, id, read_data=True): + if not self.index: + self.index = self.open_index(self.get_transaction_id()) + try: + in_index = NSIndexEntry(*((self.index[id] + (None,))[:3])) # legacy: index entries have no size element + return self.io.read(in_index.segment, in_index.offset, id, expected_size=in_index.size, read_data=read_data) + except KeyError: + raise self.ObjectNotFound(id, self.path) from None + + def get_many(self, ids, read_data=True, is_preloaded=False): + for id_ in ids: + yield self.get(id_, read_data=read_data) + + def put(self, id, data, wait=True): + """put a repo object + + Note: when doing calls with wait=False this gets async and caller must + deal with async results / exceptions later. + """ + if not self._active_txn: + self.prepare_txn(self.get_transaction_id()) + try: + in_index = self.index[id] + except KeyError: + pass + else: + # this put call supersedes a previous put to same id. + # it is essential to do a delete first to get correct quota bookkeeping + # and also a correctly updated shadow_index, so that the compaction code + # does not wrongly resurrect an old PUT by dropping a DEL that is still needed. + self._delete(id, in_index.segment, in_index.offset, in_index.size) + segment, offset = self.io.write_put(id, data) + self.storage_quota_use += header_size(TAG_PUT2) + len(data) + self.segments.setdefault(segment, 0) + self.segments[segment] += 1 + self.index[id] = NSIndexEntry(segment, offset, len(data)) + if self.storage_quota and self.storage_quota_use > self.storage_quota: + self.transaction_doomed = self.StorageQuotaExceeded( + format_file_size(self.storage_quota), format_file_size(self.storage_quota_use) + ) + raise self.transaction_doomed + + def delete(self, id, wait=True): + """delete a repo object + + Note: when doing calls with wait=False this gets async and caller must + deal with async results / exceptions later. + """ + if not self._active_txn: + self.prepare_txn(self.get_transaction_id()) + try: + in_index = self.index.pop(id) + except KeyError: + raise self.ObjectNotFound(id, self.path) from None + self._delete(id, in_index.segment, in_index.offset, in_index.size) + + def _delete(self, id, segment, offset, size): + # common code used by put and delete + # because we'll write a DEL tag to the repository, we must update the shadow index. + # this is always true, no matter whether we are called from put() or delete(). + # the compaction code needs this to not drop DEL tags if they are still required + # to keep a PUT in an earlier segment in the "effectively deleted" state. + self.shadow_index.setdefault(id, []).append(segment) + self.segments[segment] -= 1 + self.compact[segment] += header_size(TAG_PUT2) + size + segment, size = self.io.write_delete(id) + self.compact[segment] += size + self.segments.setdefault(segment, 0) + + def async_response(self, wait=True): + """Get one async result (only applies to remote repositories). + + async commands (== calls with wait=False, e.g. delete and put) have no results, + but may raise exceptions. These async exceptions must get collected later via + async_response() calls. Repeat the call until it returns None. + The previous calls might either return one (non-None) result or raise an exception. + If wait=True is given and there are outstanding responses, it will wait for them + to arrive. With wait=False, it will only return already received responses. + """ + + def preload(self, ids): + """Preload objects (only applies to remote repositories)""" + + def get_manifest(self): + try: + return self.get(Manifest.MANIFEST_ID) + except self.ObjectNotFound: + raise NoManifestError + + def put_manifest(self, data): + return self.put(Manifest.MANIFEST_ID, data) + + +class LoggedIO: + class SegmentFull(Exception): + """raised when a segment is full, before opening next""" + + header_fmt = struct.Struct(" transaction_id: + self.delete_segment(segment) + count += 1 + else: + break + logger.debug("Cleaned up %d uncommitted segment files (== everything after segment %d).", count, transaction_id) + + def is_committed_segment(self, segment): + """Check if segment ends with a COMMIT_TAG tag""" + try: + iterator = self.iter_objects(segment) + except IntegrityError: + return False + with open(self.segment_filename(segment), "rb") as fd: + try: + fd.seek(-self.header_fmt.size, os.SEEK_END) + except OSError as e: + # return False if segment file is empty or too small + if e.errno == errno.EINVAL: + return False + raise e + if fd.read(self.header_fmt.size) != self.COMMIT: + return False + seen_commit = False + while True: + try: + tag, key, offset, _, _ = next(iterator) + except IntegrityError: + return False + except StopIteration: + break + if tag == TAG_COMMIT: + seen_commit = True + continue + if seen_commit: + return False + return seen_commit + + def segment_filename(self, segment): + return os.path.join(self.path, "data", str(segment // self.segments_per_dir), str(segment)) + + def get_write_fd(self, no_new=False, want_new=False, raise_full=False): + if not no_new and (want_new or self.offset and self.offset > self.limit): + if raise_full: + raise self.SegmentFull + self.close_segment() + if not self._write_fd: + if self.segment % self.segments_per_dir == 0: + dirname = os.path.join(self.path, "data", str(self.segment // self.segments_per_dir)) + if not os.path.exists(dirname): + os.mkdir(dirname) + sync_dir(os.path.join(self.path, "data")) + self._write_fd = SyncFile(self.segment_filename(self.segment), binary=True) + self._write_fd.write(MAGIC) + self.offset = MAGIC_LEN + if self.segment in self.fds: + # we may have a cached fd for a segment file we already deleted and + # we are writing now a new segment file to same file name. get rid of + # the cached fd that still refers to the old file, so it will later + # get repopulated (on demand) with a fd that refers to the new file. + del self.fds[self.segment] + return self._write_fd + + def get_fd(self, segment): + # note: get_fd() returns a fd with undefined file pointer position, + # so callers must always seek() to desired position afterwards. + now = time.monotonic() + + def open_fd(): + fd = open(self.segment_filename(segment), "rb") + self.fds[segment] = (now, fd) + return fd + + def clean_old(): + # we regularly get rid of all old FDs here: + if now - self._fds_cleaned > FD_MAX_AGE // 8: + self._fds_cleaned = now + for k, ts_fd in list(self.fds.items()): + ts, fd = ts_fd + if now - ts > FD_MAX_AGE: + # we do not want to touch long-unused file handles to + # avoid ESTALE issues (e.g. on network filesystems). + del self.fds[k] + + clean_old() + if self._write_fd is not None: + # without this, we have a test failure now + self._write_fd.sync() + try: + ts, fd = self.fds[segment] + except KeyError: + fd = open_fd() + else: + # we only have fresh enough stuff here. + # update the timestamp of the lru cache entry. + self.fds.replace(segment, (now, fd)) + return fd + + def close_segment(self): + # set self._write_fd to None early to guard against reentry from error handling code paths: + fd, self._write_fd = self._write_fd, None + if fd is not None: + self.segment += 1 + self.offset = 0 + fd.close() + + def delete_segment(self, segment): + if segment in self.fds: + del self.fds[segment] + try: + safe_unlink(self.segment_filename(segment)) + except FileNotFoundError: + pass + + def clear_empty_dirs(self): + """Delete empty segment dirs, i.e those with no segment files.""" + data_dir = os.path.join(self.path, "data") + segment_dirs = self.get_segment_dirs(data_dir) + for segment_dir in segment_dirs: + try: + # os.rmdir will only delete the directory if it is empty + # so we don't need to explicitly check for emptiness first. + os.rmdir(segment_dir) + except OSError: + # OSError is raised by os.rmdir if directory is not empty. This is expected. + # Its subclass FileNotFoundError may be raised if the directory already does not exist. Ignorable. + pass + sync_dir(data_dir) + + def segment_exists(self, segment): + filename = self.segment_filename(segment) + # When deleting segments, they are first truncated. If truncate(2) and unlink(2) are split + # across FS transactions, then logically deleted segments will show up as truncated. + return os.path.exists(filename) and os.path.getsize(filename) + + def segment_size(self, segment): + return os.path.getsize(self.segment_filename(segment)) + + def get_segment_magic(self, segment): + fd = self.get_fd(segment) + fd.seek(0) + return fd.read(MAGIC_LEN) + + def iter_objects(self, segment, read_data=True): + """ + Return object iterator for *segment*. + + See the _read() docstring about confidence in the returned data. + + The iterator returns five-tuples of (tag, key, offset, size, data). + """ + fd = self.get_fd(segment) + offset = 0 + fd.seek(offset) + if fd.read(MAGIC_LEN) != MAGIC: + raise IntegrityError(f"Invalid segment magic [segment {segment}, offset {offset}]") + offset = MAGIC_LEN + header = fd.read(self.header_fmt.size) + while header: + size, tag, key, data = self._read( + fd, header, segment, offset, (TAG_PUT2, TAG_DELETE, TAG_COMMIT, TAG_PUT), read_data=read_data + ) + # tuple[3]: corresponds to len(data) == length of the full chunk payload (meta_len+enc_meta+enc_data) + # tuple[4]: data will be None if read_data is False. + yield tag, key, offset, size - header_size(tag), data + assert size >= 0 + offset += size + # we must get the fd via get_fd() here again as we yielded to our caller and it might + # have triggered closing of the fd we had before (e.g. by calling io.read() for + # different segment(s)). + # by calling get_fd() here again we also make our fd "recently used" so it likely + # does not get kicked out of self.fds LRUcache. + fd = self.get_fd(segment) + fd.seek(offset) + header = fd.read(self.header_fmt.size) + + def recover_segment(self, segment, filename): + logger.info("Attempting to recover " + filename) + if segment in self.fds: + del self.fds[segment] + if os.path.getsize(filename) < MAGIC_LEN + self.header_fmt.size: + # this is either a zero-byte file (which would crash mmap() below) or otherwise + # just too small to be a valid non-empty segment file, so do a shortcut here: + with SaveFile(filename, binary=True) as fd: + fd.write(MAGIC) + return + with SaveFile(filename, binary=True) as dst_fd: + with open(filename, "rb") as src_fd: + # note: file must not be 0 size or mmap() will crash. + with mmap.mmap(src_fd.fileno(), 0, access=mmap.ACCESS_READ) as mm: + # memoryview context manager is problematic, see https://bugs.python.org/issue35686 + data = memoryview(mm) + d = data + try: + dst_fd.write(MAGIC) + while len(d) >= self.header_fmt.size: + crc, size, tag = self.header_fmt.unpack(d[: self.header_fmt.size]) + size_invalid = size > MAX_OBJECT_SIZE or size < self.header_fmt.size or size > len(d) + if size_invalid or tag > MAX_TAG_ID: + d = d[1:] + continue + if tag == TAG_PUT2: + c_offset = self.HEADER_ID_SIZE + self.ENTRY_HASH_SIZE + # skip if header is invalid + if crc32(d[4:c_offset]) & 0xFFFFFFFF != crc: + d = d[1:] + continue + # skip if content is invalid + if ( + self.entry_hash(d[4 : self.HEADER_ID_SIZE], d[c_offset:size]) + != d[self.HEADER_ID_SIZE : c_offset] + ): + d = d[1:] + continue + elif tag in (TAG_DELETE, TAG_COMMIT, TAG_PUT): + if crc32(d[4:size]) & 0xFFFFFFFF != crc: + d = d[1:] + continue + else: # tag unknown + d = d[1:] + continue + dst_fd.write(d[:size]) + d = d[size:] + finally: + del d + data.release() + + def entry_hash(self, *data): + h = StreamingXXH64() + for d in data: + h.update(d) + return h.digest() + + def read(self, segment, offset, id, *, read_data=True, expected_size=None): + """ + Read entry from *segment* at *offset* with *id*. + + See the _read() docstring about confidence in the returned data. + """ + if segment == self.segment and self._write_fd: + self._write_fd.sync() + fd = self.get_fd(segment) + fd.seek(offset) + header = fd.read(self.header_fmt.size) + size, tag, key, data = self._read(fd, header, segment, offset, (TAG_PUT2, TAG_PUT), read_data=read_data) + if id != key: + raise IntegrityError( + f"Invalid segment entry header, is not for wanted id [segment {segment}, offset {offset}]" + ) + data_size_from_header = size - header_size(tag) + if expected_size is not None and expected_size != data_size_from_header: + raise IntegrityError( + f"size from repository index: {expected_size} != " f"size from entry header: {data_size_from_header}" + ) + return data + + def _read(self, fd, header, segment, offset, acceptable_tags, read_data=True): + """ + Code shared by read() and iter_objects(). + + Confidence in returned data: + PUT2 tags, read_data == True: crc32 check (header) plus digest check (header+data) + PUT2 tags, read_data == False: crc32 check (header) + PUT tags, read_data == True: crc32 check (header+data) + PUT tags, read_data == False: crc32 check can not be done, all data obtained must be considered informational + + read_data == False behaviour: + PUT2 tags: return enough of the chunk so that the client is able to decrypt the metadata, + do not read, but just seek over the data. + PUT tags: return None and just seek over the data. + """ + + def check_crc32(wanted, header, *data): + result = crc32(memoryview(header)[4:]) # skip first 32 bits of the header, they contain the crc. + for d in data: + result = crc32(d, result) + if result & 0xFFFFFFFF != wanted: + raise IntegrityError(f"Segment entry header checksum mismatch [segment {segment}, offset {offset}]") + + # See comment on MAX_TAG_ID for details + assert max(acceptable_tags) <= MAX_TAG_ID, "Exceeding MAX_TAG_ID will break backwards compatibility" + key = data = None + fmt = self.header_fmt + try: + hdr_tuple = fmt.unpack(header) + except struct.error as err: + raise IntegrityError(f"Invalid segment entry header [segment {segment}, offset {offset}]: {err}") from None + crc, size, tag = hdr_tuple + length = size - fmt.size # we already read the header + if size > MAX_OBJECT_SIZE: + # if you get this on an archive made with borg < 1.0.7 and millions of files and + # you need to restore it, you can disable this check by using "if False:" above. + raise IntegrityError(f"Invalid segment entry size {size} - too big [segment {segment}, offset {offset}]") + if size < fmt.size: + raise IntegrityError(f"Invalid segment entry size {size} - too small [segment {segment}, offset {offset}]") + if tag not in (TAG_PUT2, TAG_DELETE, TAG_COMMIT, TAG_PUT): + raise IntegrityError( + f"Invalid segment entry header, did not get a known tag " f"[segment {segment}, offset {offset}]" + ) + if tag not in acceptable_tags: + raise IntegrityError( + f"Invalid segment entry header, did not get acceptable tag " f"[segment {segment}, offset {offset}]" + ) + if tag == TAG_COMMIT: + check_crc32(crc, header) + # that's all for COMMITs. + else: + # all other tags (TAG_PUT2, TAG_DELETE, TAG_PUT) have a key + key = fd.read(32) + length -= 32 + if len(key) != 32: + raise IntegrityError( + f"Segment entry key short read [segment {segment}, offset {offset}]: " + f"expected {32}, got {len(key)} bytes" + ) + if tag == TAG_DELETE: + check_crc32(crc, header, key) + # that's all for DELETEs. + else: + # TAG_PUT: we can not do a crc32 header check here, because the crc32 is computed over header+data! + # for the check, see code below when read_data is True. + if tag == TAG_PUT2: + entry_hash = fd.read(self.ENTRY_HASH_SIZE) + length -= self.ENTRY_HASH_SIZE + if len(entry_hash) != self.ENTRY_HASH_SIZE: + raise IntegrityError( + f"Segment entry hash short read [segment {segment}, offset {offset}]: " + f"expected {self.ENTRY_HASH_SIZE}, got {len(entry_hash)} bytes" + ) + check_crc32(crc, header, key, entry_hash) + if not read_data: + if tag == TAG_PUT2: + # PUT2 is only used in new repos and they also have different RepoObj layout, + # supporting separately encrypted metadata and data. + # In this case, we return enough bytes so the client can decrypt the metadata + # and seek over the rest (over the encrypted data). + hdr_size = RepoObj.obj_header.size + hdr = fd.read(hdr_size) + length -= hdr_size + if len(hdr) != hdr_size: + raise IntegrityError( + f"Segment entry meta length short read [segment {segment}, offset {offset}]: " + f"expected {hdr_size}, got {len(hdr)} bytes" + ) + meta_size = RepoObj.obj_header.unpack(hdr)[0] + meta = fd.read(meta_size) + length -= meta_size + if len(meta) != meta_size: + raise IntegrityError( + f"Segment entry meta short read [segment {segment}, offset {offset}]: " + f"expected {meta_size}, got {len(meta)} bytes" + ) + data = hdr + meta # shortened chunk - enough so the client can decrypt the metadata + # in any case, we seek over the remainder of the chunk + oldpos = fd.tell() + seeked = fd.seek(length, os.SEEK_CUR) - oldpos + if seeked != length: + raise IntegrityError( + f"Segment entry data short seek [segment {segment}, offset {offset}]: " + f"expected {length}, got {seeked} bytes" + ) + else: # read data! + data = fd.read(length) + if len(data) != length: + raise IntegrityError( + f"Segment entry data short read [segment {segment}, offset {offset}]: " + f"expected {length}, got {len(data)} bytes" + ) + if tag == TAG_PUT2: + if self.entry_hash(memoryview(header)[4:], key, data) != entry_hash: + raise IntegrityError(f"Segment entry hash mismatch [segment {segment}, offset {offset}]") + elif tag == TAG_PUT: + check_crc32(crc, header, key, data) + return size, tag, key, data + + def write_put(self, id, data, raise_full=False): + data_size = len(data) + if data_size > MAX_DATA_SIZE: + # this would push the segment entry size beyond MAX_OBJECT_SIZE. + raise IntegrityError(f"More than allowed put data [{data_size} > {MAX_DATA_SIZE}]") + fd = self.get_write_fd(want_new=(id == Manifest.MANIFEST_ID), raise_full=raise_full) + size = data_size + self.HEADER_ID_SIZE + self.ENTRY_HASH_SIZE + offset = self.offset + header = self.header_no_crc_fmt.pack(size, TAG_PUT2) + entry_hash = self.entry_hash(header, id, data) + crc = self.crc_fmt.pack(crc32(entry_hash, crc32(id, crc32(header))) & 0xFFFFFFFF) + fd.write(b"".join((crc, header, id, entry_hash))) + fd.write(data) + self.offset += size + return self.segment, offset + + def write_delete(self, id, raise_full=False): + fd = self.get_write_fd(want_new=(id == Manifest.MANIFEST_ID), raise_full=raise_full) + header = self.header_no_crc_fmt.pack(self.HEADER_ID_SIZE, TAG_DELETE) + crc = self.crc_fmt.pack(crc32(id, crc32(header)) & 0xFFFFFFFF) + fd.write(b"".join((crc, header, id))) + self.offset += self.HEADER_ID_SIZE + return self.segment, self.HEADER_ID_SIZE + + def write_commit(self, intermediate=False): + # Intermediate commits go directly into the current segment - this makes checking their validity more + # expensive, but is faster and reduces clobber. Final commits go into a new segment. + fd = self.get_write_fd(want_new=not intermediate, no_new=intermediate) + if intermediate: + fd.sync() + header = self.header_no_crc_fmt.pack(self.header_fmt.size, TAG_COMMIT) + crc = self.crc_fmt.pack(crc32(header) & 0xFFFFFFFF) + fd.write(b"".join((crc, header))) + self.close_segment() + return self.segment - 1 # close_segment() increments it + + +assert LoggedIO.HEADER_ID_SIZE + LoggedIO.ENTRY_HASH_SIZE == 41 + 8 # see constants.MAX_OBJECT_SIZE diff --git a/src/borg/manifest.py b/src/borg/manifest.py index 29240baaa..61011f47f 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -242,8 +242,8 @@ def last_timestamp(self): def load(cls, repository, operations, key=None, *, ro_cls=RepoObj): from .item import ManifestItem from .crypto.key import key_factory - from .remote3 import RemoteRepository3 - from .repository3 import Repository3 + from .remote import RemoteRepository + from .repository import Repository cdata = repository.get_manifest() if not key: @@ -256,7 +256,7 @@ def load(cls, repository, operations, key=None, *, ro_cls=RepoObj): if m.get("version") not in (1, 2): raise ValueError("Invalid manifest version") - if isinstance(repository, (Repository3, RemoteRepository3)): + if isinstance(repository, (Repository, RemoteRepository)): from .helpers import msgpack archives = {} @@ -310,8 +310,8 @@ def get_all_mandatory_features(self): def write(self): from .item import ManifestItem - from .remote3 import RemoteRepository3 - from .repository3 import Repository3 + from .remote import RemoteRepository + from .repository import Repository # self.timestamp needs to be strictly monotonically increasing. Clocks often are not set correctly if self.timestamp is None: @@ -327,7 +327,7 @@ def write(self): assert len(self.item_keys) <= 100 self.config["item_keys"] = tuple(sorted(self.item_keys)) - if isinstance(self.repository, (Repository3, RemoteRepository3)): + if isinstance(self.repository, (Repository, RemoteRepository)): valid_keys = set() for name, info in self.archives.get_raw_dict().items(): archive = dict(name=name, id=info["id"], time=info["time"]) diff --git a/src/borg/remote.py b/src/borg/remote.py index 594cc4a8a..3bd85fd90 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -1,8 +1,10 @@ +import atexit import errno import functools import inspect import logging import os +import queue import select import shlex import shutil @@ -12,8 +14,10 @@ import tempfile import textwrap import time +import traceback from subprocess import Popen, PIPE +import borg.logger from . import __version__ from .compress import Compressor from .constants import * # NOQA @@ -21,13 +25,16 @@ from .helpers import bin_to_hex from .helpers import get_limited_unpacker from .helpers import replace_placeholders +from .helpers import sysinfo from .helpers import format_file_size from .helpers import safe_unlink from .helpers import prepare_subprocess_env, ignore_sigint from .helpers import get_socket_filename -from .locking import LockTimeout, NotLocked, NotMyLock, LockFailed -from .logger import create_logger +from .fslocking import LockTimeout, NotLocked, NotMyLock, LockFailed +from .logger import create_logger, borg_serve_log_queue +from .manifest import NoManifestError from .helpers import msgpack +from .legacyrepository import LegacyRepository from .repository import Repository from .version import parse_version, format_version from .checksums import xxh64 @@ -43,6 +50,25 @@ RATELIMIT_PERIOD = 0.1 +def os_write(fd, data): + """os.write wrapper so we do not lose data for partial writes.""" + # TODO: this issue is fixed in cygwin since at least 2.8.0, remove this + # wrapper / workaround when this version is considered ancient. + # This is happening frequently on cygwin due to its small pipe buffer size of only 64kiB + # and also due to its different blocking pipe behaviour compared to Linux/*BSD. + # Neither Linux nor *BSD ever do partial writes on blocking pipes, unless interrupted by a + # signal, in which case serve() would terminate. + amount = remaining = len(data) + while remaining: + count = os.write(fd, data) + remaining -= count + if not remaining: + break + data = data[count:] + time.sleep(count * 1e-09) + return amount + + class ConnectionClosed(Error): """Connection closed by remote host""" @@ -101,7 +127,7 @@ class ConnectionBrokenWithHint(Error): # For the client the return of the negotiate method is a dict which includes the server version. # # All method calls on the remote repository object must be allowlisted in RepositoryServer.rpc_methods and have api -# stubs in RemoteRepository. The @api decorator on these stubs is used to set server version requirements. +# stubs in RemoteRepository*. The @api decorator on these stubs is used to set server version requirements. # # Method parameters are identified only by name and never by position. Unknown parameters are ignored by the server. # If a new parameter is important and may not be ignored, on the client a parameter specific version requirement needs @@ -110,6 +136,317 @@ class ConnectionBrokenWithHint(Error): # servers still get compatible input. +class RepositoryServer: # pragma: no cover + _legacy_rpc_methods = ( # LegacyRepository + "__len__", + "check", + "commit", + "delete", + "destroy", + "get", + "list", + "negotiate", + "open", + "close", + "info", + "put", + "rollback", + "save_key", + "load_key", + "break_lock", + "inject_exception", + ) + + _rpc_methods = ( # Repository + "__len__", + "check", + "delete", + "destroy", + "get", + "list", + "negotiate", + "open", + "close", + "info", + "put", + "save_key", + "load_key", + "break_lock", + "inject_exception", + "get_manifest", + "put_manifest", + "store_list", + "store_load", + "store_store", + "store_delete", + ) + + def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, storage_quota, use_socket): + self.repository = None + self.RepoCls = None + self.rpc_methods = ("open", "close", "negotiate") + self.restrict_to_paths = restrict_to_paths + self.restrict_to_repositories = restrict_to_repositories + # This flag is parsed from the serve command line via Archiver.do_serve, + # i.e. it reflects local system policy and generally ranks higher than + # whatever the client wants, except when initializing a new repository + # (see RepositoryServer.open below). + self.append_only = append_only + self.storage_quota = storage_quota + self.client_version = None # we update this after client sends version information + if use_socket is False: + self.socket_path = None + elif use_socket is True: # --socket + self.socket_path = get_socket_filename() + else: # --socket=/some/path + self.socket_path = use_socket + + def filter_args(self, f, kwargs): + """Remove unknown named parameters from call, because client did (implicitly) say it's ok.""" + known = set(inspect.signature(f).parameters) + return {name: kwargs[name] for name in kwargs if name in known} + + def send_queued_log(self): + while True: + try: + # lr_dict contents see BorgQueueHandler + lr_dict = borg_serve_log_queue.get_nowait() + except queue.Empty: + break + else: + msg = msgpack.packb({LOG: lr_dict}) + os_write(self.stdout_fd, msg) + + def serve(self): + def inner_serve(): + os.set_blocking(self.stdin_fd, False) + assert not os.get_blocking(self.stdin_fd) + os.set_blocking(self.stdout_fd, True) + assert os.get_blocking(self.stdout_fd) + + unpacker = get_limited_unpacker("server") + shutdown_serve = False + while True: + # before processing any new RPCs, send out all pending log output + self.send_queued_log() + + if shutdown_serve: + # shutdown wanted! get out of here after sending all log output. + assert self.repository is None + return + + # process new RPCs + r, w, es = select.select([self.stdin_fd], [], [], 10) + if r: + data = os.read(self.stdin_fd, BUFSIZE) + if not data: + shutdown_serve = True + continue + unpacker.feed(data) + for unpacked in unpacker: + if isinstance(unpacked, dict): + msgid = unpacked[MSGID] + method = unpacked[MSG] + args = unpacked[ARGS] + else: + if self.repository is not None: + self.repository.close() + raise UnexpectedRPCDataFormatFromClient(__version__) + try: + # logger.debug(f"{type(self)} method: {type(self.repository)}.{method}") + if method not in self.rpc_methods: + raise InvalidRPCMethod(method) + try: + f = getattr(self, method) + except AttributeError: + f = getattr(self.repository, method) + args = self.filter_args(f, args) + res = f(**args) + except BaseException as e: + # logger.exception(e) + ex_short = traceback.format_exception_only(e.__class__, e) + ex_full = traceback.format_exception(*sys.exc_info()) + ex_trace = True + if isinstance(e, Error): + ex_short = [e.get_message()] + ex_trace = e.traceback + if isinstance(e, (self.RepoCls.DoesNotExist, self.RepoCls.AlreadyExists, PathNotAllowed)): + # These exceptions are reconstructed on the client end in RemoteRepository*.call_many(), + # and will be handled just like locally raised exceptions. Suppress the remote traceback + # for these, except ErrorWithTraceback, which should always display a traceback. + pass + else: + logging.debug("\n".join(ex_full)) + + sys_info = sysinfo() + try: + msg = msgpack.packb( + { + MSGID: msgid, + "exception_class": e.__class__.__name__, + "exception_args": e.args, + "exception_full": ex_full, + "exception_short": ex_short, + "exception_trace": ex_trace, + "sysinfo": sys_info, + } + ) + except TypeError: + msg = msgpack.packb( + { + MSGID: msgid, + "exception_class": e.__class__.__name__, + "exception_args": [ + x if isinstance(x, (str, bytes, int)) else None for x in e.args + ], + "exception_full": ex_full, + "exception_short": ex_short, + "exception_trace": ex_trace, + "sysinfo": sys_info, + } + ) + os_write(self.stdout_fd, msg) + else: + os_write(self.stdout_fd, msgpack.packb({MSGID: msgid, RESULT: res})) + if es: + shutdown_serve = True + continue + + if self.socket_path: # server for socket:// connections + try: + # remove any left-over socket file + os.unlink(self.socket_path) + except OSError: + if os.path.exists(self.socket_path): + raise + sock_dir = os.path.dirname(self.socket_path) + os.makedirs(sock_dir, exist_ok=True) + if self.socket_path.endswith(".sock"): + pid_file = self.socket_path.replace(".sock", ".pid") + else: + pid_file = self.socket_path + ".pid" + pid = os.getpid() + with open(pid_file, "w") as f: + f.write(str(pid)) + atexit.register(functools.partial(os.remove, pid_file)) + atexit.register(functools.partial(os.remove, self.socket_path)) + sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM) + sock.bind(self.socket_path) # this creates the socket file in the fs + sock.listen(0) # no backlog + os.chmod(self.socket_path, mode=0o0770) # group members may use the socket, too. + print(f"borg serve: PID {pid}, listening on socket {self.socket_path} ...", file=sys.stderr) + + while True: + connection, client_address = sock.accept() + print(f"Accepted a connection on socket {self.socket_path} ...", file=sys.stderr) + self.stdin_fd = connection.makefile("rb").fileno() + self.stdout_fd = connection.makefile("wb").fileno() + inner_serve() + print(f"Finished with connection on socket {self.socket_path} .", file=sys.stderr) + else: # server for one ssh:// connection + self.stdin_fd = sys.stdin.fileno() + self.stdout_fd = sys.stdout.fileno() + inner_serve() + + def negotiate(self, client_data): + if isinstance(client_data, dict): + self.client_version = client_data["client_version"] + else: + self.client_version = BORG_VERSION # seems to be newer than current version (no known old format) + + # not a known old format, send newest negotiate this version knows + return {"server_version": BORG_VERSION} + + def _resolve_path(self, path): + if isinstance(path, bytes): + path = os.fsdecode(path) + if path.startswith("/~/"): # /~/x = path x relative to own home dir + home_dir = os.environ.get("HOME") or os.path.expanduser("~%s" % os.environ.get("USER", "")) + path = os.path.join(home_dir, path[3:]) + elif path.startswith("/./"): # /./x = path x relative to cwd + path = path[3:] + return os.path.realpath(path) + + def open( + self, + path, + create=False, + lock_wait=None, + lock=True, + exclusive=None, + append_only=False, + make_parent_dirs=False, + v1_or_v2=False, + ): + self.RepoCls = LegacyRepository if v1_or_v2 else Repository + self.rpc_methods = self._legacy_rpc_methods if v1_or_v2 else self._rpc_methods + logging.debug("Resolving repository path %r", path) + path = self._resolve_path(path) + logging.debug("Resolved repository path to %r", path) + path_with_sep = os.path.join(path, "") # make sure there is a trailing slash (os.sep) + if self.restrict_to_paths: + # if --restrict-to-path P is given, we make sure that we only operate in/below path P. + # for the prefix check, it is important that the compared paths both have trailing slashes, + # so that a path /foobar will NOT be accepted with --restrict-to-path /foo option. + for restrict_to_path in self.restrict_to_paths: + restrict_to_path_with_sep = os.path.join(os.path.realpath(restrict_to_path), "") # trailing slash + if path_with_sep.startswith(restrict_to_path_with_sep): + break + else: + raise PathNotAllowed(path) + if self.restrict_to_repositories: + for restrict_to_repository in self.restrict_to_repositories: + restrict_to_repository_with_sep = os.path.join(os.path.realpath(restrict_to_repository), "") + if restrict_to_repository_with_sep == path_with_sep: + break + else: + raise PathNotAllowed(path) + # "borg init" on "borg serve --append-only" (=self.append_only) does not create an append only repo, + # while "borg init --append-only" (=append_only) does, regardless of the --append-only (self.append_only) + # flag for serve. + append_only = (not create and self.append_only) or append_only + self.repository = self.RepoCls( + path, + create, + lock_wait=lock_wait, + lock=lock, + append_only=append_only, + storage_quota=self.storage_quota, + exclusive=exclusive, + make_parent_dirs=make_parent_dirs, + send_log_cb=self.send_queued_log, + ) + self.repository.__enter__() # clean exit handled by serve() method + return self.repository.id + + def close(self): + if self.repository is not None: + self.repository.__exit__(None, None, None) + self.repository = None + borg.logger.flush_logging() + self.send_queued_log() + + def inject_exception(self, kind): + s1 = "test string" + s2 = "test string2" + if kind == "DoesNotExist": + raise self.RepoCls.DoesNotExist(s1) + elif kind == "AlreadyExists": + raise self.RepoCls.AlreadyExists(s1) + elif kind == "CheckNeeded": + raise self.RepoCls.CheckNeeded(s1) + elif kind == "IntegrityError": + raise IntegrityError(s1) + elif kind == "PathNotAllowed": + raise PathNotAllowed("foo") + elif kind == "ObjectNotFound": + raise self.RepoCls.ObjectNotFound(s1, s2) + elif kind == "InvalidRPCMethod": + raise InvalidRPCMethod(s1) + elif kind == "divide": + 0 // 0 + + class SleepingBandwidthLimiter: def __init__(self, limit): if limit: @@ -250,7 +587,7 @@ def __init__( location, create=False, exclusive=False, - lock_wait=None, + lock_wait=1.0, lock=True, append_only=False, make_parent_dirs=False, @@ -339,7 +676,6 @@ def __init__( exclusive=exclusive, append_only=append_only, make_parent_dirs=make_parent_dirs, - v1_or_v2=True, # make remote use Repository, not Repository3 ) info = self.info() self.version = info["version"] @@ -354,7 +690,7 @@ def __del__(self): logging.debug("still %d cached responses left in RemoteRepository" % (len(self.responses),)) if self.p or self.sock: self.close() - assert False, "cleanup happened in Repository.__del__" + assert False, "cleanup happened in RemoteRepository.__del__" def __repr__(self): return f"<{self.__class__.__name__} {self.location.canonical_path()}>" @@ -366,11 +702,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): try: if exc_type is not None: self.shutdown_time = time.monotonic() + 30 - self.rollback() finally: - # in any case, we want to close the repo cleanly, even if the - # rollback can not succeed (e.g. because the connection was - # already closed) and raised another exception: + # in any case, we want to close the repo cleanly. logger.debug( "RemoteRepository: %s bytes sent, %s bytes received, %d messages sent", format_file_size(self.tx_bytes), @@ -505,6 +838,8 @@ def handle_error(unpacked): raise NotLocked(args[0]) elif error == "NotMyLock": raise NotMyLock(args[0]) + elif error == "NoManifestError": + raise NoManifestError else: raise self.RPCError(unpacked) @@ -745,6 +1080,22 @@ def get_manifest(self): def put_manifest(self, data): """actual remoting is done via self.call in the @api decorator""" + @api(since=parse_version("2.0.0b8")) + def store_list(self, name): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b8")) + def store_load(self, name): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b8")) + def store_store(self, name, value): + """actual remoting is done via self.call in the @api decorator""" + + @api(since=parse_version("2.0.0b8")) + def store_delete(self, name): + """actual remoting is done via self.call in the @api decorator""" + class RepositoryNoCache: """A not caching Repository wrapper, passes through to repository. diff --git a/src/borg/repository.py b/src/borg/repository.py index 60bc4fd8c..0c671ab53 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1,137 +1,23 @@ -import errno -import mmap import os -import shutil -import stat -import struct -import time -from collections import defaultdict -from configparser import ConfigParser -from datetime import datetime, timezone -from functools import partial -from itertools import islice -from typing import Callable, DefaultDict +from borgstore.store import Store +from borgstore.store import ObjectNotFound as StoreObjectNotFound + +from .checksums import xxh64 from .constants import * # NOQA -from .hashindex import NSIndexEntry, NSIndex, NSIndex1, hashindex_variant -from .helpers import Error, ErrorWithTraceback, IntegrityError, format_file_size, parse_file_size +from .helpers import Error, ErrorWithTraceback, IntegrityError from .helpers import Location -from .helpers import ProgressIndicatorPercent from .helpers import bin_to_hex, hex_to_bin -from .helpers import secure_erase, safe_unlink -from .helpers import msgpack -from .helpers.lrucache import LRUCache -from .locking import Lock, LockError, LockErrorT +from .storelocking import Lock from .logger import create_logger -from .manifest import Manifest, NoManifestError -from .platform import SaveFile, SyncFile, sync_dir, safe_fadvise +from .manifest import NoManifestError from .repoobj import RepoObj -from .checksums import crc32, StreamingXXH64 -from .crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError logger = create_logger(__name__) -MAGIC = b"BORG_SEG" -MAGIC_LEN = len(MAGIC) - -TAG_PUT = 0 -TAG_DELETE = 1 -TAG_COMMIT = 2 -TAG_PUT2 = 3 - -# Highest ID usable as TAG_* value -# -# Code may expect not to find any tags exceeding this value. In particular, -# in order to speed up `borg check --repair`, any tag greater than MAX_TAG_ID -# is assumed to be corrupted. When increasing this value, in order to add more -# tags, keep in mind that old versions of Borg accessing a new repository -# may not be able to handle the new tags. -MAX_TAG_ID = 15 - -FreeSpace: Callable[[], DefaultDict] = partial(defaultdict, int) - - -def header_size(tag): - if tag == TAG_PUT2: - size = LoggedIO.HEADER_ID_SIZE + LoggedIO.ENTRY_HASH_SIZE - elif tag == TAG_PUT or tag == TAG_DELETE: - size = LoggedIO.HEADER_ID_SIZE - elif tag == TAG_COMMIT: - size = LoggedIO.header_fmt.size - else: - raise ValueError(f"unsupported tag: {tag!r}") - return size - class Repository: - """ - Filesystem based transactional key value store - - Transactionality is achieved by using a log (aka journal) to record changes. The log is a series of numbered files - called segments. Each segment is a series of log entries. The segment number together with the offset of each - entry relative to its segment start establishes an ordering of the log entries. This is the "definition" of - time for the purposes of the log. - - Log entries are either PUT, DELETE or COMMIT. - - A COMMIT is always the final log entry in a segment and marks all data from the beginning of the log until the - segment ending with the COMMIT as committed and consistent. The segment number of a segment ending with a COMMIT - is called the transaction ID of that commit, and a segment ending with a COMMIT is called committed. - - When reading from a repository it is first checked whether the last segment is committed. If it is not, then - all segments after the last committed segment are deleted; they contain log entries whose consistency is not - established by a COMMIT. - - Note that the COMMIT can't establish consistency by itself, but only manages to do so with proper support from - the platform (including the hardware). See platform.base.SyncFile for details. - - A PUT inserts a key-value pair. The value is stored in the log entry, hence the repository implements - full data logging, meaning that all data is consistent, not just metadata (which is common in file systems). - - A DELETE marks a key as deleted. - - For a given key only the last entry regarding the key, which is called current (all other entries are called - superseded), is relevant: If there is no entry or the last entry is a DELETE then the key does not exist. - Otherwise the last PUT defines the value of the key. - - By superseding a PUT (with either another PUT or a DELETE) the log entry becomes obsolete. A segment containing - such obsolete entries is called sparse, while a segment containing no such entries is called compact. - - Sparse segments can be compacted and thereby disk space freed. This destroys the transaction for which the - superseded entries where current. - - On disk layout: - - dir/README - dir/config - dir/data// - dir/index.X - dir/hints.X - - File system interaction - ----------------------- - - LoggedIO generally tries to rely on common behaviours across transactional file systems. - - Segments that are deleted are truncated first, which avoids problems if the FS needs to - allocate space to delete the dirent of the segment. This mostly affects CoW file systems, - traditional journaling file systems have a fairly good grip on this problem. - - Note that deletion, i.e. unlink(2), is atomic on every file system that uses inode reference - counts, which includes pretty much all of them. To remove a dirent the inodes refcount has - to be decreased, but you can't decrease the refcount before removing the dirent nor can you - decrease the refcount after removing the dirent. File systems solve this with a lock, - and by ensuring it all stays within the same FS transaction. - - Truncation is generally not atomic in itself, and combining truncate(2) and unlink(2) is of - course never guaranteed to be atomic. Truncation in a classic extent-based FS is done in - roughly two phases, first the extents are removed then the inode is updated. (In practice - this is of course way more complex). - - LoggedIO gracefully handles truncate/unlink splits as long as the truncate resulted in - a zero length file. Zero length segments are considered not to exist, while LoggedIO.cleanup() - will still get rid of them. - """ + """borgstore based key value store""" class AlreadyExists(Error): """A repository already exists at {}.""" @@ -198,7 +84,7 @@ def __init__( path, create=False, exclusive=False, - lock_wait=None, + lock_wait=1.0, lock=True, append_only=False, storage_quota=None, @@ -206,44 +92,27 @@ def __init__( send_log_cb=None, ): self.path = os.path.abspath(path) - self._location = Location("file://%s" % self.path) + url = "file://%s" % self.path + # use a Store with flat config storage and 2-levels-nested data storage + self.store = Store(url, levels={"config/": [0], "data/": [2]}) + self._location = Location(url) self.version = None # long-running repository methods which emit log or progress output are responsible for calling # the ._send_log method periodically to get log and progress output transferred to the borg client # in a timely manner, in case we have a RemoteRepository. # for local repositories ._send_log can be called also (it will just do nothing in that case). self._send_log = send_log_cb or (lambda: None) - self.io = None # type: LoggedIO - self.lock = None - self.index = None - # This is an index of shadowed log entries during this transaction. Consider the following sequence: - # segment_n PUT A, segment_x DELETE A - # After the "DELETE A" in segment_x the shadow index will contain "A -> [n]". - # .delete() is updating this index, it is persisted into "hints" file and is later used by .compact_segments(). - # We need the entries in the shadow_index to not accidentally drop the "DELETE A" when we compact segment_x - # only (and we do not compact segment_n), because DELETE A is still needed then because PUT A will be still - # there. Otherwise chunk A would reappear although it was previously deleted. - self.shadow_index = {} - self._active_txn = False - self.lock_wait = lock_wait - self.do_lock = lock self.do_create = create self.created = False + self.acceptable_repo_versions = (3,) + self.opened = False + self.append_only = append_only # XXX not implemented / not implementable + self.storage_quota = storage_quota # XXX not implemented + self.storage_quota_use = 0 # XXX not implemented + self.lock = None + self.do_lock = lock + self.lock_wait = lock_wait self.exclusive = exclusive - self.append_only = append_only - self.storage_quota = storage_quota - self.storage_quota_use = 0 - self.transaction_doomed = None - self.make_parent_dirs = make_parent_dirs - # v2 is the default repo version for borg 2.0 - # v1 repos must only be used in a read-only way, e.g. for - # --other-repo=V1_REPO with borg init and borg transfer! - self.acceptable_repo_versions = (1, 2) - - def __del__(self): - if self.lock: - self.close() - assert False, "cleanup happened in Repository.__del__" def __repr__(self): return f"<{self.__class__.__name__} {self.path}>" @@ -251,977 +120,224 @@ def __repr__(self): def __enter__(self): if self.do_create: self.do_create = False - self.create(self.path) + self.create() self.created = True - self.open(self.path, bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock) + self.open(exclusive=bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock) return self def __exit__(self, exc_type, exc_val, exc_tb): - if exc_type is not None: - no_space_left_on_device = exc_type is OSError and exc_val.errno == errno.ENOSPC - # The ENOSPC could have originated somewhere else besides the Repository. The cleanup is always safe, unless - # EIO or FS corruption ensues, which is why we specifically check for ENOSPC. - if self._active_txn and no_space_left_on_device: - logger.warning("No space left on device, cleaning up partial transaction to free space.") - cleanup = True - else: - cleanup = False - self._rollback(cleanup=cleanup) self.close() @property def id_str(self): return bin_to_hex(self.id) - @staticmethod - def is_repository(path): - """Check whether there is already a Borg repository at *path*.""" - try: - # Use binary mode to avoid troubles if a README contains some stuff not in our locale - with open(os.path.join(path, "README"), "rb") as fd: - # Read only the first ~100 bytes (if any), in case some README file we stumble upon is large. - readme_head = fd.read(100) - # The first comparison captures our current variant (REPOSITORY_README), the second comparison - # is an older variant of the README file (used by 1.0.x). - return b"Borg Backup repository" in readme_head or b"Borg repository" in readme_head - except OSError: - # Ignore FileNotFound, PermissionError, ... - return False + def create(self): + """Create a new empty repository""" + self.store.create() + self.store.open() + self.store.store("config/readme", REPOSITORY_README.encode()) + self.version = 3 + self.store.store("config/version", str(self.version).encode()) + self.store.store("config/id", bin_to_hex(os.urandom(32)).encode()) + self.store.close() - def check_can_create_repository(self, path): - """ - Raise an exception if a repository already exists at *path* or any parent directory. + def _set_id(self, id): + # for testing: change the id of an existing repository + assert self.opened + assert isinstance(id, bytes) and len(id) == 32 + self.id = id + self.store.store("config/id", bin_to_hex(id).encode()) - Checking parent directories is done for two reasons: - (1) It's just a weird thing to do, and usually not intended. A Borg using the "parent" repository - may be confused, or we may accidentally put stuff into the "data/" or "data//" directories. - (2) When implementing repository quotas (which we currently don't), it's important to prohibit - folks from creating quota-free repositories. Since no one can create a repository within another - repository, user's can only use the quota'd repository, when their --restrict-to-path points - at the user's repository. - """ - try: - st = os.stat(path) - except FileNotFoundError: - pass # nothing there! - except PermissionError: - raise self.PathPermissionDenied(path) from None - else: - # there is something already there! - if self.is_repository(path): - raise self.AlreadyExists(path) - if not stat.S_ISDIR(st.st_mode): - raise self.PathAlreadyExists(path) - try: - files = os.listdir(path) - except PermissionError: - raise self.PathPermissionDenied(path) from None - else: - if files: # a dir, but not empty - raise self.PathAlreadyExists(path) - else: # an empty directory is acceptable for us. - pass - - while True: - # Check all parent directories for Borg's repository README - previous_path = path - # Thus, path = previous_path/.. - path = os.path.abspath(os.path.join(previous_path, os.pardir)) - if path == previous_path: - # We reached the root of the directory hierarchy (/.. = / and C:\.. = C:\). - break - if self.is_repository(path): - raise self.AlreadyExists(path) - - def create(self, path): - """Create a new empty repository at `path`""" - self.check_can_create_repository(path) - if self.make_parent_dirs: - parent_path = os.path.join(path, os.pardir) - os.makedirs(parent_path, exist_ok=True) - if not os.path.exists(path): - try: - os.mkdir(path) - except FileNotFoundError as err: - raise self.ParentPathDoesNotExist(path) from err - with open(os.path.join(path, "README"), "w") as fd: - fd.write(REPOSITORY_README) - os.mkdir(os.path.join(path, "data")) - config = ConfigParser(interpolation=None) - config.add_section("repository") - self.version = 2 - config.set("repository", "version", str(self.version)) - config.set("repository", "segments_per_dir", str(DEFAULT_SEGMENTS_PER_DIR)) - config.set("repository", "max_segment_size", str(DEFAULT_MAX_SEGMENT_SIZE)) - config.set("repository", "append_only", str(int(self.append_only))) - if self.storage_quota: - config.set("repository", "storage_quota", str(self.storage_quota)) - else: - config.set("repository", "storage_quota", "0") - config.set("repository", "additional_free_space", "0") - config.set("repository", "id", bin_to_hex(os.urandom(32))) - self.save_config(path, config) - - def save_config(self, path, config): - config_path = os.path.join(path, "config") - old_config_path = os.path.join(path, "config.old") - - if os.path.isfile(old_config_path): - logger.warning("Old config file not securely erased on previous config update") - secure_erase(old_config_path, avoid_collateral_damage=True) - - if os.path.isfile(config_path): - link_error_msg = ( - "Failed to erase old repository config file securely (hardlinks not supported). " - "Old repokey data, if any, might persist on physical storage." - ) - try: - os.link(config_path, old_config_path) - except OSError as e: - if e.errno in (errno.EMLINK, errno.ENOSYS, errno.EPERM, errno.EACCES, errno.ENOTSUP, errno.EIO): - logger.warning(link_error_msg) - else: - raise - except AttributeError: - # some python ports have no os.link, see #4901 - logger.warning(link_error_msg) - - try: - with SaveFile(config_path) as fd: - config.write(fd) - except PermissionError as e: - # error is only a problem if we even had a lock - if self.do_lock: - raise - logger.warning( - "%s: Failed writing to '%s'. This is expected when working on " - "read-only repositories." % (e.strerror, e.filename) - ) - - if os.path.isfile(old_config_path): - secure_erase(old_config_path, avoid_collateral_damage=True) + def _lock_refresh(self): + if self.lock is not None: + self.lock.refresh() def save_key(self, keydata): - assert self.config - keydata = keydata.decode("utf-8") # remote repo: msgpack issue #99, getting bytes - # note: saving an empty key means that there is no repokey any more - self.config.set("repository", "key", keydata) - self.save_config(self.path, self.config) + # note: saving an empty key means that there is no repokey anymore + self.store.store("keys/repokey", keydata) def load_key(self): - keydata = self.config.get("repository", "key", fallback="").strip() + keydata = self.store.load("keys/repokey") # note: if we return an empty string, it means there is no repo key - return keydata.encode("utf-8") # remote repo: msgpack issue #99, returning bytes + return keydata def destroy(self): - """Destroy the repository at `self.path`""" - if self.append_only: - raise ValueError(self.path + " is in append-only mode") + """Destroy the repository""" self.close() - os.remove(os.path.join(self.path, "config")) # kill config first - shutil.rmtree(self.path) + self.store.destroy() - def get_index_transaction_id(self): - indices = sorted( - int(fn[6:]) - for fn in os.listdir(self.path) - if fn.startswith("index.") and fn[6:].isdigit() and os.stat(os.path.join(self.path, fn)).st_size != 0 - ) - if indices: - return indices[-1] - else: - return None - - def check_transaction(self): - index_transaction_id = self.get_index_transaction_id() - segments_transaction_id = self.io.get_segments_transaction_id() - if index_transaction_id is not None and segments_transaction_id is None: - # we have a transaction id from the index, but we did not find *any* - # commit in the segment files (thus no segments transaction id). - # this can happen if a lot of segment files are lost, e.g. due to a - # filesystem or hardware malfunction. it means we have no identifiable - # valid (committed) state of the repo which we could use. - msg = '%s" - although likely this is "beyond repair' % self.path # dirty hack - raise self.CheckNeeded(msg) - # Attempt to rebuild index automatically if we crashed between commit - # tag write and index save. - if index_transaction_id != segments_transaction_id: - if index_transaction_id is not None and index_transaction_id > segments_transaction_id: - replay_from = None - else: - replay_from = index_transaction_id - self.replay_segments(replay_from, segments_transaction_id) - - def get_transaction_id(self): - self.check_transaction() - return self.get_index_transaction_id() - - def break_lock(self): - Lock(os.path.join(self.path, "lock")).break_lock() - - def migrate_lock(self, old_id, new_id): - # note: only needed for local repos - if self.lock is not None: - self.lock.migrate_lock(old_id, new_id) - - def open(self, path, exclusive, lock_wait=None, lock=True): - self.path = path - try: - st = os.stat(path) - except FileNotFoundError: - raise self.DoesNotExist(path) - if not stat.S_ISDIR(st.st_mode): - raise self.InvalidRepository(path) + def open(self, *, exclusive, lock_wait=None, lock=True): + assert lock_wait is not None + self.store.open() if lock: - self.lock = Lock(os.path.join(path, "lock"), exclusive, timeout=lock_wait).acquire() + self.lock = Lock(self.store, exclusive, timeout=lock_wait).acquire() else: self.lock = None - self.config = ConfigParser(interpolation=None) - try: - with open(os.path.join(self.path, "config")) as fd: - self.config.read_file(fd) - except FileNotFoundError: - self.close() + readme = self.store.load("config/readme").decode() + if readme != REPOSITORY_README: raise self.InvalidRepository(self.path) - if "repository" not in self.config.sections(): - self.close() - raise self.InvalidRepositoryConfig(path, "no repository section found") - self.version = self.config.getint("repository", "version") + self.version = int(self.store.load("config/version").decode()) if self.version not in self.acceptable_repo_versions: self.close() raise self.InvalidRepositoryConfig( - path, "repository version %d is not supported by this borg version" % self.version + self.path, "repository version %d is not supported by this borg version" % self.version ) - self.max_segment_size = parse_file_size(self.config.get("repository", "max_segment_size")) - if self.max_segment_size >= MAX_SEGMENT_SIZE_LIMIT: - self.close() - raise self.InvalidRepositoryConfig(path, "max_segment_size >= %d" % MAX_SEGMENT_SIZE_LIMIT) # issue 3592 - self.segments_per_dir = self.config.getint("repository", "segments_per_dir") - self.additional_free_space = parse_file_size(self.config.get("repository", "additional_free_space", fallback=0)) - # append_only can be set in the constructor - # it shouldn't be overridden (True -> False) here - self.append_only = self.append_only or self.config.getboolean("repository", "append_only", fallback=False) - if self.storage_quota is None: - # self.storage_quota is None => no explicit storage_quota was specified, use repository setting. - self.storage_quota = parse_file_size(self.config.get("repository", "storage_quota", fallback=0)) - self.id = hex_to_bin(self.config.get("repository", "id").strip(), length=32) - self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir) + self.id = hex_to_bin(self.store.load("config/id").decode(), length=32) + self.opened = True - def _load_hints(self): - if (transaction_id := self.get_transaction_id()) is None: - # self is a fresh repo, so transaction_id is None and there is no hints file - return - hints = self._unpack_hints(transaction_id) - self.version = hints["version"] - self.storage_quota_use = hints["storage_quota_use"] - self.shadow_index = hints["shadow_index"] + def close(self): + if self.opened: + if self.lock: + self.lock.release() + self.lock = None + self.store.close() + self.opened = False def info(self): """return some infos about the repo (must be opened first)""" - info = dict(id=self.id, version=self.version, append_only=self.append_only) - self._load_hints() - info["storage_quota"] = self.storage_quota - info["storage_quota_use"] = self.storage_quota_use + info = dict( + id=self.id, + version=self.version, + storage_quota_use=self.storage_quota_use, + storage_quota=self.storage_quota, + append_only=self.append_only, + ) return info - def close(self): - if self.lock: - if self.io: - self.io.close() - self.io = None - self.lock.release() - self.lock = None - - def commit(self, compact=True, threshold=0.1): - """Commit transaction""" - if self.transaction_doomed: - exception = self.transaction_doomed - self.rollback() - raise exception - self.check_free_space() - segment = self.io.write_commit() - self.segments.setdefault(segment, 0) - self.compact[segment] += LoggedIO.header_fmt.size - if compact and not self.append_only: - self.compact_segments(threshold) - self.write_index() - self.rollback() - - def _read_integrity(self, transaction_id, key): - integrity_file = "integrity.%d" % transaction_id - integrity_path = os.path.join(self.path, integrity_file) - try: - with open(integrity_path, "rb") as fd: - integrity = msgpack.unpack(fd) - except FileNotFoundError: - return - if integrity.get("version") != 2: - logger.warning("Unknown integrity data version %r in %s", integrity.get("version"), integrity_file) - return - return integrity[key] - - def open_index(self, transaction_id, auto_recover=True): - if transaction_id is None: - return NSIndex() - index_path = os.path.join(self.path, "index.%d" % transaction_id) - variant = hashindex_variant(index_path) - integrity_data = self._read_integrity(transaction_id, "index") - try: - with IntegrityCheckedFile(index_path, write=False, integrity_data=integrity_data) as fd: - if variant == 2: - return NSIndex.read(fd) - if variant == 1: # legacy - return NSIndex1.read(fd) - except (ValueError, OSError, FileIntegrityError) as exc: - logger.warning("Repository index missing or corrupted, trying to recover from: %s", exc) - os.unlink(index_path) - if not auto_recover: - raise - self.prepare_txn(self.get_transaction_id()) - # don't leave an open transaction around - self.commit(compact=False) - return self.open_index(self.get_transaction_id()) - - def _unpack_hints(self, transaction_id): - hints_path = os.path.join(self.path, "hints.%d" % transaction_id) - integrity_data = self._read_integrity(transaction_id, "hints") - with IntegrityCheckedFile(hints_path, write=False, integrity_data=integrity_data) as fd: - return msgpack.unpack(fd) - - def prepare_txn(self, transaction_id, do_cleanup=True): - self._active_txn = True - if self.do_lock and not self.lock.got_exclusive_lock(): - if self.exclusive is not None: - # self.exclusive is either True or False, thus a new client is active here. - # if it is False and we get here, the caller did not use exclusive=True although - # it is needed for a write operation. if it is True and we get here, something else - # went very wrong, because we should have an exclusive lock, but we don't. - raise AssertionError("bug in code, exclusive lock should exist here") - # if we are here, this is an old client talking to a new server (expecting lock upgrade). - # or we are replaying segments and might need a lock upgrade for that. - try: - self.lock.upgrade() - except (LockError, LockErrorT): - # if upgrading the lock to exclusive fails, we do not have an - # active transaction. this is important for "serve" mode, where - # the repository instance lives on - even if exceptions happened. - self._active_txn = False - raise - if not self.index or transaction_id is None: - try: - self.index = self.open_index(transaction_id, auto_recover=False) - except (ValueError, OSError, FileIntegrityError) as exc: - logger.warning("Checking repository transaction due to previous error: %s", exc) - self.check_transaction() - self.index = self.open_index(transaction_id, auto_recover=False) - if transaction_id is None: - self.segments = {} # XXX bad name: usage_count_of_segment_x = self.segments[x] - self.compact = FreeSpace() # XXX bad name: freeable_space_of_segment_x = self.compact[x] - self.storage_quota_use = 0 - self.shadow_index.clear() - else: - if do_cleanup: - self.io.cleanup(transaction_id) - hints_path = os.path.join(self.path, "hints.%d" % transaction_id) - index_path = os.path.join(self.path, "index.%d" % transaction_id) - try: - hints = self._unpack_hints(transaction_id) - except (msgpack.UnpackException, FileNotFoundError, FileIntegrityError) as e: - logger.warning("Repository hints file missing or corrupted, trying to recover: %s", e) - if not isinstance(e, FileNotFoundError): - os.unlink(hints_path) - # index must exist at this point - os.unlink(index_path) - self.check_transaction() - self.prepare_txn(transaction_id) - return - if hints["version"] == 1: - logger.debug("Upgrading from v1 hints.%d", transaction_id) - self.segments = hints["segments"] - self.compact = FreeSpace() - self.storage_quota_use = 0 - self.shadow_index = {} - for segment in sorted(hints["compact"]): - logger.debug("Rebuilding sparse info for segment %d", segment) - self._rebuild_sparse(segment) - logger.debug("Upgrade to v2 hints complete") - elif hints["version"] != 2: - raise ValueError("Unknown hints file version: %d" % hints["version"]) - else: - self.segments = hints["segments"] - self.compact = FreeSpace(hints["compact"]) - self.storage_quota_use = hints.get("storage_quota_use", 0) - self.shadow_index = hints.get("shadow_index", {}) - # Drop uncommitted segments in the shadow index - for key, shadowed_segments in self.shadow_index.items(): - for segment in list(shadowed_segments): - if segment > transaction_id: - shadowed_segments.remove(segment) - - def write_index(self): - def flush_and_sync(fd): - fd.flush() - os.fsync(fd.fileno()) - - def rename_tmp(file): - os.replace(file + ".tmp", file) - - hints = { - "version": 2, - "segments": self.segments, - "compact": self.compact, - "storage_quota_use": self.storage_quota_use, - "shadow_index": self.shadow_index, - } - integrity = { - # Integrity version started at 2, the current hints version. - # Thus, integrity version == hints version, for now. - "version": 2 - } - transaction_id = self.io.get_segments_transaction_id() - assert transaction_id is not None - - # Log transaction in append-only mode - if self.append_only: - with open(os.path.join(self.path, "transactions"), "a") as log: - print( - "transaction %d, UTC time %s" - % (transaction_id, datetime.now(tz=timezone.utc).isoformat(timespec="microseconds")), - file=log, - ) - - # Write hints file - hints_name = "hints.%d" % transaction_id - hints_file = os.path.join(self.path, hints_name) - with IntegrityCheckedFile(hints_file + ".tmp", filename=hints_name, write=True) as fd: - msgpack.pack(hints, fd) - flush_and_sync(fd) - integrity["hints"] = fd.integrity_data - - # Write repository index - index_name = "index.%d" % transaction_id - index_file = os.path.join(self.path, index_name) - with IntegrityCheckedFile(index_file + ".tmp", filename=index_name, write=True) as fd: - # XXX: Consider using SyncFile for index write-outs. - self.index.write(fd) - flush_and_sync(fd) - integrity["index"] = fd.integrity_data - - # Write integrity file, containing checksums of the hints and index files - integrity_name = "integrity.%d" % transaction_id - integrity_file = os.path.join(self.path, integrity_name) - with open(integrity_file + ".tmp", "wb") as fd: - msgpack.pack(integrity, fd) - flush_and_sync(fd) - - # Rename the integrity file first - rename_tmp(integrity_file) - sync_dir(self.path) - # Rename the others after the integrity file is hypothetically on disk - rename_tmp(hints_file) - rename_tmp(index_file) - sync_dir(self.path) - - # Remove old auxiliary files - current = ".%d" % transaction_id - for name in os.listdir(self.path): - if not name.startswith(("index.", "hints.", "integrity.")): - continue - if name.endswith(current): - continue - os.unlink(os.path.join(self.path, name)) - self.index = None - - def check_free_space(self): - """Pre-commit check for sufficient free space necessary to perform the commit.""" - # As a baseline we take four times the current (on-disk) index size. - # At this point the index may only be updated by compaction, which won't resize it. - # We still apply a factor of four so that a later, separate invocation can free space - # (journaling all deletes for all chunks is one index size) or still make minor additions - # (which may grow the index up to twice its current size). - # Note that in a subsequent operation the committed index is still on-disk, therefore we - # arrive at index_size * (1 + 2 + 1). - # In that order: journaled deletes (1), hashtable growth (2), persisted index (1). - required_free_space = self.index.size() * 4 - - # Conservatively estimate hints file size: - # 10 bytes for each segment-refcount pair, 10 bytes for each segment-space pair - # Assume maximum of 5 bytes per integer. Segment numbers will usually be packed more densely (1-3 bytes), - # as will refcounts and free space integers. For 5 MiB segments this estimate is good to ~20 PB repo size. - # Add a generous 4K to account for constant format overhead. - hints_size = len(self.segments) * 10 + len(self.compact) * 10 + 4096 - required_free_space += hints_size - - required_free_space += self.additional_free_space - if not self.append_only: - full_segment_size = self.max_segment_size + MAX_OBJECT_SIZE - if len(self.compact) < 10: - # This is mostly for the test suite to avoid overestimated free space needs. This can be annoying - # if TMP is a small-ish tmpfs. - compact_working_space = 0 - for segment, free in self.compact.items(): - try: - compact_working_space += self.io.segment_size(segment) - free - except FileNotFoundError: - # looks like self.compact is referring to a nonexistent segment file, ignore it. - pass - logger.debug("check_free_space: Few segments, not requiring a full free segment") - compact_working_space = min(compact_working_space, full_segment_size) - logger.debug( - "check_free_space: Calculated working space for compact as %d bytes", compact_working_space - ) - required_free_space += compact_working_space - else: - # Keep one full worst-case segment free in non-append-only mode - required_free_space += full_segment_size - - try: - free_space = shutil.disk_usage(self.path).free - except OSError as os_error: - logger.warning("Failed to check free space before committing: " + str(os_error)) - return - logger.debug(f"check_free_space: Required bytes {required_free_space}, free bytes {free_space}") - if free_space < required_free_space: - if self.created: - logger.error("Not enough free space to initialize repository at this location.") - self.destroy() - else: - self._rollback(cleanup=True) - formatted_required = format_file_size(required_free_space) - formatted_free = format_file_size(free_space) - raise self.InsufficientFreeSpaceError(formatted_required, formatted_free) - - def compact_segments(self, threshold): - """Compact sparse segments by copying data into new segments""" - if not self.compact: - logger.debug("Nothing to do: compact empty") - return - quota_use_before = self.storage_quota_use - index_transaction_id = self.get_index_transaction_id() - segments = self.segments - unused = [] # list of segments, that are not used anymore - - def complete_xfer(intermediate=True): - # complete the current transfer (when some target segment is full) - nonlocal unused - # commit the new, compact, used segments - segment = self.io.write_commit(intermediate=intermediate) - self.segments.setdefault(segment, 0) - self.compact[segment] += LoggedIO.header_fmt.size - logger.debug( - "complete_xfer: Wrote %scommit at segment %d", "intermediate " if intermediate else "", segment - ) - # get rid of the old, sparse, unused segments. free space. - for segment in unused: - logger.debug("complete_xfer: Deleting unused segment %d", segment) - count = self.segments.pop(segment) - assert count == 0, "Corrupted segment reference count - corrupted index or hints" - self.io.delete_segment(segment) - del self.compact[segment] - unused = [] - - logger.debug("Compaction started (threshold is %i%%).", threshold * 100) - pi = ProgressIndicatorPercent( - total=len(self.compact), msg="Compacting segments %3.0f%%", step=1, msgid="repository.compact_segments" - ) - for segment, freeable_space in sorted(self.compact.items()): - if not self.io.segment_exists(segment): - logger.warning("Segment %d not found, but listed in compaction data", segment) - del self.compact[segment] - pi.show() - self._send_log() - continue - segment_size = self.io.segment_size(segment) - freeable_ratio = 1.0 * freeable_space / segment_size - # we want to compact if: - # - we can free a considerable relative amount of space (freeable_ratio over some threshold) - if not (freeable_ratio > threshold): - logger.debug( - "Not compacting segment %d (maybe freeable: %2.2f%% [%d bytes])", - segment, - freeable_ratio * 100.0, - freeable_space, - ) - pi.show() - self._send_log() - continue - segments.setdefault(segment, 0) - logger.debug( - "Compacting segment %d with usage count %d (maybe freeable: %2.2f%% [%d bytes])", - segment, - segments[segment], - freeable_ratio * 100.0, - freeable_space, - ) - for tag, key, offset, _, data in self.io.iter_objects(segment): - if tag == TAG_COMMIT: - continue - in_index = self.index.get(key) - is_index_object = in_index and (in_index.segment, in_index.offset) == (segment, offset) - if tag in (TAG_PUT2, TAG_PUT) and is_index_object: - try: - new_segment, offset = self.io.write_put(key, data, raise_full=True) - except LoggedIO.SegmentFull: - complete_xfer() - new_segment, offset = self.io.write_put(key, data) - self.index[key] = NSIndexEntry(new_segment, offset, len(data)) - segments.setdefault(new_segment, 0) - segments[new_segment] += 1 - segments[segment] -= 1 - if tag == TAG_PUT: - # old tag is PUT, but new will be PUT2 and use a bit more storage - self.storage_quota_use += self.io.ENTRY_HASH_SIZE - elif tag in (TAG_PUT2, TAG_PUT) and not is_index_object: - # If this is a PUT shadowed by a later tag, then it will be gone when this segment is deleted after - # this loop. Therefore it is removed from the shadow index. - try: - self.shadow_index[key].remove(segment) - except (KeyError, ValueError): - # do not remove entry with empty shadowed_segments list here, - # it is needed for shadowed_put_exists code (see below)! - pass - self.storage_quota_use -= header_size(tag) + len(data) - elif tag == TAG_DELETE and not in_index: - # If the shadow index doesn't contain this key, then we can't say if there's a shadowed older tag, - # therefore we do not drop the delete, but write it to a current segment. - key_not_in_shadow_index = key not in self.shadow_index - # If the key is in the shadow index and there is any segment with an older PUT of this - # key, we have a shadowed put. - shadowed_put_exists = key_not_in_shadow_index or any( - shadowed < segment for shadowed in self.shadow_index[key] - ) - delete_is_not_stable = index_transaction_id is None or segment > index_transaction_id - - if shadowed_put_exists or delete_is_not_stable: - # (introduced in 6425d16aa84be1eaaf88) - # This is needed to avoid object un-deletion if we crash between the commit and the deletion - # of old segments in complete_xfer(). - # - # However, this only happens if the crash also affects the FS to the effect that file deletions - # did not materialize consistently after journal recovery. If they always materialize in-order - # then this is not a problem, because the old segment containing a deleted object would be - # deleted before the segment containing the delete. - # - # Consider the following series of operations if we would not do this, i.e. this entire if: - # would be removed. - # Columns are segments, lines are different keys (line 1 = some key, line 2 = some other key) - # Legend: P=TAG_PUT/TAG_PUT2, D=TAG_DELETE, c=commit, i=index is written for latest commit - # - # Segment | 1 | 2 | 3 - # --------+-------+-----+------ - # Key 1 | P | D | - # Key 2 | P | | P - # commits | c i | c | c i - # --------+-------+-----+------ - # ^- compact_segments starts - # ^- complete_xfer commits, after that complete_xfer deletes - # segments 1 and 2 (and then the index would be written). - # - # Now we crash. But only segment 2 gets deleted, while segment 1 is still around. Now key 1 - # is suddenly undeleted (because the delete in segment 2 is now missing). - # Again, note the requirement here. We delete these in the correct order that this doesn't - # happen, and only if the FS materialization of these deletes is reordered or parts dropped - # this can happen. - # In this case it doesn't cause outright corruption, 'just' an index count mismatch, which - # will be fixed by borg-check --repair. - # - # Note that in this check the index state is the proxy for a "most definitely settled" - # repository state, i.e. the assumption is that *all* operations on segments <= index state - # are completed and stable. - try: - new_segment, size = self.io.write_delete(key, raise_full=True) - except LoggedIO.SegmentFull: - complete_xfer() - new_segment, size = self.io.write_delete(key) - self.compact[new_segment] += size - segments.setdefault(new_segment, 0) - else: - logger.debug( - "Dropping DEL for id %s - seg %d, iti %r, knisi %r, spe %r, dins %r, si %r", - bin_to_hex(key), - segment, - index_transaction_id, - key_not_in_shadow_index, - shadowed_put_exists, - delete_is_not_stable, - self.shadow_index.get(key), - ) - # we did not keep the delete tag for key (see if-branch) - if not self.shadow_index[key]: - # shadowed segments list is empty -> remove it - del self.shadow_index[key] - assert segments[segment] == 0, "Corrupted segment reference count - corrupted index or hints" - unused.append(segment) - pi.show() - self._send_log() - pi.finish() - self._send_log() - complete_xfer(intermediate=False) - self.io.clear_empty_dirs() - quota_use_after = self.storage_quota_use - logger.info("Compaction freed about %s repository space.", format_file_size(quota_use_before - quota_use_after)) - logger.debug("Compaction completed.") - - def replay_segments(self, index_transaction_id, segments_transaction_id): - # fake an old client, so that in case we do not have an exclusive lock yet, prepare_txn will upgrade the lock: - remember_exclusive = self.exclusive - self.exclusive = None - self.prepare_txn(index_transaction_id, do_cleanup=False) - try: - segment_count = sum(1 for _ in self.io.segment_iterator()) - pi = ProgressIndicatorPercent( - total=segment_count, msg="Replaying segments %3.0f%%", msgid="repository.replay_segments" - ) - for i, (segment, filename) in enumerate(self.io.segment_iterator()): - pi.show(i) - self._send_log() - if index_transaction_id is not None and segment <= index_transaction_id: - continue - if segment > segments_transaction_id: - break - objects = self.io.iter_objects(segment) - self._update_index(segment, objects) - pi.finish() - self._send_log() - self.write_index() - finally: - self.exclusive = remember_exclusive - self.rollback() - - def _update_index(self, segment, objects, report=None): - """some code shared between replay_segments and check""" - self.segments[segment] = 0 - for tag, key, offset, size, _ in objects: - if tag in (TAG_PUT2, TAG_PUT): - try: - # If this PUT supersedes an older PUT, mark the old segment for compaction and count the free space - in_index = self.index[key] - self.compact[in_index.segment] += header_size(tag) + size - self.segments[in_index.segment] -= 1 - self.shadow_index.setdefault(key, []).append(in_index.segment) - except KeyError: - pass - self.index[key] = NSIndexEntry(segment, offset, size) - self.segments[segment] += 1 - self.storage_quota_use += header_size(tag) + size - elif tag == TAG_DELETE: - try: - # if the deleted PUT is not in the index, there is nothing to clean up - in_index = self.index.pop(key) - except KeyError: - pass - else: - if self.io.segment_exists(in_index.segment): - # the old index is not necessarily valid for this transaction (e.g. compaction); if the segment - # is already gone, then it was already compacted. - self.segments[in_index.segment] -= 1 - self.compact[in_index.segment] += header_size(tag) + in_index.size - self.shadow_index.setdefault(key, []).append(in_index.segment) - elif tag == TAG_COMMIT: - continue - else: - msg = f"Unexpected tag {tag} in segment {segment}" - if report is None: - raise self.CheckNeeded(msg) - else: - report(msg) - if self.segments[segment] == 0: - self.compact[segment] = self.io.segment_size(segment) - - def _rebuild_sparse(self, segment): - """Rebuild sparse bytes count for a single segment relative to the current index.""" - try: - segment_size = self.io.segment_size(segment) - except FileNotFoundError: - # segment does not exist any more, remove it from the mappings. - # note: no need to self.compact.pop(segment), as we start from empty mapping. - self.segments.pop(segment) - return - - if self.segments[segment] == 0: - self.compact[segment] = segment_size - return - - self.compact[segment] = 0 - for tag, key, offset, size, _ in self.io.iter_objects(segment, read_data=False): - if tag in (TAG_PUT2, TAG_PUT): - in_index = self.index.get(key) - if not in_index or (in_index.segment, in_index.offset) != (segment, offset): - # This PUT is superseded later. - self.compact[segment] += header_size(tag) + size - elif tag == TAG_DELETE: - # The outcome of the DELETE has been recorded in the PUT branch already. - self.compact[segment] += header_size(tag) + size - def check(self, repair=False, max_duration=0): - """Check repository consistency + """Check repository consistency""" - This method verifies all segment checksums and makes sure - the index is consistent with the data stored in the segments. - """ - if self.append_only and repair: - raise ValueError(self.path + " is in append-only mode") - error_found = False + def log_error(msg): + nonlocal obj_corrupted + obj_corrupted = True + logger.error(f"Repo object {info.name} is corrupted: {msg}") - def report_error(msg, *args): - nonlocal error_found - error_found = True - logger.error(msg, *args) - - logger.info("Starting repository check") - assert not self._active_txn - try: - transaction_id = self.get_transaction_id() - current_index = self.open_index(transaction_id) - logger.debug("Read committed index of transaction %d", transaction_id) - except Exception as exc: - transaction_id = self.io.get_segments_transaction_id() - current_index = None - logger.debug("Failed to read committed index (%s)", exc) - if transaction_id is None: - logger.debug("No segments transaction found") - transaction_id = self.get_index_transaction_id() - if transaction_id is None: - logger.debug("No index transaction found, trying latest segment") - transaction_id = self.io.get_latest_segment() - if transaction_id is None: - report_error("This repository contains no valid data.") - return False - if repair: - self.io.cleanup(transaction_id) - segments_transaction_id = self.io.get_segments_transaction_id() - logger.debug("Segment transaction is %s", segments_transaction_id) - logger.debug("Determined transaction is %s", transaction_id) - self.prepare_txn(None) # self.index, self.compact, self.segments, self.shadow_index all empty now! - segment_count = sum(1 for _ in self.io.segment_iterator()) - logger.debug("Found %d segments", segment_count) - - partial = bool(max_duration) - assert not (repair and partial) - mode = "partial" if partial else "full" - if partial: - # continue a past partial check (if any) or start one from beginning - last_segment_checked = self.config.getint("repository", "last_segment_checked", fallback=-1) - logger.info("Skipping to segments >= %d", last_segment_checked + 1) - else: - # start from the beginning and also forget about any potential past partial checks - last_segment_checked = -1 - self.config.remove_option("repository", "last_segment_checked") - self.save_config(self.path, self.config) - t_start = time.monotonic() - pi = ProgressIndicatorPercent( - total=segment_count, msg="Checking segments %3.1f%%", step=0.1, msgid="repository.check" - ) - segment = -1 # avoid uninitialized variable if there are no segment files at all - for i, (segment, filename) in enumerate(self.io.segment_iterator()): - pi.show(i) - self._send_log() - if segment <= last_segment_checked: - continue - if segment > transaction_id: - continue - logger.debug("Checking segment file %s...", filename) - try: - objects = list(self.io.iter_objects(segment)) - except IntegrityError as err: - report_error(str(err)) - objects = [] - if repair: - self.io.recover_segment(segment, filename) - objects = list(self.io.iter_objects(segment)) - if not partial: - self._update_index(segment, objects, report_error) - if partial and time.monotonic() > t_start + max_duration: - logger.info("Finished partial segment check, last segment checked is %d", segment) - self.config.set("repository", "last_segment_checked", str(segment)) - self.save_config(self.path, self.config) - break - else: - logger.info("Finished segment check at segment %d", segment) - self.config.remove_option("repository", "last_segment_checked") - self.save_config(self.path, self.config) - - pi.finish() - self._send_log() - # self.index, self.segments, self.compact now reflect the state of the segment files up to . - # We might need to add a commit tag if no committed segment is found. - if repair and segments_transaction_id is None: - report_error(f"Adding commit tag to segment {transaction_id}") - self.io.segment = transaction_id + 1 - self.io.write_commit() - if not partial: - logger.info("Starting repository index check") - if current_index and not repair: - # current_index = "as found on disk" - # self.index = "as rebuilt in-memory from segments" - if len(current_index) != len(self.index): - report_error("Index object count mismatch.") - report_error("committed index: %d objects", len(current_index)) - report_error("rebuilt index: %d objects", len(self.index)) - else: - logger.info("Index object count match.") - line_format = "ID: %-64s rebuilt index: %-16s committed index: %-16s" - not_found = "" - for key, value in self.index.iteritems(): - current_value = current_index.get(key, not_found) - if current_value != value: - report_error(line_format, bin_to_hex(key), value, current_value) - self._send_log() - for key, current_value in current_index.iteritems(): - if key in self.index: - continue - value = self.index.get(key, not_found) - if current_value != value: - report_error(line_format, bin_to_hex(key), value, current_value) - self._send_log() - if repair: - self.write_index() - self.rollback() - if error_found: - if repair: - logger.info("Finished %s repository check, errors found and repaired.", mode) + def check_object(obj): + """Check if obj looks valid.""" + hdr_size = RepoObj.obj_header.size + obj_size = len(obj) + if obj_size >= hdr_size: + hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size])) + meta = obj[hdr_size : hdr_size + hdr.meta_size] + if hdr.meta_size != len(meta): + log_error("metadata size incorrect.") + elif hdr.meta_hash != xxh64(meta): + log_error("metadata does not match checksum.") + data = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size] + if hdr.data_size != len(data): + log_error("data size incorrect.") + elif hdr.data_hash != xxh64(data): + log_error("data does not match checksum.") else: - logger.error("Finished %s repository check, errors found.", mode) + log_error("too small.") + + # TODO: progress indicator, partial checks, ... + mode = "full" + logger.info("Starting repository check") + objs_checked = objs_errors = 0 + infos = self.store.list("data") + try: + for info in infos: + self._lock_refresh() + key = "data/%s" % info.name + try: + obj = self.store.load(key) + except StoreObjectNotFound: + # looks like object vanished since store.list(), ignore that. + continue + obj_corrupted = False + check_object(obj) + objs_checked += 1 + if obj_corrupted: + objs_errors += 1 + if repair: + # if it is corrupted, we can't do much except getting rid of it. + # but let's just retry loading it, in case the error goes away. + try: + obj = self.store.load(key) + except StoreObjectNotFound: + log_error("existing object vanished.") + else: + obj_corrupted = False + check_object(obj) + if obj_corrupted: + log_error("reloading did not help, deleting it!") + self.store.delete(key) + else: + log_error("reloading did help, inconsistent behaviour detected!") + except StoreObjectNotFound: + # it can be that there is no "data/" at all, then it crashes when iterating infos. + pass + logger.info(f"Checked {objs_checked} repository objects, {objs_errors} errors.") + if objs_errors == 0: + logger.info(f"Finished {mode} repository check, no problems found.") else: - logger.info("Finished %s repository check, no problems found.", mode) - return not error_found or repair - - def _rollback(self, *, cleanup): - if cleanup: - self.io.cleanup(self.io.get_segments_transaction_id()) - self.index = None - self._active_txn = False - self.transaction_doomed = None - - def rollback(self): - # note: when used in remote mode, this is time limited, see RemoteRepository.shutdown_time. - self._rollback(cleanup=False) + if repair: + logger.info(f"Finished {mode} repository check, errors found and repaired.") + else: + logger.error(f"Finished {mode} repository check, errors found.") + return objs_errors == 0 or repair def __len__(self): - if not self.index: - self.index = self.open_index(self.get_transaction_id()) - return len(self.index) + raise NotImplementedError def __contains__(self, id): - if not self.index: - self.index = self.open_index(self.get_transaction_id()) - return id in self.index + raise NotImplementedError def list(self, limit=None, marker=None): """ - list IDs starting from after id - in index (pseudo-random) order. + list IDs starting from after id . """ - if not self.index: - self.index = self.open_index(self.get_transaction_id()) - return [id_ for id_, _ in islice(self.index.iteritems(marker=marker), limit)] + self._lock_refresh() + collect = True if marker is None else False + ids = [] + infos = self.store.list("data") # generator yielding ItemInfos + while True: + try: + info = next(infos) + except StoreObjectNotFound: + break # can happen e.g. if "data" does not exist, pointless to continue in that case + except StopIteration: + break + else: + id = hex_to_bin(info.name) + if collect: + ids.append(id) + if len(ids) == limit: + break + elif id == marker: + collect = True + # note: do not collect the marker id + return ids def get(self, id, read_data=True): - if not self.index: - self.index = self.open_index(self.get_transaction_id()) + self._lock_refresh() + id_hex = bin_to_hex(id) + key = "data/" + id_hex try: - in_index = NSIndexEntry(*((self.index[id] + (None,))[:3])) # legacy: index entries have no size element - return self.io.read(in_index.segment, in_index.offset, id, expected_size=in_index.size, read_data=read_data) - except KeyError: + if read_data: + # read everything + return self.store.load(key) + else: + # RepoObj layout supports separately encrypted metadata and data. + # We return enough bytes so the client can decrypt the metadata. + hdr_size = RepoObj.obj_header.size + extra_size = 1024 - hdr_size # load a bit more, 1024b, reduces round trips + obj = self.store.load(key, size=hdr_size + extra_size) + hdr = obj[0:hdr_size] + if len(hdr) != hdr_size: + raise IntegrityError(f"Object too small [id {id_hex}]: expected {hdr_size}, got {len(hdr)} bytes") + meta_size = RepoObj.obj_header.unpack(hdr)[0] + if meta_size > extra_size: + # we did not get enough, need to load more, but not all. + # this should be rare, as chunk metadata is rather small usually. + obj = self.store.load(key, size=hdr_size + meta_size) + meta = obj[hdr_size : hdr_size + meta_size] + if len(meta) != meta_size: + raise IntegrityError(f"Object too small [id {id_hex}]: expected {meta_size}, got {len(meta)} bytes") + return hdr + meta + except StoreObjectNotFound: raise self.ObjectNotFound(id, self.path) from None def get_many(self, ids, read_data=True, is_preloaded=False): @@ -1234,28 +350,13 @@ def put(self, id, data, wait=True): Note: when doing calls with wait=False this gets async and caller must deal with async results / exceptions later. """ - if not self._active_txn: - self.prepare_txn(self.get_transaction_id()) - try: - in_index = self.index[id] - except KeyError: - pass - else: - # this put call supersedes a previous put to same id. - # it is essential to do a delete first to get correct quota bookkeeping - # and also a correctly updated shadow_index, so that the compaction code - # does not wrongly resurrect an old PUT by dropping a DEL that is still needed. - self._delete(id, in_index.segment, in_index.offset, in_index.size) - segment, offset = self.io.write_put(id, data) - self.storage_quota_use += header_size(TAG_PUT2) + len(data) - self.segments.setdefault(segment, 0) - self.segments[segment] += 1 - self.index[id] = NSIndexEntry(segment, offset, len(data)) - if self.storage_quota and self.storage_quota_use > self.storage_quota: - self.transaction_doomed = self.StorageQuotaExceeded( - format_file_size(self.storage_quota), format_file_size(self.storage_quota_use) - ) - raise self.transaction_doomed + self._lock_refresh() + data_size = len(data) + if data_size > MAX_DATA_SIZE: + raise IntegrityError(f"More than allowed put data [{data_size} > {MAX_DATA_SIZE}]") + + key = "data/" + bin_to_hex(id) + self.store.store(key, data) def delete(self, id, wait=True): """delete a repo object @@ -1263,26 +364,12 @@ def delete(self, id, wait=True): Note: when doing calls with wait=False this gets async and caller must deal with async results / exceptions later. """ - if not self._active_txn: - self.prepare_txn(self.get_transaction_id()) + self._lock_refresh() + key = "data/" + bin_to_hex(id) try: - in_index = self.index.pop(id) - except KeyError: + self.store.delete(key) + except StoreObjectNotFound: raise self.ObjectNotFound(id, self.path) from None - self._delete(id, in_index.segment, in_index.offset, in_index.size) - - def _delete(self, id, segment, offset, size): - # common code used by put and delete - # because we'll write a DEL tag to the repository, we must update the shadow index. - # this is always true, no matter whether we are called from put() or delete(). - # the compaction code needs this to not drop DEL tags if they are still required - # to keep a PUT in an earlier segment in the "effectively deleted" state. - self.shadow_index.setdefault(id, []).append(segment) - self.segments[segment] -= 1 - self.compact[segment] += header_size(TAG_PUT2) + size - segment, size = self.io.write_delete(id) - self.compact[segment] += size - self.segments.setdefault(segment, 0) def async_response(self, wait=True): """Get one async result (only applies to remote repositories). @@ -1298,527 +385,34 @@ def async_response(self, wait=True): def preload(self, ids): """Preload objects (only applies to remote repositories)""" + def break_lock(self): + Lock(self.store).break_lock() + + def migrate_lock(self, old_id, new_id): + # note: only needed for local repos + if self.lock is not None: + self.lock.migrate_lock(old_id, new_id) + def get_manifest(self): try: - return self.get(Manifest.MANIFEST_ID) - except self.ObjectNotFound: + return self.store.load("config/manifest") + except StoreObjectNotFound: raise NoManifestError def put_manifest(self, data): - return self.put(Manifest.MANIFEST_ID, data) + return self.store.store("config/manifest", data) - -class LoggedIO: - class SegmentFull(Exception): - """raised when a segment is full, before opening next""" - - header_fmt = struct.Struct(" transaction_id: - self.delete_segment(segment) - count += 1 - else: - break - logger.debug("Cleaned up %d uncommitted segment files (== everything after segment %d).", count, transaction_id) - - def is_committed_segment(self, segment): - """Check if segment ends with a COMMIT_TAG tag""" + def store_list(self, name): try: - iterator = self.iter_objects(segment) - except IntegrityError: - return False - with open(self.segment_filename(segment), "rb") as fd: - try: - fd.seek(-self.header_fmt.size, os.SEEK_END) - except OSError as e: - # return False if segment file is empty or too small - if e.errno == errno.EINVAL: - return False - raise e - if fd.read(self.header_fmt.size) != self.COMMIT: - return False - seen_commit = False - while True: - try: - tag, key, offset, _, _ = next(iterator) - except IntegrityError: - return False - except StopIteration: - break - if tag == TAG_COMMIT: - seen_commit = True - continue - if seen_commit: - return False - return seen_commit + return list(self.store.list(name)) + except StoreObjectNotFound: + return [] - def segment_filename(self, segment): - return os.path.join(self.path, "data", str(segment // self.segments_per_dir), str(segment)) + def store_load(self, name): + return self.store.load(name) - def get_write_fd(self, no_new=False, want_new=False, raise_full=False): - if not no_new and (want_new or self.offset and self.offset > self.limit): - if raise_full: - raise self.SegmentFull - self.close_segment() - if not self._write_fd: - if self.segment % self.segments_per_dir == 0: - dirname = os.path.join(self.path, "data", str(self.segment // self.segments_per_dir)) - if not os.path.exists(dirname): - os.mkdir(dirname) - sync_dir(os.path.join(self.path, "data")) - self._write_fd = SyncFile(self.segment_filename(self.segment), binary=True) - self._write_fd.write(MAGIC) - self.offset = MAGIC_LEN - if self.segment in self.fds: - # we may have a cached fd for a segment file we already deleted and - # we are writing now a new segment file to same file name. get rid of - # the cached fd that still refers to the old file, so it will later - # get repopulated (on demand) with a fd that refers to the new file. - del self.fds[self.segment] - return self._write_fd + def store_store(self, name, value): + return self.store.store(name, value) - def get_fd(self, segment): - # note: get_fd() returns a fd with undefined file pointer position, - # so callers must always seek() to desired position afterwards. - now = time.monotonic() - - def open_fd(): - fd = open(self.segment_filename(segment), "rb") - self.fds[segment] = (now, fd) - return fd - - def clean_old(): - # we regularly get rid of all old FDs here: - if now - self._fds_cleaned > FD_MAX_AGE // 8: - self._fds_cleaned = now - for k, ts_fd in list(self.fds.items()): - ts, fd = ts_fd - if now - ts > FD_MAX_AGE: - # we do not want to touch long-unused file handles to - # avoid ESTALE issues (e.g. on network filesystems). - del self.fds[k] - - clean_old() - if self._write_fd is not None: - # without this, we have a test failure now - self._write_fd.sync() - try: - ts, fd = self.fds[segment] - except KeyError: - fd = open_fd() - else: - # we only have fresh enough stuff here. - # update the timestamp of the lru cache entry. - self.fds.replace(segment, (now, fd)) - return fd - - def close_segment(self): - # set self._write_fd to None early to guard against reentry from error handling code paths: - fd, self._write_fd = self._write_fd, None - if fd is not None: - self.segment += 1 - self.offset = 0 - fd.close() - - def delete_segment(self, segment): - if segment in self.fds: - del self.fds[segment] - try: - safe_unlink(self.segment_filename(segment)) - except FileNotFoundError: - pass - - def clear_empty_dirs(self): - """Delete empty segment dirs, i.e those with no segment files.""" - data_dir = os.path.join(self.path, "data") - segment_dirs = self.get_segment_dirs(data_dir) - for segment_dir in segment_dirs: - try: - # os.rmdir will only delete the directory if it is empty - # so we don't need to explicitly check for emptiness first. - os.rmdir(segment_dir) - except OSError: - # OSError is raised by os.rmdir if directory is not empty. This is expected. - # Its subclass FileNotFoundError may be raised if the directory already does not exist. Ignorable. - pass - sync_dir(data_dir) - - def segment_exists(self, segment): - filename = self.segment_filename(segment) - # When deleting segments, they are first truncated. If truncate(2) and unlink(2) are split - # across FS transactions, then logically deleted segments will show up as truncated. - return os.path.exists(filename) and os.path.getsize(filename) - - def segment_size(self, segment): - return os.path.getsize(self.segment_filename(segment)) - - def get_segment_magic(self, segment): - fd = self.get_fd(segment) - fd.seek(0) - return fd.read(MAGIC_LEN) - - def iter_objects(self, segment, read_data=True): - """ - Return object iterator for *segment*. - - See the _read() docstring about confidence in the returned data. - - The iterator returns five-tuples of (tag, key, offset, size, data). - """ - fd = self.get_fd(segment) - offset = 0 - fd.seek(offset) - if fd.read(MAGIC_LEN) != MAGIC: - raise IntegrityError(f"Invalid segment magic [segment {segment}, offset {offset}]") - offset = MAGIC_LEN - header = fd.read(self.header_fmt.size) - while header: - size, tag, key, data = self._read( - fd, header, segment, offset, (TAG_PUT2, TAG_DELETE, TAG_COMMIT, TAG_PUT), read_data=read_data - ) - # tuple[3]: corresponds to len(data) == length of the full chunk payload (meta_len+enc_meta+enc_data) - # tuple[4]: data will be None if read_data is False. - yield tag, key, offset, size - header_size(tag), data - assert size >= 0 - offset += size - # we must get the fd via get_fd() here again as we yielded to our caller and it might - # have triggered closing of the fd we had before (e.g. by calling io.read() for - # different segment(s)). - # by calling get_fd() here again we also make our fd "recently used" so it likely - # does not get kicked out of self.fds LRUcache. - fd = self.get_fd(segment) - fd.seek(offset) - header = fd.read(self.header_fmt.size) - - def recover_segment(self, segment, filename): - logger.info("Attempting to recover " + filename) - if segment in self.fds: - del self.fds[segment] - if os.path.getsize(filename) < MAGIC_LEN + self.header_fmt.size: - # this is either a zero-byte file (which would crash mmap() below) or otherwise - # just too small to be a valid non-empty segment file, so do a shortcut here: - with SaveFile(filename, binary=True) as fd: - fd.write(MAGIC) - return - with SaveFile(filename, binary=True) as dst_fd: - with open(filename, "rb") as src_fd: - # note: file must not be 0 size or mmap() will crash. - with mmap.mmap(src_fd.fileno(), 0, access=mmap.ACCESS_READ) as mm: - # memoryview context manager is problematic, see https://bugs.python.org/issue35686 - data = memoryview(mm) - d = data - try: - dst_fd.write(MAGIC) - while len(d) >= self.header_fmt.size: - crc, size, tag = self.header_fmt.unpack(d[: self.header_fmt.size]) - size_invalid = size > MAX_OBJECT_SIZE or size < self.header_fmt.size or size > len(d) - if size_invalid or tag > MAX_TAG_ID: - d = d[1:] - continue - if tag == TAG_PUT2: - c_offset = self.HEADER_ID_SIZE + self.ENTRY_HASH_SIZE - # skip if header is invalid - if crc32(d[4:c_offset]) & 0xFFFFFFFF != crc: - d = d[1:] - continue - # skip if content is invalid - if ( - self.entry_hash(d[4 : self.HEADER_ID_SIZE], d[c_offset:size]) - != d[self.HEADER_ID_SIZE : c_offset] - ): - d = d[1:] - continue - elif tag in (TAG_DELETE, TAG_COMMIT, TAG_PUT): - if crc32(d[4:size]) & 0xFFFFFFFF != crc: - d = d[1:] - continue - else: # tag unknown - d = d[1:] - continue - dst_fd.write(d[:size]) - d = d[size:] - finally: - del d - data.release() - - def entry_hash(self, *data): - h = StreamingXXH64() - for d in data: - h.update(d) - return h.digest() - - def read(self, segment, offset, id, *, read_data=True, expected_size=None): - """ - Read entry from *segment* at *offset* with *id*. - - See the _read() docstring about confidence in the returned data. - """ - if segment == self.segment and self._write_fd: - self._write_fd.sync() - fd = self.get_fd(segment) - fd.seek(offset) - header = fd.read(self.header_fmt.size) - size, tag, key, data = self._read(fd, header, segment, offset, (TAG_PUT2, TAG_PUT), read_data=read_data) - if id != key: - raise IntegrityError( - f"Invalid segment entry header, is not for wanted id [segment {segment}, offset {offset}]" - ) - data_size_from_header = size - header_size(tag) - if expected_size is not None and expected_size != data_size_from_header: - raise IntegrityError( - f"size from repository index: {expected_size} != " f"size from entry header: {data_size_from_header}" - ) - return data - - def _read(self, fd, header, segment, offset, acceptable_tags, read_data=True): - """ - Code shared by read() and iter_objects(). - - Confidence in returned data: - PUT2 tags, read_data == True: crc32 check (header) plus digest check (header+data) - PUT2 tags, read_data == False: crc32 check (header) - PUT tags, read_data == True: crc32 check (header+data) - PUT tags, read_data == False: crc32 check can not be done, all data obtained must be considered informational - - read_data == False behaviour: - PUT2 tags: return enough of the chunk so that the client is able to decrypt the metadata, - do not read, but just seek over the data. - PUT tags: return None and just seek over the data. - """ - - def check_crc32(wanted, header, *data): - result = crc32(memoryview(header)[4:]) # skip first 32 bits of the header, they contain the crc. - for d in data: - result = crc32(d, result) - if result & 0xFFFFFFFF != wanted: - raise IntegrityError(f"Segment entry header checksum mismatch [segment {segment}, offset {offset}]") - - # See comment on MAX_TAG_ID for details - assert max(acceptable_tags) <= MAX_TAG_ID, "Exceeding MAX_TAG_ID will break backwards compatibility" - key = data = None - fmt = self.header_fmt - try: - hdr_tuple = fmt.unpack(header) - except struct.error as err: - raise IntegrityError(f"Invalid segment entry header [segment {segment}, offset {offset}]: {err}") from None - crc, size, tag = hdr_tuple - length = size - fmt.size # we already read the header - if size > MAX_OBJECT_SIZE: - # if you get this on an archive made with borg < 1.0.7 and millions of files and - # you need to restore it, you can disable this check by using "if False:" above. - raise IntegrityError(f"Invalid segment entry size {size} - too big [segment {segment}, offset {offset}]") - if size < fmt.size: - raise IntegrityError(f"Invalid segment entry size {size} - too small [segment {segment}, offset {offset}]") - if tag not in (TAG_PUT2, TAG_DELETE, TAG_COMMIT, TAG_PUT): - raise IntegrityError( - f"Invalid segment entry header, did not get a known tag " f"[segment {segment}, offset {offset}]" - ) - if tag not in acceptable_tags: - raise IntegrityError( - f"Invalid segment entry header, did not get acceptable tag " f"[segment {segment}, offset {offset}]" - ) - if tag == TAG_COMMIT: - check_crc32(crc, header) - # that's all for COMMITs. - else: - # all other tags (TAG_PUT2, TAG_DELETE, TAG_PUT) have a key - key = fd.read(32) - length -= 32 - if len(key) != 32: - raise IntegrityError( - f"Segment entry key short read [segment {segment}, offset {offset}]: " - f"expected {32}, got {len(key)} bytes" - ) - if tag == TAG_DELETE: - check_crc32(crc, header, key) - # that's all for DELETEs. - else: - # TAG_PUT: we can not do a crc32 header check here, because the crc32 is computed over header+data! - # for the check, see code below when read_data is True. - if tag == TAG_PUT2: - entry_hash = fd.read(self.ENTRY_HASH_SIZE) - length -= self.ENTRY_HASH_SIZE - if len(entry_hash) != self.ENTRY_HASH_SIZE: - raise IntegrityError( - f"Segment entry hash short read [segment {segment}, offset {offset}]: " - f"expected {self.ENTRY_HASH_SIZE}, got {len(entry_hash)} bytes" - ) - check_crc32(crc, header, key, entry_hash) - if not read_data: - if tag == TAG_PUT2: - # PUT2 is only used in new repos and they also have different RepoObj layout, - # supporting separately encrypted metadata and data. - # In this case, we return enough bytes so the client can decrypt the metadata - # and seek over the rest (over the encrypted data). - hdr_size = RepoObj.obj_header.size - hdr = fd.read(hdr_size) - length -= hdr_size - if len(hdr) != hdr_size: - raise IntegrityError( - f"Segment entry meta length short read [segment {segment}, offset {offset}]: " - f"expected {hdr_size}, got {len(hdr)} bytes" - ) - meta_size = RepoObj.obj_header.unpack(hdr)[0] - meta = fd.read(meta_size) - length -= meta_size - if len(meta) != meta_size: - raise IntegrityError( - f"Segment entry meta short read [segment {segment}, offset {offset}]: " - f"expected {meta_size}, got {len(meta)} bytes" - ) - data = hdr + meta # shortened chunk - enough so the client can decrypt the metadata - # in any case, we seek over the remainder of the chunk - oldpos = fd.tell() - seeked = fd.seek(length, os.SEEK_CUR) - oldpos - if seeked != length: - raise IntegrityError( - f"Segment entry data short seek [segment {segment}, offset {offset}]: " - f"expected {length}, got {seeked} bytes" - ) - else: # read data! - data = fd.read(length) - if len(data) != length: - raise IntegrityError( - f"Segment entry data short read [segment {segment}, offset {offset}]: " - f"expected {length}, got {len(data)} bytes" - ) - if tag == TAG_PUT2: - if self.entry_hash(memoryview(header)[4:], key, data) != entry_hash: - raise IntegrityError(f"Segment entry hash mismatch [segment {segment}, offset {offset}]") - elif tag == TAG_PUT: - check_crc32(crc, header, key, data) - return size, tag, key, data - - def write_put(self, id, data, raise_full=False): - data_size = len(data) - if data_size > MAX_DATA_SIZE: - # this would push the segment entry size beyond MAX_OBJECT_SIZE. - raise IntegrityError(f"More than allowed put data [{data_size} > {MAX_DATA_SIZE}]") - fd = self.get_write_fd(want_new=(id == Manifest.MANIFEST_ID), raise_full=raise_full) - size = data_size + self.HEADER_ID_SIZE + self.ENTRY_HASH_SIZE - offset = self.offset - header = self.header_no_crc_fmt.pack(size, TAG_PUT2) - entry_hash = self.entry_hash(header, id, data) - crc = self.crc_fmt.pack(crc32(entry_hash, crc32(id, crc32(header))) & 0xFFFFFFFF) - fd.write(b"".join((crc, header, id, entry_hash))) - fd.write(data) - self.offset += size - return self.segment, offset - - def write_delete(self, id, raise_full=False): - fd = self.get_write_fd(want_new=(id == Manifest.MANIFEST_ID), raise_full=raise_full) - header = self.header_no_crc_fmt.pack(self.HEADER_ID_SIZE, TAG_DELETE) - crc = self.crc_fmt.pack(crc32(id, crc32(header)) & 0xFFFFFFFF) - fd.write(b"".join((crc, header, id))) - self.offset += self.HEADER_ID_SIZE - return self.segment, self.HEADER_ID_SIZE - - def write_commit(self, intermediate=False): - # Intermediate commits go directly into the current segment - this makes checking their validity more - # expensive, but is faster and reduces clobber. Final commits go into a new segment. - fd = self.get_write_fd(want_new=not intermediate, no_new=intermediate) - if intermediate: - fd.sync() - header = self.header_no_crc_fmt.pack(self.header_fmt.size, TAG_COMMIT) - crc = self.crc_fmt.pack(crc32(header) & 0xFFFFFFFF) - fd.write(b"".join((crc, header))) - self.close_segment() - return self.segment - 1 # close_segment() increments it - - -assert LoggedIO.HEADER_ID_SIZE + LoggedIO.ENTRY_HASH_SIZE == 41 + 8 # see constants.MAX_OBJECT_SIZE + def store_delete(self, name): + return self.store.delete(name) diff --git a/src/borg/repository3.py b/src/borg/repository3.py deleted file mode 100644 index 998c76ad1..000000000 --- a/src/borg/repository3.py +++ /dev/null @@ -1,418 +0,0 @@ -import os - -from borgstore.store import Store -from borgstore.store import ObjectNotFound as StoreObjectNotFound - -from .checksums import xxh64 -from .constants import * # NOQA -from .helpers import Error, ErrorWithTraceback, IntegrityError -from .helpers import Location -from .helpers import bin_to_hex, hex_to_bin -from .locking3 import Lock -from .logger import create_logger -from .manifest import NoManifestError -from .repoobj import RepoObj - -logger = create_logger(__name__) - - -class Repository3: - """borgstore based key value store""" - - class AlreadyExists(Error): - """A repository already exists at {}.""" - - exit_mcode = 10 - - class CheckNeeded(ErrorWithTraceback): - """Inconsistency detected. Please run "borg check {}".""" - - exit_mcode = 12 - - class DoesNotExist(Error): - """Repository {} does not exist.""" - - exit_mcode = 13 - - class InsufficientFreeSpaceError(Error): - """Insufficient free space to complete transaction (required: {}, available: {}).""" - - exit_mcode = 14 - - class InvalidRepository(Error): - """{} is not a valid repository. Check repo config.""" - - exit_mcode = 15 - - class InvalidRepositoryConfig(Error): - """{} does not have a valid configuration. Check repo config [{}].""" - - exit_mcode = 16 - - class ObjectNotFound(ErrorWithTraceback): - """Object with key {} not found in repository {}.""" - - exit_mcode = 17 - - def __init__(self, id, repo): - if isinstance(id, bytes): - id = bin_to_hex(id) - super().__init__(id, repo) - - class ParentPathDoesNotExist(Error): - """The parent path of the repo directory [{}] does not exist.""" - - exit_mcode = 18 - - class PathAlreadyExists(Error): - """There is already something at {}.""" - - exit_mcode = 19 - - class StorageQuotaExceeded(Error): - """The storage quota ({}) has been exceeded ({}). Try deleting some archives.""" - - exit_mcode = 20 - - class PathPermissionDenied(Error): - """Permission denied to {}.""" - - exit_mcode = 21 - - def __init__( - self, - path, - create=False, - exclusive=False, - lock_wait=1.0, - lock=True, - append_only=False, - storage_quota=None, - make_parent_dirs=False, - send_log_cb=None, - ): - self.path = os.path.abspath(path) - url = "file://%s" % self.path - # use a Store with flat config storage and 2-levels-nested data storage - self.store = Store(url, levels={"config/": [0], "data/": [2]}) - self._location = Location(url) - self.version = None - # long-running repository methods which emit log or progress output are responsible for calling - # the ._send_log method periodically to get log and progress output transferred to the borg client - # in a timely manner, in case we have a RemoteRepository. - # for local repositories ._send_log can be called also (it will just do nothing in that case). - self._send_log = send_log_cb or (lambda: None) - self.do_create = create - self.created = False - self.acceptable_repo_versions = (3,) - self.opened = False - self.append_only = append_only # XXX not implemented / not implementable - self.storage_quota = storage_quota # XXX not implemented - self.storage_quota_use = 0 # XXX not implemented - self.lock = None - self.do_lock = lock - self.lock_wait = lock_wait - self.exclusive = exclusive - - def __repr__(self): - return f"<{self.__class__.__name__} {self.path}>" - - def __enter__(self): - if self.do_create: - self.do_create = False - self.create() - self.created = True - self.open(exclusive=bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - @property - def id_str(self): - return bin_to_hex(self.id) - - def create(self): - """Create a new empty repository""" - self.store.create() - self.store.open() - self.store.store("config/readme", REPOSITORY_README.encode()) - self.version = 3 - self.store.store("config/version", str(self.version).encode()) - self.store.store("config/id", bin_to_hex(os.urandom(32)).encode()) - self.store.close() - - def _set_id(self, id): - # for testing: change the id of an existing repository - assert self.opened - assert isinstance(id, bytes) and len(id) == 32 - self.id = id - self.store.store("config/id", bin_to_hex(id).encode()) - - def _lock_refresh(self): - if self.lock is not None: - self.lock.refresh() - - def save_key(self, keydata): - # note: saving an empty key means that there is no repokey anymore - self.store.store("keys/repokey", keydata) - - def load_key(self): - keydata = self.store.load("keys/repokey") - # note: if we return an empty string, it means there is no repo key - return keydata - - def destroy(self): - """Destroy the repository""" - self.close() - self.store.destroy() - - def open(self, *, exclusive, lock_wait=None, lock=True): - assert lock_wait is not None - self.store.open() - if lock: - self.lock = Lock(self.store, exclusive, timeout=lock_wait).acquire() - else: - self.lock = None - readme = self.store.load("config/readme").decode() - if readme != REPOSITORY_README: - raise self.InvalidRepository(self.path) - self.version = int(self.store.load("config/version").decode()) - if self.version not in self.acceptable_repo_versions: - self.close() - raise self.InvalidRepositoryConfig( - self.path, "repository version %d is not supported by this borg version" % self.version - ) - self.id = hex_to_bin(self.store.load("config/id").decode(), length=32) - self.opened = True - - def close(self): - if self.opened: - if self.lock: - self.lock.release() - self.lock = None - self.store.close() - self.opened = False - - def info(self): - """return some infos about the repo (must be opened first)""" - info = dict( - id=self.id, - version=self.version, - storage_quota_use=self.storage_quota_use, - storage_quota=self.storage_quota, - append_only=self.append_only, - ) - return info - - def check(self, repair=False, max_duration=0): - """Check repository consistency""" - - def log_error(msg): - nonlocal obj_corrupted - obj_corrupted = True - logger.error(f"Repo object {info.name} is corrupted: {msg}") - - def check_object(obj): - """Check if obj looks valid.""" - hdr_size = RepoObj.obj_header.size - obj_size = len(obj) - if obj_size >= hdr_size: - hdr = RepoObj.ObjHeader(*RepoObj.obj_header.unpack(obj[:hdr_size])) - meta = obj[hdr_size : hdr_size + hdr.meta_size] - if hdr.meta_size != len(meta): - log_error("metadata size incorrect.") - elif hdr.meta_hash != xxh64(meta): - log_error("metadata does not match checksum.") - data = obj[hdr_size + hdr.meta_size : hdr_size + hdr.meta_size + hdr.data_size] - if hdr.data_size != len(data): - log_error("data size incorrect.") - elif hdr.data_hash != xxh64(data): - log_error("data does not match checksum.") - else: - log_error("too small.") - - # TODO: progress indicator, partial checks, ... - mode = "full" - logger.info("Starting repository check") - objs_checked = objs_errors = 0 - infos = self.store.list("data") - try: - for info in infos: - self._lock_refresh() - key = "data/%s" % info.name - try: - obj = self.store.load(key) - except StoreObjectNotFound: - # looks like object vanished since store.list(), ignore that. - continue - obj_corrupted = False - check_object(obj) - objs_checked += 1 - if obj_corrupted: - objs_errors += 1 - if repair: - # if it is corrupted, we can't do much except getting rid of it. - # but let's just retry loading it, in case the error goes away. - try: - obj = self.store.load(key) - except StoreObjectNotFound: - log_error("existing object vanished.") - else: - obj_corrupted = False - check_object(obj) - if obj_corrupted: - log_error("reloading did not help, deleting it!") - self.store.delete(key) - else: - log_error("reloading did help, inconsistent behaviour detected!") - except StoreObjectNotFound: - # it can be that there is no "data/" at all, then it crashes when iterating infos. - pass - logger.info(f"Checked {objs_checked} repository objects, {objs_errors} errors.") - if objs_errors == 0: - logger.info(f"Finished {mode} repository check, no problems found.") - else: - if repair: - logger.info(f"Finished {mode} repository check, errors found and repaired.") - else: - logger.error(f"Finished {mode} repository check, errors found.") - return objs_errors == 0 or repair - - def __len__(self): - raise NotImplementedError - - def __contains__(self, id): - raise NotImplementedError - - def list(self, limit=None, marker=None): - """ - list IDs starting from after id . - """ - self._lock_refresh() - collect = True if marker is None else False - ids = [] - infos = self.store.list("data") # generator yielding ItemInfos - while True: - try: - info = next(infos) - except StoreObjectNotFound: - break # can happen e.g. if "data" does not exist, pointless to continue in that case - except StopIteration: - break - else: - id = hex_to_bin(info.name) - if collect: - ids.append(id) - if len(ids) == limit: - break - elif id == marker: - collect = True - # note: do not collect the marker id - return ids - - def get(self, id, read_data=True): - self._lock_refresh() - id_hex = bin_to_hex(id) - key = "data/" + id_hex - try: - if read_data: - # read everything - return self.store.load(key) - else: - # RepoObj layout supports separately encrypted metadata and data. - # We return enough bytes so the client can decrypt the metadata. - hdr_size = RepoObj.obj_header.size - extra_size = 1024 - hdr_size # load a bit more, 1024b, reduces round trips - obj = self.store.load(key, size=hdr_size + extra_size) - hdr = obj[0:hdr_size] - if len(hdr) != hdr_size: - raise IntegrityError(f"Object too small [id {id_hex}]: expected {hdr_size}, got {len(hdr)} bytes") - meta_size = RepoObj.obj_header.unpack(hdr)[0] - if meta_size > extra_size: - # we did not get enough, need to load more, but not all. - # this should be rare, as chunk metadata is rather small usually. - obj = self.store.load(key, size=hdr_size + meta_size) - meta = obj[hdr_size : hdr_size + meta_size] - if len(meta) != meta_size: - raise IntegrityError(f"Object too small [id {id_hex}]: expected {meta_size}, got {len(meta)} bytes") - return hdr + meta - except StoreObjectNotFound: - raise self.ObjectNotFound(id, self.path) from None - - def get_many(self, ids, read_data=True, is_preloaded=False): - for id_ in ids: - yield self.get(id_, read_data=read_data) - - def put(self, id, data, wait=True): - """put a repo object - - Note: when doing calls with wait=False this gets async and caller must - deal with async results / exceptions later. - """ - self._lock_refresh() - data_size = len(data) - if data_size > MAX_DATA_SIZE: - raise IntegrityError(f"More than allowed put data [{data_size} > {MAX_DATA_SIZE}]") - - key = "data/" + bin_to_hex(id) - self.store.store(key, data) - - def delete(self, id, wait=True): - """delete a repo object - - Note: when doing calls with wait=False this gets async and caller must - deal with async results / exceptions later. - """ - self._lock_refresh() - key = "data/" + bin_to_hex(id) - try: - self.store.delete(key) - except StoreObjectNotFound: - raise self.ObjectNotFound(id, self.path) from None - - def async_response(self, wait=True): - """Get one async result (only applies to remote repositories). - - async commands (== calls with wait=False, e.g. delete and put) have no results, - but may raise exceptions. These async exceptions must get collected later via - async_response() calls. Repeat the call until it returns None. - The previous calls might either return one (non-None) result or raise an exception. - If wait=True is given and there are outstanding responses, it will wait for them - to arrive. With wait=False, it will only return already received responses. - """ - - def preload(self, ids): - """Preload objects (only applies to remote repositories)""" - - def break_lock(self): - Lock(self.store).break_lock() - - def migrate_lock(self, old_id, new_id): - # note: only needed for local repos - if self.lock is not None: - self.lock.migrate_lock(old_id, new_id) - - def get_manifest(self): - try: - return self.store.load("config/manifest") - except StoreObjectNotFound: - raise NoManifestError - - def put_manifest(self, data): - return self.store.store("config/manifest", data) - - def store_list(self, name): - try: - return list(self.store.list(name)) - except StoreObjectNotFound: - return [] - - def store_load(self, name): - return self.store.load(name) - - def store_store(self, name, value): - return self.store.store(name, value) - - def store_delete(self, name): - return self.store.delete(name) diff --git a/src/borg/locking3.py b/src/borg/storelocking.py similarity index 100% rename from src/borg/locking3.py rename to src/borg/storelocking.py diff --git a/src/borg/testsuite/archiver/__init__.py b/src/borg/testsuite/archiver/__init__.py index 7f0d63d25..2ebcb4573 100644 --- a/src/borg/testsuite/archiver/__init__.py +++ b/src/borg/testsuite/archiver/__init__.py @@ -24,8 +24,8 @@ from ...logger import flush_logging from ...manifest import Manifest from ...platform import get_flags -from ...remote3 import RemoteRepository3 -from ...repository3 import Repository3 +from ...remote import RemoteRepository +from ...repository import Repository from .. import has_lchflags, is_utime_fully_supported, have_fuse_mtime_ns, st_mtime_ns_round, no_selinux from .. import changedir from .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported @@ -166,7 +166,7 @@ def create_src_archive(archiver, name, ts=None): def open_archive(repo_path, name): - repository = Repository3(repo_path, exclusive=True) + repository = Repository(repo_path, exclusive=True) with repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) archive = Archive(manifest, name) @@ -175,9 +175,9 @@ def open_archive(repo_path, name): def open_repository(archiver): if archiver.get_kind() == "remote": - return RemoteRepository3(Location(archiver.repository_location)) + return RemoteRepository(Location(archiver.repository_location)) else: - return Repository3(archiver.repository_path, exclusive=True) + return Repository(archiver.repository_path, exclusive=True) def create_regular_file(input_path, name, size=0, contents=None): @@ -253,12 +253,12 @@ def create_test_files(input_path, create_hardlinks=True): def _extract_repository_id(repo_path): - with Repository3(repo_path) as repository: + with Repository(repo_path) as repository: return repository.id def _set_repository_id(repo_path, id): - with Repository3(repo_path) as repository: + with Repository(repo_path) as repository: repository._set_id(id) return repository.id diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index 973cb0a91..21f617b87 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -8,9 +8,9 @@ from ...constants import * # NOQA from ...helpers import bin_to_hex, msgpack from ...manifest import Manifest -from ...remote3 import RemoteRepository3 -from ...repository3 import Repository3 -from ..repository3 import fchunk +from ...remote import RemoteRepository +from ...repository import Repository +from ..repository import fchunk from . import cmd, src_file, create_src_archive, open_archive, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA @@ -191,7 +191,7 @@ def test_missing_manifest(archivers, request): check_cmd_setup(archiver) archive, repository = open_archive(archiver.repository_path, "archive1") with repository: - if isinstance(repository, (Repository3, RemoteRepository3)): + if isinstance(repository, (Repository, RemoteRepository)): repository.store_delete("config/manifest") else: repository.delete(Manifest.MANIFEST_ID) @@ -342,7 +342,7 @@ def test_extra_chunks(archivers, request): pytest.skip("only works locally") check_cmd_setup(archiver) cmd(archiver, "check", exit_code=0) - with Repository3(archiver.repository_location, exclusive=True) as repository: + with Repository(archiver.repository_location, exclusive=True) as repository: chunk = fchunk(b"xxxx") repository.put(b"01234567890123456789012345678901", chunk) cmd(archiver, "check", "-v", exit_code=0) # check does not deal with orphans anymore @@ -362,9 +362,9 @@ def fake_xxh64(data, seed=0): return b"fakefake" import borg.repoobj - import borg.repository3 + import borg.repository - with patch.object(borg.repoobj, "xxh64", fake_xxh64), patch.object(borg.repository3, "xxh64", fake_xxh64): + with patch.object(borg.repoobj, "xxh64", fake_xxh64), patch.object(borg.repository, "xxh64", fake_xxh64): check_cmd_setup(archiver) shutil.rmtree(archiver.repository_path) cmd(archiver, "rcreate", *init_args) @@ -431,7 +431,7 @@ def test_empty_repository(archivers, request): if archiver.get_kind() == "remote": pytest.skip("only works locally") check_cmd_setup(archiver) - with Repository3(archiver.repository_location, exclusive=True) as repository: + with Repository(archiver.repository_location, exclusive=True) as repository: for id_ in repository.list(): repository.delete(id_) cmd(archiver, "check", exit_code=1) diff --git a/src/borg/testsuite/archiver/checks.py b/src/borg/testsuite/archiver/checks.py index df8a88002..efea8e1fb 100644 --- a/src/borg/testsuite/archiver/checks.py +++ b/src/borg/testsuite/archiver/checks.py @@ -9,8 +9,8 @@ from ...helpers import Location, get_security_dir, bin_to_hex from ...helpers import EXIT_ERROR from ...manifest import Manifest, MandatoryFeatureUnsupported -from ...remote3 import RemoteRepository3, PathNotAllowed -from ...repository3 import Repository3 +from ...remote import RemoteRepository, PathNotAllowed +from ...repository import Repository from .. import llfuse from .. import changedir from . import cmd, _extract_repository_id, create_test_files @@ -25,7 +25,7 @@ def get_security_directory(repo_path): def add_unknown_feature(repo_path, operation): - with Repository3(repo_path, exclusive=True) as repository: + with Repository(repo_path, exclusive=True) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) manifest.config["feature_flags"] = {operation.value: {"mandatory": ["unknown-feature"]}} manifest.write() @@ -259,7 +259,7 @@ def test_unknown_mandatory_feature_in_cache(archivers, request): remote_repo = archiver.get_kind() == "remote" print(cmd(archiver, "rcreate", RK_ENCRYPTION)) - with Repository3(archiver.repository_path, exclusive=True) as repository: + with Repository(archiver.repository_path, exclusive=True) as repository: if remote_repo: repository._location = Location(archiver.repository_location) manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -271,7 +271,7 @@ def test_unknown_mandatory_feature_in_cache(archivers, request): if archiver.FORK_DEFAULT: cmd(archiver, "create", "test", "input") - with Repository3(archiver.repository_path, exclusive=True) as repository: + with Repository(archiver.repository_path, exclusive=True) as repository: if remote_repo: repository._location = Location(archiver.repository_location) manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) @@ -283,26 +283,26 @@ def test_unknown_mandatory_feature_in_cache(archivers, request): def test_remote_repo_restrict_to_path(remote_archiver): original_location, repo_path = remote_archiver.repository_location, remote_archiver.repository_path # restricted to repo directory itself: - with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-path", repo_path]): + with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-path", repo_path]): cmd(remote_archiver, "rcreate", RK_ENCRYPTION) # restricted to repo directory itself, fail for other directories with same prefix: - with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-path", repo_path]): + with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-path", repo_path]): with pytest.raises(PathNotAllowed): remote_archiver.repository_location = original_location + "_0" cmd(remote_archiver, "rcreate", RK_ENCRYPTION) # restricted to a completely different path: - with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-path", "/foo"]): + with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-path", "/foo"]): with pytest.raises(PathNotAllowed): remote_archiver.repository_location = original_location + "_1" cmd(remote_archiver, "rcreate", RK_ENCRYPTION) path_prefix = os.path.dirname(repo_path) # restrict to repo directory's parent directory: - with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-path", path_prefix]): + with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-path", path_prefix]): remote_archiver.repository_location = original_location + "_2" cmd(remote_archiver, "rcreate", RK_ENCRYPTION) # restrict to repo directory's parent directory and another directory: with patch.object( - RemoteRepository3, "extra_test_args", ["--restrict-to-path", "/foo", "--restrict-to-path", path_prefix] + RemoteRepository, "extra_test_args", ["--restrict-to-path", "/foo", "--restrict-to-path", path_prefix] ): remote_archiver.repository_location = original_location + "_3" cmd(remote_archiver, "rcreate", RK_ENCRYPTION) @@ -311,10 +311,10 @@ def test_remote_repo_restrict_to_path(remote_archiver): def test_remote_repo_restrict_to_repository(remote_archiver): repo_path = remote_archiver.repository_path # restricted to repo directory itself: - with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-repository", repo_path]): + with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-repository", repo_path]): cmd(remote_archiver, "rcreate", RK_ENCRYPTION) parent_path = os.path.join(repo_path, "..") - with patch.object(RemoteRepository3, "extra_test_args", ["--restrict-to-repository", parent_path]): + with patch.object(RemoteRepository, "extra_test_args", ["--restrict-to-repository", parent_path]): with pytest.raises(PathNotAllowed): cmd(remote_archiver, "rcreate", RK_ENCRYPTION) diff --git a/src/borg/testsuite/archiver/create_cmd.py b/src/borg/testsuite/archiver/create_cmd.py index a210fd2ec..8bc546fc3 100644 --- a/src/borg/testsuite/archiver/create_cmd.py +++ b/src/borg/testsuite/archiver/create_cmd.py @@ -16,7 +16,7 @@ from ...constants import * # NOQA from ...manifest import Manifest from ...platform import is_cygwin, is_win32, is_darwin -from ...repository3 import Repository3 +from ...repository import Repository from ...helpers import CommandError, BackupPermissionError from .. import has_lchflags from .. import changedir @@ -644,7 +644,7 @@ def test_create_dry_run(archivers, request): cmd(archiver, "rcreate", RK_ENCRYPTION) cmd(archiver, "create", "--dry-run", "test", "input") # Make sure no archive has been created - with Repository3(archiver.repository_path) as repository: + with Repository(archiver.repository_path) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) assert len(manifest.archives) == 0 diff --git a/src/borg/testsuite/archiver/key_cmds.py b/src/borg/testsuite/archiver/key_cmds.py index 32a56fdd6..ef00e007e 100644 --- a/src/borg/testsuite/archiver/key_cmds.py +++ b/src/borg/testsuite/archiver/key_cmds.py @@ -9,7 +9,7 @@ from ...helpers import CommandError from ...helpers import bin_to_hex, hex_to_bin from ...helpers import msgpack -from ...repository3 import Repository3 +from ...repository import Repository from .. import key from . import RK_ENCRYPTION, KF_ENCRYPTION, cmd, _extract_repository_id, _set_repository_id, generate_archiver_tests @@ -129,7 +129,7 @@ def test_key_export_repokey(archivers, request): assert export_contents.startswith("BORG_KEY " + bin_to_hex(repo_id) + "\n") - with Repository3(archiver.repository_path) as repository: + with Repository(archiver.repository_path) as repository: repo_key = AESOCBRepoKey(repository) repo_key.load(None, Passphrase.env_passphrase()) @@ -138,12 +138,12 @@ def test_key_export_repokey(archivers, request): assert repo_key.crypt_key == backup_key.crypt_key - with Repository3(archiver.repository_path) as repository: + with Repository(archiver.repository_path) as repository: repository.save_key(b"") cmd(archiver, "key", "import", export_file) - with Repository3(archiver.repository_path) as repository: + with Repository(archiver.repository_path) as repository: repo_key2 = AESOCBRepoKey(repository) repo_key2.load(None, Passphrase.env_passphrase()) @@ -302,7 +302,7 @@ def test_init_defaults_to_argon2(archivers, request): """https://github.com/borgbackup/borg/issues/747#issuecomment-1076160401""" archiver = request.getfixturevalue(archivers) cmd(archiver, "rcreate", RK_ENCRYPTION) - with Repository3(archiver.repository_path) as repository: + with Repository(archiver.repository_path) as repository: key = msgpack.unpackb(binascii.a2b_base64(repository.load_key())) assert key["algorithm"] == "argon2 chacha20-poly1305" @@ -313,7 +313,7 @@ def test_change_passphrase_does_not_change_algorithm_argon2(archivers, request): os.environ["BORG_NEW_PASSPHRASE"] = "newpassphrase" cmd(archiver, "key", "change-passphrase") - with Repository3(archiver.repository_path) as repository: + with Repository(archiver.repository_path) as repository: key = msgpack.unpackb(binascii.a2b_base64(repository.load_key())) assert key["algorithm"] == "argon2 chacha20-poly1305" @@ -323,6 +323,6 @@ def test_change_location_does_not_change_algorithm_argon2(archivers, request): cmd(archiver, "rcreate", KF_ENCRYPTION) cmd(archiver, "key", "change-location", "repokey") - with Repository3(archiver.repository_path) as repository: + with Repository(archiver.repository_path) as repository: key = msgpack.unpackb(binascii.a2b_base64(repository.load_key())) assert key["algorithm"] == "argon2 chacha20-poly1305" diff --git a/src/borg/testsuite/archiver/mount_cmds.py b/src/borg/testsuite/archiver/mount_cmds.py index 6b1a0df05..292aff748 100644 --- a/src/borg/testsuite/archiver/mount_cmds.py +++ b/src/borg/testsuite/archiver/mount_cmds.py @@ -7,7 +7,7 @@ from ... import xattr, platform from ...constants import * # NOQA -from ...locking3 import Lock +from ...storelocking import Lock from ...helpers import flags_noatime, flags_normal from .. import has_lchflags, llfuse from .. import changedir, no_selinux, same_ts_ns diff --git a/src/borg/testsuite/archiver/rcompress_cmd.py b/src/borg/testsuite/archiver/rcompress_cmd.py index 12325c81a..8abfc146e 100644 --- a/src/borg/testsuite/archiver/rcompress_cmd.py +++ b/src/borg/testsuite/archiver/rcompress_cmd.py @@ -1,7 +1,7 @@ import os from ...constants import * # NOQA -from ...repository3 import Repository3 +from ...repository import Repository from ...manifest import Manifest from ...compress import ZSTD, ZLIB, LZ4, CNONE from ...helpers import bin_to_hex @@ -12,7 +12,7 @@ def test_rcompress(archiver): def check_compression(ctype, clevel, olevel): """check if all the chunks in the repo are compressed/obfuscated like expected""" - repository = Repository3(archiver.repository_path, exclusive=True) + repository = Repository(archiver.repository_path, exclusive=True) with repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) marker = None diff --git a/src/borg/testsuite/archiver/rename_cmd.py b/src/borg/testsuite/archiver/rename_cmd.py index 7a1637733..5a1b65c0a 100644 --- a/src/borg/testsuite/archiver/rename_cmd.py +++ b/src/borg/testsuite/archiver/rename_cmd.py @@ -1,6 +1,6 @@ from ...constants import * # NOQA from ...manifest import Manifest -from ...repository3 import Repository3 +from ...repository import Repository from . import cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA @@ -21,7 +21,7 @@ def test_rename(archivers, request): cmd(archiver, "extract", "test.3", "--dry-run") cmd(archiver, "extract", "test.4", "--dry-run") # Make sure both archives have been renamed - with Repository3(archiver.repository_path) as repository: + with Repository(archiver.repository_path) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) assert len(manifest.archives) == 2 assert "test.3" in manifest.archives diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index 9a6a6bcb3..beca83505 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -9,14 +9,14 @@ from ..crypto.key import AESOCBRepoKey from ..hashindex import ChunkIndex from ..manifest import Manifest -from ..repository3 import Repository3 +from ..repository import Repository class TestAdHocCache: @pytest.fixture def repository(self, tmpdir): self.repository_location = os.path.join(str(tmpdir), "repository") - with Repository3(self.repository_location, exclusive=True, create=True) as repository: + with Repository(self.repository_location, exclusive=True, create=True) as repository: repository.put(H(1), b"1234") yield repository @@ -51,7 +51,7 @@ def test_deletes_chunks_during_lifetime(self, cache, repository): assert cache.seen_chunk(H(5)) == 1 cache.chunk_decref(H(5), 1, Statistics()) assert not cache.seen_chunk(H(5)) - with pytest.raises(Repository3.ObjectNotFound): + with pytest.raises(Repository.ObjectNotFound): repository.get(H(5)) def test_files_cache(self, cache): diff --git a/src/borg/testsuite/locking.py b/src/borg/testsuite/fslocking.py similarity index 99% rename from src/borg/testsuite/locking.py rename to src/borg/testsuite/fslocking.py index 131fdef3a..d091626c8 100644 --- a/src/borg/testsuite/locking.py +++ b/src/borg/testsuite/fslocking.py @@ -6,7 +6,7 @@ import pytest from ..platform import get_process_id, process_alive -from ..locking import ( +from ..fslocking import ( TimeoutTimer, ExclusiveLock, Lock, diff --git a/src/borg/testsuite/legacyrepository.py b/src/borg/testsuite/legacyrepository.py new file mode 100644 index 000000000..d4e33097b --- /dev/null +++ b/src/borg/testsuite/legacyrepository.py @@ -0,0 +1,1115 @@ +import logging +import os +import sys +from typing import Optional +from unittest.mock import patch + +import pytest + +from ..checksums import xxh64 +from ..hashindex import NSIndex +from ..helpers import Location +from ..helpers import IntegrityError +from ..helpers import msgpack +from ..fslocking import Lock, LockFailed +from ..platformflags import is_win32 +from ..legacyremote import LegacyRemoteRepository, InvalidRPCMethod, PathNotAllowed +from ..legacyrepository import LegacyRepository, LoggedIO +from ..legacyrepository import MAGIC, MAX_DATA_SIZE, TAG_DELETE, TAG_PUT2, TAG_PUT, TAG_COMMIT +from ..repoobj import RepoObj +from .hashindex import H + + +@pytest.fixture() +def repository(tmp_path): + repository_location = os.fspath(tmp_path / "repository") + yield LegacyRepository(repository_location, exclusive=True, create=True) + + +@pytest.fixture() +def remote_repository(tmp_path): + if is_win32: + pytest.skip("Remote repository does not yet work on Windows.") + repository_location = Location("ssh://__testsuite__" + os.fspath(tmp_path / "repository")) + yield LegacyRemoteRepository(repository_location, exclusive=True, create=True) + + +def pytest_generate_tests(metafunc): + # Generates tests that run on both local and remote repos + if "repo_fixtures" in metafunc.fixturenames: + metafunc.parametrize("repo_fixtures", ["repository", "remote_repository"]) + + +def get_repository_from_fixture(repo_fixtures, request): + # returns the repo object from the fixture for tests that run on both local and remote repos + return request.getfixturevalue(repo_fixtures) + + +def reopen(repository, exclusive: Optional[bool] = True, create=False): + if isinstance(repository, LegacyRepository): + if repository.io is not None or repository.lock is not None: + raise RuntimeError("Repo must be closed before a reopen. Cannot support nested repository contexts.") + return LegacyRepository(repository.path, exclusive=exclusive, create=create) + + if isinstance(repository, LegacyRemoteRepository): + if repository.p is not None or repository.sock is not None: + raise RuntimeError("Remote repo must be closed before a reopen. Cannot support nested repository contexts.") + return LegacyRemoteRepository(repository.location, exclusive=exclusive, create=create) + + raise TypeError( + f"Invalid argument type. Expected 'Repository' or 'RemoteRepository', received '{type(repository).__name__}'." + ) + + +def get_path(repository): + if isinstance(repository, LegacyRepository): + return repository.path + + if isinstance(repository, LegacyRemoteRepository): + return repository.location.path + + raise TypeError( + f"Invalid argument type. Expected 'Repository' or 'RemoteRepository', received '{type(repository).__name__}'." + ) + + +def fchunk(data, meta=b""): + # create a raw chunk that has valid RepoObj layout, but does not use encryption or compression. + hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data)) + assert isinstance(data, bytes) + chunk = hdr + meta + data + return chunk + + +def pchunk(chunk): + # parse data and meta from a raw chunk made by fchunk + hdr_size = RepoObj.obj_header.size + hdr = chunk[:hdr_size] + meta_size, data_size = RepoObj.obj_header.unpack(hdr)[0:2] + meta = chunk[hdr_size : hdr_size + meta_size] + data = chunk[hdr_size + meta_size : hdr_size + meta_size + data_size] + return data, meta + + +def pdchunk(chunk): + # parse only data from a raw chunk made by fchunk + return pchunk(chunk)[0] + + +def add_keys(repository): + repository.put(H(0), fchunk(b"foo")) + repository.put(H(1), fchunk(b"bar")) + repository.put(H(3), fchunk(b"bar")) + repository.commit(compact=False) + repository.put(H(1), fchunk(b"bar2")) + repository.put(H(2), fchunk(b"boo")) + repository.delete(H(3)) + + +def repo_dump(repository, label=None): + label = label + ": " if label is not None else "" + H_trans = {H(i): i for i in range(10)} + H_trans[None] = -1 # key == None appears in commits + tag_trans = {TAG_PUT2: "put2", TAG_PUT: "put", TAG_DELETE: "del", TAG_COMMIT: "comm"} + for segment, fn in repository.io.segment_iterator(): + for tag, key, offset, size, _ in repository.io.iter_objects(segment): + print("%s%s H(%d) -> %s[%d..+%d]" % (label, tag_trans[tag], H_trans[key], fn, offset, size)) + print() + + +def test_basic_operations(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + for x in range(100): + repository.put(H(x), fchunk(b"SOMEDATA")) + key50 = H(50) + assert pdchunk(repository.get(key50)) == b"SOMEDATA" + repository.delete(key50) + with pytest.raises(LegacyRepository.ObjectNotFound): + repository.get(key50) + repository.commit(compact=False) + with reopen(repository) as repository: + with pytest.raises(LegacyRepository.ObjectNotFound): + repository.get(key50) + for x in range(100): + if x == 50: + continue + assert pdchunk(repository.get(H(x))) == b"SOMEDATA" + + +def test_multiple_transactions(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repository.put(H(0), fchunk(b"foo")) + repository.put(H(1), fchunk(b"foo")) + repository.commit(compact=False) + repository.delete(H(0)) + repository.put(H(1), fchunk(b"bar")) + repository.commit(compact=False) + assert pdchunk(repository.get(H(1))) == b"bar" + + +def test_read_data(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + meta, data = b"meta", b"data" + hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data)) + chunk_complete = hdr + meta + data + chunk_short = hdr + meta + repository.put(H(0), chunk_complete) + repository.commit(compact=False) + assert repository.get(H(0)) == chunk_complete + assert repository.get(H(0), read_data=True) == chunk_complete + assert repository.get(H(0), read_data=False) == chunk_short + + +def test_consistency(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repository.put(H(0), fchunk(b"foo")) + assert pdchunk(repository.get(H(0))) == b"foo" + repository.put(H(0), fchunk(b"foo2")) + assert pdchunk(repository.get(H(0))) == b"foo2" + repository.put(H(0), fchunk(b"bar")) + assert pdchunk(repository.get(H(0))) == b"bar" + repository.delete(H(0)) + with pytest.raises(LegacyRepository.ObjectNotFound): + repository.get(H(0)) + + +def test_consistency2(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repository.put(H(0), fchunk(b"foo")) + assert pdchunk(repository.get(H(0))) == b"foo" + repository.commit(compact=False) + repository.put(H(0), fchunk(b"foo2")) + assert pdchunk(repository.get(H(0))) == b"foo2" + repository.rollback() + assert pdchunk(repository.get(H(0))) == b"foo" + + +def test_overwrite_in_same_transaction(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repository.put(H(0), fchunk(b"foo")) + repository.put(H(0), fchunk(b"foo2")) + repository.commit(compact=False) + assert pdchunk(repository.get(H(0))) == b"foo2" + + +def test_single_kind_transactions(repo_fixtures, request): + # put + with get_repository_from_fixture(repo_fixtures, request) as repository: + repository.put(H(0), fchunk(b"foo")) + repository.commit(compact=False) + # replace + with reopen(repository) as repository: + repository.put(H(0), fchunk(b"bar")) + repository.commit(compact=False) + # delete + with reopen(repository) as repository: + repository.delete(H(0)) + repository.commit(compact=False) + + +def test_list(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + for x in range(100): + repository.put(H(x), fchunk(b"SOMEDATA")) + repository.commit(compact=False) + repo_list = repository.list() + assert len(repo_list) == 100 + first_half = repository.list(limit=50) + assert len(first_half) == 50 + assert first_half == repo_list[:50] + second_half = repository.list(marker=first_half[-1]) + assert len(second_half) == 50 + assert second_half == repo_list[50:] + assert len(repository.list(limit=50)) == 50 + + +def test_max_data_size(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + max_data = b"x" * (MAX_DATA_SIZE - RepoObj.obj_header.size) + repository.put(H(0), fchunk(max_data)) + assert pdchunk(repository.get(H(0))) == max_data + with pytest.raises(IntegrityError): + repository.put(H(1), fchunk(max_data + b"x")) + + +def _assert_sparse(repository): + # the superseded 123456... PUT + assert repository.compact[0] == 41 + 8 + len(fchunk(b"123456789")) + # a COMMIT + assert repository.compact[1] == 9 + # the DELETE issued by the superseding PUT (or issued directly) + assert repository.compact[2] == 41 + repository._rebuild_sparse(0) + assert repository.compact[0] == 41 + 8 + len(fchunk(b"123456789")) # 9 is chunk or commit? + + +def test_sparse1(repository): + with repository: + repository.put(H(0), fchunk(b"foo")) + repository.put(H(1), fchunk(b"123456789")) + repository.commit(compact=False) + repository.put(H(1), fchunk(b"bar")) + _assert_sparse(repository) + + +def test_sparse2(repository): + with repository: + repository.put(H(0), fchunk(b"foo")) + repository.put(H(1), fchunk(b"123456789")) + repository.commit(compact=False) + repository.delete(H(1)) + _assert_sparse(repository) + + +def test_sparse_delete(repository): + with repository: + chunk0 = fchunk(b"1245") + repository.put(H(0), chunk0) + repository.delete(H(0)) + repository.io._write_fd.sync() + # the on-line tracking works on a per-object basis... + assert repository.compact[0] == 41 + 8 + 41 + len(chunk0) + repository._rebuild_sparse(0) + # ...while _rebuild_sparse can mark whole segments as completely sparse (which then includes the segment magic) + assert repository.compact[0] == 41 + 8 + 41 + len(chunk0) + len(MAGIC) + repository.commit(compact=True) + assert 0 not in [segment for segment, _ in repository.io.segment_iterator()] + + +def test_uncommitted_garbage(repository): + with repository: + # uncommitted garbage should be no problem, it is cleaned up automatically. + # we just have to be careful with invalidation of cached FDs in LoggedIO. + repository.put(H(0), fchunk(b"foo")) + repository.commit(compact=False) + # write some crap to an uncommitted segment file + last_segment = repository.io.get_latest_segment() + with open(repository.io.segment_filename(last_segment + 1), "wb") as f: + f.write(MAGIC + b"crapcrapcrap") + with reopen(repository) as repository: + # usually, opening the repo and starting a transaction should trigger a cleanup. + repository.put(H(0), fchunk(b"bar")) # this may trigger compact_segments() + repository.commit(compact=True) + # the point here is that nothing blows up with an exception. + + +def test_replay_of_missing_index(repository): + with repository: + add_keys(repository) + for name in os.listdir(repository.path): + if name.startswith("index."): + os.unlink(os.path.join(repository.path, name)) + with reopen(repository) as repository: + assert len(repository) == 3 + assert repository.check() is True + + +def test_crash_before_compact_segments(repository): + with repository: + add_keys(repository) + repository.compact_segments = None + try: + repository.commit(compact=True) + except TypeError: + pass + with reopen(repository) as repository: + assert len(repository) == 3 + assert repository.check() is True + + +def test_crash_before_write_index(repository): + with repository: + add_keys(repository) + repository.write_index = None + try: + repository.commit(compact=False) + except TypeError: + pass + with reopen(repository) as repository: + assert len(repository) == 3 + assert repository.check() is True + + +def test_replay_lock_upgrade_old(repository): + with repository: + add_keys(repository) + for name in os.listdir(repository.path): + if name.startswith("index."): + os.unlink(os.path.join(repository.path, name)) + with patch.object(Lock, "upgrade", side_effect=LockFailed) as upgrade: + with reopen(repository, exclusive=None) as repository: + # simulate old client that always does lock upgrades + # the repo is only locked by a shared read lock, but to replay segments, + # we need an exclusive write lock - check if the lock gets upgraded. + with pytest.raises(LockFailed): + len(repository) + upgrade.assert_called_once_with() + + +def test_replay_lock_upgrade(repository): + with repository: + add_keys(repository) + for name in os.listdir(repository.path): + if name.startswith("index."): + os.unlink(os.path.join(repository.path, name)) + with patch.object(Lock, "upgrade", side_effect=LockFailed) as upgrade: + with reopen(repository, exclusive=False) as repository: + # current client usually does not do lock upgrade, except for replay + # the repo is only locked by a shared read lock, but to replay segments, + # we need an exclusive write lock - check if the lock gets upgraded. + with pytest.raises(LockFailed): + len(repository) + upgrade.assert_called_once_with() + + +def test_crash_before_deleting_compacted_segments(repository): + with repository: + add_keys(repository) + repository.io.delete_segment = None + try: + repository.commit(compact=False) + except TypeError: + pass + with reopen(repository) as repository: + assert len(repository) == 3 + assert repository.check() is True + assert len(repository) == 3 + + +def test_ignores_commit_tag_in_data(repository): + with repository: + repository.put(H(0), LoggedIO.COMMIT) + with reopen(repository) as repository: + io = repository.io + assert not io.is_committed_segment(io.get_latest_segment()) + + +def test_moved_deletes_are_tracked(repository): + with repository: + repository.put(H(1), fchunk(b"1")) + repository.put(H(2), fchunk(b"2")) + repository.commit(compact=False) + repo_dump(repository, "p1 p2 c") + repository.delete(H(1)) + repository.commit(compact=True) + repo_dump(repository, "d1 cc") + last_segment = repository.io.get_latest_segment() - 1 + num_deletes = 0 + for tag, key, offset, size, _ in repository.io.iter_objects(last_segment): + if tag == TAG_DELETE: + assert key == H(1) + num_deletes += 1 + assert num_deletes == 1 + assert last_segment in repository.compact + repository.put(H(3), fchunk(b"3")) + repository.commit(compact=True) + repo_dump(repository, "p3 cc") + assert last_segment not in repository.compact + assert not repository.io.segment_exists(last_segment) + for segment, _ in repository.io.segment_iterator(): + for tag, key, offset, size, _ in repository.io.iter_objects(segment): + assert tag != TAG_DELETE + assert key != H(1) + # after compaction, there should be no empty shadowed_segments lists left over. + # we have no put or del anymore for H(1), so we lost knowledge about H(1). + assert H(1) not in repository.shadow_index + + +def test_shadowed_entries_are_preserved1(repository): + # this tests the shadowing-by-del behaviour + with repository: + get_latest_segment = repository.io.get_latest_segment + repository.put(H(1), fchunk(b"1")) + # This is the segment with our original PUT of interest + put_segment = get_latest_segment() + repository.commit(compact=False) + # we now delete H(1), and force this segment not to be compacted, which can happen + # if it's not sparse enough (symbolized by H(2) here). + repository.delete(H(1)) + repository.put(H(2), fchunk(b"1")) + del_segment = get_latest_segment() + # we pretend these are mostly dense (not sparse) and won't be compacted + del repository.compact[put_segment] + del repository.compact[del_segment] + repository.commit(compact=True) + # we now perform an unrelated operation on the segment containing the DELETE, + # causing it to be compacted. + repository.delete(H(2)) + repository.commit(compact=True) + assert repository.io.segment_exists(put_segment) + assert not repository.io.segment_exists(del_segment) + # basic case, since the index survived this must be ok + assert H(1) not in repository + # nuke index, force replay + os.unlink(os.path.join(repository.path, "index.%d" % get_latest_segment())) + # must not reappear + assert H(1) not in repository + + +def test_shadowed_entries_are_preserved2(repository): + # this tests the shadowing-by-double-put behaviour, see issue #5661 + # assume this repo state: + # seg1: PUT H1 + # seg2: COMMIT + # seg3: DEL H1, PUT H1, DEL H1, PUT H2 + # seg4: COMMIT + # Note how due to the final DEL H1 in seg3, H1 is effectively deleted. + # + # compaction of only seg3: + # PUT H1 gets dropped because it is not needed any more. + # DEL H1 must be kept, because there is still a PUT H1 in seg1 which must not + # "reappear" in the index if the index gets rebuilt. + with repository: + get_latest_segment = repository.io.get_latest_segment + repository.put(H(1), fchunk(b"1")) + # This is the segment with our original PUT of interest + put_segment = get_latest_segment() + repository.commit(compact=False) + # We now put H(1) again (which implicitly does DEL(H(1)) followed by PUT(H(1), ...)), + # delete H(1) afterwards, and force this segment to not be compacted, which can happen + # if it's not sparse enough (symbolized by H(2) here). + repository.put(H(1), fchunk(b"1")) + repository.delete(H(1)) + repository.put(H(2), fchunk(b"1")) + delete_segment = get_latest_segment() + # We pretend these are mostly dense (not sparse) and won't be compacted + del repository.compact[put_segment] + del repository.compact[delete_segment] + repository.commit(compact=True) + # Now we perform an unrelated operation on the segment containing the DELETE, + # causing it to be compacted. + repository.delete(H(2)) + repository.commit(compact=True) + assert repository.io.segment_exists(put_segment) + assert not repository.io.segment_exists(delete_segment) + # Basic case, since the index survived this must be ok + assert H(1) not in repository + # Nuke index, force replay + os.unlink(os.path.join(repository.path, "index.%d" % get_latest_segment())) + # Must not reappear + assert H(1) not in repository # F + + +def test_shadow_index_rollback(repository): + with repository: + repository.put(H(1), fchunk(b"1")) + repository.delete(H(1)) + assert repository.shadow_index[H(1)] == [0] + repository.commit(compact=True) + repo_dump(repository, "p1 d1 cc") + # note how an empty list means that nothing is shadowed for sure + assert repository.shadow_index[H(1)] == [] # because the deletion is considered unstable + repository.put(H(1), b"1") + repository.delete(H(1)) + repo_dump(repository, "p1 d1") + # 0 put/delete; 1 commit; 2 compacted; 3 commit; 4 put/delete + assert repository.shadow_index[H(1)] == [4] + repository.rollback() + repo_dump(repository, "r") + repository.put(H(2), fchunk(b"1")) + # after the rollback, segment 4 shouldn't be considered anymore + assert repository.shadow_index[H(1)] == [] # because the deletion is considered unstable + + +def test_destroy_append_only(repository): + with repository: + # can't destroy append only repo (via the API) + repository.append_only = True + with pytest.raises(ValueError): + repository.destroy() + assert repository.append_only + + +def test_append_only(repository): + def segments_in_repository(repo): + return len(list(repo.io.segment_iterator())) + + with repository: + repository.append_only = True + repository.put(H(0), fchunk(b"foo")) + repository.commit(compact=False) + + repository.append_only = False + assert segments_in_repository(repository) == 2 + repository.put(H(0), fchunk(b"foo")) + repository.commit(compact=True) + # normal: compact squashes the data together, only one segment + assert segments_in_repository(repository) == 2 + + repository.append_only = True + assert segments_in_repository(repository) == 2 + repository.put(H(0), fchunk(b"foo")) + repository.commit(compact=False) + # append only: does not compact, only new segments written + assert segments_in_repository(repository) == 4 + + +def test_additional_free_space(repository): + with repository: + add_keys(repository) + repository.config.set("repository", "additional_free_space", "1000T") + repository.save_key(b"shortcut to save_config") + with reopen(repository) as repository: + repository.put(H(0), fchunk(b"foobar")) + with pytest.raises(LegacyRepository.InsufficientFreeSpaceError): + repository.commit(compact=False) + assert os.path.exists(repository.path) + + +def test_create_free_space(repository): + with repository: + repository.additional_free_space = 1e20 + with pytest.raises(LegacyRepository.InsufficientFreeSpaceError): + add_keys(repository) + assert not os.path.exists(repository.path) + + +def test_tracking(repository): + with repository: + assert repository.storage_quota_use == 0 + ch1 = fchunk(bytes(1234)) + repository.put(H(1), ch1) + assert repository.storage_quota_use == len(ch1) + 41 + 8 + ch2 = fchunk(bytes(5678)) + repository.put(H(2), ch2) + assert repository.storage_quota_use == len(ch1) + len(ch2) + 2 * (41 + 8) + repository.delete(H(1)) + assert repository.storage_quota_use == len(ch1) + len(ch2) + 2 * (41 + 8) # we have not compacted yet + repository.commit(compact=False) + assert repository.storage_quota_use == len(ch1) + len(ch2) + 2 * (41 + 8) # we have not compacted yet + with reopen(repository) as repository: + # open new transaction; hints and thus quota data is not loaded unless needed. + ch3 = fchunk(b"") + repository.put(H(3), ch3) + repository.delete(H(3)) + assert repository.storage_quota_use == len(ch1) + len(ch2) + len(ch3) + 3 * ( + 41 + 8 + ) # we have not compacted yet + repository.commit(compact=True) + assert repository.storage_quota_use == len(ch2) + 41 + 8 + + +def test_exceed_quota(repository): + with repository: + assert repository.storage_quota_use == 0 + repository.storage_quota = 80 + ch1 = fchunk(b"x" * 7) + repository.put(H(1), ch1) + assert repository.storage_quota_use == len(ch1) + 41 + 8 + repository.commit(compact=False) + with pytest.raises(LegacyRepository.StorageQuotaExceeded): + ch2 = fchunk(b"y" * 13) + repository.put(H(2), ch2) + assert repository.storage_quota_use == len(ch1) + len(ch2) + (41 + 8) * 2 # check ch2!? + with pytest.raises(LegacyRepository.StorageQuotaExceeded): + repository.commit(compact=False) + assert repository.storage_quota_use == len(ch1) + len(ch2) + (41 + 8) * 2 # check ch2!? + with reopen(repository) as repository: + repository.storage_quota = 161 + # open new transaction; hints and thus quota data is not loaded unless needed. + repository.put(H(1), ch1) + # we have 2 puts for H(1) here and not yet compacted. + assert repository.storage_quota_use == len(ch1) * 2 + (41 + 8) * 2 + repository.commit(compact=True) + assert repository.storage_quota_use == len(ch1) + 41 + 8 # now we have compacted. + + +def make_auxiliary(repository): + with repository: + repository.put(H(0), fchunk(b"foo")) + repository.commit(compact=False) + + +def do_commit(repository): + with repository: + repository.put(H(0), fchunk(b"fox")) + repository.commit(compact=False) + + +def test_corrupted_hints(repository): + make_auxiliary(repository) + with open(os.path.join(repository.path, "hints.1"), "ab") as fd: + fd.write(b"123456789") + do_commit(repository) + + +def test_deleted_hints(repository): + make_auxiliary(repository) + os.unlink(os.path.join(repository.path, "hints.1")) + do_commit(repository) + + +def test_deleted_index(repository): + make_auxiliary(repository) + os.unlink(os.path.join(repository.path, "index.1")) + do_commit(repository) + + +def test_unreadable_hints(repository): + make_auxiliary(repository) + hints = os.path.join(repository.path, "hints.1") + os.unlink(hints) + os.mkdir(hints) + with pytest.raises(OSError): + do_commit(repository) + + +def test_index(repository): + make_auxiliary(repository) + with open(os.path.join(repository.path, "index.1"), "wb") as fd: + fd.write(b"123456789") + do_commit(repository) + + +def test_index_outside_transaction(repository): + make_auxiliary(repository) + with open(os.path.join(repository.path, "index.1"), "wb") as fd: + fd.write(b"123456789") + with repository: + assert len(repository) == 1 + + +def _corrupt_index(repository): + # HashIndex is able to detect incorrect headers and file lengths, + # but on its own it can't tell if the data is correct. + index_path = os.path.join(repository.path, "index.1") + with open(index_path, "r+b") as fd: + index_data = fd.read() + # Flip one bit in a key stored in the index + corrupted_key = (int.from_bytes(H(0), "little") ^ 1).to_bytes(32, "little") + corrupted_index_data = index_data.replace(H(0), corrupted_key) + assert corrupted_index_data != index_data + assert len(corrupted_index_data) == len(index_data) + fd.seek(0) + fd.write(corrupted_index_data) + + +def test_index_corrupted(repository): + make_auxiliary(repository) + _corrupt_index(repository) + with repository: + # data corruption is detected due to mismatching checksums, and fixed by rebuilding the index. + assert len(repository) == 1 + assert pdchunk(repository.get(H(0))) == b"foo" + + +def test_index_corrupted_without_integrity(repository): + make_auxiliary(repository) + _corrupt_index(repository) + integrity_path = os.path.join(repository.path, "integrity.1") + os.unlink(integrity_path) + with repository: + # since the corrupted key is not noticed, the repository still thinks it contains one key... + assert len(repository) == 1 + with pytest.raises(LegacyRepository.ObjectNotFound): + # ... but the real, uncorrupted key is not found in the corrupted index. + repository.get(H(0)) + + +def test_unreadable_index(repository): + make_auxiliary(repository) + index = os.path.join(repository.path, "index.1") + os.unlink(index) + os.mkdir(index) + with pytest.raises(OSError): + do_commit(repository) + + +def test_unknown_integrity_version(repository): + make_auxiliary(repository) + # for now an unknown integrity data version is ignored and not an error. + integrity_path = os.path.join(repository.path, "integrity.1") + with open(integrity_path, "r+b") as fd: + msgpack.pack({b"version": 4.7}, fd) # borg only understands version 2 + fd.truncate() + with repository: + # no issues accessing the repository + assert len(repository) == 1 + assert pdchunk(repository.get(H(0))) == b"foo" + + +def _subtly_corrupted_hints_setup(repository): + with repository: + repository.append_only = True + assert len(repository) == 1 + assert pdchunk(repository.get(H(0))) == b"foo" + repository.put(H(1), fchunk(b"bar")) + repository.put(H(2), fchunk(b"baz")) + repository.commit(compact=False) + repository.put(H(2), fchunk(b"bazz")) + repository.commit(compact=False) + hints_path = os.path.join(repository.path, "hints.5") + with open(hints_path, "r+b") as fd: + hints = msgpack.unpack(fd) + fd.seek(0) + # corrupt segment refcount + assert hints["segments"][2] == 1 + hints["segments"][2] = 0 + msgpack.pack(hints, fd) + fd.truncate() + + +def test_subtly_corrupted_hints(repository): + make_auxiliary(repository) + _subtly_corrupted_hints_setup(repository) + with repository: + repository.append_only = False + repository.put(H(3), fchunk(b"1234")) + # do a compaction run, which succeeds since the failed checksum prompted a rebuild of the index+hints. + repository.commit(compact=True) + assert len(repository) == 4 + assert pdchunk(repository.get(H(0))) == b"foo" + assert pdchunk(repository.get(H(1))) == b"bar" + assert pdchunk(repository.get(H(2))) == b"bazz" + + +def test_subtly_corrupted_hints_without_integrity(repository): + make_auxiliary(repository) + _subtly_corrupted_hints_setup(repository) + integrity_path = os.path.join(repository.path, "integrity.5") + os.unlink(integrity_path) + with repository: + repository.append_only = False + repository.put(H(3), fchunk(b"1234")) + # do a compaction run, which fails since the corrupted refcount wasn't detected and causes an assertion failure. + with pytest.raises(AssertionError) as exc_info: + repository.commit(compact=True) + assert "Corrupted segment reference count" in str(exc_info.value) + + +def list_indices(repo_path): + return [name for name in os.listdir(repo_path) if name.startswith("index.")] + + +def check(repository, repo_path, repair=False, status=True): + assert repository.check(repair=repair) == status + # Make sure no tmp files are left behind + tmp_files = [name for name in os.listdir(repo_path) if "tmp" in name] + assert tmp_files == [], "Found tmp files" + + +def get_objects(repository, *ids): + for id_ in ids: + pdchunk(repository.get(H(id_))) + + +def add_objects(repository, segments): + for ids in segments: + for id_ in ids: + repository.put(H(id_), fchunk(b"data")) + repository.commit(compact=False) + + +def get_head(repo_path): + return sorted(int(n) for n in os.listdir(os.path.join(repo_path, "data", "0")) if n.isdigit())[-1] + + +def open_index(repo_path): + return NSIndex.read(os.path.join(repo_path, f"index.{get_head(repo_path)}")) + + +def corrupt_object(repo_path, id_): + idx = open_index(repo_path) + segment, offset, _ = idx[H(id_)] + with open(os.path.join(repo_path, "data", "0", str(segment)), "r+b") as fd: + fd.seek(offset) + fd.write(b"BOOM") + + +def delete_segment(repository, segment): + repository.io.delete_segment(segment) + + +def delete_index(repo_path): + os.unlink(os.path.join(repo_path, f"index.{get_head(repo_path)}")) + + +def rename_index(repo_path, new_name): + os.replace(os.path.join(repo_path, f"index.{get_head(repo_path)}"), os.path.join(repo_path, new_name)) + + +def list_objects(repository): + return {int(key) for key in repository.list()} + + +def test_repair_corrupted_segment(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repo_path = get_path(repository) + add_objects(repository, [[1, 2, 3], [4, 5], [6]]) + assert {1, 2, 3, 4, 5, 6} == list_objects(repository) + check(repository, repo_path, status=True) + corrupt_object(repo_path, 5) + with pytest.raises(IntegrityError): + get_objects(repository, 5) + repository.rollback() + # make sure a regular check does not repair anything + check(repository, repo_path, status=False) + check(repository, repo_path, status=False) + # make sure a repair actually repairs the repo + check(repository, repo_path, repair=True, status=True) + get_objects(repository, 4) + check(repository, repo_path, status=True) + assert {1, 2, 3, 4, 6} == list_objects(repository) + + +def test_repair_missing_segment(repository): + # only test on local repo - files in RemoteRepository cannot be deleted + with repository: + add_objects(repository, [[1, 2, 3], [4, 5, 6]]) + assert {1, 2, 3, 4, 5, 6} == list_objects(repository) + check(repository, repository.path, status=True) + delete_segment(repository, 2) + repository.rollback() + check(repository, repository.path, repair=True, status=True) + assert {1, 2, 3} == list_objects(repository) + + +def test_repair_missing_commit_segment(repository): + # only test on local repo - files in RemoteRepository cannot be deleted + with repository: + add_objects(repository, [[1, 2, 3], [4, 5, 6]]) + delete_segment(repository, 3) + with pytest.raises(LegacyRepository.ObjectNotFound): + get_objects(repository, 4) + assert {1, 2, 3} == list_objects(repository) + + +def test_repair_corrupted_commit_segment(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repo_path = get_path(repository) + add_objects(repository, [[1, 2, 3], [4, 5, 6]]) + with open(os.path.join(repo_path, "data", "0", "3"), "r+b") as fd: + fd.seek(-1, os.SEEK_END) + fd.write(b"X") + with pytest.raises(LegacyRepository.ObjectNotFound): + get_objects(repository, 4) + check(repository, repo_path, status=True) + get_objects(repository, 3) + assert {1, 2, 3} == list_objects(repository) + + +def test_repair_no_commits(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repo_path = get_path(repository) + add_objects(repository, [[1, 2, 3]]) + with open(os.path.join(repo_path, "data", "0", "1"), "r+b") as fd: + fd.seek(-1, os.SEEK_END) + fd.write(b"X") + with pytest.raises(LegacyRepository.CheckNeeded): + get_objects(repository, 4) + check(repository, repo_path, status=False) + check(repository, repo_path, status=False) + assert list_indices(repo_path) == ["index.1"] + check(repository, repo_path, repair=True, status=True) + assert list_indices(repo_path) == ["index.2"] + check(repository, repo_path, status=True) + get_objects(repository, 3) + assert {1, 2, 3} == list_objects(repository) + + +def test_repair_missing_index(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repo_path = get_path(repository) + add_objects(repository, [[1, 2, 3], [4, 5, 6]]) + delete_index(repo_path) + check(repository, repo_path, status=True) + get_objects(repository, 4) + assert {1, 2, 3, 4, 5, 6} == list_objects(repository) + + +def test_repair_index_too_new(repo_fixtures, request): + with get_repository_from_fixture(repo_fixtures, request) as repository: + repo_path = get_path(repository) + add_objects(repository, [[1, 2, 3], [4, 5, 6]]) + assert list_indices(repo_path) == ["index.3"] + rename_index(repo_path, "index.100") + check(repository, repo_path, status=True) + assert list_indices(repo_path) == ["index.3"] + get_objects(repository, 4) + assert {1, 2, 3, 4, 5, 6} == list_objects(repository) + + +def test_crash_before_compact(repository): + # only test on local repo - we can't mock-patch a RemoteRepository class in another process! + with repository: + repository.put(H(0), fchunk(b"data")) + repository.put(H(0), fchunk(b"data2")) + # simulate a crash before compact + with patch.object(LegacyRepository, "compact_segments") as compact: + repository.commit(compact=True) + compact.assert_called_once_with(0.1) + with reopen(repository) as repository: + check(repository, repository.path, repair=True) + assert pdchunk(repository.get(H(0))) == b"data2" + + +def test_hints_persistence(repository): + with repository: + repository.put(H(0), fchunk(b"data")) + repository.delete(H(0)) + repository.commit(compact=False) + shadow_index_expected = repository.shadow_index + compact_expected = repository.compact + segments_expected = repository.segments + # close and re-open the repository (create fresh Repository instance) to + # check whether hints were persisted to / reloaded from disk + with reopen(repository) as repository: + repository.put(H(42), fchunk(b"foobar")) # this will call prepare_txn() and load the hints data + # check if hints persistence worked: + assert shadow_index_expected == repository.shadow_index + assert compact_expected == repository.compact + del repository.segments[2] # ignore the segment created by put(H(42), ...) + assert segments_expected == repository.segments + with reopen(repository) as repository: + check(repository, repository.path, repair=True) + with reopen(repository) as repository: + repository.put(H(42), fchunk(b"foobar")) # this will call prepare_txn() and load the hints data + assert shadow_index_expected == repository.shadow_index + # sizes do not match, with vs. without header? + # assert compact_expected == repository.compact + del repository.segments[2] # ignore the segment created by put(H(42), ...) + assert segments_expected == repository.segments + + +def test_hints_behaviour(repository): + with repository: + repository.put(H(0), fchunk(b"data")) + assert repository.shadow_index == {} + assert len(repository.compact) == 0 + repository.delete(H(0)) + repository.commit(compact=False) + # now there should be an entry for H(0) in shadow_index + assert H(0) in repository.shadow_index + assert len(repository.shadow_index[H(0)]) == 1 + assert 0 in repository.compact # segment 0 can be compacted + repository.put(H(42), fchunk(b"foobar")) # see also do_compact() + repository.commit(compact=True, threshold=0.0) # compact completely! + # nothing to compact anymore! no info left about stuff that does not exist anymore: + assert H(0) not in repository.shadow_index + # segment 0 was compacted away, no info about it left: + assert 0 not in repository.compact + assert 0 not in repository.segments + + +def _get_mock_args(): + class MockArgs: + remote_path = "borg" + umask = 0o077 + debug_topics = [] + rsh = None + + def __contains__(self, item): + # to behave like argparse.Namespace + return hasattr(self, item) + + return MockArgs() + + +def test_remote_invalid_rpc(remote_repository): + with remote_repository: + with pytest.raises(InvalidRPCMethod): + remote_repository.call("__init__", {}) + + +def test_remote_rpc_exception_transport(remote_repository): + with remote_repository: + s1 = "test string" + + try: + remote_repository.call("inject_exception", {"kind": "DoesNotExist"}) + except LegacyRepository.DoesNotExist as e: + assert len(e.args) == 1 + assert e.args[0] == remote_repository.location.processed + + try: + remote_repository.call("inject_exception", {"kind": "AlreadyExists"}) + except LegacyRepository.AlreadyExists as e: + assert len(e.args) == 1 + assert e.args[0] == remote_repository.location.processed + + try: + remote_repository.call("inject_exception", {"kind": "CheckNeeded"}) + except LegacyRepository.CheckNeeded as e: + assert len(e.args) == 1 + assert e.args[0] == remote_repository.location.processed + + try: + remote_repository.call("inject_exception", {"kind": "IntegrityError"}) + except IntegrityError as e: + assert len(e.args) == 1 + assert e.args[0] == s1 + + try: + remote_repository.call("inject_exception", {"kind": "PathNotAllowed"}) + except PathNotAllowed as e: + assert len(e.args) == 1 + assert e.args[0] == "foo" + + try: + remote_repository.call("inject_exception", {"kind": "ObjectNotFound"}) + except LegacyRepository.ObjectNotFound as e: + assert len(e.args) == 2 + assert e.args[0] == s1 + assert e.args[1] == remote_repository.location.processed + + try: + remote_repository.call("inject_exception", {"kind": "InvalidRPCMethod"}) + except InvalidRPCMethod as e: + assert len(e.args) == 1 + assert e.args[0] == s1 + + try: + remote_repository.call("inject_exception", {"kind": "divide"}) + except LegacyRemoteRepository.RPCError as e: + assert e.unpacked + assert e.get_message() == "ZeroDivisionError: integer division or modulo by zero\n" + assert e.exception_class == "ZeroDivisionError" + assert len(e.exception_full) > 0 + + +def test_remote_ssh_cmd(remote_repository): + with remote_repository: + args = _get_mock_args() + remote_repository._args = args + assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "example.com"] + assert remote_repository.ssh_cmd(Location("ssh://user@example.com/foo")) == ["ssh", "user@example.com"] + assert remote_repository.ssh_cmd(Location("ssh://user@example.com:1234/foo")) == [ + "ssh", + "-p", + "1234", + "user@example.com", + ] + os.environ["BORG_RSH"] = "ssh --foo" + assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "--foo", "example.com"] + + +def test_remote_borg_cmd(remote_repository): + with remote_repository: + assert remote_repository.borg_cmd(None, testing=True) == [sys.executable, "-m", "borg", "serve"] + args = _get_mock_args() + # XXX without next line we get spurious test fails when using pytest-xdist, root cause unknown: + logging.getLogger().setLevel(logging.INFO) + # note: test logger is on info log level, so --info gets added automagically + assert remote_repository.borg_cmd(args, testing=False) == ["borg", "serve", "--info"] + args.remote_path = "borg-0.28.2" + assert remote_repository.borg_cmd(args, testing=False) == ["borg-0.28.2", "serve", "--info"] + args.debug_topics = ["something_client_side", "repository_compaction"] + assert remote_repository.borg_cmd(args, testing=False) == [ + "borg-0.28.2", + "serve", + "--info", + "--debug-topic=borg.debug.repository_compaction", + ] + args = _get_mock_args() + args.storage_quota = 0 + assert remote_repository.borg_cmd(args, testing=False) == ["borg", "serve", "--info"] + args.storage_quota = 314159265 + assert remote_repository.borg_cmd(args, testing=False) == [ + "borg", + "serve", + "--info", + "--storage-quota=314159265", + ] + args.rsh = "ssh -i foo" + remote_repository._args = args + assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "-i", "foo", "example.com"] diff --git a/src/borg/testsuite/platform.py b/src/borg/testsuite/platform.py index 40ea3d78c..527bb1178 100644 --- a/src/borg/testsuite/platform.py +++ b/src/borg/testsuite/platform.py @@ -8,7 +8,7 @@ from ..platform import acl_get, acl_set from ..platform import get_process_id, process_alive from . import unopened_tempfile -from .locking import free_pid # NOQA +from .fslocking import free_pid # NOQA def fakeroot_detected(): diff --git a/src/borg/testsuite/repoobj.py b/src/borg/testsuite/repoobj.py index f34fa07d0..44c364d81 100644 --- a/src/borg/testsuite/repoobj.py +++ b/src/borg/testsuite/repoobj.py @@ -3,14 +3,14 @@ from ..constants import ROBJ_FILE_STREAM, ROBJ_MANIFEST, ROBJ_ARCHIVE_META from ..crypto.key import PlaintextKey from ..helpers.errors import IntegrityError -from ..repository3 import Repository3 +from ..repository import Repository from ..repoobj import RepoObj, RepoObj1 from ..compress import LZ4 @pytest.fixture def repository(tmpdir): - return Repository3(tmpdir, create=True) + return Repository(tmpdir, create=True) @pytest.fixture diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index 3ad620bd5..c5419f147 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -2,19 +2,15 @@ import os import sys from typing import Optional -from unittest.mock import patch import pytest from ..checksums import xxh64 -from ..hashindex import NSIndex from ..helpers import Location from ..helpers import IntegrityError -from ..helpers import msgpack -from ..locking import Lock, LockFailed from ..platformflags import is_win32 from ..remote import RemoteRepository, InvalidRPCMethod, PathNotAllowed -from ..repository import Repository, LoggedIO, MAGIC, MAX_DATA_SIZE, TAG_DELETE, TAG_PUT2, TAG_PUT, TAG_COMMIT +from ..repository import Repository, MAX_DATA_SIZE from ..repoobj import RepoObj from .hashindex import H @@ -46,7 +42,7 @@ def get_repository_from_fixture(repo_fixtures, request): def reopen(repository, exclusive: Optional[bool] = True, create=False): if isinstance(repository, Repository): - if repository.io is not None or repository.lock is not None: + if repository.opened: raise RuntimeError("Repo must be closed before a reopen. Cannot support nested repository contexts.") return Repository(repository.path, exclusive=exclusive, create=create) @@ -60,20 +56,8 @@ def reopen(repository, exclusive: Optional[bool] = True, create=False): ) -def get_path(repository): - if isinstance(repository, Repository): - return repository.path - - if isinstance(repository, RemoteRepository): - return repository.location.path - - raise TypeError( - f"Invalid argument type. Expected 'Repository' or 'RemoteRepository', received '{type(repository).__name__}'." - ) - - def fchunk(data, meta=b""): - # create a raw chunk that has valid RepoObj layout, but does not use encryption or compression. + # format chunk: create a raw chunk that has valid RepoObj layout, but does not use encryption or compression. hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data)) assert isinstance(data, bytes) chunk = hdr + meta + data @@ -81,7 +65,7 @@ def fchunk(data, meta=b""): def pchunk(chunk): - # parse data and meta from a raw chunk made by fchunk + # parse chunk: parse data and meta from a raw chunk made by fchunk hdr_size = RepoObj.obj_header.size hdr = chunk[:hdr_size] meta_size, data_size = RepoObj.obj_header.unpack(hdr)[0:2] @@ -95,27 +79,6 @@ def pdchunk(chunk): return pchunk(chunk)[0] -def add_keys(repository): - repository.put(H(0), fchunk(b"foo")) - repository.put(H(1), fchunk(b"bar")) - repository.put(H(3), fchunk(b"bar")) - repository.commit(compact=False) - repository.put(H(1), fchunk(b"bar2")) - repository.put(H(2), fchunk(b"boo")) - repository.delete(H(3)) - - -def repo_dump(repository, label=None): - label = label + ": " if label is not None else "" - H_trans = {H(i): i for i in range(10)} - H_trans[None] = -1 # key == None appears in commits - tag_trans = {TAG_PUT2: "put2", TAG_PUT: "put", TAG_DELETE: "del", TAG_COMMIT: "comm"} - for segment, fn in repository.io.segment_iterator(): - for tag, key, offset, size, _ in repository.io.iter_objects(segment): - print("%s%s H(%d) -> %s[%d..+%d]" % (label, tag_trans[tag], H_trans[key], fn, offset, size)) - print() - - def test_basic_operations(repo_fixtures, request): with get_repository_from_fixture(repo_fixtures, request) as repository: for x in range(100): @@ -125,7 +88,6 @@ def test_basic_operations(repo_fixtures, request): repository.delete(key50) with pytest.raises(Repository.ObjectNotFound): repository.get(key50) - repository.commit(compact=False) with reopen(repository) as repository: with pytest.raises(Repository.ObjectNotFound): repository.get(key50) @@ -135,17 +97,6 @@ def test_basic_operations(repo_fixtures, request): assert pdchunk(repository.get(H(x))) == b"SOMEDATA" -def test_multiple_transactions(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repository.put(H(0), fchunk(b"foo")) - repository.put(H(1), fchunk(b"foo")) - repository.commit(compact=False) - repository.delete(H(0)) - repository.put(H(1), fchunk(b"bar")) - repository.commit(compact=False) - assert pdchunk(repository.get(H(1))) == b"bar" - - def test_read_data(repo_fixtures, request): with get_repository_from_fixture(repo_fixtures, request) as repository: meta, data = b"meta", b"data" @@ -153,7 +104,6 @@ def test_read_data(repo_fixtures, request): chunk_complete = hdr + meta + data chunk_short = hdr + meta repository.put(H(0), chunk_complete) - repository.commit(compact=False) assert repository.get(H(0)) == chunk_complete assert repository.get(H(0), read_data=True) == chunk_complete assert repository.get(H(0), read_data=False) == chunk_short @@ -172,45 +122,10 @@ def test_consistency(repo_fixtures, request): repository.get(H(0)) -def test_consistency2(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repository.put(H(0), fchunk(b"foo")) - assert pdchunk(repository.get(H(0))) == b"foo" - repository.commit(compact=False) - repository.put(H(0), fchunk(b"foo2")) - assert pdchunk(repository.get(H(0))) == b"foo2" - repository.rollback() - assert pdchunk(repository.get(H(0))) == b"foo" - - -def test_overwrite_in_same_transaction(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repository.put(H(0), fchunk(b"foo")) - repository.put(H(0), fchunk(b"foo2")) - repository.commit(compact=False) - assert pdchunk(repository.get(H(0))) == b"foo2" - - -def test_single_kind_transactions(repo_fixtures, request): - # put - with get_repository_from_fixture(repo_fixtures, request) as repository: - repository.put(H(0), fchunk(b"foo")) - repository.commit(compact=False) - # replace - with reopen(repository) as repository: - repository.put(H(0), fchunk(b"bar")) - repository.commit(compact=False) - # delete - with reopen(repository) as repository: - repository.delete(H(0)) - repository.commit(compact=False) - - def test_list(repo_fixtures, request): with get_repository_from_fixture(repo_fixtures, request) as repository: for x in range(100): repository.put(H(x), fchunk(b"SOMEDATA")) - repository.commit(compact=False) repo_list = repository.list() assert len(repo_list) == 100 first_half = repository.list(limit=50) @@ -231,555 +146,6 @@ def test_max_data_size(repo_fixtures, request): repository.put(H(1), fchunk(max_data + b"x")) -def _assert_sparse(repository): - # the superseded 123456... PUT - assert repository.compact[0] == 41 + 8 + len(fchunk(b"123456789")) - # a COMMIT - assert repository.compact[1] == 9 - # the DELETE issued by the superseding PUT (or issued directly) - assert repository.compact[2] == 41 - repository._rebuild_sparse(0) - assert repository.compact[0] == 41 + 8 + len(fchunk(b"123456789")) # 9 is chunk or commit? - - -def test_sparse1(repository): - with repository: - repository.put(H(0), fchunk(b"foo")) - repository.put(H(1), fchunk(b"123456789")) - repository.commit(compact=False) - repository.put(H(1), fchunk(b"bar")) - _assert_sparse(repository) - - -def test_sparse2(repository): - with repository: - repository.put(H(0), fchunk(b"foo")) - repository.put(H(1), fchunk(b"123456789")) - repository.commit(compact=False) - repository.delete(H(1)) - _assert_sparse(repository) - - -def test_sparse_delete(repository): - with repository: - chunk0 = fchunk(b"1245") - repository.put(H(0), chunk0) - repository.delete(H(0)) - repository.io._write_fd.sync() - # the on-line tracking works on a per-object basis... - assert repository.compact[0] == 41 + 8 + 41 + len(chunk0) - repository._rebuild_sparse(0) - # ...while _rebuild_sparse can mark whole segments as completely sparse (which then includes the segment magic) - assert repository.compact[0] == 41 + 8 + 41 + len(chunk0) + len(MAGIC) - repository.commit(compact=True) - assert 0 not in [segment for segment, _ in repository.io.segment_iterator()] - - -def test_uncommitted_garbage(repository): - with repository: - # uncommitted garbage should be no problem, it is cleaned up automatically. - # we just have to be careful with invalidation of cached FDs in LoggedIO. - repository.put(H(0), fchunk(b"foo")) - repository.commit(compact=False) - # write some crap to an uncommitted segment file - last_segment = repository.io.get_latest_segment() - with open(repository.io.segment_filename(last_segment + 1), "wb") as f: - f.write(MAGIC + b"crapcrapcrap") - with reopen(repository) as repository: - # usually, opening the repo and starting a transaction should trigger a cleanup. - repository.put(H(0), fchunk(b"bar")) # this may trigger compact_segments() - repository.commit(compact=True) - # the point here is that nothing blows up with an exception. - - -def test_replay_of_missing_index(repository): - with repository: - add_keys(repository) - for name in os.listdir(repository.path): - if name.startswith("index."): - os.unlink(os.path.join(repository.path, name)) - with reopen(repository) as repository: - assert len(repository) == 3 - assert repository.check() is True - - -def test_crash_before_compact_segments(repository): - with repository: - add_keys(repository) - repository.compact_segments = None - try: - repository.commit(compact=True) - except TypeError: - pass - with reopen(repository) as repository: - assert len(repository) == 3 - assert repository.check() is True - - -def test_crash_before_write_index(repository): - with repository: - add_keys(repository) - repository.write_index = None - try: - repository.commit(compact=False) - except TypeError: - pass - with reopen(repository) as repository: - assert len(repository) == 3 - assert repository.check() is True - - -def test_replay_lock_upgrade_old(repository): - with repository: - add_keys(repository) - for name in os.listdir(repository.path): - if name.startswith("index."): - os.unlink(os.path.join(repository.path, name)) - with patch.object(Lock, "upgrade", side_effect=LockFailed) as upgrade: - with reopen(repository, exclusive=None) as repository: - # simulate old client that always does lock upgrades - # the repo is only locked by a shared read lock, but to replay segments, - # we need an exclusive write lock - check if the lock gets upgraded. - with pytest.raises(LockFailed): - len(repository) - upgrade.assert_called_once_with() - - -def test_replay_lock_upgrade(repository): - with repository: - add_keys(repository) - for name in os.listdir(repository.path): - if name.startswith("index."): - os.unlink(os.path.join(repository.path, name)) - with patch.object(Lock, "upgrade", side_effect=LockFailed) as upgrade: - with reopen(repository, exclusive=False) as repository: - # current client usually does not do lock upgrade, except for replay - # the repo is only locked by a shared read lock, but to replay segments, - # we need an exclusive write lock - check if the lock gets upgraded. - with pytest.raises(LockFailed): - len(repository) - upgrade.assert_called_once_with() - - -def test_crash_before_deleting_compacted_segments(repository): - with repository: - add_keys(repository) - repository.io.delete_segment = None - try: - repository.commit(compact=False) - except TypeError: - pass - with reopen(repository) as repository: - assert len(repository) == 3 - assert repository.check() is True - assert len(repository) == 3 - - -def test_ignores_commit_tag_in_data(repository): - with repository: - repository.put(H(0), LoggedIO.COMMIT) - with reopen(repository) as repository: - io = repository.io - assert not io.is_committed_segment(io.get_latest_segment()) - - -def test_moved_deletes_are_tracked(repository): - with repository: - repository.put(H(1), fchunk(b"1")) - repository.put(H(2), fchunk(b"2")) - repository.commit(compact=False) - repo_dump(repository, "p1 p2 c") - repository.delete(H(1)) - repository.commit(compact=True) - repo_dump(repository, "d1 cc") - last_segment = repository.io.get_latest_segment() - 1 - num_deletes = 0 - for tag, key, offset, size, _ in repository.io.iter_objects(last_segment): - if tag == TAG_DELETE: - assert key == H(1) - num_deletes += 1 - assert num_deletes == 1 - assert last_segment in repository.compact - repository.put(H(3), fchunk(b"3")) - repository.commit(compact=True) - repo_dump(repository, "p3 cc") - assert last_segment not in repository.compact - assert not repository.io.segment_exists(last_segment) - for segment, _ in repository.io.segment_iterator(): - for tag, key, offset, size, _ in repository.io.iter_objects(segment): - assert tag != TAG_DELETE - assert key != H(1) - # after compaction, there should be no empty shadowed_segments lists left over. - # we have no put or del anymore for H(1), so we lost knowledge about H(1). - assert H(1) not in repository.shadow_index - - -def test_shadowed_entries_are_preserved1(repository): - # this tests the shadowing-by-del behaviour - with repository: - get_latest_segment = repository.io.get_latest_segment - repository.put(H(1), fchunk(b"1")) - # This is the segment with our original PUT of interest - put_segment = get_latest_segment() - repository.commit(compact=False) - # we now delete H(1), and force this segment not to be compacted, which can happen - # if it's not sparse enough (symbolized by H(2) here). - repository.delete(H(1)) - repository.put(H(2), fchunk(b"1")) - del_segment = get_latest_segment() - # we pretend these are mostly dense (not sparse) and won't be compacted - del repository.compact[put_segment] - del repository.compact[del_segment] - repository.commit(compact=True) - # we now perform an unrelated operation on the segment containing the DELETE, - # causing it to be compacted. - repository.delete(H(2)) - repository.commit(compact=True) - assert repository.io.segment_exists(put_segment) - assert not repository.io.segment_exists(del_segment) - # basic case, since the index survived this must be ok - assert H(1) not in repository - # nuke index, force replay - os.unlink(os.path.join(repository.path, "index.%d" % get_latest_segment())) - # must not reappear - assert H(1) not in repository - - -def test_shadowed_entries_are_preserved2(repository): - # this tests the shadowing-by-double-put behaviour, see issue #5661 - # assume this repo state: - # seg1: PUT H1 - # seg2: COMMIT - # seg3: DEL H1, PUT H1, DEL H1, PUT H2 - # seg4: COMMIT - # Note how due to the final DEL H1 in seg3, H1 is effectively deleted. - # - # compaction of only seg3: - # PUT H1 gets dropped because it is not needed any more. - # DEL H1 must be kept, because there is still a PUT H1 in seg1 which must not - # "reappear" in the index if the index gets rebuilt. - with repository: - get_latest_segment = repository.io.get_latest_segment - repository.put(H(1), fchunk(b"1")) - # This is the segment with our original PUT of interest - put_segment = get_latest_segment() - repository.commit(compact=False) - # We now put H(1) again (which implicitly does DEL(H(1)) followed by PUT(H(1), ...)), - # delete H(1) afterwards, and force this segment to not be compacted, which can happen - # if it's not sparse enough (symbolized by H(2) here). - repository.put(H(1), fchunk(b"1")) - repository.delete(H(1)) - repository.put(H(2), fchunk(b"1")) - delete_segment = get_latest_segment() - # We pretend these are mostly dense (not sparse) and won't be compacted - del repository.compact[put_segment] - del repository.compact[delete_segment] - repository.commit(compact=True) - # Now we perform an unrelated operation on the segment containing the DELETE, - # causing it to be compacted. - repository.delete(H(2)) - repository.commit(compact=True) - assert repository.io.segment_exists(put_segment) - assert not repository.io.segment_exists(delete_segment) - # Basic case, since the index survived this must be ok - assert H(1) not in repository - # Nuke index, force replay - os.unlink(os.path.join(repository.path, "index.%d" % get_latest_segment())) - # Must not reappear - assert H(1) not in repository # F - - -def test_shadow_index_rollback(repository): - with repository: - repository.put(H(1), fchunk(b"1")) - repository.delete(H(1)) - assert repository.shadow_index[H(1)] == [0] - repository.commit(compact=True) - repo_dump(repository, "p1 d1 cc") - # note how an empty list means that nothing is shadowed for sure - assert repository.shadow_index[H(1)] == [] # because the deletion is considered unstable - repository.put(H(1), b"1") - repository.delete(H(1)) - repo_dump(repository, "p1 d1") - # 0 put/delete; 1 commit; 2 compacted; 3 commit; 4 put/delete - assert repository.shadow_index[H(1)] == [4] - repository.rollback() - repo_dump(repository, "r") - repository.put(H(2), fchunk(b"1")) - # after the rollback, segment 4 shouldn't be considered anymore - assert repository.shadow_index[H(1)] == [] # because the deletion is considered unstable - - -def test_destroy_append_only(repository): - with repository: - # can't destroy append only repo (via the API) - repository.append_only = True - with pytest.raises(ValueError): - repository.destroy() - assert repository.append_only - - -def test_append_only(repository): - def segments_in_repository(repo): - return len(list(repo.io.segment_iterator())) - - with repository: - repository.append_only = True - repository.put(H(0), fchunk(b"foo")) - repository.commit(compact=False) - - repository.append_only = False - assert segments_in_repository(repository) == 2 - repository.put(H(0), fchunk(b"foo")) - repository.commit(compact=True) - # normal: compact squashes the data together, only one segment - assert segments_in_repository(repository) == 2 - - repository.append_only = True - assert segments_in_repository(repository) == 2 - repository.put(H(0), fchunk(b"foo")) - repository.commit(compact=False) - # append only: does not compact, only new segments written - assert segments_in_repository(repository) == 4 - - -def test_additional_free_space(repository): - with repository: - add_keys(repository) - repository.config.set("repository", "additional_free_space", "1000T") - repository.save_key(b"shortcut to save_config") - with reopen(repository) as repository: - repository.put(H(0), fchunk(b"foobar")) - with pytest.raises(Repository.InsufficientFreeSpaceError): - repository.commit(compact=False) - assert os.path.exists(repository.path) - - -def test_create_free_space(repository): - with repository: - repository.additional_free_space = 1e20 - with pytest.raises(Repository.InsufficientFreeSpaceError): - add_keys(repository) - assert not os.path.exists(repository.path) - - -def test_tracking(repository): - with repository: - assert repository.storage_quota_use == 0 - ch1 = fchunk(bytes(1234)) - repository.put(H(1), ch1) - assert repository.storage_quota_use == len(ch1) + 41 + 8 - ch2 = fchunk(bytes(5678)) - repository.put(H(2), ch2) - assert repository.storage_quota_use == len(ch1) + len(ch2) + 2 * (41 + 8) - repository.delete(H(1)) - assert repository.storage_quota_use == len(ch1) + len(ch2) + 2 * (41 + 8) # we have not compacted yet - repository.commit(compact=False) - assert repository.storage_quota_use == len(ch1) + len(ch2) + 2 * (41 + 8) # we have not compacted yet - with reopen(repository) as repository: - # open new transaction; hints and thus quota data is not loaded unless needed. - ch3 = fchunk(b"") - repository.put(H(3), ch3) - repository.delete(H(3)) - assert repository.storage_quota_use == len(ch1) + len(ch2) + len(ch3) + 3 * ( - 41 + 8 - ) # we have not compacted yet - repository.commit(compact=True) - assert repository.storage_quota_use == len(ch2) + 41 + 8 - - -def test_exceed_quota(repository): - with repository: - assert repository.storage_quota_use == 0 - repository.storage_quota = 80 - ch1 = fchunk(b"x" * 7) - repository.put(H(1), ch1) - assert repository.storage_quota_use == len(ch1) + 41 + 8 - repository.commit(compact=False) - with pytest.raises(Repository.StorageQuotaExceeded): - ch2 = fchunk(b"y" * 13) - repository.put(H(2), ch2) - assert repository.storage_quota_use == len(ch1) + len(ch2) + (41 + 8) * 2 # check ch2!? - with pytest.raises(Repository.StorageQuotaExceeded): - repository.commit(compact=False) - assert repository.storage_quota_use == len(ch1) + len(ch2) + (41 + 8) * 2 # check ch2!? - with reopen(repository) as repository: - repository.storage_quota = 161 - # open new transaction; hints and thus quota data is not loaded unless needed. - repository.put(H(1), ch1) - # we have 2 puts for H(1) here and not yet compacted. - assert repository.storage_quota_use == len(ch1) * 2 + (41 + 8) * 2 - repository.commit(compact=True) - assert repository.storage_quota_use == len(ch1) + 41 + 8 # now we have compacted. - - -def make_auxiliary(repository): - with repository: - repository.put(H(0), fchunk(b"foo")) - repository.commit(compact=False) - - -def do_commit(repository): - with repository: - repository.put(H(0), fchunk(b"fox")) - repository.commit(compact=False) - - -def test_corrupted_hints(repository): - make_auxiliary(repository) - with open(os.path.join(repository.path, "hints.1"), "ab") as fd: - fd.write(b"123456789") - do_commit(repository) - - -def test_deleted_hints(repository): - make_auxiliary(repository) - os.unlink(os.path.join(repository.path, "hints.1")) - do_commit(repository) - - -def test_deleted_index(repository): - make_auxiliary(repository) - os.unlink(os.path.join(repository.path, "index.1")) - do_commit(repository) - - -def test_unreadable_hints(repository): - make_auxiliary(repository) - hints = os.path.join(repository.path, "hints.1") - os.unlink(hints) - os.mkdir(hints) - with pytest.raises(OSError): - do_commit(repository) - - -def test_index(repository): - make_auxiliary(repository) - with open(os.path.join(repository.path, "index.1"), "wb") as fd: - fd.write(b"123456789") - do_commit(repository) - - -def test_index_outside_transaction(repository): - make_auxiliary(repository) - with open(os.path.join(repository.path, "index.1"), "wb") as fd: - fd.write(b"123456789") - with repository: - assert len(repository) == 1 - - -def _corrupt_index(repository): - # HashIndex is able to detect incorrect headers and file lengths, - # but on its own it can't tell if the data is correct. - index_path = os.path.join(repository.path, "index.1") - with open(index_path, "r+b") as fd: - index_data = fd.read() - # Flip one bit in a key stored in the index - corrupted_key = (int.from_bytes(H(0), "little") ^ 1).to_bytes(32, "little") - corrupted_index_data = index_data.replace(H(0), corrupted_key) - assert corrupted_index_data != index_data - assert len(corrupted_index_data) == len(index_data) - fd.seek(0) - fd.write(corrupted_index_data) - - -def test_index_corrupted(repository): - make_auxiliary(repository) - _corrupt_index(repository) - with repository: - # data corruption is detected due to mismatching checksums, and fixed by rebuilding the index. - assert len(repository) == 1 - assert pdchunk(repository.get(H(0))) == b"foo" - - -def test_index_corrupted_without_integrity(repository): - make_auxiliary(repository) - _corrupt_index(repository) - integrity_path = os.path.join(repository.path, "integrity.1") - os.unlink(integrity_path) - with repository: - # since the corrupted key is not noticed, the repository still thinks it contains one key... - assert len(repository) == 1 - with pytest.raises(Repository.ObjectNotFound): - # ... but the real, uncorrupted key is not found in the corrupted index. - repository.get(H(0)) - - -def test_unreadable_index(repository): - make_auxiliary(repository) - index = os.path.join(repository.path, "index.1") - os.unlink(index) - os.mkdir(index) - with pytest.raises(OSError): - do_commit(repository) - - -def test_unknown_integrity_version(repository): - make_auxiliary(repository) - # for now an unknown integrity data version is ignored and not an error. - integrity_path = os.path.join(repository.path, "integrity.1") - with open(integrity_path, "r+b") as fd: - msgpack.pack({b"version": 4.7}, fd) # borg only understands version 2 - fd.truncate() - with repository: - # no issues accessing the repository - assert len(repository) == 1 - assert pdchunk(repository.get(H(0))) == b"foo" - - -def _subtly_corrupted_hints_setup(repository): - with repository: - repository.append_only = True - assert len(repository) == 1 - assert pdchunk(repository.get(H(0))) == b"foo" - repository.put(H(1), fchunk(b"bar")) - repository.put(H(2), fchunk(b"baz")) - repository.commit(compact=False) - repository.put(H(2), fchunk(b"bazz")) - repository.commit(compact=False) - hints_path = os.path.join(repository.path, "hints.5") - with open(hints_path, "r+b") as fd: - hints = msgpack.unpack(fd) - fd.seek(0) - # corrupt segment refcount - assert hints["segments"][2] == 1 - hints["segments"][2] = 0 - msgpack.pack(hints, fd) - fd.truncate() - - -def test_subtly_corrupted_hints(repository): - make_auxiliary(repository) - _subtly_corrupted_hints_setup(repository) - with repository: - repository.append_only = False - repository.put(H(3), fchunk(b"1234")) - # do a compaction run, which succeeds since the failed checksum prompted a rebuild of the index+hints. - repository.commit(compact=True) - assert len(repository) == 4 - assert pdchunk(repository.get(H(0))) == b"foo" - assert pdchunk(repository.get(H(1))) == b"bar" - assert pdchunk(repository.get(H(2))) == b"bazz" - - -def test_subtly_corrupted_hints_without_integrity(repository): - make_auxiliary(repository) - _subtly_corrupted_hints_setup(repository) - integrity_path = os.path.join(repository.path, "integrity.5") - os.unlink(integrity_path) - with repository: - repository.append_only = False - repository.put(H(3), fchunk(b"1234")) - # do a compaction run, which fails since the corrupted refcount wasn't detected and causes an assertion failure. - with pytest.raises(AssertionError) as exc_info: - repository.commit(compact=True) - assert "Corrupted segment reference count" in str(exc_info.value) - - -def list_indices(repo_path): - return [name for name in os.listdir(repo_path) if name.startswith("index.")] - - def check(repository, repo_path, repair=False, status=True): assert repository.check(repair=repair) == status # Make sure no tmp files are left behind @@ -787,209 +153,6 @@ def check(repository, repo_path, repair=False, status=True): assert tmp_files == [], "Found tmp files" -def get_objects(repository, *ids): - for id_ in ids: - pdchunk(repository.get(H(id_))) - - -def add_objects(repository, segments): - for ids in segments: - for id_ in ids: - repository.put(H(id_), fchunk(b"data")) - repository.commit(compact=False) - - -def get_head(repo_path): - return sorted(int(n) for n in os.listdir(os.path.join(repo_path, "data", "0")) if n.isdigit())[-1] - - -def open_index(repo_path): - return NSIndex.read(os.path.join(repo_path, f"index.{get_head(repo_path)}")) - - -def corrupt_object(repo_path, id_): - idx = open_index(repo_path) - segment, offset, _ = idx[H(id_)] - with open(os.path.join(repo_path, "data", "0", str(segment)), "r+b") as fd: - fd.seek(offset) - fd.write(b"BOOM") - - -def delete_segment(repository, segment): - repository.io.delete_segment(segment) - - -def delete_index(repo_path): - os.unlink(os.path.join(repo_path, f"index.{get_head(repo_path)}")) - - -def rename_index(repo_path, new_name): - os.replace(os.path.join(repo_path, f"index.{get_head(repo_path)}"), os.path.join(repo_path, new_name)) - - -def list_objects(repository): - return {int(key) for key in repository.list()} - - -def test_repair_corrupted_segment(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repo_path = get_path(repository) - add_objects(repository, [[1, 2, 3], [4, 5], [6]]) - assert {1, 2, 3, 4, 5, 6} == list_objects(repository) - check(repository, repo_path, status=True) - corrupt_object(repo_path, 5) - with pytest.raises(IntegrityError): - get_objects(repository, 5) - repository.rollback() - # make sure a regular check does not repair anything - check(repository, repo_path, status=False) - check(repository, repo_path, status=False) - # make sure a repair actually repairs the repo - check(repository, repo_path, repair=True, status=True) - get_objects(repository, 4) - check(repository, repo_path, status=True) - assert {1, 2, 3, 4, 6} == list_objects(repository) - - -def test_repair_missing_segment(repository): - # only test on local repo - files in RemoteRepository cannot be deleted - with repository: - add_objects(repository, [[1, 2, 3], [4, 5, 6]]) - assert {1, 2, 3, 4, 5, 6} == list_objects(repository) - check(repository, repository.path, status=True) - delete_segment(repository, 2) - repository.rollback() - check(repository, repository.path, repair=True, status=True) - assert {1, 2, 3} == list_objects(repository) - - -def test_repair_missing_commit_segment(repository): - # only test on local repo - files in RemoteRepository cannot be deleted - with repository: - add_objects(repository, [[1, 2, 3], [4, 5, 6]]) - delete_segment(repository, 3) - with pytest.raises(Repository.ObjectNotFound): - get_objects(repository, 4) - assert {1, 2, 3} == list_objects(repository) - - -def test_repair_corrupted_commit_segment(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repo_path = get_path(repository) - add_objects(repository, [[1, 2, 3], [4, 5, 6]]) - with open(os.path.join(repo_path, "data", "0", "3"), "r+b") as fd: - fd.seek(-1, os.SEEK_END) - fd.write(b"X") - with pytest.raises(Repository.ObjectNotFound): - get_objects(repository, 4) - check(repository, repo_path, status=True) - get_objects(repository, 3) - assert {1, 2, 3} == list_objects(repository) - - -def test_repair_no_commits(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repo_path = get_path(repository) - add_objects(repository, [[1, 2, 3]]) - with open(os.path.join(repo_path, "data", "0", "1"), "r+b") as fd: - fd.seek(-1, os.SEEK_END) - fd.write(b"X") - with pytest.raises(Repository.CheckNeeded): - get_objects(repository, 4) - check(repository, repo_path, status=False) - check(repository, repo_path, status=False) - assert list_indices(repo_path) == ["index.1"] - check(repository, repo_path, repair=True, status=True) - assert list_indices(repo_path) == ["index.2"] - check(repository, repo_path, status=True) - get_objects(repository, 3) - assert {1, 2, 3} == list_objects(repository) - - -def test_repair_missing_index(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repo_path = get_path(repository) - add_objects(repository, [[1, 2, 3], [4, 5, 6]]) - delete_index(repo_path) - check(repository, repo_path, status=True) - get_objects(repository, 4) - assert {1, 2, 3, 4, 5, 6} == list_objects(repository) - - -def test_repair_index_too_new(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repo_path = get_path(repository) - add_objects(repository, [[1, 2, 3], [4, 5, 6]]) - assert list_indices(repo_path) == ["index.3"] - rename_index(repo_path, "index.100") - check(repository, repo_path, status=True) - assert list_indices(repo_path) == ["index.3"] - get_objects(repository, 4) - assert {1, 2, 3, 4, 5, 6} == list_objects(repository) - - -def test_crash_before_compact(repository): - # only test on local repo - we can't mock-patch a RemoteRepository class in another process! - with repository: - repository.put(H(0), fchunk(b"data")) - repository.put(H(0), fchunk(b"data2")) - # simulate a crash before compact - with patch.object(Repository, "compact_segments") as compact: - repository.commit(compact=True) - compact.assert_called_once_with(0.1) - with reopen(repository) as repository: - check(repository, repository.path, repair=True) - assert pdchunk(repository.get(H(0))) == b"data2" - - -def test_hints_persistence(repository): - with repository: - repository.put(H(0), fchunk(b"data")) - repository.delete(H(0)) - repository.commit(compact=False) - shadow_index_expected = repository.shadow_index - compact_expected = repository.compact - segments_expected = repository.segments - # close and re-open the repository (create fresh Repository instance) to - # check whether hints were persisted to / reloaded from disk - with reopen(repository) as repository: - repository.put(H(42), fchunk(b"foobar")) # this will call prepare_txn() and load the hints data - # check if hints persistence worked: - assert shadow_index_expected == repository.shadow_index - assert compact_expected == repository.compact - del repository.segments[2] # ignore the segment created by put(H(42), ...) - assert segments_expected == repository.segments - with reopen(repository) as repository: - check(repository, repository.path, repair=True) - with reopen(repository) as repository: - repository.put(H(42), fchunk(b"foobar")) # this will call prepare_txn() and load the hints data - assert shadow_index_expected == repository.shadow_index - # sizes do not match, with vs. without header? - # assert compact_expected == repository.compact - del repository.segments[2] # ignore the segment created by put(H(42), ...) - assert segments_expected == repository.segments - - -def test_hints_behaviour(repository): - with repository: - repository.put(H(0), fchunk(b"data")) - assert repository.shadow_index == {} - assert len(repository.compact) == 0 - repository.delete(H(0)) - repository.commit(compact=False) - # now there should be an entry for H(0) in shadow_index - assert H(0) in repository.shadow_index - assert len(repository.shadow_index[H(0)]) == 1 - assert 0 in repository.compact # segment 0 can be compacted - repository.put(H(42), fchunk(b"foobar")) # see also do_compact() - repository.commit(compact=True, threshold=0.0) # compact completely! - # nothing to compact anymore! no info left about stuff that does not exist anymore: - assert H(0) not in repository.shadow_index - # segment 0 was compacted away, no info about it left: - assert 0 not in repository.compact - assert 0 not in repository.segments - - def _get_mock_args(): class MockArgs: remote_path = "borg" diff --git a/src/borg/testsuite/repository3.py b/src/borg/testsuite/repository3.py deleted file mode 100644 index 3f34299b5..000000000 --- a/src/borg/testsuite/repository3.py +++ /dev/null @@ -1,277 +0,0 @@ -import logging -import os -import sys -from typing import Optional - -import pytest - -from ..checksums import xxh64 -from ..helpers import Location -from ..helpers import IntegrityError -from ..platformflags import is_win32 -from ..remote3 import RemoteRepository3, InvalidRPCMethod, PathNotAllowed -from ..repository3 import Repository3, MAX_DATA_SIZE -from ..repoobj import RepoObj -from .hashindex import H - - -@pytest.fixture() -def repository(tmp_path): - repository_location = os.fspath(tmp_path / "repository") - yield Repository3(repository_location, exclusive=True, create=True) - - -@pytest.fixture() -def remote_repository(tmp_path): - if is_win32: - pytest.skip("Remote repository does not yet work on Windows.") - repository_location = Location("ssh://__testsuite__" + os.fspath(tmp_path / "repository")) - yield RemoteRepository3(repository_location, exclusive=True, create=True) - - -def pytest_generate_tests(metafunc): - # Generates tests that run on both local and remote repos - if "repo_fixtures" in metafunc.fixturenames: - metafunc.parametrize("repo_fixtures", ["repository", "remote_repository"]) - - -def get_repository_from_fixture(repo_fixtures, request): - # returns the repo object from the fixture for tests that run on both local and remote repos - return request.getfixturevalue(repo_fixtures) - - -def reopen(repository, exclusive: Optional[bool] = True, create=False): - if isinstance(repository, Repository3): - if repository.opened: - raise RuntimeError("Repo must be closed before a reopen. Cannot support nested repository contexts.") - return Repository3(repository.path, exclusive=exclusive, create=create) - - if isinstance(repository, RemoteRepository3): - if repository.p is not None or repository.sock is not None: - raise RuntimeError("Remote repo must be closed before a reopen. Cannot support nested repository contexts.") - return RemoteRepository3(repository.location, exclusive=exclusive, create=create) - - raise TypeError( - f"Invalid argument type. Expected 'Repository3' or 'RemoteRepository3', received '{type(repository).__name__}'." - ) - - -def fchunk(data, meta=b""): - # format chunk: create a raw chunk that has valid RepoObj layout, but does not use encryption or compression. - hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data)) - assert isinstance(data, bytes) - chunk = hdr + meta + data - return chunk - - -def pchunk(chunk): - # parse chunk: parse data and meta from a raw chunk made by fchunk - hdr_size = RepoObj.obj_header.size - hdr = chunk[:hdr_size] - meta_size, data_size = RepoObj.obj_header.unpack(hdr)[0:2] - meta = chunk[hdr_size : hdr_size + meta_size] - data = chunk[hdr_size + meta_size : hdr_size + meta_size + data_size] - return data, meta - - -def pdchunk(chunk): - # parse only data from a raw chunk made by fchunk - return pchunk(chunk)[0] - - -def test_basic_operations(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - for x in range(100): - repository.put(H(x), fchunk(b"SOMEDATA")) - key50 = H(50) - assert pdchunk(repository.get(key50)) == b"SOMEDATA" - repository.delete(key50) - with pytest.raises(Repository3.ObjectNotFound): - repository.get(key50) - with reopen(repository) as repository: - with pytest.raises(Repository3.ObjectNotFound): - repository.get(key50) - for x in range(100): - if x == 50: - continue - assert pdchunk(repository.get(H(x))) == b"SOMEDATA" - - -def test_read_data(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - meta, data = b"meta", b"data" - hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data)) - chunk_complete = hdr + meta + data - chunk_short = hdr + meta - repository.put(H(0), chunk_complete) - assert repository.get(H(0)) == chunk_complete - assert repository.get(H(0), read_data=True) == chunk_complete - assert repository.get(H(0), read_data=False) == chunk_short - - -def test_consistency(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - repository.put(H(0), fchunk(b"foo")) - assert pdchunk(repository.get(H(0))) == b"foo" - repository.put(H(0), fchunk(b"foo2")) - assert pdchunk(repository.get(H(0))) == b"foo2" - repository.put(H(0), fchunk(b"bar")) - assert pdchunk(repository.get(H(0))) == b"bar" - repository.delete(H(0)) - with pytest.raises(Repository3.ObjectNotFound): - repository.get(H(0)) - - -def test_list(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - for x in range(100): - repository.put(H(x), fchunk(b"SOMEDATA")) - repo_list = repository.list() - assert len(repo_list) == 100 - first_half = repository.list(limit=50) - assert len(first_half) == 50 - assert first_half == repo_list[:50] - second_half = repository.list(marker=first_half[-1]) - assert len(second_half) == 50 - assert second_half == repo_list[50:] - assert len(repository.list(limit=50)) == 50 - - -def test_max_data_size(repo_fixtures, request): - with get_repository_from_fixture(repo_fixtures, request) as repository: - max_data = b"x" * (MAX_DATA_SIZE - RepoObj.obj_header.size) - repository.put(H(0), fchunk(max_data)) - assert pdchunk(repository.get(H(0))) == max_data - with pytest.raises(IntegrityError): - repository.put(H(1), fchunk(max_data + b"x")) - - -def check(repository, repo_path, repair=False, status=True): - assert repository.check(repair=repair) == status - # Make sure no tmp files are left behind - tmp_files = [name for name in os.listdir(repo_path) if "tmp" in name] - assert tmp_files == [], "Found tmp files" - - -def _get_mock_args(): - class MockArgs: - remote_path = "borg" - umask = 0o077 - debug_topics = [] - rsh = None - - def __contains__(self, item): - # to behave like argparse.Namespace - return hasattr(self, item) - - return MockArgs() - - -def test_remote_invalid_rpc(remote_repository): - with remote_repository: - with pytest.raises(InvalidRPCMethod): - remote_repository.call("__init__", {}) - - -def test_remote_rpc_exception_transport(remote_repository): - with remote_repository: - s1 = "test string" - - try: - remote_repository.call("inject_exception", {"kind": "DoesNotExist"}) - except Repository3.DoesNotExist as e: - assert len(e.args) == 1 - assert e.args[0] == remote_repository.location.processed - - try: - remote_repository.call("inject_exception", {"kind": "AlreadyExists"}) - except Repository3.AlreadyExists as e: - assert len(e.args) == 1 - assert e.args[0] == remote_repository.location.processed - - try: - remote_repository.call("inject_exception", {"kind": "CheckNeeded"}) - except Repository3.CheckNeeded as e: - assert len(e.args) == 1 - assert e.args[0] == remote_repository.location.processed - - try: - remote_repository.call("inject_exception", {"kind": "IntegrityError"}) - except IntegrityError as e: - assert len(e.args) == 1 - assert e.args[0] == s1 - - try: - remote_repository.call("inject_exception", {"kind": "PathNotAllowed"}) - except PathNotAllowed as e: - assert len(e.args) == 1 - assert e.args[0] == "foo" - - try: - remote_repository.call("inject_exception", {"kind": "ObjectNotFound"}) - except Repository3.ObjectNotFound as e: - assert len(e.args) == 2 - assert e.args[0] == s1 - assert e.args[1] == remote_repository.location.processed - - try: - remote_repository.call("inject_exception", {"kind": "InvalidRPCMethod"}) - except InvalidRPCMethod as e: - assert len(e.args) == 1 - assert e.args[0] == s1 - - try: - remote_repository.call("inject_exception", {"kind": "divide"}) - except RemoteRepository3.RPCError as e: - assert e.unpacked - assert e.get_message() == "ZeroDivisionError: integer division or modulo by zero\n" - assert e.exception_class == "ZeroDivisionError" - assert len(e.exception_full) > 0 - - -def test_remote_ssh_cmd(remote_repository): - with remote_repository: - args = _get_mock_args() - remote_repository._args = args - assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "example.com"] - assert remote_repository.ssh_cmd(Location("ssh://user@example.com/foo")) == ["ssh", "user@example.com"] - assert remote_repository.ssh_cmd(Location("ssh://user@example.com:1234/foo")) == [ - "ssh", - "-p", - "1234", - "user@example.com", - ] - os.environ["BORG_RSH"] = "ssh --foo" - assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "--foo", "example.com"] - - -def test_remote_borg_cmd(remote_repository): - with remote_repository: - assert remote_repository.borg_cmd(None, testing=True) == [sys.executable, "-m", "borg", "serve"] - args = _get_mock_args() - # XXX without next line we get spurious test fails when using pytest-xdist, root cause unknown: - logging.getLogger().setLevel(logging.INFO) - # note: test logger is on info log level, so --info gets added automagically - assert remote_repository.borg_cmd(args, testing=False) == ["borg", "serve", "--info"] - args.remote_path = "borg-0.28.2" - assert remote_repository.borg_cmd(args, testing=False) == ["borg-0.28.2", "serve", "--info"] - args.debug_topics = ["something_client_side", "repository_compaction"] - assert remote_repository.borg_cmd(args, testing=False) == [ - "borg-0.28.2", - "serve", - "--info", - "--debug-topic=borg.debug.repository_compaction", - ] - args = _get_mock_args() - args.storage_quota = 0 - assert remote_repository.borg_cmd(args, testing=False) == ["borg", "serve", "--info"] - args.storage_quota = 314159265 - assert remote_repository.borg_cmd(args, testing=False) == [ - "borg", - "serve", - "--info", - "--storage-quota=314159265", - ] - args.rsh = "ssh -i foo" - remote_repository._args = args - assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "-i", "foo", "example.com"] diff --git a/src/borg/testsuite/locking3.py b/src/borg/testsuite/storelocking.py similarity index 98% rename from src/borg/testsuite/locking3.py rename to src/borg/testsuite/storelocking.py index b9c4d3697..b4586a6bf 100644 --- a/src/borg/testsuite/locking3.py +++ b/src/borg/testsuite/storelocking.py @@ -4,7 +4,7 @@ from borgstore.store import Store -from ..locking3 import Lock, LockFailed, NotLocked +from ..storelocking import Lock, LockFailed, NotLocked ID1 = "foo", 1, 1 ID2 = "bar", 2, 2 From 7714b6542a2b8864a868cf50ec3369cae3451d3a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 24 Aug 2024 02:33:34 +0200 Subject: [PATCH 39/79] support sftp: repositories via borgstore --- src/borg/archiver/_common.py | 12 ++++++++++++ src/borg/helpers/parseformat.py | 21 +++++++++++++++++++++ src/borg/repository.py | 24 ++++++++++++++++-------- src/borg/testsuite/helpers.py | 8 ++++++++ src/borg/testsuite/repository.py | 2 +- 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 7cb4b24f0..4449c4f63 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -46,6 +46,18 @@ def get_repository( args=args, ) + elif location.proto in ("sftp", "file") and not v1_or_v2: # stuff directly supported by borgstore + repository = Repository( + location, + create=create, + exclusive=exclusive, + lock_wait=lock_wait, + lock=lock, + append_only=append_only, + make_parent_dirs=make_parent_dirs, + storage_quota=storage_quota, + ) + else: RepoCls = LegacyRepository if v1_or_v2 else Repository repository = RepoCls( diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 3b3af0dbb..3e8699006 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -454,6 +454,19 @@ class Location: re.VERBOSE, ) # path + sftp_re = re.compile( + r""" + (?Psftp):// # sftp:// + """ + + optional_user_re + + host_re + + r""" # user@ (optional), host name or address + (?::(?P\d+))? # :port (optional) + """ + + abs_path_re, + re.VERBOSE, + ) # path + socket_re = re.compile( r""" (?Psocket):// # socket:// @@ -518,6 +531,14 @@ def normpath_special(p): return ("/." + p) if relative else p m = self.ssh_re.match(text) + if m: + self.proto = m.group("proto") + self.user = m.group("user") + self._host = m.group("host") + self.port = m.group("port") and int(m.group("port")) or None + self.path = normpath_special(m.group("path")) + return True + m = self.sftp_re.match(text) if m: self.proto = m.group("proto") self.user = m.group("user") diff --git a/src/borg/repository.py b/src/borg/repository.py index 0c671ab53..b44d3bcdd 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -81,7 +81,7 @@ class PathPermissionDenied(Error): def __init__( self, - path, + path_or_location, create=False, exclusive=False, lock_wait=1.0, @@ -91,8 +91,16 @@ def __init__( make_parent_dirs=False, send_log_cb=None, ): - self.path = os.path.abspath(path) - url = "file://%s" % self.path + if isinstance(path_or_location, Location): + location = path_or_location + if location.proto == "file": + url = f"file://{location.path}" # frequently users give without file:// prefix + else: + url = location.processed # location as given by user, processed placeholders + else: + url = "file://%s" % os.path.abspath(path_or_location) + location = Location(url) + self.location = location # use a Store with flat config storage and 2-levels-nested data storage self.store = Store(url, levels={"config/": [0], "data/": [2]}) self._location = Location(url) @@ -115,7 +123,7 @@ def __init__( self.exclusive = exclusive def __repr__(self): - return f"<{self.__class__.__name__} {self.path}>" + return f"<{self.__class__.__name__} {self.location}>" def __enter__(self): if self.do_create: @@ -176,12 +184,12 @@ def open(self, *, exclusive, lock_wait=None, lock=True): self.lock = None readme = self.store.load("config/readme").decode() if readme != REPOSITORY_README: - raise self.InvalidRepository(self.path) + raise self.InvalidRepository(str(self.location)) self.version = int(self.store.load("config/version").decode()) if self.version not in self.acceptable_repo_versions: self.close() raise self.InvalidRepositoryConfig( - self.path, "repository version %d is not supported by this borg version" % self.version + str(self.location), "repository version %d is not supported by this borg version" % self.version ) self.id = hex_to_bin(self.store.load("config/id").decode(), length=32) self.opened = True @@ -338,7 +346,7 @@ def get(self, id, read_data=True): raise IntegrityError(f"Object too small [id {id_hex}]: expected {meta_size}, got {len(meta)} bytes") return hdr + meta except StoreObjectNotFound: - raise self.ObjectNotFound(id, self.path) from None + raise self.ObjectNotFound(id, str(self.location)) from None def get_many(self, ids, read_data=True, is_preloaded=False): for id_ in ids: @@ -369,7 +377,7 @@ def delete(self, id, wait=True): try: self.store.delete(key) except StoreObjectNotFound: - raise self.ObjectNotFound(id, self.path) from None + raise self.ObjectNotFound(id, str(self.location)) from None def async_response(self, wait=True): """Get one async result (only applies to remote repositories). diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index c9a83f6ab..4f29ce748 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -187,6 +187,14 @@ def test_ssh(self, monkeypatch, keys_dir): "host='2a02:0001:0002:0003:0004:0005:0006:0007', port=1234, path='/some/path')" ) + def test_sftp(self, monkeypatch, keys_dir): + monkeypatch.delenv("BORG_REPO", raising=False) + assert ( + repr(Location("sftp://user@host:1234/some/path")) + == "Location(proto='sftp', user='user', host='host', port=1234, path='/some/path')" + ) + assert Location("sftp://user@host:1234/some/path").to_key_filename() == keys_dir + "host__some_path" + def test_socket(self, monkeypatch, keys_dir): monkeypatch.delenv("BORG_REPO", raising=False) assert ( diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index c5419f147..e787ecc3e 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -44,7 +44,7 @@ def reopen(repository, exclusive: Optional[bool] = True, create=False): if isinstance(repository, Repository): if repository.opened: raise RuntimeError("Repo must be closed before a reopen. Cannot support nested repository contexts.") - return Repository(repository.path, exclusive=exclusive, create=create) + return Repository(repository.location, exclusive=exclusive, create=create) if isinstance(repository, RemoteRepository): if repository.p is not None or repository.sock is not None: From ec8a127b01216518d7ddee61950c6e01fc0dc39e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 24 Aug 2024 15:08:27 +0200 Subject: [PATCH 40/79] BORG_REPO env var: behave the same when unset or when set to empty string --- src/borg/helpers/parseformat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 3e8699006..f1ff42775 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -512,7 +512,7 @@ def parse(self, text, overrides={}): if not text: # we did not get a text to parse, so we try to fetch from the environment text = os.environ.get(self.repo_env_var) - if text is None: + if not text: # None or "" return self.raw = text # as given by user, might contain placeholders From 22b68b01991c1e562ed770ade9168a8ef5ea1af3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 24 Aug 2024 16:10:54 +0200 Subject: [PATCH 41/79] add sftp: to repository url format docs --- docs/usage/general/repository-urls.rst.inc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/usage/general/repository-urls.rst.inc b/docs/usage/general/repository-urls.rst.inc index 2d1fc27d5..e6167d08a 100644 --- a/docs/usage/general/repository-urls.rst.inc +++ b/docs/usage/general/repository-urls.rst.inc @@ -20,6 +20,9 @@ Note: you may also prepend a ``file://`` to a filesystem path to get URL style. ``ssh://user@host:port/~/path/to/repo`` - path relative to user's home directory +**Remote repositories** accessed via sftp: + +``sftp://user@host:port/path/to/repo`` - absolute path` If you frequently need the same repo URL, it is a good idea to set the ``BORG_REPO`` environment variable to set a default for the repo URL: From 3408e942debe9ea19562d62fef1b7076fc799ad1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 24 Aug 2024 16:21:45 +0200 Subject: [PATCH 42/79] add sftp: / borgstore to quickstart docs --- docs/quickstart.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index de3c8bb05..96bc6837f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -270,7 +270,7 @@ A passphrase should be a single line of text. Any trailing linefeed will be stripped. Do not use empty passphrases, as these can be trivially guessed, which does not -leave any encrypted data secure. +leave any encrypted data secure. Avoid passphrases containing non-ASCII characters. Borg can process any unicode text, but problems may arise at input due to text @@ -420,6 +420,15 @@ You can also use other remote filesystems in a similar way. Just be careful, not all filesystems out there are really stable and working good enough to be acceptable for backup usage. +Other kinds of repositories +--------------------------- + +Due to using the `borgstore` project, borg now also supports other kinds of +(remote) repositories besides `file:` and `ssh:`: + +- sftp: the borg client will directly talk to an sftp server. + This does not require borg being installed on the sftp server. +- Others may come in the future, adding backends to `borgstore` is rather simple. Restoring a backup ------------------ From 1a382a8bcf1734817918dfea039beca43a498b96 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 25 Aug 2024 01:53:03 +0200 Subject: [PATCH 43/79] set repository._location only --- src/borg/repository.py | 13 ++++++------- src/borg/testsuite/repository.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index b44d3bcdd..94807ee35 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -100,10 +100,9 @@ def __init__( else: url = "file://%s" % os.path.abspath(path_or_location) location = Location(url) - self.location = location + self._location = location # use a Store with flat config storage and 2-levels-nested data storage self.store = Store(url, levels={"config/": [0], "data/": [2]}) - self._location = Location(url) self.version = None # long-running repository methods which emit log or progress output are responsible for calling # the ._send_log method periodically to get log and progress output transferred to the borg client @@ -123,7 +122,7 @@ def __init__( self.exclusive = exclusive def __repr__(self): - return f"<{self.__class__.__name__} {self.location}>" + return f"<{self.__class__.__name__} {self._location}>" def __enter__(self): if self.do_create: @@ -184,12 +183,12 @@ def open(self, *, exclusive, lock_wait=None, lock=True): self.lock = None readme = self.store.load("config/readme").decode() if readme != REPOSITORY_README: - raise self.InvalidRepository(str(self.location)) + raise self.InvalidRepository(str(self._location)) self.version = int(self.store.load("config/version").decode()) if self.version not in self.acceptable_repo_versions: self.close() raise self.InvalidRepositoryConfig( - str(self.location), "repository version %d is not supported by this borg version" % self.version + str(self._location), "repository version %d is not supported by this borg version" % self.version ) self.id = hex_to_bin(self.store.load("config/id").decode(), length=32) self.opened = True @@ -346,7 +345,7 @@ def get(self, id, read_data=True): raise IntegrityError(f"Object too small [id {id_hex}]: expected {meta_size}, got {len(meta)} bytes") return hdr + meta except StoreObjectNotFound: - raise self.ObjectNotFound(id, str(self.location)) from None + raise self.ObjectNotFound(id, str(self._location)) from None def get_many(self, ids, read_data=True, is_preloaded=False): for id_ in ids: @@ -377,7 +376,7 @@ def delete(self, id, wait=True): try: self.store.delete(key) except StoreObjectNotFound: - raise self.ObjectNotFound(id, str(self.location)) from None + raise self.ObjectNotFound(id, str(self._location)) from None def async_response(self, wait=True): """Get one async result (only applies to remote repositories). diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index e787ecc3e..da07cfdd0 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -44,7 +44,7 @@ def reopen(repository, exclusive: Optional[bool] = True, create=False): if isinstance(repository, Repository): if repository.opened: raise RuntimeError("Repo must be closed before a reopen. Cannot support nested repository contexts.") - return Repository(repository.location, exclusive=exclusive, create=create) + return Repository(repository._location, exclusive=exclusive, create=create) if isinstance(repository, RemoteRepository): if repository.p is not None or repository.sock is not None: From a15cd1e493155dba1acfc93b5c2682935dba5717 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 25 Aug 2024 01:57:47 +0200 Subject: [PATCH 44/79] repository: remove __len__ and __contains__ --- src/borg/repository.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 94807ee35..59e160cf3 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -286,12 +286,6 @@ def check_object(obj): logger.error(f"Finished {mode} repository check, errors found.") return objs_errors == 0 or repair - def __len__(self): - raise NotImplementedError - - def __contains__(self, id): - raise NotImplementedError - def list(self, limit=None, marker=None): """ list IDs starting from after id . From c67cf07522b47729698838575c5acea38dd55d88 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 27 Aug 2024 01:32:48 +0200 Subject: [PATCH 45/79] Repository.list: return [(id, stored_size), ...] Note: LegacyRepository still returns [id, ...] and so does RemoteRepository.list, if the remote repo is a LegacyRepository. also: use LIST_SCAN_LIMIT --- src/borg/archive.py | 40 ++++++++++---------- src/borg/archiver/compact_cmd.py | 6 +-- src/borg/archiver/debug_cmd.py | 26 +++++++------ src/borg/archiver/rcompress_cmd.py | 8 ++-- src/borg/cache.py | 8 ++-- src/borg/repository.py | 11 +++--- src/borg/testsuite/archiver/check_cmd.py | 4 +- src/borg/testsuite/archiver/rcompress_cmd.py | 8 ++-- src/borg/testsuite/repository.py | 2 +- 9 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 4160ab249..c44eb506f 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1696,10 +1696,10 @@ def init_chunks(self): result = self.repository.list(limit=LIST_SCAN_LIMIT, marker=marker) if not result: break - marker = result[-1] - init_entry = ChunkIndexEntry(refcount=0, size=0) - for id_ in result: - self.chunks[id_] = init_entry + marker = result[-1][0] + init_entry = ChunkIndexEntry(refcount=0, size=0) # unknown plaintext size (!= stored size!) + for id, stored_size in result: + self.chunks[id] = init_entry def make_key(self, repository): attempt = 0 @@ -1737,7 +1737,7 @@ def make_key(self, repository): def verify_data(self): logger.info("Starting cryptographic data integrity verification...") chunks_count_index = len(self.chunks) - chunks_count_segments = 0 + chunks_count_repo = 0 errors = 0 defect_chunks = [] pi = ProgressIndicatorPercent( @@ -1745,16 +1745,16 @@ def verify_data(self): ) marker = None while True: - chunk_ids = self.repository.list(limit=100, marker=marker) - if not chunk_ids: + result = self.repository.list(limit=100, marker=marker) + if not result: break - marker = chunk_ids[-1] - chunks_count_segments += len(chunk_ids) - chunk_data_iter = self.repository.get_many(chunk_ids) - chunk_ids_revd = list(reversed(chunk_ids)) - while chunk_ids_revd: + marker = result[-1][0] + chunks_count_repo += len(result) + chunk_data_iter = self.repository.get_many(id for id, _ in result) + result_revd = list(reversed(result)) + while result_revd: pi.show() - chunk_id = chunk_ids_revd.pop(-1) # better efficiency + chunk_id, _ = result_revd.pop(-1) # better efficiency try: encrypted_data = next(chunk_data_iter) except (Repository.ObjectNotFound, IntegrityErrorBase) as err: @@ -1764,9 +1764,9 @@ def verify_data(self): if isinstance(err, IntegrityErrorBase): defect_chunks.append(chunk_id) # as the exception killed our generator, make a new one for remaining chunks: - if chunk_ids_revd: - chunk_ids = list(reversed(chunk_ids_revd)) - chunk_data_iter = self.repository.get_many(chunk_ids) + if result_revd: + result = list(reversed(result_revd)) + chunk_data_iter = self.repository.get_many(id for id, _ in result) else: try: # we must decompress, so it'll call assert_id() in there: @@ -1777,10 +1777,10 @@ def verify_data(self): logger.error("chunk %s, integrity error: %s", bin_to_hex(chunk_id), integrity_error) defect_chunks.append(chunk_id) pi.finish() - if chunks_count_index != chunks_count_segments: - logger.error("Repo/Chunks index object count vs. segment files object count mismatch.") + if chunks_count_index != chunks_count_repo: + logger.error("Chunks index object count vs. repository object count mismatch.") logger.error( - "Repo/Chunks index: %d objects != segment files: %d objects", chunks_count_index, chunks_count_segments + "Chunks index: %d objects != Chunks repository: %d objects", chunks_count_index, chunks_count_repo ) if defect_chunks: if self.repair: @@ -1820,7 +1820,7 @@ def verify_data(self): log = logger.error if errors else logger.info log( "Finished cryptographic data integrity verification, verified %d chunks with %d integrity errors.", - chunks_count_segments, + chunks_count_repo, errors, ) diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index d9c912448..629da8395 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -51,9 +51,9 @@ def get_repository_chunks(self) -> Dict[bytes, int]: result = self.repository.list(limit=LIST_SCAN_LIMIT, marker=marker) if not result: break - marker = result[-1] - for chunk_id in result: - repository_chunks[chunk_id] = 0 # plaintext size unknown + marker = result[-1][0] + for id, stored_size in result: + repository_chunks[id] = 0 # plaintext size unknown return repository_chunks def analyze_archives(self) -> Tuple[Dict[bytes, int], Dict[bytes, int], int, int, int]: diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index 9ad4beb8e..ddaeb8b8e 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -123,17 +123,18 @@ def decrypt_dump(id, cdata): fd.write(data) # set up the key without depending on a manifest obj - ids = repository.list(limit=1, marker=None) - cdata = repository.get(ids[0]) + result = repository.list(limit=1, marker=None) + id, _ = result[0] + cdata = repository.get(id) key = key_factory(repository, cdata) repo_objs = RepoObj(key) marker = None while True: - ids = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) - if not ids: + result = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) + if not result: break - marker = ids[-1] - for id in ids: + marker = result[-1][0] + for id, stored_size in result: cdata = repository.get(id) decrypt_dump(id, cdata) print("Done.") @@ -168,8 +169,9 @@ def print_finding(info, wanted, data, offset): from ..crypto.key import key_factory # set up the key without depending on a manifest obj - ids = repository.list(limit=1, marker=None) - cdata = repository.get(ids[0]) + result = repository.list(limit=1, marker=None) + id, _ = result[0] + cdata = repository.get(id) key = key_factory(repository, cdata) repo_objs = RepoObj(key) @@ -178,11 +180,11 @@ def print_finding(info, wanted, data, offset): last_id = None i = 0 while True: - ids = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) - if not ids: + result = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) + if not result: break - marker = ids[-1] - for id in ids: + marker = result[-1][0] + for id, stored_size in result: cdata = repository.get(id) _, data = repo_objs.parse(id, cdata, ro_type=ROBJ_DONTCARE) diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index 4df544e67..aef976a32 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -20,12 +20,12 @@ def find_chunks(repository, repo_objs, stats, ctype, clevel, olevel): compr_keys = stats["compr_keys"] = set() compr_wanted = ctype, clevel, olevel marker = None - chunks_limit = 1000 while True: - chunk_ids = repository.list(limit=chunks_limit, marker=marker) - if not chunk_ids: + result = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) + if not result: break - marker = chunk_ids[-1] + marker = result[-1][0] + chunk_ids = [id for id, _ in result] for id, chunk_no_data in zip(chunk_ids, repository.get_many(chunk_ids, read_data=False)): meta = repo_objs.parse_meta(id, chunk_no_data, ro_type=ROBJ_DONTCARE) compr_found = meta["ctype"], meta["clevel"], meta.get("olevel", -1) diff --git a/src/borg/cache.py b/src/borg/cache.py index d2cfe268d..0f452cb9c 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -639,14 +639,14 @@ def _load_chunks_from_repo(self): num_requests += 1 if not result: break - marker = result[-1] + marker = result[-1][0] # All chunks from the repository have a refcount of MAX_VALUE, which is sticky, # therefore we can't/won't delete them. Chunks we added ourselves in this transaction # are tracked correctly. - init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) - for id_ in result: + init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) # plaintext size + for id, stored_size in result: num_chunks += 1 - chunks[id_] = init_entry + chunks[id] = init_entry # Cache does not contain the manifest. if not isinstance(self.repository, (Repository, RemoteRepository)): del chunks[self.manifest.MANIFEST_ID] diff --git a/src/borg/repository.py b/src/borg/repository.py index 59e160cf3..c827f279d 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -288,11 +288,12 @@ def check_object(obj): def list(self, limit=None, marker=None): """ - list IDs starting from after id . + list infos starting from after id . + each info is a tuple (id, storage_size). """ self._lock_refresh() collect = True if marker is None else False - ids = [] + result = [] infos = self.store.list("data") # generator yielding ItemInfos while True: try: @@ -304,13 +305,13 @@ def list(self, limit=None, marker=None): else: id = hex_to_bin(info.name) if collect: - ids.append(id) - if len(ids) == limit: + result.append((id, info.size)) + if len(result) == limit: break elif id == marker: collect = True # note: do not collect the marker id - return ids + return result def get(self, id, read_data=True): self._lock_refresh() diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index 21f617b87..d38e38d87 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -432,6 +432,6 @@ def test_empty_repository(archivers, request): pytest.skip("only works locally") check_cmd_setup(archiver) with Repository(archiver.repository_location, exclusive=True) as repository: - for id_ in repository.list(): - repository.delete(id_) + for id, _ in repository.list(): + repository.delete(id) cmd(archiver, "check", exit_code=1) diff --git a/src/borg/testsuite/archiver/rcompress_cmd.py b/src/borg/testsuite/archiver/rcompress_cmd.py index 8abfc146e..22c634a3f 100644 --- a/src/borg/testsuite/archiver/rcompress_cmd.py +++ b/src/borg/testsuite/archiver/rcompress_cmd.py @@ -17,11 +17,11 @@ def check_compression(ctype, clevel, olevel): manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) marker = None while True: - ids = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) - if not ids: + result = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) + if not result: break - marker = ids[-1] - for id in ids: + marker = result[-1][0] + for id, _ in result: chunk = repository.get(id, read_data=True) meta, data = manifest.repo_objs.parse( id, chunk, ro_type=ROBJ_DONTCARE diff --git a/src/borg/testsuite/repository.py b/src/borg/testsuite/repository.py index da07cfdd0..05cb74ea4 100644 --- a/src/borg/testsuite/repository.py +++ b/src/borg/testsuite/repository.py @@ -131,7 +131,7 @@ def test_list(repo_fixtures, request): first_half = repository.list(limit=50) assert len(first_half) == 50 assert first_half == repo_list[:50] - second_half = repository.list(marker=first_half[-1]) + second_half = repository.list(marker=first_half[-1][0]) assert len(second_half) == 50 assert second_half == repo_list[50:] assert len(repository.list(limit=50)) == 50 From ec1d89f4778d9addcfb936f10805b1d075704466 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 27 Aug 2024 02:32:29 +0200 Subject: [PATCH 46/79] compact: better stats - compression factor - dedup factor - repo size All values are approx. values without considering overheads. --- src/borg/archiver/compact_cmd.py | 24 +++++++++++++++------- src/borg/testsuite/archiver/compact_cmd.py | 6 +++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index 629da8395..e0d37e3ac 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -20,13 +20,19 @@ def __init__(self, repository, manifest): self.repository = repository assert isinstance(repository, (Repository, RemoteRepository)) self.manifest = manifest - self.repository_chunks = None # what we have in the repository + self.repository_chunks = None # what we have in the repository, id -> stored_size self.used_chunks = None # what archives currently reference self.wanted_chunks = None # chunks that would be nice to have for next borg check --repair self.total_files = None # overall number of source files written to all archives in this repo self.total_size = None # overall size of source file content data written to all archives self.archives_count = None # number of archives + @property + def repository_size(self): + if self.repository_chunks is None: + return None + return sum(self.repository_chunks.values()) # sum of stored sizes + def garbage_collect(self): """Removes unused chunks from a repository.""" logger.info("Starting compaction / garbage collection...") @@ -53,7 +59,7 @@ def get_repository_chunks(self) -> Dict[bytes, int]: break marker = result[-1][0] for id, stored_size in result: - repository_chunks[id] = 0 # plaintext size unknown + repository_chunks[id] = stored_size return repository_chunks def analyze_archives(self) -> Tuple[Dict[bytes, int], Dict[bytes, int], int, int, int]: @@ -110,6 +116,7 @@ def report_and_delete(self): logger.warning(f"{len(missing_found)} previously missing objects re-appeared!" + run_repair) set_ec(EXIT_WARNING) + repo_size_before = self.repository_size referenced_chunks = set(self.used_chunks) | set(self.wanted_chunks) unused = set(self.repository_chunks) - referenced_chunks logger.info(f"Repository has {len(unused)} objects to delete.") @@ -123,15 +130,18 @@ def report_and_delete(self): self.repository.delete(id) del self.repository_chunks[id] pi.finish() + repo_size_after = self.repository_size count = len(self.repository_chunks) - logger.info(f"Repository has {count} objects now.") - logger.info(f"Overall statistics, considering all {self.archives_count} archives in this repository:") - logger.info(f"Source files count (before deduplication): {self.total_files}") - logger.info(f"Source files size (before deduplication): {format_file_size(self.total_size, precision=0)}") + logger.info(f"Source data size was {format_file_size(self.total_size, precision=0)} in {self.total_files} files.") dsize = sum(self.used_chunks[id] for id in self.repository_chunks) - logger.info(f"Deduplicated size (before compression, encryption): {format_file_size(dsize, precision=0)}") + logger.info(f"Repository size is {format_file_size(self.repository_size, precision=0)} in {count} objects.") + if self.total_size != 0: + logger.info(f"Space reduction factor due to deduplication: {dsize / self.total_size:.3f}") + if dsize != 0: + logger.info(f"Space reduction factor due to compression: {self.repository_size / dsize:.3f}") + logger.info(f"Compaction saved {format_file_size(repo_size_before - repo_size_after, precision=0)}.") class CompactMixIn: diff --git a/src/borg/testsuite/archiver/compact_cmd.py b/src/borg/testsuite/archiver/compact_cmd.py index c1dc3fcb7..1b90ecf5b 100644 --- a/src/borg/testsuite/archiver/compact_cmd.py +++ b/src/borg/testsuite/archiver/compact_cmd.py @@ -11,7 +11,7 @@ def test_compact_empty_repository(archivers, request): output = cmd(archiver, "compact", "-v", exit_code=0) assert "Starting compaction" in output - assert "Repository has 0 objects now." in output + assert "Repository size is 0 B in 0 objects." in output assert "Finished compaction" in output @@ -25,7 +25,7 @@ def test_compact_after_deleting_all_archives(archivers, request): output = cmd(archiver, "compact", "-v", exit_code=0) assert "Starting compaction" in output assert "Deleting " in output - assert "Repository has 0 objects now." in output + assert "Repository size is 0 B in 0 objects." in output assert "Finished compaction" in output @@ -40,5 +40,5 @@ def test_compact_after_deleting_some_archives(archivers, request): output = cmd(archiver, "compact", "-v", exit_code=0) assert "Starting compaction" in output assert "Deleting " in output - assert "Repository has 0 objects now, using approx. 0 B." not in output + assert "Repository size is 0 B in 0 objects." not in output assert "Finished compaction" in output From a40978ae1b00b1dd16685f7a4e83e2bb81e373a5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 27 Aug 2024 02:46:13 +0200 Subject: [PATCH 47/79] blacken the code --- src/borg/archiver/__init__.py | 15 +++++++-------- src/borg/archiver/compact_cmd.py | 14 ++++++-------- src/borg/helpers/__init__.py | 1 + src/borg/helpers/process.py | 8 +++++--- src/borg/testsuite/archiver/disk_full.py | 1 + src/borg/testsuite/archiver/mount_cmds.py | 10 ++++++---- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index e1c8512ab..2279b90dd 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -582,14 +582,13 @@ def main(): # pragma: no cover # Register fault handler for SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL. faulthandler.enable() - with signal_handler("SIGINT", raising_signal_handler(KeyboardInterrupt)), signal_handler( - "SIGHUP", raising_signal_handler(SigHup) - ), signal_handler("SIGTERM", raising_signal_handler(SigTerm)), signal_handler( - "SIGUSR1", sig_info_handler - ), signal_handler( - "SIGUSR2", sig_trace_handler - ), signal_handler( - "SIGINFO", sig_info_handler + with ( + signal_handler("SIGINT", raising_signal_handler(KeyboardInterrupt)), + signal_handler("SIGHUP", raising_signal_handler(SigHup)), + signal_handler("SIGTERM", raising_signal_handler(SigTerm)), + signal_handler("SIGUSR1", sig_info_handler), + signal_handler("SIGUSR2", sig_trace_handler), + signal_handler("SIGINFO", sig_info_handler), ): archiver = Archiver() msg = msgid = tb = None diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index e0d37e3ac..51144a107 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -39,13 +39,9 @@ def garbage_collect(self): logger.info("Getting object IDs present in the repository...") self.repository_chunks = self.get_repository_chunks() logger.info("Computing object IDs used by archives...") - ( - self.used_chunks, - self.wanted_chunks, - self.total_files, - self.total_size, - self.archives_count, - ) = self.analyze_archives() + (self.used_chunks, self.wanted_chunks, self.total_files, self.total_size, self.archives_count) = ( + self.analyze_archives() + ) self.report_and_delete() logger.info("Finished compaction / garbage collection...") @@ -134,7 +130,9 @@ def report_and_delete(self): count = len(self.repository_chunks) logger.info(f"Overall statistics, considering all {self.archives_count} archives in this repository:") - logger.info(f"Source data size was {format_file_size(self.total_size, precision=0)} in {self.total_files} files.") + logger.info( + f"Source data size was {format_file_size(self.total_size, precision=0)} in {self.total_files} files." + ) dsize = sum(self.used_chunks[id] for id in self.repository_chunks) logger.info(f"Repository size is {format_file_size(self.repository_size, precision=0)} in {count} objects.") if self.total_size != 0: diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py index 53555e7e6..d62e45f1d 100644 --- a/src/borg/helpers/__init__.py +++ b/src/borg/helpers/__init__.py @@ -5,6 +5,7 @@ Code used to be in borg/helpers.py but was split into the modules in this package, which are imported into here for compatibility. """ + import os from typing import List from collections import namedtuple diff --git a/src/borg/helpers/process.py b/src/borg/helpers/process.py index 4149b7eda..cd8303d85 100644 --- a/src/borg/helpers/process.py +++ b/src/borg/helpers/process.py @@ -82,9 +82,11 @@ def daemonizing(*, timeout=5): logger.debug("Daemonizing: Foreground process (%s, %s, %s) is waiting for background process..." % old_id) exit_code = EXIT_SUCCESS # Indeed, SIGHUP and SIGTERM handlers should have been set on archiver.run(). Just in case... - with signal_handler("SIGINT", raising_signal_handler(KeyboardInterrupt)), signal_handler( - "SIGHUP", raising_signal_handler(SigHup) - ), signal_handler("SIGTERM", raising_signal_handler(SigTerm)): + with ( + signal_handler("SIGINT", raising_signal_handler(KeyboardInterrupt)), + signal_handler("SIGHUP", raising_signal_handler(SigHup)), + signal_handler("SIGTERM", raising_signal_handler(SigTerm)), + ): try: if timeout > 0: time.sleep(timeout) diff --git a/src/borg/testsuite/archiver/disk_full.py b/src/borg/testsuite/archiver/disk_full.py index 5f85a2931..0d3617381 100644 --- a/src/borg/testsuite/archiver/disk_full.py +++ b/src/borg/testsuite/archiver/disk_full.py @@ -14,6 +14,7 @@ if the directory does not exist, the test will be skipped. """ + import errno import os import random diff --git a/src/borg/testsuite/archiver/mount_cmds.py b/src/borg/testsuite/archiver/mount_cmds.py index 292aff748..1a8bb12ff 100644 --- a/src/borg/testsuite/archiver/mount_cmds.py +++ b/src/borg/testsuite/archiver/mount_cmds.py @@ -32,16 +32,18 @@ def test_fuse_mount_hardlinks(archivers, request): ignore_perms = ["-o", "ignore_permissions,defer_permissions"] else: ignore_perms = ["-o", "ignore_permissions"] - with fuse_mount(archiver, mountpoint, "-a", "test", "--strip-components=2", *ignore_perms), changedir( - os.path.join(mountpoint, "test") + with ( + fuse_mount(archiver, mountpoint, "-a", "test", "--strip-components=2", *ignore_perms), + changedir(os.path.join(mountpoint, "test")), ): assert os.stat("hardlink").st_nlink == 2 assert os.stat("subdir/hardlink").st_nlink == 2 assert open("subdir/hardlink", "rb").read() == b"123456" assert os.stat("aaaa").st_nlink == 2 assert os.stat("source2").st_nlink == 2 - with fuse_mount(archiver, mountpoint, "input/dir1", "-a", "test", *ignore_perms), changedir( - os.path.join(mountpoint, "test") + with ( + fuse_mount(archiver, mountpoint, "input/dir1", "-a", "test", *ignore_perms), + changedir(os.path.join(mountpoint, "test")), ): assert os.stat("input/dir1/hardlink").st_nlink == 2 assert os.stat("input/dir1/subdir/hardlink").st_nlink == 2 From 57268909f88cc8c8452645b754fe2ac693d87974 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 27 Aug 2024 02:49:22 +0200 Subject: [PATCH 48/79] upgrade black to 24.x --- .github/workflows/black.yaml | 2 +- .pre-commit-config.yaml | 2 +- requirements.d/codestyle.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/black.yaml b/.github/workflows/black.yaml index f382af79a..6d473e0af 100644 --- a/.github/workflows/black.yaml +++ b/.github/workflows/black.yaml @@ -12,4 +12,4 @@ jobs: - uses: actions/checkout@v4 - uses: psf/black@stable with: - version: "~= 23.0" + version: "~= 24.0" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b6d0d390..16e334217 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 24.8.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/requirements.d/codestyle.txt b/requirements.d/codestyle.txt index b9e137e68..4a92e2c83 100644 --- a/requirements.d/codestyle.txt +++ b/requirements.d/codestyle.txt @@ -1 +1 @@ -black >=23.0, <24 +black >=24.0, <25 From d27b7a7981b509e01b3acf6abeb6de9c98afa9a6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 28 Aug 2024 22:14:12 +0200 Subject: [PATCH 49/79] cache: remove transactions, load files/chunks cache on demand --- src/borg/archive.py | 1 - src/borg/archiver/key_cmds.py | 4 - src/borg/archiver/prune_cmd.py | 1 - src/borg/archiver/recreate_cmd.py | 1 - src/borg/archiver/rename_cmd.py | 1 - src/borg/cache.py | 125 +++++++++----------------- src/borg/testsuite/archiver/checks.py | 2 - src/borg/testsuite/cache.py | 9 -- 8 files changed, 40 insertions(+), 104 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index c44eb506f..84bab78ef 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -659,7 +659,6 @@ def save(self, name=None, comment=None, timestamp=None, stats=None, additional_m pass self.manifest.archives[name] = (self.id, metadata.time) self.manifest.write() - self.cache.commit() return metadata def calc_stats(self, cache, want_unique=True): diff --git a/src/borg/archiver/key_cmds.py b/src/borg/archiver/key_cmds.py index f2e5c12d9..6fbac5f09 100644 --- a/src/borg/archiver/key_cmds.py +++ b/src/borg/archiver/key_cmds.py @@ -74,11 +74,7 @@ def do_change_location(self, args, repository, manifest, cache): manifest.repo_objs.key = key_new manifest.write() - # we need to rewrite cache config and security key-type info, - # so that the cached key-type will match the repo key-type. - cache.begin_txn() # need to start a cache transaction, otherwise commit() does nothing. cache.key = key_new - cache.commit() loc = key_new.find_key() if hasattr(key_new, "find_key") else None if args.keep: diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index 6c7ebf3ff..a33901770 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -143,7 +143,6 @@ def do_prune(self, args, repository, manifest): raise Error("Got Ctrl-C / SIGINT.") elif uncommitted_deletes > 0: manifest.write() - cache.commit() def build_parser_prune(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index 90260efab..7d30b41d3 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -49,7 +49,6 @@ def do_recreate(self, args, repository, manifest, cache): logger.info("Skipped archive %s: Nothing to do. Archive was not processed.", name) if not args.dry_run: manifest.write() - cache.commit() def build_parser_recreate(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog diff --git a/src/borg/archiver/rename_cmd.py b/src/borg/archiver/rename_cmd.py index 39b36571a..cb8660c48 100644 --- a/src/borg/archiver/rename_cmd.py +++ b/src/borg/archiver/rename_cmd.py @@ -17,7 +17,6 @@ def do_rename(self, args, repository, manifest, cache, archive): """Rename an existing archive""" archive.rename(args.newname) manifest.write() - cache.commit() def build_parser_rename(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog diff --git a/src/borg/cache.py b/src/borg/cache.py index 0f452cb9c..7c126c3cc 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -391,9 +391,15 @@ class FilesCacheMixin: def __init__(self, cache_mode): self.cache_mode = cache_mode - self.files = None + self._files = None self._newest_cmtime = None + @property + def files(self): + if self._files is None: + self._files = self._read_files_cache() + return self._files + def files_cache_name(self): suffix = os.environ.get("BORG_FILES_CACHE_SUFFIX", "") return self.FILES_CACHE_NAME + "." + suffix if suffix else self.FILES_CACHE_NAME @@ -412,7 +418,7 @@ def _read_files_cache(self): if "d" in self.cache_mode: # d(isabled) return - self.files = {} + files = {} logger.debug("Reading files cache ...") files_cache_logger.debug("FILES-CACHE-LOAD: starting...") msg = None @@ -432,7 +438,7 @@ def _read_files_cache(self): for path_hash, item in u: entry = FileCacheEntry(*item) # in the end, this takes about 240 Bytes per file - self.files[path_hash] = msgpack.packb(entry._replace(age=entry.age + 1)) + files[path_hash] = msgpack.packb(entry._replace(age=entry.age + 1)) except (TypeError, ValueError) as exc: msg = "The files cache seems invalid. [%s]" % str(exc) break @@ -443,18 +449,20 @@ def _read_files_cache(self): if msg is not None: logger.warning(msg) logger.warning("Continuing without files cache - expect lower performance.") - self.files = {} - files_cache_logger.debug("FILES-CACHE-LOAD: finished, %d entries loaded.", len(self.files)) + files = {} + files_cache_logger.debug("FILES-CACHE-LOAD: finished, %d entries loaded.", len(files)) + return files - def _write_files_cache(self): + def _write_files_cache(self, files): if self._newest_cmtime is None: # was never set because no files were modified/added self._newest_cmtime = 2**63 - 1 # nanoseconds, good until y2262 ttl = int(os.environ.get("BORG_FILES_CACHE_TTL", 20)) files_cache_logger.debug("FILES-CACHE-SAVE: starting...") + # TODO: use something like SaveFile here, but that didn't work due to SyncFile missing .seek(). with IntegrityCheckedFile(path=os.path.join(self.path, self.files_cache_name()), write=True) as fd: entry_count = 0 - for path_hash, item in self.files.items(): + for path_hash, item in files.items(): # Only keep files seen in this backup that are older than newest cmtime seen in this backup - # this is to avoid issues with filesystem snapshots and cmtime granularity. # Also keep files from older backups that have not reached BORG_FILES_CACHE_TTL yet. @@ -562,18 +570,23 @@ class ChunksMixin: Chunks index related code for misc. Cache implementations. """ + def __init__(self): + self._chunks = None + + @property + def chunks(self): + if self._chunks is None: + self._chunks = self._load_chunks_from_repo() + return self._chunks + def chunk_incref(self, id, size, stats): assert isinstance(size, int) and size > 0 - if not self._txn_active: - self.begin_txn() count, _size = self.chunks.incref(id) stats.update(size, False) return ChunkListEntry(id, size) def chunk_decref(self, id, size, stats, wait=True): assert isinstance(size, int) and size > 0 - if not self._txn_active: - self.begin_txn() count, _size = self.chunks.decref(id) if count == 0: del self.chunks[id] @@ -583,8 +596,6 @@ def chunk_decref(self, id, size, stats, wait=True): stats.update(-size, False) def seen_chunk(self, id, size=None): - if not self._txn_active: - self.begin_txn() entry = self.chunks.get(id, ChunkIndexEntry(0, None)) if entry.refcount and size is not None: assert isinstance(entry.size, int) @@ -609,8 +620,6 @@ def add_chunk( ro_type=ROBJ_FILE_STREAM, ): assert ro_type is not None - if not self._txn_active: - self.begin_txn() if size is None: if compress: size = len(data) # data is still uncompressed @@ -641,7 +650,7 @@ def _load_chunks_from_repo(self): break marker = result[-1][0] # All chunks from the repository have a refcount of MAX_VALUE, which is sticky, - # therefore we can't/won't delete them. Chunks we added ourselves in this transaction + # therefore we can't/won't delete them. Chunks we added ourselves in this borg run # are tracked correctly. init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) # plaintext size for id, stored_size in result: @@ -684,13 +693,13 @@ def __init__( :param cache_mode: what shall be compared in the file stat infos vs. cached stat infos comparison """ FilesCacheMixin.__init__(self, cache_mode) + ChunksMixin.__init__(self) assert isinstance(manifest, Manifest) self.manifest = manifest self.repository = manifest.repository self.key = manifest.key self.repo_objs = manifest.repo_objs self.progress = progress - self._txn_active = False self.path = cache_dir(self.repository, path) self.security_manager = SecurityManager(self.repository) @@ -714,10 +723,12 @@ def __init__( raise def __enter__(self): + self._chunks = None return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() + self._chunks = None def create(self): """Create a new empty cache at `self.path`""" @@ -727,69 +738,24 @@ def create(self): self.cache_config.create() self._create_empty_files_cache(self.path) - def _do_open(self): - self.cache_config.load() - self.chunks = self._load_chunks_from_repo() - self._read_files_cache() - def open(self): if not os.path.isdir(self.path): raise Exception("%s Does not look like a Borg cache" % self.path) self.cache_config.open() - self.rollback() + self.cache_config.load() def close(self): - if self.cache_config is not None: - self.cache_config.close() - self.cache_config = None - - def begin_txn(self): - # Initialize transaction snapshot - pi = ProgressIndicatorMessage(msgid="cache.begin_transaction") - txn_dir = os.path.join(self.path, "txn.tmp") - os.mkdir(txn_dir) - pi.output("Initializing cache transaction: Reading config") - shutil.copy(os.path.join(self.path, "config"), txn_dir) - pi.output("Initializing cache transaction: Reading files") - try: - shutil.copy(os.path.join(self.path, self.files_cache_name()), txn_dir) - except FileNotFoundError: - self._create_empty_files_cache(txn_dir) - os.replace(txn_dir, os.path.join(self.path, "txn.active")) - pi.finish() - self._txn_active = True - - def commit(self): - if not self._txn_active: - return self.security_manager.save(self.manifest, self.key) - pi = ProgressIndicatorMessage(msgid="cache.commit") - if self.files is not None: + pi = ProgressIndicatorMessage(msgid="cache.close") + if self._files is not None: pi.output("Saving files cache") - integrity_data = self._write_files_cache() + integrity_data = self._write_files_cache(self._files) self.cache_config.integrity[self.files_cache_name()] = integrity_data pi.output("Saving cache config") self.cache_config.save(self.manifest) - os.replace(os.path.join(self.path, "txn.active"), os.path.join(self.path, "txn.tmp")) - shutil.rmtree(os.path.join(self.path, "txn.tmp")) - self._txn_active = False + self.cache_config.close() pi.finish() - - def rollback(self): - # Remove partial transaction - if os.path.exists(os.path.join(self.path, "txn.tmp")): - shutil.rmtree(os.path.join(self.path, "txn.tmp")) - # Roll back active transaction - txn_dir = os.path.join(self.path, "txn.active") - if os.path.exists(txn_dir): - shutil.copy(os.path.join(txn_dir, "config"), self.path) - shutil.copy(os.path.join(txn_dir, self.discover_files_cache_name(txn_dir)), self.path) - txn_tmp = os.path.join(self.path, "txn.tmp") - os.replace(txn_dir, txn_tmp) - if os.path.exists(txn_tmp): - shutil.rmtree(txn_tmp) - self._txn_active = False - self._do_open() + self.cache_config = None def check_cache_compatibility(self): my_features = Manifest.SUPPORTED_REPO_FEATURES @@ -805,7 +771,7 @@ def check_cache_compatibility(self): def wipe_cache(self): logger.warning("Discarding incompatible cache and forcing a cache rebuild") - self.chunks = ChunkIndex() + self._chunks = ChunkIndex() self._create_empty_files_cache(self.path) self.cache_config.manifest_id = "" self.cache_config._config.set("cache", "manifest", "") @@ -835,12 +801,12 @@ class AdHocCache(ChunksMixin): """ def __init__(self, manifest, warn_if_unencrypted=True, lock_wait=None, iec=False): + ChunksMixin.__init__(self) assert isinstance(manifest, Manifest) self.manifest = manifest self.repository = manifest.repository self.key = manifest.key self.repo_objs = manifest.repo_objs - self._txn_active = False self.security_manager = SecurityManager(self.repository) self.security_manager.assert_secure(manifest, self.key, lock_wait=lock_wait) @@ -848,10 +814,13 @@ def __init__(self, manifest, warn_if_unencrypted=True, lock_wait=None, iec=False # Public API def __enter__(self): + self._chunks = None return self def __exit__(self, exc_type, exc_val, exc_tb): - pass + if exc_type is None: + self.security_manager.save(self.manifest, self.key) + self._chunks = None files = None # type: ignore cache_mode = "d" @@ -862,17 +831,3 @@ def file_known_and_unchanged(self, hashed_path, path_hash, st): def memorize_file(self, hashed_path, path_hash, st, chunks): pass - - def commit(self): - if not self._txn_active: - return - self.security_manager.save(self.manifest, self.key) - self._txn_active = False - - def rollback(self): - self._txn_active = False - del self.chunks - - def begin_txn(self): - self._txn_active = True - self.chunks = self._load_chunks_from_repo() diff --git a/src/borg/testsuite/archiver/checks.py b/src/borg/testsuite/archiver/checks.py index efea8e1fb..e6c407e8d 100644 --- a/src/borg/testsuite/archiver/checks.py +++ b/src/borg/testsuite/archiver/checks.py @@ -264,9 +264,7 @@ def test_unknown_mandatory_feature_in_cache(archivers, request): repository._location = Location(archiver.repository_location) manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) with Cache(repository, manifest) as cache: - cache.begin_txn() cache.cache_config.mandatory_features = {"unknown-feature"} - cache.commit() if archiver.FORK_DEFAULT: cmd(archiver, "create", "test", "input") diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index beca83505..2d86bede0 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -59,15 +59,6 @@ def test_files_cache(self, cache): assert cache.cache_mode == "d" assert cache.files is None - def test_txn(self, cache): - assert not cache._txn_active - cache.seen_chunk(H(5)) - assert cache._txn_active - assert cache.chunks - cache.rollback() - assert not cache._txn_active - assert not hasattr(cache, "chunks") - def test_incref_after_add_chunk(self, cache): assert cache.add_chunk(H(3), {}, b"5678", stats=Statistics()) == (H(3), 4) assert cache.chunk_incref(H(3), 4, Statistics()) == (H(3), 4) From ef47666627a90c0489a7af90bc58456f817a273a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 13:38:11 +0200 Subject: [PATCH 50/79] cache/hashindex: remove decref method, don't try to remove chunks on exceptions When the AdhocCache(WithFiles) queries chunk IDs from the repo to build the chunks index, it won't know their refcount and thus all chunks in the index have their refcount at the MAX_VALUE (representing "infinite") and that would never decrease nor could that ever reach zero and get the chunk deleted from the repo. Only completely new chunks first written in the current borg run have a valid refcount. In some exception handlers, borg tried to clean up chunks that won't be used by an item by decref'ing them. That is either: - pointless due to refcount being at MAX_VALUE - inefficient, because the user might retry the backup and would need to transmit these chunks to the repo again. We'll just rely on borg compact ONLY to clean up any unused/orphan chunks. --- src/borg/archive.py | 200 ++++++++++++++------------------ src/borg/cache.py | 10 -- src/borg/hashindex.pyi | 1 - src/borg/hashindex.pyx | 14 --- src/borg/selftest.py | 2 +- src/borg/testsuite/cache.py | 14 --- src/borg/testsuite/hashindex.py | 22 +--- 7 files changed, 88 insertions(+), 175 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 84bab78ef..240938d34 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -952,7 +952,6 @@ def set_meta(self, key, value): new_id = self.key.id_hash(data) self.cache.add_chunk(new_id, {}, data, stats=self.stats, ro_type=ROBJ_ARCHIVE_META) self.manifest.archives[self.name] = (new_id, metadata.time) - self.cache.chunk_decref(self.id, 1, self.stats) self.id = new_id def rename(self, name): @@ -1309,20 +1308,11 @@ def process_pipe(self, *, path, cache, fd, mode, user=None, group=None): item.uid = uid if gid is not None: item.gid = gid - try: - self.process_file_chunks( - item, cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(fd)) - ) - except BackupOSError: - # see comments in process_file's exception handler, same issue here. - for chunk in item.get("chunks", []): - cache.chunk_decref(chunk.id, chunk.size, self.stats, wait=False) - raise - else: - item.get_size(memorize=True) - self.stats.nfiles += 1 - self.add_item(item, stats=self.stats) - return status + self.process_file_chunks(item, cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(fd))) + item.get_size(memorize=True) + self.stats.nfiles += 1 + self.add_item(item, stats=self.stats) + return status def process_file(self, *, path, parent_fd, name, st, cache, flags=flags_normal, last_try=False, strip_prefix): with self.create_helper(path, st, None, strip_prefix=strip_prefix) as ( @@ -1343,93 +1333,81 @@ def process_file(self, *, path, parent_fd, name, st, cache, flags=flags_normal, # so it can be extracted / accessed in FUSE mount like a regular file. # this needs to be done early, so that part files also get the patched mode. item.mode = stat.S_IFREG | stat.S_IMODE(item.mode) - # we begin processing chunks now (writing or incref'ing them to the repository), - # which might require cleanup (see except-branch): - try: - if hl_chunks is not None: # create_helper gave us chunks from a previous hardlink - item.chunks = [] - for chunk_id, chunk_size in hl_chunks: - # process one-by-one, so we will know in item.chunks how far we got - chunk_entry = cache.chunk_incref(chunk_id, chunk_size, self.stats) - item.chunks.append(chunk_entry) - else: # normal case, no "2nd+" hardlink - if not is_special_file: - hashed_path = safe_encode(os.path.join(self.cwd, path)) - started_hashing = time.monotonic() - path_hash = self.key.id_hash(hashed_path) - self.stats.hashing_time += time.monotonic() - started_hashing - known, chunks = cache.file_known_and_unchanged(hashed_path, path_hash, st) + # we begin processing chunks now. + if hl_chunks is not None: # create_helper gave us chunks from a previous hardlink + item.chunks = [] + for chunk_id, chunk_size in hl_chunks: + # process one-by-one, so we will know in item.chunks how far we got + chunk_entry = cache.chunk_incref(chunk_id, chunk_size, self.stats) + item.chunks.append(chunk_entry) + else: # normal case, no "2nd+" hardlink + if not is_special_file: + hashed_path = safe_encode(os.path.join(self.cwd, path)) + started_hashing = time.monotonic() + path_hash = self.key.id_hash(hashed_path) + self.stats.hashing_time += time.monotonic() - started_hashing + known, chunks = cache.file_known_and_unchanged(hashed_path, path_hash, st) + else: + # in --read-special mode, we may be called for special files. + # there should be no information in the cache about special files processed in + # read-special mode, but we better play safe as this was wrong in the past: + hashed_path = path_hash = None + known, chunks = False, None + if chunks is not None: + # Make sure all ids are available + for chunk in chunks: + if not cache.seen_chunk(chunk.id): + # cache said it is unmodified, but we lost a chunk: process file like modified + status = "M" + break else: - # in --read-special mode, we may be called for special files. - # there should be no information in the cache about special files processed in - # read-special mode, but we better play safe as this was wrong in the past: - hashed_path = path_hash = None - known, chunks = False, None - if chunks is not None: - # Make sure all ids are available + item.chunks = [] for chunk in chunks: - if not cache.seen_chunk(chunk.id): - # cache said it is unmodified, but we lost a chunk: process file like modified - status = "M" - break + # process one-by-one, so we will know in item.chunks how far we got + cache.chunk_incref(chunk.id, chunk.size, self.stats) + item.chunks.append(chunk) + status = "U" # regular file, unchanged + else: + status = "M" if known else "A" # regular file, modified or added + self.print_file_status(status, path) + # Only chunkify the file if needed + changed_while_backup = False + if "chunks" not in item: + with backup_io("read"): + self.process_file_chunks( + item, + cache, + self.stats, + self.show_progress, + backup_io_iter(self.chunker.chunkify(None, fd)), + ) + self.stats.chunking_time = self.chunker.chunking_time + if not is_win32: # TODO for win32 + with backup_io("fstat2"): + st2 = os.fstat(fd) + # special files: + # - fifos change naturally, because they are fed from the other side. no problem. + # - blk/chr devices don't change ctime anyway. + changed_while_backup = not is_special_file and st.st_ctime_ns != st2.st_ctime_ns + if changed_while_backup: + # regular file changed while we backed it up, might be inconsistent/corrupt! + if last_try: + status = "C" # crap! retries did not help. else: - item.chunks = [] - for chunk in chunks: - # process one-by-one, so we will know in item.chunks how far we got - cache.chunk_incref(chunk.id, chunk.size, self.stats) - item.chunks.append(chunk) - status = "U" # regular file, unchanged - else: - status = "M" if known else "A" # regular file, modified or added - self.print_file_status(status, path) - # Only chunkify the file if needed - changed_while_backup = False - if "chunks" not in item: - with backup_io("read"): - self.process_file_chunks( - item, - cache, - self.stats, - self.show_progress, - backup_io_iter(self.chunker.chunkify(None, fd)), - ) - self.stats.chunking_time = self.chunker.chunking_time - if not is_win32: # TODO for win32 - with backup_io("fstat2"): - st2 = os.fstat(fd) - # special files: - # - fifos change naturally, because they are fed from the other side. no problem. - # - blk/chr devices don't change ctime anyway. - changed_while_backup = not is_special_file and st.st_ctime_ns != st2.st_ctime_ns - if changed_while_backup: - # regular file changed while we backed it up, might be inconsistent/corrupt! - if last_try: - status = "C" # crap! retries did not help. - else: - raise BackupError("file changed while we read it!") - if not is_special_file and not changed_while_backup: - # we must not memorize special files, because the contents of e.g. a - # block or char device will change without its mtime/size/inode changing. - # also, we must not memorize a potentially inconsistent/corrupt file that - # changed while we backed it up. - cache.memorize_file(hashed_path, path_hash, st, item.chunks) - self.stats.files_stats[status] += 1 # must be done late - if not changed_while_backup: - status = None # we already called print_file_status - self.stats.nfiles += 1 - item.update(self.metadata_collector.stat_ext_attrs(st, path, fd=fd)) - item.get_size(memorize=True) - return status - except BackupOSError: - # Something went wrong and we might need to clean up a bit. - # Maybe we have already incref'ed some file content chunks in the repo - - # but we will not add an item (see add_item in create_helper) and thus - # they would be orphaned chunks in case that we commit the transaction. - for chunk in item.get("chunks", []): - cache.chunk_decref(chunk.id, chunk.size, self.stats, wait=False) - # Now that we have cleaned up the chunk references, we can re-raise the exception. - # This will skip processing of this file, but might retry or continue with the next one. - raise + raise BackupError("file changed while we read it!") + if not is_special_file and not changed_while_backup: + # we must not memorize special files, because the contents of e.g. a + # block or char device will change without its mtime/size/inode changing. + # also, we must not memorize a potentially inconsistent/corrupt file that + # changed while we backed it up. + cache.memorize_file(hashed_path, path_hash, st, item.chunks) + self.stats.files_stats[status] += 1 # must be done late + if not changed_while_backup: + status = None # we already called print_file_status + self.stats.nfiles += 1 + item.update(self.metadata_collector.stat_ext_attrs(st, path, fd=fd)) + item.get_size(memorize=True) + return status class TarfileObjectProcessors: @@ -1524,21 +1502,15 @@ def process_file(self, *, tarinfo, status, type, tar): with self.create_helper(tarinfo, status, type) as (item, status): self.print_file_status(status, tarinfo.name) status = None # we already printed the status - try: - fd = tar.extractfile(tarinfo) - self.process_file_chunks( - item, self.cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(fd)) - ) - item.get_size(memorize=True, from_chunks=True) - self.stats.nfiles += 1 - # we need to remember ALL files, see HardLinkManager.__doc__ - self.hlm.remember(id=tarinfo.name, info=item.chunks) - return status - except BackupOSError: - # see comment in FilesystemObjectProcessors.process_file, same issue here. - for chunk in item.get("chunks", []): - self.cache.chunk_decref(chunk.id, chunk.size, self.stats, wait=False) - raise + fd = tar.extractfile(tarinfo) + self.process_file_chunks( + item, self.cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(fd)) + ) + item.get_size(memorize=True, from_chunks=True) + self.stats.nfiles += 1 + # we need to remember ALL files, see HardLinkManager.__doc__ + self.hlm.remember(id=tarinfo.name, info=item.chunks) + return status def valid_msgpacked_dict(d, keys_serialized): diff --git a/src/borg/cache.py b/src/borg/cache.py index 7c126c3cc..b36fb3c63 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -585,16 +585,6 @@ def chunk_incref(self, id, size, stats): stats.update(size, False) return ChunkListEntry(id, size) - def chunk_decref(self, id, size, stats, wait=True): - assert isinstance(size, int) and size > 0 - count, _size = self.chunks.decref(id) - if count == 0: - del self.chunks[id] - self.repository.delete(id, wait=wait) - stats.update(-size, True) - else: - stats.update(-size, False) - def seen_chunk(self, id, size=None): entry = self.chunks.get(id, ChunkIndexEntry(0, None)) if entry.refcount and size is not None: diff --git a/src/borg/hashindex.pyi b/src/borg/hashindex.pyi index a50a76e4c..14a22859f 100644 --- a/src/borg/hashindex.pyi +++ b/src/borg/hashindex.pyi @@ -38,7 +38,6 @@ class ChunkKeyIterator: class ChunkIndex(IndexBase): def add(self, key: bytes, refs: int, size: int) -> None: ... - def decref(self, key: bytes) -> CIE: ... def incref(self, key: bytes) -> CIE: ... def iteritems(self, marker: bytes = ...) -> Iterator: ... def merge(self, other_index) -> None: ... diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 4f0560523..594754e41 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -407,20 +407,6 @@ cdef class ChunkIndex(IndexBase): data[0] = _htole32(refcount) return refcount, _le32toh(data[1]) - def decref(self, key): - """Decrease refcount for 'key', return (refcount, size)""" - assert len(key) == self.key_size - data = hashindex_get(self.index, key) - if not data: - raise KeyError(key) - cdef uint32_t refcount = _le32toh(data[0]) - # Never decrease a reference count of zero - assert 0 < refcount <= _MAX_VALUE, "invalid reference count" - if refcount != _MAX_VALUE: - refcount -= 1 - data[0] = _htole32(refcount) - return refcount, _le32toh(data[1]) - def iteritems(self, marker=None): cdef const unsigned char *key iter = ChunkKeyIterator(self.key_size) diff --git a/src/borg/selftest.py b/src/borg/selftest.py index e410d0a33..c24a48e7c 100644 --- a/src/borg/selftest.py +++ b/src/borg/selftest.py @@ -33,7 +33,7 @@ ChunkerTestCase, ] -SELFTEST_COUNT = 30 +SELFTEST_COUNT = 28 class SelfTestResult(TestResult): diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index 2d86bede0..79b26e04c 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -7,7 +7,6 @@ from ..archive import Statistics from ..cache import AdHocCache from ..crypto.key import AESOCBRepoKey -from ..hashindex import ChunkIndex from ..manifest import Manifest from ..repository import Repository @@ -38,22 +37,9 @@ def cache(self, repository, key, manifest): def test_does_not_contain_manifest(self, cache): assert not cache.seen_chunk(Manifest.MANIFEST_ID) - def test_does_not_delete_existing_chunks(self, repository, cache): - assert cache.seen_chunk(H(1)) == ChunkIndex.MAX_VALUE - cache.chunk_decref(H(1), 1, Statistics()) - assert repository.get(H(1)) == b"1234" - def test_seen_chunk_add_chunk_size(self, cache): assert cache.add_chunk(H(1), {}, b"5678", stats=Statistics()) == (H(1), 4) - def test_deletes_chunks_during_lifetime(self, cache, repository): - cache.add_chunk(H(5), {}, b"1010", stats=Statistics()) - assert cache.seen_chunk(H(5)) == 1 - cache.chunk_decref(H(5), 1, Statistics()) - assert not cache.seen_chunk(H(5)) - with pytest.raises(Repository.ObjectNotFound): - repository.get(H(5)) - def test_files_cache(self, cache): assert cache.file_known_and_unchanged(b"foo", bytes(32), None) == (False, None) assert cache.cache_mode == "d" diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 34c3c9456..f853e021e 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -198,9 +198,6 @@ def test_chunkindex_limit(self): # first incref to move it to the limit refcount, *_ = idx.incref(H(1)) assert refcount == ChunkIndex.MAX_VALUE - for i in range(5): - refcount, *_ = idx.decref(H(1)) - assert refcount == ChunkIndex.MAX_VALUE def _merge(self, refcounta, refcountb): def merge(refcount1, refcount2): @@ -252,27 +249,12 @@ def test_incref_limit(self): refcount, *_ = idx1[H(1)] assert refcount == ChunkIndex.MAX_VALUE - def test_decref_limit(self): - idx1 = ChunkIndex() - idx1[H(1)] = ChunkIndex.MAX_VALUE, 6 - idx1.decref(H(1)) - refcount, *_ = idx1[H(1)] - assert refcount == ChunkIndex.MAX_VALUE - - def test_decref_zero(self): - idx1 = ChunkIndex() - idx1[H(1)] = 0, 0 - with self.assert_raises(AssertionError): - idx1.decref(H(1)) - - def test_incref_decref(self): + def test_incref(self): idx1 = ChunkIndex() idx1.add(H(1), 5, 6) assert idx1[H(1)] == (5, 6) idx1.incref(H(1)) assert idx1[H(1)] == (6, 6) - idx1.decref(H(1)) - assert idx1[H(1)] == (5, 6) def test_setitem_raises(self): idx1 = ChunkIndex() @@ -283,8 +265,6 @@ def test_keyerror(self): idx = ChunkIndex() with self.assert_raises(KeyError): idx.incref(H(1)) - with self.assert_raises(KeyError): - idx.decref(H(1)) with self.assert_raises(KeyError): idx[H(1)] with self.assert_raises(OverflowError): From bafbf625e73bc56e6737a25d100acbcdf6ef6a29 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 15:07:27 +0200 Subject: [PATCH 51/79] ArchiveChecker.verify_data: simplify / optimize .init_chunks has just built self.chunks using repository.list(), so don't call that again, but just iterate over self.chunks. also some other changes, making the code much simpler. --- src/borg/archive.py | 58 +++++++++++++++------------------------------ 1 file changed, 19 insertions(+), 39 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 240938d34..14a34ed4f 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1707,52 +1707,32 @@ def make_key(self, repository): def verify_data(self): logger.info("Starting cryptographic data integrity verification...") - chunks_count_index = len(self.chunks) - chunks_count_repo = 0 + chunks_count = len(self.chunks) errors = 0 defect_chunks = [] pi = ProgressIndicatorPercent( - total=chunks_count_index, msg="Verifying data %6.2f%%", step=0.01, msgid="check.verify_data" + total=chunks_count, msg="Verifying data %6.2f%%", step=0.01, msgid="check.verify_data" ) - marker = None - while True: - result = self.repository.list(limit=100, marker=marker) - if not result: - break - marker = result[-1][0] - chunks_count_repo += len(result) - chunk_data_iter = self.repository.get_many(id for id, _ in result) - result_revd = list(reversed(result)) - while result_revd: - pi.show() - chunk_id, _ = result_revd.pop(-1) # better efficiency + for chunk_id, _ in self.chunks.iteritems(): + pi.show() + try: + encrypted_data = self.repository.get(chunk_id) + except (Repository.ObjectNotFound, IntegrityErrorBase) as err: + self.error_found = True + errors += 1 + logger.error("chunk %s: %s", bin_to_hex(chunk_id), err) + if isinstance(err, IntegrityErrorBase): + defect_chunks.append(chunk_id) + else: try: - encrypted_data = next(chunk_data_iter) - except (Repository.ObjectNotFound, IntegrityErrorBase) as err: + # we must decompress, so it'll call assert_id() in there: + self.repo_objs.parse(chunk_id, encrypted_data, decompress=True, ro_type=ROBJ_DONTCARE) + except IntegrityErrorBase as integrity_error: self.error_found = True errors += 1 - logger.error("chunk %s: %s", bin_to_hex(chunk_id), err) - if isinstance(err, IntegrityErrorBase): - defect_chunks.append(chunk_id) - # as the exception killed our generator, make a new one for remaining chunks: - if result_revd: - result = list(reversed(result_revd)) - chunk_data_iter = self.repository.get_many(id for id, _ in result) - else: - try: - # we must decompress, so it'll call assert_id() in there: - self.repo_objs.parse(chunk_id, encrypted_data, decompress=True, ro_type=ROBJ_DONTCARE) - except IntegrityErrorBase as integrity_error: - self.error_found = True - errors += 1 - logger.error("chunk %s, integrity error: %s", bin_to_hex(chunk_id), integrity_error) - defect_chunks.append(chunk_id) + logger.error("chunk %s, integrity error: %s", bin_to_hex(chunk_id), integrity_error) + defect_chunks.append(chunk_id) pi.finish() - if chunks_count_index != chunks_count_repo: - logger.error("Chunks index object count vs. repository object count mismatch.") - logger.error( - "Chunks index: %d objects != Chunks repository: %d objects", chunks_count_index, chunks_count_repo - ) if defect_chunks: if self.repair: # if we kill the defect chunk here, subsequent actions within this "borg check" @@ -1791,7 +1771,7 @@ def verify_data(self): log = logger.error if errors else logger.info log( "Finished cryptographic data integrity verification, verified %d chunks with %d integrity errors.", - chunks_count_repo, + chunks_count, errors, ) From 266e6caa80403c4921bf08379f00c3be2bf37885 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 15:23:37 +0200 Subject: [PATCH 52/79] ArchiveChecker: remove unused possibly_superseded code We don't care about unused or superseded repo objects any more here, borg compact will deal with them. --- src/borg/archive.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 14a34ed4f..001d5ddaa 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1594,7 +1594,6 @@ def __next__(self): class ArchiveChecker: def __init__(self): self.error_found = False - self.possibly_superseded = set() def check( self, @@ -1842,10 +1841,6 @@ def rebuild_refcounts( if not isinstance(self.repository, (Repository, RemoteRepository)): self.chunks.pop(Manifest.MANIFEST_ID, None) - def mark_as_possibly_superseded(id_): - if self.chunks.get(id_, ChunkIndexEntry(0, 0)).refcount == 0: - self.possibly_superseded.add(id_) - def add_callback(chunk): id_ = self.key.id_hash(chunk) cdata = self.repo_objs.format(id_, {}, chunk, ro_type=ROBJ_ARCHIVE_STREAM) @@ -1931,7 +1926,6 @@ def replacement_chunk(size): ) ) add_reference(chunk_id, size) - mark_as_possibly_superseded(chunk_current[0]) # maybe orphaned the all-zero replacement chunk chunk_list.append([chunk_id, size]) # list-typed element as chunks_healthy is list-of-lists offset += size if chunks_replaced and not has_chunks_healthy: @@ -2062,7 +2056,6 @@ def valid_item(obj): self.error_found = True del self.manifest.archives[info.name] continue - mark_as_possibly_superseded(archive_id) cdata = self.repository.get(archive_id) try: _, data = self.repo_objs.parse(archive_id, cdata, ro_type=ROBJ_ARCHIVE_META) @@ -2082,12 +2075,6 @@ def valid_item(obj): verify_file_chunks(info.name, item) items_buffer.add(item) items_buffer.flush(flush=True) - for previous_item_id in archive_get_items( - archive, repo_objs=self.repo_objs, repository=self.repository - ): - mark_as_possibly_superseded(previous_item_id) - for previous_item_ptr in archive.item_ptrs: - mark_as_possibly_superseded(previous_item_ptr) archive.item_ptrs = archive_put_items( items_buffer.chunks, repo_objs=self.repo_objs, add_reference=add_reference ) @@ -2102,7 +2089,6 @@ def finish(self): if self.repair: logger.info("Writing Manifest.") self.manifest.write() - logger.info("Committing repo.") class ArchiveRecreater: From e9c42a7d6b9580c3dd44b10b65e5d9ffe21f6b99 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 15:43:32 +0200 Subject: [PATCH 53/79] ArchiveChecker: .rebuild_refcounts -> .rebuild_archives --- src/borg/archive.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 001d5ddaa..ddac5be90 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1645,7 +1645,7 @@ def check( if not isinstance(repository, (Repository, RemoteRepository)): del self.chunks[Manifest.MANIFEST_ID] self.manifest = self.rebuild_manifest() - self.rebuild_refcounts( + self.rebuild_archives( match=match, first=first, last=last, sort_by=sort_by, older=older, oldest=oldest, newer=newer, newest=newest ) self.finish() @@ -1830,13 +1830,10 @@ def valid_archive(obj): logger.info("Manifest rebuild complete.") return manifest - def rebuild_refcounts( + def rebuild_archives( self, first=0, last=0, sort_by="", match=None, older=None, newer=None, oldest=None, newest=None ): - """Rebuild object reference counts by walking the metadata - - Missing and/or incorrect data is repaired when detected - """ + """Analyze and rebuild archives, expecting some damage and trying to make stuff consistent again.""" # Exclude the manifest from chunks (manifest entry might be already deleted from self.chunks) if not isinstance(self.repository, (Repository, RemoteRepository)): self.chunks.pop(Manifest.MANIFEST_ID, None) @@ -2044,7 +2041,7 @@ def valid_item(obj): num_archives = len(archive_infos) pi = ProgressIndicatorPercent( - total=num_archives, msg="Checking archives %3.1f%%", step=0.1, msgid="check.rebuild_refcounts" + total=num_archives, msg="Checking archives %3.1f%%", step=0.1, msgid="check.rebuild_archives" ) with cache_if_remote(self.repository) as repository: for i, info in enumerate(archive_infos): From f9d2e6827bdf87f89ab2bad25585e08699d76db7 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 17:22:52 +0200 Subject: [PATCH 54/79] ArchiveChecker: don't do precise refcounting here That's the job of borg compact and not needed inside borg check. check only needs to know if a chunk is present in the repo. --- src/borg/archive.py | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index ddac5be90..8ce1c5d3b 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1619,14 +1619,14 @@ def check( :param oldest/newest: only check archives older/newer than timedelta from oldest/newest archive timestamp :param verify_data: integrity verification of data referenced by archives """ + if not isinstance(repository, (Repository, RemoteRepository)): + logger.error("Checking legacy repositories is not supported.") + return False logger.info("Starting archive consistency check...") self.check_all = not any((first, last, match, older, newer, oldest, newest)) self.repair = repair self.repository = repository self.init_chunks() - if not isinstance(repository, (Repository, RemoteRepository)) and not self.chunks: - logger.error("Repository contains no apparent data at all, cannot continue check/repair.") - return False self.key = self.make_key(repository) self.repo_objs = RepoObj(self.key) if verify_data: @@ -1642,8 +1642,6 @@ def check( except IntegrityErrorBase as exc: logger.error("Repository manifest is corrupted: %s", exc) self.error_found = True - if not isinstance(repository, (Repository, RemoteRepository)): - del self.chunks[Manifest.MANIFEST_ID] self.manifest = self.rebuild_manifest() self.rebuild_archives( match=match, first=first, last=last, sort_by=sort_by, older=older, oldest=oldest, newer=newer, newest=newest @@ -1656,10 +1654,7 @@ def check( return self.repair or not self.error_found def init_chunks(self): - """Fetch a list of all object keys from repository""" - # Explicitly set the initial usable hash table capacity to avoid performance issues - # due to hash table "resonance". - # Since reconstruction of archive items can add some new chunks, add 10 % headroom. + """Fetch a list of all object keys from repository and initialize self.chunks""" self.chunks = ChunkIndex() marker = None while True: @@ -1667,7 +1662,11 @@ def init_chunks(self): if not result: break marker = result[-1][0] - init_entry = ChunkIndexEntry(refcount=0, size=0) # unknown plaintext size (!= stored size!) + # the repo says it has these chunks, so we assume they are referenced chunks. + # we do not care for refcounting or garbage collection here, so we just set refcount = MAX_VALUE. + # borg compact will deal with any unused/orphan chunks. + # we do not know the plaintext size (!= stored_size), thus we set size = 0. + init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) for id, stored_size in result: self.chunks[id] = init_entry @@ -1834,9 +1833,6 @@ def rebuild_archives( self, first=0, last=0, sort_by="", match=None, older=None, newer=None, oldest=None, newest=None ): """Analyze and rebuild archives, expecting some damage and trying to make stuff consistent again.""" - # Exclude the manifest from chunks (manifest entry might be already deleted from self.chunks) - if not isinstance(self.repository, (Repository, RemoteRepository)): - self.chunks.pop(Manifest.MANIFEST_ID, None) def add_callback(chunk): id_ = self.key.id_hash(chunk) @@ -1844,12 +1840,11 @@ def add_callback(chunk): add_reference(id_, len(chunk), cdata) return id_ - def add_reference(id_, size, cdata=None): - try: - self.chunks.incref(id_) - except KeyError: + def add_reference(id_, size, cdata): + # either we already have this chunk in repo and chunks index or we add it now + if id_ not in self.chunks: assert cdata is not None - self.chunks[id_] = ChunkIndexEntry(refcount=1, size=size) + self.chunks[id_] = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=size) if self.repair: self.repository.put(id_, cdata) @@ -1900,9 +1895,7 @@ def replacement_chunk(size): ) ) chunk_id, size = chunk_current - if chunk_id in self.chunks: - add_reference(chunk_id, size) - else: + if chunk_id not in self.chunks: logger.warning( "{}: {}: Missing all-zero replacement chunk detected (Byte {}-{}, Chunk {}). " "Generating new replacement chunk.".format( @@ -1914,15 +1907,13 @@ def replacement_chunk(size): add_reference(chunk_id, size, cdata) else: if chunk_current == chunk_healthy: - # normal case, all fine. - add_reference(chunk_id, size) + pass # normal case, all fine. else: logger.info( "{}: {}: Healed previously missing file chunk! (Byte {}-{}, Chunk {}).".format( archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id) ) ) - add_reference(chunk_id, size) chunk_list.append([chunk_id, size]) # list-typed element as chunks_healthy is list-of-lists offset += size if chunks_replaced and not has_chunks_healthy: From ccc84c7a4eba753d10c275f96cebd4ab328bb516 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 17:59:26 +0200 Subject: [PATCH 55/79] cache: renamed .chunk_incref -> .reuse_chunk, boolean .seen_chunk reuse_chunk is the complement of add_chunk for already existing chunks. It doesn't do refcounting anymore. .seen_chunk does not return the refcount anymore, but just whether the chunk exists. If we add a new chunk, it immediately sets its refcount to MAX_VALUE, so there is no difference anymore between previously existing chunks and new chunks added. This makes the stats even more useless, but we have less complexity. --- src/borg/archive.py | 8 ++++---- src/borg/archiver/transfer_cmd.py | 4 ++-- src/borg/cache.py | 27 ++++++++++++--------------- src/borg/testsuite/cache.py | 9 ++++----- src/borg/upgrade.py | 2 +- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 8ce1c5d3b..57db40dc2 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1338,7 +1338,7 @@ def process_file(self, *, path, parent_fd, name, st, cache, flags=flags_normal, item.chunks = [] for chunk_id, chunk_size in hl_chunks: # process one-by-one, so we will know in item.chunks how far we got - chunk_entry = cache.chunk_incref(chunk_id, chunk_size, self.stats) + chunk_entry = cache.reuse_chunk(chunk_id, chunk_size, self.stats) item.chunks.append(chunk_entry) else: # normal case, no "2nd+" hardlink if not is_special_file: @@ -1364,7 +1364,7 @@ def process_file(self, *, path, parent_fd, name, st, cache, flags=flags_normal, item.chunks = [] for chunk in chunks: # process one-by-one, so we will know in item.chunks how far we got - cache.chunk_incref(chunk.id, chunk.size, self.stats) + cache.reuse_chunk(chunk.id, chunk.size, self.stats) item.chunks.append(chunk) status = "U" # regular file, unchanged else: @@ -2169,7 +2169,7 @@ def process_item(self, archive, target, item): def process_chunks(self, archive, target, item): if not target.recreate_rechunkify: for chunk_id, size in item.chunks: - self.cache.chunk_incref(chunk_id, size, target.stats) + self.cache.reuse_chunk(chunk_id, size, target.stats) return item.chunks chunk_iterator = self.iter_chunks(archive, target, list(item.chunks)) chunk_processor = partial(self.chunk_processor, target) @@ -2179,7 +2179,7 @@ def chunk_processor(self, target, chunk): chunk_id, data = cached_hash(chunk, self.key.id_hash) size = len(data) if chunk_id in self.seen_chunks: - return self.cache.chunk_incref(chunk_id, size, target.stats) + return self.cache.reuse_chunk(chunk_id, size, target.stats) chunk_entry = self.cache.add_chunk(chunk_id, {}, data, stats=target.stats, wait=False, ro_type=ROBJ_FILE_STREAM) self.cache.repository.async_response(wait=False) self.seen_chunks.add(chunk_entry.id) diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index 780513e55..b9e962869 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -100,7 +100,7 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non if "chunks" in item: chunks = [] for chunk_id, size in item.chunks: - chunk_present = cache.seen_chunk(chunk_id, size) != 0 + chunk_present = cache.seen_chunk(chunk_id, size) if not chunk_present: # target repo does not yet have this chunk if not dry_run: cdata = other_repository.get(chunk_id) @@ -147,7 +147,7 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non transfer_size += size else: if not dry_run: - chunk_entry = cache.chunk_incref(chunk_id, size, archive.stats) + chunk_entry = cache.reuse_chunk(chunk_id, size, archive.stats) chunks.append(chunk_entry) present_size += size if not dry_run: diff --git a/src/borg/cache.py b/src/borg/cache.py index b36fb3c63..3ea4f0370 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -579,12 +579,6 @@ def chunks(self): self._chunks = self._load_chunks_from_repo() return self._chunks - def chunk_incref(self, id, size, stats): - assert isinstance(size, int) and size > 0 - count, _size = self.chunks.incref(id) - stats.update(size, False) - return ChunkListEntry(id, size) - def seen_chunk(self, id, size=None): entry = self.chunks.get(id, ChunkIndexEntry(0, None)) if entry.refcount and size is not None: @@ -593,7 +587,12 @@ def seen_chunk(self, id, size=None): # AdHocWithFilesCache / AdHocCache: # Here *size* is used to update the chunk's size information, which will be zero for existing chunks. self.chunks[id] = entry._replace(size=size) - return entry.refcount + return entry.refcount != 0 + + def reuse_chunk(self, id, size, stats): + assert isinstance(size, int) and size > 0 + stats.update(size, False) + return ChunkListEntry(id, size) def add_chunk( self, @@ -615,15 +614,15 @@ def add_chunk( size = len(data) # data is still uncompressed else: raise ValueError("when giving compressed data for a chunk, the uncompressed size must be given also") - refcount = self.seen_chunk(id, size) - if refcount: - return self.chunk_incref(id, size, stats) + exists = self.seen_chunk(id, size) + if exists: + return self.reuse_chunk(id, size, stats) cdata = self.repo_objs.format( id, meta, data, compress=compress, size=size, ctype=ctype, clevel=clevel, ro_type=ro_type ) self.repository.put(id, cdata, wait=wait) - self.chunks.add(id, 1, size) - stats.update(size, not refcount) + self.chunks.add(id, ChunkIndex.MAX_VALUE, size) + stats.update(size, not exists) return ChunkListEntry(id, size) def _load_chunks_from_repo(self): @@ -639,9 +638,7 @@ def _load_chunks_from_repo(self): if not result: break marker = result[-1][0] - # All chunks from the repository have a refcount of MAX_VALUE, which is sticky, - # therefore we can't/won't delete them. Chunks we added ourselves in this borg run - # are tracked correctly. + # All chunks have a refcount of MAX_VALUE, which is sticky, therefore we can't/won't delete them. init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) # plaintext size for id, stored_size in result: num_chunks += 1 diff --git a/src/borg/testsuite/cache.py b/src/borg/testsuite/cache.py index 79b26e04c..f1e6e558a 100644 --- a/src/borg/testsuite/cache.py +++ b/src/borg/testsuite/cache.py @@ -45,11 +45,10 @@ def test_files_cache(self, cache): assert cache.cache_mode == "d" assert cache.files is None - def test_incref_after_add_chunk(self, cache): + def test_reuse_after_add_chunk(self, cache): assert cache.add_chunk(H(3), {}, b"5678", stats=Statistics()) == (H(3), 4) - assert cache.chunk_incref(H(3), 4, Statistics()) == (H(3), 4) + assert cache.reuse_chunk(H(3), 4, Statistics()) == (H(3), 4) - def test_existing_incref_after_add_chunk(self, cache): - """This case occurs with part files, see Archive.chunk_file.""" + def test_existing_reuse_after_add_chunk(self, cache): assert cache.add_chunk(H(1), {}, b"5678", stats=Statistics()) == (H(1), 4) - assert cache.chunk_incref(H(1), 4, Statistics()) == (H(1), 4) + assert cache.reuse_chunk(H(1), 4, Statistics()) == (H(1), 4) diff --git a/src/borg/upgrade.py b/src/borg/upgrade.py index 22a27c18c..35d71bec2 100644 --- a/src/borg/upgrade.py +++ b/src/borg/upgrade.py @@ -85,7 +85,7 @@ def upgrade_item(self, *, item): if chunks is not None: item.chunks = chunks for chunk_id, chunk_size in chunks: - self.cache.chunk_incref(chunk_id, chunk_size, self.archive.stats) + self.cache.reuse_chunk(chunk_id, chunk_size, self.archive.stats) if chunks_healthy is not None: item.chunks_healthy = chunks del item.source # not used for hardlinks any more, replaced by hlid From ddf6812703ff34d3319ba8d51c4de6a0c88eeb7e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 18:09:57 +0200 Subject: [PATCH 56/79] ChunkIndex: remove .incref method --- src/borg/hashindex.pyi | 1 - src/borg/hashindex.pyx | 13 ------------- src/borg/selftest.py | 2 +- src/borg/testsuite/hashindex.py | 26 -------------------------- 4 files changed, 1 insertion(+), 41 deletions(-) diff --git a/src/borg/hashindex.pyi b/src/borg/hashindex.pyi index 14a22859f..dbb8b7ad1 100644 --- a/src/borg/hashindex.pyi +++ b/src/borg/hashindex.pyi @@ -38,7 +38,6 @@ class ChunkKeyIterator: class ChunkIndex(IndexBase): def add(self, key: bytes, refs: int, size: int) -> None: ... - def incref(self, key: bytes) -> CIE: ... def iteritems(self, marker: bytes = ...) -> Iterator: ... def merge(self, other_index) -> None: ... def stats_against(self, master_index) -> Tuple: ... diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 594754e41..3668169bb 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -394,19 +394,6 @@ cdef class ChunkIndex(IndexBase): assert _le32toh(data[0]) <= _MAX_VALUE, "invalid reference count" return data != NULL - def incref(self, key): - """Increase refcount for 'key', return (refcount, size)""" - assert len(key) == self.key_size - data = hashindex_get(self.index, key) - if not data: - raise KeyError(key) - cdef uint32_t refcount = _le32toh(data[0]) - assert refcount <= _MAX_VALUE, "invalid reference count" - if refcount != _MAX_VALUE: - refcount += 1 - data[0] = _htole32(refcount) - return refcount, _le32toh(data[1]) - def iteritems(self, marker=None): cdef const unsigned char *key iter = ChunkKeyIterator(self.key_size) diff --git a/src/borg/selftest.py b/src/borg/selftest.py index c24a48e7c..8bca0a041 100644 --- a/src/borg/selftest.py +++ b/src/borg/selftest.py @@ -33,7 +33,7 @@ ChunkerTestCase, ] -SELFTEST_COUNT = 28 +SELFTEST_COUNT = 25 class SelfTestResult(TestResult): diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index f853e021e..69e1455d9 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -189,16 +189,6 @@ def test_size_on_disk_accurate(self): class HashIndexRefcountingTestCase(BaseTestCase): - def test_chunkindex_limit(self): - idx = ChunkIndex() - idx[H(1)] = ChunkIndex.MAX_VALUE - 1, 1 - - # 5 is arbitrary, any number of incref/decrefs shouldn't move it once it's limited - for i in range(5): - # first incref to move it to the limit - refcount, *_ = idx.incref(H(1)) - assert refcount == ChunkIndex.MAX_VALUE - def _merge(self, refcounta, refcountb): def merge(refcount1, refcount2): idx1 = ChunkIndex() @@ -242,20 +232,6 @@ def test_chunkindex_add(self): idx1.add(H(1), 1, 2) assert idx1[H(1)] == (6, 2) - def test_incref_limit(self): - idx1 = ChunkIndex() - idx1[H(1)] = ChunkIndex.MAX_VALUE, 6 - idx1.incref(H(1)) - refcount, *_ = idx1[H(1)] - assert refcount == ChunkIndex.MAX_VALUE - - def test_incref(self): - idx1 = ChunkIndex() - idx1.add(H(1), 5, 6) - assert idx1[H(1)] == (5, 6) - idx1.incref(H(1)) - assert idx1[H(1)] == (6, 6) - def test_setitem_raises(self): idx1 = ChunkIndex() with self.assert_raises(AssertionError): @@ -263,8 +239,6 @@ def test_setitem_raises(self): def test_keyerror(self): idx = ChunkIndex() - with self.assert_raises(KeyError): - idx.incref(H(1)) with self.assert_raises(KeyError): idx[H(1)] with self.assert_raises(OverflowError): From 15c70397c12bec19b02a81278daec63360224681 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 18:26:26 +0200 Subject: [PATCH 57/79] ChunkIndex: remove unused .merge method LocalCache used this to assemble a new overall chunks index from multiple chunks.archive.d's single-archive chunks indexes. --- src/borg/hashindex.pyi | 1 - src/borg/hashindex.pyx | 9 ---- src/borg/selftest.py | 2 +- src/borg/testsuite/hashindex.py | 75 --------------------------------- 4 files changed, 1 insertion(+), 86 deletions(-) diff --git a/src/borg/hashindex.pyi b/src/borg/hashindex.pyi index dbb8b7ad1..e8492ec85 100644 --- a/src/borg/hashindex.pyi +++ b/src/borg/hashindex.pyi @@ -39,7 +39,6 @@ class ChunkKeyIterator: class ChunkIndex(IndexBase): def add(self, key: bytes, refs: int, size: int) -> None: ... def iteritems(self, marker: bytes = ...) -> Iterator: ... - def merge(self, other_index) -> None: ... def stats_against(self, master_index) -> Tuple: ... def summarize(self) -> Tuple: ... def zero_csize_ids(self) -> int: ... diff --git a/src/borg/hashindex.pyx b/src/borg/hashindex.pyx index 3668169bb..6e9404f10 100644 --- a/src/borg/hashindex.pyx +++ b/src/borg/hashindex.pyx @@ -428,15 +428,6 @@ cdef class ChunkIndex(IndexBase): if not hashindex_set(self.index, key, data): raise Exception('hashindex_set failed') - def merge(self, ChunkIndex other): - cdef unsigned char *key = NULL - - while True: - key = hashindex_next_key(other.index, key) - if not key: - break - self._add(key, (key + self.key_size)) - cdef class ChunkKeyIterator: cdef ChunkIndex idx diff --git a/src/borg/selftest.py b/src/borg/selftest.py index 8bca0a041..e53ae0683 100644 --- a/src/borg/selftest.py +++ b/src/borg/selftest.py @@ -33,7 +33,7 @@ ChunkerTestCase, ] -SELFTEST_COUNT = 25 +SELFTEST_COUNT = 19 class SelfTestResult(TestResult): diff --git a/src/borg/testsuite/hashindex.py b/src/borg/testsuite/hashindex.py index 69e1455d9..6bf3a2ab1 100644 --- a/src/borg/testsuite/hashindex.py +++ b/src/borg/testsuite/hashindex.py @@ -124,23 +124,6 @@ def test_iteritems(self): self.assert_equal(len(second_half), 50) self.assert_equal(second_half, all[50:]) - def test_chunkindex_merge(self): - idx1 = ChunkIndex() - idx1[H(1)] = 1, 100 - idx1[H(2)] = 2, 200 - idx1[H(3)] = 3, 300 - # no H(4) entry - idx2 = ChunkIndex() - idx2[H(1)] = 4, 100 - idx2[H(2)] = 5, 200 - # no H(3) entry - idx2[H(4)] = 6, 400 - idx1.merge(idx2) - assert idx1[H(1)] == (5, 100) - assert idx1[H(2)] == (7, 200) - assert idx1[H(3)] == (3, 300) - assert idx1[H(4)] == (6, 400) - class HashIndexExtraTestCase(BaseTestCase): """These tests are separate because they should not become part of the selftest.""" @@ -189,42 +172,6 @@ def test_size_on_disk_accurate(self): class HashIndexRefcountingTestCase(BaseTestCase): - def _merge(self, refcounta, refcountb): - def merge(refcount1, refcount2): - idx1 = ChunkIndex() - idx1[H(1)] = refcount1, 1 - idx2 = ChunkIndex() - idx2[H(1)] = refcount2, 1 - idx1.merge(idx2) - refcount, *_ = idx1[H(1)] - return refcount - - result = merge(refcounta, refcountb) - # check for commutativity - assert result == merge(refcountb, refcounta) - return result - - def test_chunkindex_merge_limit1(self): - # Check that it does *not* limit at MAX_VALUE - 1 - # (MAX_VALUE is odd) - half = ChunkIndex.MAX_VALUE // 2 - assert self._merge(half, half) == ChunkIndex.MAX_VALUE - 1 - - def test_chunkindex_merge_limit2(self): - # 3000000000 + 2000000000 > MAX_VALUE - assert self._merge(3000000000, 2000000000) == ChunkIndex.MAX_VALUE - - def test_chunkindex_merge_limit3(self): - # Crossover point: both addition and limit semantics will yield the same result - half = ChunkIndex.MAX_VALUE // 2 - assert self._merge(half + 1, half) == ChunkIndex.MAX_VALUE - - def test_chunkindex_merge_limit4(self): - # Beyond crossover, result of addition would be 2**31 - half = ChunkIndex.MAX_VALUE // 2 - assert self._merge(half + 2, half) == ChunkIndex.MAX_VALUE - assert self._merge(half + 1, half + 1) == ChunkIndex.MAX_VALUE - def test_chunkindex_add(self): idx1 = ChunkIndex() idx1.add(H(1), 5, 6) @@ -283,17 +230,6 @@ def test_identical_creation(self): serialized = self._serialize_hashindex(idx1) assert self._unpack(serialized) == self._unpack(self.HASHINDEX) - def test_read_known_good(self): - idx1 = self._deserialize_hashindex(self.HASHINDEX) - assert idx1[H(1)] == (1, 2) - assert idx1[H(2)] == (2**31 - 1, 0) - assert idx1[H(3)] == (4294962296, 0) - - idx2 = ChunkIndex() - idx2[H(3)] = 2**32 - 123456, 6 - idx1.merge(idx2) - assert idx1[H(3)] == (ChunkIndex.MAX_VALUE, 6) - class HashIndexIntegrityTestCase(HashIndexDataTestCase): def write_integrity_checked_index(self, tempdir): @@ -427,17 +363,6 @@ def test_all_at_back(self): self.compare_compact("ED****") self.compare_compact("D*****") - def test_merge(self): - master = ChunkIndex() - idx1 = ChunkIndex() - idx1[H(1)] = 1, 100 - idx1[H(2)] = 2, 200 - idx1[H(3)] = 3, 300 - idx1.compact() - assert idx1.size() == 1024 + 3 * (32 + 2 * 4) - master.merge(idx1) - self.compare_indexes(idx1, master) - class NSIndexTestCase(BaseTestCase): def test_nsindex_segment_limit(self): From 07ab6e02f42da58d71f554b6b2b9784e49a494d0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 20:53:06 +0200 Subject: [PATCH 58/79] hashindex types: remove some unused stuff --- src/borg/hashindex.pyi | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/borg/hashindex.pyi b/src/borg/hashindex.pyi index e8492ec85..722baaf9f 100644 --- a/src/borg/hashindex.pyi +++ b/src/borg/hashindex.pyi @@ -39,9 +39,6 @@ class ChunkKeyIterator: class ChunkIndex(IndexBase): def add(self, key: bytes, refs: int, size: int) -> None: ... def iteritems(self, marker: bytes = ...) -> Iterator: ... - def stats_against(self, master_index) -> Tuple: ... - def summarize(self) -> Tuple: ... - def zero_csize_ids(self) -> int: ... def __contains__(self, key: bytes) -> bool: ... def __getitem__(self, key: bytes) -> Type[ChunkIndexEntry]: ... def __setitem__(self, key: bytes, value: CIE) -> None: ... @@ -61,22 +58,14 @@ class NSIndex(IndexBase): def __contains__(self, key: bytes) -> bool: ... def __getitem__(self, key: bytes) -> Any: ... def __setitem__(self, key: bytes, value: Any) -> None: ... - def flags(self, key: bytes, mask: int, value: int = None) -> int: ... class NSIndex1(IndexBase): # legacy def iteritems(self, *args, **kwargs) -> Iterator: ... def __contains__(self, key: bytes) -> bool: ... def __getitem__(self, key: bytes) -> Any: ... def __setitem__(self, key: bytes, value: Any) -> None: ... - def flags(self, key: bytes, mask: int, value: int = None) -> int: ... class FuseVersionsIndex(IndexBase): def __contains__(self, key: bytes) -> bool: ... def __getitem__(self, key: bytes) -> Any: ... def __setitem__(self, key: bytes, value: Any) -> None: ... - -class CacheSynchronizer: - size_totals: int - num_files_totals: int - def __init__(self, chunks_index: Any) -> None: ... - def feed(self, chunk: bytes) -> None: ... From e2aa9d56d0720b1415ae8a456d2a11059afd5fa0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 29 Aug 2024 23:07:18 +0200 Subject: [PATCH 59/79] build_chunkindex_from_repo: reduce code duplication --- src/borg/archive.py | 23 +++------------- src/borg/cache.py | 66 ++++++++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 54 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 57db40dc2..2012060a7 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -22,7 +22,7 @@ from . import xattr from .chunker import get_chunker, Chunk -from .cache import ChunkListEntry +from .cache import ChunkListEntry, build_chunkindex_from_repo from .crypto.key import key_factory, UnsupportedPayloadError from .compress import CompressionSpec from .constants import * # NOQA @@ -50,7 +50,7 @@ from .item import Item, ArchiveItem, ItemDiff from .platform import acl_get, acl_set, set_flags, get_flags, swidth, hostname from .remote import RemoteRepository, cache_if_remote -from .repository import Repository, LIST_SCAN_LIMIT, NoManifestError +from .repository import Repository, NoManifestError from .repoobj import RepoObj has_link = hasattr(os, "link") @@ -1626,7 +1626,7 @@ def check( self.check_all = not any((first, last, match, older, newer, oldest, newest)) self.repair = repair self.repository = repository - self.init_chunks() + self.chunks = build_chunkindex_from_repo(self.repository) self.key = self.make_key(repository) self.repo_objs = RepoObj(self.key) if verify_data: @@ -1653,23 +1653,6 @@ def check( logger.info("Archive consistency check complete, no problems found.") return self.repair or not self.error_found - def init_chunks(self): - """Fetch a list of all object keys from repository and initialize self.chunks""" - self.chunks = ChunkIndex() - marker = None - while True: - result = self.repository.list(limit=LIST_SCAN_LIMIT, marker=marker) - if not result: - break - marker = result[-1][0] - # the repo says it has these chunks, so we assume they are referenced chunks. - # we do not care for refcounting or garbage collection here, so we just set refcount = MAX_VALUE. - # borg compact will deal with any unused/orphan chunks. - # we do not know the plaintext size (!= stored_size), thus we set size = 0. - init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) - for id, stored_size in result: - self.chunks[id] = init_entry - def make_key(self, repository): attempt = 0 diff --git a/src/borg/cache.py b/src/borg/cache.py index 3ea4f0370..5107d847b 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -565,6 +565,37 @@ def memorize_file(self, hashed_path, path_hash, st, chunks): ) +def build_chunkindex_from_repo(repository): + logger.debug("querying the chunk IDs list from the repo...") + chunks = ChunkIndex() + t0 = perf_counter() + num_requests = 0 + num_chunks = 0 + marker = None + while True: + result = repository.list(limit=LIST_SCAN_LIMIT, marker=marker) + num_requests += 1 + if not result: + break + marker = result[-1][0] + # The repo says it has these chunks, so we assume they are referenced chunks. + # We do not care for refcounting anymore, so we just set refcount = MAX_VALUE. + # We do not know the plaintext size (!= stored_size), thus we set size = 0. + init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) + for id, stored_size in result: + num_chunks += 1 + chunks[id] = init_entry + # Cache does not contain the manifest. + if not isinstance(repository, (Repository, RemoteRepository)): + del chunks[Manifest.MANIFEST_ID] + duration = perf_counter() - t0 or 0.001 + # Chunk IDs in a list are encoded in 34 bytes: 1 byte msgpack header, 1 byte length, 32 ID bytes. + # Protocol overhead is neglected in this calculation. + speed = format_file_size(num_chunks * 34 / duration) + logger.debug(f"queried {num_chunks} chunk IDs in {duration} s ({num_requests} requests), ~{speed}/s") + return chunks + + class ChunksMixin: """ Chunks index related code for misc. Cache implementations. @@ -576,7 +607,7 @@ def __init__(self): @property def chunks(self): if self._chunks is None: - self._chunks = self._load_chunks_from_repo() + self._chunks = build_chunkindex_from_repo(self.repository) return self._chunks def seen_chunk(self, id, size=None): @@ -625,39 +656,6 @@ def add_chunk( stats.update(size, not exists) return ChunkListEntry(id, size) - def _load_chunks_from_repo(self): - logger.debug("Cache: querying the chunk IDs list from the repo...") - chunks = ChunkIndex() - t0 = perf_counter() - num_requests = 0 - num_chunks = 0 - marker = None - while True: - result = self.repository.list(limit=LIST_SCAN_LIMIT, marker=marker) - num_requests += 1 - if not result: - break - marker = result[-1][0] - # All chunks have a refcount of MAX_VALUE, which is sticky, therefore we can't/won't delete them. - init_entry = ChunkIndexEntry(refcount=ChunkIndex.MAX_VALUE, size=0) # plaintext size - for id, stored_size in result: - num_chunks += 1 - chunks[id] = init_entry - # Cache does not contain the manifest. - if not isinstance(self.repository, (Repository, RemoteRepository)): - del chunks[self.manifest.MANIFEST_ID] - duration = perf_counter() - t0 or 0.01 - logger.debug( - "Cache: queried %d chunk IDs in %.2f s (%d requests), ~%s/s", - num_chunks, - duration, - num_requests, - format_file_size(num_chunks * 34 / duration), - ) - # Chunk IDs in a list are encoded in 34 bytes: 1 byte msgpack header, 1 byte length, 32 ID bytes. - # Protocol overhead is neglected in this calculation. - return chunks - class AdHocWithFilesCache(FilesCacheMixin, ChunksMixin): """ From 551834acfc788d0580704374b38c46d2a908cf66 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 30 Aug 2024 00:09:41 +0200 Subject: [PATCH 60/79] rcompress: not supported for legacy repos --- src/borg/archiver/rcompress_cmd.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index aef976a32..58c58931c 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -102,6 +102,9 @@ def get_csettings(c): ctype, clevel, olevel = c.ID, c.level, -1 return ctype, clevel, olevel + if not isinstance(repository, (Repository, RemoteRepository)): + raise Error("rcompress not supported for legacy repositories.") + repo_objs = manifest.repo_objs ctype, clevel, olevel = get_csettings(repo_objs.compressor) # desired compression set by --compression @@ -111,11 +114,6 @@ def get_csettings(c): recompress_candidate_count = len(recompress_ids) chunks_limit = min(1000, max(100, recompress_candidate_count // 1000)) - if not isinstance(repository, (Repository, RemoteRepository)): - # start a new transaction - data = repository.get_manifest() - repository.put_manifest(data) - pi = ProgressIndicatorPercent( total=len(recompress_ids), msg="Recompressing %3.1f%%", step=0.1, msgid="rcompress.process_chunks" ) From 86dc673649413c096cd76752d42dd4c832d581d1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 31 Aug 2024 02:44:04 +0200 Subject: [PATCH 61/79] compact: fix dsize computation if wanted chunks are present in repo --- src/borg/archiver/compact_cmd.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index 51144a107..cf905fd1c 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -4,7 +4,7 @@ from ._common import with_repository from ..archive import Archive from ..constants import * # NOQA -from ..helpers import set_ec, EXIT_WARNING, EXIT_ERROR, format_file_size +from ..helpers import set_ec, EXIT_WARNING, EXIT_ERROR, format_file_size, bin_to_hex from ..helpers import ProgressIndicatorPercent from ..manifest import Manifest from ..remote import RemoteRepository @@ -133,7 +133,14 @@ def report_and_delete(self): logger.info( f"Source data size was {format_file_size(self.total_size, precision=0)} in {self.total_files} files." ) - dsize = sum(self.used_chunks[id] for id in self.repository_chunks) + dsize = 0 + for id in self.repository_chunks: + if id in self.used_chunks: + dsize += self.used_chunks[id] + elif id in self.wanted_chunks: + dsize += self.wanted_chunks[id] + else: + raise KeyError(bin_to_hex(id)) logger.info(f"Repository size is {format_file_size(self.repository_size, precision=0)} in {count} objects.") if self.total_size != 0: logger.info(f"Space reduction factor due to deduplication: {dsize / self.total_size:.3f}") From dc9fff9953dcc83d51aea255b26b5de90ecd0ba4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 31 Aug 2024 04:13:04 +0200 Subject: [PATCH 62/79] locking: ignore+delete locks of dead processes --- src/borg/storelocking.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/borg/storelocking.py b/src/borg/storelocking.py index 091dbbf44..bc049cab9 100644 --- a/src/borg/storelocking.py +++ b/src/borg/storelocking.py @@ -107,8 +107,17 @@ def _delete_lock(self, key, *, ignore_not_found=False): if not ignore_not_found: raise - def _get_locks(self): + def _is_stale_lock(self, lock): now = datetime.datetime.now(datetime.timezone.utc) + if lock["dt"] < now - self.stale_td: + # lock is too old, it was not refreshed. + return True + if not platform.process_alive(lock["hostid"], lock["processid"], lock["threadid"]): + # we KNOW that the lock owning process is dead. + return True + return False + + def _get_locks(self): locks = {} try: infos = list(self.store.list("locks")) @@ -118,14 +127,12 @@ def _get_locks(self): key = info.name content = self.store.load(f"locks/{key}") lock = json.loads(content.decode("utf-8")) - dt = datetime.datetime.fromisoformat(lock["time"]) - stale = dt < now - self.stale_td - if stale: + lock["key"] = key + lock["dt"] = datetime.datetime.fromisoformat(lock["time"]) + if self._is_stale_lock(lock): # ignore it and delete it (even if it is not from us) self._delete_lock(key, ignore_not_found=True) else: - lock["key"] = key - lock["dt"] = dt locks[key] = lock return locks From 60a592d50f7c534021e06745319c4a785da6ab6d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 31 Aug 2024 19:32:15 +0200 Subject: [PATCH 63/79] with-lock: refresh repo lock while subprocess is running, fixes #8347 otherwise the lock might become stale and could get killed by any other borg process. note: ThreadRunner class written by PyCharm AI and only needed small enhancements. nice. --- src/borg/archiver/lock_cmds.py | 8 +++++- src/borg/helpers/__init__.py | 2 +- src/borg/helpers/process.py | 48 ++++++++++++++++++++++++++++++++++ src/borg/repository.py | 2 ++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver/lock_cmds.py b/src/borg/archiver/lock_cmds.py index a8839b631..1a9c0051e 100644 --- a/src/borg/archiver/lock_cmds.py +++ b/src/borg/archiver/lock_cmds.py @@ -4,7 +4,7 @@ from ._common import with_repository from ..cache import Cache from ..constants import * # NOQA -from ..helpers import prepare_subprocess_env, set_ec, CommandError +from ..helpers import prepare_subprocess_env, set_ec, CommandError, ThreadRunner from ..logger import create_logger @@ -15,6 +15,10 @@ class LocksMixIn: @with_repository(manifest=False, exclusive=True) def do_with_lock(self, args, repository): """run a user specified command with the repository lock held""" + # the repository lock needs to get refreshed regularly, or it will be killed as stale. + # refreshing the lock is not part of the repository API, so we do it indirectly via repository.info. + lock_refreshing_thread = ThreadRunner(sleep_interval=60, target=repository.info) + lock_refreshing_thread.start() env = prepare_subprocess_env(system=True) try: # we exit with the return code we get from the subprocess @@ -22,6 +26,8 @@ def do_with_lock(self, args, repository): set_ec(rc) except (FileNotFoundError, OSError, ValueError) as e: raise CommandError(f"Error while trying to run '{args.command}': {e}") + finally: + lock_refreshing_thread.terminate() @with_repository(lock=False, manifest=False) def do_break_lock(self, args, repository): diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py index d62e45f1d..23833dd52 100644 --- a/src/borg/helpers/__init__.py +++ b/src/borg/helpers/__init__.py @@ -39,7 +39,7 @@ from .parseformat import swidth_slice, ellipsis_truncate from .parseformat import BorgJsonEncoder, basic_json_data, json_print, json_dump, prepare_dump_dict from .parseformat import Highlander, MakePathSafeAction -from .process import daemonize, daemonizing +from .process import daemonize, daemonizing, ThreadRunner from .process import signal_handler, raising_signal_handler, sig_int, ignore_sigint, SigHup, SigTerm from .process import popen_with_error_handling, is_terminal, prepare_subprocess_env, create_filter_process from .progress import ProgressIndicatorPercent, ProgressIndicatorMessage diff --git a/src/borg/helpers/process.py b/src/borg/helpers/process.py index cd8303d85..1112f9807 100644 --- a/src/borg/helpers/process.py +++ b/src/borg/helpers/process.py @@ -6,6 +6,7 @@ import subprocess import sys import time +import threading import traceback from .. import __version__ @@ -398,3 +399,50 @@ def create_filter_process(cmd, stream, stream_close, inbound=True): if borg_succeeded and rc: # if borg did not succeed, we know that we killed the filter process raise Error("filter %s failed, rc=%d" % (cmd, rc)) + + +class ThreadRunner: + def __init__(self, sleep_interval, target, *args, **kwargs): + """ + Initialize the ThreadRunner with a target function and its arguments. + + :param sleep_interval: The interval (in seconds) to sleep between executions of the target function. + :param target: The target function to be run in the thread. + :param args: The positional arguments to be passed to the target function. + :param kwargs: The keyword arguments to be passed to the target function. + """ + self._target = target + self._args = args + self._kwargs = kwargs + self._sleep_interval = sleep_interval + self._thread = None + self._keep_running = threading.Event() + self._keep_running.set() + + def _run_with_termination(self): + """ + Wrapper function to check if the thread should keep running. + """ + while self._keep_running.is_set(): + self._target(*self._args, **self._kwargs) + # sleep up to self._sleep_interval, but end the sleep early if we shall not keep running: + count = 1000 + micro_sleep = float(self._sleep_interval) / count + while self._keep_running.is_set() and count > 0: + time.sleep(micro_sleep) + count -= 1 + + def start(self): + """ + Start the thread. + """ + self._thread = threading.Thread(target=self._run_with_termination) + self._thread.start() + + def terminate(self): + """ + Signal the thread to stop and wait for it to finish. + """ + if self._thread is not None: + self._keep_running.clear() + self._thread.join() diff --git a/src/borg/repository.py b/src/borg/repository.py index c827f279d..f5b3dc20c 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -203,6 +203,8 @@ def close(self): def info(self): """return some infos about the repo (must be opened first)""" + # note: don't do anything expensive here or separate the lock refresh into a separate method. + self._lock_refresh() # do not remove, see do_with_lock() info = dict( id=self.id, version=self.version, From 7bf0f47fea2a8783e3874c214f085aa6e86b398a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 31 Aug 2024 21:21:45 +0200 Subject: [PATCH 64/79] check repository: implement --max-duration and checkpoints, fixes #6039 --- src/borg/repository.py | 45 ++++++++++++++++++++++-- src/borg/testsuite/archiver/check_cmd.py | 6 ++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index f5b3dc20c..691c3f45a 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1,4 +1,5 @@ import os +import time from borgstore.store import Store from borgstore.store import ObjectNotFound as StoreObjectNotFound @@ -241,15 +242,38 @@ def check_object(obj): else: log_error("too small.") - # TODO: progress indicator, partial checks, ... - mode = "full" - logger.info("Starting repository check") + # TODO: progress indicator, ... + partial = bool(max_duration) + assert not (repair and partial) + mode = "partial" if partial else "full" + logger.info(f"Starting {mode} repository check") + if partial: + # continue a past partial check (if any) or from a checkpoint or start one from beginning + try: + last_key_checked = self.store.load("config/last-key-checked").decode() + except StoreObjectNotFound: + last_key_checked = "" + else: + # start from the beginning and also forget about any potential past partial checks + last_key_checked = "" + try: + self.store.delete("config/last-key-checked") + except StoreObjectNotFound: + pass + if last_key_checked: + logger.info(f"Skipping to keys after {last_key_checked}.") + else: + logger.info("Starting from beginning.") + t_start = time.monotonic() + t_last_checkpoint = t_start objs_checked = objs_errors = 0 infos = self.store.list("data") try: for info in infos: self._lock_refresh() key = "data/%s" % info.name + if key <= last_key_checked: # needs sorted keys + continue try: obj = self.store.load(key) except StoreObjectNotFound: @@ -275,6 +299,21 @@ def check_object(obj): self.store.delete(key) else: log_error("reloading did help, inconsistent behaviour detected!") + now = time.monotonic() + if now > t_last_checkpoint + 300: # checkpoint every 5 mins + t_last_checkpoint = now + logger.info(f"Checkpointing at key {key}.") + self.store.store("config/last-key-checked", key.encode()) + if partial and now > t_start + max_duration: + logger.info(f"Finished partial repository check, last key checked is {key}.") + self.store.store("config/last-key-checked", key.encode()) + break + else: + logger.info("Finished repository check.") + try: + self.store.delete("config/last-key-checked") + except StoreObjectNotFound: + pass except StoreObjectNotFound: # it can be that there is no "data/" at all, then it crashes when iterating infos. pass diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index d38e38d87..b6265f0c1 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -28,15 +28,15 @@ def test_check_usage(archivers, request): check_cmd_setup(archiver) output = cmd(archiver, "check", "-v", "--progress", exit_code=0) - assert "Starting repository check" in output + assert "Starting full repository check" in output assert "Starting archive consistency check" in output output = cmd(archiver, "check", "-v", "--repository-only", exit_code=0) - assert "Starting repository check" in output + assert "Starting full repository check" in output assert "Starting archive consistency check" not in output output = cmd(archiver, "check", "-v", "--archives-only", exit_code=0) - assert "Starting repository check" not in output + assert "Starting full repository check" not in output assert "Starting archive consistency check" in output output = cmd(archiver, "check", "-v", "--archives-only", "--match-archives=archive2", exit_code=0) From 1cd2f4dca364b10f3a1f1a0c6ee17abb88863517 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 31 Aug 2024 23:40:27 +0200 Subject: [PATCH 65/79] locking: deal with potential auto-expire during suspend --- src/borg/storelocking.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/borg/storelocking.py b/src/borg/storelocking.py index bc049cab9..8f1979eb3 100644 --- a/src/borg/storelocking.py +++ b/src/borg/storelocking.py @@ -225,7 +225,18 @@ def refresh(self): now = datetime.datetime.now(datetime.timezone.utc) if self.last_refresh_dt is not None and now > self.last_refresh_dt + self.refresh_td: old_locks = self._find_locks(only_mine=True) - assert len(old_locks) == 1 + if len(old_locks) == 0: + # crap, my lock has been removed. :-( + # this can happen e.g. if my machine has been suspended while doing a backup, so that the + # lock will auto-expire. a borg client on another machine might then kill that lock. + # if my machine then wakes up again, the lock will have vanished and we get here. + # in this case, we need to abort the operation, because the other borg might have removed + # repo objects we have written, but the referential tree was not yet full present, e.g. + # no archive has been added yet to the manifest, thus all objects looked unused/orphaned. + # another scenario when this can happen is a careless user running break-lock on another + # machine without making sure there is no borg activity in that repo. + raise LockTimeout(str(self.store)) # our lock was killed, there is no safe way to continue. + assert len(old_locks) == 1 # there shouldn't be more than 1 old_lock = old_locks[0] if old_lock["dt"] < now - self.refresh_td: self._create_lock(exclusive=old_lock["exclusive"]) From b14c050f690aa249291126a491da4193fedd77a0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 1 Sep 2024 02:07:32 +0200 Subject: [PATCH 66/79] 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 @@ def get_func(args): 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 @@ def build_parser(self): 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 @@ def do_rcreate(self, args, repository, *, other_repository=None, other_manifest= " 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.", + ) From ace97fadec4ed84b9e54524d79edcbe1f38912f4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 1 Sep 2024 15:39:50 +0200 Subject: [PATCH 67/79] docs: updates / removing outdated stuff --- README.rst | 2 +- docs/deployment/hosting-repositories.rst | 2 - docs/faq.rst | 90 +---- docs/internals/compaction.odg | Bin 28928 -> 0 bytes docs/internals/compaction.png | Bin 331649 -> 0 bytes docs/internals/data-structures.rst | 493 +++++++---------------- docs/internals/object-graph.odg | Bin 29733 -> 28430 bytes docs/internals/object-graph.png | Bin 389349 -> 99968 bytes docs/internals/security.rst | 30 +- docs/quickstart.rst | 21 +- 10 files changed, 179 insertions(+), 459 deletions(-) delete mode 100644 docs/internals/compaction.odg delete mode 100644 docs/internals/compaction.png diff --git a/README.rst b/README.rst index 00157d847..f65cf9da8 100644 --- a/README.rst +++ b/README.rst @@ -69,7 +69,7 @@ Main features **Speed** * performance-critical code (chunking, compression, encryption) is implemented in C/Cython - * local caching of files/chunks index data + * local caching * quick detection of unmodified files **Data encryption** diff --git a/docs/deployment/hosting-repositories.rst b/docs/deployment/hosting-repositories.rst index 55fd3e15e..b0efbf696 100644 --- a/docs/deployment/hosting-repositories.rst +++ b/docs/deployment/hosting-repositories.rst @@ -68,8 +68,6 @@ can be filled to the specified quota. If storage quotas are used, ensure that all deployed Borg releases support storage quotas. -Refer to :ref:`internals_storage_quota` for more details on storage quotas. - **Specificities: Append-only repositories** Running ``borg init`` via a ``borg serve --append-only`` server will **not** diff --git a/docs/faq.rst b/docs/faq.rst index 1ceea731c..e36fa3120 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -14,7 +14,7 @@ What is the difference between a repo on an external hard drive vs. repo on a se If Borg is running in client/server mode, the client uses SSH as a transport to talk to the remote agent, which is another Borg process (Borg is installed on the server, too) started automatically by the client. The Borg server is doing -storage-related low-level repo operations (get, put, commit, check, compact), +storage-related low-level repo operations (list, load and store objects), while the Borg client does the high-level stuff: deduplication, encryption, compression, dealing with archives, backups, restores, etc., which reduces the amount of data that goes over the network. @@ -27,17 +27,7 @@ which is slower. Can I back up from multiple servers into a single repository? ------------------------------------------------------------- -In order for the deduplication used by Borg to work, it -needs to keep a local cache containing checksums of all file -chunks already stored in the repository. This cache is stored in -``~/.cache/borg/``. If Borg detects that a repository has been -modified since the local cache was updated it will need to rebuild -the cache. This rebuild can be quite time consuming. - -So, yes it's possible. But it will be most efficient if a single -repository is only modified from one place. Also keep in mind that -Borg will keep an exclusive lock on the repository while creating -or deleting archives, which may make *simultaneous* backups fail. +Yes, you can! Even simultaneously. Can I back up to multiple, swapped backup targets? -------------------------------------------------- @@ -131,13 +121,13 @@ If a backup stops mid-way, does the already-backed-up data stay there? Yes, the data transferred into the repo stays there - just avoid running ``borg compact`` before you completed the backup, because that would remove -unused chunks. +chunks that were already transferred to the repo, but not (yet) referenced +by an archive. If a backup was interrupted, you normally do not need to do anything special, -just invoke ``borg create`` as you always do. If the repository is still locked, -you may need to run ``borg break-lock`` before the next backup. You may use the -same archive name as in previous attempt or a different one (e.g. if you always -include the current datetime), it does not matter. +just invoke ``borg create`` as you always do. You may use the same archive name +as in previous attempt or a different one (e.g. if you always include the +current datetime), it does not matter. Borg always does full single-pass backups, so it will start again from the beginning - but it will be much faster, because some of the data was @@ -201,23 +191,6 @@ Yes, if you want to detect accidental data damage (like bit rot), use the If you want to be able to detect malicious tampering also, use an encrypted repo. It will then be able to check using CRCs and HMACs. -Can I use Borg on SMR hard drives? ----------------------------------- - -SMR (shingled magnetic recording) hard drives are very different from -regular hard drives. Applications have to behave in certain ways or -performance will be heavily degraded. - -Borg ships with default settings suitable for SMR drives, -and has been successfully tested on *Seagate Archive v2* drives -using the ext4 file system. - -Some Linux kernel versions between 3.19 and 4.5 had various bugs -handling device-managed SMR drives, leading to IO errors, unresponsive -drives and unreliable operation in general. - -For more details, refer to :issue:`2252`. - .. _faq-integrityerror: I get an IntegrityError or similar - what now? @@ -336,7 +309,7 @@ Why is the time elapsed in the archive stats different from wall clock time? ---------------------------------------------------------------------------- Borg needs to write the time elapsed into the archive metadata before finalizing -the archive and committing the repo & cache. +the archive and saving the files cache. This means when Borg is run with e.g. the ``time`` command, the duration shown in the archive stats may be shorter than the full time the command runs for. @@ -372,8 +345,7 @@ will of course delete everything in the archive, not only some files. :ref:`borg_recreate` command to rewrite all archives with a different ``--exclude`` pattern. See the examples in the manpage for more information. -Finally, run :ref:`borg_compact` with the ``--threshold 0`` option to delete the -data chunks from the repository. +Finally, run :ref:`borg_compact` to delete the data chunks from the repository. Can I safely change the compression level or algorithm? -------------------------------------------------------- @@ -383,6 +355,7 @@ are calculated *before* compression. New compression settings will only be applied to new chunks, not existing chunks. So it's safe to change them. +Use ``borg rcompress`` to efficiently recompress a complete repository. Security ######## @@ -728,7 +701,7 @@ This can make creation of the first archive slower, but saves time and disk space on subsequent runs. Here what Borg does when you run ``borg create``: - Borg chunks the file (using the relatively expensive buzhash algorithm) -- It then computes the "id" of the chunk (hmac-sha256 (often slow, except +- It then computes the "id" of the chunk (hmac-sha256 (slow, except if your CPU has sha256 acceleration) or blake2b (fast, in software)) - Then it checks whether this chunk is already in the repo (local hashtable lookup, fast). If so, the processing of the chunk is completed here. Otherwise it needs to @@ -739,9 +712,8 @@ and disk space on subsequent runs. Here what Borg does when you run ``borg creat - Transmits to repo. If the repo is remote, this usually involves an SSH connection (does its own encryption / authentication). - Stores the chunk into a key/value store (the key is the chunk id, the value - is the data). While doing that, it computes CRC32 / XXH64 of the data (repo low-level - checksum, used by borg check --repository) and also updates the repo index - (another hashtable). + is the data). While doing that, it computes XXH64 of the data (repo low-level + checksum, used by borg check --repository). Subsequent backups are usually very fast if most files are unchanged and only a few are new or modified. The high performance on unchanged files primarily depends @@ -969,6 +941,12 @@ To achieve this, run ``borg create`` within the mountpoint/snapshot directory: cd /mnt/rootfs borg create rootfs_backup . +Another way (without changing the directory) is to use the slashdot hack: + +:: + + borg create rootfs_backup /mnt/rootfs/./ + I am having troubles with some network/FUSE/special filesystem, why? -------------------------------------------------------------------- @@ -1048,16 +1026,6 @@ to make it behave correctly:: .. _workaround: https://unix.stackexchange.com/a/123236 -Can I disable checking for free disk space? -------------------------------------------- - -In some cases, the free disk space of the target volume is reported incorrectly. -This can happen for CIFS- or FUSE shares. If you are sure that your target volume -will always have enough disk space, you can use the following workaround to disable -checking for free disk space:: - - borg config -- additional_free_space -2T - How do I rename a repository? ----------------------------- @@ -1074,26 +1042,6 @@ It may be useful to set ``BORG_RELOCATED_REPO_ACCESS_IS_OK=yes`` to avoid the prompts when renaming multiple repositories or in a non-interactive context such as a script. See :doc:`deployment` for an example. -The repository quota size is reached, what can I do? ----------------------------------------------------- - -The simplest solution is to increase or disable the quota and resume the backup: - -:: - - borg config /path/to/repo storage_quota 0 - -If you are bound to the quota, you have to free repository space. The first to -try is running :ref:`borg_compact` to free unused backup space (see also -:ref:`separate_compaction`): - -:: - - borg compact /path/to/repo - -If your repository is already compacted, run :ref:`borg_prune` or -:ref:`borg_delete` to delete archives that you do not need anymore, and then run -``borg compact`` again. My backup disk is full, what can I do? -------------------------------------- diff --git a/docs/internals/compaction.odg b/docs/internals/compaction.odg deleted file mode 100644 index 8d193e009ef5a74ee636561343834fa324ad5e3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28928 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 UghdGo3LWyGgM{c#>t9Fz2U00%sQ>@~ diff --git a/docs/internals/compaction.png b/docs/internals/compaction.png deleted file mode 100644 index d5c53a680e1b4b6c0711a7f9c04265470250e316..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 331649 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

*4b-OVX=tS(djB$fPOzIC^#b4!2G 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℘-ESk#VaKI;g>D~?FctnR@^0JcdA)sao2o2#_N+-)a^cwe`
50~#f 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%0DZwA#lKx^usE}U)x0GI>otSt6^8RFmh9t)| zs#E3oz!x3>h96_ji%By*im~bGtCxw0_Jlmq^0R!2dD2$yQ9eX@AWnDagJRN$jkVbo z+OSF|WZ$ZJM2rP~{7yI1JXBp$4)(Qs8ed^`zj3YebG>*dmu~o^LY}vd5`y~eC_=y{ zL?}7w&Zz`j?*~SJ&g9onw`YMu5WS+n8D{pYxI`?Xk9kMHV;FE(`~I{+*haisTCJ6s@}(l(u4+ho=zhvc6$g*^_CMTv@f)qxlG@*`aq{b)4Iq?zO3t#; zZ0)$)jaB{POQZ#3V;DiJl@mlAs*7Sr@*!&fkkCtZdPJxyBh^NQsKCANZg&>3r_%!K z*2vd`F#Ul_%ih)k(H(1U@0^)vHPCh5?9*d1X&B@?@Ss!UotJi|#_@*&D=sO^*bQQ& zaaw%uG53YICFj_NE+ZqOZpx>LH;{=y0n!B4%9bic(aGl#-B(^Kp+Hl?)kR!*5NAdJjPq>p=MY<^38pE1R2p$28lC04kbz!z5v4nb< zoSOHjxNE9X$nbPza7AQu#rEDLqLf(-u@;;#H4wgAIy@Ak_V2cAX#jq43}7A=AsD@> zn^IvchB#6eRH1#O260VH&OLB<`3Wu47kTPng{YP+6(E>jnnN1GusAkGejYEl_M6jv zEvd-ozmMR3sCsJu2v_a#)_O<9ba=W)l|{j|^x$kJTQ6yj50(m$8K0@>;g0#yBnt%b zGz;S_xbqRreVAz(`QSU>?6YcXp~P+IcB&ED8`g8Y+A!Nln1p0{2(#Z^3z=r+;@vNY zQ&q=YNzG2-tlmpA>b2*ASsyQtzFDH4m^#Gg9BMbKd&5IITpV)o1JDCQVbrFh@)Ld4 z2L6Ha6a^BCr5Ns>nNLty;bZ{yBBTj4lC&gF5vz8Z6q16vDw|G6Sp}LU&>Iy1w1nc; z`}ssW$Dujp$NX#3fb?`yO`@3ZP;)J5>DY8Q4r&-anFc9B*fw*K>zL10MTT95@84Gr zjn3OxyYo8HcpqRdmyu|B58|vh(uH;+d7O-xkuY3`ejA`pPWjV@C3+KcumtZ`C-s@= znb6^cqf8QbE!)Z#K?2&K4B+eJj&+V8(fqc&kw^1V66Y;CA{zXvm#%lDj4Bh~DbC?J z^DNxdq`bV`6@f=TTo&QI%S@MBP$1uTR*M!n89V}kgc*FI`NQJ1CFZsF1C>H2+KUzfUTD_&(lOdT z$}BO?`LI!RqJDy}PQkT$=ItzWv?vnO4`01_9_yqE8{UKZrsaELN!NQp=a4q?S`N2W ziS|DuA|j$U)1cv$_|O=GT0zf=*}Z#zQsI}janv2OGQaRlEYY%IN8o0?p7d~q@WFEM z$It}}Xy>DEXR<(HY8Tokqv^G%Rss?~LNOuUe)>FL5U;Mzv=i6iL@gfr>gEQuOzuP1 zfW>tpZ7;dxwrP(icMHkhG?c-kNHtl9TeF>)tK@??>uQWheQ3oyIxz%xMPk7$#1^(1 z^wTr{zD?qd86ZOnm|uIssoE}}hNsbfdv$(%sQw8&CLg4>%Cbh;_o5UN!nBv|$J1l? z2T?MKqDlKoE9Sza(V@?Tx1yC5Q#<7pxG}qQ%O1VIBr(k>S70El89f5@J7(IX0_+w4 z1$~5#<9_Sbt+N>gDORUOCnxWiV{LxH<;lLrsC9PGS?Wdu2zbG!Z<9Oepvv&0Ni^i~ za|QC%b8=;4x&m>=WnU!X1iYr3gGzM%WZuAs>wc#jlwZJvG+3J7MCM(nJ5%c)>QnF{ z)kJLnYZ@!MVdc5iAhOSLT05*fpO$dFo>tOxL}pCb6EENHXI?|&D4NFY8V}nwXwx?9 zspeqpNTf6f$qu^~_Bbf%0G6hh=ZFTEBQJ7y87Wx@J>CT*! z*%aIlzJ8Qmx6qIj4h_|M1_he6bvWufXGqw}?j-l_udTciWZmykoLwc1g}dcBZR>pTD zy(_?{SL6Ih7$%iLl|axEA>ai2U^Q8CVeF#m+?X@ZXIB$Zq``sw6VWP4yEJessTbkR zjs?hNUaw$ytOK?U5#lVkPpg<6ec|z{qc#xA-xz&JHf{ka=x%R_YT}lZ7+wMs;!ZQS zdw$b9Q2MNnqZ}nh&@Ku1rahKPhS0CkV+ne;!sZ`!x(c@7m)ppa?Xtt0DmNkvJNv6r zdpx29EOs;a2J5dKu%H^MWOwDcB;M#345goaBmNmLa-tz zM>m|1R_Y6dE#2NQu`Glj&qBDOOGcdH@8Jq6zI>OCEsjnB>XHRc^w*&V1nBla)7QUq zB_LvVG$A_7_NMjfxX?MVKyr!Aez7vb+0Xx(NA$g%8B5clI?^g0aF9jjE$ zS<_xI+ZoC0{vgL8C#J9-+B1-SHN*JyQ#VSVxW3Z#iXf61$U@xlwc=0EQPad5q>I$s z$xE4uE48As^K_B7SZda*EL_!0>N8B0>KZTa2=*kjL{XiZ7?8d}6{$YpBjL9y%^)=B z_FY0D&*0$+YHa@?G1c-`Cd~Mm6*%q~hHm82$0iuatHpaxn){1tlId@Fd zQJZYBwBR2b8sw&pf;xUy{s|IB)j_C!NBqwqu@X({{Nx~HL2UIe)wnaHBW=W${u}&Z zq;5<10!eoQBjO$JoC=Jp+@oRZ{Vir~ZWhf`tuJ@6oU=f)qH` zIUVh3LGh-3Ep+P!ohcEUL2f8W!|~%;Q(@O(i9eAils}bm_0(2`h7fWiWN3173=WPe9qLQZ((R? z7}aXlF$;f`6AU25KKFZ6^K@D%F-?!pB-n^SbORyCf*o&;C8Ys{u9jv8?4d$9DZE)x z9z^^HvC4x$8szjX`2g<#E%H^aCr~akx$53Sjyf>b6tD1SG2|sL*33@s( z6Ra z+qChgDT5P!LhLmlMLx(ym2jGo&003hiF}gpm!s$qD_W9#8{YS%_z$?v$qm7X3cV5==AIFLW*@xe#QU{k$E@Mvwh&j z&V%Da3o{@<+ukAGbK^yX~nG2SLQ$yoQGH?NJkirt(Kp>mw z;>9Ykf{>jqg|alfL@DJjuuTen|9-U69pdM{&}=-_Cwtn^pGN5;EKtvl;f=tXRG1EX zqiQqFeCWMBVE_lGd1xc4-UB7w3Nb&sfWVYfSNHf?xXU1@MA`6|SGww!l|>K0rWl_m z4M<~}bv#y_sbh*ZrT{ZwId?@DdT+wO)255!o0D(0ybn7(MZxz()c z_GmGK*1`6Bn>v?Ua&Z+|f4x*o+k9sr6qnqOGKPgLH(XI(e?mUo z}2VgI&U>@K6 zJ_jHEG3MDAcu(RJ3ei}hImp`HzRUu+s)el!jI=Z~A3+?JK^#lzFy2(cM4Ux~o`~1c zb})vxk$*|y$51;X)sB~?h`0HW&ojlEmbOz%IwSFF=dSef796f0OO;*c^<074x|-SU ztY*OK9(~t`_v0Z6BUqE5*)p@T(n<2cgU_Ov&r|$tdQZe`cBIIhg7%+MC*b)lOc%rg z{Y|}o3IIzEC;lgJ0IJxTW40v4J|TZU-X0`muZSl_Vj$k!WoAAy(gCF22+!04+6dYI z5X_}EwTD9_;?_^wLUZ_F1;_*)SMNTrxpWu`_~1CW;uN?^qpaZEQ$F(&xZHl@-Brf? z!aK#iC~D)YP$Sl))g0LfH~)Z@P-wzzQQCt~UCNs2AA9`7 zh&TiT8)U}fMKU2WgipuSuo<*W7|`0>EYTxild9aFwi`YHJ{*C%m+^GL;3d1AH{L9? zdq^{F$KPMuYIP$^wQuU6&RuGZy1qFfR84H}W5SXnoEG1_+)YoB4`v;WZAR{z=|Da- z09b+nY#6GiQj_m0fOzp{*q;jGh6K`dI!8u5*{=|k6V;SL9*J^gtb>SV;u4KT>Ey^r zv}rNGM~Y0I{WJr~&0{&7FjJ~q(_VGIRKQ`As)X%b>Jm|eNy}k0k+Nj-Rezq0bGu7B z@DO-&=FQs{nKNkPr2o^Qvpyw;!86N}0^d)yru^)7IVe=snhNaE%V%zZ`HTSPIbM9r zs}FP{moV>x#??5z2qbGUnQJgL_$OCN*~W`hz$p0RtTi6-W6G=XrQ)q8yy?8#RDWuf;A2k!%puj9Y;tE;a<9-EdI=1 zZZx4JN!k3~r}r>y6AtnMd{y^!j9v3ayetZ^_8W zlrshU9)(}I73 zXooQ9eU?5vEUTf(bE30CfykU$2kr(203WzUAyVJs$)2nN^HO1pL>5aGL!(If!W9lgQ~&8Hh_!6Y3|)_SWaJ0X=p zKCy6-l$7*Y^k@IAQ^Gbb5P#p%P)fzQ1UJ*mgmKH9eZVNq0alRHs-r1TC2tl@0%VkW z{G<$)5n(rTJ?Z3JQYe|~)DG2dcqg&wjG5T6Uxg`Vso7;KNjSOt6>NE?<~~2%FUJKj zt$`gcflFsKr2TtF=2qp^i0f19a+t!o(U-sN??;?M`+~6k4k;vf$yMw9jc*I}I zsIS`C+{BM;*Hp%IL@CBOyP59$GrRbHqpy-S5oJp$JD-Yk-+x$SP3GI)?&me}ujV!* za$9T-apvcD%XTten0>6^@hFRYorWteF83smYw-LZ~F=nZzyCjO9OCLliPY zG$9Gi!c<@sr*tlZBG$wie{vB|=}2w|D$b-xv}tLM!5KH@eHo19j#0(-z_I6LldOfY zB5Qe`B{e+SazPW|0{Y>OPh0=Xo6W(ozM^EZ!^O|rYq{FD2UWyccElaz?wlaflwo)S z$4$fTpnmzf%3ZzB0-nNOr;h{_rpJAaMqUB0l;IIndnU^^P)KvQciwoFS8q3-L?# zxS5)fcuz7&ykmrlqb7>cqBvptDR^#BDv6W2H;Y{}d9dzkM!9mY_!I1GP=$#PznpD! zDe0?6bp#M>q%y!7*MRmNp)r6j<+o{N>Q79J>9rkPSuj&w)D~-XI%mijady=_etzhY zi+EGA)z#`#mZx*kp1K|P;~15k7k`DdWW_>8_Hg=%-|i%*#Vj7Moa)yrkiJ*xJqUyG z6;j}@>Y!YmCly>xO;VO5*q^rA)of)GbW=*bV48g$;4)&fHhHeHz*gB=&wsC z;{XWx3e!;g_?CqDJ0$t&4rxrkyiyE`LQfX-jGne@E*)bB@>pQd*FEY)5<;C8JZ6R; zGk(JNw41Kq{5(<410>}~hFKW1%S4hiswJ|G;Zd9DV$+3>vDc;x&!PGBgO5J-6b0Of z$JdsVE#F(=OXPFnP3yVnIZe*x)@Ia&$%65IBz_W`LBX||q(KXjD~ApUvw zHH)>C2B7{g!Z}i=i}D->4jS557)-S_3=G15*#I6or)i(nIDwXD2In2j$jHdzv3p>w z7ebr|Hx%A`c;u$2m6vGksG*@@A`Wn8l_c;RgdG=C{&L@U14YTz1j8K?DnovVcm4R3 z>8uh5({CkQ7UeQL6i{z}`~|zuf$NE{)qp^ zmoW9Gduv;pd_%3^_LTv{wGk5C@Ag0T#{-91Ur%rM78DrX!&yaZt81?0Dv7Y<9&!XT zdT6}f+8Iovt@+;eV<}0FZL4DWXLp7L_YmQ%JZDK~%^5 zE&B!pvd53zvLL`wj|7Vy4mP?=FI~RuflT*?urwKl!8!uSDoJhjNT_3Ke*QW!0f9Do zq_^op@+mj97xXWTfqFlx5HGQoA9bRC_G%n zAL-Tt^9jJ4m4sSt%`f&yzuPu#z#~cVTR-Ma!TA~q9UH)RYXGvwwKcGk{gpU11mp52 z&~h}W8Xzg#3#`N*P+Szj#PHrws(EuLLh%f;c9*e;>~{{NrU)Mxhg2yX~0J-Icg&Zu?56UfbhhfR-L?bmi4 zULxq`L@QAU*Pra__a4na>BuwF(=x7)HVRi>KT0YI4D;38fAm`N!pFeCM3ahY_o7bzSkE(e-5Wl(vM=v26~E~7DIj+^BQ}3D zg?3c&jTt4vZxtj+B$4qX#%9LC!)XYjRrum`Y8Cb=rrf)pZB8c-)OB+0LFnUJe@#un z#-@~9Jhoj$^-s$rF`3yz-QT}|7C?nc_m_7&`CqrxCpR$_b%=jNC40ets|5Pcy#Uu~ z@+-WTWzB^13%$C&s!i*K6h0p_2$i$&%2aAPZ0&+T0qV1-yJul06K9Qrqneohn~mE{ zOtSY=ppmm<#&n`mTS`hQ9ehk7lb8RYl74Eusy!fiv#YC0UVBg7_ctO)F!4Zwxw^1A zZr&!eIuMH}=)MBgo@kV&)HuKy(m_f^+^_8F1>08Qdr!CxU#a}zvza@C#$b+gi<}Zw zQMsO`Udi2GYDEtq$nrKDa8JH@MMngBnRZzeY4mm+n9zY_RXum}=FN2EANIi`_hW*OlI^Lerq@r!+HRP? zufXuP8?JP6DtPX?$as5vi{rQ7JQH+7Pl%QTB~F;p10L@y#G_naM!pu=$u(bw7fcLz z%)7qEYH zkH!&oZ0eaHZ|v6vxnSn~QWG-zl&O#ILd4=#%9{*<2+@Y~IA zv?+|}g1MSWBTjiNTs}9PZ$5I(7Xj$ZkGz_T#A2nA7_iQh>zO~CGnK*4B!m6Ub1-@} zP4n{-Y;S$A)t1D6+&TyO7xpLG@^b%%zXRCnV!dN)duZj0q@8KH6o}KZHwpi=Fx1_q&G# zr6RK{%#G(shRmug5u>>))qHdXsjWmy?XP0G>o2y~{&%JHa%k%|LGnLO9Goh59DXU= zb5z0jR=51x1iPt0zwviQ0@js7lz^U&Ub3`%n|X{SOEnvvN{J1KJ-l zC9esA=gYV$P6qDwJBvV9NoT*6h3AB#C;}c*LBqH}f z49WdEJ0dRX(>chqp6$5h?9UBf|B()Tp6jz16wRlW8fM zVaKBdk+Oq3=2IX7B4s6WV`G=e_WaaQh&_6hsP|~;cz?4K1dAA;%c}9ehLsZXqQYPN zM>8@evWfa<{Vl(fqJN)V#r2`lrsw@-icrL_jfR33g3PX>g&v${lOH*bb!5SFj5ioay;A zwg~Q$BGS8$DNMsq(KW{geLdfRRpT}l*G>6= zzS@6jYFb2zAs8Jtn^lc5 z3CyR@KMHtvBzR_IEHHAbBk5}0eNdWQr+r;i_~%k!)p5FW_3r=-eQYDpkADDNU<+gj z{7>20k1?^Z^xKMFjsb_T1@!I%q~~RHD;-qC3sDk|HvW+aZBAZ0|P#Tk)B@tP=!Oo??7nS-6g0) zQiMpT>k7!jxghFlDSO?Sqm@g=`zuft$RU(3b^(qN3{o>oScv8y&glpqiT=CHEsrgw zSALnMZ#b=#nZG#q?y%}b7TxuNA6?a){OuMW)a*tYd>p1`MQmQUzbSgO#xl{bZQv~? zG&g?0YK2aJm2^}o(SBw-^EF3Q(v0~j*FO@OZ&w>6tzMkNZP1e0vD)l+e)VnFdvEeh z>`o$1XzR{M*KnGC?)*HsD6i$_<~G0*gewO!k}Lpanj9UihVbl7(WIs1`8R&QnHvsM zUxycZv9@^LX_Ve=77)p;PHS>^?Dt@k; zJ_u$HY@gn7@i^DMxbILfKl&>2prLcQ$ZD#-H^)8ETgA?V*a=sALh{bD)uA-3iKNTj zXEI{X&<}l*k_f{>d+Kv*=1w|NLqi6A@=!ygx&5m57o#JjG59XuStHO(QJyZv#l^tO z@jAEG@+t(U<=nY*KkDm+t(V|*rlhDC+!q~)g^3ya)*4w;2{(|*=jljU9_(67&d!GZ zORVX$e~mR=>7{?!)x*|z+f{A&rU5Q`&W!GifcxZH1lv9XHdM8{G~YB7+&v%KbJTp| z(7`(`jMZdgJc6lZJEBgf)W=y9rdML6uM&A@(Sw1RgE@tTgM*Ju+QW9-q~+R>!cGVC zH`Nq2HgiI`%*yTjP0K*Xh~UgNzU z`d%p}&&g}yB}d`hlrmG(aGU`6MNl#cdD3_~kFN~4=bO-5QJ^NocpYf=_L@8;#CIoj(i$CA3uj*J^xb2* z>$tT)vqn~;JTxJr5H3Hjo?g-?{c6SGWJv~Rk$FUBeGF6BP6#6NU;7r%YR;^{qJ}x|C4kHt^|Mn@Yf9?SC~Y) zyLUS}M;wPaqWj%X^(8TD1l-36ilfFZv7BkU{ZmiKD6NY2r>xYo8+!6R`?OqlI6dJ3 zlk4P*7&CWltPkf0=qvJPtHa88H z>3>FADgmJeskH7Kkw-d`Z3no!vm&~;clLDlHJ_K+_>Oixbg!FP(4X^lv=`v5SCjwORnfmn4Pho1f|;D#rn!H%@taJcL`c)OfQb>RS#Hr*Q}4ev zv_M((KY>^F{DfTRpQ~2}pn8S;C>)Dkrgd;as!KS71Mbe+P^M5fohl;LOd2{myZ}aw z~AKs`;Fv7EKFGg+4;{8H&v z^y+R$MSf06)$B}k>dQU%?G$rFp{>K#-mxSxv)`-vyQFbru)V;8K}=}FAu4ZgiGMFp z-1tg?Zt*)aY;U`cu#;j(i%w4IyPEZd5n0!JC%Uwjw|TKMVhbsbWaNXYQU(ce!c$LYhrw+sKvvj_Z9SXvWa#K#Bf9&(Vl@}f89 zHka6}^y5@WEZJVh&!8= zNs2EUwu}uM2q;$$jHLEQaQPBDiGoMyfg^-Fk6+qFu(?vGyvGha%!-E6hXyE2WYapgXMPJCDLaA|X)zZ|KLfL}g^)A?sup%+oh+PY{Dw(K6@ z+`0w+C1zcy9T5_i(gC`vBN0*9{S`ZK&~v#pQ&e&aOWb$dw`P3lUJa$njX*j$qPIw1 zfQk;Nfd)Pote@WfFXwwsE7nrY1e}?#)>G>pLUv!?V!OVCs|YjscUOWwS4}oYMd@BkEl}#6O0NUfUJoRQ?nMHq*i$Y=nt#mjI~{E0@|KW9 z-AWmJb@Pdb@1FQ<)j9D1$h85&uzTOJ$)SSeGi)dtAe0(1bFg0+`(Zvl3|L)MW5q1P zo#3$w*;pKW61PxCPcGT~~RDyy4 zmiOJ^&ZkS#mt0pKE;fcN)M3-mE}pPs`gE5Or4-pK!Bb0OKSxt$#IqM)J(HKtDn-!K zZllVhL?PxBbiMicK6isGNrfX|2mdmwM-}}yHO}S?aPDj0etH29?-BM26j`qM2lA)G zH8u}dScm>2mI7%m5}nKJ*@+3Lia{+ClWYc%j|YwTqjJg2$#_o{TP=~8nVQzGoXUWe zI?|zVALVki+`I^9_3CqtZ~@9S307k561N%45@Q;^Qs~Bt~+o2pBX?sLy$C*oe@`*Di&L1^~ zbN3xb&YnN70cEEez>KZ4ZftA2_riXFwpkkgwg7XXS!rQ{1ng6(|gmcg6Gdkgnl9rEv^o9055|6G%L*z4ySB`yDur@~VDm`Zg?ddkM{O5i2F9htc-vTxb2-u%%kT%1J)sv+bbG7aS^iANmNZoYs zck2EHl+VJ4{F&)cs1xtpl+HkPewa&R5kc1M?$UUxPZ+4ZxsB_s-~m-> z@?J&3-yw6BdkOrZQ`6k7-J8^YM3u$@t{!?!!+V8C2NZaQ6JMm84r(#w4%Tl8^qQkF z*n{1|z`)=QA%Dqr+9+e2xh~qt#YMUgYu8okqY_TFf?HD&3~#Q=Wc4pEW0cNr@9tv0 zPF%CIw$9$$TEd!Z#dtY{5@c>Hxpf#0_W`*s73Ul;`aE^AFM^hZqYBCMJcl|pT2;$6xVbyVNtaxEh zOSwKEf&0S`>qt{00>-SPe1==3suZGjQM=04G}4uR)ljT<0r~^p>)%V3_m%XdQKh@} z2LkaSwgTlp5)kFLM*HGHWT?DhwlOYfK5c?{W5B`sa z?}@jsKhg1c3^*s1)j1W^C$=BP*{{u?QR<6kB3apbTjJAzx!d?YC~k2t4`^;GD-rFT zn=)Z1#Fe~n41l7~D1KIFu;U7u#rtIA+jr)*a0O+Xrn@{OP4s8D_|KYrZ5L}O!5Vy{ zD>5LasP{wcSxE8(Is+%fAy!WjqX(^KeJ52(K5^+U97V_M{QUezbD>C?{Ka(h^7tmC zPMntt+=VzKoWTjEGlT$8q{_{i+1M(oA67-&u;4dHwVoy9xeTM|K}RU}eWQL=7mjMxms-zKp4-V4d)Z0?P)R zq}Sr+Z{$J&IZUzCOKk~suf{dLMQuPc_YEH4E?OZ7LLCH*2fuvGVDSAHeujTJ-XcIq zq_c6IWCWj5a-_VX8wTa#2Ka=o%@_`#-C%98bN1kYxfiBLp<^!Se*-%Cn`{m)bSQVw z>w$8aQrYA0h82m@9wQXv=>*l}AdfJPto#vF2cRl%(BFga4rGOjKbMdi2Rt2ExNgR%Czc4UJ?`kkNF{xW874&vLoFZh>@k#VpsB5OoPUr*EDtLuZ z)D|{qtAqfR4uIZ423XyFV$d0F3}dCCq7uHZxcoU7u`qmx$Y~QapxizmN-3M%LVGJm`LB@P)9G@S&i}x^Z6&EZD&@elZ6Z$7&Ef_pq)I&L&rSp_0%`vWESlhfXEt<_nC3Mf18Gh&lY8!Vmv7*!8xR}OHQ zYkYAdd!;2szoa~FEGfH~Bvup9r7Q5paQBfkH9Uq zy0cvdx#4*+ZwQ@)#v7>iHdapLb52f98f?z@8UQomS{6{QF%PDZ0{dM{jZ+vn8Zho^ z0uDYMi3jlbU00n2PXnFZ_VP_|5OL29mKN4;wB3U_@s4?SuFaa3e2R2}D6@Eu9~v;= z-+2pJN`Jm19$q)wk@CgsI$P}8qJ8AAHoa$+QrP-E>4$U4mK&uO7+lmnPriBUJXzu_ z7dU9xwa~G)J&bI}o(SSaUBq*y!g{!+QY5uA#};PmCPP7a!K-L1#&SE$`_;nNQ~tKV`?_l*u3q7#qjF}Bfr*8q zaK3W=WRjx^RIKPH3elC4*{e%QidOExqee?~YR?)@>~IYM-IcLieZN#L-=2-t1>+YQMtSz1sHa&9a5Hz0L6 z_UscEWiI?^zms_`zeZvKFF~y3yED~j(;T#jqvo||mEUu}nB|jI8o9}F;_?|X9NP{l z=tb9nPh>hY&>Fy-hTucfK`5i4p%IO<(0KPh`Jtrf>3II!WtU&%JGEC4T9GYbYNI(V zVyDGUAK>k$K6%%#orz0a&wi`UQ9@wB*7i^}L2KeFX8gp|++gyNk7rU>M%|`>=Ea3H zm>zzX@>1~R(fccVy(S$F>2iPi#5U8#OS?os@D1_m415wL>FmP!1!|!*8KsO) zaORf*$>upoa0De}vF7gvD4C%#`_T#+v=IOJ2Ve`PN!;_X3~FrLm4GOTpu($(i`~}7 zms2HaRUtFc3(kr+Xu;Bglot+$j}8UPUCy?r=(DUsT13j{Dy>jVZuHnU?tEwLMg{fz zg&q}|&m&+UUvAAS4)5sHD`@N~U_XaghDI?6Fa?qY2d20z{31%P0w^hW?=bf%UAXn>`eDx$O7-Jek(zj4yS9SfSJD=$ggRhfb&hhud2E=#CT|GF=W|xgaekB}=~@Z+ zg>MD(?Yx@2n$tScug^`-FRLiAqy1zn60|zF`DhFp_GDBBs5!f)7UD#L=&#V@1b#h6 zo>Ea~{e^gH%$k6jG22;pcvue`fW==lS4y4uvL_`pGy*n$c-DWUNo2f8&@1uqWPDy2 z8q(5;_(eYjd`>kv`Zs^S?+wwj4+MA0nJR;V6aoumh!J*aV%oCSYSuSp0opC>n3tI0 zDR1u=ZiI-;oRgKIPW)amZj})iX6`1nK#gmk@`-S;(+i`bzU4h0Pe|KohokX5jRe6y ze8bldkH1tin!*)t`HeRssLF#}O*(B0x>b zmrr>CfX*0Kzb#F7!)ZMt)<+fOuM3-Ze-?@y{(MJzPx_QwBJYjnjf4(#VXJKjIJqj@ z+TiWesFf*N8$t-FnrQ_a)0*~f=VIIUZfHw+!yC@Ri--PJ%k@Var9wfOeldl=`)9j5 z**W>r!Cub+ruxxVphf3Yr}e>XzHUsltMrbU9162UF^XGifqlt>kH9Au_GfT)@+YG+ zb!J1d<&ECgWRDw|T1a`S`6+={G=erZu&;j@eNVUT)=#v~Vl(ZZt2ykTMG@#3KFQbn z3Lc01;b>== zOTqBz9f^gmeTftorJY*s4&v(xbqYS8)ssr5rYU?c@pN}+MN6UD4_oH^u* zBe@PC(X?y>SoP5`Ff99pszeG7R5;jq7I=3RT5q`=q4Y#81*i2{l%JU=rWdU5M-en6 zm5VV4+E`8e;L$`m#2|QXCaMoVRvm5@sg19uP>Ua9q$z_{*)25Z zCW%AU=w&Ko_oM0GVsEFv%+L|*j+4lXN~xkqxVi&slY*f+Ix~d5!M3*%YD_ruV=L{a!O?VM!FR?y)!KAJicqhElApSVWr{ytj4Anf zJMFCL7NYN++8kDhs#O1T?} z%7OFBPs~^Lk%D9)TXy_-_AZ4Z+lhS1|H3fw{JgIjo&8W% zB>Aq#Ff}Qj%o3)oh)PrbK>KQDlZ4IT(#Jub!+~7h;h5XkMQ(x7L2)KTx!HHH$$n~L zn3`(T@;lwB%8KQ|a9@o4J`=8(15sT{|1dEJ1(^j$HGyIJSx0g1d;zc;9(n&^HMAJH zG3?L{YA3gicu=tJjgEi|#uwso{u8oQ`VLg6jGFV)M@7}awkD& z=qDSl(S(^S%DQv?%!P3K4B8|y(xTANV>4p>$AtNhIDt3_>PZjM9}b1 zps3%)&iGdZJgLVTf`27&(X=901J{R(t1lMXpPww+RbL~(zl+VgyofBT*B zndzpn)-$J0>?dUIpSr9|kt${XXw$d$;iE_AusDit@gC0hE)jR?d!JOz;&gM%t~l7f z{XVmQ7v-magFV?TdGdf;ohoSWg&FlZfyw69?<;08ZRn&=Q`gKgJU(TR?z8paL=0^P zuv@*f5YqSX9W}XhQ@qblFvEkENA2z9djGZT0FK&6k2HpQs>GZnJ9+6ud2Ac2gsq~= zg&a1U{LXiiR+{1*?aC!pN7T=t^$ti^q5`=NWGrUT9!-9}nZidYeaFDSup*X)S-{H! zTklF5&e(@%CBx-*6sq6iX^R=tRfOU|)s%MRzIgE~sig@mTGN31!ASvZ+2S+$qP0}) zMfKei=FavZs;Abd*fUJ00%p+s6~DhW*TJ%QH*wQ9K>sqexAMDLsy<1vFdzC57T!mA ztn(wGXk-GbL2;`=@!S*X%bLl%1bP5kkC&FM<6%lVaoO$C$q(1xttzD^oyg`nZv=XO1vYg72n&gpgy4z9t>njTfVwx&Y+={Q)vEk&h!B zMORhNyY|YNDQ*xC3NtHYK6rJK48RuxI&2qxEyp>Zvl%`I^evgw;eOk4`z+;=kHW#0 zka!qyIkV*VhM6E!`}b&*}W%m5P11 zWT*FT_Cm?~_j<$wHFqk!)Y%M2mnx3}3Hm9yt@u1E%*e>FFPUQ?2G%R0gJ(dyBXd_j zSMpi}(7un}<8l-s|Mrj0{`DK@F#qFkAg_l`r6}7YD5(3SOyO%c$_ijCeNF?>084BT zhm1_-aSQPP@IW^hEr{IQM0r)2BW4}81vo+D^UCPto zAvgwwlw7;>b9$Px+K=O%XpiN~*pz-UEbPPQTl?b9bBtftm-}4LukP;!HDAvke4A0u z7i?B*V0>=^wO`XnF7bcJd&{UQ+irhU1;GHMLrOqONFy3Gk?xXGkVZm~4ryrt z>9Puo%*a4xW%-pwsS zpfBt_aeX)W{g>23aWg(#Z*;9R2sQ`@S+9(mjj-Q2Y|#;(`d~HZb~@b~eao2&-l?bL zvlt{C#Ip_|?czTyeOxxgp1G(~Ue#~^weI$OEud)W=a)y1Y(Btm_IZ6@!eTElC95)- z-F_w?fQB59Y%%f{52$gZmz zuQd<&w4#w>odMNm3JMB4C@#-529Cis*FS|Ehss!XNFi9*EeuSTuVkjjPd#hlU%U+3 zts`d)o1>q&?to9OfHO>n6im0F>!_V+Cd>o5oT;GNc^YGEd!Q7j-T2I#t!mI1GbE(? zUfu_3oFC?-j0Qi2-)1MXdWq{^TAXku^0qR!P2=5t*lq$(W~nCtFvFC*4sXF$P%wp@ z++k`*&Tjz$)JY^!-83B6vRxej^+SPcyHO54+aU0_oe7j8c6WD+@~86~kud;C+iQ58 z1mqIoYeQ7`!V8cx)zXvs@3Bu~W&pF1=`(IMrBPVDb4w##DPw&5QL}vM3lYY=EDDnJ z$ITAhH5T0Lr_bm)v^*#oS}5l!Lt^~>*G~xCZ<7yjr%qHht8BhEp}^6i;Yn*}R);L; zj{w-Kb&P;-a`za}%w}fk>5_Dn$paAgvz5;}a%1 z|L(nfmuStTX-1Go2V7(cBeoJ;&hd!M*0?$A){|?NW z8v%Re@&;l4d3#*8l;jFSiud`GNB2ShFSx*Mk`_V2Dg5kcC-)|QORb%~J*7Vu>#(RF z4MZbqVFzk12gucZxe0Q<38Y%*^V5uaA7mt7{7$opS0f~XOjN?HvI|w`2y&uUUEf9M zQ4GB)yDmDBpJyY?@>2k@^T>=0#4jwIz(_}*Le<=kbLj6cn52yQ3KSApC!9}{RSj>k z5q7l^CN;qXu}DB-FxiJ4wW*Kp@+VGJoew7tpON8Ox$o}}{MMpGt2gvHq-I%U6Y+@I zpU7vEiM%S<9>G|BmdTmQ<~&X$VlHmRd#2_o{OGon+dW)9S9(jsdKVe*!?$O0DD;+v zdDD-D^I{*+6f*DzE)^-S)l)?Vw8#C5V(=IF_A|Jw%)ZR$WM4z0r}xz>vI10Fey8!$ zi-pdFyMP9^WY>EG-_s2;Z+1ghm~MAss-MO7q7!&}fg^4gJIApWzYEKD5lM8d?V~yE zdKXK-I=^jz6)pny-y!npk}23eI07ZnVk`^`!RaYG6+EQqVCW)Y6L&G$zRT1+8U_G| zl|0M=thPr~m$3S-fR@HIvQgNVgt3eVHo4Lc#O}cXTS;=(bE0jK46RKkye*HU$&|CQ zC(>oLUqnwd(L0cPnz*rHQ8w@v&s(97s(GjBQN+7nV+bXV$Hyl>TE)t3(HU`%IC6br zv>bJqy~Klgz4*`Hm@-DQX5Y4Ka+tZIsM+_7)G0}50m~YWHV~4taP$<-lJ02~x?tlx zGTlPIOmpj26k@!zZ0);ReQ}iHyK028LU@Sn#kF**u!!FQ5Lh1pl9b9jjTedceFU?O_P(CkdKlRpfAXi8#zJ0UNzD z`wsnizm~x*%ja-izL(k(1VV-YDpjlUOsMdN>glC(|DOg!3+*)$mx_JQ5-*a}gBVz{ zYJp9a-vOv~NIKUkiDeFQ;%A#2?r#32}~u7ae^BpR>D zm)pDEzvItQ$198$!l0HH@t|y3o~BIOWpz-7S*PNz(*!(hp+T55zS>$vnhNWP8n$t_ z4`XF8d(8s^)<0Ck9r36uT}bbqXwbU^d04{jl?|}e>_wFHrfj;)JC7FDj7cS5m1~~L z`W&6BReac>@ojIv|6cq!QVso-r}9L}#J)S0hus_sb##K2IB}lR4VhMfs=mXbd>~=6i)=OPWRr|?@78+jF>Hr3te>` ztLBG`Kj4iUPw0Sq*RaiVXZ}+{@t`NtHF3U)7j|o#VU1dgklb7$&Ikb>={9LS_Dwy* z7AS^Hz!%3~g}^>ekS0IUAKK*h_S>66qiA+tQK!p@s~D8^a=TdnC%# zeJa&|&#~paD6%VKU+$=MH9o&Hd3WJ0haFAa0oPd$WhyTwgO4)qN{HO*`(MYkhG#4hC_$P(M0d8QKnfl+e46&QP_d!Gt?Xicrp_A zoPZRo-*1XoB4D>jP369mVMaJ`yxNt_Ra9f8=YUIc#caok?}CetH%4)@wxcazza2pg zmZ7-B{eni8h1jkKLWU3?of2%`{;9Q2qP!|9qU|oZzPP%ycs%(NR}B z_WC4n#QkoCOg3Lja&Pvzo3{gm?o1IJAQKfS_0qnBJA8`Nlk<2ak)l^nN4$-2oSREm&Vsx}`xO(?$= z_07_-?Zam52qjsW@C}!yooR`2=XOfXE~CbSE`_wMR#7QfgW`#Dwn#KC9F-8R5QaJfAwxS-5uRj9emPJZXvxQlSZ@XAo;?)yk| zKhnWXWt+7~od-@dbX`-JwM(ml_bf=#FXH-C2w4SF*^O$|H5@mane8Q6DaD+@kBX1Nw} zk`L6s!>TZ8y-`waEA3oAxS~8@z5MBR8k&S>>5sa!XFChp)})9sKW?_6xy6+)WAF91 znPiV~sw}?8-H@>6x==DSnsCBce~z{B{B=)@lF*)2aU*N|jFa>c#uqvY@B7zJ?jOH+ z-HMDl%!M6!>*(~Ta*^j1;Sa{aL3VN4D+&i{8|#c25Aq6ug*$Q4F(N+|DB7gdt$S38 zp%L#;aJb&_ZhRPGXL2+QjMQ>82ZS|h7zciPly8=JJ|@>E5ET`bP6%m?>|ueeTN24a z=O~=aHOD^r*K zMQgLDzWyR=*`J@+6H~3qpFIQnIj&`J#zxW|*K1`{&P~o6>4Q{_t_zCwQIx{``hwd= zPvleL!%aF49tCTpv(4!rxq}9^jCCbVM4!*A+n|pzUU9|m=yz_m+%uvy#Vi58b(8ob zgirQMmH-iSL_sx78W6AynDmp+dUmn1Qus>Q0Bsq<`C>ODss(jW`kHM#E1TO(m*0$Fxy6?)Gk9HJ-6-ihSj4kr?$%YEEMmu8p{JLek+6${lrJgIEQD$QFEuNTv}qK-cL!BUMTP$AFmR!p5K`hmdFmU8N{ zfBWluE|P43uotQqP#d&e=L&fW&819zopej`e_vT@{*HTna3SQ zOfEY+6eG3==iR}+{1tQBc0@c&w?d+Y{eDvqMGiIGF#cS>EO>89t4=MI)&eH{*|Act zldXRy5Lu9z9!Ox^nyB5%syCF6qv=YCw`Z(&SX$5JyE*TX&MZ;k!8j1q&giR7V$Hh( zd75$sJj6$A5Zc&h7bVvw*j)RhXM1&}#JZntQcQ!?@XTw}zPn+A(s2QQj5g#a=%}5)VQsn}iupj1&7e^jHwwbuh`lE<;USE`tIdLwCgvbP zAaBQ9-l(5fkF4h8@KBmB2&mi7Q*`B%TM#6f1<7iv0_^ryLDDM3%_c@eqOUqBIMq{r zn{Pd)+^74cu)}UZ&#jm-wy_akv}TdxtaO6KS6g79>ems&cH%6qyA+|TjM>-kHL}%E z=vBHoZS2hKt zp2;18?5$e`x8i>!4ShSBAl*68bA0+K^Nq8tb!Dow7Dz|e{qf2!4q7hG!cLVQ&Ck)9 z$rDKb9Q~v*3%W#!xL+g8Sy~--lZ}jN`Y29VtUDG^P&34{8r&O71OEj>&rU@D*sE7C zXzhVzLo$7ZC}R3r4Qmbf#1Mtc@qIqA&KQ=eT=?!ExvI8+Rfxfe8FK7)yMN}+X85U&eimk8 zVTL-L*PL*WA!`hEhducj`{gNZz@dLdop0=4(%u}U$>4fH92;O=>k ze{s62G;a&keH?$@^7QPMX6jqh;>i3gJesujpm*hrGj};nBc9OZbKCIw2pzm9I4Pt^ z8g(ovMO?$etZd3v&nN0!U9|ftk{A1aP!wIY$5O-PZiaB(#o$Vdnij_^BYrC(J1wCC zj1jMh?Qzlp?wB&Y_R5badIBkBco(k2 z_bWecT6XN5g+74z0zIxz!tojXRlM$3ME4vFnZdyNS*XhTWu7dKCUtAJ?|z!hRbx_L z?`!sy4{YMH0G<~lEIZbj!D=8HHFzPaY(KR)J`!f%AHh|@Sh z&z<&#)t#5G3R>f;V)EyLOD}|Wy6hGBYF9gg3lN)`Oa~Q&2Zt(5<)WcKM@Y}MdwX|W zU1>@(n>P_rMp0sK-J=?JJw?Q>Jg&W@K%RI!kWr@pZYJOElE?l?ro>GyTeS%p zE3MwEwSFEaqWlB58Sj_JNZr`|^($Jj;%9@qeO%0EVD18Ae5vJJ3yMxNS&phEfSV#_ zVqhq{2{X?^MA ztXu$lhg`Yn#1KSZJ_V6=*1OS91j!dz5=*Lk)ew>)RTmlSXa>*d$g>QFrk65^>br9B zU1l+*e#bm|Q8KP|avf$FgRc4+tmOB(@-~{TAE)96h&-OJM8dq}>o#A=lA}mT3LgyD zJLT(;WALkB1c;cqVRohTabfRXt<3RC=NOAx=BUn;+t8+8QqP#jH7+z6k7PaJ-QNE} zY&*iDeedE6L0f!Jlx13?V%8%wog=OQV`mL?r;mcGBeVQ;?;vPv@r;jGnvtK_PAd|F z#$JoGzS&oSd@RlFSmDFcjTnQWo_DKz!*hgo5teoJh&ToQ9kVRqjog;C>OeqT2fWDr{%)U-aiI;x9s?7ePF9Gl0qj!JZ$?7(vJm4 zfz@6U-WiZaFRC}Tm4UR!T*Rsysp4$mFz+B^Q_1>j`4b(5>n+}^o>dUpo~$FW1#kUA z#v7~SPeV-ezXkxQFc4Sxcy&3B`@TOhni{VcJ$1eY#WSr0+s7{DCKM|fCgPQiN!YN+ zJm#!Nb858P4US}>TewW|K(tMZi-Xxcgq`tXAoF_AvBu^#Q(@3Ds-E+ym<09Q)T%HOE2`SyF2v_O+ z!T8Nd+=*bzXC-Im`Sa(3#Hm-vjrB!*j$N+nn=Bjq`CaszcYf0slB#+oUjG<ZuBAScnpqt@c$u&L`8CF%xK`nLys-x=o74Qf z;yD4|6FTBhhykUn(XdyFrR!cBl{8Zu`8FCEE=?j(OURnnC>z3(KoH%VJi)S;Vlu`l zc*Iy~J*_n5tjn&OSdLH%y`^aQMIFKvv`2hks@8{*ePI7`7r?q7OZN&eK1W=lpl`0b z?Yq=!lb@9a%scVk)q2k^#vDkfwy7Iocxa1D;vq3oYim-%ljL!d2Ty;P$%h@NScx0K zIf^Tmj-iHB+p~jm816h)y5b9!E^|)zKBfwer~&C*y@IShK&FpYn+IXu4=rRvR86Aw zL|p7FgfFXRkLn!VEj~>eXm*fIe`&O8#*SSe@yb%ZT;;qR$ZUEXJM&#G3EBLpEhAxP zZ@`h!! zD)Qv`c>nWLOKJ9pO%ss$eO?a8^)Q&YE_tdhEVjUJBq@X)GDQIv2G zQDWyx)IXB9z1x3Ppmtk)&t6BKL27@mXx~z!{+uQJbn3`p&7NzJ9XpA0A$IxM?`0|^ z(Ee)D%`v&&6^U(Mp`3Koba^%)(ttNznAmSZkv6jwwwCx|EWgrgm}jNJFX{#pE9X7d z@~-z%l4ja46S;jITy!C1+AdGaF>p+n+c1>Dn_6`kKEounTrk_)io4ELwlMM{f2$+(EVyV{G`Y5|R&+wgM z(Y=HCSl`2go)Z23Gz3AU!!?PvItB; zz>OP7IQfOAy4HPHuaFOKfMSxq{E%K_Kt_A~p`%yCPR?!1H&V~2sb5nW5vi5Feq*wM z;q80D%}D+E76J0AD6;c^y;Yj(+P2d`q-6FDAHnT_Yw$JHS;Q%*B`8?$rZFDEAk>5l zW{RgL$^(sKK)Z}pnwB=v3CXq$qQ^_5!G-2v<}`dHbO#1)Mme^p$IRS9vS^q-`gd5+NV`M9Yx|pTDZ4 zq%^a&Wxx6)5jDv2Y=0xAAjOzN(072MrQpC`7Oeb8mM(O^vW@-CKn-@nx~3)ih4pG) z;@1(KZo|ZSOz}_y|E+0U931%dpRrisUmRM7^}jygZ}I`Fw5Z5>8oBRqLjeDK;4}Q% zT)ljKefRKB;}QacgAq{6uvvqiZv<4Xc^>))qDV$AzV~m4-`aYuZ29LRJj&C*R|aUo zL&fa~+zAR4l6sIP<2&HBx4Mu57fDYd44)62NvHRa`}4w{1KMgVy%Gm(Px)=2BEL|s zxdnZ%0}=vVUcSA?i#*%y6~rY0Cou9)z09aZaR4lMuE?Q)X|T&30ET*^4qnJJ=pfGz zki1JDyF@-WxU+f*`WeeC%+2>OceP6OzY`&v`}3792DRJ%I`URHa)5jkC?KZW!^H%k zC!e$RvC3A6_29|_dIC{ni_jbajdTOpYnKT~Y@K(@3O3{}eU6c7-Y zCkTf_4TaT4ytKh`fUfVo@gvJ-Scgm?=De>uyW4kSVxkq|V6v)m;cb|Jj+mO4SNpEa z>$^@zUo@ao-P{W?DODU&Ma#H@$xd!-(_BGfZXCa0LP4G%*_VIBogpvVy z;~aOH2EY?m$fbW_teibzK9c+?&8YGOYf;Ex_n&s>v$K}q%wy8o5rgD%$ zx3A62%zPdn*EQ9vciXsKcS{X(c>A3-_qu+v(%AbJX`TFpYjw_wjfb4nPfW~BnSL%! z;zl)A+_09ih%sK(vGn=LHW9KhdvwTem4K-C3~3W`mKxLgcJ&}<tv!{U;^XjR$3l|QVgTAhvIk{u{_Q^&wwa)jTn6)B3JM$&% zzRZoq%+cYEcGq$Y3a^X1KP?Qyi0u+Xm@<#u}T&H2!R7KD7M}Q^N3CwzK(qPsu&9lW?MMB#x?kj*-{1wDX>HwfScispf_q z{$7dwR1IS~WOF1}T#ZOm4%zxmmglrI>zvHD&(FNFZ4k=-_UX8#C=X7*A2iK{V*LvC zk<0bW*UWAmxmDJBQo>0-Es7{@XoUMq)g=GnYpmK6Zg5qss*7%)-p4$zVG$=tS9?`B-&%pKu)n*7!6bg_%uzzt`AzWzbRho|J* z&m7r|Gn%C1TmekTdKpFjbL&T_$G?H1qIX6u&EH%FT$b-B93oEt!irj$=Q|Vm=|Du= zztMivW~?Gt`pvyJVBw9}^KEZ|QL`Tg6}w2s=h~uj_hDYIhAig&2d+`h@@?Vo7^&yJ zg*^e4YE)o7e&E;PB=AX*9&JL9ZQMCE557W_wN2CkAWn08eC)&9)~}-t?K2=zVkPJ# z^p+QqbyaEzF1MS*8(skcIUI>o@{(kGIGGADB7VM>hCQDo&Y7D-kf09WrSuY^(Q|n} z|L>p^rq=rZL`?afDBv?|-`7p{r{i5S4?tvoffm*jr@DGUV^h)4Opr|L zggt*z-*IbP^? zGL0V07-qc;CfCnA3dc&58$zhJ=sR3_ zy?(eor@Mtm`}w{lA|9A(lC#d?)>hhaqn(Px{Su{>+|dY15yNEp*3qa|=)v}^5;Cb4 zBn(oxSa=q#l?SS!dZ1NUeS15&8at*He`KFHoE`M1?emo8;}5?IG|OgNzIHsHc-o0Q zrm+!GXs9&c_#~opXd%U8$+evxqSDg>nVnaf@rXV@Y8G3y$0FXq@ zWS}+Ta5D=HVPJ(wuGo4w(0pW(Dfg#fib=ADWQeIeJCqpOKUH75{8i9V%yPgu)iQ+% z!he0)F`&E`7SNzM8}Lwv1%arDnwql;3JhI{xO-{^C7{mjKh~7U#-U0$$}EFs`{P6)&I$vn$o9Tr7p%s+3(lI|MftehN$udB2JB1`i}#qmW=r zrj{d}d8R!mjxPa%DJTzhsNL*BXa72AYFXt!M?{^48u;g3{QvF)Hpr@Pl^YvbF8_JO z-0o7o1K0Df7l#_gp9=b~&m}RU{b>r2B`e%p_$YrwYsfNLT>MGcW#nz}_lvXj(w_?a zuOFFYa_LV~@#ovx<=LPA5xxESHnTk9k8n%;{rdml*Xh28*5%nA=p@{_Z{jm_v-3u<3AE2@IwWD8$C~imj4gU0aj7($L-Nnu}+I!o&&@uRPZMH z2~hk9VRth3A!($gmeqfq731go6#e_0Mf!1!{vbE2R2l3&LF7z&FMNfk$24FtO>!;0BzXtgnIp+$5Oy z-|N8at8~A!ES>`z zc4%m~%WE&O`d5MZbstt(x0>qe^KlrOHKdJH=EVY=$Am;ohv0Y!@aW`;&tbayJd`s{ z{s95x(2xDb&K@RY)jt#imVY1GuA_$#^}|Lf>|HAc5Ld#iFtQcXf~jcXFfFL#DRgHF z1N*8u(l~>lSDQjYLN87TnKaj#7ZuJ%|2%dvGh+xu@HB1pkhzJ_IRsrL3gdjz9V#(z52C_GN`JAc3WxLE1s?r)`PZ$aM1VDmv z?8Tde%pTWY=$n0<9c19)sj(?iytXdATWh3g5YNCrS`#`6HUd`&-k~*r#xJe%P@ERQrxCIC3mHJEI-LJAs13 zkF@oICxdZ*fnnxOg5z^2&0GhTYGD%Dn@1TZe=S?^x5{U8$IJRYRQC~JL2gMG@>*O3 zVH>CS;jim?cCmaq&E9p8{SxpE0a!EZaH#>Qc~VPD%RBwLifdR{krh9CdK95`mV#ne zrG65S!g90kxK#*a; zSl7Kw4|yjkgM%`J*y$phctWaJjt(Y>Zz79Zi>4^^PA>;v0PRSo3Th6BbC0xQUBm96v6_8vX^hxdzR z-{VRnvo7Kx>$)M<#b<8Wc6PddE^BP`I7{s0oqa{@T2JVkGoFx&61f|}^ZOghnxbZYaQGD%KtmtyrZ;hUNL*p7$@4?x4jg)zf8 zaK!kKtr8ng<)N1+PZGnT#|;A*F&VLM0&qhBYD^+{m|tH>1oA%gXAi_0h(pXHO&P2> znr6M}1`n(TrIB4{{jHxZent=o{YYC+FC2h?P(e>>yi80JWJ?S=709-@-_i0B-#bz~ z)0zqVpRtUQMKXitUg;iyHV@O;spr)~5x4K@&+zHKhO>#8Nn5QF7Vyl9eWmrU#^M$$ zZlWw-&p?f1w7%hCH0N`%GC;pQ+j=vG>Jge6kBs%Ia4*JXo#4Y=x;u9s!ejsGErOe; zuERt6#c*_Q=IocEJ2CeRWYu^W!#FPt_nU%?R;64hUX2wPZ?=Qnt-lEfWYNVk8S zX!dDvxkr}=eV&-_kb(|9Il1%E#ynS;OTF%{e>uX=ic&0)_f#w-oj&l|37vRs z*CJ_S(~)S%LQUYiXvOW@Fil8Ps0n**^CtV<7VN<0wk5 zA;*P%BjNnr)A0`lI_Op(js(A0s6a$O)%Yab>WkkQ@5eGULzP+=N9+BcFSX zoVTK!ylyPbXB`~(P^U?CnY-iS%ALqR2gt0*fYpzcc$M=7azAO0CjOYq5U0fF3j^KI z++(d;sT-bvBu4eQn$bJ=eEkFtkc9MI`EqB3NgXROiyZ2#$VS6LbwOH1@ujiGU1OepxnkC<>%^Ej2+7A>bXne5@u$`P}5rwSs( zrdHz^sAi#~>5NqeZSyVYUuASv>+4?@ihp*(UopNZOk*j>Zdu6X=+wh%Qek3t$ z;_Y#av7vfIoVLQf(OgBPl7-dLw%P!3Se_8E>SG7qpe>da+DdPV!cZR{9hash@GVT$ z8(h|q$?1vi_*$Z-l#oBbRaE)Q^R1rDX_9+(!sNp{9;&XC?3p|A?B0HqF|1ObF=usm zEUtXj6r(E5xc;XI{s#A!<;%;S_lb$}V0jJ)pe7s&5owY_@L|Mq*x+1~V9__{v6qb4 ze5qU8p>92$g++@d`bgOw3cHxjPCy>a7m;8L5+BoDTv=f}dn<-L6fJ_S5-@^5=p|qJ zq>t)~J+ERHTRmI3u`OYMsqdWieJ0{JO=gT!)-qc_Fh+tMF z3#Wr=_C^K~78cfR``7>~s2JF_8(eZ%c9COIpIIP`FXch)90tOFOQCDAxhh&HTI71@ z-4oZ3j%vb<$sctXQitH9#Q0z{vdbr8#@)UjRE7KZ0mrFdE z-gHktWBlnJIHWM9Rf~U_qPcGTGL=8?3&hC<+XT(GNOC6FlJLy2D`DEH-0n61Z)Py} zrvKeAgXMF|mpjkJh>ZcS{i*uC24=^r=Gl3FV*C;PpupGlviz8v8q=xJdrv{-5Q@6& z$sluVqffoYQ$Hg?x1Bt->bQQY>FS^+&e1%U+kC^&$-t$(^ z#?@9_)Q@g>DR&SKi)UcyyBdo4GAWjb(04DfCeZXCg3XrG0P!VQsQ|F7PF66;HbFrR_veD!nTT zRwFByCs3S#$((7v=nT}7jgr5#@doG8I4u<8%-M%NvVg?oxAt>bVS03*5K!OO4iU}| z)OKb&-tZ$N$Vv;|MmQdNiYL7^>FEooQ5MO@HrJ=|?+~DniVOHL)X@-T%17u*f+qb1 z6%+qtg7~CUMAUMq5Q)CURg3{zMZV!?_LIqCx(yw?YT~H zBP)T{$jkb#r={XbMRKrfF`#^N6&idSx29YeP;s>oU{kuQ+7ZUctvUyD@XHVvUR$DV zloBgey+z)B7diXNYT9K}1L6~H88hY5>5_Vq3JCb);^_<&Fo|<_eLkq<%JErp8gNQP9VKyE?5`kdwlEeDQDl6o&H8mt zQl|nYRO;KP~sp=ZceV6e)x<;6ByaT8Bq?P!wEz}3e!k;lZO4;P*DOKN8rL5 z15XT8>iI~uL*(As*_j+uSyZvH*{#kI>!HFbB(}%(W~98L;yBM+=>Gj5L#?d~xvXs= zZFbO(RzXd@7KdaJSBD{;zZ?M7qc*5-DFh%VfY$Q}fc_v1r29y)7wTjZ54ca)I}X0M z^&tg1BbGnjeu{_r5Trx<;PI@!>VNLYr)WUoZUD+~&mS<^?Lyr!9?Cl&NXg|ZwexA@ z{^OZ)<%@(_ub2wp)Hp3`lpir2GDiSRfceikFMo)Aec>7ys`li7Q_)1?9!zvdTNJEP z`heEO>}sb2f5#RyPVe7ZD^6INSzTcB+dVNsj^U}UI}{F>D*RX4>S5-SW=9QGbm&8t zMU8;aYj)t%DR}(i%GLAOgc+A(XxpQp68lmCecRNh3K%0dtlB)GoR5HQ!V_ow`C|_x zv{9gk-iAj=_$1tQXkrq$*A+OOohiUJo)p$lJq*3$2$EFAbY92LYDd)o5^P#eHXiY# zQX;kdIGrv+iawO?w&12{p!csS^piv;+qlD}E-Pq=%H#imv^_mH74$pz#yS1;!n~fM zh!sIcEo<`+;?>{J)^b|WF)?w*?l1eNJ%0cPw^<9bKb$2NJRjZ9f0l_v9vteRPmfO= zEP*a9eYLPNny-+oF%Ux1$i?q(0ckuof2p+m&fy9uI*_v!!Dn&+yCeyO7yrhtnSe&# z0$O&vnQEjf+dn{RRoAQ zYVp!D0M@bBXvXZEcO9J2N|}4GI5!D+OVzt00SNLu??{?s$b8PMJ+!yL|E(QW^L*fn z-_twGw+EU{1cqyEQbHKmAOF37FrY!L=u!ab)`ne>9(aDuzizwAeh@;%FiC1x6$jcL zK3g5ycbq(PnSx%cJ#oj4vlfO-m2qgDlAG< zu;_FR?kdw5f8;hJ&sGoIaq>Z94*+a)LZSf}wxI^3r`36(!xg0y;Eq}sn1byCu41M6 z-i(V1VcQZUFb_}kgNNTQ54Bw#QK`{XJ3_UM97v23kd^g5J!I!I)a&4Kh&c)ac_hd% z;zp$S&@mRZ-aq~%*U^a+E%BTLrO9$!%ckvi0<`$FXc*Ifuv)j3zU(%#bXlsdlED|mp--nif5wlPk)-2{;(CrV#(*3Oh;wFYdjNv}N{7g>$U zS}fyUqn0kVJYg_>E;HbRQ~bb1=8EfB5p*I)n6Y^$;3qc2%^R(L)urd-8*Ay6S}(X+ zFLtIkbh|bg7R|*fePMC(!LlJ2y?-Cxsm^<(+uHP3d>(Yn3L@~y*GK?+IOE`c*ml}XP=sSW7_Y}r`XPIn8h|zd0aVhOJGB#{X)B%2PX!3 zwhr(Azee5x0t9Q2aBgsL+Xmz@e_#ZL^#bV_d@Mc++wM8|fkXjCigUZ4i)=tw%Gej+lN2 z(`^9o=xzyR7T~-U-nVXV3;*XZLzolbIMdS7jG$0qC0GQsC@l}^ub%?)?6&AIV1G-H z5`LmDUn>N6iFi9`AbLhh^xR3~p)|2r1kLhI<7UWdNWg9fTTxpH002Vn&dy%@ zt3ymnc=U=MR^t?lQlQD9g9gh8@)@g(7}bM!>o zfy-^#7#J7=8_8(N<~gwLgU-fooZ{x{>X@yvU!AX0lo3h|$ET>4IgZT?t?pu>q_ApzQ2r9bQi%6@O zzJY=1PfhsmFDbEkS3vso&z!_Vst$&xhI<}`NTiEt>z!%ut8P~~>Jja7H!NuiYSB_J zPK?~#J`Kn2O};rZ`#6!iXy70MyZJKCF1hoy@*e{pCma@(Mt-=DZd#uw%CD?49NVwl zQ*98C8R3bXq{CtrVug`OZ1+Ay9luS}cULT4ZxM~NHBjZfTSIh0L@`u|xq$EtTbx^6 z^T7y?Ka$%x-e-Acxm}M^7@9o!#&vS>;#nDi#d#C)9KWpvHxX?z{;(%v@osgmFb@xp zo9=GC3UpUeL>TQ%cz=zJ&>m`P>5NWQ8Es$vp3q6PhO29 zkOCneN*z7DwMGLSD4bgTud1}&wgqj5cGI(}Ag{a3q9+NVHP&sjF1b%@a&1P9a*79- zcAmATIa536GHtUk-|cv&2?Tp?uk1{8QQS2zH$6d8f^DPfs_e@4`x4t!HhX8CV&zf! z0dk?g+}U!9RwQkRG~KZHOY9R?=(kxOXbWy834ELDxIg#$y<)s-qn?PU_h_YbQ5&^H zu2JAXuW6O;AL5aL6<6UBQ9#0XS9_UCr0M|jtav*rKphtft4Rom+`<45mg&T9WC2TU zdpt`DOhF664+(^lFbq0}a45E9E_Z>nM%)fLD5^ioVho-d2~e9!z$*9^Mvdj1d<2F} z0xGQ>%27BOcram!PK@*`;5n^ODz`qrjFL0gALOLWLkCbMt+twINM4ekzR00|h?UpN z!-E&tQ7PAB2I*FhH^1XY>BSz}X4CZwOQG76KEeI`NjD_7<|T2e(zRPsYBgOhs&%>e zbBdUe70lDW6xeIJo!B!wR30CiOG=8Y<@=_v#>{z{Dj366gd(QjUWNFY%8G6 z)FKyEf2@$IL2ow!dr;Ao&bMtvJ~CNAAjFJp3k7WDfIKk_>eEoz9#^X-0BMxNgIRf@ z$9sDhv7h=;6>X{fLF2?o`WugN?O9tbtA7pbRMa*G9M@|c(O=GXlRJK?DVse?ybbq; z+|0Q9oA-{g^el?Hjp^4^Y);Oqb*Rmog!?NF5IzJ&!J%ffq+v?=+)`uBtZOvODUP3- z+Sj7$JsJ3!XH??9t8$UDDs&)4UCmeha7mUXjvdMVELqzjP0C>!|A9+?@c)BL-?IB3 zap`s78U%e$d7V6=VGxJ0X4+L&Z&a?lggoYHtp^b^$?gdRyGRHy^OiQhqs56Zg0PZK%14-)QFzu#xtX%_e#=q;_7{ShBTGd(r5}?*UTlq=QmKj%OhfZV{jk8vP9nR{v_FT#&2d2z0uIyq%G zY~R)co88FX?>E9-7-x3*X6tSWw=cpRYjEhJM%%FH?no%vok}E1Ep~rzv_oxnIJ6|dt;*{E5iV&k>tJ{6j zXY2D9rS$a@YwHaW2bDE!ksk|`v&80P+1^p)0-7H{=-hGV38!X70>4nBKXq<-0c`g^|HjTGpg}4qoSfpC%`-v(~jyCCJdAPHghVnjhDnaKL$%w z>_-pQ5qkuIPjhfhr;epVzM<0$^XYR=raWQi@V#X-$>(gO{&b6Ep}N6>I%{sV;}$Ur z8jiq!g|F|jlGI*ALfb0xjOI2*2_sY`D^;&vW=Y3{%nl=MJ4M9Hu&?a<|$lJ_XL+}#5kd~S*G^?Cpi3#!^c}zgSy3< zCCMKE4_~tV2M-sO`k(Od>$((?&BY3Ct=2^!u(_=$J||jD$2-2FcmQO)=Tyz@>oHt` z!wPT6d?{4uG83JDPqPg668A;Uy%1aXU>3J^pQz5z#?S(TdLL%bMwlc zsS+<(l$a|K^5J&FfdY*ng?=61S8ZRsp=tBERaRcjIZEn;(8)tE;KQ4!;IRM!BN633Gwv3*JpLAs(knNwj-WG%4*sSbPqTHCvyUqx;ZhS$;yS$@ z-!yCYjcA4jFumLm%Nyr}x0xvpBGNZb+L^59y@N;+Ub=GZ7OnijsZlYeU!#f?yPi3| z`e^Rp;847lnOCZ>r9~dcqWp$ghlBC44F(DtMl_x6TZ+=sF`aKV4SaeU!Y2Lk;Q`pj zwjYcx962WtIhYu_gn9zFri(0SbNyVLRbs*Y{W(Pim$O}(qqqxQ80i(hMVDwV@(%TI zxgUG)LrjNUvI_w|e$K9siT|aF`a2bw)5Uo5p>It0dH}iiikkiG)6-2nQtrANQiUp( z9Lbq0lecyZ)uH3P%IQsKjjfYE@D9n9f8Z!&0f1DgRXk1Zxc$_B2aQp6*&|X2x!WOM zF`>{M6TS-Q{YMM;Kf&w_Xh{E5L?>Vbg6v>{Mrw-wC7238LLMGaP@{E?6lwML4sL)f zEbD9VKe{I_3K8G+NpP3%C9>&M%oo7iDP~UBl>hoeT0z;E3^o;S0Gg(XD=d0_mymWy z1@P5_JSg=a{807qaNE=gBCxiQf}ZP`m{ngm%qGBf^8v(`f23JND78pXf0KY!DV3M1 zxVTt;f{f!uEE|FSEJ2>#)ufIl=Uaj9i!*t4Xsw*2q@>pRJC!2aH|m@X1$R2ER-Z(N zG93G5=Gi?GGpvKbRJDIGx71dol5z9cm@ zLSBpd$CpO@1A~J2r~3y6)DBNhQtJ8eqsYj~C7|YISfKt|uA66v*~*hYB!(Ke-ISr7 z>!^rFv2*;&V4S&>N{`Jp@#*Tm@@c`kq4Q||+unWeEqcWrcRv%KbAUa_-DbaD*ORyy zdFuDr6;Sj(KpS6!-Pn8@njCavSi6SUM8I<_@@#!>OZEX}35ox4bKoFz^1;GGOLPEM*1d$GDn51+|swhe~N_R*KDAF-$Lm^#DGk03_(p+hYBYhRLT()Y zoY^}Ug3wUC5VZf`R(#vwg$y<7p3ciVJwP{YApoDG_jY0Zk@#^2_##Yp+A#< zW`2VoJDpqLiKK`2Jvl95i&8e6j5%(Bfr~^}=g0@R^`1Z)?}oB|8<0vB3CmiWVPx44 zE>rjk6%k$|C&({j-*yr}cGJC$jZC`$4oQET8yp-oAZZt9wmZ2JqRrC5iP$UvpZX2E zaRq4R^;OCPkwwJ_pjrynDRU+0eRHoD8Fj{TJoNy&;Q;Ux4-6qu{WY8^9#gPPzwGb?U3lg(_0b5vU)U1RAeP!#hbBzV#x-`)R2JXuu2B0ktlpTu%Ai=T|$U zfHqvtt)2w#1+EQ%Eto~-7X4Wt+>!apiR8ZS5Ke_R8*cxA*KS3s3RCZTt@!M}-M{5I13<^!^~4nM zGrm31OMiXFHg1%qx-f_X7Sa;MbdQ)z1DyfTEuZNZx@@bEHr0y?Z|J&%OK!J3;<2=v z-QF0le0qt&Yk}(PHdtE)0mvy6d9Og1L}419Pc({t)(PQ4V)D$CF1Hqae?y!fDXAB) zx?WPvN}f_tJX6`lBtGy<$;R1$w|SG7WOL^wPBh<0B%1BuQgha-m+P-i&gO4MA#K+* zKh!#omS##~GnHxdEGecuNfpW%*zwez%ou+B8Q*TQ;AhVzCPC4#L_$$Y=ASyv8>j*Q zti&(2`@ssps{{P>!joM0e7!T1*NM8=)I%>{dXDqS zBm1^*jeC~G4liZuy$FuY^H<6mukP&&EDW#7>5f_!AnZNB^jHD(O<#8+A|kR|q6lO; zkpOHOzfro#a)&_I^$65WzvV_Ko+8xBVCarzHvz3Bfet^$(|9^?7^W-|a~+~HBWG~Q zqE1mE+A7~z*mXv5RHVQap^R}2cz2pwhgobNEJyfzz%ZTU{CQ?1o&x4zmt-Jo1DrRT z0cKZ>V74Czei0tjHPIX@YzuALkZjcgs8hNX{B`ZUXwCEx&*$IN}V4=aWr%XWs+K5^byBovJ2cq17*ppa`ABNoc80^HUA5AG2?U^yezhF! z=AM3J1vbz$DdUt12MLuH%lh=s;&un^46B>&J^r;LJD0TBmwGd@Z#REb$pxMB+F!>- z;5PMse1GQ)(Y9j5s^q%34tF@K7h3~>y5j$ZrKtzxQ3+sVG=uexKTK6)9of#_Y6XJP z85?-KoEZwq46ck=Ty~;kB9_BfU>@rwj^^Qa!lmTlI-{7QQOE-dXif-5p2M7%8Zfe!-uPXbEGj0>cJNErvqi)))U& zbyb58^=2Ujrt`gNd72-LX24(=k+06cwCLmHq-cj0&bc%E>YYP8?zM7`-LJ^Ua)>7P zSA9HC$3IF{gh_|!Zh0Lr$7$rhp?}V%KvQ)1J;O{ohV@*uO=tWi3&X+>6%3?`P&mA0Mn0UzKFIpzJ?E`*UhN_EsR~UgJq(TQ-Or15w9=BedhNJ`cTg5MzT z;qSCBK)P=RzDfF3To~OF48SR|-TvG1;k$=|gwphPRjMx?&NFFxs^r%s<+vLBCH5GLDbKxM(w5^@YPjE1 z*jUYfaGbRz(R2k8_%9ZcQZr0yt6eewD)){?CDcPkVABQ{;WrLp6VL=fjVWuwq{whL z!-`;R_ldiQtPzWN)y2E9s<&R(&jqy`mtyVyzCG*y8;kBq>#vZ+*nwK_eErd$MN*k*M6AVBt9a+UsD6r(8M#&K zT<9eqj_?nt$VCIl+$Z_f= z)f)ck&~ybx66&82c|pZz^h{O3xzYDb>un*{CwMZKW7(DN(saoQd*^bil#WvYzt;SD zhknq!0rPjIqB}1(9P=oXDJIEgKJUb2)<46We$T&m1CMM;j-6Rt=4QNN5a;ibg{i5j zC*|*SakC-}ZE%4z3e^0*B?mJQS7gWYn0o~T2EMvTY$D(5!JAHeo{lEpfaHL6m@q+} zM?QHbl{@@qzi73`=;YjWo)O)e&98)KV=g{8pJS$nl4Z{#HNZ5NeaMk~E=Tk338T7; zg`SYXBaJCM##Ld;w}P1KpZ4NY4|l>N57?T9gMVev2nsUA0(S5 zItc6CfO-M<)a1U)c4nqR)*KQW<(P-k`DK47R#IjryaerQA+E-ywJ6t20mtf zj|a+1iwSHdD(%LU-;{of|FpbZV6BA*oZtG=9OO7 z(h_}pDxbiX<*(5fQx&h6c-362v~aRcTg^;!!JMrH_2K8ni(G0`f>(N*6X9ohBs$3} z6QYW393QSEpL=;R{yc8S+-qA3G3ZnA%jyb%+yz+AFv8_!9|~)}BMQ{Tcz(<4R-cXxLsaOOw!-aU}Q^CZ)=RrNHb)yMWt{zqlw zbWDB%y6Za5i)u_OSm&lISr(T+ktg%J=SmU0r9poWygRlB*!a-xy}Gk=q(*xKt0oi$ z=lD|I5(~P;bEBxao}q7Sm8+@IEFC3S-aRPH^W1-r<)XlpOPDUrrtpRgH-NVu-cnv>E4MDEjrX(sW;8PeAD^Y zT@I<+rkIhYr19bU29;thxnQvr6%xN8XG2I@R_BVszNZ)w+Vw zir#^$`u?i7-A-N$1HH$SCORPx7DQ85iUYfq&(t0#VzJV83~{)lj+`bLthi_FR_ji7!oTjw5^AG?}sE2v7SLF$}U8CZ23pcmNMb~A(*-@gTQte>+4PNw1u33h?rULs_;_V;e^knhOs?uq zKo!%%zH3G}5DsNk?gol=IM9#b5w|y|b)pDG#&0VP9X`p`{LSplM{Vw&t(PR?M7O*2 zP%dCJBmF&SOYVHSv?o^3Z4>sd?B${-RT0}q(@kfAOM=T%8UVfN$yfV(KvY6o@rh0v zdZce}|BD48zw`f_diFXdgj_s(UsS~Jc(ol%zD~6!SR=8D-1ssA2Z0d+X9VeE)&7a2^&W6dIZ@z-B!QGiBogSK;*cBvIvWkC4!DS- zHsolP9|^4h@U)I^&9o7;phd(npiw^oT998_8~+7)i~b*y5aX#sZMq4PMh?ppwwyx_#u2=J+chkxKTww848>s;H}x$gLsok949<0Z#3{Rfv9ObM{Qu$o18RagK0?VLgh~W3Q2{6UV}#G! zXh;a9`M+srXMjcfyQORe`X+vbm)FeNVbqj448G}ud0I~s5RN4X_bd?zZjJ9h0s{04a3dD6yIJto*Po-Ac+> zNKLKJ;&r&CQGI;0UwERneYLqgyJ^QXTYRGA{ULPQ^I&*f^&(kenG<~nw|2d{HwbfhxR8mF7uk=@ zA+!hpUIBcz^iYoNV`!bZwZQo8Qdhm}CfgRUjdGaZqv4}@al*Wv6wG)+8O@4|iz@@F zwSY?}=qJ#zXzDwE-%JYXdi~#X(RzZ#qPCFm1n7T7C`ql`>icjim}ScmpFALrRTFgf z^i+a?lc|f6OLOh~sY-;(9tTr``Q4q}fn1F*m*kVy`b%ziMIzY&&o4^Xfr`koHaXd# zwt}t?0VBc8&v6pSXiKTL?M5h0Rsyv;JaPpq80DV(^0pd5`225Xn9D*dIXQJ5rwH3_ zdje4h=V05ej#np;*J~iW_|x@qU-gsb0=Lvot0C^W{6HSWyl*5{)Cm+>w-cN5D*X37 zxM2TJo&Mk^NP)k{Bh>d@f^G2FUDfzWTisbw*TfXR*yZ!TgU!BpTw|IuFk!i%N zYeY7Ii!OtRN8$pkcKJQ5W57by&PUE8K$#av{s5gyx~>EM`ei!h)|)U>-;z%*>nuUZ zzfMY;i09N;VJ`r%ui6w4?TA@N5xbe@hX@>sV>`3uZ_JJwP3hcJg%$Q1ik6)08peTH zTzq_oxMR7VxG{=36W~X;$AK)N{#7y1#`e#h5c#Z3&pL~F#ia9*gv!)FctGSmXAdisB!`i=dhwhWjeFv3^BWQ6qX1XukB7~{YHcaYpco4Zh zaPc4HDZcwST|t*CQb?^gA$0|S=7Ot_yMBardp5_eQMDY&nJJ)`mo|hJH!FVZ-%>g# zs=!!*w(u#Bc1hONx?$q3mIb-?x|W1f6aAp2r3>W8hI*SbgotJgpz9mx^^9f?5f_yy zzpOuu!qbel2%m6Qxgk&vP6X#>ryHb2!Rz16hu=?shqZ3nbJ+98W8%?0FBD>-a3%5% z*(sY0bG+vdh_UWb3m&vfM%}OD=Hlw2ILEZhHvzj)7)H&UONeXXamxN1M=$#*{S9lW?gYs*IB5h&cFK3^i?sCOVQH6n@i!%ogc|EnYxnH`;3Uk1wO&Rg@Z zssU4%zR*Zc(Y1xm8v?%cIr<`!YwTB5b_i^mGj@Y83NgrE*JZjBIfQ>?4BhWNm0M2m zHjXqV%a%51q8?UP(5G{gU`gDxb>bIE_{xLj@PVPD2D~&4@vNmXu0CIb`SLh!Ns1>y;OF&SrJH zOApNl%Qb;Z!*03hAMR(zu_Evg{z?4o`QW-fBcc6U(8Fi?Oc-T$4L)N=MRS6kCAx`& za>G8>HmXyo0r86MI=r1~IRX< z5{0fNTMf%u@Y1!hzaPXs2?*E;$!~VdBYc#O+-R$oaj0C4B;_&o>~c3+{d8 zvmCs|#pQU;4^}to0BzEt$1b`3^ZCS_p9lIElSt`cm(HrNp-l4f=jv<1=^mo>aW`=z zmyV=TTj^<%q~n>*+GQpMOiIk|6@Lj=AV?Z@cGr!VJ@^o+rM_3d1Cgh3VFn_o51om( zXP3+$Ts9D+jk(E2H0bD_dLz=J2rT-2`SSY;Y?e~TrQDYX>IX71SBkqQCiE++t3!cF z8w$EDJc+hj8-fy<8vme%%1lyrd|RdD<>f`HQ$DNFYx$#ZSBlkWcf&x~6@IO7_YHXn z+9Kjzox7C1S6;&r%W;w!KW-}R4>?011xqnIw?Yxyv2zoxkRJh_}X4m2I z7i0Y28=t1uGh!tD!czUExq>Y?+E)S1q~%TJGHBF$Y1 zZYP!+N-vNeBIcbjFXLkMudyy3dQ8vvYzlHrdTk9e5GYt_aU4c1)GxMs+;k7TqBrQ= ztv02l*-nWTGZ~X?c90__97~Sv=Nm}izADu(RAE%6o~$&d?I1E>{54hxB^f=}4j>r@ z{}(dtBSee>$CoI9Sj6tAs4quiYy#60U;Jt$1L=7JQz$+ru5p4|p*||IhEB_Q*UC@s z4i-H!)@AT9I4BS!rZ~r|No2~({MIM$!SAq^3S|%HTk5~cS#BOYVex#*q!^4c!+lO; zkFL7KV~?=|>2cPalZ-wT#+V{NCf(a;vZ`pV+AE=b;Z-3%b)V3IsUmH~QtpvxCeGJ( zF-~3PeL^20-8M!_93G|%d231uW$dd>iG`St8rn9|zTnU|`cn;}mt>C96 ztO--uTg>k{4BFfs!N0(35zTNb0p|&GhR;PoSK*?^Y8GjWeNKA%r#Dc<2}*?5nAd{3 znUe^-hcJA-Gz(}}?_8j7R5mV{-LycoW^rvqy>pt5@8A&e`2muYi(*&Ed~CQ>4&KxU z7_$2w?~>k`Ec;Q6(qoj;qjujnraH$Gl6J;JY>H9vpwH(zuRP-w$5&lFHuZ;Wi2vee z&KndiEV80C>$4s(XS}=aLswZa<9Fs*Uq&>9B(0>ZthIjQ&ul?;T`WlTuzic%;46E3 zcq3yolr{b{rqDbX`jQ&imOf>OKTrOb} zcbb;?gGcp@qJ^g7+U9y@R-;a$J?r`zTayB6-N}7D^cDI|Mo|w3A?SPuMfV>RR(r13 zv*rjCc$j?L;9GJSd9$<2P|e4|n;To#`o254ZkW-_(J(JOKYT(L{P- z<3S2w^u2uSp6`QzCEZeoNw8MyvlQ(&OqnhqzxUl-4dhAlf9f9D1mg7g%t%G16iFMu~(Z-f^W7;i^`75ET+?B zMCdIJzP}W1%VnaKmy-OUch$Y-#`2EBbP@NBkO&dK-^X+N~ z7irM~S@NZD;`Vz{VgmO~5wbOf)aWqdK>T#*`;87OCF()2$|Q{VM@f9RvF2?@ zg-`9n7A<@YymEZ;DSYaz4f~|m!B(7S3&Nmc7?FO{#+Ret#>yazwd!CN%KxYiI}Gzh zdPVa{bx!#wL=4tAQW0Q|SCqqdMCALiqY&@GFSBZ%LQGfTRzl@*5&cIMv82;b702Qy zE4Akg^y|-k6Vi)RqpYfPQqn)2u))XCw9i?9sjF93xHOx^`N$?2|56W@Ex~VId!y(Z zj^;giT30=yrWrYrSig3R74WS~pRxibLksxyu&nxdv#tc90|{sYyf9L&D7AD$=I`O+ z0J^C6bG%w3Pq$JT7pn;{@pOzQKnt)7(METlDj?w7TZR2ua&mH(&Q6<_ z0b*akW{g_s9Jab{+82V?v+w?8Fa7#mvT+JN1Jioq;{i%$o17mz*>8UotQ0bMS@pM; zVK&PRFeoT=lV$~mc1iYMZC+@=`I>agwJ6pe zJA(~Jgn2ErAt33@W!We7uN+);F=6C zxVbnfSr3TsNS3AQzHCf-qXH*1>Qh^UTvtWx@Ye)7Tgd>9iTRH5Pg-Q+{< zV+Yi7$0{pb%%Z#W?>sBiLJ@Kqn2gpW%oL$Q41(FbWvjplG0ws52SR!MISjA>SXs^~ zLf7?VrwJynA0nzS3Vy;~%(7$o%qgPClE6}ox3{xu6E&;o-%HTWFWKM%JPl=H4Lw&J z6wsmLQw+_SZn)rl`03QV1saM(<*pK|-qy_bLEhY|vb@bv2ciWEm6w|rT{?^TY)m7Q zx+y4V9?wSgGhHjSSd9EI$>Mof?GUOupKaHMZ5roSzIcJ9+$=h|wsd4=QYxXVTkXR9 z)q%P4f-(8>Z{2g`vZ`E8nT>^x-3w@ISvzFr$1<@zI6Oos0%;g4&{wBj zbe{d1dW=Qe_I)DDH~a(E*%R@ZasxUzQ~Ec{Zf4Ro z_)8vMTBg$y*nfK!tsyIl%cy@l`CV~$Rot;WwXV*%hy$`*K&f1&@x?;jK( z_WA&JQg6uAIr&J*uSA)p%r2qmD^vWV{_D3C1qn}oo%&^!H0z;5p^ah+X~QbLJ2wY9*I4ZMQtNTI^txrS@7(ea z+Lq2OH(YqkP7rkG)~TBvj*g!fga@|l`YE3mUb#Xczmtpv^P$f2^if9O&91z?c*fkz z=sgcsJ#mRt@Tr=~nPNi0BcZ$8DhICKqNHnQ{d6fQW`7<0>Qm5AsuvVNM+}krrXaMw z@F5IrktPgVs#Ki9dwG68mHUMyUn%WVVQOJT+ewxr`OqXhQlmgyNDdA0Fcg9X=Jlp| z&c(G9OSV?-kNXT;g|mCRu=r36QdW!!6_y4^Jk7VhTyD@hwn(WIiZc&Ti)uLQi`cyDJXrs<9d^bQIhP2#JN+{=07hF4M^nc zarK>*cWyB&L~MBE3)KShxMGFb)w#?|)&V0{>iCz*Wcfv5<)g%U+BDDCP2J>-cR5pd zk6B+4A9GqHIkgx+_9lKfS>0Lxn3DOPGE+__rBSyE)8HiKj6Mld5nEA=)Tv~w46eAG zk52~XJFXtLp$i3|4(3W)L zm%3y&75jE|sa;XD+-Zko|M>L{@jd1I0mmPW#vCfS^cN(Pw}SB{dJ=91t1OauG~HkA zpq6DX_Es7ps8yJwsSv8AF6FYje1%^;&SL7aNJ5kC9>2LEaJ)US1{*o{vP^^aqN&(3 zk;5ZQ##4BI=28zk1wcH8L|ajoAFlg9r^xh2UbD1(4j(|iSBXDEU~7c4!^;e;iC zx_}Y9Lw?Gqy}fy#pp#@OBGQoO4H1?o$r>#Pn^!@PQ5iW0>9Dsmb&IdnUBC$xt5`E7 zc{79ysM^n?X1T4m&n(8OPi#0gURzn)DPi*37&OW8G`Z(oa=X&5UQYR9bS{5m* zTduI)S>)*&-NP}|5cdi5ek`_-<7FdD<^vbN5i?XTY5X^%-&P*;^Os@t%MRW#K4e07 zthCpD{fhN8PK|$OZ>wrBd=~qyQoxi2IHX-sPF1Bim5J^s$@igUv??cSe;g+~nr+aQ zB*XqTfe$9W&n^nyE$O1+o19=__Qqo`oXT}Nn0O{ziES-+?XdA3zL>ngpALBl-b!&cW#Z@CV0G*=PRGhSit99^bdz z?{P#i-)np_-H}Pm^2GN7hHrJi7!m*Il?6A^q=3i^Zv>azTwS>vE*eNFiuGs|CL`D! z6^5*Sgtwz5%L~qXhT?sDI9-OlPC5ajfCS-TyDHPC;l9XAbg;2gh03LxAArcZ4~4(| z_}-`(KjoZUMR2*;LYAm!(vxM)a{}H!AG%)PzSrcg_G*Y9``gE~Wv4(Og`ZOy)6S&! zHP102xzf;b;r84vI&k1gCtdgybAuEhFQ2+AjBf7f9{g$3YB27-Key5Inst zO%^><}fBz2rS%nfcp%R5kE373lqQ%MR4iLS8d584y? z=gixQS@Ngkdos)i_$~oHE#DaXK|X{4g;9$Omqm2L-EC#QQpBpVE2TWUmiWRRLqjLS zyjpleCFFN(xp#3Mg$t9&PD^@ zbg8aSEgahKTd8QAv%H*sm(+LK=oVGF%)g^2WRre*QMIqij^mx}+)sg>deef-uL?CM z+VZQeE9{zW@kPmUV4XcHRZTEwk1>R64!_#3JPY>cvBVpp>XWOtJH| zoil;(CAxdcpdp1!}}JpC$lv?xu-wE6xy76tKV!ZO=Cl%~^50^`y{cDfE zKXs6&Rjo@;h(&4E_PuSI-#K`7{57XCz24(sO0j%C3#C3dIO)WDP&^YYfG1P=c-uuS zbH5!$XBoytz~8gCy?pcZrcgfFY26o95A1xZaudPInx9T2(^#&3>hU0Jv>6CGU=rWZ z(qQuMllVjfWD@VQ`|p$ZYrW-yh^bciOt_`qmkw+Fx%lw zzQI209cJ|NbenhYIUtO(PjG`d3oC(rs@n})IRV?Aj8rMF{v|d3Lk@xiOk!BtXg()z+plxv`WEu1*B|B`3hlg)cf)+i#OA;(D2Zi@YWmgu?V z{=(nlZWufJGlFVvC7~O_o+29_tGg`d$hjl;!JHys5atEU3FEokdiCD&*Ioy~W&Lm~ z+5puO-D&qr6AUzdm_zEj2P%(0A8e)Ht$Q~7xa-Zj<3MX%!Qf%~pnrv>S1a+Pc2+6c zWY^{_|Kq1#R&O8caCu}f_fe#q!HhrbZ9oZ~*Ozw%E&v70);@q?K!W+NnMf&*`h!)k z`PzPVOz^MWOpG8UKMx6#sb zj{T*N12lTvN~;NY8&z3e*Ikx_g?D(9KUF+EYVIJVp@`=nzdFymJ?y?vz^a88??z>? zOsG;~r$}8+lGBOg!gF^k2-$imLr9d{kbhe9G?K#dZHjv_Roe|5H-dt=&YrHgD5gjn zQ*h-%f-b)pX;M*+wuXilK=uQV?=U^W!&Hk=C@_3I3GEq7b%CEfFt4I01$e zE5g|Rf8C2g8prig@bo3+5Lzy`&Y#5A<^KU zYK~zdgh+w>!!#C@2!lTQ1DIQu*4;uMh#?YO!X9Eqpt|De6!T;eF#8&~YOrZi%HoOK+vr8^Y z#PeX61ZN_}qY0k%{-;j9`)VfxMkg$x#yGG z7e+=#UO!cbXd)7ULYz$~aH_HW+du|4yr6@8T{)fUz zICaW54+eZReOXGW55NlM_V*t@b{|5`S+57iP}gM`RY96^jLg-H#(O3o>hrvNo6QYV z^E}pW+Tv%YPW2*gLEw0~{OHjmQb^iZJC_KihBfT(OlZIg!z}b48nP6sXOLDjbsn>2 zcWf2-^LnkRDm-b-1W)kqo>;-R9k`{aP->t`h5 zN5p0A!qrn{PEJnmGzASCytBd4X?eGl%DEG4 zyeiyU-bJT3Jdjy&mVVnDq_D)i)WO&RJCwl_&o-KR#MgR;L-1i> z=ld&@YsY^l=UZRzWfsc|pH-{%9QvDE2~2}W7$%pW%TizX5#pW8R3?lVA=zi+_HU@w z-iA@2)5mA$n&zGp!Dxd`FyE0$(5FdM~HV8;>}Jf z_IcM@1Ca#5oH^sf_v_)Y9eoAY#f!}hK3a6NGIz7$!**dmu`Cijx*`~>3j0bT$^#ut zdg<1QMB_K?rP#I$)^=vVaETT+!nls9=%BsETLCl?He2N$wKBaEi@cf&#wP}s)%<%h ztk1R-!sC`sIQXehBO+RFGg(;C_XJ_Qgb}?cKs>^o5wdjFX-NOEqwNIki0dNUwkC+= zM0e5(rsn0^h@K{k2RwWZRb1)XU{cQUHS{8ml3&TRidR-vB1ybkHX0i4XrO#6 zslt|FPk#b#-8C(}S_Ay%fDK%njgBo_$MLi9vO&>5*>BV+#l(>dJK@Ky?sFykY<%z*>L| z`}ULX;D@mhMNnvD*@bdsI4Yi|j9IitkSRN1LLflMHZ&xkS?W#iyekKv$O&@*`Gp#e zYUIrN^YuTJ9Kfv!bo?(2!uDxd@P;x}{u7r6c^%PqSG+A6AtNwm_Fv_4;5N^Vgg6a( z17QiVnL7DuZTdgtBmM9BICtd#H+&pxgu(*s`-DjK`|(^OnTS`|GT2@vA#%~s$g)4i zWym{l{lkIzuWt4chEM*VIWSFdN;$IYR;>90r}<~0Zso>_Pa>Q>w*C;F#nXZ+(5-x4 zsQB(KC4~FN_H=YCzRFlmhwYICkZ>K491c(KaI5DHYjrKTaUtIBkj&_;)AcA>C;%+{ zJotV+LUgEqzT!W@!1&9TFNIJnCg5(KB%Oi8-yeh|(%~1Dkf0Lq+ClNKjYZ_CW9dex z>mXi$FoqZpm;)oX7cfw6xtqufx#+A|_D{dH+d=A&L-+XD*ciCUlp8lHKi@h&J zRD6dA2a7FR6~tW+U=Q^H&B7#nsq7Dx4k4aBeE%;rho^`gAXB0R=&XFZ)UZrJ_<=$p zM~hC?oxnKb9X&y+Ag%I=lH#8Hv=*p zhLAjtbx<}$gQr~dbsK#^XYu;^DL9nHMLrt=KOzNmN7`1!z!+Wk(_kpkM;hT*%mWLgSD6Cq*aU|{2$cR?e3Pc1{%EL{YZkSdCH*^T_ zr|$Pbb@FWHL%5wNpiAP0x-WsF#n}>3 zy@pBDIZn=8A^J_wudw))!#7ew%^PaJ4}d~C%0l^rk%S#gcd2$r4BGzVG&_gba^9O$VlaK3jSkx0Umz$9u%%t2)^6+wsFzSV!mHZJ>d8kC~)Rfsv@w;}|Y z$8-4z29_zyOlGlgKeqn{=A6tj=sxd!D}o9`Rz9aozZ53(6nby*nr-v8NO2-nEfm^_XwsCGgwG7cv2&3K9bX! zgc!0Lzr9)aFONl5+Z$gU*PV!Z9)w#2ecrGN4*19`$+@xfseD4Y6aJb!%-YLsOWMW==hPn)57|EcJ zvoR(x)O)e0`FD}BKZMfjVtY!L6!- zYHLdjlmDUF`>D7@M3hidMYE}~pZ0lMl5wNrrTNsUBI{hWT(-|`|GF$&U|C)??OMEy zD8S$j(k=vVcUMJTHK@K~I?iSF0dK&JpCVSKAq@{JOQ00&yF@7d9^Nv;|IrwT!Ie$G z10jm_|6;aCA^XanphLUdI-idv5qTE(W8g5EYe)xMMXQtY3q;4*wx5g^RC>Kn{+#l} ztQgUkw|XE>g0HG>!=d<3=f<=~818sR&x2%Q-U|eo$OZCGLx@?}@;pe8%Cp~4{qvqM zZWj?#Y^9mM)k`!Dh?{kex0W<4;7E;v3yly-vW4r=?r%!deF=m#OLxN|i}^c<7y8>r zMt;X-gNHBF-K2jjw@DOOc)B9?xFFMKWh^)bFeMxwUv1MrqJBa#$FQd0QvTy4eM0vu z(nPM!CFpSAc76S`A|eb8o`Dpd&sjTBD&KCWtRHl7*i>mPZnq+Qi#I-z~RJ zqxtxcnFA{pY*!wiDJv-YSFMKMzcYh7r5i9H_&9DJ8Xo@NjBzwzIVpZq6> zioZ+kK?teSy4WGB3L4 zLeH?g&XrwL+`KpKljNhHh*iUV zO2i=K>sLiuT{ky3TrV##mFs~s(W$AaIJmgyM_&KDXU$D7*werKZZ~ctA-KsG(5g+a z8V6^JO@Uq6_xE452pBJ=6L=h7VvZZPAlxVW_2R!KTjl;Z9;LQmOeF_#%AORQXbH%a zCjopwtl5>*+JD2HLxw~>ndcoM#dAx*UqhBNfJIEfv0Qd^xHH)X7s~Mlc-MtO?#tm9 zxVi0-Y^KC26g;O`AyD=p4@^AmA`*lI1Nvp3sb#psi?4ySe-jF3^H+a7C;s_2k*FOa z*sICO2=UknA%sF&-JsY*=&~f>M+HYpVB10opqC2AYadc%e*k}n<1gT~Qdk*AhDM+S zu-nqsgrdn zn+$78_;XesXhUv8#(XeBwTCoZd(Qzh64L8{7Y7|0=Z`Lw}P zq|k+zRa$^ly$&^sL%65O!0?Sz3TV=%yt)Nw?k0G!cm7!qsx5U4u*zX9px#4Y;)SjEw_ODqG+CDwuJ20j=J* zZB#Xh;4W{~DI{|wUQZ0eq>ru{v|M|S>cD8_=*cW%%)fZh@W(qx3F?LK0z8P9yzxv1 z{GJlCG5E0>SELYZo?7FWqq}#tj!O@#!_rz_dTr29;lGR%i;dh&C{zE?BIvv zLJ4AWN$BY4Oe(>NcevOzGD}lQ<~arIit}*AD^=_1=;-ugQoybVE22Z^>68}?m)j(d z?k^92xCdi=&cWH<=C(E|P=n|!TLEM;1Lo+x2T&#>#uG>g7t`bJH}tOKAQa~1o%E84 zFFX7u_R`wRvBdf;Gc0t)kSj?y{KlE5|yeDf1MLHt$(^(0njFb$& zc}*yWOQJU_uFj&w?fClA*lvGAHfqcBxDW(;{b5XXrTwqTX9ZIFjv`Kb-OlTFlFiKi zwLi30IJjs{dn>ixASQyK`$w_fQ%dwcW%Aie3UA1e(ae8iJ-hPM#nm+!EN_CKX*CJ$nx1B|9-pzrLe631H_$rg^7c2x%>#DvygSy4&$Rw<`;{h$Y+_CRk^U9! z%V)oa-G~O_XAo!wf`RLQ-$!$IZ!5PwSyZ$`_x`{5x~71oPV^8gbWSkc;sHI(MM65E z3*%=YAUslRJncd!G)CeOet_>522~aXfMSx{#SXfoiiC@;&A@RxvvZAOQdD=$rfQ>T zoYLC8pJv|~o$U5`<+oGIS5Zr=yn6HMT8?FlpRH*%UF&e!iFls-K%}*^uc=o?{>s$G zPx*oiI!pBp##tTodsg>ua{Au1F(o3<&(feiFKKrZk$yk?GS)s|IFn<5-)FSxEvgC= zhKn$3WC3L8A~K|#sQ2PMW772TV>6@I!Mh8 z6TcFJ5ky4|uu2Qk03oKYp%mQWPo89vLxSS2$H`+}qfOOz$7S51<;#`FJrSaomR2R6 z-ENVqA829JEwf;Ozkje0dbqA}wso;6KhYt-FSqIUBg$e2>LawzBzEad;~=Zc|8%hU z<;3k%wR*3nd@WPBys@*u@?Po3c4Xn$47QHe#biS}r3BctuRanPh2u3I>rvq+4YMm`mF#69~3OARq5N zfI~HPT>stOGmsNc6WbhJ04ozs{P{X(3ny7^=$)m6ZiAl_Sas4X&nH^)CXR0ot#AZQ zS&h6*x1nNjk?omA`PX3_u>)=fRtHRe*{_g2BOdz}8H-Li7!0U*lKg9unM5H46K`Ma z14>X$yhd;xMUE)nWe6*|v9S&P{r!ZF{i_#^r=YOVyFpG0s=|dd70zf!O=!wWM?`S0 z=xAj#zLNhNWF~UB& z_(0=-+q&?!6YFFY+gv2sm+#y7oz}Neuh}$+7;UV|T&=Z-es3I}KW+Sp<+L-v^wFJB zX6Di|vFs#PFKTORcb5w5(~5EM2~3%$o=R*(LK2M!W5x!@kC>*)RLUZ`mFu?K``7R@JDO|qL(OIrnkzqkP`NVx@q5i!+uhoa9ef}e*;?~D z{6R%^@oeA?iKi2EgLwzha+6DwFIj~5ofKz_+P<0i(Ingq*&u5FI4+p7CBxMjE%JG( zkd`GPl6k7OV1)<|y7BnS7cYu~*PNzX_pFdowMu1)UmbD#$LtXtW4OmO$D-hn=Ny@7%##$_TAe(X!$YwqCq$5>g}2zzwb(kfU38-bZnOs|;%iur zN}S38FJs%gZoB=;dNP>zq6A;d3*0X*1mM8gRY*sC%=-JRJ znu5d9Zz}W^?$qy*eqV|(7}4I^rk}U|e#?H#Gk!zV zq_P`-?8i5n3UKE9&|~f%Q1CDtuM)e!5~!Za;iFLcO3$gp$v2&tWM55)GKPW|#8q20CSu)s z$Jd**@s^kEzOPkUo&(d0WyY6(GTKh3{mE#<_ox12ml1*3WlXo>FOE*PB_5>WvB;!; znT0VE(%`4y8wu{Y^RxKA6=|ch3_OU(kbj ztZL!@tD@x$bs?R-%!*qKYZsn&-(XD*yX$pp>K$1nPpp8aZ*r6R&$!-PL451V47==_ zp;#NV{hr&3q}Rdb=6mw}9EKOeb{to2w*oOkzrE~bXY}=HG;WuSI|P2``h?(v*hzE% z)inaghPiH@Smnm#JEPcTq|e9BJe65J4!v~LIaVVZ!;bvtam>W=OW))G^=gxF*<+q@ z-GgDCW8Kb-qr$s^nDq>UB?Tf7d~(Zux62IiL*g??QT}k#Ze0KF1tLY4pTm6F-EZ+B z+RZPy>yHEEQ}E%HcvpbU19h$>ov5eo4ZZVGW+7ZEvFcF?pKK3-_LTy{#t$n*TTDs{ zl&NTlKT*3p?fu_Juc=?(jl$n@neTc*YN7>6mjNOd(Gk4=Op&C z@0LW=>8e!MhO}s3vGX=zm?d7Bd~f{5+bgg{JO5ZoGXU;$SxhRdo1VYtD9x1BN}edK z!|C|)ts@C6vpC1)QMIc6@@IEJgqVWm15GS7H=0VIEPDFYS2Y{$t&LX+HZcmoCE9*N zu%A+UT-)Y2saNQ@jJH1U#=rVImmcCgw%T4fFdwNyXLapCA<64)XSyum_qm^*>3azd zZ`4~!42%a@&Uwin30%Jy|I6l=+0(myP=k|DQL(?CE^>4H6?reoKh;B>u!VPSLw@%( z;o*&Y`Grdm$O8zJ(}~q=io+q3|G!(!8s}wy3|hybqHXd98+~cotiW6g!#}xg95E=w zPFCZ!@+H27szRdNfCv3sA}_VI6Gp5`c2J=tL z8z(Vqn7+-r+4@6ug#Y;X@c4NC+3MRNud}k?KSRziFV}bP5}Aq_Iggk)&s7#$43E4R z8L^2Ef8jjgDw-ln{jY!&_P^m|r~l1|z~?VWh}>)Kcm5(GCuj8Ae?zDg@uz6YFB}Lv zuO$61+TH@H%C-9!RY5_eJC#tnL!_ld1Vp+)2@#R*20U6@W{_+Cb)`j=adADR!~KKS{lr&UPdyvtwllmJvlx;uKhG7KK363lVJV) ziwBfsS@L=9LhH7Lv^-Tm!m)CG0^?5 zZT|Vg#-;$DZ?6(c%{^{jUd&#TQRFeg3*(%IlY$SE*Q@~;1hB%`NB-3ju*1&^p(nZz zRtR^K=F|T9aHN=5tU@%vpTNS|#pN`09ZYI7FHuISe$m)0tr~s0f`lxJc%FV)e})ql z9}eTLZK%HIcK*&o14UtBe4Cnj!aW9i?JDR<-H~}9^!A7|oaP6uE5bUwZttr`B{tit z=2E6TE{a8Fa`B^{xVl$}9iNF-o<0#xJorkqmpkrQ?~ad;-~IGK1%8}gS^Q7i-qXAG zw!EZirQc>8J%7-8?rEoY67X10^7`zrC?7{JYZc8FkWVZU9(fWSp4$@RHn1Z`vJVWK z_CB?8r)~TCT@<~P;w9cgAak6alTe!ZB=b_&?)aU5cBklha9~R0&v%b|qkZPVl&6fj zQaK-wFi1G2(&AW6`>6inf7Vua!ugOu%+&MwRtAT|f3U^!FazN&;y+ab>1k zjbpEEz7IJ5`|MBc?i1JS!s-yQnyMj%(MvnA^8{=ITSv%tIsi!IdN_C^^uZb$0gqF; zrK}_9OL!5(79Jqw6axM61|1!p==NNjqb=nAu>wl_@jHM<*O5VI8StUd095SeOxqoX zVOI!PL+*g)O!`?{q7c#h1+°gfFiJ&V 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

pi*O!;Ir;ti^ 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 diff --git a/docs/internals/data-structures.rst b/docs/internals/data-structures.rst index 7237306bc..45de9ee34 100644 --- a/docs/internals/data-structures.rst +++ b/docs/internals/data-structures.rst @@ -19,63 +19,51 @@ discussion about internals`_ and also on static code analysis. Repository ---------- -.. Some parts of this description were taken from the Repository docstring +Borg stores its data in a `Repository`, which is a key-value store and has +the following structure: -Borg stores its data in a `Repository`, which is a file system based -transactional key-value store. Thus the repository does not know about -the concept of archives or items. +config/ + readme + simple text object telling that this is a Borg repository + id + the unique repository ID encoded as hexadecimal number text + version + the repository version encoded as decimal number text + manifest + some data about the repository, binary + last-key-checked + repository check progress (partial checks, full checks' checkpointing), + path of last object checked as text + space-reserve.N + purely random binary data to reserve space, e.g. for disk-full emergencies -Each repository has the following file structure: +There is a list of pointers to archive objects in this directory: -README - simple text file telling that this is a Borg repository +archives/ + 0000... .. ffff... -config - repository configuration +The actual data is stored into a nested directory structure, using the full +object ID as name. Each (encrypted and compressed) object is stored separately. data/ - directory where the actual data is stored + 00/ .. ff/ + 00/ .. ff/ + 0000... .. ffff... -hints.%d - hints for repository compaction +keys/ + repokey + When using encryption in repokey mode, the encrypted, passphrase protected + key is stored here as a base64 encoded text. -index.%d - repository index +locks/ + used by the locking system to manage shared and exclusive locks. -lock.roster and lock.exclusive/* - used by the locking system to manage shared and exclusive locks - -Transactionality is achieved by using a log (aka journal) to record changes. The log is a series of numbered files -called segments_. Each segment is a series of log entries. The segment number together with the offset of each -entry relative to its segment start establishes an ordering of the log entries. This is the "definition" of -time for the purposes of the log. - -.. _config-file: - -Config file -~~~~~~~~~~~ - -Each repository has a ``config`` file which is a ``INI``-style file -and looks like this:: - - [repository] - version = 2 - segments_per_dir = 1000 - max_segment_size = 524288000 - id = 57d6c1d52ce76a836b532b0e42e677dec6af9fca3673db511279358828a21ed6 - -This is where the ``repository.id`` is stored. It is a unique -identifier for repositories. It will not change if you move the -repository around so you can make a local transfer then decide to move -the repository to another (even remote) location at a later time. Keys ~~~~ -Repository keys are byte-strings of fixed length (32 bytes), they -don't have a particular meaning (except for the Manifest_). - -Normally the keys are computed like this:: +Repository object IDs (which are used as key into the key-value store) are +byte-strings of fixed length (256bit, 32 bytes), computed like this:: key = id = id_hash(plaintext_data) # plain = not encrypted, not compressed, not obfuscated @@ -84,247 +72,68 @@ The id_hash function depends on the :ref:`encryption mode `. As the id / key is used for deduplication, id_hash must be a cryptographically strong hash or MAC. -Segments -~~~~~~~~ +Repository objects +~~~~~~~~~~~~~~~~~~ -Objects referenced by a key are stored inline in files (`segments`) of approx. -500 MB size in numbered subdirectories of ``repo/data``. The number of segments -per directory is controlled by the value of ``segments_per_dir``. If you change -this value in a non-empty repository, you may also need to relocate the segment -files manually. +Each repository object is stored separately, under its ID into data/xx/yy/xxyy... -A segment starts with a magic number (``BORG_SEG`` as an eight byte ASCII string), -followed by a number of log entries. Each log entry consists of (in this order): +A repo object has a structure like this: -* crc32 checksum (uint32): - - for PUT2: CRC32(size + tag + key + digest) - - for PUT: CRC32(size + tag + key + payload) - - for DELETE: CRC32(size + tag + key) - - for COMMIT: CRC32(size + tag) -* size (uint32) of the entry (including the whole header) -* tag (uint8): PUT(0), DELETE(1), COMMIT(2) or PUT2(3) -* key (256 bit) - only for PUT/PUT2/DELETE -* payload (size - 41 bytes) - only for PUT -* xxh64 digest (64 bit) = XXH64(size + tag + key + payload) - only for PUT2 -* payload (size - 41 - 8 bytes) - only for PUT2 +* 32bit meta size +* 32bit data size +* 64bit xxh64(meta) +* 64bit xxh64(data) +* meta +* data -PUT2 is new since repository version 2. For new log entries PUT2 is used. -PUT is still supported to read version 1 repositories, but not generated any more. -If we talk about ``PUT`` in general, it shall usually mean PUT2 for repository -version 2+. +The size and xxh64 hashes can be used for server-side corruption checks without +needing to decrypt anything (which would require the borg key). -Those files are strictly append-only and modified only once. +The overall size of repository objects varies from very small (a small source +file will be stored as a single repo object) to medium (big source files will +be cut into medium sized chunks of some MB). -When an object is written to the repository a ``PUT`` entry is written -to the file containing the object id and payload. If an object is deleted -a ``DELETE`` entry is appended with the object id. +Metadata and data are separately encrypted and authenticated (depending on +the user's choices). -A ``COMMIT`` tag is written when a repository transaction is -committed. The segment number of the segment containing -a commit is the **transaction ID**. +See :ref:`data-encryption` for a graphic outlining the anatomy of the +encryption. -When a repository is opened any ``PUT`` or ``DELETE`` operations not -followed by a ``COMMIT`` tag are discarded since they are part of a -partial/uncommitted transaction. +Repo object metadata +~~~~~~~~~~~~~~~~~~~~ -The size of individual segments is limited to 4 GiB, since the offset of entries -within segments is stored in a 32-bit unsigned integer in the repository index. +Metadata is a msgpacked (and encrypted/authenticated) dict with: -Objects / Payload structure -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- ctype (compression type 0..255) +- clevel (compression level 0..255) +- csize (overall compressed (and maybe obfuscated) data size) +- psize (only when obfuscated: payload size without the obfuscation trailer) +- size (uncompressed size of the data) -All data (the manifest, archives, archive item stream chunks and file data -chunks) is compressed, optionally obfuscated and encrypted. This produces some -additional metadata (size and compression information), which is separately -serialized and also encrypted. - -See :ref:`data-encryption` for a graphic outlining the anatomy of the encryption in Borg. -What you see at the bottom there is done twice: once for the data and once for the metadata. - -An object (the payload part of a segment file log entry) must be like: - -- length of encrypted metadata (16bit unsigned int) -- encrypted metadata (incl. encryption header), when decrypted: - - - msgpacked dict with: - - - ctype (compression type 0..255) - - clevel (compression level 0..255) - - csize (overall compressed (and maybe obfuscated) data size) - - psize (only when obfuscated: payload size without the obfuscation trailer) - - size (uncompressed size of the data) -- encrypted data (incl. encryption header), when decrypted: - - - compressed data (with an optional all-zero-bytes obfuscation trailer) - -This new, more complex repo v2 object format was implemented to be able to query the -metadata efficiently without having to read, transfer and decrypt the (usually much bigger) -data part. - -The metadata is encrypted not to disclose potentially sensitive information that could be -used for e.g. fingerprinting attacks. +Having this separately encrypted metadata makes it more efficient to query +the metadata without having to read, transfer and decrypt the (usually much +bigger) data part. The compression `ctype` and `clevel` is explained in :ref:`data-compression`. -Index, hints and integrity -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The **repository index** is stored in ``index.`` and is used to -determine an object's location in the repository. It is a HashIndex_, -a hash table using open addressing. - -It maps object keys_ to: - -* segment number (unit32) -* offset of the object's entry within the segment (uint32) -* size of the payload, not including the entry header (uint32) -* flags (uint32) - -The **hints file** is a msgpacked file named ``hints.``. -It contains: - -* version -* list of segments -* compact -* shadow_index -* storage_quota_use - -The **integrity file** is a msgpacked file named ``integrity.``. -It contains checksums of the index and hints files and is described in the -:ref:`Checksumming data structures ` section below. - -If the index or hints are corrupted, they are re-generated automatically. -If they are outdated, segments are replayed from the index state to the currently -committed transaction. - Compaction ~~~~~~~~~~ -For a given key only the last entry regarding the key, which is called current (all other entries are called -superseded), is relevant: If there is no entry or the last entry is a DELETE then the key does not exist. -Otherwise the last PUT defines the value of the key. +``borg compact`` is used to free repository space. It will: -By superseding a PUT (with either another PUT or a DELETE) the log entry becomes obsolete. A segment containing -such obsolete entries is called sparse, while a segment containing no such entries is called compact. +- list all object IDs present in the repository +- read all archives and determine which object IDs are in use +- remove all unused objects from the repository +- inform / warn about anything remarkable it found: -Since writing a ``DELETE`` tag does not actually delete any data and -thus does not free disk space any log-based data store will need a -compaction strategy (somewhat analogous to a garbage collector). + - warn about IDs used, but not present (data loss!) + - inform about IDs that reappeared that were previously lost +- compute statistics about: -Borg uses a simple forward compacting algorithm, which avoids modifying existing segments. -Compaction runs when a commit is issued with ``compact=True`` parameter, e.g. -by the ``borg compact`` command (unless the :ref:`append_only_mode` is active). + - compression and deduplication factors + - repository space usage and space freed -The compaction algorithm requires two inputs in addition to the segments themselves: - -(i) Which segments are sparse, to avoid scanning all segments (impractical). - Further, Borg uses a conditional compaction strategy: Only those - segments that exceed a threshold sparsity are compacted. - - To implement the threshold condition efficiently, the sparsity has - to be stored as well. Therefore, Borg stores a mapping ``(segment - id,) -> (number of sparse bytes,)``. - -(ii) Each segment's reference count, which indicates how many live objects are in a segment. - This is not strictly required to perform the algorithm. Rather, it is used to validate - that a segment is unused before deleting it. If the algorithm is incorrect, or the reference - count was not accounted correctly, then an assertion failure occurs. - -These two pieces of information are stored in the hints file (`hints.N`) -next to the index (`index.N`). - -Compaction may take some time if a repository has been kept in append-only mode -or ``borg compact`` has not been used for a longer time, which both has caused -the number of sparse segments to grow. - -Compaction processes sparse segments from oldest to newest; sparse segments -which don't contain enough deleted data to justify compaction are skipped. This -avoids doing e.g. 500 MB of writing current data to a new segment when only -a couple kB were deleted in a segment. - -Segments that are compacted are read in entirety. Current entries are written to -a new segment, while superseded entries are omitted. After each segment an intermediary -commit is written to the new segment. Then, the old segment is deleted -(asserting that the reference count diminished to zero), freeing disk space. - -A simplified example (excluding conditional compaction and with simpler -commit logic) showing the principal operation of compaction: - -.. figure:: compaction.png - :figwidth: 100% - :width: 100% - -(The actual algorithm is more complex to avoid various consistency issues, refer to -the ``borg.repository`` module for more comments and documentation on these issues.) - -.. _internals_storage_quota: - -Storage quotas -~~~~~~~~~~~~~~ - -Quotas are implemented at the Repository level. The active quota of a repository -is determined by the ``storage_quota`` `config` entry or a run-time override (via :ref:`borg_serve`). -The currently used quota is stored in the hints file. Operations (PUT and DELETE) during -a transaction modify the currently used quota: - -- A PUT adds the size of the *log entry* to the quota, - i.e. the length of the data plus the 41 byte header. -- A DELETE subtracts the size of the deleted log entry from the quota, - which includes the header. - -Thus, PUT and DELETE are symmetric and cancel each other out precisely. - -The quota does not track on-disk size overheads (due to conditional compaction -or append-only mode). In normal operation the inclusion of the log entry headers -in the quota act as a faithful proxy for index and hints overheads. - -By tracking effective content size, the client can *always* recover from a full quota -by deleting archives. This would not be possible if the quota tracked on-disk size, -since journaling DELETEs requires extra disk space before space is freed. -Tracking effective size on the other hand accounts DELETEs immediately as freeing quota. - -.. rubric:: Enforcing the quota - -The storage quota is meant as a robust mechanism for service providers, therefore -:ref:`borg_serve` has to enforce it without loopholes (e.g. modified clients). -The following sections refer to using quotas on remotely accessed repositories. -For local access, consider *client* and *serve* the same. -Accordingly, quotas cannot be enforced with local access, -since the quota can be changed in the repository config. - -The quota is enforcible only if *all* :ref:`borg_serve` versions -accessible to clients support quotas (see next section). Further, quota is -per repository. Therefore, ensure clients can only access a defined set of repositories -with their quotas set, using ``--restrict-to-repository``. - -If the client exceeds the storage quota the ``StorageQuotaExceeded`` exception is -raised. Normally a client could ignore such an exception and just send a ``commit()`` -command anyway, circumventing the quota. However, when ``StorageQuotaExceeded`` is raised, -it is stored in the ``transaction_doomed`` attribute of the repository. -If the transaction is doomed, then commit will re-raise this exception, aborting the commit. - -The transaction_doomed indicator is reset on a rollback (which erases the quota-exceeding -state). - -.. rubric:: Compatibility with older servers and enabling quota after-the-fact - -If no quota data is stored in the hints file, Borg assumes zero quota is used. -Thus, if a repository with an enabled quota is written to with an older ``borg serve`` -version that does not understand quotas, then the quota usage will be erased. - -The client version is irrelevant to the storage quota and has no part in it. -The form of error messages due to exceeding quota varies with client versions. - -A similar situation arises when upgrading from a Borg release that did not have quotas. -Borg will start tracking quota use from the time of the upgrade, starting at zero. - -If the quota shall be enforced accurately in these cases, either - -- delete the ``index.N`` and ``hints.N`` files, forcing Borg to rebuild both, - re-acquiring quota data in the process, or -- edit the msgpacked ``hints.N`` file (not recommended and thus not - documented further). The object graph ---------------- @@ -344,10 +153,10 @@ More on how this helps security in :ref:`security_structural_auth`. The manifest ~~~~~~~~~~~~ -The manifest is the root of the object hierarchy. It references -all archives in a repository, and thus all data in it. -Since no object references it, it cannot be stored under its ID key. -Instead, the manifest has a fixed all-zero key. +Compared to borg 1.x: + +- the manifest moved from object ID 0 to config/manifest +- the archives list has been moved from the manifest to archives/* The manifest is rewritten each time an archive is created, deleted, or modified. It looks like this: @@ -523,17 +332,18 @@ these may/may not be implemented and purely serve as examples. Archives ~~~~~~~~ -Each archive is an object referenced by the manifest. The archive object -itself does not store any of the data contained in the archive it describes. +Each archive is an object referenced by an entry below archives/. +The archive object itself does not store any of the data contained in the +archive it describes. Instead, it contains a list of chunks which form a msgpacked stream of items_. The archive object itself further contains some metadata: * *version* -* *name*, which might differ from the name set in the manifest. +* *name*, which might differ from the name set in the archives/* object. When :ref:`borg_check` rebuilds the manifest (e.g. if it was corrupted) and finds more than one archive object with the same name, it adds a counter to the name - in the manifest, but leaves the *name* field of the archives as it was. + in archives/*, but leaves the *name* field of the archives as they were. * *item_ptrs*, a list of "pointer chunk" IDs. Each "pointer chunk" contains a list of chunk IDs of item metadata. * *command_line*, the command line which was used to create the archive @@ -676,7 +486,7 @@ In memory, the files cache is a key -> value mapping (a Python *dict*) and conta - file size - file ctime_ns (or mtime_ns) - age (0 [newest], 1, 2, 3, ..., BORG_FILES_CACHE_TTL - 1) - - list of chunk ids representing the file's contents + - list of chunk (id, size) tuples representing the file's contents To determine whether a file has not changed, cached values are looked up via the key in the mapping and compared to the current file attribute values. @@ -717,9 +527,9 @@ The on-disk format of the files cache is a stream of msgpacked tuples (key, valu Loading the files cache involves reading the file, one msgpack object at a time, unpacking it, and msgpacking the value (in an effort to save memory). -The **chunks cache** is stored in ``cache/chunks`` and is used to determine -whether we already have a specific chunk, to count references to it and also -for statistics. +The **chunks cache** is not persisted to disk, but dynamically built in memory +by querying the existing object IDs from the repository. +It is used to determine whether we already have a specific chunk. The chunks cache is a key -> value mapping and contains: @@ -728,14 +538,10 @@ The chunks cache is a key -> value mapping and contains: - chunk id_hash * value: - - reference count - - size + - reference count (always MAX_VALUE as we do not refcount anymore) + - size (0 for prev. existing objects, we can't query their plaintext size) -The chunks cache is a HashIndex_. Due to some restrictions of HashIndex, -the reference count of each given chunk is limited to a constant, MAX_VALUE -(introduced below in HashIndex_), approximately 2**32. -If a reference count hits MAX_VALUE, decrementing it yields MAX_VALUE again, -i.e. the reference count is pinned to MAX_VALUE. +The chunks cache is a HashIndex_. .. _cache-memory-usage: @@ -747,14 +553,12 @@ Here is the estimated memory usage of Borg - it's complicated:: chunk_size ~= 2 ^ HASH_MASK_BITS (for buzhash chunker, BLOCK_SIZE for fixed chunker) chunk_count ~= total_file_size / chunk_size - repo_index_usage = chunk_count * 48 - chunks_cache_usage = chunk_count * 40 - files_cache_usage = total_file_count * 240 + chunk_count * 80 + files_cache_usage = total_file_count * 240 + chunk_count * 165 - mem_usage ~= repo_index_usage + chunks_cache_usage + files_cache_usage - = chunk_count * 164 + total_file_count * 240 + mem_usage ~= chunks_cache_usage + files_cache_usage + = chunk_count * 205 + total_file_count * 240 Due to the hashtables, the best/usual/worst cases for memory allocation can be estimated like that:: @@ -772,11 +576,9 @@ It is also assuming that typical chunk size is 2^HASH_MASK_BITS (if you have a lot of files smaller than this statistical medium chunk size, you will have more chunks than estimated above, because 1 file is at least 1 chunk). -If a remote repository is used the repo index will be allocated on the remote side. - -The chunks cache, files cache and the repo index are all implemented as hash -tables. A hash table must have a significant amount of unused entries to be -fast - the so-called load factor gives the used/unused elements ratio. +The chunks cache and files cache are all implemented as hash tables. +A hash table must have a significant amount of unused entries to be fast - +the so-called load factor gives the used/unused elements ratio. When a hash table gets full (load factor getting too high), it needs to be grown (allocate new, bigger hash table, copy all elements over to it, free old @@ -802,7 +604,7 @@ b) with ``create --chunker-params buzhash,19,23,21,4095`` (default): HashIndex --------- -The chunks cache and the repository index are stored as hash tables, with +The chunks cache is implemented as a hash table, with only one slot per bucket, spreading hash collisions to the following buckets. As a consequence the hash is just a start position for a linear search. If a key is looked up that is not in the table, then the hash table @@ -905,7 +707,7 @@ Both modes ~~~~~~~~~~ Encryption keys (and other secrets) are kept either in a key file on the client -('keyfile' mode) or in the repository config on the server ('repokey' mode). +('keyfile' mode) or in the repository under keys/repokey ('repokey' mode). In both cases, the secrets are generated from random and then encrypted by a key derived from your passphrase (this happens on the client before the key is stored into the keyfile or as repokey). @@ -923,8 +725,7 @@ Key files When initializing a repository with one of the "keyfile" encryption modes, Borg creates an associated key file in ``$HOME/.config/borg/keys``. -The same key is also used in the "repokey" modes, which store it in the repository -in the configuration file. +The same key is also used in the "repokey" modes, which store it in the repository. The internal data structure is as follows: @@ -1016,11 +817,10 @@ methods in one repo does not influence deduplication. See ``borg create --help`` about how to specify the compression level and its default. -Lock files ----------- +Lock files (fslocking) +---------------------- -Borg uses locks to get (exclusive or shared) access to the cache and -the repository. +Borg uses filesystem locks to get (exclusive or shared) access to the cache. The locking system is based on renaming a temporary directory to `lock.exclusive` (for @@ -1037,24 +837,46 @@ to `lock.exclusive`, it has the lock for it. If renaming fails denotes a thread on the host which is still alive), lock acquisition fails. The cache lock is usually in `~/.cache/borg/REPOID/lock.*`. -The repository lock is in `repository/lock.*`. + +Locks (storelocking) +-------------------- + +To implement locking based on ``borgstore``, borg stores objects below locks/. + +The objects contain: + +- a timestamp when lock was created (or refreshed) +- host / process / thread information about lock owner +- lock type: exclusive or shared + +Using that information, borg implements: + +- lock auto-expiry: if a lock is old and has not been refreshed in time, + it will be automatically ignored and deleted. the primary purpose of this + is to get rid of stale locks by borg processes on other machines. +- lock auto-removal if the owner process is dead. the primary purpose of this + is to quickly get rid of stale locks by borg processes on the same machine. + +Breaking the locks +------------------ In case you run into troubles with the locks, you can use the ``borg break-lock`` command after you first have made sure that no Borg process is running on any machine that accesses this resource. Be very careful, the cache or repository might get damaged if multiple processes use it at the same time. +If there is an issue just with the repository lock, it will usually resolve +automatically (see above), just retry later. + + Checksumming data structures ---------------------------- As detailed in the previous sections, Borg generates and stores various files -containing important meta data, such as the repository index, repository hints, -chunks caches and files cache. +containing important meta data, such as the files cache. -Data corruption in these files can damage the archive data in a repository, -e.g. due to wrong reference counts in the chunks cache. Only some parts of Borg -were designed to handle corrupted data structures, so a corrupted files cache -may cause crashes or write incorrect archives. +Data corruption in the files cache could create incorrect archives, e.g. due +to wrong object IDs or sizes in the files cache. Therefore, Borg calculates checksums when writing these files and tests checksums when reading them. Checksums are generally 64-bit XXH64 hashes. @@ -1086,11 +908,11 @@ xxHash was expressly designed for data blocks of these sizes. Lower layer — file_integrity ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To accommodate the different transaction models used for the cache and repository, -there is a lower layer (borg.crypto.file_integrity.IntegrityCheckedFile) -wrapping a file-like object, performing streaming calculation and comparison of checksums. -Checksum errors are signalled by raising an exception (borg.crypto.file_integrity.FileIntegrityError) -at the earliest possible moment. +There is a lower layer (borg.crypto.file_integrity.IntegrityCheckedFile) +wrapping a file-like object, performing streaming calculation and comparison +of checksums. +Checksum errors are signalled by raising an exception at the earliest possible +moment (borg.crypto.file_integrity.FileIntegrityError). .. rubric:: Calculating checksums @@ -1138,14 +960,9 @@ a ".integrity" file next to the data file. Upper layer ~~~~~~~~~~~ -Storage of integrity data depends on the component using it, since they have -different transaction mechanisms, and integrity data needs to be -transacted with the data it is supposed to protect. - .. rubric:: Main cache files: chunks and files cache -The integrity data of the ``chunks`` and ``files`` caches is stored in the -cache ``config``, since all three are transacted together. +The integrity data of the ``files`` cache is stored in the cache ``config``. The ``[integrity]`` section is used: @@ -1161,7 +978,7 @@ The ``[integrity]`` section is used: [integrity] manifest = 10e...21c - chunks = {"algorithm": "XXH64", "digests": {"HashHeader": "eab...39e3", "final": "e2a...b24"}} + files = {"algorithm": "XXH64", "digests": {"HashHeader": "eab...39e3", "final": "e2a...b24"}} The manifest ID is duplicated in the integrity section due to the way all Borg versions handle the config file. Instead of creating a "new" config file from @@ -1181,44 +998,6 @@ easy to tell whether the checksums concern the current state of the cache. Integrity errors are fatal in these files, terminating the program, and are not automatically corrected at this time. -.. _integrity_repo: - -.. rubric:: Repository index and hints - -The repository associates index and hints files with a transaction by including the -transaction ID in the file names. Integrity data is stored in a third file -("integrity."). Like the hints file, it is msgpacked: - -.. code-block:: python - - { - 'version': 2, - 'hints': '{"algorithm": "XXH64", "digests": {"final": "411208db2aa13f1a"}}', - 'index': '{"algorithm": "XXH64", "digests": {"HashHeader": "846b7315f91b8e48", "final": "cb3e26cadc173e40"}}' - } - -The *version* key started at 2, the same version used for the hints. Since Borg has -many versioned file formats, this keeps the number of different versions in use -a bit lower. - -The other keys map an auxiliary file, like *index* or *hints* to their integrity data. -Note that the JSON is stored as-is, and not as part of the msgpack structure. - -Integrity errors result in deleting the affected file(s) (index/hints) and rebuilding the index, -which is the same action taken when corruption is noticed in other ways (e.g. HashIndex can -detect most corrupted headers, but not data corruption). A warning is logged as well. -The exit code is not influenced, since remote repositories cannot perform that action. -Raising the exit code would be possible for local repositories, but is not implemented. - -Unlike the cache design this mechanism can have false positives whenever an older version -*rewrites* the auxiliary files for a transaction created by a newer version, -since that might result in a different index (due to hash-table resizing) or hints file -(hash ordering, or the older version 1 format), while not invalidating the integrity file. - -For example, using 1.1 on a repository, noticing corruption or similar issues and then running -``borg-1.0 check --repair``, which rewrites the index and hints, results in this situation. -Borg 1.1 would erroneously report checksum errors in the hints and/or index files and trigger -an automatic rebuild of these files. HardLinkManager and the hlid concept ------------------------------------ diff --git a/docs/internals/object-graph.odg b/docs/internals/object-graph.odg index c4060e6ee459563579bab6ce3d094c6726d9b9e1..7b7cadfa0b56ee8ffb7609f57bac96e7b2276b96 100644 GIT binary patch literal 28430 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

MonB+rQ_{v?Ix1kQat= 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%0DhPy8X|f;lv7 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�Z8Qtk>DFP47EH^aM6_m-+o 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
  • eqYAX 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 diff --git a/docs/internals/security.rst b/docs/internals/security.rst index 68bd647b2..aada49e3f 100644 --- a/docs/internals/security.rst +++ b/docs/internals/security.rst @@ -31,14 +31,14 @@ deleted between attacks). Under these circumstances Borg guarantees that the attacker cannot 1. modify the data of any archive without the client detecting the change -2. rename, remove or add an archive without the client detecting the change +2. rename or add an archive without the client detecting the change 3. recover plain-text data 4. recover definite (heuristics based on access patterns are possible) structural information such as the object graph (which archives refer to what chunks) The attacker can always impose a denial of service per definition (he could -forbid connections to the repository, or delete it entirely). +forbid connections to the repository, or delete it partly or entirely). .. _security_structural_auth: @@ -47,12 +47,12 @@ Structural Authentication ------------------------- Borg is fundamentally based on an object graph structure (see :ref:`internals`), -where the root object is called the manifest. +where the root objects are the archives. Borg follows the `Horton principle`_, which states that not only the message must be authenticated, but also its meaning (often expressed through context), because every object used is referenced by a -parent object through its object ID up to the manifest. The object ID in +parent object through its object ID up to the archive list entry. The object ID in Borg is a MAC of the object's plaintext, therefore this ensures that an attacker cannot change the context of an object without forging the MAC. @@ -64,8 +64,8 @@ represent packed file metadata. On their own, it's not clear that these objects would represent what they do, but by the archive item referring to them in a particular part of its own data structure assigns this meaning. -This results in a directed acyclic graph of authentication from the manifest -to the data chunks of individual files. +This results in a directed acyclic graph of authentication from the archive +list entry to the data chunks of individual files. Above used to be all for borg 1.x and was the reason why it needed the tertiary authentication mechanism (TAM) for manifest and archives. @@ -80,11 +80,23 @@ the object ID (via giving the ID as AAD), there is no way an attacker (without access to the borg key) could change the type of the object or move content to a different object ID. -This effectively 'anchors' the manifest (and also other metadata, like archives) -to the key, which is controlled by the client, thereby anchoring the entire DAG, -making it impossible for an attacker to add, remove or modify any part of the +This effectively 'anchors' each archive to the key, which is controlled by the +client, thereby anchoring the DAG starting from the archives list entry, +making it impossible for an attacker to add or modify any part of the DAG without Borg being able to detect the tampering. +Please note that removing an archive by removing an entry from archives/* +is possible and is done by ``borg delete`` and ``borg prune`` within their +normal operation. An attacker could also remove some entries there, but, due to +encryption, would not know what exactly they are removing. An attacker with +repository access could also remove other parts of the repository or the whole +repository, so there is not much point in protecting against archive removal. + +The borg 1.x way of having the archives list within the manifest chunk was +problematic as it required a read-modify-write operation on the manifest, +requiring a lock on the repository. We want to try less locking and more +parallelism in future. + Passphrase notes ---------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 96bc6837f..025b5be55 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -35,18 +35,6 @@ of free space on the destination filesystem that has your backup repository (and also on ~/.cache). A few GB should suffice for most hard-drive sized repositories. See also :ref:`cache-memory-usage`. -Borg doesn't use space reserved for root on repository disks (even when run as root). -On file systems which do not support this mechanism (e.g. XFS) we recommend to reserve -some space in Borg itself just to be safe by adjusting the ``additional_free_space`` -setting (a good starting point is ``2G``):: - - borg config additional_free_space 2G - -If Borg runs out of disk space, it tries to free as much space as it -can while aborting the current operation safely, which allows the user to free more space -by deleting/pruning archives. This mechanism is not bullet-proof in some -circumstances [1]_. - If you do run out of disk space, it can be hard or impossible to free space, because Borg needs free space to operate - even to delete backup archives. @@ -55,18 +43,13 @@ in your backup log files (you check them regularly anyway, right?). Also helpful: -- create a big file as a "space reserve", that you can delete to free space +- use `borg rspace` to reserve some disk space that can be freed when the fs + does not have free space any more. - if you use LVM: use a LV + a filesystem that you can resize later and have some unallocated PEs you can add to the LV. - consider using quotas - use `prune` and `compact` regularly -.. [1] This failsafe can fail in these circumstances: - - - The underlying file system doesn't support statvfs(2), or returns incorrect - data, or the repository doesn't reside on a single file system - - Other tasks fill the disk simultaneously - - Hard quotas (which may not be reflected in statvfs(2)) Important note about permissions -------------------------------- From 60e88efa943a73adfee256cb6fcf0a177427bd39 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 1 Sep 2024 20:50:36 +0200 Subject: [PATCH 68/79] repository: catch store backend exception, re-raise as repo exception --- src/borg/repository.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/borg/repository.py b/src/borg/repository.py index 691c3f45a..23c3b1509 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -3,6 +3,7 @@ from borgstore.store import Store from borgstore.store import ObjectNotFound as StoreObjectNotFound +from borgstore.backends.errors import BackendDoesNotExist as StoreBackendDoesNotExist from .checksums import xxh64 from .constants import * # NOQA @@ -177,7 +178,10 @@ def destroy(self): def open(self, *, exclusive, lock_wait=None, lock=True): assert lock_wait is not None - self.store.open() + try: + self.store.open() + except StoreBackendDoesNotExist: + raise self.DoesNotExist(str(self._location)) from None if lock: self.lock = Lock(self.store, exclusive, timeout=lock_wait).acquire() else: From b82ced274f009c7029a4a9b2f174b8e6b32939a9 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 5 Sep 2024 23:15:02 +0200 Subject: [PATCH 69/79] refactor: move archives related code from Manifest to Archives class --- src/borg/manifest.py | 117 ++++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 51 deletions(-) diff --git a/src/borg/manifest.py b/src/borg/manifest.py index 61011f47f..505ec537a 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -75,9 +75,71 @@ class Archives(abc.MutableMapping): str timestamps or datetime timestamps. """ - def __init__(self): + def __init__(self, repository): + from .repository import Repository + from .remote import RemoteRepository + + self.repository = repository + self.legacy = not isinstance(repository, (Repository, RemoteRepository)) # key: str archive name, value: dict('id': bytes_id, 'time': str_iso_ts) self._archives = {} + self.manifest = None + + def prepare(self, manifest, m): + self.manifest = manifest + if not self.legacy: + self.load() + else: + self.set_raw_dict(m.archives) + + def finish(self, manifest): + self.manifest = manifest # note: .prepare is not always called + if not self.legacy: + self.save() + manifest_archives = {} + else: + manifest_archives = StableDict(self.get_raw_dict()) + return manifest_archives + + def load(self): + # load archives list from store + from .helpers import msgpack + + archives = {} + try: + infos = list(self.repository.store_list("archives")) + except ObjectNotFound: + infos = [] + for info in infos: + info = ItemInfo(*info) # RPC does not give us a NamedTuple + value = self.repository.store_load(f"archives/{info.name}") + _, value = self.manifest.repo_objs.parse(hex_to_bin(info.name), value, ro_type=ROBJ_MANIFEST) + archive = msgpack.unpackb(value) + archives[archive["name"]] = dict(id=archive["id"], time=archive["time"]) + self.set_raw_dict(archives) + + def save(self): + # save archives list to store + valid_keys = set() + for name, info in self.get_raw_dict().items(): + archive = dict(name=name, id=info["id"], time=info["time"]) + value = self.manifest.key.pack_metadata(archive) # + id = self.manifest.repo_objs.id_hash(value) + key = bin_to_hex(id) + value = self.manifest.repo_objs.format(id, {}, value, ro_type=ROBJ_MANIFEST) + self.repository.store_store(f"archives/{key}", value) + valid_keys.add(key) + # now, delete all other keys in archives/ which are not in valid keys / in the manifest anymore. + # TODO: this is a dirty hack to simulate the old manifest behaviour closely, but also means + # keeping its problems, like read-modify-write behaviour requiring an exclusive lock. + try: + infos = list(self.repository.store_list("archives")) + except ObjectNotFound: + infos = [] + for info in infos: + info = ItemInfo(*info) # RPC does not give us a NamedTuple + if info.name not in valid_keys: + self.repository.store_delete(f"archives/{info.name}") def __len__(self): return len(self._archives) @@ -222,7 +284,7 @@ class Operation(enum.Enum): MANIFEST_ID = b"\0" * 32 def __init__(self, key, repository, item_keys=None, ro_cls=RepoObj): - self.archives = Archives() + self.archives = Archives(repository) self.config = {} self.key = key self.repo_objs = ro_cls(key) @@ -242,8 +304,6 @@ def last_timestamp(self): def load(cls, repository, operations, key=None, *, ro_cls=RepoObj): from .item import ManifestItem from .crypto.key import key_factory - from .remote import RemoteRepository - from .repository import Repository cdata = repository.get_manifest() if not key: @@ -255,25 +315,7 @@ def load(cls, repository, operations, key=None, *, ro_cls=RepoObj): manifest.id = manifest.repo_objs.id_hash(data) if m.get("version") not in (1, 2): raise ValueError("Invalid manifest version") - - if isinstance(repository, (Repository, RemoteRepository)): - from .helpers import msgpack - - archives = {} - try: - infos = list(repository.store_list("archives")) - except ObjectNotFound: - infos = [] - for info in infos: - info = ItemInfo(*info) # RPC does not give us a NamedTuple - value = repository.store_load(f"archives/{info.name}") - _, value = manifest.repo_objs.parse(hex_to_bin(info.name), value, ro_type=ROBJ_MANIFEST) - archive = msgpack.unpackb(value) - archives[archive["name"]] = dict(id=archive["id"], time=archive["time"]) - manifest.archives.set_raw_dict(archives) - else: - manifest.archives.set_raw_dict(m.archives) - + manifest.archives.prepare(manifest, m) manifest.timestamp = m.get("timestamp") manifest.config = m.config # valid item keys are whatever is known in the repo or every key we know @@ -310,8 +352,6 @@ def get_all_mandatory_features(self): def write(self): from .item import ManifestItem - from .remote import RemoteRepository - from .repository import Repository # self.timestamp needs to be strictly monotonically increasing. Clocks often are not set correctly if self.timestamp is None: @@ -326,32 +366,7 @@ def write(self): assert all(len(name) <= 255 for name in self.archives) assert len(self.item_keys) <= 100 self.config["item_keys"] = tuple(sorted(self.item_keys)) - - if isinstance(self.repository, (Repository, RemoteRepository)): - valid_keys = set() - for name, info in self.archives.get_raw_dict().items(): - archive = dict(name=name, id=info["id"], time=info["time"]) - value = self.key.pack_metadata(archive) - id = self.repo_objs.id_hash(value) - key = bin_to_hex(id) - value = self.repo_objs.format(id, {}, value, ro_type=ROBJ_MANIFEST) - self.repository.store_store(f"archives/{key}", value) - valid_keys.add(key) - # now, delete all other keys in archives/ which are not in valid keys / in the manifest anymore. - # TODO: this is a dirty hack to simulate the old manifest behaviour closely, but also means - # keeping its problems, like read-modify-write behaviour requiring an exclusive lock. - try: - infos = list(self.repository.store_list("archives")) - except ObjectNotFound: - infos = [] - for info in infos: - info = ItemInfo(*info) # RPC does not give us a NamedTuple - if info.name not in valid_keys: - self.repository.store_delete(f"archives/{info.name}") - manifest_archives = {} - else: - manifest_archives = StableDict(self.archives.get_raw_dict()) - + manifest_archives = self.archives.finish(self) manifest = ManifestItem( version=2, archives=manifest_archives, timestamp=self.timestamp, config=StableDict(self.config) ) From b56c81bf62f3e4ed918123f2c17b333d566181ac Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 5 Sep 2024 23:57:53 +0200 Subject: [PATCH 70/79] manifest.archives: refactor api Archives was built with a dictionary-like api, but in future we want to go away from a read-modify-write archives list. --- src/borg/archive.py | 26 ++++----- src/borg/archiver/debug_cmd.py | 2 +- src/borg/archiver/delete_cmd.py | 2 +- src/borg/archiver/rdelete_cmd.py | 2 +- src/borg/archiver/transfer_cmd.py | 2 +- src/borg/manifest.py | 71 ++++++++++++++--------- src/borg/testsuite/archiver/create_cmd.py | 2 +- src/borg/testsuite/archiver/rename_cmd.py | 6 +- 8 files changed, 63 insertions(+), 50 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 2012060a7..eeec97d0d 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -492,7 +492,7 @@ def __init__( self.create = create if self.create: self.items_buffer = CacheChunkBuffer(self.cache, self.key, self.stats) - if name in manifest.archives: + if manifest.archives.exists(name): raise self.AlreadyExists(name) else: info = self.manifest.archives.get(name) @@ -610,7 +610,7 @@ def add_item(self, item, show_progress=True, stats=None): def save(self, name=None, comment=None, timestamp=None, stats=None, additional_metadata=None): name = name or self.name - if name in self.manifest.archives: + if self.manifest.archives.exists(name): raise self.AlreadyExists(name) self.items_buffer.flush(flush=True) item_ptrs = archive_put_items( @@ -657,7 +657,7 @@ def save(self, name=None, comment=None, timestamp=None, stats=None, additional_m raise while self.repository.async_response(wait=True) is not None: pass - self.manifest.archives[name] = (self.id, metadata.time) + self.manifest.archives.create(name, self.id, metadata.time) self.manifest.write() return metadata @@ -951,22 +951,22 @@ def set_meta(self, key, value): data = self.key.pack_metadata(metadata.as_dict()) new_id = self.key.id_hash(data) self.cache.add_chunk(new_id, {}, data, stats=self.stats, ro_type=ROBJ_ARCHIVE_META) - self.manifest.archives[self.name] = (new_id, metadata.time) + self.manifest.archives.create(self.name, new_id, metadata.time, overwrite=True) self.id = new_id def rename(self, name): - if name in self.manifest.archives: + if self.manifest.archives.exists(name): raise self.AlreadyExists(name) oldname = self.name self.name = name self.set_meta("name", name) - del self.manifest.archives[oldname] + self.manifest.archives.delete(oldname) def delete(self): # quick and dirty: we just nuke the archive from the archives list - that will # potentially orphan all chunks previously referenced by the archive, except the ones also # referenced by other archives. In the end, "borg compact" will clean up and free space. - del self.manifest.archives[self.name] + self.manifest.archives.delete(self.name) @staticmethod def compare_archives_iter( @@ -1798,16 +1798,16 @@ def valid_archive(obj): archive = ArchiveItem(internal_dict=archive) name = archive.name logger.info("Found archive %s", name) - if name in manifest.archives: + if manifest.archives.exists(name): i = 1 while True: new_name = "%s.%d" % (name, i) - if new_name not in manifest.archives: + if not manifest.archives.exists(new_name): break i += 1 logger.warning("Duplicate archive name %s, storing as %s", name, new_name) name = new_name - manifest.archives[name] = (chunk_id, archive.time) + manifest.archives.create(name, chunk_id, archive.time) pi.finish() logger.info("Manifest rebuild complete.") return manifest @@ -2025,7 +2025,7 @@ def valid_item(obj): if archive_id not in self.chunks: logger.error("Archive metadata block %s is missing!", bin_to_hex(archive_id)) self.error_found = True - del self.manifest.archives[info.name] + self.manifest.archives.delete(info.name) continue cdata = self.repository.get(archive_id) try: @@ -2033,7 +2033,7 @@ def valid_item(obj): except IntegrityError as integrity_error: logger.error("Archive metadata block %s is corrupted: %s", bin_to_hex(archive_id), integrity_error) self.error_found = True - del self.manifest.archives[info.name] + self.manifest.archives.delete(info.name) continue archive = self.key.unpack_archive(data) archive = ArchiveItem(internal_dict=archive) @@ -2053,7 +2053,7 @@ def valid_item(obj): new_archive_id = self.key.id_hash(data) cdata = self.repo_objs.format(new_archive_id, {}, data, ro_type=ROBJ_ARCHIVE_META) add_reference(new_archive_id, len(data), cdata) - self.manifest.archives[info.name] = (new_archive_id, info.ts) + self.manifest.archives.create(info.name, new_archive_id, info.ts, overwrite=True) pi.finish() def finish(self): diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index ddaeb8b8e..07ceceeb2 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -46,7 +46,7 @@ def do_debug_dump_archive(self, args, repository, manifest): """dump decoded archive metadata (not: data)""" repo_objs = manifest.repo_objs try: - archive_meta_orig = manifest.archives.get_raw_dict()[args.name] + archive_meta_orig = manifest.archives.get(args.name, raw=True) except KeyError: raise Archive.DoesNotExist(args.name) diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index 127e7184c..6712baa59 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -33,7 +33,7 @@ def do_delete(self, args, repository): try: # this does NOT use Archive.delete, so this code hopefully even works in cases a corrupt archive # would make the code in class Archive crash, so the user can at least get rid of such archives. - current_archive = manifest.archives.pop(archive_name) + current_archive = manifest.archives.delete(archive_name) except KeyError: self.print_warning(f"Archive {archive_name} not found ({i}/{len(archive_names)}).") else: diff --git a/src/borg/archiver/rdelete_cmd.py b/src/borg/archiver/rdelete_cmd.py index 30bb66d96..e1cfc43e9 100644 --- a/src/borg/archiver/rdelete_cmd.py +++ b/src/borg/archiver/rdelete_cmd.py @@ -29,7 +29,7 @@ def do_rdelete(self, args, repository): msg = [] try: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - n_archives = len(manifest.archives) + n_archives = manifest.archives.count() msg.append( f"You requested to DELETE the following repository completely " f"*including* {n_archives} archives it contains:" diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index b9e962869..d5842c73b 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -78,7 +78,7 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non for name in archive_names: transfer_size = 0 present_size = 0 - if name in manifest.archives and not dry_run: + if manifest.archives.exists(name) and not dry_run: print(f"{name}: archive is already present in destination repo, skipping.") else: if not dry_run: diff --git a/src/borg/manifest.py b/src/borg/manifest.py index 505ec537a..0d53ee083 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -1,6 +1,6 @@ import enum import re -from collections import abc, namedtuple +from collections import namedtuple from datetime import datetime, timedelta, timezone from operator import attrgetter from collections.abc import Sequence @@ -68,11 +68,13 @@ def get_first_and_last_archive_ts(archives_list): return archives -class Archives(abc.MutableMapping): +class Archives: """ - Nice wrapper around the archives dict, making sure only valid types/values get in - and we can deal with str keys (and it internally encodes to byte keys) and either - str timestamps or datetime timestamps. + Manage the list of archives. + + We still need to support the borg 1.x manifest-with-list-of-archives, + so borg transfer can work. + borg2 has separate items archives/* in the borgstore. """ def __init__(self, repository): @@ -88,20 +90,20 @@ def __init__(self, repository): def prepare(self, manifest, m): self.manifest = manifest if not self.legacy: - self.load() + self._load() else: - self.set_raw_dict(m.archives) + self._set_raw_dict(m.archives) def finish(self, manifest): self.manifest = manifest # note: .prepare is not always called if not self.legacy: - self.save() + self._save() manifest_archives = {} else: - manifest_archives = StableDict(self.get_raw_dict()) + manifest_archives = StableDict(self._get_raw_dict()) return manifest_archives - def load(self): + def _load(self): # load archives list from store from .helpers import msgpack @@ -116,12 +118,12 @@ def load(self): _, value = self.manifest.repo_objs.parse(hex_to_bin(info.name), value, ro_type=ROBJ_MANIFEST) archive = msgpack.unpackb(value) archives[archive["name"]] = dict(id=archive["id"], time=archive["time"]) - self.set_raw_dict(archives) + self._set_raw_dict(archives) - def save(self): + def _save(self): # save archives list to store valid_keys = set() - for name, info in self.get_raw_dict().items(): + for name, info in self._get_raw_dict().items(): archive = dict(name=name, id=info["id"], time=info["time"]) value = self.manifest.key.pack_metadata(archive) # id = self.manifest.repo_objs.id_hash(value) @@ -141,33 +143,44 @@ def save(self): if info.name not in valid_keys: self.repository.store_delete(f"archives/{info.name}") - def __len__(self): + def count(self): + # return the count of archives in the repo return len(self._archives) - def __iter__(self): - return iter(self._archives) + def exists(self, name): + # check if an archive with this name exists + assert isinstance(name, str) + return name in self._archives - def __getitem__(self, name): + def names(self): + # yield the names of all archives + yield from self._archives + + def get(self, name, raw=False): assert isinstance(name, str) values = self._archives.get(name) if values is None: raise KeyError - ts = parse_timestamp(values["time"]) - return ArchiveInfo(name=name, id=values["id"], ts=ts) + if not raw: + ts = parse_timestamp(values["time"]) + return ArchiveInfo(name=name, id=values["id"], ts=ts) + else: + return dict(name=name, id=values["id"], time=values["time"]) - def __setitem__(self, name, info): + def create(self, name, id, ts, *, overwrite=False): assert isinstance(name, str) - assert isinstance(info, tuple) - id, ts = info assert isinstance(id, bytes) if isinstance(ts, datetime): ts = ts.isoformat(timespec="microseconds") assert isinstance(ts, str) + if name in self._archives and not overwrite: + raise KeyError("archive already exists") self._archives[name] = {"id": id, "time": ts} - def __delitem__(self, name): + def delete(self, name): + # delete an archive assert isinstance(name, str) - del self._archives[name] + self._archives.pop(name) def list( self, @@ -203,7 +216,7 @@ def list( if isinstance(sort_by, (str, bytes)): raise TypeError("sort_by must be a sequence of str") - archives = self.values() + archives = [self.get(name) for name in self.names()] regex = get_regex_from_pattern(match or "re:.*") regex = re.compile(regex + match_end) archives = [x for x in archives if regex.match(x.name) is not None] @@ -240,14 +253,14 @@ def list_considering(self, args): newest=getattr(args, "newest", None), ) - def set_raw_dict(self, d): + def _set_raw_dict(self, d): """set the dict we get from the msgpack unpacker""" for k, v in d.items(): assert isinstance(k, str) assert isinstance(v, dict) and "id" in v and "time" in v self._archives[k] = v - def get_raw_dict(self): + def _get_raw_dict(self): """get the dict we can give to the msgpack packer""" return self._archives @@ -362,8 +375,8 @@ def write(self): max_ts = max(incremented_ts, now_ts) self.timestamp = max_ts.isoformat(timespec="microseconds") # include checks for limits as enforced by limited unpacker (used by load()) - assert len(self.archives) <= MAX_ARCHIVES - assert all(len(name) <= 255 for name in self.archives) + assert self.archives.count() <= MAX_ARCHIVES + assert all(len(name) <= 255 for name in self.archives.names()) assert len(self.item_keys) <= 100 self.config["item_keys"] = tuple(sorted(self.item_keys)) manifest_archives = self.archives.finish(self) diff --git a/src/borg/testsuite/archiver/create_cmd.py b/src/borg/testsuite/archiver/create_cmd.py index 8bc546fc3..595f4ced0 100644 --- a/src/borg/testsuite/archiver/create_cmd.py +++ b/src/borg/testsuite/archiver/create_cmd.py @@ -646,7 +646,7 @@ def test_create_dry_run(archivers, request): # Make sure no archive has been created with Repository(archiver.repository_path) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - assert len(manifest.archives) == 0 + assert manifest.archives.count() == 0 def test_progress_on(archivers, request): diff --git a/src/borg/testsuite/archiver/rename_cmd.py b/src/borg/testsuite/archiver/rename_cmd.py index 5a1b65c0a..fa5d3d265 100644 --- a/src/borg/testsuite/archiver/rename_cmd.py +++ b/src/borg/testsuite/archiver/rename_cmd.py @@ -23,6 +23,6 @@ def test_rename(archivers, request): # Make sure both archives have been renamed with Repository(archiver.repository_path) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - assert len(manifest.archives) == 2 - assert "test.3" in manifest.archives - assert "test.4" in manifest.archives + assert manifest.archives.count() == 2 + assert manifest.archives.exists("test.3") + assert manifest.archives.exists("test.4") From ef7dd76da12c1b109fb6021dd9010595c9d23dc5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 6 Sep 2024 12:34:59 +0200 Subject: [PATCH 71/79] manifest: no read-modify-write for borgstore archives list previously, borg always read all archives entries, modified the list in memory, wrote back to the repository (similar as borg 1.x did). now borg works directly with archives/* in the borgstore. --- src/borg/manifest.py | 155 +++++++++++++--------- src/borg/testsuite/archiver/create_cmd.py | 2 +- src/borg/testsuite/archiver/rename_cmd.py | 6 +- 3 files changed, 98 insertions(+), 65 deletions(-) diff --git a/src/borg/manifest.py b/src/borg/manifest.py index 0d53ee083..ceed2c334 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -77,7 +77,7 @@ class Archives: borg2 has separate items archives/* in the borgstore. """ - def __init__(self, repository): + def __init__(self, repository, manifest): from .repository import Repository from .remote import RemoteRepository @@ -85,87 +85,98 @@ def __init__(self, repository): self.legacy = not isinstance(repository, (Repository, RemoteRepository)) # key: str archive name, value: dict('id': bytes_id, 'time': str_iso_ts) self._archives = {} - self.manifest = None + self.manifest = manifest def prepare(self, manifest, m): - self.manifest = manifest if not self.legacy: - self._load() + pass else: self._set_raw_dict(m.archives) def finish(self, manifest): - self.manifest = manifest # note: .prepare is not always called if not self.legacy: - self._save() manifest_archives = {} else: manifest_archives = StableDict(self._get_raw_dict()) return manifest_archives - def _load(self): - # load archives list from store - from .helpers import msgpack - - archives = {} - try: - infos = list(self.repository.store_list("archives")) - except ObjectNotFound: - infos = [] - for info in infos: - info = ItemInfo(*info) # RPC does not give us a NamedTuple - value = self.repository.store_load(f"archives/{info.name}") - _, value = self.manifest.repo_objs.parse(hex_to_bin(info.name), value, ro_type=ROBJ_MANIFEST) - archive = msgpack.unpackb(value) - archives[archive["name"]] = dict(id=archive["id"], time=archive["time"]) - self._set_raw_dict(archives) - - def _save(self): - # save archives list to store - valid_keys = set() - for name, info in self._get_raw_dict().items(): - archive = dict(name=name, id=info["id"], time=info["time"]) - value = self.manifest.key.pack_metadata(archive) # - id = self.manifest.repo_objs.id_hash(value) - key = bin_to_hex(id) - value = self.manifest.repo_objs.format(id, {}, value, ro_type=ROBJ_MANIFEST) - self.repository.store_store(f"archives/{key}", value) - valid_keys.add(key) - # now, delete all other keys in archives/ which are not in valid keys / in the manifest anymore. - # TODO: this is a dirty hack to simulate the old manifest behaviour closely, but also means - # keeping its problems, like read-modify-write behaviour requiring an exclusive lock. - try: - infos = list(self.repository.store_list("archives")) - except ObjectNotFound: - infos = [] - for info in infos: - info = ItemInfo(*info) # RPC does not give us a NamedTuple - if info.name not in valid_keys: - self.repository.store_delete(f"archives/{info.name}") - def count(self): # return the count of archives in the repo - return len(self._archives) + if not self.legacy: + try: + infos = list(self.repository.store_list("archives")) + except ObjectNotFound: + infos = [] + return len(infos) # we do not check here if entries are valid + else: + return len(self._archives) def exists(self, name): # check if an archive with this name exists assert isinstance(name, str) - return name in self._archives + if not self.legacy: + return name in self.names() + else: + return name in self._archives + + def _infos(self): + # yield the infos of all archives: (store_key, archive_info) + from .helpers import msgpack + + if not self.legacy: + try: + infos = list(self.repository.store_list("archives")) + except ObjectNotFound: + infos = [] + for info in infos: + info = ItemInfo(*info) # RPC does not give us a NamedTuple + value = self.repository.store_load(f"archives/{info.name}") + _, value = self.manifest.repo_objs.parse(hex_to_bin(info.name), value, ro_type=ROBJ_MANIFEST) + archive_info = msgpack.unpackb(value) + yield info.name, archive_info + else: + for name in self._archives: + archive_info = dict(name=name, id=self._archives[name]["id"], time=self._archives[name]["time"]) + yield None, archive_info + + def _lookup_name(self, name, raw=False): + assert isinstance(name, str) + assert not self.legacy + for store_key, archive_info in self._infos(): + if archive_info["name"] == name: + if not raw: + ts = parse_timestamp(archive_info["time"]) + return store_key, ArchiveInfo(name=name, id=archive_info["id"], ts=ts) + else: + return store_key, archive_info + else: + raise KeyError(name) def names(self): # yield the names of all archives - yield from self._archives + if not self.legacy: + for _, archive_info in self._infos(): + yield archive_info["name"] + else: + yield from self._archives def get(self, name, raw=False): assert isinstance(name, str) - values = self._archives.get(name) - if values is None: - raise KeyError - if not raw: - ts = parse_timestamp(values["time"]) - return ArchiveInfo(name=name, id=values["id"], ts=ts) + if not self.legacy: + try: + store_key, archive_info = self._lookup_name(name, raw=raw) + return archive_info + except KeyError: + return None else: - return dict(name=name, id=values["id"], time=values["time"]) + values = self._archives.get(name) + if values is None: + return None + if not raw: + ts = parse_timestamp(values["time"]) + return ArchiveInfo(name=name, id=values["id"], ts=ts) + else: + return dict(name=name, id=values["id"], time=values["time"]) def create(self, name, id, ts, *, overwrite=False): assert isinstance(name, str) @@ -173,14 +184,36 @@ def create(self, name, id, ts, *, overwrite=False): if isinstance(ts, datetime): ts = ts.isoformat(timespec="microseconds") assert isinstance(ts, str) - if name in self._archives and not overwrite: - raise KeyError("archive already exists") - self._archives[name] = {"id": id, "time": ts} + if not self.legacy: + try: + store_key, _ = self._lookup_name(name) + except KeyError: + pass + else: + # looks like we already have an archive list entry with that name + if not overwrite: + raise KeyError("archive already exists") + else: + self.repository.store_delete(f"archives/{store_key}") + archive = dict(name=name, id=id, time=ts) + value = self.manifest.key.pack_metadata(archive) + id = self.manifest.repo_objs.id_hash(value) + key = bin_to_hex(id) + value = self.manifest.repo_objs.format(id, {}, value, ro_type=ROBJ_MANIFEST) + self.repository.store_store(f"archives/{key}", value) + else: + if self.exists(name) and not overwrite: + raise KeyError("archive already exists") + self._archives[name] = {"id": id, "time": ts} def delete(self, name): # delete an archive assert isinstance(name, str) - self._archives.pop(name) + if not self.legacy: + store_key, archive_info = self._lookup_name(name) + self.repository.store_delete(f"archives/{store_key}") + else: + self._archives.pop(name) def list( self, @@ -297,7 +330,7 @@ class Operation(enum.Enum): MANIFEST_ID = b"\0" * 32 def __init__(self, key, repository, item_keys=None, ro_cls=RepoObj): - self.archives = Archives(repository) + self.archives = Archives(repository, self) self.config = {} self.key = key self.repo_objs = ro_cls(key) diff --git a/src/borg/testsuite/archiver/create_cmd.py b/src/borg/testsuite/archiver/create_cmd.py index 595f4ced0..342d97c60 100644 --- a/src/borg/testsuite/archiver/create_cmd.py +++ b/src/borg/testsuite/archiver/create_cmd.py @@ -646,7 +646,7 @@ def test_create_dry_run(archivers, request): # Make sure no archive has been created with Repository(archiver.repository_path) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - assert manifest.archives.count() == 0 + assert manifest.archives.count() == 0 def test_progress_on(archivers, request): diff --git a/src/borg/testsuite/archiver/rename_cmd.py b/src/borg/testsuite/archiver/rename_cmd.py index fa5d3d265..40ede1a60 100644 --- a/src/borg/testsuite/archiver/rename_cmd.py +++ b/src/borg/testsuite/archiver/rename_cmd.py @@ -23,6 +23,6 @@ def test_rename(archivers, request): # Make sure both archives have been renamed with Repository(archiver.repository_path) as repository: manifest = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) - assert manifest.archives.count() == 2 - assert manifest.archives.exists("test.3") - assert manifest.archives.exists("test.4") + assert manifest.archives.count() == 2 + assert manifest.archives.exists("test.3") + assert manifest.archives.exists("test.4") From 84121685557deb50e1915859488e475edd80f632 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 6 Sep 2024 22:17:46 +0200 Subject: [PATCH 72/79] check: only write to repo if --repair is given old borg just didn't commit the transaction and thus caused a transaction rollback if not in repair mode. we can't do that anymore, thus we must avoid modifying the repo if not in repair mode. --- src/borg/archive.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index eeec97d0d..f9604f849 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -2025,7 +2025,11 @@ def valid_item(obj): if archive_id not in self.chunks: logger.error("Archive metadata block %s is missing!", bin_to_hex(archive_id)) self.error_found = True - self.manifest.archives.delete(info.name) + if self.repair: + logger.error(f"Deleting broken archive {info.name}.") + self.manifest.archives.delete(info.name) + else: + logger.error(f"Would delete broken archive {info.name}.") continue cdata = self.repository.get(archive_id) try: @@ -2033,7 +2037,11 @@ def valid_item(obj): except IntegrityError as integrity_error: logger.error("Archive metadata block %s is corrupted: %s", bin_to_hex(archive_id), integrity_error) self.error_found = True - self.manifest.archives.delete(info.name) + if self.repair: + logger.error(f"Deleting broken archive {info.name}.") + self.manifest.archives.delete(info.name) + else: + logger.error(f"Would delete broken archive {info.name}.") continue archive = self.key.unpack_archive(data) archive = ArchiveItem(internal_dict=archive) @@ -2046,14 +2054,17 @@ def valid_item(obj): verify_file_chunks(info.name, item) items_buffer.add(item) items_buffer.flush(flush=True) - archive.item_ptrs = archive_put_items( - items_buffer.chunks, repo_objs=self.repo_objs, add_reference=add_reference - ) - data = self.key.pack_metadata(archive.as_dict()) - new_archive_id = self.key.id_hash(data) - cdata = self.repo_objs.format(new_archive_id, {}, data, ro_type=ROBJ_ARCHIVE_META) - add_reference(new_archive_id, len(data), cdata) - self.manifest.archives.create(info.name, new_archive_id, info.ts, overwrite=True) + if self.repair: + archive.item_ptrs = archive_put_items( + items_buffer.chunks, repo_objs=self.repo_objs, add_reference=add_reference + ) + data = self.key.pack_metadata(archive.as_dict()) + new_archive_id = self.key.id_hash(data) + logger.debug(f"archive id old: {bin_to_hex(archive_id)}") + logger.debug(f"archive id new: {bin_to_hex(new_archive_id)}") + cdata = self.repo_objs.format(new_archive_id, {}, data, ro_type=ROBJ_ARCHIVE_META) + add_reference(new_archive_id, len(data), cdata) + self.manifest.archives.create(info.name, new_archive_id, info.ts, overwrite=True) pi.finish() def finish(self): From 0e183b225d04e0a42f20347c9bee705a50345b68 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 6 Sep 2024 23:50:02 +0200 Subject: [PATCH 73/79] shared locking for many borg commands not for check and compact, these need an exclusive lock. to try parallel repo access on same machine, same user, one needs to use a non-locking cache implementation: export BORG_CACHE_IMPL=adhoc this is slow due the missing files cache in that implementation, but unproblematic because no caches/indexes are persisted. --- src/borg/archiver/create_cmd.py | 2 +- src/borg/archiver/debug_cmd.py | 2 +- src/borg/archiver/delete_cmd.py | 2 +- src/borg/archiver/key_cmds.py | 4 ++-- src/borg/archiver/prune_cmd.py | 2 +- src/borg/archiver/rcompress_cmd.py | 2 +- src/borg/archiver/recreate_cmd.py | 2 +- src/borg/archiver/rename_cmd.py | 2 +- src/borg/archiver/tar_cmds.py | 2 +- src/borg/archiver/transfer_cmd.py | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 1d8bb0600..166e302be 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -41,7 +41,7 @@ class CreateMixIn: - @with_repository(exclusive=True, compatibility=(Manifest.Operation.WRITE,)) + @with_repository(compatibility=(Manifest.Operation.WRITE,)) def do_create(self, args, repository, manifest): """Create new archive""" key = manifest.key diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index 07ceceeb2..6f747ec88 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -281,7 +281,7 @@ def do_debug_format_obj(self, args, repository, manifest): with open(args.object_path, "wb") as f: f.write(data_encrypted) - @with_repository(manifest=False, exclusive=True) + @with_repository(manifest=False) def do_debug_put_obj(self, args, repository): """put file contents into the repository""" with open(args.path, "rb") as f: diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index 6712baa59..1e4b1f17b 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -12,7 +12,7 @@ class DeleteMixIn: - @with_repository(exclusive=True, manifest=False) + @with_repository(manifest=False) def do_delete(self, args, repository): """Delete archives""" self.output_list = args.output_list diff --git a/src/borg/archiver/key_cmds.py b/src/borg/archiver/key_cmds.py index 6fbac5f09..50e0315b4 100644 --- a/src/borg/archiver/key_cmds.py +++ b/src/borg/archiver/key_cmds.py @@ -83,7 +83,7 @@ def do_change_location(self, args, repository, manifest, cache): key.remove(key.target) # remove key from current location logger.info(f"Key moved to {loc}") - @with_repository(lock=False, exclusive=False, manifest=False, cache=False) + @with_repository(lock=False, manifest=False, cache=False) def do_key_export(self, args, repository): """Export the repository key for backup""" manager = KeyManager(repository) @@ -102,7 +102,7 @@ def do_key_export(self, args, repository): except IsADirectoryError: raise CommandError(f"'{args.path}' must be a file, not a directory") - @with_repository(lock=False, exclusive=False, manifest=False, cache=False) + @with_repository(lock=False, manifest=False, cache=False) def do_key_import(self, args, repository): """Import the repository key from backup""" manager = KeyManager(repository) diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index a33901770..c9cabbf31 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -70,7 +70,7 @@ def prune_split(archives, rule, n, kept_because=None): class PruneMixIn: - @with_repository(exclusive=True, compatibility=(Manifest.Operation.DELETE,)) + @with_repository(compatibility=(Manifest.Operation.DELETE,)) def do_prune(self, args, repository, manifest): """Prune repository archives according to specified rules""" if not any( diff --git a/src/borg/archiver/rcompress_cmd.py b/src/borg/archiver/rcompress_cmd.py index 58c58931c..c9bfecfe0 100644 --- a/src/borg/archiver/rcompress_cmd.py +++ b/src/borg/archiver/rcompress_cmd.py @@ -88,7 +88,7 @@ def format_compression_spec(ctype, clevel, olevel): class RCompressMixIn: - @with_repository(cache=False, manifest=True, exclusive=True, compatibility=(Manifest.Operation.CHECK,)) + @with_repository(cache=False, manifest=True, compatibility=(Manifest.Operation.CHECK,)) def do_rcompress(self, args, repository, manifest): """Repository (re-)compression""" diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index 7d30b41d3..68116edab 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -15,7 +15,7 @@ class RecreateMixIn: - @with_repository(cache=True, exclusive=True, compatibility=(Manifest.Operation.CHECK,)) + @with_repository(cache=True, compatibility=(Manifest.Operation.CHECK,)) def do_recreate(self, args, repository, manifest, cache): """Re-create archives""" matcher = build_matcher(args.patterns, args.paths) diff --git a/src/borg/archiver/rename_cmd.py b/src/borg/archiver/rename_cmd.py index cb8660c48..ba94a0691 100644 --- a/src/borg/archiver/rename_cmd.py +++ b/src/borg/archiver/rename_cmd.py @@ -11,7 +11,7 @@ class RenameMixIn: - @with_repository(exclusive=True, cache=True, compatibility=(Manifest.Operation.CHECK,)) + @with_repository(cache=True, compatibility=(Manifest.Operation.CHECK,)) @with_archive def do_rename(self, args, repository, manifest, cache, archive): """Rename an existing archive""" diff --git a/src/borg/archiver/tar_cmds.py b/src/borg/archiver/tar_cmds.py index a01a21688..6ede4b348 100644 --- a/src/borg/archiver/tar_cmds.py +++ b/src/borg/archiver/tar_cmds.py @@ -240,7 +240,7 @@ def item_to_paxheaders(format, item): for pattern in matcher.get_unmatched_include_patterns(): self.print_warning_instance(IncludePatternNeverMatchedWarning(pattern)) - @with_repository(cache=True, exclusive=True, compatibility=(Manifest.Operation.WRITE,)) + @with_repository(cache=True, compatibility=(Manifest.Operation.WRITE,)) def do_import_tar(self, args, repository, manifest, cache): """Create a backup archive from a tarball""" self.output_filter = args.output_filter diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index d5842c73b..ac407f653 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -17,7 +17,7 @@ class TransferMixIn: @with_other_repository(manifest=True, compatibility=(Manifest.Operation.READ,)) - @with_repository(exclusive=True, manifest=True, cache=True, compatibility=(Manifest.Operation.WRITE,)) + @with_repository(manifest=True, cache=True, compatibility=(Manifest.Operation.WRITE,)) def do_transfer(self, args, *, repository, manifest, cache, other_repository=None, other_manifest=None): """archives transfer from other repository, optionally upgrade data format""" key = manifest.key From a509a0c463e3d2f93af281f212199105376edae3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 6 Sep 2024 23:50:42 +0200 Subject: [PATCH 74/79] locking: no traceback on lock timeout (expected) --- src/borg/storelocking.py | 3 +-- src/borg/testsuite/archiver/lock_cmds.py | 2 +- src/borg/testsuite/storelocking.py | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/borg/storelocking.py b/src/borg/storelocking.py index 8f1979eb3..dc111f9c1 100644 --- a/src/borg/storelocking.py +++ b/src/borg/storelocking.py @@ -186,8 +186,7 @@ def acquire(self): self._delete_lock(key, ignore_not_found=True) # wait a random bit before retrying time.sleep(self.retry_delay_min + (self.retry_delay_max - self.retry_delay_min) * random.random()) - # timeout - raise LockFailed(str(self.store), "timeout") + raise LockTimeout(str(self.store)) def release(self): locks = self._find_locks(only_mine=True) diff --git a/src/borg/testsuite/archiver/lock_cmds.py b/src/borg/testsuite/archiver/lock_cmds.py index 8fce39bd9..9b7857f6b 100644 --- a/src/borg/testsuite/archiver/lock_cmds.py +++ b/src/borg/testsuite/archiver/lock_cmds.py @@ -37,7 +37,7 @@ def test_with_lock(tmp_path): out, err_out = p2.communicate() assert "second command" not in out # command2 is "locked out" assert "Failed to create/acquire the lock" in err_out - assert p2.returncode == 72 # LockTimeout: could not acquire the lock, p1 already has it + assert p2.returncode == 73 # LockTimeout: could not acquire the lock, p1 already has it out, err_out = p1.communicate() assert "first command" in out # command1 was executed and had the lock assert not err_out diff --git a/src/borg/testsuite/storelocking.py b/src/borg/testsuite/storelocking.py index b4586a6bf..4fbf0be34 100644 --- a/src/borg/testsuite/storelocking.py +++ b/src/borg/testsuite/storelocking.py @@ -4,7 +4,7 @@ from borgstore.store import Store -from ..storelocking import Lock, LockFailed, NotLocked +from ..storelocking import Lock, NotLocked, LockTimeout ID1 = "foo", 1, 1 ID2 = "bar", 2, 2 @@ -37,11 +37,11 @@ def test_got_exclusive_lock(self, lockstore): def test_exclusive_lock(self, lockstore): # there must not be 2 exclusive locks with Lock(lockstore, exclusive=True, id=ID1): - with pytest.raises(LockFailed): + with pytest.raises(LockTimeout): Lock(lockstore, exclusive=True, id=ID2).acquire() # acquiring an exclusive lock will time out if the non-exclusive does not go away with Lock(lockstore, exclusive=False, id=ID1): - with pytest.raises(LockFailed): + with pytest.raises(LockTimeout): Lock(lockstore, exclusive=True, id=ID2).acquire() def test_double_nonexclusive_lock_succeeds(self, lockstore): From bc1f90b6412ed44155bbcf101361a2a483ad5040 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 7 Sep 2024 10:50:04 +0200 Subject: [PATCH 75/79] check: do not create addtl. archives dir entries if we already have one if the manifest file is missing, check generated *.1 *.2 ... archives although an entry for the correct name and id was already present. BUG! this is because if the manifest is lost, that does not imply anymore that the complete archives directory is also lost, as it did in borg 1.x. Also improved log messages a bit. --- src/borg/archive.py | 27 ++++++++++++++++++--------- src/borg/manifest.py | 13 +++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index f9604f849..528a658ff 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1757,7 +1757,7 @@ def verify_data(self): ) def rebuild_manifest(self): - """Rebuild the manifest object if it is missing + """Rebuild the manifest object and the archives list. Iterates through all objects in the repository looking for archive metadata blocks. """ @@ -1767,7 +1767,7 @@ def valid_archive(obj): return False return REQUIRED_ARCHIVE_KEYS.issubset(obj) - logger.info("Rebuilding missing manifest, this might take some time...") + logger.info("Rebuilding missing manifest and missing archives directory entries, this might take some time...") # as we have lost the manifest, we do not know any more what valid item keys we had. # collecting any key we encounter in a damaged repo seems unwise, thus we just use # the hardcoded list from the source code. thus, it is not recommended to rebuild a @@ -1775,7 +1775,10 @@ def valid_archive(obj): # within this repository (assuming that newer borg versions support more item keys). manifest = Manifest(self.key, self.repository) pi = ProgressIndicatorPercent( - total=len(self.chunks), msg="Rebuilding manifest %6.2f%%", step=0.01, msgid="check.rebuild_manifest" + total=len(self.chunks), + msg="Rebuilding manifest and archives directory %6.2f%%", + step=0.01, + msgid="check.rebuild_manifest", ) for chunk_id, _ in self.chunks.iteritems(): pi.show() @@ -1797,19 +1800,25 @@ def valid_archive(obj): archive = self.key.unpack_archive(data) archive = ArchiveItem(internal_dict=archive) name = archive.name - logger.info("Found archive %s", name) - if manifest.archives.exists(name): + logger.info(f"Found archive {name}, id {bin_to_hex(chunk_id)}.") + if manifest.archives.exists_name_and_id(name, chunk_id): + logger.info("We already have an archives directory entry for this.") + elif not manifest.archives.exists(name): + # no archives list entry yet and name is not taken yet, create an entry + logger.warning(f"Creating archives directory entry for {name}.") + manifest.archives.create(name, chunk_id, archive.time) + else: + # we don't have an entry yet, but the name is taken by something else i = 1 while True: new_name = "%s.%d" % (name, i) if not manifest.archives.exists(new_name): break i += 1 - logger.warning("Duplicate archive name %s, storing as %s", name, new_name) - name = new_name - manifest.archives.create(name, chunk_id, archive.time) + logger.warning(f"Creating archives directory entry using {new_name}.") + manifest.archives.create(new_name, chunk_id, archive.time) pi.finish() - logger.info("Manifest rebuild complete.") + logger.info("Manifest and archives directory rebuild complete.") return manifest def rebuild_archives( diff --git a/src/borg/manifest.py b/src/borg/manifest.py index ceed2c334..a5eb7b89a 100644 --- a/src/borg/manifest.py +++ b/src/borg/manifest.py @@ -119,6 +119,19 @@ def exists(self, name): else: return name in self._archives + def exists_name_and_id(self, name, id): + # check if an archive with this name AND id exists + assert isinstance(name, str) + assert isinstance(id, bytes) + if not self.legacy: + for _, archive_info in self._infos(): + if archive_info["name"] == name and archive_info["id"] == id: + return True + else: + return False + else: + raise NotImplementedError + def _infos(self): # yield the infos of all archives: (store_key, archive_info) from .helpers import msgpack From 682aedba5030e90b8a78488c89dceda7d7a0e91b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 7 Sep 2024 17:15:04 +0200 Subject: [PATCH 76/79] check --repair --undelete-archives: bring archives back from the dead borg delete and borg prune do a quick and dirty archive deletion, just removing the archives directory entry for them. --undelete-archives can still find the archive metadata objects by completely scanning the repository and re-create missing archives directory entries. but only until borg compact would remove all unused data. if only the manifest is missing or corrupted, do not run that scan, it is not required for the manifest anymore. --- src/borg/archive.py | 52 +++++++++++++++--------- src/borg/archiver/check_cmd.py | 15 +++++++ src/borg/testsuite/archiver/check_cmd.py | 6 ++- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 528a658ff..c8a739911 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1601,6 +1601,7 @@ def check( *, verify_data=False, repair=False, + undelete_archives=False, match=None, sort_by="", first=0, @@ -1613,6 +1614,7 @@ def check( """Perform a set of checks on 'repository' :param repair: enable repair mode, write updated or corrected data into repository + :param undelete_archives: create archive directory entries that are missing :param first/last/sort_by: only check this number of first/last archives ordered by sort_by :param match: only check archives matching this pattern :param older/newer: only check archives older/newer than timedelta from now @@ -1631,18 +1633,24 @@ def check( self.repo_objs = RepoObj(self.key) if verify_data: self.verify_data() + rebuild_manifest = False try: repository.get_manifest() except NoManifestError: + logger.error("Repository manifest is missing.") self.error_found = True - self.manifest = self.rebuild_manifest() + rebuild_manifest = True else: try: self.manifest = Manifest.load(repository, (Manifest.Operation.CHECK,), key=self.key) except IntegrityErrorBase as exc: logger.error("Repository manifest is corrupted: %s", exc) self.error_found = True - self.manifest = self.rebuild_manifest() + rebuild_manifest = True + if rebuild_manifest: + self.manifest = self.rebuild_manifest() + if undelete_archives: + self.rebuild_archives_directory() self.rebuild_archives( match=match, first=first, last=last, sort_by=sort_by, older=older, oldest=oldest, newer=newer, newest=newest ) @@ -1757,9 +1765,22 @@ def verify_data(self): ) def rebuild_manifest(self): - """Rebuild the manifest object and the archives list. + """Rebuild the manifest object.""" + + logger.info("Rebuilding missing/corrupted manifest.") + # as we have lost the manifest, we do not know any more what valid item keys we had. + # collecting any key we encounter in a damaged repo seems unwise, thus we just use + # the hardcoded list from the source code. thus, it is not recommended to rebuild a + # lost manifest on a older borg version than the most recent one that was ever used + # within this repository (assuming that newer borg versions support more item keys). + return Manifest(self.key, self.repository) + + def rebuild_archives_directory(self): + """Rebuild the archives directory, undeleting archives. Iterates through all objects in the repository looking for archive metadata blocks. + When finding some that do not have a corresponding archives directory entry, it will + create that entry (undeleting all archives). """ def valid_archive(obj): @@ -1767,18 +1788,12 @@ def valid_archive(obj): return False return REQUIRED_ARCHIVE_KEYS.issubset(obj) - logger.info("Rebuilding missing manifest and missing archives directory entries, this might take some time...") - # as we have lost the manifest, we do not know any more what valid item keys we had. - # collecting any key we encounter in a damaged repo seems unwise, thus we just use - # the hardcoded list from the source code. thus, it is not recommended to rebuild a - # lost manifest on a older borg version than the most recent one that was ever used - # within this repository (assuming that newer borg versions support more item keys). - manifest = Manifest(self.key, self.repository) + logger.info("Rebuilding missing archives directory entries, this might take some time...") pi = ProgressIndicatorPercent( total=len(self.chunks), - msg="Rebuilding manifest and archives directory %6.2f%%", + msg="Rebuilding missing archives directory entries %6.2f%%", step=0.01, - msgid="check.rebuild_manifest", + msgid="check.rebuild_archives_directory", ) for chunk_id, _ in self.chunks.iteritems(): pi.show() @@ -1801,25 +1816,24 @@ def valid_archive(obj): archive = ArchiveItem(internal_dict=archive) name = archive.name logger.info(f"Found archive {name}, id {bin_to_hex(chunk_id)}.") - if manifest.archives.exists_name_and_id(name, chunk_id): + if self.manifest.archives.exists_name_and_id(name, chunk_id): logger.info("We already have an archives directory entry for this.") - elif not manifest.archives.exists(name): + elif not self.manifest.archives.exists(name): # no archives list entry yet and name is not taken yet, create an entry logger.warning(f"Creating archives directory entry for {name}.") - manifest.archives.create(name, chunk_id, archive.time) + self.manifest.archives.create(name, chunk_id, archive.time) else: # we don't have an entry yet, but the name is taken by something else i = 1 while True: new_name = "%s.%d" % (name, i) - if not manifest.archives.exists(new_name): + if not self.manifest.archives.exists(new_name): break i += 1 logger.warning(f"Creating archives directory entry using {new_name}.") - manifest.archives.create(new_name, chunk_id, archive.time) + self.manifest.archives.create(new_name, chunk_id, archive.time) pi.finish() - logger.info("Manifest and archives directory rebuild complete.") - return manifest + logger.info("Rebuilding missing archives directory entries completed.") def rebuild_archives( self, first=0, last=0, sort_by="", match=None, older=None, newer=None, oldest=None, newest=None diff --git a/src/borg/archiver/check_cmd.py b/src/borg/archiver/check_cmd.py index ed4dc3927..6f075e8f4 100644 --- a/src/borg/archiver/check_cmd.py +++ b/src/borg/archiver/check_cmd.py @@ -37,6 +37,8 @@ def do_check(self, args, repository): ) if args.repair and args.max_duration: raise CommandError("--repair does not allow --max-duration argument.") + if args.undelete_archives and not args.repair: + raise CommandError("--undelete-archives requires --repair argument.") if args.max_duration and not args.repo_only: # when doing a partial repo check, we can only check xxh64 hashes in repository files. # also, there is no max_duration support in the archives check code anyway. @@ -48,6 +50,7 @@ def do_check(self, args, repository): repository, verify_data=args.verify_data, repair=args.repair, + undelete_archives=args.undelete_archives, match=args.match_archives, sort_by=args.sort_by or "ts", first=args.first, @@ -175,6 +178,12 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser): chunks of a "zero-patched" file reappear, this effectively "heals" the file. Consequently, if lost chunks were repaired earlier, it is advised to run ``--repair`` a second time after creating some new backups. + + If ``--repair --undelete-archives`` is given, Borg will scan the repository + for archive metadata and if it finds some where no corresponding archives + directory entry exists, it will create the entries. This is basically undoing + ``borg delete archive`` or ``borg prune ...`` commands and only possible before + ``borg compact`` would remove the archives' data completely. """ ) subparser = subparsers.add_parser( @@ -202,6 +211,12 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser): subparser.add_argument( "--repair", dest="repair", action="store_true", help="attempt to repair any inconsistencies found" ) + subparser.add_argument( + "--undelete-archives", + dest="undelete_archives", + action="store_true", + help="attempt to undelete archives (use with --repair)", + ) subparser.add_argument( "--max-duration", metavar="SECONDS", diff --git a/src/borg/testsuite/archiver/check_cmd.py b/src/borg/testsuite/archiver/check_cmd.py index b6265f0c1..2f65110ce 100644 --- a/src/borg/testsuite/archiver/check_cmd.py +++ b/src/borg/testsuite/archiver/check_cmd.py @@ -289,7 +289,11 @@ def test_manifest_rebuild_duplicate_archive(archivers, request): archive_id = repo_objs.id_hash(archive) repository.put(archive_id, repo_objs.format(archive_id, {}, archive, ro_type=ROBJ_ARCHIVE_META)) cmd(archiver, "check", exit_code=1) - cmd(archiver, "check", "--repair", exit_code=0) + # when undeleting archives, borg check will discover both the original archive1 as well as + # the fake archive1 we created above. for the fake one, a new archives directory entry + # named archive1.1 will be created because we request undeleting archives and there + # is no archives directory entry for the fake archive yet. + cmd(archiver, "check", "--repair", "--undelete-archives", exit_code=0) output = cmd(archiver, "rlist") assert "archive1" in output assert "archive1.1" in output From 7442cbf8cf715e790f43cb47b31cb26e7f612098 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 7 Sep 2024 18:52:05 +0200 Subject: [PATCH 77/79] update CHANGES --- docs/changes.rst | 64 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 90be2b0bb..3a330eedc 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,8 +12,8 @@ This section provides information about security and corruption issues. Upgrade Notes ============= -borg 1.2.x to borg 2.0 ----------------------- +borg 1.2.x/1.4.x to borg 2.0 +---------------------------- Compatibility notes: @@ -21,11 +21,11 @@ Compatibility notes: We tried to put all the necessary "breaking" changes into this release, so we hopefully do not need another breaking release in the near future. The changes - were necessary for improved security, improved speed, unblocking future - improvements, getting rid of legacy crap / design limitations, having less and - simpler code to maintain. + were necessary for improved security, improved speed and parallelism, + unblocking future improvements, getting rid of legacy crap and design + limitations, having less and simpler code to maintain. - You can use "borg transfer" to transfer archives from borg 1.1/1.2 repos to + You can use "borg transfer" to transfer archives from borg 1.2/1.4 repos to a new borg 2.0 repo, but it will need some time and space. Before using "borg transfer", you must have upgraded to borg >= 1.2.6 (or @@ -84,6 +84,7 @@ Compatibility notes: - removed --nobsdflags (use --noflags) - removed --noatime (default now, see also --atime) - removed --save-space option (does not change behaviour) +- removed --bypass-lock option - using --list together with --progress is now disallowed (except with --log-json), #7219 - the --glob-archives option was renamed to --match-archives (the short option name -a is unchanged) and extended to support different pattern styles: @@ -114,12 +115,61 @@ Compatibility notes: fail now that somehow "worked" before (but maybe didn't work as intended due to the contradicting options). - .. _changelog: Change Log 2.x ============== +Version 2.0.0b10 (2024-09-09) +----------------------------- + +TL;DR: this is a huge change and the first very fundamental change in how borg +works since ever: + +- you will need to create new repos. +- likely more exciting than previous betas, definitely not for production. + +New features: + +- borgstore based repository, file:, ssh: and sftp: for now, more possible. +- repository stores objects separately now, not using segment files. + this has more fs overhead, but needs much less I/O because no segment + files compaction is required anymore. also, no repository index is + needed anymore because we can directly find the objects by their ID. +- locking: new borgstore based repository locking with automatic stale + lock removal (if lock does not get refreshed, if lock owner process is dead). +- simultaneous repository access for many borg commands except check/compact. + the cache lock for adhocwithfiles is still exclusive though, so use + BORG_CACHE_IMPL=adhoc if you want to try that out using only 1 machine + and 1 user (that implementation doesn't use a cache lock). When using + multiple client machines or users, it also works with the default cache. +- delete/prune: much quicker now and can be undone. +- check --repair --undelete-archives: bring archives back from the dead. +- rspace: manage reserved space in repository (avoid dead-end situation if + repository fs runs full). + +Bugs/issues fixed: + +- a lot! all linked from PR #8332. + +Other changes: + +- repository: remove transactions, solved differently and much simpler now + (convergence and write order primarily). +- repository: replaced precise reference counting with "object exists in repo?" + and "garbage collection of unused objects". +- cache: remove transactions, remove chunks cache. + removed LocalCache, BORG_CACHE_IMPL=local, solving all related issues. + as in beta 9, adhowwithfiles is the default implementation. +- compact: needs the borg key now (run it clientside), -v gives nice stats. +- transfer: archive transfers from borg 1.x need the --from-borg1 option +- check: reimplemented / bigger changes. +- code: got rid of a metric ton of not needed complexity. + when borg does not need to read borg 1.x repos/archives anymore, after + users have transferred their archives, even much more can be removed. +- docs: updated / removed outdated stuff + + Version 2.0.0b9 (2024-07-20) ---------------------------- From b50ed04ffc58014b6353f751ee08859ee0d572dd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 7 Sep 2024 22:31:48 +0200 Subject: [PATCH 78/79] build_usage / build_man --- docs/man/borg-benchmark-cpu.1 | 2 +- docs/man/borg-benchmark-crud.1 | 2 +- docs/man/borg-benchmark.1 | 2 +- docs/man/borg-break-lock.1 | 2 +- docs/man/borg-check.1 | 32 +++++--- docs/man/borg-common.1 | 7 +- docs/man/borg-compact.1 | 31 ++----- docs/man/borg-compression.1 | 2 +- docs/man/borg-create.1 | 18 +--- docs/man/borg-delete.1 | 21 +---- docs/man/borg-diff.1 | 2 +- docs/man/borg-export-tar.1 | 2 +- docs/man/borg-extract.1 | 2 +- docs/man/borg-import-tar.1 | 8 +- docs/man/borg-info.1 | 2 +- docs/man/borg-key-change-location.1 | 2 +- docs/man/borg-key-change-passphrase.1 | 2 +- docs/man/borg-key-export.1 | 2 +- docs/man/borg-key-import.1 | 2 +- docs/man/borg-key.1 | 2 +- docs/man/borg-list.1 | 6 +- docs/man/borg-match-archives.1 | 2 +- docs/man/borg-mount.1 | 5 +- docs/man/borg-patterns.1 | 2 +- docs/man/borg-placeholders.1 | 2 +- docs/man/borg-prune.1 | 21 +---- docs/man/borg-rcompress.1 | 15 +--- docs/man/borg-rcreate.1 | 15 +++- docs/man/borg-rdelete.1 | 2 +- docs/man/borg-recreate.1 | 8 +- docs/man/borg-rename.1 | 2 +- docs/man/borg-rinfo.1 | 11 +-- docs/man/borg-rlist.1 | 5 +- docs/man/borg-rspace.1 | 94 +++++++++++++++++++++ docs/man/borg-serve.1 | 2 +- docs/man/borg-transfer.1 | 39 +++++---- docs/man/borg-umount.1 | 2 +- docs/man/borg-version.1 | 2 +- docs/man/borg-with-lock.1 | 2 +- docs/man/borg.1 | 70 +++++++++------- docs/man/borgfs.1 | 5 +- docs/usage.rst | 1 + docs/usage/check.rst.inc | 32 +++++--- docs/usage/common-options.rst.inc | 3 +- docs/usage/compact.rst.inc | 39 +++------ docs/usage/create.rst.inc | 16 +--- docs/usage/delete.rst.inc | 83 ++++++++----------- docs/usage/import-tar.rst.inc | 6 -- docs/usage/list.rst.inc | 2 - docs/usage/mount.rst.inc | 3 - docs/usage/prune.rst.inc | 113 +++++++++++--------------- docs/usage/rcompress.rst.inc | 13 +-- docs/usage/rcreate.rst.inc | 17 +++- docs/usage/recreate.rst.inc | 32 +++----- docs/usage/rinfo.rst.inc | 11 +-- docs/usage/rlist.rst.inc | 3 - docs/usage/rspace.rst | 1 + docs/usage/rspace.rst.inc | 80 ++++++++++++++++++ docs/usage/transfer.rst.inc | 38 ++++++--- 59 files changed, 485 insertions(+), 465 deletions(-) create mode 100644 docs/man/borg-rspace.1 create mode 100644 docs/usage/rspace.rst create mode 100644 docs/usage/rspace.rst.inc diff --git a/docs/man/borg-benchmark-cpu.1 b/docs/man/borg-benchmark-cpu.1 index 50cca65ac..7d55244ed 100644 --- a/docs/man/borg-benchmark-cpu.1 +++ b/docs/man/borg-benchmark-cpu.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-BENCHMARK-CPU" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-BENCHMARK-CPU" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-benchmark-cpu \- Benchmark CPU bound operations. .SH SYNOPSIS diff --git a/docs/man/borg-benchmark-crud.1 b/docs/man/borg-benchmark-crud.1 index e4efc752f..2e066e797 100644 --- a/docs/man/borg-benchmark-crud.1 +++ b/docs/man/borg-benchmark-crud.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-BENCHMARK-CRUD" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-BENCHMARK-CRUD" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-benchmark-crud \- Benchmark Create, Read, Update, Delete for archives. .SH SYNOPSIS diff --git a/docs/man/borg-benchmark.1 b/docs/man/borg-benchmark.1 index 4dd0fcb5f..ca1227810 100644 --- a/docs/man/borg-benchmark.1 +++ b/docs/man/borg-benchmark.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-BENCHMARK" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-BENCHMARK" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-benchmark \- benchmark command .SH SYNOPSIS diff --git a/docs/man/borg-break-lock.1 b/docs/man/borg-break-lock.1 index 3bb5d2441..4df141d38 100644 --- a/docs/man/borg-break-lock.1 +++ b/docs/man/borg-break-lock.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-BREAK-LOCK" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-BREAK-LOCK" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-break-lock \- Break the repository lock (e.g. in case it was left by a dead borg. .SH SYNOPSIS diff --git a/docs/man/borg-check.1 b/docs/man/borg-check.1 index b4040573c..8c674a294 100644 --- a/docs/man/borg-check.1 +++ b/docs/man/borg-check.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-CHECK" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-CHECK" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-check \- Check repository consistency .SH SYNOPSIS @@ -40,8 +40,8 @@ It consists of two major steps: .INDENT 0.0 .IP 1. 3 Checking the consistency of the repository itself. This includes checking -the segment magic headers, and both the metadata and data of all objects in -the segments. The read data is checked by size and CRC. Bit rot and other +the file magic headers, and both the metadata and data of all objects in +the repository. The read data is checked by size and hash. Bit rot and other types of accidental damage can be detected this way. Running the repository check can be split into multiple partial checks using \fB\-\-max\-duration\fP\&. When checking a remote repository, please note that the checks run on the @@ -77,13 +77,12 @@ archive checks, nor enable repair mode. Consequently, if you want to use .sp \fBWarning:\fP Please note that partial repository checks (i.e. running it with \fB\-\-max\-duration\fP) can only perform non\-cryptographic checksum checks on the -segment files. A full repository check (i.e. without \fB\-\-max\-duration\fP) can -also do a repository index check. Enabling partial repository checks excepts -archive checks for the same reason. Therefore partial checks may be useful with -very large repositories only where a full check would take too long. +repository files. Enabling partial repository checks excepts archive checks +for the same reason. Therefore partial checks may be useful with very large +repositories only where a full check would take too long. .sp The \fB\-\-verify\-data\fP option will perform a full integrity verification (as -opposed to checking the CRC32 of the segment) of data, which means reading the +opposed to checking just the xxh64) of data, which means reading the data from the repository, decrypting and decompressing it. It is a complete cryptographic verification and hence very time consuming, but will detect any accidental and malicious corruption. Tamper\-resistance is only guaranteed for @@ -122,17 +121,15 @@ by definition, a potentially lossy task. In practice, repair mode hooks into both the repository and archive checks: .INDENT 0.0 .IP 1. 3 -When checking the repository\(aqs consistency, repair mode will try to recover -as many objects from segments with integrity errors as possible, and ensure -that the index is consistent with the data stored in the segments. +When checking the repository\(aqs consistency, repair mode removes corrupted +objects from the repository after it did a 2nd try to read them correctly. .IP 2. 3 When checking the consistency and correctness of archives, repair mode might remove whole archives from the manifest if their archive metadata chunk is corrupt or lost. On a chunk level (i.e. the contents of files), repair mode will replace corrupt or lost chunks with a same\-size replacement chunk of zeroes. If a previously zeroed chunk reappears, repair mode will restore -this lost chunk using the new chunk. Lastly, repair mode will also delete -orphaned chunks (e.g. caused by read errors while creating the archive). +this lost chunk using the new chunk. .UNINDENT .sp Most steps taken by repair mode have a one\-time effect on the repository, like @@ -152,6 +149,12 @@ replace the all\-zero replacement chunk by the reappeared chunk. If all lost chunks of a \(dqzero\-patched\(dq file reappear, this effectively \(dqheals\(dq the file. Consequently, if lost chunks were repaired earlier, it is advised to run \fB\-\-repair\fP a second time after creating some new backups. +.sp +If \fB\-\-repair \-\-undelete\-archives\fP is given, Borg will scan the repository +for archive metadata and if it finds some where no corresponding archives +directory entry exists, it will create the entries. This is basically undoing +\fBborg delete archive\fP or \fBborg prune ...\fP commands and only possible before +\fBborg compact\fP would remove the archives\(aq data completely. .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. @@ -170,6 +173,9 @@ perform cryptographic archive data integrity verification (conflicts with \fB\-\ .B \-\-repair attempt to repair any inconsistencies found .TP +.B \-\-undelete\-archives +attempt to undelete archives (use with \-\-repair) +.TP .BI \-\-max\-duration \ SECONDS do only a partial repo check for max. SECONDS seconds (Default: unlimited) .UNINDENT diff --git a/docs/man/borg-common.1 b/docs/man/borg-common.1 index ad10ac4a9..7b71033db 100644 --- a/docs/man/borg-common.1 +++ b/docs/man/borg-common.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-COMMON" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-COMMON" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-common \- Common options of Borg commands .SH SYNOPSIS @@ -64,10 +64,7 @@ format using IEC units (1KiB = 1024B) Output one JSON object per log line instead of formatted text. .TP .BI \-\-lock\-wait \ SECONDS -wait at most SECONDS for acquiring a repository/cache lock (default: 1). -.TP -.B \-\-bypass\-lock -Bypass locking mechanism +wait at most SECONDS for acquiring a repository/cache lock (default: 10). .TP .B \-\-show\-version show/log the borg version diff --git a/docs/man/borg-compact.1 b/docs/man/borg-compact.1 index 31c5d1cdb..8b4fc4caf 100644 --- a/docs/man/borg-compact.1 +++ b/docs/man/borg-compact.1 @@ -27,40 +27,25 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-COMPACT" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-COMPACT" 1 "2024-09-08" "" "borg backup tool" .SH NAME -borg-compact \- compact segment files in the repository +borg-compact \- Collect garbage in repository .SH SYNOPSIS .sp borg [common options] compact [options] .SH DESCRIPTION .sp -This command frees repository space by compacting segments. +Free repository space by deleting unused chunks. .sp -Use this regularly to avoid running out of space \- you do not need to use this -after each borg command though. It is especially useful after deleting archives, -because only compaction will really free repository space. +borg compact analyzes all existing archives to find out which chunks are +actually used. There might be unused chunks resulting from borg delete or prune, +which can be removed to free space in the repository. .sp -borg compact does not need a key, so it is possible to invoke it from the -client or also from the server. -.sp -Depending on the amount of segments that need compaction, it may take a while, -so consider using the \fB\-\-progress\fP option. -.sp -A segment is compacted if the amount of saved space is above the percentage value -given by the \fB\-\-threshold\fP option. If omitted, a threshold of 10% is used. -When using \fB\-\-verbose\fP, borg will output an estimate of the freed space. -.sp -See \fIseparate_compaction\fP in Additional Notes for more details. +Differently than borg 1.x, borg2\(aqs compact needs the borg key if the repo is +encrypted. .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. -.SS optional arguments -.INDENT 0.0 -.TP -.BI \-\-threshold \ PERCENT -set minimum threshold for saved space in PERCENT (Default: 10) -.UNINDENT .SH EXAMPLES .INDENT 0.0 .INDENT 3.5 diff --git a/docs/man/borg-compression.1 b/docs/man/borg-compression.1 index 22c69ce71..0d94a8adf 100644 --- a/docs/man/borg-compression.1 +++ b/docs/man/borg-compression.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-COMPRESSION" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-COMPRESSION" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-compression \- Details regarding compression .SH DESCRIPTION diff --git a/docs/man/borg-create.1 b/docs/man/borg-create.1 index d0616c639..c507178eb 100644 --- a/docs/man/borg-create.1 +++ b/docs/man/borg-create.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-CREATE" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-CREATE" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-create \- Create new archive .SH SYNOPSIS @@ -53,9 +53,7 @@ stdin\fP below for details. The archive will consume almost no disk space for files or parts of files that have already been stored in other archives. .sp -The archive name needs to be unique. It must not end in \(aq.checkpoint\(aq or -\(aq.checkpoint.N\(aq (with N being a number), because these names are used for -checkpoints and treated in special ways. +The archive name needs to be unique. .sp In the archive name, you may use the following placeholders: {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. @@ -155,12 +153,6 @@ only display items with the given status characters (see description) .B \-\-json output stats as JSON. Implies \fB\-\-stats\fP\&. .TP -.B \-\-no\-cache\-sync -experimental: do not synchronize the chunks cache. -.TP -.B \-\-no\-cache\-sync\-forced -experimental: do not synchronize the chunks cache (forced). -.TP .B \-\-prefer\-adhoc\-cache experimental: prefer AdHocCache (w/o files cache) over AdHocWithFilesCache (with files cache). .TP @@ -260,12 +252,6 @@ add a comment text to the archive .BI \-\-timestamp \ TIMESTAMP manually specify the archive creation date/time (yyyy\-mm\-ddThh:mm:ss[(+|\-)HH:MM] format, (+|\-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. .TP -.BI \-c \ SECONDS\fR,\fB \ \-\-checkpoint\-interval \ SECONDS -write checkpoint every SECONDS seconds (Default: 1800) -.TP -.BI \-\-checkpoint\-volume \ BYTES -write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing) -.TP .BI \-\-chunker\-params \ PARAMS specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095 .TP diff --git a/docs/man/borg-delete.1 b/docs/man/borg-delete.1 index 9e8baf0f4..7ecc0d314 100644 --- a/docs/man/borg-delete.1 +++ b/docs/man/borg-delete.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-DELETE" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-DELETE" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-delete \- Delete archives .SH SYNOPSIS @@ -42,16 +42,9 @@ you run \fBborg compact\fP\&. .sp When in doubt, use \fB\-\-dry\-run \-\-list\fP to see what would be deleted. .sp -When using \fB\-\-stats\fP, you will get some statistics about how much data was -deleted \- the \(dqDeleted data\(dq deduplicated size there is most interesting as -that is how much your repository will shrink. -Please note that the \(dqAll archives\(dq stats refer to the state after deletion. -.sp You can delete multiple archives by specifying a matching pattern, using the \fB\-\-match\-archives PATTERN\fP option (for more info on these patterns, see \fIborg_patterns\fP). -.sp -Always first use \fB\-\-dry\-run \-\-list\fP to see what would be deleted. .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. @@ -63,18 +56,6 @@ do not change repository .TP .B \-\-list output verbose list of archives -.TP -.B \-\-consider\-checkpoints -consider checkpoint archives for deletion (default: not considered). -.TP -.B \-s\fP,\fB \-\-stats -print statistics for the deleted archive -.TP -.B \-\-force -force deletion of corrupted archives, use \fB\-\-force \-\-force\fP in case \fB\-\-force\fP does not work. -.TP -.BI \-c \ SECONDS\fR,\fB \ \-\-checkpoint\-interval \ SECONDS -write checkpoint every SECONDS seconds (Default: 1800) .UNINDENT .SS Archive filters .INDENT 0.0 diff --git a/docs/man/borg-diff.1 b/docs/man/borg-diff.1 index ab9e24f77..64343833e 100644 --- a/docs/man/borg-diff.1 +++ b/docs/man/borg-diff.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-DIFF" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-DIFF" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-diff \- Diff contents of two archives .SH SYNOPSIS diff --git a/docs/man/borg-export-tar.1 b/docs/man/borg-export-tar.1 index 53632e171..4c559aedb 100644 --- a/docs/man/borg-export-tar.1 +++ b/docs/man/borg-export-tar.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-EXPORT-TAR" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-EXPORT-TAR" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-export-tar \- Export archive contents as a tarball .SH SYNOPSIS diff --git a/docs/man/borg-extract.1 b/docs/man/borg-extract.1 index 33585fd97..dd47e489d 100644 --- a/docs/man/borg-extract.1 +++ b/docs/man/borg-extract.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-EXTRACT" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-EXTRACT" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-extract \- Extract archive contents .SH SYNOPSIS diff --git a/docs/man/borg-import-tar.1 b/docs/man/borg-import-tar.1 index 1b0be536d..ac5b6b96a 100644 --- a/docs/man/borg-import-tar.1 +++ b/docs/man/borg-import-tar.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-IMPORT-TAR" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-IMPORT-TAR" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-import-tar \- Create a backup archive from a tarball .SH SYNOPSIS @@ -126,12 +126,6 @@ add a comment text to the archive .BI \-\-timestamp \ TIMESTAMP manually specify the archive creation date/time (yyyy\-mm\-ddThh:mm:ss[(+|\-)HH:MM] format, (+|\-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. .TP -.BI \-c \ SECONDS\fR,\fB \ \-\-checkpoint\-interval \ SECONDS -write checkpoint every SECONDS seconds (Default: 1800) -.TP -.BI \-\-checkpoint\-volume \ BYTES -write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing) -.TP .BI \-\-chunker\-params \ PARAMS specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095 .TP diff --git a/docs/man/borg-info.1 b/docs/man/borg-info.1 index a9812b154..58dbd5d27 100644 --- a/docs/man/borg-info.1 +++ b/docs/man/borg-info.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-INFO" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-INFO" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-info \- Show archive details such as disk space used .SH SYNOPSIS diff --git a/docs/man/borg-key-change-location.1 b/docs/man/borg-key-change-location.1 index 7ffb2f185..a845b14d0 100644 --- a/docs/man/borg-key-change-location.1 +++ b/docs/man/borg-key-change-location.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-KEY-CHANGE-LOCATION" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-KEY-CHANGE-LOCATION" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-key-change-location \- Change repository key location .SH SYNOPSIS diff --git a/docs/man/borg-key-change-passphrase.1 b/docs/man/borg-key-change-passphrase.1 index a1c6c6b2c..c2b8d5f63 100644 --- a/docs/man/borg-key-change-passphrase.1 +++ b/docs/man/borg-key-change-passphrase.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-KEY-CHANGE-PASSPHRASE" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-KEY-CHANGE-PASSPHRASE" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-key-change-passphrase \- Change repository key file passphrase .SH SYNOPSIS diff --git a/docs/man/borg-key-export.1 b/docs/man/borg-key-export.1 index c202b3a87..55bd41f41 100644 --- a/docs/man/borg-key-export.1 +++ b/docs/man/borg-key-export.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-KEY-EXPORT" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-KEY-EXPORT" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-key-export \- Export the repository key for backup .SH SYNOPSIS diff --git a/docs/man/borg-key-import.1 b/docs/man/borg-key-import.1 index 424008678..d98963768 100644 --- a/docs/man/borg-key-import.1 +++ b/docs/man/borg-key-import.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-KEY-IMPORT" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-KEY-IMPORT" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-key-import \- Import the repository key from backup .SH SYNOPSIS diff --git a/docs/man/borg-key.1 b/docs/man/borg-key.1 index 7ce6eb6e9..734563b7b 100644 --- a/docs/man/borg-key.1 +++ b/docs/man/borg-key.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-KEY" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-KEY" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-key \- Manage a keyfile or repokey of a repository .SH SYNOPSIS diff --git a/docs/man/borg-list.1 b/docs/man/borg-list.1 index 9d997585b..113681b06 100644 --- a/docs/man/borg-list.1 +++ b/docs/man/borg-list.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-LIST" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-LIST" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-list \- List archive contents .SH SYNOPSIS @@ -186,12 +186,8 @@ flags: file flags .IP \(bu 2 size: file size .IP \(bu 2 -dsize: deduplicated size -.IP \(bu 2 num_chunks: number of chunks in this file .IP \(bu 2 -unique_chunks: number of unique chunks in this file -.IP \(bu 2 mtime: file modification time .IP \(bu 2 ctime: file change time diff --git a/docs/man/borg-match-archives.1 b/docs/man/borg-match-archives.1 index 6d5b39ef7..362ac404f 100644 --- a/docs/man/borg-match-archives.1 +++ b/docs/man/borg-match-archives.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-MATCH-ARCHIVES" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-MATCH-ARCHIVES" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-match-archives \- Details regarding match-archives .SH DESCRIPTION diff --git a/docs/man/borg-mount.1 b/docs/man/borg-mount.1 index 81b7fc218..a5da12b28 100644 --- a/docs/man/borg-mount.1 +++ b/docs/man/borg-mount.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-MOUNT" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-MOUNT" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-mount \- Mount archive or an entire repository as a FUSE filesystem .SH SYNOPSIS @@ -110,9 +110,6 @@ paths to extract; patterns are supported .SS optional arguments .INDENT 0.0 .TP -.B \-\-consider\-checkpoints -Show checkpoint archives in the repository contents list (default: hidden). -.TP .B \-f\fP,\fB \-\-foreground stay in foreground, do not daemonize .TP diff --git a/docs/man/borg-patterns.1 b/docs/man/borg-patterns.1 index 6850b83ed..c6e33e003 100644 --- a/docs/man/borg-patterns.1 +++ b/docs/man/borg-patterns.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-PATTERNS" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-PATTERNS" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-patterns \- Details regarding patterns .SH DESCRIPTION diff --git a/docs/man/borg-placeholders.1 b/docs/man/borg-placeholders.1 index e570eb9cb..14d66338c 100644 --- a/docs/man/borg-placeholders.1 +++ b/docs/man/borg-placeholders.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-PLACEHOLDERS" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-PLACEHOLDERS" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-placeholders \- Details regarding placeholders .SH DESCRIPTION diff --git a/docs/man/borg-prune.1 b/docs/man/borg-prune.1 index 066d3ba7f..a1411e721 100644 --- a/docs/man/borg-prune.1 +++ b/docs/man/borg-prune.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-PRUNE" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-PRUNE" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-prune \- Prune repository archives according to specified rules .SH SYNOPSIS @@ -45,11 +45,6 @@ certain number of historic backups. This retention policy is commonly referred t \fI\%GFS\fP (Grandfather\-father\-son) backup rotation scheme. .sp -Also, prune automatically removes checkpoint archives (incomplete archives left -behind by interrupted backup runs) except if the checkpoint is the latest -archive (and thus still needed). Checkpoint archives are not considered when -comparing archive counts against the retention limits (\fB\-\-keep\-X\fP). -.sp If you use \-\-match\-archives (\-a), then only archives that match the pattern are considered for deletion and only those archives count towards the totals specified by the rules. @@ -85,11 +80,6 @@ The \fB\-\-keep\-last N\fP option is doing the same as \fB\-\-keep\-secondly N\f keep the last N archives under the assumption that you do not create more than one backup archive in the same second). .sp -When using \fB\-\-stats\fP, you will get some statistics about how much data was -deleted \- the \(dqDeleted data\(dq deduplicated size there is most interesting as -that is how much your repository will shrink. -Please note that the \(dqAll archives\(dq stats refer to the state after pruning. -.sp You can influence how the \fB\-\-list\fP output is formatted by using the \fB\-\-short\fP option (less wide output) or by giving a custom format using \fB\-\-format\fP (see the \fBborg rlist\fP description for more details about the format string). @@ -102,12 +92,6 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .B \-n\fP,\fB \-\-dry\-run do not change repository .TP -.B \-\-force -force pruning of corrupted archives, use \fB\-\-force \-\-force\fP in case \fB\-\-force\fP does not work. -.TP -.B \-s\fP,\fB \-\-stats -print statistics for the deleted archive -.TP .B \-\-list output verbose list of archives it keeps/prunes .TP @@ -146,9 +130,6 @@ number of monthly archives to keep .TP .B \-y\fP,\fB \-\-keep\-yearly number of yearly archives to keep -.TP -.BI \-c \ SECONDS\fR,\fB \ \-\-checkpoint\-interval \ SECONDS -write checkpoint every SECONDS seconds (Default: 1800) .UNINDENT .SS Archive filters .INDENT 0.0 diff --git a/docs/man/borg-rcompress.1 b/docs/man/borg-rcompress.1 index 64b9556e8..5f97ddd08 100644 --- a/docs/man/borg-rcompress.1 +++ b/docs/man/borg-rcompress.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-RCOMPRESS" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-RCOMPRESS" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-rcompress \- Repository (re-)compression .SH SYNOPSIS @@ -37,20 +37,14 @@ borg [common options] rcompress [options] .sp Repository (re\-)compression (and/or re\-obfuscation). .sp -Reads all chunks in the repository (in on\-disk order, this is important for -compaction) and recompresses them if they are not already using the compression -type/level and obfuscation level given via \fB\-\-compression\fP\&. +Reads all chunks in the repository and recompresses them if they are not already +using the compression type/level and obfuscation level given via \fB\-\-compression\fP\&. .sp If the outcome of the chunk processing indicates a change in compression type/level or obfuscation level, the processed chunk is written to the repository. Please note that the outcome might not always be the desired compression type/level \- if no compression gives a shorter output, that might be chosen. .sp -Every \fB\-\-checkpoint\-interval\fP, progress is committed to the repository and -the repository is compacted (this is to keep temporary repo space usage in bounds). -A lower checkpoint interval means lower temporary repo space usage, but also -slower progress due to higher overhead (and vice versa). -.sp Please note that this command can not work in low (or zero) free disk space conditions. .sp @@ -72,9 +66,6 @@ select compression algorithm, see the output of the \(dqborg help compression\(d .TP .B \-s\fP,\fB \-\-stats print statistics -.TP -.BI \-c \ SECONDS\fR,\fB \ \-\-checkpoint\-interval \ SECONDS -write checkpoint every SECONDS seconds (Default: 1800) .UNINDENT .SH EXAMPLES .INDENT 0.0 diff --git a/docs/man/borg-rcreate.1 b/docs/man/borg-rcreate.1 index d26f08e33..bb23f0d7a 100644 --- a/docs/man/borg-rcreate.1 +++ b/docs/man/borg-rcreate.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-RCREATE" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-RCREATE" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-rcreate \- Create a new, empty repository .SH SYNOPSIS @@ -35,8 +35,8 @@ borg-rcreate \- Create a new, empty repository borg [common options] rcreate [options] .SH DESCRIPTION .sp -This command creates a new, empty repository. A repository is a filesystem -directory containing the deduplicated data from zero or more archives. +This command creates a new, empty repository. A repository is a \fBborgstore\fP store +containing the deduplicated data from zero or more archives. .SS Encryption mode TLDR .sp The encryption mode can only be configured when creating a new repository \- you can @@ -226,6 +226,12 @@ Optionally, if you use \fB\-\-copy\-crypt\-key\fP you can also keep the same cry keys to manage. .sp Creating related repositories is useful e.g. if you want to use \fBborg transfer\fP later. +.SS Creating a related repository for data migration from borg 1.2 or 1.4 +.sp +You can use \fBborg rcreate \-\-other\-repo ORIG_REPO \-\-from\-borg1 ...\fP to create a related +repository that uses the same secret key material as the given other/original repository. +.sp +Then use \fBborg transfer \-\-other\-repo ORIG_REPO \-\-from\-borg1 ...\fP to transfer the archives. .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. @@ -235,6 +241,9 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .BI \-\-other\-repo \ SRC_REPOSITORY reuse the key material from the other repository .TP +.B \-\-from\-borg1 +other repository is borg 1.x +.TP .BI \-e \ MODE\fR,\fB \ \-\-encryption \ MODE select encryption key mode \fB(required)\fP .TP diff --git a/docs/man/borg-rdelete.1 b/docs/man/borg-rdelete.1 index f7447dcb1..f8056c562 100644 --- a/docs/man/borg-rdelete.1 +++ b/docs/man/borg-rdelete.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-RDELETE" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-RDELETE" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-rdelete \- Delete a repository .SH SYNOPSIS diff --git a/docs/man/borg-recreate.1 b/docs/man/borg-recreate.1 index 218032595..143ba3d9f 100644 --- a/docs/man/borg-recreate.1 +++ b/docs/man/borg-recreate.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-RECREATE" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-RECREATE" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-recreate \- Re-create archives .SH SYNOPSIS @@ -157,12 +157,6 @@ consider archives newer than (now \- TIMESPAN), e.g. 7d or 12m. .BI \-\-target \ TARGET create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) .TP -.BI \-c \ SECONDS\fR,\fB \ \-\-checkpoint\-interval \ SECONDS -write checkpoint every SECONDS seconds (Default: 1800) -.TP -.BI \-\-checkpoint\-volume \ BYTES -write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing) -.TP .BI \-\-comment \ COMMENT add a comment text to the archive .TP diff --git a/docs/man/borg-rename.1 b/docs/man/borg-rename.1 index 48c2b1a99..ce67e3947 100644 --- a/docs/man/borg-rename.1 +++ b/docs/man/borg-rename.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-RENAME" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-RENAME" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-rename \- Rename an existing archive .SH SYNOPSIS diff --git a/docs/man/borg-rinfo.1 b/docs/man/borg-rinfo.1 index 3ba96e3b1..ba8a9a475 100644 --- a/docs/man/borg-rinfo.1 +++ b/docs/man/borg-rinfo.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-RINFO" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-RINFO" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-rinfo \- Show repository infos .SH SYNOPSIS @@ -36,15 +36,6 @@ borg [common options] rinfo [options] .SH DESCRIPTION .sp This command displays detailed information about the repository. -.sp -Please note that the deduplicated sizes of the individual archives do not add -up to the deduplicated size of the repository (\(dqall archives\(dq), because the two -are meaning different things: -.sp -This archive / deduplicated size = amount of data stored ONLY for this archive -= unique chunks of this archive. -All archives / deduplicated size = amount of data stored in the repo -= all chunks in the repository. .SH OPTIONS .sp See \fIborg\-common(1)\fP for common options of Borg commands. diff --git a/docs/man/borg-rlist.1 b/docs/man/borg-rlist.1 index da38b71ee..c3480b469 100644 --- a/docs/man/borg-rlist.1 +++ b/docs/man/borg-rlist.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-RLIST" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-RLIST" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-rlist \- List the archives contained in a repository .SH SYNOPSIS @@ -42,9 +42,6 @@ See \fIborg\-common(1)\fP for common options of Borg commands. .SS optional arguments .INDENT 0.0 .TP -.B \-\-consider\-checkpoints -Show checkpoint archives in the repository contents list (default: hidden). -.TP .B \-\-short only print the archive names, nothing else .TP diff --git a/docs/man/borg-rspace.1 b/docs/man/borg-rspace.1 new file mode 100644 index 000000000..8e5034c4d --- /dev/null +++ b/docs/man/borg-rspace.1 @@ -0,0 +1,94 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "BORG-RSPACE" 1 "2024-09-08" "" "borg backup tool" +.SH NAME +borg-rspace \- Manage reserved space in repository +.SH SYNOPSIS +.sp +borg [common options] rspace [options] +.SH DESCRIPTION +.sp +This command manages reserved space in a repository. +.sp +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). +.sp +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\(aqt forget to reserve space again, in case you run into that +situation again at a later time. +.sp +Examples: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +# 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 +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Reserved space is always rounded up to use full reservation blocks of 64MiB. +.SH OPTIONS +.sp +See \fIborg\-common(1)\fP for common options of Borg commands. +.SS optional arguments +.INDENT 0.0 +.TP +.BI \-\-reserve \ SPACE +Amount of space to reserve (e.g. 100M, 1G). Default: 0. +.TP +.B \-\-free +Free all reserved space. Don\(aqt forget to reserve space later again. +.UNINDENT +.SH SEE ALSO +.sp +\fIborg\-common(1)\fP +.SH AUTHOR +The Borg Collective +.\" Generated by docutils manpage writer. +. diff --git a/docs/man/borg-serve.1 b/docs/man/borg-serve.1 index b5ab1dbeb..7a198b940 100644 --- a/docs/man/borg-serve.1 +++ b/docs/man/borg-serve.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-SERVE" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-SERVE" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-serve \- Start in server mode. This command is usually not used manually. .SH SYNOPSIS diff --git a/docs/man/borg-transfer.1 b/docs/man/borg-transfer.1 index f6cd45450..f7dc50f9a 100644 --- a/docs/man/borg-transfer.1 +++ b/docs/man/borg-transfer.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-TRANSFER" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-TRANSFER" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-transfer \- archives transfer from other repository, optionally upgrade data format .SH SYNOPSIS @@ -46,7 +46,14 @@ any case) and keep data compressed \(dqas is\(dq (saves time as no data compress If you want to globally change compression while transferring archives to the DST_REPO, give \fB\-\-compress=WANTED_COMPRESSION \-\-recompress=always\fP\&. .sp -Suggested use for general purpose archive transfer (not repo upgrades): +The default is to transfer all archives. +.sp +You could use the misc. archive filter options to limit which archives it will +transfer, e.g. using the \fB\-a\fP option. This is recommended for big +repositories with multiple data sets to keep the runtime per invocation lower. +.SS General purpose archive transfer +.sp +Transfer borg2 archives into a related other borg2 repository: .INDENT 0.0 .INDENT 3.5 .sp @@ -54,7 +61,7 @@ Suggested use for general purpose archive transfer (not repo upgrades): .ft C # create a related DST_REPO (reusing key material from SRC_REPO), so that # chunking and chunk id generation will work in the same way as before. -borg \-\-repo=DST_REPO rcreate \-\-other\-repo=SRC_REPO \-\-encryption=DST_ENC +borg \-\-repo=DST_REPO rcreate \-\-encryption=DST_ENC \-\-other\-repo=SRC_REPO # transfer archives from SRC_REPO to DST_REPO borg \-\-repo=DST_REPO transfer \-\-other\-repo=SRC_REPO \-\-dry\-run # check what it would do @@ -64,26 +71,23 @@ borg \-\-repo=DST_REPO transfer \-\-other\-repo=SRC_REPO \-\-dry\-run # check! .fi .UNINDENT .UNINDENT +.SS Data migration / upgrade from borg 1.x .sp -The default is to transfer all archives, including checkpoint archives. -.sp -You could use the misc. archive filter options to limit which archives it will -transfer, e.g. using the \fB\-a\fP option. This is recommended for big -repositories with multiple data sets to keep the runtime per invocation lower. -.sp -For repository upgrades (e.g. from a borg 1.2 repo to a related borg 2.0 repo), usage is -quite similar to the above: +To migrate your borg 1.x archives into a related, new borg2 repository, usage is quite similar +to the above, but you need the \fB\-\-from\-borg1\fP option: .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C -# fast: compress metadata with zstd,3, but keep data chunks compressed as they are: -borg \-\-repo=DST_REPO transfer \-\-other\-repo=SRC_REPO \-\-upgrader=From12To20 \e - \-\-compress=zstd,3 \-\-recompress=never +borg \-\-repo=DST_REPO rcreate \-\-encryption=DST_ENC \-\-other\-repo=SRC_REPO \-\-from\-borg1 -# compress metadata and recompress data with zstd,3 -borg \-\-repo=DST_REPO transfer \-\-other\-repo=SRC_REPO \-\-upgrader=From12To20 \e +# to continue using lz4 compression as you did in SRC_REPO: +borg \-\-repo=DST_REPO transfer \-\-other\-repo=SRC_REPO \-\-from\-borg1 \e + \-\-compress=lz4 \-\-recompress=never + +# alternatively, to recompress everything to zstd,3: +borg \-\-repo=DST_REPO transfer \-\-other\-repo=SRC_REPO \-\-from\-borg1 \e \-\-compress=zstd,3 \-\-recompress=always .ft P .fi @@ -101,6 +105,9 @@ do not change repository, just check .BI \-\-other\-repo \ SRC_REPOSITORY transfer archives from the other repository .TP +.B \-\-from\-borg1 +other repository is borg 1.x +.TP .BI \-\-upgrader \ UPGRADER use the upgrader to convert transferred data (default: no conversion) .TP diff --git a/docs/man/borg-umount.1 b/docs/man/borg-umount.1 index 46e4058a3..88cac080b 100644 --- a/docs/man/borg-umount.1 +++ b/docs/man/borg-umount.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-UMOUNT" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-UMOUNT" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-umount \- un-mount the FUSE filesystem .SH SYNOPSIS diff --git a/docs/man/borg-version.1 b/docs/man/borg-version.1 index 1635c4110..94fff3f58 100644 --- a/docs/man/borg-version.1 +++ b/docs/man/borg-version.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-VERSION" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-VERSION" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-version \- Display the borg client / borg server version .SH SYNOPSIS diff --git a/docs/man/borg-with-lock.1 b/docs/man/borg-with-lock.1 index db07e8ec9..9bb9abf68 100644 --- a/docs/man/borg-with-lock.1 +++ b/docs/man/borg-with-lock.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG-WITH-LOCK" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG-WITH-LOCK" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg-with-lock \- run a user specified command with the repository lock held .SH SYNOPSIS diff --git a/docs/man/borg.1 b/docs/man/borg.1 index d2944b145..08e75c8dd 100644 --- a/docs/man/borg.1 +++ b/docs/man/borg.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORG" 1 "2024-07-19" "" "borg backup tool" +.TH "BORG" 1 "2024-09-08" "" "borg backup tool" .SH NAME borg \- deduplicating and encrypting backup tool .SH SYNOPSIS @@ -238,6 +238,10 @@ Note: you may also prepend a \fBfile://\fP to a filesystem path to get URL style .sp \fBssh://user@host:port/~/path/to/repo\fP \- path relative to user\(aqs home directory .sp +\fBRemote repositories\fP accessed via sftp: +.sp +\fBsftp://user@host:port/path/to/repo\fP \- absolute path\(ga +.sp If you frequently need the same repo URL, it is a good idea to set the \fBBORG_REPO\fP environment variable to set a default for the repo URL: .INDENT 0.0 @@ -491,10 +495,6 @@ given order, e.g.: Choose the implementation for the clientside cache, choose one of: .INDENT 7.0 .IP \(bu 2 -\fBlocal\fP: uses a persistent chunks cache and keeps it in a perfect state (precise refcounts and -sizes), requiring a potentially resource expensive cache sync in multi\-client scenarios. -Also has a persistent files cache. -.IP \(bu 2 \fBadhoc\fP: builds a non\-persistent chunks cache by querying the repo. Chunks cache contents are somewhat sloppy for already existing chunks, concerning their refcount (\(dqinfinite\(dq) and size (0). No files cache (slow, will chunk all input files). DEPRECATED. @@ -698,38 +698,48 @@ mode 600, root:root). .UNINDENT .SS File systems .sp -We strongly recommend against using Borg (or any other database\-like -software) on non\-journaling file systems like FAT, since it is not -possible to assume any consistency in case of power failures (or a -sudden disconnect of an external drive or similar failures). +We recommend using a reliable, scalable journaling filesystem for the +repository, e.g. zfs, btrfs, ext4, apfs. .sp -While Borg uses a data store that is resilient against these failures -when used on journaling file systems, it is not possible to guarantee -this with some hardware \-\- independent of the software used. We don\(aqt -know a list of affected hardware. +Borg now uses the \fBborgstore\fP package to implement the key/value store it +uses for the repository. .sp -If you are suspicious whether your Borg repository is still consistent -and readable after one of the failures mentioned above occurred, run -\fBborg check \-\-verify\-data\fP to make sure it is consistent. -Requirements for Borg repository file systems +It currently uses the \fBfile:\fP Store (posixfs backend) either with a local +directory or via ssh and a remote \fBborg serve\fP agent using borgstore on the +remote side. +.sp +This means that it will store each chunk into a separate filesystem file +(for more details, see the \fBborgstore\fP project). +.sp +This has some pros and cons (compared to legacy borg 1.x\(aqs segment files): +.sp +Pros: .INDENT 0.0 .IP \(bu 2 -Long file names +Simplicity and better maintainability of the borg code. .IP \(bu 2 -At least three directory levels with short names +Sometimes faster, less I/O, better scalability: e.g. borg compact can just +remove unused chunks by deleting a single file and does not need to read +and re\-write segment files to free space. .IP \(bu 2 -Typically, file sizes up to a few hundred MB. -Large repositories may require large files (>2 GB). +In future, easier to adapt to other kinds of storage: +borgstore\(aqs backends are quite simple to implement. +A \fBsftp:\fP backend already exists, cloud storage might be easy to add. .IP \(bu 2 -Up to 1000 files per directory. +Parallel repository access with less locking is easier to implement. +.UNINDENT +.sp +Cons: +.INDENT 0.0 .IP \(bu 2 -rename(2) / MoveFile(Ex) should work as specified, i.e. on the same file system -it should be a move (not a copy) operation, and in case of a directory -it should fail if the destination exists and is not an empty directory, -since this is used for locking. +The repository filesystem will have to deal with a big amount of files (there +are provisions in borgstore against having too many files in a single directory +by using a nested directory structure). .IP \(bu 2 -Also hardlinks are used for more safe and secure file updating (e.g. of the repo -config file), but the code tries to work also if hardlinks are not supported. +Bigger fs space usage overhead (will depend on allocation block size \- modern +filesystems like zfs are rather clever here using a variable block size). +.IP \(bu 2 +Sometimes slower, due to less sequential / more random access operations. .UNINDENT .SS Units .sp @@ -747,6 +757,10 @@ For more information about that, see: \fI\%https://xkcd.com/1179/\fP .sp Unless otherwise noted, we display local date and time. Internally, we store and process date and time as UTC. +TIMESPAN +.sp +Some options accept a TIMESPAN parameter, which can be given as a +number of days (e.g. \fB7d\fP) or months (e.g. \fB12m\fP). .SS Resource Usage .sp Borg might use a lot of resources depending on the size of the data set it is dealing with. diff --git a/docs/man/borgfs.1 b/docs/man/borgfs.1 index 7c37e09f1..9e32579b4 100644 --- a/docs/man/borgfs.1 +++ b/docs/man/borgfs.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "BORGFS" 1 "2024-07-19" "" "borg backup tool" +.TH "BORGFS" 1 "2024-09-08" "" "borg backup tool" .SH NAME borgfs \- Mount archive or an entire repository as a FUSE filesystem .SH SYNOPSIS @@ -54,9 +54,6 @@ paths to extract; patterns are supported .B \-V\fP,\fB \-\-version show version number and exit .TP -.B \-\-consider\-checkpoints -Show checkpoint archives in the repository contents list (default: hidden). -.TP .B \-f\fP,\fB \-\-foreground stay in foreground, do not daemonize .TP diff --git a/docs/usage.rst b/docs/usage.rst index c27e41806..d88f968ea 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -37,6 +37,7 @@ Usage usage/general usage/rcreate + usage/rspace usage/rlist usage/rinfo usage/rcompress diff --git a/docs/usage/check.rst.inc b/docs/usage/check.rst.inc index e0764302b..f046aaa0e 100644 --- a/docs/usage/check.rst.inc +++ b/docs/usage/check.rst.inc @@ -23,6 +23,8 @@ borg check +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ | | ``--repair`` | attempt to repair any inconsistencies found | +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``--undelete-archives`` | attempt to undelete archives (use with --repair) | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ | | ``--max-duration SECONDS`` | do only a partial repo check for max. SECONDS seconds (Default: unlimited) | +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ | .. class:: borg-common-opt-ref | @@ -65,6 +67,7 @@ borg check --archives-only only perform archives checks --verify-data perform cryptographic archive data integrity verification (conflicts with ``--repository-only``) --repair attempt to repair any inconsistencies found + --undelete-archives attempt to undelete archives (use with --repair) --max-duration SECONDS do only a partial repo check for max. SECONDS seconds (Default: unlimited) @@ -89,8 +92,8 @@ The check command verifies the consistency of a repository and its archives. It consists of two major steps: 1. Checking the consistency of the repository itself. This includes checking - the segment magic headers, and both the metadata and data of all objects in - the segments. The read data is checked by size and CRC. Bit rot and other + the file magic headers, and both the metadata and data of all objects in + the repository. The read data is checked by size and hash. Bit rot and other types of accidental damage can be detected this way. Running the repository check can be split into multiple partial checks using ``--max-duration``. When checking a remote repository, please note that the checks run on the @@ -125,13 +128,12 @@ archive checks, nor enable repair mode. Consequently, if you want to use **Warning:** Please note that partial repository checks (i.e. running it with ``--max-duration``) can only perform non-cryptographic checksum checks on the -segment files. A full repository check (i.e. without ``--max-duration``) can -also do a repository index check. Enabling partial repository checks excepts -archive checks for the same reason. Therefore partial checks may be useful with -very large repositories only where a full check would take too long. +repository files. Enabling partial repository checks excepts archive checks +for the same reason. Therefore partial checks may be useful with very large +repositories only where a full check would take too long. The ``--verify-data`` option will perform a full integrity verification (as -opposed to checking the CRC32 of the segment) of data, which means reading the +opposed to checking just the xxh64) of data, which means reading the data from the repository, decrypting and decompressing it. It is a complete cryptographic verification and hence very time consuming, but will detect any accidental and malicious corruption. Tamper-resistance is only guaranteed for @@ -168,17 +170,15 @@ by definition, a potentially lossy task. In practice, repair mode hooks into both the repository and archive checks: -1. When checking the repository's consistency, repair mode will try to recover - as many objects from segments with integrity errors as possible, and ensure - that the index is consistent with the data stored in the segments. +1. When checking the repository's consistency, repair mode removes corrupted + objects from the repository after it did a 2nd try to read them correctly. 2. When checking the consistency and correctness of archives, repair mode might remove whole archives from the manifest if their archive metadata chunk is corrupt or lost. On a chunk level (i.e. the contents of files), repair mode will replace corrupt or lost chunks with a same-size replacement chunk of zeroes. If a previously zeroed chunk reappears, repair mode will restore - this lost chunk using the new chunk. Lastly, repair mode will also delete - orphaned chunks (e.g. caused by read errors while creating the archive). + this lost chunk using the new chunk. Most steps taken by repair mode have a one-time effect on the repository, like removing a lost archive from the repository. However, replacing a corrupt or @@ -196,4 +196,10 @@ repair mode Borg will check whether a previously lost chunk reappeared and will replace the all-zero replacement chunk by the reappeared chunk. If all lost chunks of a "zero-patched" file reappear, this effectively "heals" the file. Consequently, if lost chunks were repaired earlier, it is advised to run -``--repair`` a second time after creating some new backups. \ No newline at end of file +``--repair`` a second time after creating some new backups. + +If ``--repair --undelete-archives`` is given, Borg will scan the repository +for archive metadata and if it finds some where no corresponding archives +directory entry exists, it will create the entries. This is basically undoing +``borg delete archive`` or ``borg prune ...`` commands and only possible before +``borg compact`` would remove the archives' data completely. \ No newline at end of file diff --git a/docs/usage/common-options.rst.inc b/docs/usage/common-options.rst.inc index 793aedd84..31a9df865 100644 --- a/docs/usage/common-options.rst.inc +++ b/docs/usage/common-options.rst.inc @@ -8,8 +8,7 @@ -p, --progress show progress information --iec format using IEC units (1KiB = 1024B) --log-json Output one JSON object per log line instead of formatted text. ---lock-wait SECONDS wait at most SECONDS for acquiring a repository/cache lock (default: 1). ---bypass-lock Bypass locking mechanism +--lock-wait SECONDS wait at most SECONDS for acquiring a repository/cache lock (default: 10). --show-version show/log the borg version --show-rc show/log the return code (rc) --umask M set umask to M (local only, default: 0077) diff --git a/docs/usage/compact.rst.inc b/docs/usage/compact.rst.inc index 946b376fc..8fad820d0 100644 --- a/docs/usage/compact.rst.inc +++ b/docs/usage/compact.rst.inc @@ -12,15 +12,11 @@ borg compact .. class:: borg-options-table - +-------------------------------------------------------+-------------------------+----------------------------------------------------------------+ - | **optional arguments** | - +-------------------------------------------------------+-------------------------+----------------------------------------------------------------+ - | | ``--threshold PERCENT`` | set minimum threshold for saved space in PERCENT (Default: 10) | - +-------------------------------------------------------+-------------------------+----------------------------------------------------------------+ - | .. class:: borg-common-opt-ref | - | | - | :ref:`common_options` | - +-------------------------------------------------------+-------------------------+----------------------------------------------------------------+ + +-------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+ .. raw:: html @@ -34,30 +30,17 @@ borg compact - optional arguments - --threshold PERCENT set minimum threshold for saved space in PERCENT (Default: 10) - - :ref:`common_options` | Description ~~~~~~~~~~~ -This command frees repository space by compacting segments. +Free repository space by deleting unused chunks. -Use this regularly to avoid running out of space - you do not need to use this -after each borg command though. It is especially useful after deleting archives, -because only compaction will really free repository space. +borg compact analyzes all existing archives to find out which chunks are +actually used. There might be unused chunks resulting from borg delete or prune, +which can be removed to free space in the repository. -borg compact does not need a key, so it is possible to invoke it from the -client or also from the server. - -Depending on the amount of segments that need compaction, it may take a while, -so consider using the ``--progress`` option. - -A segment is compacted if the amount of saved space is above the percentage value -given by the ``--threshold`` option. If omitted, a threshold of 10% is used. -When using ``--verbose``, borg will output an estimate of the freed space. - -See :ref:`separate_compaction` in Additional Notes for more details. \ No newline at end of file +Differently than borg 1.x, borg2's compact needs the borg key if the repo is +encrypted. \ No newline at end of file diff --git a/docs/usage/create.rst.inc b/docs/usage/create.rst.inc index bf6129692..7924e4adf 100644 --- a/docs/usage/create.rst.inc +++ b/docs/usage/create.rst.inc @@ -31,10 +31,6 @@ borg create +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--json`` | output stats as JSON. Implies ``--stats``. | +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--no-cache-sync`` | experimental: do not synchronize the chunks cache. | - +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--no-cache-sync-forced`` | experimental: do not synchronize the chunks cache (forced). | - +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--prefer-adhoc-cache`` | experimental: prefer AdHocCache (w/o files cache) over AdHocWithFilesCache (with files cache). | +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--stdin-name NAME`` | use NAME in archive for stdin data (default: 'stdin') | @@ -105,10 +101,6 @@ borg create +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--timestamp TIMESTAMP`` | manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. | +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``-c SECONDS``, ``--checkpoint-interval SECONDS`` | write checkpoint every SECONDS seconds (Default: 1800) | - +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--checkpoint-volume BYTES`` | write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing) | - +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--chunker-params PARAMS`` | specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095 | +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. | @@ -136,8 +128,6 @@ borg create --list output verbose list of items (files, dirs, ...) --filter STATUSCHARS only display items with the given status characters (see description) --json output stats as JSON. Implies ``--stats``. - --no-cache-sync experimental: do not synchronize the chunks cache. - --no-cache-sync-forced experimental: do not synchronize the chunks cache (forced). --prefer-adhoc-cache experimental: prefer AdHocCache (w/o files cache) over AdHocWithFilesCache (with files cache). --stdin-name NAME use NAME in archive for stdin data (default: 'stdin') --stdin-user USER set user USER in archive for stdin data (default: do not store user/uid) @@ -180,8 +170,6 @@ borg create Archive options --comment COMMENT add a comment text to the archive --timestamp TIMESTAMP manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. - -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) - --checkpoint-volume BYTES write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing) --chunker-params PARAMS specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095 -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. @@ -207,9 +195,7 @@ stdin* below for details. The archive will consume almost no disk space for files or parts of files that have already been stored in other archives. -The archive name needs to be unique. It must not end in '.checkpoint' or -'.checkpoint.N' (with N being a number), because these names are used for -checkpoints and treated in special ways. +The archive name needs to be unique. In the archive name, you may use the following placeholders: {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others. diff --git a/docs/usage/delete.rst.inc b/docs/usage/delete.rst.inc index c5740ca67..688bce06e 100644 --- a/docs/usage/delete.rst.inc +++ b/docs/usage/delete.rst.inc @@ -12,43 +12,35 @@ borg delete .. class:: borg-options-table - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | **optional arguments** | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``-n``, ``--dry-run`` | do not change repository | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--list`` | output verbose list of archives | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--consider-checkpoints`` | consider checkpoint archives for deletion (default: not considered). | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``-s``, ``--stats`` | print statistics for the deleted archive | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--force`` | force deletion of corrupted archives, use ``--force --force`` in case ``--force`` does not work. | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``-c SECONDS``, ``--checkpoint-interval SECONDS`` | write checkpoint every SECONDS seconds (Default: 1800) | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | .. class:: borg-common-opt-ref | - | | - | :ref:`common_options` | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | **Archive filters** — Archive filters can be applied to repository targets. | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archive names matching the pattern. see "borg help match-archives". | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--sort-by KEYS`` | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id; default is: timestamp | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--first N`` | consider first N archives after other filters were applied | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--last N`` | consider last N archives after other filters were applied | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--oldest TIMESPAN`` | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g. 7d or 12m. | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--newest TIMESPAN`` | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g. 7d or 12m. | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--older TIMESPAN`` | consider archives older than (now - TIMESPAN), e.g. 7d or 12m. | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--newer TIMESPAN`` | consider archives newer than (now - TIMESPAN), e.g. 7d or 12m. | - +-----------------------------------------------------------------------------+---------------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``-n``, ``--dry-run`` | do not change repository | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``--list`` | output verbose list of archives | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | **Archive filters** — Archive filters can be applied to repository targets. | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archive names matching the pattern. see "borg help match-archives". | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``--sort-by KEYS`` | Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id; default is: timestamp | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``--first N`` | consider first N archives after other filters were applied | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``--last N`` | consider last N archives after other filters were applied | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``--oldest TIMESPAN`` | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g. 7d or 12m. | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``--newest TIMESPAN`` | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g. 7d or 12m. | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``--older TIMESPAN`` | consider archives older than (now - TIMESPAN), e.g. 7d or 12m. | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ + | | ``--newer TIMESPAN`` | consider archives newer than (now - TIMESPAN), e.g. 7d or 12m. | + +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ .. raw:: html @@ -63,12 +55,8 @@ borg delete optional arguments - -n, --dry-run do not change repository - --list output verbose list of archives - --consider-checkpoints consider checkpoint archives for deletion (default: not considered). - -s, --stats print statistics for the deleted archive - --force force deletion of corrupted archives, use ``--force --force`` in case ``--force`` does not work. - -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) + -n, --dry-run do not change repository + --list output verbose list of archives :ref:`common_options` @@ -95,13 +83,6 @@ 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 pattern, using the ``--match-archives PATTERN`` option (for more info on these patterns, -see :ref:`borg_patterns`). - -Always first use ``--dry-run --list`` to see what would be deleted. \ No newline at end of file +see :ref:`borg_patterns`). \ No newline at end of file diff --git a/docs/usage/import-tar.rst.inc b/docs/usage/import-tar.rst.inc index fcf0eaa5f..a20700882 100644 --- a/docs/usage/import-tar.rst.inc +++ b/docs/usage/import-tar.rst.inc @@ -43,10 +43,6 @@ borg import-tar +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--timestamp TIMESTAMP`` | manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. | +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``-c SECONDS``, ``--checkpoint-interval SECONDS`` | write checkpoint every SECONDS seconds (Default: 1800) | - +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--checkpoint-volume BYTES`` | write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing) | - +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--chunker-params PARAMS`` | specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095 | +-------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. | @@ -83,8 +79,6 @@ borg import-tar Archive options --comment COMMENT add a comment text to the archive --timestamp TIMESTAMP manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. - -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) - --checkpoint-volume BYTES write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing) --chunker-params PARAMS specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE). default: buzhash,19,23,21,4095 -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. diff --git a/docs/usage/list.rst.inc b/docs/usage/list.rst.inc index bd99efcc0..88634bc86 100644 --- a/docs/usage/list.rst.inc +++ b/docs/usage/list.rst.inc @@ -127,9 +127,7 @@ Keys available only when listing files in an archive: - flags: file flags - size: file size -- dsize: deduplicated size - num_chunks: number of chunks in this file -- unique_chunks: number of unique chunks in this file - mtime: file modification time - ctime: file change time diff --git a/docs/usage/mount.rst.inc b/docs/usage/mount.rst.inc index 777a86b1b..ec8c634f0 100644 --- a/docs/usage/mount.rst.inc +++ b/docs/usage/mount.rst.inc @@ -21,8 +21,6 @@ borg mount +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ | **optional arguments** | +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ - | | ``--consider-checkpoints`` | Show checkpoint archives in the repository contents list (default: hidden). | - +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ | | ``-f``, ``--foreground`` | stay in foreground, do not daemonize | +-----------------------------------------------------------------------------+----------------------------------------------+-----------------------------------------------------------------------------------------------------------+ | | ``-o`` | Extra mount options | @@ -81,7 +79,6 @@ borg mount optional arguments - --consider-checkpoints Show checkpoint archives in the repository contents list (default: hidden). -f, --foreground stay in foreground, do not daemonize -o Extra mount options --numeric-ids use numeric user and group identifiers from archive(s) diff --git a/docs/usage/prune.rst.inc b/docs/usage/prune.rst.inc index 0504f15d3..dfa46e1cc 100644 --- a/docs/usage/prune.rst.inc +++ b/docs/usage/prune.rst.inc @@ -12,59 +12,53 @@ borg prune .. class:: borg-options-table - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | **optional arguments** | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``-n``, ``--dry-run`` | do not change repository | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--force`` | force pruning of corrupted archives, use ``--force --force`` in case ``--force`` does not work. | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``-s``, ``--stats`` | print statistics for the deleted archive | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--list`` | output verbose list of archives it keeps/prunes | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--short`` | use a less wide archive part format | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--list-pruned`` | output verbose list of archives it prunes | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--list-kept`` | output verbose list of archives it keeps | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--format FORMAT`` | specify format for the archive part (default: "{archive:<36} {time} [{id}]") | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--keep-within INTERVAL`` | keep all archives within this time interval | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--keep-last``, ``--keep-secondly`` | number of secondly archives to keep | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--keep-minutely`` | number of minutely archives to keep | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``-H``, ``--keep-hourly`` | number of hourly archives to keep | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``-d``, ``--keep-daily`` | number of daily archives to keep | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``-w``, ``--keep-weekly`` | number of weekly archives to keep | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``-m``, ``--keep-monthly`` | number of monthly archives to keep | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``-y``, ``--keep-yearly`` | number of yearly archives to keep | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``-c SECONDS``, ``--checkpoint-interval SECONDS`` | write checkpoint every SECONDS seconds (Default: 1800) | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | .. class:: borg-common-opt-ref | - | | - | :ref:`common_options` | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | **Archive filters** — Archive filters can be applied to repository targets. | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archive names matching the pattern. see "borg help match-archives". | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--oldest TIMESPAN`` | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g. 7d or 12m. | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--newest TIMESPAN`` | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g. 7d or 12m. | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--older TIMESPAN`` | consider archives older than (now - TIMESPAN), e.g. 7d or 12m. | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ - | | ``--newer TIMESPAN`` | consider archives newer than (now - TIMESPAN), e.g. 7d or 12m. | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------+ + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | **optional arguments** | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``-n``, ``--dry-run`` | do not change repository | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--list`` | output verbose list of archives it keeps/prunes | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--short`` | use a less wide archive part format | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--list-pruned`` | output verbose list of archives it prunes | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--list-kept`` | output verbose list of archives it keeps | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--format FORMAT`` | specify format for the archive part (default: "{archive:<36} {time} [{id}]") | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--keep-within INTERVAL`` | keep all archives within this time interval | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--keep-last``, ``--keep-secondly`` | number of secondly archives to keep | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--keep-minutely`` | number of minutely archives to keep | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``-H``, ``--keep-hourly`` | number of hourly archives to keep | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``-d``, ``--keep-daily`` | number of daily archives to keep | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``-w``, ``--keep-weekly`` | number of weekly archives to keep | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``-m``, ``--keep-monthly`` | number of monthly archives to keep | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``-y``, ``--keep-yearly`` | number of yearly archives to keep | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | **Archive filters** — Archive filters can be applied to repository targets. | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``-a PATTERN``, ``--match-archives PATTERN`` | only consider archive names matching the pattern. see "borg help match-archives". | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--oldest TIMESPAN`` | consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g. 7d or 12m. | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--newest TIMESPAN`` | consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g. 7d or 12m. | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--older TIMESPAN`` | consider archives older than (now - TIMESPAN), e.g. 7d or 12m. | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ + | | ``--newer TIMESPAN`` | consider archives newer than (now - TIMESPAN), e.g. 7d or 12m. | + +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------+ .. raw:: html @@ -80,8 +74,6 @@ borg prune optional arguments -n, --dry-run do not change repository - --force force pruning of corrupted archives, use ``--force --force`` in case ``--force`` does not work. - -s, --stats print statistics for the deleted archive --list output verbose list of archives it keeps/prunes --short use a less wide archive part format --list-pruned output verbose list of archives it prunes @@ -95,7 +87,6 @@ borg prune -w, --keep-weekly number of weekly archives to keep -m, --keep-monthly number of monthly archives to keep -y, --keep-yearly number of yearly archives to keep - -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) :ref:`common_options` @@ -122,11 +113,6 @@ certain number of historic backups. This retention policy is commonly referred t `GFS `_ (Grandfather-father-son) backup rotation scheme. -Also, prune automatically removes checkpoint archives (incomplete archives left -behind by interrupted backup runs) except if the checkpoint is the latest -archive (and thus still needed). Checkpoint archives are not considered when -comparing archive counts against the retention limits (``--keep-X``). - If you use --match-archives (-a), then only archives that match the pattern are considered for deletion and only those archives count towards the totals specified by the rules. @@ -162,11 +148,6 @@ The ``--keep-last N`` option is doing the same as ``--keep-secondly N`` (and it keep the last N archives under the assumption that you do not create more than one backup archive in the same second). -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 pruning. - You can influence how the ``--list`` output is formatted by using the ``--short`` option (less wide output) or by giving a custom format using ``--format`` (see the ``borg rlist`` description for more details about the format string). \ No newline at end of file diff --git a/docs/usage/rcompress.rst.inc b/docs/usage/rcompress.rst.inc index 97d19c247..9d3861895 100644 --- a/docs/usage/rcompress.rst.inc +++ b/docs/usage/rcompress.rst.inc @@ -19,8 +19,6 @@ borg rcompress +-------------------------------------------------------+---------------------------------------------------+--------------------------------------------------------------------------------------------------+ | | ``-s``, ``--stats`` | print statistics | +-------------------------------------------------------+---------------------------------------------------+--------------------------------------------------------------------------------------------------+ - | | ``-c SECONDS``, ``--checkpoint-interval SECONDS`` | write checkpoint every SECONDS seconds (Default: 1800) | - +-------------------------------------------------------+---------------------------------------------------+--------------------------------------------------------------------------------------------------+ | .. class:: borg-common-opt-ref | | | | :ref:`common_options` | @@ -41,7 +39,6 @@ borg rcompress optional arguments -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. -s, --stats print statistics - -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) :ref:`common_options` @@ -52,20 +49,14 @@ Description Repository (re-)compression (and/or re-obfuscation). -Reads all chunks in the repository (in on-disk order, this is important for -compaction) and recompresses them if they are not already using the compression -type/level and obfuscation level given via ``--compression``. +Reads all chunks in the repository and recompresses them if they are not already +using the compression type/level and obfuscation level given via ``--compression``. If the outcome of the chunk processing indicates a change in compression type/level or obfuscation level, the processed chunk is written to the repository. Please note that the outcome might not always be the desired compression type/level - if no compression gives a shorter output, that might be chosen. -Every ``--checkpoint-interval``, progress is committed to the repository and -the repository is compacted (this is to keep temporary repo space usage in bounds). -A lower checkpoint interval means lower temporary repo space usage, but also -slower progress due to higher overhead (and vice versa). - Please note that this command can not work in low (or zero) free disk space conditions. diff --git a/docs/usage/rcreate.rst.inc b/docs/usage/rcreate.rst.inc index 9082fb562..b61acd497 100644 --- a/docs/usage/rcreate.rst.inc +++ b/docs/usage/rcreate.rst.inc @@ -17,6 +17,8 @@ borg rcreate +-------------------------------------------------------+------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--other-repo SRC_REPOSITORY`` | reuse the key material from the other repository | +-------------------------------------------------------+------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--from-borg1`` | other repository is borg 1.x | + +-------------------------------------------------------+------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``-e MODE``, ``--encryption MODE`` | select encryption key mode **(required)** | +-------------------------------------------------------+------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--append-only`` | create an append-only mode repository. Note that this only affects the low level structure of the repository, and running `delete` or `prune` will still be allowed. See :ref:`append_only_mode` in Additional Notes for more details. | @@ -46,6 +48,7 @@ borg rcreate optional arguments --other-repo SRC_REPOSITORY reuse the key material from the other repository + --from-borg1 other repository is borg 1.x -e MODE, --encryption MODE select encryption key mode **(required)** --append-only create an append-only mode repository. Note that this only affects the low level structure of the repository, and running `delete` or `prune` will still be allowed. See :ref:`append_only_mode` in Additional Notes for more details. --storage-quota QUOTA Set storage quota of the new repository (e.g. 5G, 1.5T). Default: no quota. @@ -59,8 +62,8 @@ borg rcreate Description ~~~~~~~~~~~ -This command creates a new, empty repository. A repository is a filesystem -directory containing the deduplicated data from zero or more archives. +This command creates a new, empty repository. A repository is a ``borgstore`` store +containing the deduplicated data from zero or more archives. Encryption mode TLDR ++++++++++++++++++++ @@ -173,4 +176,12 @@ Optionally, if you use ``--copy-crypt-key`` you can also keep the same crypt_key (used for authenticated encryption). Might be desired e.g. if you want to have less keys to manage. -Creating related repositories is useful e.g. if you want to use ``borg transfer`` later. \ No newline at end of file +Creating related repositories is useful e.g. if you want to use ``borg transfer`` later. + +Creating a related repository for data migration from borg 1.2 or 1.4 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +You can use ``borg rcreate --other-repo ORIG_REPO --from-borg1 ...`` to create a related +repository that uses the same secret key material as the given other/original repository. + +Then use ``borg transfer --other-repo ORIG_REPO --from-borg1 ...`` to transfer the archives. \ No newline at end of file diff --git a/docs/usage/recreate.rst.inc b/docs/usage/recreate.rst.inc index 16ee53b21..46f0638c4 100644 --- a/docs/usage/recreate.rst.inc +++ b/docs/usage/recreate.rst.inc @@ -67,10 +67,6 @@ borg recreate +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--target TARGET`` | create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) | +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``-c SECONDS``, ``--checkpoint-interval SECONDS`` | write checkpoint every SECONDS seconds (Default: 1800) | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--checkpoint-volume BYTES`` | write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing) | - +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--comment COMMENT`` | add a comment text to the archive | +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--timestamp TIMESTAMP`` | manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. | @@ -115,21 +111,19 @@ borg recreate Archive filters - -a PATTERN, --match-archives PATTERN only consider archive names matching the pattern. see "borg help match-archives". - --sort-by KEYS Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id; default is: timestamp - --first N consider first N archives after other filters were applied - --last N consider last N archives after other filters were applied - --oldest TIMESPAN consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g. 7d or 12m. - --newest TIMESPAN consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g. 7d or 12m. - --older TIMESPAN consider archives older than (now - TIMESPAN), e.g. 7d or 12m. - --newer TIMESPAN consider archives newer than (now - TIMESPAN), e.g. 7d or 12m. - --target TARGET create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) - -c SECONDS, --checkpoint-interval SECONDS write checkpoint every SECONDS seconds (Default: 1800) - --checkpoint-volume BYTES write checkpoint every BYTES bytes (Default: 0, meaning no volume based checkpointing) - --comment COMMENT add a comment text to the archive - --timestamp TIMESTAMP manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. - -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. - --chunker-params PARAMS rechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the chunker defaults. default: do not rechunk + -a PATTERN, --match-archives PATTERN only consider archive names matching the pattern. see "borg help match-archives". + --sort-by KEYS Comma-separated list of sorting keys; valid keys are: timestamp, archive, name, id; default is: timestamp + --first N consider first N archives after other filters were applied + --last N consider last N archives after other filters were applied + --oldest TIMESPAN consider archives between the oldest archive's timestamp and (oldest + TIMESPAN), e.g. 7d or 12m. + --newest TIMESPAN consider archives between the newest archive's timestamp and (newest - TIMESPAN), e.g. 7d or 12m. + --older TIMESPAN consider archives older than (now - TIMESPAN), e.g. 7d or 12m. + --newer TIMESPAN consider archives newer than (now - TIMESPAN), e.g. 7d or 12m. + --target TARGET create a new archive with the name ARCHIVE, do not replace existing archive (only applies for a single archive) + --comment COMMENT add a comment text to the archive + --timestamp TIMESTAMP manually specify the archive creation date/time (yyyy-mm-ddThh:mm:ss[(+|-)HH:MM] format, (+|-)HH:MM is the UTC offset, default: local time zone). Alternatively, give a reference file/directory. + -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. + --chunker-params PARAMS rechunk using given chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, HASH_MASK_BITS, HASH_WINDOW_SIZE) or `default` to use the chunker defaults. default: do not rechunk Description diff --git a/docs/usage/rinfo.rst.inc b/docs/usage/rinfo.rst.inc index fa57cdebd..098a7c7ea 100644 --- a/docs/usage/rinfo.rst.inc +++ b/docs/usage/rinfo.rst.inc @@ -44,13 +44,4 @@ borg rinfo Description ~~~~~~~~~~~ -This command displays detailed information about the repository. - -Please note that the deduplicated sizes of the individual archives do not add -up to the deduplicated size of the repository ("all archives"), because the two -are meaning different things: - -This archive / deduplicated size = amount of data stored ONLY for this archive -= unique chunks of this archive. -All archives / deduplicated size = amount of data stored in the repo -= all chunks in the repository. \ No newline at end of file +This command displays detailed information about the repository. \ No newline at end of file diff --git a/docs/usage/rlist.rst.inc b/docs/usage/rlist.rst.inc index 2c3b5fcb8..d96d1df71 100644 --- a/docs/usage/rlist.rst.inc +++ b/docs/usage/rlist.rst.inc @@ -15,8 +15,6 @@ borg rlist +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | **optional arguments** | +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | | ``--consider-checkpoints`` | Show checkpoint archives in the repository contents list (default: hidden). | - +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--short`` | only print the archive names, nothing else | +-----------------------------------------------------------------------------+----------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--format FORMAT`` | specify format for archive listing (default: "{archive:<36} {time} [{id}]{NL}") | @@ -59,7 +57,6 @@ borg rlist optional arguments - --consider-checkpoints Show checkpoint archives in the repository contents list (default: hidden). --short only print the archive names, nothing else --format FORMAT specify format for archive listing (default: "{archive:<36} {time} [{id}]{NL}") --json Format output as JSON. The form of ``--format`` is ignored, but keys used in it are added to the JSON output. Some keys are always present. Note: JSON can only represent text. diff --git a/docs/usage/rspace.rst b/docs/usage/rspace.rst new file mode 100644 index 000000000..0913340fd --- /dev/null +++ b/docs/usage/rspace.rst @@ -0,0 +1 @@ +.. include:: rspace.rst.inc diff --git a/docs/usage/rspace.rst.inc b/docs/usage/rspace.rst.inc new file mode 100644 index 000000000..28e8ce62a --- /dev/null +++ b/docs/usage/rspace.rst.inc @@ -0,0 +1,80 @@ +.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit! + +.. _borg_rspace: + +borg rspace +----------- +.. code-block:: none + + borg [common options] rspace [options] + +.. only:: html + + .. class:: borg-options-table + + +-------------------------------------------------------+---------------------+---------------------------------------------------------------------+ + | **optional arguments** | + +-------------------------------------------------------+---------------------+---------------------------------------------------------------------+ + | | ``--reserve SPACE`` | Amount of space to reserve (e.g. 100M, 1G). Default: 0. | + +-------------------------------------------------------+---------------------+---------------------------------------------------------------------+ + | | ``--free`` | Free all reserved space. Don't forget to reserve space later again. | + +-------------------------------------------------------+---------------------+---------------------------------------------------------------------+ + | .. class:: borg-common-opt-ref | + | | + | :ref:`common_options` | + +-------------------------------------------------------+---------------------+---------------------------------------------------------------------+ + + .. raw:: html + + + +.. only:: latex + + + + optional arguments + --reserve SPACE Amount of space to reserve (e.g. 100M, 1G). Default: 0. + --free Free all reserved space. Don't forget to reserve space later again. + + + :ref:`common_options` + | + +Description +~~~~~~~~~~~ + +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. \ No newline at end of file diff --git a/docs/usage/transfer.rst.inc b/docs/usage/transfer.rst.inc index f1c3d570a..5b6d45335 100644 --- a/docs/usage/transfer.rst.inc +++ b/docs/usage/transfer.rst.inc @@ -19,6 +19,8 @@ borg transfer +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--other-repo SRC_REPOSITORY`` | transfer archives from the other repository | +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + | | ``--from-borg1`` | other repository is borg 1.x | + +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``--upgrader UPGRADER`` | use the upgrader to convert transferred data (default: no conversion) | +-----------------------------------------------------------------------------+---------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | | ``-C COMPRESSION``, ``--compression COMPRESSION`` | select compression algorithm, see the output of the "borg help compression" command for details. | @@ -63,6 +65,7 @@ borg transfer optional arguments -n, --dry-run do not change repository, just check --other-repo SRC_REPOSITORY transfer archives from the other repository + --from-borg1 other repository is borg 1.x --upgrader UPGRADER use the upgrader to convert transferred data (default: no conversion) -C COMPRESSION, --compression COMPRESSION select compression algorithm, see the output of the "borg help compression" command for details. --recompress MODE recompress data chunks according to `MODE` and ``--compression``. Possible modes are `always`: recompress unconditionally; and `never`: do not recompress (faster: re-uses compressed data chunks w/o change).If no MODE is given, `always` will be used. Not passing --recompress is equivalent to "--recompress never". @@ -96,31 +99,40 @@ any case) and keep data compressed "as is" (saves time as no data compression is If you want to globally change compression while transferring archives to the DST_REPO, give ``--compress=WANTED_COMPRESSION --recompress=always``. -Suggested use for general purpose archive transfer (not repo upgrades):: +The default is to transfer all archives. + +You could use the misc. archive filter options to limit which archives it will +transfer, e.g. using the ``-a`` option. This is recommended for big +repositories with multiple data sets to keep the runtime per invocation lower. + +General purpose archive transfer +++++++++++++++++++++++++++++++++ + +Transfer borg2 archives into a related other borg2 repository:: # create a related DST_REPO (reusing key material from SRC_REPO), so that # chunking and chunk id generation will work in the same way as before. - borg --repo=DST_REPO rcreate --other-repo=SRC_REPO --encryption=DST_ENC + borg --repo=DST_REPO rcreate --encryption=DST_ENC --other-repo=SRC_REPO # transfer archives from SRC_REPO to DST_REPO borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check what it would do borg --repo=DST_REPO transfer --other-repo=SRC_REPO # do it! borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check! anything left? -The default is to transfer all archives, including checkpoint archives. -You could use the misc. archive filter options to limit which archives it will -transfer, e.g. using the ``-a`` option. This is recommended for big -repositories with multiple data sets to keep the runtime per invocation lower. +Data migration / upgrade from borg 1.x +++++++++++++++++++++++++++++++++++++++ -For repository upgrades (e.g. from a borg 1.2 repo to a related borg 2.0 repo), usage is -quite similar to the above:: +To migrate your borg 1.x archives into a related, new borg2 repository, usage is quite similar +to the above, but you need the ``--from-borg1`` option:: - # fast: compress metadata with zstd,3, but keep data chunks compressed as they are: - borg --repo=DST_REPO transfer --other-repo=SRC_REPO --upgrader=From12To20 \ - --compress=zstd,3 --recompress=never + borg --repo=DST_REPO rcreate --encryption=DST_ENC --other-repo=SRC_REPO --from-borg1 - # compress metadata and recompress data with zstd,3 - borg --repo=DST_REPO transfer --other-repo=SRC_REPO --upgrader=From12To20 \ + # to continue using lz4 compression as you did in SRC_REPO: + borg --repo=DST_REPO transfer --other-repo=SRC_REPO --from-borg1 \ + --compress=lz4 --recompress=never + + # alternatively, to recompress everything to zstd,3: + borg --repo=DST_REPO transfer --other-repo=SRC_REPO --from-borg1 \ --compress=zstd,3 --recompress=always From 3794e328906172fa83291c47de61ea73ce0e510f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 8 Sep 2024 12:32:35 +0200 Subject: [PATCH 79/79] --append-only and --storage-quota are not supported (yet?) --- src/borg/archiver/rcreate_cmd.py | 6 +++++- src/borg/archiver/serve_cmd.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/borg/archiver/rcreate_cmd.py b/src/borg/archiver/rcreate_cmd.py index 39597a848..d96e6d6ad 100644 --- a/src/borg/archiver/rcreate_cmd.py +++ b/src/borg/archiver/rcreate_cmd.py @@ -4,7 +4,7 @@ from ..cache import Cache from ..constants import * # NOQA from ..crypto.key import key_creator, key_argument_names -from ..helpers import CancelledByUser +from ..helpers import CancelledByUser, CommandError from ..helpers import location_validator, Location from ..helpers import parse_storage_quota from ..manifest import Manifest @@ -19,6 +19,10 @@ class RCreateMixIn: @with_other_repository(manifest=True, compatibility=(Manifest.Operation.READ,)) def do_rcreate(self, args, repository, *, other_repository=None, other_manifest=None): """Create a new, empty repository""" + if args.storage_quota is not None: + raise CommandError("storage-quota is not supported (yet?)") + if args.append_only: + raise CommandError("append-only is not supported (yet?)") other_key = other_manifest.key if other_manifest is not None else None path = args.location.canonical_path() logger.info('Initializing repository at "%s"' % path) diff --git a/src/borg/archiver/serve_cmd.py b/src/borg/archiver/serve_cmd.py index 8cc613c58..d16949834 100644 --- a/src/borg/archiver/serve_cmd.py +++ b/src/borg/archiver/serve_cmd.py @@ -2,7 +2,7 @@ from ._common import Highlander from ..constants import * # NOQA -from ..helpers import parse_storage_quota +from ..helpers import parse_storage_quota, CommandError from ..remote import RepositoryServer from ..logger import create_logger @@ -13,6 +13,10 @@ class ServeMixIn: def do_serve(self, args): """Start in server mode. This command is usually not used manually.""" + if args.append_only: + raise CommandError("append-only is not supported (yet?)") + if args.storage_quota is not None: + raise CommandError("storage-quota is not supported (yet?)") RepositoryServer( restrict_to_paths=args.restrict_to_paths, restrict_to_repositories=args.restrict_to_repositories,
  • Kp`aaT*Ay#&O;_@wrip(jPRq0Xi&3 znPbO2uz7;ao(wUYnT+X%#2#?G$e~9YdW66qhEnz7@uBIuFyiDC5xmYh5p;uVwHVfL zAOtp?zzKT!d?d9gCd^@?_|%#ld`5MorFRf^KN$9bjHgUPOuz{n&Vl42nm^&UY24@F z>LC^1m;FU=3jf*(9{v)okLK*BwZL+4Z)NP|W`4}8(!X+_C;_3*$f49@DbXCR%k?Pi3OTN&*kwy`KZ#n2%# zj;QyOy#AZhR7;&{u%ykV7^V^Tc3xz^7kJ)iK+#k)G!YV=i!4Gj-1GKM^<3T{WxgIB zd9x+*$@txhAB|TpNQqO);7%4FMcOU2d|e@2h~3J`R<#vS3k;ym3?7I1@Ck9E;u8R+3|H5MNw&Aq*#p=s49n(1uURa}v`T*+~x^l3kE z%R%Dd?_+fI`K95NC2V5;X@-I0*TI(cIy#>?WA(L>-j}U$W$lQqT3oeZ#N=2u1d+*p zPm;~Vl+Gg{JaU3-Bf%(9>7J90vQcFDk+do(pyP zBDV=1?P9_`N}Fyv`+43LXV(%uRuhM>#9RGc1Yn`8D4G9PGv6%fxbit0d$p!G7bAhi zAuR+Z^!W2_Tj6M1y6+gl7d#ff6GjKHTOAavtiue>x+iz%}~&&Te=9LJ24yDTtCVZZ4@TL*XO=HsmnAd zt*|gQKPi2MB7^y-f@_7jdH|{{o}^i|aC8$#E-o9+hX zYRlOy_VQGw1X%|{eDjiuvP)ORod`Z?Uxk7<-XA>7D8)x3#+9d6lryD&{2=DgaN|>S zME}hHck))6P1&zIQ+nFa91I@x*gjnQsGw!IZo}X`BovKbRT59qa(5@N-Nn#ucRP?y ziPc`3l=C@!!249!RVG*%Ym8haNv*zQW|=-Dv=zK9NFpn`vcvfPqap`ezDLO&{d1`M z*KCWqt>TQyXr;U;Jqb!cr>p%Z^&-5&;5xiM+qeHumE_XjJVLu&j9CCG2TZqmVSjYQ zN5#gKO=?;*r87d6%l#H?iWFpLVy~mJEgH6pVT(O-wO`pb3BefGMD+}3?)SqTn3q;&SCDsnhX;9Po=R%m$F*D~Ux!$sMD<1=1_*SjIxLgDX+Jho$e^Rku1 z^$IoVfA!1N*0EVo0`GkS)q0f&;^ORsrw?`Gi*FC;$AH}d{d2b98uN>e6W%&kO6Jjx zikv7=)yq^sQXiIGLaXeOxK&(hv%)z0oA?ABT%8U?dEFLJAfrExTxF3LBUC1hp-(j(Co>)voigcU3&m7Ade)Y zC+2)BExZ#{_B_1a9oZHYUZ*P=v4y+Ho`pt5f@J+KtPc|G=$5f!gFuzlcMbMq`RLo> z+lGh)3onh4Cx4fkwAjkHMfk4{hQEn&p|5)AW+0n|4*0T6kpBK8=|(!#s(V@~k6Kq^4(VY)EJ}y6nS8}rAGNs>Jg{Fq zg8y+H?Idjq+GzAn?k2RIZ*%?!Kf5ggo7bO4Rx{f0yh+X8%)!fvlXjM(aerD`yG2rk zU^9d@LDB7PqHlgM6@c~;=u`|8Y5@V0OV*GsikK1DqKuB9C3aMk3oh%<;)%Wc}e?A zS^AUoW9!G0Eq+jtttXig@*#19$5^&hSt$<|d4~_Y9nd)@uf^vWzI0xN|HzbXZVYgP zAS_lIhl<-TV|VCpr5`Kh>FG)cm-tVnDuj6)*m+IFJbr(^9_yPY!92RIeMeVcxTWwQv=APF(6c(hv>U3!5aHq#Gu0+qsX?zr5y1(UvcDbCzz6EEec6O zw)P=a$uiHnDvufKv3JnMmFeYfq>D(XqcCBFQg54E4Duw_ov3}yGuoEAfx)jG_j+|byUR^}go;8X5arfXyl#qhF{nE zvwIq^C*bd~cn6&hq9tSn8ywIqfx}%v`F1F*jvv(SoB@wrB+v)}C{LCYa7JVe#@p-v z09j#n)KpSl zs@^k4jfxKA2I&d8Vz~_)4&IdndOp$*b)Gb6fq!-fLqIS!O)#|4f2SkB+rW&8z1N`- zdC=t}9P6HA04ovC58DA8D86-TmY|8VLqME9-G?zy)#U4zz9|0k3ko2ZN>8C?S6FGsUp%Is!7~u3AG1;mGAMr( ztx9w{=iZ~i3^VZgUJDB5e8MjA$NQa6_QYL4I4zpg(yxSc0DPliN|Zdp4_|JA0=zYZ zIg$VGuF5cSsu3tP`#h2f&OsPMYJ6}2^U;BU%p3q>HuV5)En(jD)GJuQQdO-9Oj>m zPSwfzwK2&U!^g6Km^)x&OvtrC-`_jRdEhJ3{xE;f3!vO8z&EP zi-UwTjEF`Di5BJi24Hbt*!3hs;(0g>Ee^}OA+OYbo4a$zhxiN~1B|NZhYbNf(%Bkl zxc==n&RMhnZCI4}W>RLb{m(N(VBphrL*M3`ps~vaq?@T-vXj{GbBzLy}&AZ7;vv{>|qLIFnOCNkNJ}&pEsrs8lNIrZc<=ZHi{?;_9H^c>!!kN=SjiLo0Sa3uwWnZS$+ed zTWBA>o~_y0(n{$T8*?h=;*rA_fapc~`0}{fZBC z@-MP{lRS}q>Csxk4Y}AaWB(o%4IV7UaJ|f0XE)RUn~%d+)xO33K|?Zp|MPO%Jy>W1 z4e6Fi+4b`8bg5@7teL3Y2;zmbB>Jj6UO4eQJOJ@A9vZ_{rHuQh&O zj+aC9sEFf35;5%X&L9G!poPc+BQn-KSYJ1A0}z8-%#w)JIZ}%UVuiNp5P8YK{V)Fz zMFWqRgcrTEKOXtdjJLzk=qi^i()r!{l<^@XVGzuJ2zr0;F5%W}NJuPYY~3rN!I1Tj z0xmf@+me`2#}R(aDC{N>VI%~tyGG2Gxh7tidb)2w?G~!JXY&m;*JW@8#5r};?S*a&T)ZvGlNekX0C;QCP4~zG0Y##bbwqC^C2imnOi9!G)8>#SmuArb&1)qR;ZVy`DnzQ*hl0>5%(k3*xt6!_jhe6 z*5^jT>LdHZQeIot=MN`$106#?ETk}Z6u5W;&{k?V@t6av4x%h%H()LZj zS5Ad)Uk~!7tM9XyzE-P+1RTlvg878CIlsuvYDvA_i4^sZo@rMYaJ~qNi~M2W=vF86 zh3OU@q-JB#<;-o;JKR`*2~>+^A2XeV%Skv}So}C);<8;|ORltv{>_1x zHOf^Rs{R4_hAEL}%_Z&y?iJV8(?$i6eqE=w;&)@&Mk}A@<}GpY48p|hKP1k^;+;FC zPj^NgeduauNV-P~$Utpn&S_yew9Kc*`h+fs@owdyPil9rvpXg?SQEZqcOH~eG}UDB z20luwynx-H^d_#--8(ESZ@v|GPyoeOv%6`tv>s{!w^qh!u%x(sFgO z>Tk=5ZVhU`x#dv+J4%;HfUwVYG6kC}ecmW=v0Ggjozp|2svDiF`_iv3SeYUbN~ZmA zT}~C4Xs5j1_}prBg|FN$a)${JD0t|g_eCi=_$}~iuL1?3h7H`_wj%$Y3q5$KyD||v zHa_ac0z{F!@g#u5kqW%w)h~enT6ty&tVp~7z^Fjur5Ps{=S|G${R)GmB{vTQc1&cA z1!MtiVO_%l0#9E*qbQ;+M+YSUFc{m^W11lWJRL49V3d+yj0~I4T>?^I(DwZbdxVBng9?5?W+BoM961}n4HNJ|z~@F}x^O5RG58^bZ~+CPV8rk}4k&@ZV(tQ< z@C94Q5a#onkVp#f5s;PlLhw)lBm1*B=m|w(@s$Ypnr@pY6#v8TDJqNs)uP7(fT-S}ke5$Tz?m5V_=VxrlupE?w||t}mx?bV-(bqno!ke> zA?J#Q(y^+O66Fyo(t!k?zVg7^C*C~3IiEX0F?cnDkwxDZ4xAmz*>Q6=>bL345! z?X#=NK-zoEP1~BWfN;jRa0obs&^f?mykg!$a~f8U0gDhAxz+|m7-{G@y^0^9zZ0P> z4_}aTqM?it)Y%Z=C@F67P;leBrlQ*c;3Y_81$=Tui%9S|S#fPzz{L-A9S0IoV--4< zCqbAr=!FVlz(b%XvMNZxRVVe~3X`)U1bhd;lOdAv1Q!4du=z}q7nHb?Xj$4ak`#}A z3wk8~P-UV5B{~uWDM=t_)>=z%Oc&Tv?0X+@NnTO zHA$+LNsOH2h~;#uoP_6ZFyWFA_|=dVD5*1>Kn3rhhziKaA`)Y+RtLjF*z_v0OEqho z5ez1d0evH4t4W3SYKM+G zY6)zQB@;TCwb7u$d_WEfxP>rMfkTFDc3%E}W*N~lLIR5DW^Bhr(4Tue<=w8|_dbn63FVBS77;g;Y<`&lEA1TTa+*Oo1@UFm4dW@K(TZ zGX=bnrOMegfwxUDA_0D?B~Q!cekgF%zK4%7ID?-f;z(ek;W)4DX8=k$3E}Q;2FMwh zVYaO)p9-}CF7kv{CL1~#D8p~PJl7y=CUAPGc23ArXYyyFbY?6%HSKMUH*6dqeY-{% zt$Z4%O#t^dX@p_cSVYKBV$fZBJ-y5T12{pgqIP8j@Ku1JvFP(_vS-j+)-d<7>J#at zK3e2r6osk>U?c+Aw`+Nj+%zbhtC_V@&A?;}AS~j9;A+sE;?vK%KBCQqvy;VVCpoxT z^|N;0h`?_QNhlRvA_UPlNa)>41xDb-(tBHRvT@kAE!HW7>G-JdS;g=~V0eP5U8ZUo zCn|~sN)j%gczKSPYb>H_ST9CS~5xP0e(lrx+~GvNS|%Gh^Y7;Cl%6lnqX z6JB~1vpfLH{L}>DR)rYCEb)`t@>J1otBcLMeny5QPVvZK?psQgA;C3-Pw&V=wRKt} zATSQzdCdq)~+UgumA}Vh>8Z3DQh6XxD!qRoD5K?#-hP3sR;S0 zoDpxC>>!SvV~@&{qOaj)%41j3I0*8q(MO*O*8x5=^rPpj;U7yKBzUUxStNiVp5nq@)bj z$K^CCTPb17;1T=|BVLiv9}8YWylIv8EdhceB6YVFN5;5Q25P>+%r` zj$Z6lL25UEr524LxASm0?a%hAiLch?FfutfUAH{1DXXk|`*>kmVar@JKHpRr2j0Y! z9As8F_%SgnYw9N(ZVvsWCsXw0u2N?m0Hqwx2OipdXaE_t1h-hEkXm7D`z^?^|3m#o zTinA_j9NLd$B!7$t&=RBPX4?OE5kkAYYAIK!une){oHVK9|B_to=)!?=dtMUx8oCn zXD5-6gHMB@sZ-2$5ZP`;HT6gyjddIl+dw5ZayAKKMMaH6d_MrSini*sL1~}7d$u&W zYSk#DoQJ8Bm)rh@NSl`DI5(^KPtD}Y(dalY7O2GlEenSg*N*@eNLc@P&i^jo?>pc7uk+utX03-=dtbAkJrDQ3 z_r3SNE`Q1vYfLFc*xpg22`e{Wh1<5eEC@t)u+zcPB^LG^b*9hLiZOKJX!3DE9~AM~ zo~_6%VJF4XE;ak*`cQ%v(7@X7OdS!Ry;dmDTP5-)!YG}4H8cOI-c}`yAG*A@BPvy< zetMfHkz+|dU_JvVW=NH|n0P)V;L+6(Jd;qeEEDFef&fFR>ko=LW5+AlqAX?VVk2FMG;J}$-RfISe#G)^P&3HoDs8g2nko` zh-j@C69f=^a#S{4wVw)N;=%=vEjYS9$XXwg{AGmMg{Z0NX)<8b}?P zmvlyKPkucBB~p)H6M{ZoobcN-k5E|{wDBZ=(8q?atYlAk|2}R$eXu6glnK2wYRjF- zUCA3s3P@cHuuOFN;HRp>PhwJR&@GY8CLm%*tz!WJmn1R{crgTwEQz#=%3+Lvk;{7f zi?B1bk>?W%pf>UW)+A5dv#<)$_jAvu7fx3jhJ4R3Cj^gC?}zLD>Ui`RDuoP2v{$-1 z(`aBE!b>m=LBqnXahTv0<`&(GS;3=Yl+b}q<;bazVF0q2owpPq3YtZjFGUY3D`TG* zDTth-2J5;4c<-?@xD&{XJ=$I8!WiYx_hhhfQ_z3!BEcXGa$1Y#bvh9k!IEx5XJ|JG6^|0BxsK6?*jroCrH15I8E65 zXb*=QhnlG93h4{F@yf0W*-M~eB~x_Ep5bf(TH16)3T$#*e$tgCV;f+c*r5ef#IFJ3 z27_%dp8N@ok)?$7Np_NBh}n+?2Pg=Hjm!mThlf7*DlK8z5a<1>%;vCJ9lw{zL2*Vx z-+i82(+d$C)|`xh0(&E~oE}1;`b@jw6(%6fcEl18i6lF6jmXQin#hf7+1|2k^Jk_H zBP6yAX)NCavRsd$7N6*??5lRRY}v2VX~0-UD~ zP+NmlFBa}d7C1_-r(Vk*Mc&xL6VmM3Xy@W?h3KgXpi)$HMZA{oL*4pA{FlwnmxX1Bl2VttEq=TZw*6H0}3s z&y1WLJP@V6)Tdr6LQqDaEzjkLJ96KR2K6Lc43KIc4{F zXL*g-)<7Bj79SQPaCc7v2x33m;8XsY9@=YN(bAA86B#At{;=-CnpGP=Zib;KB3Gu{LCb}OITJ$5W8 zaF_6w!rEmkk`y(8amN73Me9#ZvV@%4vbc3>%uazd3$g(A&U7{+FVEna#MK3#c$B}{ zhrUj?8lJ^*z&K?#3Q>2U?(l&&^I#(+D@=)LeSlVVN<+m6Iyg3D3Zmuwp^7^H5Y6TZ z^dB^}K@chegSj!$!r2VziLDsR^t@K?9M?Ep1gMCB%A<1}UvV{FS>$zz{P>R-@hTC; z5hNU{tO#LzjM7r3mh-bZLPF5W(XyAt-4AiZ>huMUF|WMNA<69zGzd6dAOJyv^aA3o z&5B`U;#j*UX+F&_T*N!v78NILfixn3>Vam5?~@4^aok=9Gq;9xo3i_kYX8-%T)K?- zP)otHHSPW6EucnqozV?R6@2Iy%r26WpN<9CHz4S!2fGO{jx}L`zWWuF78>fhbXye~ zz1XBkC?lQ3ww^zkbP+GhU}_#k5+RH6N-Lp^Jspk*TT+>%wZH5#=!E?&{Y)n zR+Oc2wQ!LT;=L8Svc}>9NMQ-Ah%SwQk)8iWnxDa{KM?f1612L-zqLF3V8Y`FCi0k7x7+wCnCr){?LfL#pWUb)yq7@-5eV7dV4 zK8+|`PV=iLRD$D1XdItOx)QG+#QVo$^D8A|bF314mqN99#>fk3X^p46wJqadON1&q zVkmJx%$Dv#cNdZq^RPJOe@232(D1u?%M}mNSGaDN5}m&d^H5(HhqZjyDVMtmJZE}P zDTN^lRIp$Lf7geBzyd6ZsI7=e5zCH6Q})#W>^-~Rca?zy;sjd7bcu_o1v_Aw9XPFK ze$NXah-?vQFqPRz2g0XFm6?greLrx8kO80HaZ<7}ME}AY0E5}doL%j9(jQ_TmZ$v7 zcCX{mnb(^gP$3SDr?UtHru$|@VkjC&Zpat+qZqi!Xg@}}%|Xl0Xq9jaUP@QkRRUGi zrSp@mx7}o|#sJ5pi9e%39cn3&TpF;Jg?Hq*PS9IrDmO-5r@o@e+1; z+@7opQ(NDv6nI^3U{2`;KoA{*kwnx+r+K8kcz6-zY_*IQC-dT7%1DFpF-T`#V^@^O z*+;p>fTi|gsN!TVAC;mEP0`hy)kKW9Xu$&8k<*Ub@9r~q_A-C+?{xE7l;+T5z^)6Vruk2sY14Z1q?nIS5(2&f| z-Vg3x9}Qk7={)gOqrvtQ*pGo)$H2&fCq7YM50Vz9`2mgZG3l59u5B^&At^{En~P;N z8wd5HrAtP(7=7&XxPeoGbrkJU-DoY$*~SM3GHz+OCg}LvYTSXP$yDz+LdNw!K@i8^ zMm9jQ%;R}NAm(usY|}d&1HlUEPBfq~v>6EYCWX*}JQ3)$OYMpD|F~m#p-*Fz2pyLj z+=hZlsusZb(#m`k*9jnEo1luCV7W|J18WHlEuZX_o#CpEl|A#>8q+M!P-YQTmeIohfqD{n60iFp1OJ%=LqcQ!0&jJR!U z@x~e=m&r7d$1~RCsH0;?_p`@t?G;+gh`uOCq7K}^9_`x_p#Cc$lBJbX1ba7505(HL zQDh>taR0t}(&Lsi>GsmQ5!GXMa-pP?(Bq|xvuI|5GQIdcz&H+zw*QX5xS=dfv)aCH zDds_k)h$v3t@4zWhFMy*-&-^o~6QGvpV5`3Slb1ouf zcrM@zonrlaloLxW-!Kyhpn-*jG_xNC1X4BSmdD;eewv#zqQWBX|7->Yoz*bCeF1Xf z*>MC<%?Q*XU;OGsp)nyFad^; z+oMT?9k1%SDOad~W9K7Ors+Z8B@MLdac{1(aY0@u&&@aueX>C+bg;_?i!?lK6ax5B zQ;b{{t|Fj>87sLVq$15v7Y*bLL6~oI<7ZQA-j>FUXy3&qRMR)zt56(TFw&^t#{&U zlj761Cz6o$h~x_cp=lej(dPj=>(Hl!ZHzW*q62F>s_DL5!5} zPe#aZ8KjE9uW0h0!5M5z`tP2;oBtHv>N?IJUl)Mk)>^~{Ce!xy7MQEOJ6}vW` z!cA!DW)pacqBYsU`hb|>jxECK9Pz8RPajkol0_e&s}g>oxmEdb6ZGCye2+I8H@wK2 z-jFf**<+689Wz0K4t<5#*#$V>b$FQ1E5hX|aZXfH452P$7)Xk#D#7P~?QzhVaM!e% z!*6nnMBXBr5&#IPeh5`&$k*YhVQ77JFElgQ3 zinQ}gMn}!iVK!%9$|t9^cv2g-JO(FcEq_R>XrKw2Q-ch_ehq-g?Q~YP&rp$;pQ6?( zi}u#AGhg3*l{zDu)2WS)U~ORLq`AHl=6jS{PGHWrE-YoF>_h=6G%Zupd#sSviAUfx z%G>pj^V=LE&p0((m6|bAxP>guwhYJ4{BqqU+#J4Q_fwkyVKua`DJ18T@_5kKDmzrl zZ^jTD$Ao4N6S@P@%^ju_y9%yJ5(2h2P9|0OAn1k8uyvOKJ#ZP?)T!XT11;3-m0BgPz zwY6js(A(0+V4FNUIl)U0^*LihV#Xk~PJT?_;AArDiLEs;j~$9ng8Ey6WB6;l=?pIT zsG(iYhkXZ)RER%}sKriMbDifdH3;EpS`8(r0y^~@n z4)Z%_(sWRgQ&HFzw`dy~(bXzwB?DT7$2fpgSlny6*Go;$C}Jkf9g#3Zb;r}e?B$*8WmKvZXl^D$+>bu#mJ9{6?mph z{MKt4LwzFP7%>xN4)kHfhOQo9x_RT!1O-vUvFt7qkgS z7=Nl`0`XiOS56}~xA{>kPoDyi!;Os8AdWQW10$AW(@%Rmynb7g(xQTZKwH8>$=s4& zB0Epx+M%MMI1LEGT^oqn@D9?oxFWxIjo>&*cGs0VywG{%-j1bNo;I7N1{mvg@ZLns zGm}O(v0>ahvIQfC<~}_mv8pmMyIc7W8ejB40->Cyw_3vt5<8@pu&}DV2dt1L&dMmS zv~jsZHJxD=>mAHH`L;msbgGFBe#)O8YPLtql@@vHch=c3wUu4c9F@qMkhYc5Cdt8S z+UzP<=k%H|%ITMrW5+_6??4M|%fcr$S?tX9DN`qg<)(3exL4>TzBos%$FRhNHcdZR z7%zA%Wp082?kag&U5pKOkaaC}72Pw;alDo<%9TvsJ3D*tt0Q$2l_U?*SbAr=qd)lK z_3jJ)v7Y?tzIAvp^PVaUhGi|f4*!G=bnVbJH(O7I5yo5gr}3BNL*q&4gva$d!~Rky zsKi|)k48~b6~PaSC*c%8V<1mVtc2w@PmK!JZUDGVP6EqDo29;zH0o2ySfW2HlWNr6L|G~bC#wcn{ z15;IiZYG?%R#=#3(#Bot8cPpZ5w&@tp^CN}T_TFFUG=`|>})_e>@Ek6 zu0R?~2lpF~MqUU)6d?@Arn8V1Ax^L{0Aegc`pbp+wi`yGTA_=cE(WaqtVLY|rxeH~_TW76gD50zw4Oc$ArKO{tH(x$OwYIqWp-X_9uX z?v$6;@D1|tIZ}ak;pY#I{h4!$8i!(jDqR~;8Z}{pWHj*bakR2-5qFsb08ozA;eP$Z z>wJ2nscU&iq0Bb~G{AP76DC9}Ya~6hDhS<5+lAHlS)eWr_NNx`3iWh@y=ClROF6{&%Rw zdGW@Q78(HeioXl(c8w?9hH?HFMx6;NKts6U1B>E#nah6AErtoYgn;`K$nOt>i|Gns zTI^??#%IX4Y*QLNVi~AN>DS%X*mz!xpsH;o9m1J6faW7FoRb=~qH3eIAJn31A3QG! z!vVsS5=f7S$YvKsBAE&LiLKcw(;WYOhi9U~1mW?|l)SQc*aHN%>tn~Y9p_Ku_S^@8 z!mmlLlOJQ6dS=#b#~}8YMz%-&^bMCnKiXiR{qWWO^{Atd{X4FCy2ZTrSLFpc!qG~) zFLLOnY+Xs$UyhdI`vMRZ}i1$*tm?O1OTJ3AYM_y0gTa zHxdJfZ(E;!9~}SRb20pXl>GMSK!NmKs)(b(yK+>@1Zb-1sn#l6NBlF@Bp#}ijhn0Y zZvt%;G0K7EV&YmedDoE3QD3m9T_P7vKCeMfG+nGT&l@ zAaL&4*NszFwn_%=4BKJ)B=tRiN~)v*mDA&Igkgm0v7ujlZ(_b>(K9SiH8Q|R;9MSH z%V!uZUrl0ollJiBV3c7gd2Yn-q>trLxX*ySjdf_dTv1zn2VE^xk~~vWl!^0vQZq_9 zS5Mea`2pQiJ9b*L^ba%ao%OOp1V5}DF?2GT8TuPDzJZU`@uaziN>)F{cX#b}ecIqv zc19j4b||=IBb{c_aC%X-=`T|p; z^Vp1$9=7>+Q7K^H+Jd!Bw{87riLaq6>QJsD$z?W-3rLM)E zn`uQI*O4u+J)XfKa&BY7hxyK=inhpHpqUf=5&dJAs|@>7hL$7kD@W?3HCspL$5Wy~ zPV>tn$Z*ps#YbdNj+dA(ZE`PPV3O%zGZ|(X&I{)z`y28sZbROW>xX1LeEm?dBAy~T zEkMlQ!InCp?X_R!Qyz=zA}hOm?b|Q)7rkGJqN%?x7#5~}-s|ni(CThO$FDCu8`yf> zeDWf;zK1u{zrjVhnU^~xmq>gA889aMv~P~^JHAwpkmf6=R<<08S=P#juh|H<R9JnVQ}APb{dhvYu+$ zkylKM9zE}td!SSQAng_b!2q_0-*NHq3mq{7tkARZHxKI;q|;<7P06zdr_SyiOxZlW z^mcq0GowH75zaWOF?MIxCHH#9B2|GvLP&lg!1>)#Q%xfKQu?=?;Azbb?)$ecBqs3N z?wr=qmPojHHdLlYf6o?^Cx4?COopv+OMX$}{>oQ9vh8sJyB*gJIQP#R?^u8AHCyL% z<31TOgTND6oaCo?C(1>8@2jwW70Il1N{XB5$E@wV+u?WVaCTAZ9{b$5mF>pPwCmkE z6`A!E*45qz+@UH1y?WCs^MkCrh9R3T>sw$)7xI<~SIMi~=T)!SB*0`;RE5#YqU4!p zCAZ&W?)0ON6)h04vUJ?2W_@3k7)TP5uRt$9PQE8YHX7aG79>_Rn5$MUBg=8;^5mOU zv{rb7>qhIj$nTwv5fy{H2UVdgNmtuNp(Li-y_R-vf_Z6 z>VxNBN^X1S7y7Ss*m*uC+1x+H;u26j4C{AoK5f7-vlmgo?t2yf`~}(MD2tvjLpj=H z)HUNERL_wun1M%}WGCTWMpD!Z3aWq|nEn+`Gi%AyBO!@fkQ`cywig?fR4Le$I1gc} zO{IG5&d(01SjjHE+TTaZpX*c_cRei^Bo$|po2VVpPo4qK^VB+t&gs|99L8O78&6*cfj|Oun_*(Z@9Xv=R|YtFcf{Z_Z*NOwi4&FL)#tO; zr&nI)t9ps|cCwg7PIo8OJzA~we>3vR>pi==By(hsWN@hLIIMX;(M*La)x?-5zV8Z; zcYj3`adRc1bGOhf+`f9=IjP(0n!|FqLw(eN#A+4ef>6C51b@}v*Z zqj5aC<4@9g`qZXU-G{N$<+XOW*oY{?!Uy|wQ{QXc1)f3uu@e0hnM z|Aq4n9<58?EM2_-eWqK=ua(nji$_E?y86?ptqm^oH*UEPRz8}0G>p~wY2JG3)94D- zGV4+QR-1G)#r4YUc)f7BY{QqLCt;SX?Czftgxbd6{>GcFdzc7W--KB}j||UgH0jKIa8?JmNg}UX zsN8mF%>7jIl_p;}g-|FU*YNBoLY~9P@rDrF(gc}qVeF?h# z0uTN22A10Ei($s@>|}>m-qK@_Q1rNL^_<0(m!HCZTAtUNXx5Z>;(u~R-_GAOmJv^@ zMGtam>pM+6Tt3WK*3yjc5q{TLJHWyduT8QUgB_VK0N{&j7bO|vHJ2F zDU!NO5;d%%Aw#Xqji8KA4polYf22AfNv%x0jrub{II8hcA5!|JJf$@36Nie%oAH2o zDJJDiDOf|xg&um!30!xoY5&1Xz6(rOa_Wn|pep(V?3!m13v#v|=557UN27CI0s8qZ zck#}-6P1j1iB5q)CBA1~kzIE#A%C|Xe4hLm@p9L?(S!M_NH&CUoU{W(geKdaG!L8G z1rZ>Z*K*lpT{vDaHF!=7bK|t8aPo5V`@fXX%1d^m9Nt#C{An2u?=&)Bvi#`w=H~@; z7_E@E#g{I@$Oe3sR6+bkNQdbun&*~@XmeP`L z8(V+CxOu0lo|Z?q^Ge!-@+ZsE+2g&4VvLTjeQBFnQIgvE{-yi1Pu;Lxv~Q3>s>%$% z^LBhCo75uRx97|;6*w`z{qFlx^4)Oq(0gQV#w-uz$yW;A5(SSpcHhKXqCpVTW444w ze{*at!WBJtT57834ek7RU-0C0<#y1vkRYG+|b`!+E-!9wE7!VO_N}QPjgi$g~(YzbBIrB)Ts%dpfqbbZy ziu3bP;(aRf*zFy<>T-zp;%w>G%&pb71QUZA z-|o((Ax2DPcKh?YWiC||-?f&NA!AY9?%on)g=l2ryj}BaImgg;m}}8D(Y6l=0|OhY zN@`Lu>2b5CxwPUZmTlBT65ipa2Yap5Y!d9_FL=aIOp9nVQ?(0^Q)!V} zjJJA<=(!qOJDIxP8hV0O`>C zTUd#qJ__nudMD}wJW@8SNc3hOxkvO|QWSMBryi%#(BAnkEGX|r`xAN&sdUoG*2LVD z)M7NF*Mzwqo=~&H{{~fg%`e5m7W>-G>xWH`#-I=)^ePtG5anB#mj3gS$cB+?pW-_O z7q`B+TT$&W-j{^JsJB}J(9VF#3tLOHd%Vh>1bo2z4To91wnR-j-k z#37?iL?_5py+M??uUevzth^Ylk>6WuqQh|P#I1!}H^r?J_vYO~+`6rswX$AJ{_(0>y@|{O~s@tcV0h)3$m@w1x2UxBMjQX=J6-8C{I^#pcJA4U0-;wGy9y z!i&9^(IID5iQpa&F8?v(jWItS;O1Va{?d+1@e|`6bBc#ihVBFVf#~7Ys>g2^8u0@W4knW9(#HhY64)S`A z_or)jAL>3SrTI+ZRQKz}&96qKY3$#kL^E6FOqwpQRHTlN-(+011br#bEi&`D$!BSh z0m(@y;EBLj3oxbj>Cm~gsxaxgDF}En!FQKupW9?{#6_?sq>Y z^vZ&;QXCt-%TCpbfij*wI&GNYwt8r*ZD!5a^Di7#9uS91go_=1%58zaZC|D6B-v&1x*k3N)L(e?mspHU3M<)szmOQp>S2`ig zejl`^iy5odv|{~5%<>g)xzIg{2SM#N0r=pc7;1}&RQKU|7;YbIarYixR+*1*NYSZ9 z$jzXnw}gT(CL#D(SlT{37G3=wx1L}jbxncjvr4x&@3_5*^|E}@-@Vl>soy)YcE8?spf*gpXlQJf*cS`Z)yYLf2st#! zR^vQsz`HrSyTX0pie~+*%0bNMFs@l*PsH zf&;c=|f@H>4a9NqP{N| zH?`z4`UHm4Xiax+&-GcxWPRSHv^)E<`Rh0Rp(qQ)e*i^`Ihpns{o#Mo8X!ka0Cm-j zmH0IEZV9;DcZJ*AdHvT^uqI90saKFV=0Ym^!;&GxGv-xWgKeA%XvOIOb+Wp0((*#r zt9a5(k~k(A0+H*VVYiA8LbqLfB8Wd2^jclvRU;amQ`%7kB3-*g&7WPGUo(pmr{xme zZc{&@5$DB9)l++%YD{Fn&X2Rtq)y6|Sr5@V!UT?^Mb*&y! z{_f}Y8bmA`1b}xaQb>Jm2pSpqXF03ilkz|Im$=ox*ZNZefZrlef6M@N`Tth1`q%IO zQxkyS8diVo5$f{ak^Zh|_0KpAe?U-Iz5fB{e<)o2GuXf09zb>Dzc=b%6|epo>0fV8 z`adE4O#$nlaeniG{xQ%0fb&NY>z`5nwKb?S9RK6K{;rVq&p7|un&tn1^G7l3e~Uta zA~F5J>``6#PyDWbq5Re6zq`!enXx}+d-q@RWdC0A i?>^wSNBv`V@PF{H`q~(nzl#y0UOXsHz3p<$8$ literal 29733 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 WQiY*~0|6jG|NRcp-_SHY0RIQdK%_SS diff --git a/docs/internals/object-graph.png b/docs/internals/object-graph.png index f0d1f0001c45f92665ced9673093a597034191da..503f1649985dab410fe58fa0953dea44b716e1a2 100644 GIT binary patch literal 99968 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