Skip to content

Commit b9498ca

Browse files
KenKundertKen Kundert
andauthored
Allow timespan to be specified with common time units (#8626)
allow timespan to be specified with common time units, fixes #8624 Co-authored-by: Ken Kundert <ken@theKunderts.net>
1 parent 40df2f3 commit b9498ca

File tree

7 files changed

+200
-66
lines changed

7 files changed

+200
-66
lines changed

docs/usage/general/date-time.rst.inc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Internally, we store and process date and time as UTC.
1212

1313
.. rubric:: TIMESPAN
1414

15-
Some options accept a TIMESPAN parameter, which can be given as a
16-
number of days (e.g. ``7d``) or months (e.g. ``12m``).
17-
15+
Some options accept a TIMESPAN parameter, which can be given as a number of
16+
years (e.g. ``2y``), months (e.g. ``12m``), weeks (e.g. ``2w``),
17+
days (e.g. ``7d``), hours (e.g. ``8H``), minutes (e.g. ``30M``),
18+
or seconds (e.g. ``150S``).

src/borg/archiver/prune_cmd.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
logger = create_logger()
1919

2020

21-
def prune_within(archives, hours, kept_because):
22-
target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600)
21+
def prune_within(archives, seconds, kept_because):
22+
target = datetime.now(timezone.utc) - timedelta(seconds=seconds)
2323
kept_counter = 0
2424
result = []
2525
for a in archives:
@@ -241,10 +241,10 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser):
241241
series.
242242
243243
The ``--keep-within`` option takes an argument of the form "<int><char>",
244-
where char is "H", "d", "w", "m", "y". For example, ``--keep-within 2d`` means
245-
to keep all archives that were created within the past 48 hours.
246-
"1m" is taken to mean "31d". The archives kept with this option do not
247-
count towards the totals specified by any other options.
244+
where char is "y", "m", "w", "d", "H", "M", or "S". For example,
245+
``--keep-within 2d`` means to keep all archives that were created within
246+
the past 2 days. "1m" is taken to mean "31d". The archives kept with
247+
this option do not count towards the totals specified by any other options.
248248
249249
A good procedure is to thin out more and more the older your backups get.
250250
As an example, ``--keep-daily 7`` means to keep the latest backup on each day,

src/borg/helpers/parseformat.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,26 +126,38 @@ def positive_int_validator(value):
126126

127127

128128
def interval(s):
129-
"""Convert a string representing a valid interval to a number of hours."""
130-
multiplier = {"H": 1, "d": 24, "w": 24 * 7, "m": 24 * 31, "y": 24 * 365}
129+
"""Convert a string representing a valid interval to a number of seconds."""
130+
seconds_in_a_minute = 60
131+
seconds_in_an_hour = 60 * seconds_in_a_minute
132+
seconds_in_a_day = 24 * seconds_in_an_hour
133+
seconds_in_a_week = 7 * seconds_in_a_day
134+
seconds_in_a_month = 31 * seconds_in_a_day
135+
seconds_in_a_year = 365 * seconds_in_a_day
136+
multiplier = dict(
137+
y=seconds_in_a_year,
138+
m=seconds_in_a_month,
139+
w=seconds_in_a_week,
140+
d=seconds_in_a_day,
141+
H=seconds_in_an_hour,
142+
M=seconds_in_a_minute,
143+
S=1,
144+
)
131145

132146
if s.endswith(tuple(multiplier.keys())):
133147
number = s[:-1]
134148
suffix = s[-1]
135149
else:
136-
# range suffixes in ascending multiplier order
137-
ranges = [k for k, v in sorted(multiplier.items(), key=lambda t: t[1])]
138-
raise argparse.ArgumentTypeError(f'Unexpected interval time unit "{s[-1]}": expected one of {ranges!r}')
150+
raise argparse.ArgumentTypeError(f'Unexpected time unit "{s[-1]}": choose from {", ".join(multiplier)}')
139151

140152
try:
141-
hours = int(number) * multiplier[suffix]
153+
seconds = int(number) * multiplier[suffix]
142154
except ValueError:
143-
hours = -1
155+
seconds = -1
144156

145-
if hours <= 0:
146-
raise argparse.ArgumentTypeError('Unexpected interval number "%s": expected an integer greater than 0' % number)
157+
if seconds <= 0:
158+
raise argparse.ArgumentTypeError(f'Invalid number "{number}": expected positive integer')
147159

148-
return hours
160+
return seconds
149161

150162

151163
def ChunkerParams(s):
@@ -579,10 +591,10 @@ def validator(text):
579591

580592

