Merge pull request #9422 from ThomasWaldmann/deletion-fixes
Some checks are pending
Lint / lint (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (Haiku, false, haiku, r1beta5) (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.7) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
CodeQL / Analyze (push) Waiting to run

prune/delete fixes
This commit is contained in:
TW 2026-03-01 14:01:11 +01:00 committed by GitHub
commit b38107c506
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 29 additions and 8 deletions

View file

@ -36,6 +36,8 @@ class DeleteMixIn:
logger_list = logging.getLogger("borg.output.list")
for i, archive_info in enumerate(archive_infos, 1):
name, id, hex_id = archive_info.name, archive_info.id, bin_to_hex(archive_info.id)
# format early before deletion of the archive
archive_formatted = format_archive(archive_info)
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.
@ -47,7 +49,7 @@ class DeleteMixIn:
deleted = True
if self.output_list:
msg = "Would delete: {} ({}/{})" if dry_run else "Deleted archive: {} ({}/{})"
logger_list.info(msg.format(format_archive(archive_info), i, count))
logger_list.info(msg.format(archive_formatted, i, count))
if dry_run:
logger.info("Finished dry-run.")
elif deleted:

View file

@ -179,29 +179,32 @@ class PruneMixIn:
archives_deleted = 0
uncommitted_deletes = 0
pi = ProgressIndicatorPercent(total=len(to_delete), msg="Pruning archives %3.0f%%", msgid="prune")
for archive in archives:
for archive_info in archives:
if sig_int and sig_int.action_done():
break
if archive in to_delete:
# format_item may internally load the archive from the repository,
# so we must call it before deleting the archive.
archive_formatted = formatter.format_item(archive_info, jsonline=False)
if archive_info in to_delete:
pi.show()
if args.dry_run:
log_message = "Would prune:"
else:
archives_deleted += 1
log_message = "Pruning archive (%d/%d):" % (archives_deleted, to_delete_len)
archive = Archive(manifest, archive.id, cache=cache)
archive = Archive(manifest, archive_info.id, cache=cache)
archive.delete()
uncommitted_deletes += 1
else:
log_message = "Keeping archive (rule: {rule} #{num}):".format(
rule=kept_because[archive.id][0], num=kept_because[archive.id][1]
rule=kept_because[archive_info.id][0], num=kept_because[archive_info.id][1]
)
if (
args.output_list
or (args.list_pruned and archive in to_delete)
or (args.list_kept and archive not in to_delete)
or (args.list_pruned and archive_info in to_delete)
or (args.list_kept and archive_info not in to_delete)
):
list_logger.info(f"{log_message:<44} {formatter.format_item(archive, jsonline=False)}")
list_logger.info(f"{log_message:<44} {archive_formatted}")
pi.finish()
if sig_int:
raise Error("Got Ctrl-C / SIGINT.")

View file

@ -400,3 +400,19 @@ def test_prune_split_no_archives():
assert keep == []
assert kept_because == {}
def test_prune_list_with_metadata_format(archivers, request):
# Regression test for: prune --list with a format string that requires loading
# archive metadata (e.g. {hostname}) must not fail when archives are deleted.
# The bug was that format_item() was called after archive.delete(), causing
# Archive.DoesNotExist when the formatter tried to lazy-load the archive.
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
cmd(archiver, "create", "test1", src_dir)
cmd(archiver, "create", "test2", src_dir)
# {hostname} is a "call key" that triggers lazy loading of the archive from the repo.
# With the buggy code this would raise Archive.DoesNotExist for the pruned archive.
output = cmd(archiver, "prune", "--list", "--keep-daily=1", "--format={name} {hostname}{NL}")
assert "test1" in output
assert "test2" in output