diff --git a/docs/misc/prune-example-interval.txt b/docs/misc/prune-example-interval.txt index 0106a69f2..e96e0a0ba 100644 --- a/docs/misc/prune-example-interval.txt +++ b/docs/misc/prune-example-interval.txt @@ -21,20 +21,20 @@ borg prune \ --keep-weekly 4 \ --keep-monthly 5m \ --keep-yearly 2 \ - --since '2026-06-04 16:00' + --from '2026-06-04 16:00' ``` The `--keep-*` options reflect the intended retention policy exactly. Note the different wording in the retention policy for weekly and yearly archives: They aren't *interval*-based, but *count*-based. -Another important detail here is `--since`. Without it, intervals would be +Another important detail here is `--from`. Without it, intervals would be calculated relative to the actual start time of `prune` - in this case 16:12. Since your backups are always created at 16:00, this 12-minute shift would move the cutoff point of intervals and could cause archives near the boundary to unexpectedly fall outside the expected time window. -By specifying `--since '2026-06-04 16:00'`, all intervals are anchored to the +By specifying `--from '2026-06-04 16:00'`, all intervals are anchored to the intended reference time (16:00), not the moment `prune` happens to run. This ensures stable and predictable retention behavior, independent of when `prune` actually runs. @@ -48,7 +48,7 @@ Archives kept by the `--keep-daily` rule are marked by a "d" to the right, archives kept by the `--keep-weekly` rule are marked by a "w" to the right, archives kept by the `--keep-monthly` rule are marked by a "m" to the right, archives kept by the `--keep-yearly` rule are marked by a "y" to the right, and -archives kept by the `--since` rule are marked by a "x" to the right. +archives kept by the `--from` rule are marked by a "x" to the right. Calendar view @@ -91,25 +91,25 @@ List view 5. 2026-05-29 5. 2025-12-31 6. 2026-05-28 -2026-06-04 is additionally kept due to `--since`. +2026-06-04 is additionally kept due to `--from`. Notes ----- The current day's archive is always kept, because `create` ran after the date -given with `--since`. For `prune`, it's as if this archive doesn't exist (yet). +given with `--from`. For `prune`, it's as if this archive doesn't exist (yet). 2026-06-03 was skipped, so no archive can be kept with `--keep-daily` for that day. Other than with a *count*-based policy, no compensation is made for an *interval* like `--keep-daily 1w`, so the rule simply keeps one archive fewer. -2026-05-28 16:00 is exactly one week before `--since`. Since `create` always +2026-05-28 16:00 is exactly one week before `--from`. Since `create` always runs after 16:00, the archive created on 2026-05-28 is kept, too. Without -`--since`, Borg would cut off at 2026-05-28 16:12 instead, which would likely -mean that the archive created on 2026-05-28 would be pruned. `--since` ensures +`--from`, Borg would cut off at 2026-05-28 16:12 instead, which would likely +mean that the archive created on 2026-05-28 would be pruned. `--from` ensures that 2026-05-28 is consistently kept. If you want it consistently pruned, try a -later reference time, e.g. `--since '2026-06-04 23:59:59'`. +later reference time, e.g. `--from '2026-06-04 23:59:59'`. 2026-05-31 is considered not only by `--keep-daily`, but by `--keep-weekly` and `--keep-monthly`, too. The archive is effectively kept by `--keep-daily`, diff --git a/docs/usage/general/date-time.rst.inc b/docs/usage/general/date-time.rst.inc index 8a44ab672..db1fcca60 100644 --- a/docs/usage/general/date-time.rst.inc +++ b/docs/usage/general/date-time.rst.inc @@ -20,7 +20,7 @@ days (e.g. ``7d``), hours (e.g. ``8H``), minutes (e.g. ``30M``), or seconds The ``borg prune`` ``--keep-*`` retention options accept either a plain count (e.g. ``--keep-daily 7``, keeping up to 7 daily archives) or a time interval (e.g. ``--keep-daily 7d``, keeping one daily archive per day within a 7-day window). -When using interval-based retention, ``--since`` may be specified to set the +When using interval-based retention, ``--from`` may be specified to set the reference timestamp for the interval (defaults to the current time). Please note that Borg treats months (e.g. ``12m``) as fixed 31-day periods diff --git a/docs/usage/prune.rst b/docs/usage/prune.rst index cedcc92a6..9fecbef8d 100644 --- a/docs/usage/prune.rst +++ b/docs/usage/prune.rst @@ -27,7 +27,7 @@ The ``--keep-*`` options accept either a **count** (e.g. ``--keep-daily 7``) or a **time interval** (e.g. ``--keep-daily 7d``). A count keeps up to *N* archives per period (e.g. the last 7 daily archives), while an interval keeps one archive per period within that time span (e.g. one daily archive per day in the -last 7-day window). When using intervals, you may also specify ``--since`` to +last 7-day window). When using intervals, you may also specify ``--from`` to set the reference timestamp for interval calculation. :: @@ -55,7 +55,7 @@ set the reference timestamp for interval calculation. $ borg prune -v --list --dry-run --keep-daily=7d # Same as above, but with a fixed reference timestamp: - $ borg prune -v --list --dry-run --since 2025-12-01T00:00:00+02:00 --keep-daily=7d + $ borg prune -v --list --dry-run --from 2025-12-01T00:00:00+02:00 --keep-daily=7d # Keep the last 14 archives using `--keep`: $ borg prune -v --list --dry-run --keep 14 diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index 1e371e04b..1911b864a 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -107,8 +107,8 @@ PRUNE_QUARTERLY_13WEEKLY = PruningRule("quarterly_13weekly", quarterly_13weekly_ PRUNE_QUARTERLY_3MONTHLY = PruningRule("quarterly_3monthly", quarterly_3monthly_period_func) PRUNE_YEARLY = PruningRule("yearly", pattern_period_func("%Y")) -# Fake rule used to indicate archives skipped by --since -PRUNE_SINCE = PruningRule("skip", unique_period_func()) +# Fake rule used to indicate archives skipped by --from +PRUNE_FROM = PruningRule("skip", unique_period_func()) PRUNING_RULES = [ PRUNE_KEEP, @@ -182,18 +182,18 @@ class PruneMixIn: # Archives to keep along with the rule that ensured them being kept keep = {} - since = getattr(args, PRUNE_SINCE.key) + from_timestamp = getattr(args, PRUNE_FROM.key) candidate_archives = archives - if since is not None: - base_timestamp = since + if from_timestamp is not None: + base_timestamp = from_timestamp - # `--since` is a prefilter: Archives made at or after this time are kept by default. They are not considered + # `--from` is a prefilter: Archives made at or after this time are kept by default. They are not considered # for pruning at all and thus won't falsely occupy an active retention period. for archive in archives: - if archive.ts < since: + if archive.ts < from_timestamp: break - keep[archive] = KeepResult(rule=PRUNE_SINCE, idx=len(keep)) + keep[archive] = KeepResult(rule=PRUNE_FROM, idx=len(keep)) candidate_archives = archives[len(keep) :] else: base_timestamp = datetime.now().astimezone() @@ -366,7 +366,7 @@ class PruneMixIn: policy. It accepts a count or a time interval for retention (e.g. ``10`` or ``7d``, ``4w``). With a count it keeps at most that many recent archives; with an interval it keeps all archives created within - that time window. When ``--since`` is given together with an interval + that time window. When ``--from`` is given together with an interval retention, the interval is measured backwards from that timestamp instead of from the current time. See ``Date and Time`` docs for exact INTERVAL format. @@ -380,14 +380,14 @@ class PruneMixIn: period, e.g. one per day or one per month until the retention count is met). With a retention interval, they keep one archive per period within that time span (e.g. at most one per day in a span of seven - days, even if some days had none) -- measured from ``--since`` if given, + days, even if some days had none) -- measured from ``--from`` if given, otherwise from the current time. Specifying a count of ``-1`` (or the word ``all``) means no limit. A zero count or zero-length interval keeps nothing. - The ``--since`` option restricts pruning to archives older than the given + The ``--from`` option restricts pruning to archives older than the given TIMESTAMP. Archives made at or after this timestamp are kept unconditionally - as a pre-filter. When ``--since`` is used together with interval-based + as a pre-filter. When ``--from`` is used together with interval-based ``--keep-*`` options (e.g. ``--keep-daily 7d``), the interval is measured backwards from the given timestamp rather than from the current time. Count-based retention does not count the unconditionally kept archives. @@ -417,10 +417,10 @@ class PruneMixIn: older history gradually thins out with time. For example, ``--keep-daily 7d --keep-weekly 4w --keep-monthly 6`` keeps an archive per day for the past week, per week for the past month, and - one per month for six months after that. Combine this with ``--since`` + one per month for six months after that. Combine this with ``--from`` to align time windows to calendar boundaries rather than the exact moment you run prune for more predictable behavior of coarser rules: - ``--keep-daily 7d --keep-weekly 4w --since $(date +%F)``. + ``--keep-daily 7d --keep-weekly 4w --from $(date +%F)``. Count-based retention keeps archives less bound to time. For instance, ``--keep-yearly 3`` retains 3 yearly archives however far back they @@ -476,9 +476,9 @@ class PruneMixIn: "Some keys are always present. Note: JSON can only represent text.", ) subparser.add_argument( - "--since", + "--from", metavar="TIMESTAMP", - dest=PRUNE_SINCE.key, + dest=PRUNE_FROM.key, type=timestamp, action=Highlander, help="only consider archives older than this for pruning", diff --git a/src/borg/testsuite/archiver/prune_cmd_test.py b/src/borg/testsuite/archiver/prune_cmd_test.py index 4d14f3cd4..396a7f342 100644 --- a/src/borg/testsuite/archiver/prune_cmd_test.py +++ b/src/borg/testsuite/archiver/prune_cmd_test.py @@ -154,14 +154,14 @@ def test_prune_repository_example_interval(archivers, request, backup_files): "prune", "--list", "--dry-run", - "--since=2026-06-04T16:00:00+00:00", + "--from=2026-06-04T16:00:00+00:00", "--keep-daily=1w", "--keep-weekly=4", "--keep-monthly=5m", "--keep-yearly=2", ) - # 2026-06-04 is kept unconditionally by the --since prefilter. + # 2026-06-04 is kept unconditionally by the --from prefilter. assert re.search(r"Keeping archive \(rule: skip #1\):\s+backup_2026-06-04", output) daily_kept = [ @@ -530,14 +530,14 @@ def test_prune_keep_int_or_interval(archivers, request, backup_files, keep_arg): archiver = request.getfixturevalue(archivers) cmd(archiver, "repo-create", RK_ENCRYPTION) dt = datetime(2023, 12, 31, 23, 59, 59, tzinfo=timezone.utc) - # Two archives at the --since boundary prove the inclusive timestamp check. + # Two archives at the --from since boundary prove the inclusive timestamp check. _create_archive_dt(archiver, backup_files, "test-1", dt) _create_archive_dt( archiver, backup_files, "test-2", dt - timedelta(microseconds=999999) ) # Would be pruned if `secondly`-rule was active. _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(seconds=1)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(seconds=1, microseconds=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: skip #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: keep #1\):\s+test-2", output) assert re.search(r"Keeping archive \(rule: keep #2\):\s+test-3", output) @@ -553,7 +553,7 @@ def test_prune_keep_secondly_int_or_interval(archivers, request, backup_files, k _create_archive_dt(archiver, backup_files, "test-2", dt - timedelta(seconds=1, microseconds=999999)) _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(seconds=2)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(seconds=2, microseconds=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: secondly #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: secondly #2\):\s+test-2", output) assert re.search(r"Would prune:\s+test-3", output) @@ -570,7 +570,7 @@ def test_prune_keep_minutely_int_or_interval(archivers, request, backup_files, k _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(minutes=2)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(minutes=3)) _create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(minutes=3, microseconds=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: minutely #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: minutely #2\):\s+test-2", output) assert re.search(r"Would prune:\s+test-3", output) @@ -588,7 +588,7 @@ def test_prune_keep_hourly_int_or_interval(archivers, request, backup_files, kee _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(hours=2)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(hours=3)) _create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(hours=3, microseconds=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: hourly #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: hourly #2\):\s+test-2", output) assert re.search(r"Would prune:\s+test-3", output) @@ -606,7 +606,7 @@ def test_prune_keep_daily_int_or_interval(archivers, request, backup_files, keep _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=2)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=3)) _create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=3, microseconds=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: daily #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: daily #2\):\s+test-2", output) assert re.search(r"Would prune:\s+test-3", output) @@ -624,7 +624,7 @@ def test_prune_keep_weekly_int_or_interval(archivers, request, backup_files, kee _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=14)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=21)) _create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=21, microseconds=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: weekly #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: weekly #2\):\s+test-2", output) assert re.search(r"Would prune:\s+test-3", output) @@ -642,7 +642,7 @@ def test_prune_keep_monthly_int_or_interval(archivers, request, backup_files, ke _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=62)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=93)) _create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=93, microseconds=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: monthly #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: monthly #2\):\s+test-2", output) assert re.search(r"Would prune:\s+test-3", output) @@ -661,7 +661,7 @@ def test_prune_keep_13weekly_int_or_interval(archivers, request, backup_files, k _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=182)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=273)) _create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=273, microseconds=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: quarterly_13weekly #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: quarterly_13weekly #2\):\s+test-2", output) assert re.search(r"Would prune:\s+test-3", output) @@ -680,7 +680,7 @@ def test_prune_keep_3monthly_int_or_interval(archivers, request, backup_files, k _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=275)) _create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=275, microseconds=1)) # 275d is the interval from dt to the oldest kept monthly archive - output = cmd(archiver, "prune", "--list", "--short", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--short", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: quarterly_3monthly #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: quarterly_3monthly #2\):\s+test-2", output) assert re.search(r"Would prune:\s+test-3", output) @@ -698,7 +698,7 @@ def test_prune_keep_yearly_int_or_interval(archivers, request, backup_files, kee _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=730)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=1095)) _create_archive_dt(archiver, backup_files, "test-5", dt - timedelta(days=1095, microseconds=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: yearly #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: yearly #2\):\s+test-2", output) assert re.search(r"Would prune:\s+test-3", output) @@ -714,7 +714,7 @@ def test_prune_keep_daily_all(archivers, request, backup_files, keep_arg): _create_archive_dt(archiver, backup_files, "test-1", dt - timedelta(days=1)) _create_archive_dt(archiver, backup_files, "test-2", dt - timedelta(days=2)) _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=3)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: daily #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: daily #2\):\s+test-2", output) assert re.search(r"Keeping archive \(rule: daily #3\):\s+test-3", output) @@ -730,7 +730,7 @@ def test_prune_keep_flat_all(archivers, request, backup_files, keep_arg): _create_archive_dt(archiver, backup_files, "test-2", dt - timedelta(microseconds=2)) _create_archive_dt(archiver, backup_files, "test-3", dt - timedelta(days=3)) _create_archive_dt(archiver, backup_files, "test-4", dt - timedelta(days=33333)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), keep_arg) + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), keep_arg) assert re.search(r"Keeping archive \(rule: keep #1\):\s+test-1", output) assert re.search(r"Keeping archive \(rule: keep #2\):\s+test-2", output) assert re.search(r"Keeping archive \(rule: keep #3\):\s+test-3", output) @@ -824,7 +824,7 @@ def test_prune_int_rolling_schedule_oldest_retention(): assert archives[-1].ts.strftime("%m-%d") == "01-31" -def test_prune_since_prefiltered_archives_ignored_in_pruning(archivers, request, backup_files): +def test_prune_from_prefiltered_archives_ignored_in_pruning(archivers, request, backup_files): archiver = request.getfixturevalue(archivers) cmd(archiver, "repo-create", RK_ENCRYPTION) dt = datetime(2024, 6, 6, 12, 0, 0, tzinfo=timezone.utc) @@ -833,7 +833,7 @@ def test_prune_since_prefiltered_archives_ignored_in_pruning(archivers, request, _create_archive_dt(archiver, backup_files, "test-b", dt - timedelta(hours=1)) _create_archive_dt(archiver, backup_files, "test-c", dt - timedelta(days=1)) - output = cmd(archiver, "prune", "--list", "--dry-run", "--since", dt.isoformat(), "--keep-daily=1") + output = cmd(archiver, "prune", "--list", "--dry-run", "--from", dt.isoformat(), "--keep-daily=1") # 'test-b' is kept, meaning 'test-a' was entirely skipped for pruning consideration. # They would otherwise have occupied the same period.