581593
def relative_time_marker_validator(text: str):
582-
time_marker_regex = r"^\d+[md]$"
594+
time_marker_regex = r"^\d+[ymwdHMS]$"
583595
match = re.compile(time_marker_regex).search(text)
584596
if not match:
585-
raise argparse.ArgumentTypeError(f"Invalid relative time marker used: {text}")
597+
raise argparse.ArgumentTypeError(f"Invalid relative time marker used: {text}, choose from y, m, w, d, H, M, S")
586598
else:
587599
return text
588600

src/borg/helpers/time.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,28 @@ def calculate_relative_offset(format_string, from_ts, earlier=False):
119119
from_ts = archive_ts_now()
120120

121121
if format_string is not None:
122-
offset_regex = re.compile(r"(?P<offset>\d+)(?P<unit>[md])")
122+
offset_regex = re.compile(r"(?P<offset>\d+)(?P<unit>[ymwdHMS])")
123123
match = offset_regex.search(format_string)
124124

125125
if match:
126126
unit = match.group("unit")
127127
offset = int(match.group("offset"))
128128
offset *= -1 if earlier else 1
129129

130-
if unit == "d":
131-
return from_ts + timedelta(days=offset)
130+
if unit == "y":
131+
return from_ts.replace(year=from_ts.year + offset)
132132
elif unit == "m":
133133
return offset_n_months(from_ts, offset)
134+
elif unit == "w":
135+
return from_ts + timedelta(days=offset * 7)
136+
elif unit == "d":
137+
return from_ts + timedelta(days=offset)
138+
elif unit == "H":
139+
return from_ts + timedelta(seconds=offset * 60 * 60)
140+
elif unit == "M":
141+
return from_ts + timedelta(seconds=offset * 60)
142+
elif unit == "S":
143+
return from_ts + timedelta(seconds=offset)
134144

135145
raise ValueError(f"Invalid relative ts offset format: {format_string}")
136146

src/borg/testsuite/archiver/check_cmd_test.py

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,32 +58,80 @@ def test_date_matching(archivers, request):
5858

5959
shutil.rmtree(archiver.repository_path)
6060
cmd(archiver, "repo-create", RK_ENCRYPTION)
61-
earliest_ts = "2022-11-20T23:59:59"
62-
ts_in_between = "2022-12-18T23:59:59"
63-
create_src_archive(archiver, "archive1", ts=earliest_ts)
64-
create_src_archive(archiver, "archive2", ts=ts_in_between)
65-
create_src_archive(archiver, "archive3")
61+
create_src_archive(archiver, "archive-2022-11-20", ts="2022-11-20T23:59:59")
62+
create_src_archive(archiver, "archive-2022-12-18", ts="2022-12-18T23:59:59")
63+
create_src_archive(archiver, "archive-now")
6664
cmd(archiver, "check", "-v", "--archives-only", "--oldest=23e", exit_code=2)
6765

66+
output = cmd(archiver, "check", "-v", "--archives-only", "--oldest=1y", exit_code=0)
67+
assert "archive-2022-11-20" in output
68+
assert "archive-2022-12-18" in output
69+
assert "archive-now" not in output
70+
71+
output = cmd(archiver, "check", "-v", "--archives-only", "--newest=1y", exit_code=0)
72+
assert "archive-2022-11-20" not in output
73+
assert "archive-2022-12-18" not in output
74+
assert "archive-now" in output
75+
6876
output = cmd(archiver, "check", "-v", "--archives-only", "--oldest=1m", exit_code=0)
69-
assert "archive1" in output
70-
assert "archive2" in output
71-
assert "archive3" not in output
77+
assert "archive-2022-11-20" in output
78+
assert "archive-2022-12-18" in output
79+
assert "archive-now" not in output
7280

7381
output = cmd(archiver, "check", "-v", "--archives-only", "--newest=1m", exit_code=0)
74-
assert "archive3" in output
75-
assert "archive2" not in output
76-
assert "archive1" not in output
82+
assert "archive-2022-11-20" not in output
83+
assert "archive-2022-12-18" not in output
84+
assert "archive-now" in output
85+
86+
output = cmd(archiver, "check", "-v", "--archives-only", "--oldest=4w", exit_code=0)
87+
assert "archive-2022-11-20" in output
88+
assert "archive-2022-12-18" in output
89+
assert "archive-now" not in output
90+
91+
output = cmd(archiver, "check", "-v", "--archives-only", "--newest=4w", exit_code=0)
92+
assert "archive-2022-11-20" not in output
93+
assert "archive-2022-12-18" not in output
94+
assert "archive-now" in output
7795

7896
output = cmd(archiver, "check", "-v", "--archives-only", "--newer=1d", exit_code=0)
79-
assert "archive3" in output
80-
assert "archive1" not in output
81-
assert "archive2" not in output
97+
assert "archive-2022-11-20" not in output
98+
assert "archive-2022-12-18" not in output
99+
assert "archive-now" in output
82100

