mirror of
https://github.com/borgbackup/borg.git
synced 2026-04-27 17:18:52 -04:00
Add --json flag to borg prune for structured output (#9512)
Some checks failed
Lint / lint (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / security (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
CI / asan_ubsan (push) Has been cancelled
CI / native_tests (push) Has been cancelled
CI / vm_tests (Haiku, false, haiku, r1beta5) (push) Has been cancelled
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Has been cancelled
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Has been cancelled
CI / vm_tests (OpenBSD, false, openbsd, 7.8) (push) Has been cancelled
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Has been cancelled
CI / windows_tests (push) Has been cancelled
Some checks failed
Lint / lint (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / security (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
CI / asan_ubsan (push) Has been cancelled
CI / native_tests (push) Has been cancelled
CI / vm_tests (Haiku, false, haiku, r1beta5) (push) Has been cancelled
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Has been cancelled
CI / vm_tests (OmniOS, false, omnios, r151056) (push) Has been cancelled
CI / vm_tests (OpenBSD, false, openbsd, 7.8) (push) Has been cancelled
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Has been cancelled
CI / windows_tests (push) Has been cancelled
prune: add --json option, fixes #9222 Enable programmatic extraction of prune/keep decisions via structured JSON output, instead of parsing log message text. Follows the repo-list --json pattern: outputs a single JSON object with repository, encryption, and archives array. Each archive includes pruned (bool), rule, and rule_number fields.
This commit is contained in:
parent
8eff8270c0
commit
6bc41465a4
2 changed files with 89 additions and 12 deletions
|
|
@ -8,6 +8,7 @@ from ._common import with_repository, Highlander
|
|||
from ..constants import * # NOQA
|
||||
from ..helpers import ArchiveFormatter, interval, sig_int, ProgressIndicatorPercent, CommandError, Error
|
||||
from ..helpers import archivename_validator
|
||||
from ..helpers import json_print, basic_json_data
|
||||
from ..helpers.argparsing import ArgumentParser
|
||||
from ..manifest import Manifest
|
||||
|
||||
|
|
@ -168,8 +169,11 @@ class PruneMixIn:
|
|||
keep += prune_split(archives, rule, num, kept_because)
|
||||
|
||||
to_delete = set(archives) - set(keep)
|
||||
logger.info("Found %d archives.", len(archives))
|
||||
logger.info("Keeping %d archives, pruning %d archives.", len(keep), len(to_delete))
|
||||
if not args.json:
|
||||
logger.info("Found %d archives.", len(archives))
|
||||
logger.info("Keeping %d archives, pruning %d archives.", len(keep), len(to_delete))
|
||||
if args.json:
|
||||
output_data = []
|
||||
list_logger = logging.getLogger("borg.output.list")
|
||||
# set up counters for the progress display
|
||||
to_delete_len = len(to_delete)
|
||||
|
|
@ -178,29 +182,50 @@ class PruneMixIn:
|
|||
for archive_info in archives:
|
||||
if sig_int and sig_int.action_done():
|
||||
break
|
||||
# format_item may internally load the archive from the repository,
|
||||
# get_item_data/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 args.json:
|
||||
archive_data = formatter.get_item_data(archive_info, jsonline=True)
|
||||
else:
|
||||
archive_formatted = formatter.format_item(archive_info, jsonline=False)
|
||||
if archive_info in to_delete:
|
||||
pi.show()
|
||||
if not args.json:
|
||||
pi.show()
|
||||
archives_deleted += 1
|
||||
if args.dry_run:
|
||||
log_message = "Would prune:"
|
||||
else:
|
||||
log_message = "Pruning archive (%d/%d):" % (archives_deleted, to_delete_len)
|
||||
manifest.archives.delete_by_id(archive_info.id)
|
||||
archives_deleted += 1
|
||||
if args.json:
|
||||
archive_data["kept"] = False
|
||||
archive_data["deleted_archive_number"] = archives_deleted
|
||||
else:
|
||||
log_message = "Keeping archive (rule: {rule} #{num}):".format(
|
||||
rule=kept_because[archive_info.id][0], num=kept_because[archive_info.id][1]
|
||||
)
|
||||
if (
|
||||
rule, num = kept_because[archive_info.id]
|
||||
log_message = "Keeping archive (rule: {rule} #{num}):".format(rule=rule, num=num)
|
||||
if args.json:
|
||||
archive_data["kept"] = True
|
||||
archive_data["keep_rule"] = rule
|
||||
archive_data["kept_archive_number"] = num
|
||||
if args.json:
|
||||
if (
|
||||
args.output_list
|
||||
or not (args.list_pruned or args.list_kept)
|
||||
or (args.list_pruned and archive_info in to_delete)
|
||||
or (args.list_kept and archive_info not in to_delete)
|
||||
):
|
||||
output_data.append(archive_data)
|
||||
elif (
|
||||
args.output_list
|
||||
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} {archive_formatted}")
|
||||
pi.finish()
|
||||
if archives_deleted > 0:
|
||||
if not args.json:
|
||||
pi.finish()
|
||||
if args.json:
|
||||
json_print(basic_json_data(manifest, extra={"archives": output_data}))
|
||||
if archives_deleted > 0 and not args.dry_run:
|
||||
manifest.write()
|
||||
self.print_warning('Done. Run "borg compact" to free space.', wc=None)
|
||||
if sig_int:
|
||||
|
|
@ -295,6 +320,14 @@ class PruneMixIn:
|
|||
action=Highlander,
|
||||
help="specify format for the archive part " '(default: "{archive:<36} {time} [{id}]")',
|
||||
)
|
||||
subparser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Format output as JSON. "
|
||||
"The form of ``--format`` is ignored, "
|
||||
"but keys used in it are added to the JSON output. "
|
||||
"Some keys are always present. Note: JSON can only represent text.",
|
||||
)
|
||||
subparser.add_argument(
|
||||
"--keep-within",
|
||||
metavar="INTERVAL",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
import re
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
|
|
@ -416,3 +417,46 @@ def test_prune_list_with_metadata_format(archivers, request, backup_files):
|
|||
output = cmd(archiver, "prune", "--list", "--keep-daily=1", "--format={name} {hostname}{NL}")
|
||||
assert "test1" in output
|
||||
assert "test2" in output
|
||||
|
||||
|
||||
def test_prune_json(archivers, request, backup_files):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
cmd(archiver, "repo-create", RK_ENCRYPTION)
|
||||
cmd(archiver, "create", "test1", backup_files)
|
||||
cmd(archiver, "create", "test2", backup_files)
|
||||
prune_result = json.loads(cmd(archiver, "prune", "--json", "--dry-run", "--keep-daily=1"))
|
||||
assert "repository" in prune_result
|
||||
assert "encryption" in prune_result
|
||||
assert len(prune_result["repository"]["id"]) == 64
|
||||
archives = prune_result["archives"]
|
||||
assert len(archives) == 2
|
||||
kept = [a for a in archives if a["kept"]]
|
||||
pruned = [a for a in archives if not a["kept"]]
|
||||
assert len(kept) == 1
|
||||
assert len(pruned) == 1
|
||||
assert kept[0]["name"] == "test2"
|
||||
assert kept[0]["keep_rule"] == "daily"
|
||||
assert kept[0]["kept_archive_number"] == 1
|
||||
assert "deleted_archive_number" not in kept[0]
|
||||
assert pruned[0]["name"] == "test1"
|
||||
assert pruned[0]["deleted_archive_number"] == 1
|
||||
assert "keep_rule" not in pruned[0]
|
||||
assert "kept_archive_number" not in pruned[0]
|
||||
for archive in archives:
|
||||
assert "name" in archive
|
||||
assert "id" in archive
|
||||
assert "time" in archive
|
||||
assert "kept" in archive
|
||||
|
||||
|
||||
def test_prune_json_list_pruned(archivers, request, backup_files):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
cmd(archiver, "repo-create", RK_ENCRYPTION)
|
||||
cmd(archiver, "create", "test1", backup_files)
|
||||
cmd(archiver, "create", "test2", backup_files)
|
||||
prune_result = json.loads(cmd(archiver, "prune", "--json", "--dry-run", "--list-pruned", "--keep-daily=1"))
|
||||
archives = prune_result["archives"]
|
||||
assert len(archives) == 1
|
||||
assert archives[0]["name"] == "test1"
|
||||
assert archives[0]["kept"] is False
|
||||
assert archives[0]["deleted_archive_number"] == 1
|
||||
|
|
|
|||
Loading…
Reference in a new issue