Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/borg/archiver/prune_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,29 @@ def do_prune(self, args, repository, manifest):
'At least one of the "keep-within", "keep-last", '
'"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", '
'"keep-weekly", "keep-monthly", "keep-13weekly", "keep-3monthly", '
'or "keep-yearly" settings must be specified.'
'"keep-yearly", or "keep-all" settings must be specified.'
)

# --keep-all is an alias for --keep-last=<infinite> and must not be
# used together with any other --keep-... option, because that would be
# misleading. Enforce this at argument parsing/validation time.
keep_all = args.secondly == float("inf")
if keep_all:
if any(
(
args.minutely,
args.hourly,
args.daily,
args.weekly,
args.monthly,
args.quarterly_13weekly,
args.quarterly_3monthly,
args.yearly,
args.within,
Comment on lines +151 to +159
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that secondly is missing here!

)
):
raise CommandError("--keep-all cannot be combined with other --keep-... options.")

if args.format is not None:
format = args.format
elif args.short:
Expand Down Expand Up @@ -320,6 +340,13 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser):
action=Highlander,
help="number of secondly archives to keep",
)
subparser.add_argument(
"--keep-all",
dest="secondly",
action="store_const",
const=float("inf"),
help="keep all archives (alias of --keep-last=<infinite>)",
)
subparser.add_argument(
"--keep-minutely",
dest="minutely",
Expand Down
72 changes: 71 additions & 1 deletion src/borg/testsuite/archiver/prune_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ...constants import * # NOQA
from ...archiver.prune_cmd import prune_split, prune_within
from . import cmd, RK_ENCRYPTION, src_dir, generate_archiver_tests
from ...helpers import interval
from ...helpers import interval, CommandError

pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA

Expand Down Expand Up @@ -272,6 +272,76 @@ def __repr__(self):
return f"{self.id}: {self.ts.isoformat()}"


def test_prune_keep_all(archivers, request):
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
# create a few archives with distinct seconds so 'secondly' rule can keep all
_create_archive_ts(archiver, "a1", 2025, 1, 1, 0, 0, 0)
_create_archive_ts(archiver, "a2", 2025, 1, 1, 0, 0, 1)
_create_archive_ts(archiver, "a3", 2025, 1, 1, 0, 0, 2)

# Dry-run prune: nothing should be pruned, all should be kept under 'secondly' rule
output = cmd(archiver, "prune", "--list", "--dry-run", "--keep-all")
assert re.search(r"Keeping archive \(rule: secondly(?:\[oldest\])? #\d+\):\s+a1", output)
assert re.search(r"Keeping archive \(rule: secondly(?:\[oldest\])? #\d+\):\s+a2", output)
assert re.search(r"Keeping archive \(rule: secondly(?:\[oldest\])? #\d+\):\s+a3", output)
assert "Would prune:" not in output
assert "Pruning archive" not in output
output = cmd(archiver, "repo-list", "--format", "{name}{NL}")
names = set(output.splitlines())
assert names == {"a1", "a2", "a3"}

# Real prune with --keep-all should also not delete anything
cmd(archiver, "prune", "--keep-all")
output = cmd(archiver, "repo-list", "--format", "{name}{NL}")
names = set(output.splitlines())
assert names == {"a1", "a2", "a3"}


def test_prune_keep_all_mutually_exclusive_with_others(archivers, request):
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
# create a single archive
_create_archive_ts(archiver, "x1", 2025, 1, 1, 0, 0, 0)
# Using --keep-all together with any other keep option must error out
output = cmd(archiver, "prune", "--keep-all", "--keep-daily=1", exit_code=CommandError().exit_code, fork=True)
assert "--keep-all cannot be combined" in output


def test_prune_keep_all_mutually_exclusive_with_within(archivers, request):
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
_create_archive_ts(archiver, "x1", 2025, 1, 1, 0, 0, 0)
output = cmd(archiver, "prune", "--keep-all", "--keep-within", "1d", exit_code=CommandError().exit_code, fork=True)
assert "--keep-all cannot be combined" in output


def test_prune_keep_all_and_keep_last_2(archivers, request):
# Problem: --keep-all, --keep-secondly and --keep-last=X use the same variable
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
# Create three archives with distinct seconds
_create_archive_ts(archiver, "c1", 2025, 1, 1, 0, 0, 0)
_create_archive_ts(archiver, "c2", 2025, 1, 1, 0, 0, 1)
_create_archive_ts(archiver, "c3", 2025, 1, 1, 0, 0, 2)

# Dry-run prune: with conflicting options, keep-all dominates
output = cmd(archiver, "prune", "--list", "--dry-run", "--keep-all", "--keep-last=2")
# Expect all kept under 'secondly' rule, nothing would be pruned
assert re.search(r"Keeping archive \(rule: secondly(?:\[oldest\])? #\d+\):\s+c1", output)
assert re.search(r"Keeping archive \(rule: secondly(?:\[oldest\])? #\d+\):\s+c2", output)
assert re.search(r"Keeping archive \(rule: secondly(?:\[oldest\])? #\d+\):\s+c3", output)
assert "Would prune:" not in output

# Dry-run prune: with conflicting options, keep-all dominates
output = cmd(archiver, "prune", "--list", "--dry-run", "--keep-last=2", "--keep-all")
# Expect all kept under 'secondly' rule, nothing would be pruned
assert re.search(r"Keeping archive \(rule: secondly(?:\[oldest\])? #\d+\):\s+c1", output)
assert re.search(r"Keeping archive \(rule: secondly(?:\[oldest\])? #\d+\):\s+c2", output)
assert re.search(r"Keeping archive \(rule: secondly(?:\[oldest\])? #\d+\):\s+c3", output)
assert "Would prune:" not in output


# This is the local timezone of the system running the tests.
# We need this e.g. to construct archive timestamps for the prune tests,
# because borg prune operates in the local timezone (it first converts the
Expand Down
Loading