mirror of
https://github.com/borgbackup/borg.git
synced 2026-10-12 02:11:36 +00:00
check: test a full --repair with a corrupt index and a corrupt pack, refs #10434
This commit is contained in:
@@ -1489,9 +1489,9 @@ class Repository:
|
||||
rebuilds and persists it (see ArchiveChecker.check and ArchiveChecker.finish), refs #10434. Packs
|
||||
are verified by the store hash, which is content-addressing rather than a MAC, so that check
|
||||
detects accidental corruption but not tampering; the rebuild therefore checks every object with
|
||||
validate, see below, refs #9901, #10026. If any pack is corrupt the index is not rebuilt, refs
|
||||
#10026. Pack ids found corrupt are kept in cache/checked-packs, refs #9696. That object is stored
|
||||
in the key's envelope, too, so check() needs the key (see set_key).
|
||||
validate, see below, refs #9901, #10026. With repo_only, if any pack is corrupt, the index is not
|
||||
rebuilt, refs #10026. Pack ids found corrupt are kept in cache/checked-packs, refs #9696. That
|
||||
object is stored in the key's envelope, too, so check() needs the key (see set_key).
|
||||
|
||||
A pack recorded corrupt fails the check, also on a partial run that stops before re-reaching
|
||||
it. The record clears at the check that finds the pack intact again or gone (removed by
|
||||
@@ -1810,7 +1810,8 @@ class Repository:
|
||||
else:
|
||||
# a full check's archives phase reads archive/item metadata (and file content with
|
||||
# --verify-data), so it repairs a corrupt pack holding such objects; warn rather than fail.
|
||||
logger.warning(f"{done} {mode} repository check, corrupt pack(s) found{so_far}.")
|
||||
deferred = "; index corrupt, the archives check rebuilds it from the packs" if index_deferred else ""
|
||||
logger.warning(f"{done} {mode} repository check, corrupt pack(s) found{deferred}{so_far}.")
|
||||
elif drops:
|
||||
# drops come from a repository-only rebuild only; no archives phase follows it.
|
||||
logger.error(
|
||||
|
||||
@@ -951,6 +951,32 @@ def test_check_repair_rebuilds_corrupt_index(archivers, request, mode, message):
|
||||
assert "archive1" in cmd(archiver, "repo-list") # and remains usable
|
||||
|
||||
|
||||
def test_check_repair_rebuilds_corrupt_index_with_corrupt_pack(archivers, request):
|
||||
# A corrupt index and a corrupt pack: a full --repair rebuilds and stores the index in the archives check.
|
||||
# The corrupt pack stays recorded corrupt, so a following check still fails on it, refs #10434.
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
check_cmd_setup(archiver)
|
||||
archive, repository = open_archive(archiver.repository_path, "archive1")
|
||||
with repository:
|
||||
bad_pack = sorted(info.name for info in repository.store_list("packs"))[0]
|
||||
name = f"packs/{bad_pack}"
|
||||
repository.store_store(name, corrupt(repository.store_load(name), -1))
|
||||
for info in repository.store_list("index"): # rot every index fragment
|
||||
name = f"index/{info.name}"
|
||||
repository.store_store(name, corrupt(repository.store_load(name), 0))
|
||||
output = cmd(archiver, "check", "-v", "--repair", exit_code=0)
|
||||
assert "corrupt pack(s) found; index corrupt, the archives check rebuilds it from the packs." in output
|
||||
archive, repository = open_archive(archiver.repository_path, "archive1")
|
||||
with repository:
|
||||
index_infos = list(repository.store_list("index"))
|
||||
assert index_infos # a fresh index was stored
|
||||
for info in index_infos: # each fragment's content matches its store hash name
|
||||
assert repository.store.hash(f"index/{info.name}", algorithm=STORE_HASH_NAME) == info.name
|
||||
output = cmd(archiver, "check", "-v", exit_code=1)
|
||||
assert "Checked 1 index files (0 errors)" in output
|
||||
assert f"Corrupt pack: {bad_pack}" in output
|
||||
|
||||
|
||||
def test_check_repair_walks_packs_once_for_corrupt_index(archiver, monkeypatch):
|
||||
# A full --repair with a corrupt index walks the objects of each pack once, for the index rebuild in the
|
||||
# archives check, refs #10434.
|
||||
|
||||
@@ -1938,6 +1938,37 @@ def test_check_repair_refuses_when_pack_corrupt(tmp_path):
|
||||
assert repository.check(repair=False) is False # index was not rebuilt; still corrupt
|
||||
|
||||
|
||||
def test_check_full_repair_defers_corrupt_index_with_corrupt_pack(tmp_path, caplog):
|
||||
# check(repair=True, repo_only=False) with a corrupt index and a corrupt pack succeeds and stores no
|
||||
# index: the archives phase rebuilds the index and repairs what the corrupt pack held, refs #10434.
|
||||
location = os.fspath(tmp_path / "repo")
|
||||
with Repository(location, exclusive=True, create=True) as repository:
|
||||
repository.put(H(1), fchunk(b"GOOD-CHUNK", chunk_id=H(1)))
|
||||
repository.flush() # seal a pack holding H(1)
|
||||
repository.put(H(2), fchunk(b"LOST-CHUNK", chunk_id=H(2)))
|
||||
repository.flush() # seal a separate pack holding H(2)
|
||||
with reopen(repository) as repository:
|
||||
bad_pack_id = repository.chunks[H(2)].pack_id
|
||||
bad_pack_name = "packs/" + bin_to_hex(bad_pack_id)
|
||||
data = bytearray(repository.store_load(bad_pack_name))
|
||||
data[-1] ^= 0xFF # rot the pack holding H(2): its content no longer matches its store hash name
|
||||
repository.store_store(bad_pack_name, bytes(data))
|
||||
for info in repository.store_list("index"): # rot every fragment
|
||||
name = f"index/{info.name}"
|
||||
idata = bytearray(repository.store_load(name))
|
||||
idata[0] ^= 0xFF
|
||||
repository.store_store(name, bytes(idata))
|
||||
with reopen(repository) as repository:
|
||||
index_before = {info.name for info in repository.store_list("index")}
|
||||
with caplog.at_level(logging.INFO, logger="borg.repository"):
|
||||
assert repository.check(repair=True, repo_only=False, validate=validate_any) is True
|
||||
assert "and 2 packs (1 errors)." in caplog.text
|
||||
assert "corrupt pack(s) found; index corrupt, the archives check rebuilds it from the packs." in caplog.text
|
||||
assert {info.name for info in repository.store_list("index")} == index_before # nothing stored
|
||||
assert bad_pack_name in [f"packs/{info.name}" for info in repository.store_list("packs")] # not dropped
|
||||
assert PackTracker.load(repository).corrupt_ids() == [bad_pack_id] # recorded corrupt
|
||||
|
||||
|
||||
@pytest.mark.parametrize("repo_only", [True, False])
|
||||
def test_check_repair_leaves_index_when_interrupted(tmp_path, caplog, monkeypatch, repo_only):
|
||||
# an interrupted repair (SIGINT before every pack is verified) must not rebuild the index from
|
||||
|
||||
Reference in New Issue
Block a user