83101
output = cmd(archiver, "check", "-v", "--archives-only", "--older=1d", exit_code=0)
84-
assert "archive1" in output
85-
assert "archive2" in output
86-
assert "archive3" not in output
102+
assert "archive-2022-11-20" in output
103+
assert "archive-2022-12-18" in output
104+
assert "archive-now" not in output
105+
106+
output = cmd(archiver, "check", "-v", "--archives-only", "--newer=24H", exit_code=0)
107+
assert "archive-2022-11-20" not in output
108+
assert "archive-2022-12-18" not in output
109+
assert "archive-now" in output
110+
111+
output = cmd(archiver, "check", "-v", "--archives-only", "--older=24H", exit_code=0)
112+
assert "archive-2022-11-20" in output
113+
assert "archive-2022-12-18" in output
114+
assert "archive-now" not in output
115+
116+
output = cmd(archiver, "check", "-v", "--archives-only", "--newer=1440M", exit_code=0)
117+
assert "archive-2022-11-20" not in output
118+
assert "archive-2022-12-18" not in output
119+
assert "archive-now" in output
120+
121+
output = cmd(archiver, "check", "-v", "--archives-only", "--older=1440M", exit_code=0)
122+
assert "archive-2022-11-20" in output
123+
assert "archive-2022-12-18" in output
124+
assert "archive-now" not in output
125+
126+
output = cmd(archiver, "check", "-v", "--archives-only", "--newer=86400S", exit_code=0)
127+
assert "archive-2022-11-20" not in output
128+
assert "archive-2022-12-18" not in output
129+
assert "archive-now" in output
130+
131+
output = cmd(archiver, "check", "-v", "--archives-only", "--older=86400S", exit_code=0)
132+
assert "archive-2022-11-20" in output
133+
assert "archive-2022-12-18" in output
134+
assert "archive-now" not in output
87135

88136
# check for output when timespan older than the earliest archive is given. Issue #1711
89137
output = cmd(archiver, "check", "-v", "--archives-only", "--older=9999m", exit_code=0)

src/borg/testsuite/archiver/repo_list_cmd_test.py

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,32 +57,82 @@ def test_size_nfiles(archivers, request):
5757
def test_date_matching(archivers, request):
5858
archiver = request.getfixturevalue(archivers)
5959
cmd(archiver, "repo-create", RK_ENCRYPTION)
60-
earliest_ts = "2022-11-20T23:59:59"
61-
ts_in_between = "2022-12-18T23:59:59"
62-
create_src_archive(archiver, "archive1", ts=earliest_ts)
63-
create_src_archive(archiver, "archive2", ts=ts_in_between)
64-
create_src_archive(archiver, "archive3")
65-
cmd(archiver, "repo-list", "-v", "--oldest=23e", exit_code=2)
60+
61+
create_src_archive(archiver, "archive-2022-11-20", ts="2022-11-20T23:59:59")
62+
create_src_archive(archiver, "archive-2022-12-18", ts="2022-12-18T23:59:59")
63+
create_src_archive(archiver, "archive-now")
64+
65+
cmd(archiver, "check", "-v", "--oldest=23e", exit_code=2)
66+
67+
output = cmd(archiver, "repo-list", "-v", "--oldest=1y", exit_code=0)
68+
assert "archive-2022-11-20" in output
69+
assert "archive-2022-12-18" in output
70+
assert "archive-now" not in output
71+
72+
output = cmd(archiver, "repo-list", "-v", "--newest=1y", exit_code=0)
73+
assert "archive-2022-11-20" not in output
74+
assert "archive-2022-12-18" not in output
75+
assert "archive-now" in output
6676

6777
output = cmd(archiver, "repo-list", "-v", "--oldest=1m", exit_code=0)
68-
assert "archive1" in output
69-
assert "archive2" in output
70-
assert "archive3" not in output
78+
assert "archive-2022-11-20" in output
79+
assert "archive-2022-12-18" in output
80+
assert "archive-now" not in output
7181

7282
output = cmd(archiver, "repo-list", "-v", "--newest=1m", exit_code=0)
73-
assert "archive3" in output
74-
assert "archive2" not in output
75-
assert "archive1" not in output
83+
assert "archive-2022-11-20" not in output
84+
assert "archive-2022-12-18" not in output
85+
assert "archive-now" in output
86+
87+
output = cmd(archiver, "repo-list", "-v", "--oldest=4w", exit_code=0)
88+
assert "archive-2022-11-20" in output
89+
assert "archive-2022-12-18" in output
90+
assert "archive-now" not in output
91+
92+
output = cmd(archiver, "repo-list", "-v", "--newest=4w", exit_code=0)
93+
assert "archive-2022-11-20" not in output
94+
assert "archive-2022-12-18" not in output
95+
assert "archive-now" in output
7696

