diff --git a/src/borg/repository.py b/src/borg/repository.py index 422a514007..7a9707e534 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -287,6 +287,19 @@ def __init__(self, id, repo): id = bin_to_hex(id) super().__init__(id, repo) + class PackNotFound(ErrorWithTraceback): + """Object with key {} is indexed to pack {}, but that whole pack is missing from repository {}.""" + + exit_mcode = 23 + + # a missing pack means the index is stale or more than one object was lost. + def __init__(self, id, pack_id, repo): + if isinstance(id, bytes): + id = bin_to_hex(id) + if isinstance(pack_id, bytes): + pack_id = bin_to_hex(pack_id) + super().__init__(id, pack_id, repo) + class ParentPathDoesNotExist(Error): """The parent path of the repository directory [{}] does not exist.""" @@ -836,7 +849,7 @@ def get_many(self, ids, read_data=True, raise_missing=True): reader = self._cached_pack_reader(entry.pack_id) except StoreObjectNotFound: if raise_missing: - raise self.ObjectNotFound(id_, str(self._location)) from None + raise self.PackNotFound(id_, entry.pack_id, str(self._location)) from None yield None else: yield reader.read(entry.obj_offset, entry.obj_size) diff --git a/src/borg/testsuite/repository_test.py b/src/borg/testsuite/repository_test.py index be43d3de1b..4f09999b29 100644 --- a/src/borg/testsuite/repository_test.py +++ b/src/borg/testsuite/repository_test.py @@ -253,6 +253,25 @@ def test_get_many_missing_id_yields_none(repo_fixtures, request): assert result[2] == repository.get(H(2)) +def test_get_many_missing_pack_raises_pack_not_found(repo_fixtures, request): + # get_many raises PackNotFound when a chunk's index entry points to a pack that is not in the + # store; with raise_missing=False it yields None instead. + objects = [(H(i), fchunk(b"payload-%02d" % i, chunk_id=H(i))) for i in range(2)] + with get_repository_from_fixture(repo_fixtures, request) as repository: + repository._pack_writer.max_count = 2 # one pack: {H0, H1} + for chunk_id, chunk in objects: + repository.put(chunk_id, chunk) + repository.flush() + pack_id = repository.chunks[H(0)].pack_id + repository.store_delete("packs/" + bin_to_hex(pack_id)) # delete the pack, keep its index entry + + with pytest.raises(Repository.PackNotFound): + list(repository.get_many([H(0)])) + with pytest.raises(Repository.PackNotFound): + list(repository.get_many([H(0), H(1)])) # one id's pack missing in a batch + assert list(repository.get_many([H(0)], raise_missing=False)) == [None] # raise_missing=False -> None + + def test_get_many_repeated_ids(repo_fixtures, request): # A dedup'd item repeats chunk ids, e.g. [A, A, B, C, B]. Each repeat returns the right bytes # and each pack is loaded once: the cached pack serves the later visits.