Skip to content

Commit 3c204c3

Browse files
prune: fix Archive.DoesNotExist when using --list, fixes #9416
format_item() can trigger lazy loading of archive metadata (e.g. hostname, username, size) from the repository. Previously it was called after archive.delete(), which caused Archive.DoesNotExist for pruned archives. Fix: call formatter.format_item() early, before any deletion takes place. Also added a test.
1 parent f3ac2e0 commit 3c204c3

File tree

2 files changed

+26
-7
lines changed

2 files changed

+26
-7
lines changed

src/borg/archiver/prune_cmd.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,29 +179,32 @@ def do_prune(self, args, repository, manifest):
179179
archives_deleted = 0
180180
uncommitted_deletes = 0
181181
pi = ProgressIndicatorPercent(total=len(to_delete), msg="Pruning archives %3.0f%%", msgid="prune")
182-
for archive in archives:
182+
for archive_info in archives:
183183
if sig_int and sig_int.action_done():
184184
break
185-
if archive in to_delete:
185+
# format_item may internally load the archive from the repository,
186+
# so we must call it before deleting the archive.
187+
archive_formatted = formatter.format_item(archive_info, jsonline=False)
188+
if archive_info in to_delete:
186189
pi.show()
187190
if args.dry_run:
188191
log_message = "Would prune:"
189192
else:
190193
archives_deleted += 1
191194
log_message = "Pruning archive (%d/%d):" % (archives_deleted, to_delete_len)
192-
archive = Archive(manifest, archive.id, cache=cache)
195+
archive = Archive(manifest, archive_info.id, cache=cache)
193196
archive.delete()
194197
uncommitted_deletes += 1
195198
else:
196199
log_message = "Keeping archive (rule: {rule} #{num}):".format(
197-
rule=kept_because[archive.id][0], num=kept_because[archive.id][1]
200+
rule=kept_because[archive_info.id][0], num=kept_because[archive_info.id][1]
198201
)
199202
if (
200203
args.output_list
201-
or (args.list_pruned and archive in to_delete)
202-
or (args.list_kept and archive not in to_delete)
204+
or (args.list_pruned and archive_info in to_delete)
205+
or (args.list_kept and archive_info not in to_delete)
203206
):
204-
list_logger.info(f"{log_message:<44} {formatter.format_item(archive, jsonline=False)}")
207+
list_logger.info(f"{log_message:<44} {archive_formatted}")
205208
pi.finish()
206209
if sig_int:
207210
raise Error("Got Ctrl-C / SIGINT.")

src/borg/testsuite/archiver/prune_cmd_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,19 @@ def test_prune_split_no_archives():
400400

401401
assert keep == []
402402
assert kept_because == {}
403+
404+
405+
def test_prune_list_with_metadata_format(archivers, request):
406+
# Regression test for: prune --list with a format string that requires loading
407+
# archive metadata (e.g. {hostname}) must not fail when archives are deleted.
408+
# The bug was that format_item() was called after archive.delete(), causing
409+
# Archive.DoesNotExist when the formatter tried to lazy-load the archive.
410+
archiver = request.getfixturevalue(archivers)
411+
cmd(archiver, "repo-create", RK_ENCRYPTION)
412+
cmd(archiver, "create", "test1", src_dir)
413+
cmd(archiver, "create", "test2", src_dir)
414+
# {hostname} is a "call key" that triggers lazy loading of the archive from the repo.
415+
# With the buggy code this would raise Archive.DoesNotExist for the pruned archive.
416+
output = cmd(archiver, "prune", "--list", "--keep-daily=1", "--format={name} {hostname}{NL}")
417+
assert "test1" in output
418+
assert "test2" in output

0 commit comments

Comments
 (0)