7797
output = cmd(archiver, "repo-list", "-v", "--newer=1d", exit_code=0)
78-
assert "archive3" in output
79-
assert "archive1" not in output
80-
assert "archive2" not in output
98+
assert "archive-2022-11-20" not in output
99+
assert "archive-2022-12-18" not in output
100+
assert "archive-now" in output
81101

82102
output = cmd(archiver, "repo-list", "-v", "--older=1d", exit_code=0)
83-
assert "archive1" in output
84-
assert "archive2" in output
85-
assert "archive3" not in output
103+
assert "archive-2022-11-20" in output
104+
assert "archive-2022-12-18" in output
105+
assert "archive-now" not in output
106+
107+
output = cmd(archiver, "repo-list", "-v", "--newer=24H", exit_code=0)
108+
assert "archive-2022-11-20" not in output
109+
assert "archive-2022-12-18" not in output
110+
assert "archive-now" in output
111+
112+
output = cmd(archiver, "repo-list", "-v", "--older=24H", exit_code=0)
113+
assert "archive-2022-11-20" in output
114+
assert "archive-2022-12-18" in output
115+
assert "archive-now" not in output
116+
117+
output = cmd(archiver, "repo-list", "-v", "--newer=1440M", exit_code=0)
118+
assert "archive-2022-11-20" not in output
119+
assert "archive-2022-12-18" not in output
120+
assert "archive-now" in output
121+
122+
output = cmd(archiver, "repo-list", "-v", "--older=1440M", exit_code=0)
123+
assert "archive-2022-11-20" in output
124+
assert "archive-2022-12-18" in output
125+
assert "archive-now" not in output
126+
127+
output = cmd(archiver, "repo-list", "-v", "--newer=86400S", exit_code=0)
128+
assert "archive-2022-11-20" not in output
129+
assert "archive-2022-12-18" not in output
130+
assert "archive-now" in output
131+
132+
output = cmd(archiver, "repo-list", "-v", "--older=86400S", exit_code=0)
133+
assert "archive-2022-11-20" in output
134+
assert "archive-2022-12-18" in output
135+
assert "archive-now" not in output
86136

87137

88138
def test_repo_list_json(archivers, request):

src/borg/testsuite/helpers_test.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -553,17 +553,28 @@ def test_prune_split_no_archives():
553553
assert kept_because == {}
554554

555555

556-
@pytest.mark.parametrize("timeframe, num_hours", [("1H", 1), ("1d", 24), ("1w", 168), ("1m", 744), ("1y", 8760)])
557-
def test_interval(timeframe, num_hours):
558-
assert interval(timeframe) == num_hours
556+
@pytest.mark.parametrize(
557+
"timeframe, num_secs",
558+
[
559+
("5S", 5),
560+
("2M", 2 * 60),
561+
("1H", 60 * 60),
562+
("1d", 24 * 60 * 60),
563+
("1w", 7 * 24 * 60 * 60),
564+
("1m", 31 * 24 * 60 * 60),
565+
("1y", 365 * 24 * 60 * 60),
566+
],
567+
)
568+
def test_interval(timeframe, num_secs):
569+
assert interval(timeframe) == num_secs
559570

560571

561572
@pytest.mark.parametrize(
562573
"invalid_interval, error_tuple",
563574
[
564-
("H", ('Unexpected interval number "": expected an integer greater than 0',)),
565-
("-1d", ('Unexpected interval number "-1": expected an integer greater than 0',)),
566-
("food", ('Unexpected interval number "foo": expected an integer greater than 0',)),
575+
("H", ('Invalid number "": expected positive integer',)),
576+
("-1d", ('Invalid number "-1": expected positive integer',)),
577+
("food", ('Invalid number "foo": expected positive integer',)),
567578
],
568579
)
569580
def test_interval_time_unit(invalid_interval, error_tuple):
@@ -575,7 +586,7 @@ def test_interval_time_unit(invalid_interval, error_tuple):
575586
def test_interval_number():
576587
with pytest.raises(ArgumentTypeError) as exc:
577588
interval("5")
578-
assert exc.value.args == ("Unexpected interval time unit \"5\": expected one of ['H', 'd', 'w', 'm', 'y']",)
589+
assert exc.value.args == ('Unexpected time unit "5": choose from y, m, w, d, H, M, S',)
579590

580591

581592
def test_prune_within():
@@ -595,6 +606,8 @@ def dotest(test_archives, within, indices):
595606
test_dates = [now - timedelta(seconds=s) for s in test_offsets]
596607
test_archives = [MockArchive(date, i) for i, date in enumerate(test_dates)]
597608

609+
dotest(test_archives, "15S", [])
610+
dotest(test_archives, "2M", [0])
598611
dotest(test_archives, "1H", [0])
599612
dotest(test_archives, "2H", [0, 1])
600613
dotest(test_archives, "3H", [0, 1, 2])

0 commit comments

Comments
 (0)