Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 5 additions & 18 deletions src/borg/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -2267,14 +2267,7 @@ def check(
# decryption per object and it needs the key, so read the key here if we do not have it yet.
# manifest_only=True: the other key source make_key uses is self.chunks, built just below.
if repair and self.key is None:
try:
self.key = self.make_key(repository, manifest_only=True)
except IntegrityError as err:
logger.warning(
f"Could not read the key ({err}), so the rebuild can not validate object headers: "
"a pack with a corrupt object header is indexed up to that header and the rest "
"of it is dropped."
)
self.key = self.make_key(repository, manifest_only=True)
if self.key is not None:
# the validator decrypts metadata slots, so it needs a RepoObj built from the key.
self.repo_objs = RepoObj(self.key)
Expand All @@ -2284,22 +2277,16 @@ def check(
self.chunks = build_chunkindex_from_repo(
self.repository,
slow_rebuild=repair,
# validate is None only without --repair and without the key: a corrupt object header then
# raises CorruptPack.
validate=validate,
# dropped content is a check finding, with or without --repair.
on_drop=self.note_dropped_objects,
# without a validator the rebuild can not resync past a corrupt object header. --repair
# drops the rest of that pack to get on with the repair; without --repair the rebuild
# raises, so an index missing objects that are still there can not make the check report
# them as gone.
drop_corrupt_tail=repair,
write_immediately=False,
)
# repository.chunks is a separate index, lazily built when repository.get() resolves a
# chunk location. It walks the same packs, so give it the same corrupt-header handling the
# rebuild above got - otherwise the check aborts at a header it just resynced past, halfway
# through its diagnosis. Dropping the rest of that pack stays a --repair action.
# repository.chunks is a separate index, built from the same packs on first use. Its rebuild
# uses the same validator as the rebuild above.
self.repository.chunkindex_validate = validate
self.repository.chunkindex_drop_corrupt_tail = repair
if self.key is None:
self.key = self.make_key(repository)
self.repo_objs = RepoObj(self.key)
Expand Down
31 changes: 25 additions & 6 deletions src/borg/archiver/check_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from ._common import with_repository, Highlander
from ..archive import ArchiveChecker
from ..constants import * # NOQA
from ..crypto.key import key_from_repository
from ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, Error, IntegrityError
from ..helpers import relative_time_marker_validator, yes, ArchiveFormatter, sig_int
from ..helpers.argparsing import ArgumentParser
from ..helpers.time import archive_ts_now, calculate_relative_offset
from ..repoobj import RepoObj, object_validator

from ..logger import create_logger

Expand Down Expand Up @@ -69,7 +71,10 @@ def do_check(self, args, repository):
try:
archive_checker.key = archive_checker.make_key(repository, manifest_only=True)
except IntegrityError:
pass # will try to make key later again
if args.repair:
# repair needs the key to validate the index rebuild. The manifest did not give it,
# so read it from the objects the chunk index lists.
archive_checker.key = key_from_repository(repository)
if args.format is not None:
format = args.format
else:
Expand All @@ -78,8 +83,18 @@ def do_check(self, args, repository):
# the repository check has finished, which can take hours.
ArchiveFormatter.validate_format(format)
if not args.archives_only:
validate = None # the object validator for the index rebuild, which only a repair does
if args.repair:
# ids=(): read the key from the manifest only. Chunk objects are found through the index,
# which this check may find corrupt.
key = archive_checker.key if not args.repo_only else key_from_repository(repository, ())
validate = object_validator(RepoObj(key))
if not repository.check(
repair=args.repair, max_duration=args.max_duration, max_age=max_age, repo_only=args.repo_only
repair=args.repair,
max_duration=args.max_duration,
max_age=max_age,
repo_only=args.repo_only,
validate=validate,
):
set_ec(EXIT_WARNING)
if sig_int: # repository check interrupted; skip the archive check
Expand Down Expand Up @@ -244,10 +259,14 @@ 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 rebuilds the repository
index from the packs if the index is corrupt, provided every pack is intact. If
any pack is corrupt, the repository check leaves the index and the packs untouched
and reports the corruption; salvaging a corrupt pack's still-intact objects is not
implemented yet (refs #8572).
index from the packs if the index is corrupt, provided every pack matches its
store hash. If any pack fails its store hash, the repository check leaves the
index and the packs untouched and reports it; salvaging the intact objects of
such a pack is not implemented yet (refs #8572). The rebuild authenticates
each object's header and metadata with the key, leaves an object that fails
this out of the index and reports it as an error. Repair therefore always
needs the key, ``--repository-only`` included, and aborts if the key can not
be read.

2. When checking the consistency and correctness of archives, repair mode might
remove whole archives from the manifest if their archive metadata chunk is
Expand Down
17 changes: 5 additions & 12 deletions src/borg/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,20 +892,15 @@ def build_chunkindex_from_repo(
fragments_only=False,
validate=None,
on_drop=None,
drop_corrupt_tail=False,
write_immediately=False,
init_flags=ChunkIndex.F_USED,
):
# fragments_only: build the index from the index/ fragments only, returning None if they cannot be
# read completely, and never write to the repo.
# validate: a repo object validator, handed to PackReader.iter_headers so the rebuild skips the
# objects that fail it.
# on_drop: a callable, handed to PackReader.iter_headers, which calls it once per place where
# the walk skips content. It only reports, it does not change what the walk does.
# drop_corrupt_tail: without a validator, index a pack with a corrupt object header up to that
# header and drop the rest of it, instead of raising, see PackReader.iter_headers.
# With neither of the two, a corrupt object header aborts the rebuild with CorruptPack: the
# index would be missing every object after it.
# validate: a repo object validator or None, passed to PackReader.iter_headers. With a validator,
# the rebuild skips the objects that fail it; without one, a corrupt object header raises CorruptPack.
# on_drop: a callable or None, passed to PackReader.iter_headers, called once per byte range the
# validating walk skips.
assert not (slow_rebuild and fragments_only)
assert not (fragments_only and write_immediately) # fragments_only never writes to the repo
# first, try to build a fresh, mostly complete chunk index from centrally stored index fragments:
Expand Down Expand Up @@ -1002,9 +997,7 @@ def build_chunkindex_from_repo(
pack_id = hex_to_bin(info.name)
reader = PackReader(repository.store, pack_id)
try:
for chunk_id, obj_offset, obj_size in reader.iter_headers(
validate=validate, on_drop=on_drop, drop_corrupt_tail=drop_corrupt_tail
):
for chunk_id, obj_offset, obj_size in reader.iter_headers(validate=validate, on_drop=on_drop):
num_chunks += 1
chunks[chunk_id] = ChunkIndexEntry(
flags=init_flags, size=0, pack_id=pack_id, obj_offset=obj_offset, obj_size=obj_size
Expand Down
Loading
Loading