Skip to content

Commit 66b105a

Browse files
committed
Adds fuzzy prune intervals and comprehensive interval test
1 parent b60ebb5 commit 66b105a

6 files changed

Lines changed: 442 additions & 115 deletions

File tree

src/borg/archiver/prune_cmd.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from ..archive import Archive
1111
from ..cache import Cache
1212
from ..constants import * # NOQA
13-
from ..helpers import interval, int_or_interval, sig_int, archivename_validator
13+
from ..helpers import interval, int_or_flexibledelta, sig_int, archivename_validator
1414
from ..helpers import ArchiveFormatter, ProgressIndicatorPercent, CommandError, Error
1515
from ..manifest import Manifest
1616

@@ -105,11 +105,11 @@ def quarterly_3monthly_period_func(a):
105105
DATETIME_MIN_WITH_ZONE = datetime.min.replace(tzinfo=timezone.utc)
106106

107107

108-
def prune_split(archives, rule, n_or_interval, base_timestamp, kept_because={}):
109-
if isinstance(n_or_interval, int):
110-
n, earliest_timestamp = n_or_interval, None
108+
def prune_split(archives, rule, n_or_flexibledelta, base_timestamp, kept_because={}):
109+
if isinstance(n_or_flexibledelta, int):
110+
n, earliest_timestamp = n_or_flexibledelta, None
111111
else:
112-
n, earliest_timestamp = None, base_timestamp - n_or_interval
112+
n, earliest_timestamp = None, n_or_flexibledelta.subtract_from(base_timestamp, calendar=True)
113113

114114
def can_retain(a, keep):
115115
if n is not None:
@@ -194,9 +194,9 @@ def do_prune(self, args, repository, manifest):
194194
base_timestamp = datetime.now().astimezone()
195195
# find archives which need to be kept because of the various time period rules
196196
for rule in PRUNING_PATTERNS.keys():
197-
num_or_interval = getattr(args, rule, None)
198-
if num_or_interval is not None:
199-
keep += prune_split(archives, rule, num_or_interval, base_timestamp, kept_because)
197+
n_or_flexibledelta = getattr(args, rule, None)
198+
if n_or_flexibledelta is not None:
199+
keep += prune_split(archives, rule, n_or_flexibledelta, base_timestamp, kept_because)
200200

201201
to_delete = set(archives) - set(keep)
202202
with Cache(repository, manifest, iec=args.iec) as cache:
@@ -346,74 +346,74 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser):
346346
subparser.add_argument(
347347
"--keep",
348348
dest="keep",
349-
type=int_or_interval,
349+
type=int_or_flexibledelta,
350350
action=Highlander,
351351
help="number or time interval of archives to keep",
352352
)
353353
subparser.add_argument(
354354
"--keep-secondly",
355355
dest="secondly",
356-
type=int_or_interval,
356+
type=int_or_flexibledelta,
357357
action=Highlander,
358358
help="number or time interval of secondly archives to keep",
359359
)
360360
subparser.add_argument(
361361
"--keep-minutely",
362362
dest="minutely",
363-
type=int_or_interval,
363+
type=int_or_flexibledelta,
364364
action=Highlander,
365365
help="number or time interval of minutely archives to keep",
366366
)
367367
subparser.add_argument(
368368
"-H",
369369
"--keep-hourly",
370370
dest="hourly",
371-
type=int_or_interval,
371+
type=int_or_flexibledelta,
372372
action=Highlander,
373373
help="number or time interval of hourly archives to keep",
374374
)
375375
subparser.add_argument(
376376
"-d",
377377
"--keep-daily",
378378
dest="daily",
379-
type=int_or_interval,
379+
type=int_or_flexibledelta,
380380
action=Highlander,
381381
help="number or time interval of daily archives to keep",
382382
)
383383
subparser.add_argument(
384384
"-w",
385385
"--keep-weekly",
386386
dest="weekly",
387-
type=int_or_interval,
387+
type=int_or_flexibledelta,
388388
action=Highlander,
389389
help="number or time interval of weekly archives to keep",
390390
)
391391
subparser.add_argument(
392392
"-m",
393393
"--keep-monthly",
394394
dest="monthly",
395-
type=int_or_interval,
395+
type=int_or_flexibledelta,
396396
action=Highlander,
397397
help="number or time interval of monthly archives to keep",
398398
)
399399
quarterly_group = subparser.add_mutually_exclusive_group()
400400
quarterly_group.add_argument(
401401
"--keep-13weekly",
402402
dest="quarterly_13weekly",
403-
type=int_or_interval,
403+
type=int_or_flexibledelta,
404404
help="number or time interval of quarterly archives to keep (13 week strategy)",
405405
)
406406
quarterly_group.add_argument(
407407
"--keep-3monthly",
408408
dest="quarterly_3monthly",
409-
type=int_or_interval,
409+
type=int_or_flexibledelta,
410410
help="number or time interval of quarterly archives to keep (3 month strategy)",
411411
)
412412
subparser.add_argument(
413413
"-y",
414414
"--keep-yearly",
415415
dest="yearly",
416-
type=int_or_interval,
416+
type=int_or_flexibledelta,
417417
action=Highlander,
418418
help="number or time interval of yearly archives to keep",
419419
)

src/borg/helpers/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from .misc import ChunkIteratorFileWrapper, open_item, chunkit, iter_separated, ErrorIgnoringTextIOWrapper
2828
from .parseformat import bin_to_hex, hex_to_bin, safe_encode, safe_decode
2929
from .parseformat import text_to_json, binary_to_json, remove_surrogates, join_cmd
30-
from .parseformat import eval_escapes, decode_dict, positive_int_validator, interval, int_or_interval
30+
from .parseformat import eval_escapes, decode_dict, positive_int_validator, interval, int_or_flexibledelta
3131
from .parseformat import PathSpec, SortBySpec, ChunkerParams, FilesCacheMode, partial_format, DatetimeWrapper
3232
from .parseformat import format_file_size, parse_file_size, FileSize
3333
from .parseformat import sizeof_fmt, sizeof_fmt_iec, sizeof_fmt_decimal, Location, text_validator
@@ -43,7 +43,7 @@
4343
from .process import popen_with_error_handling, is_terminal, prepare_subprocess_env, create_filter_process
4444
from .progress import ProgressIndicatorPercent, ProgressIndicatorMessage
4545
from .time import parse_timestamp, timestamp, safe_timestamp, safe_s, safe_ns, MAX_S, SUPPORT_32BIT_PLATFORMS
46-
from .time import format_time, format_timedelta, OutputTimestamp, archive_ts_now
46+
from .time import format_time, format_timedelta, OutputTimestamp, archive_ts_now, FlexibleDelta
4747
from .yes_no import yes, TRUISH, FALSISH, DEFAULTISH
4848

4949
from .msgpack import is_slow_msgpack, is_supported_msgpack, get_limited_unpacker

src/borg/helpers/parseformat.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pathlib import Path
1414
from typing import ClassVar, Any, TYPE_CHECKING, Literal
1515
from collections import OrderedDict
16-
from datetime import datetime, timezone, timedelta
16+
from datetime import datetime, timezone
1717
from functools import partial
1818
from string import Formatter
1919

@@ -24,7 +24,7 @@
2424
from .errors import Error
2525
from .fs import get_keys_dir, make_path_safe
2626
from .msgpack import Timestamp
27-
from .time import OutputTimestamp, format_time, safe_timestamp
27+
from .time import OutputTimestamp, format_time, safe_timestamp, FlexibleDelta
2828
from .. import __version__ as borg_version
2929
from .. import __version_tuple__ as borg_version_tuple
3030
from ..constants import * # NOQA
@@ -161,15 +161,15 @@ def interval(s):
161161
return seconds
162162

163163

164-
def int_or_interval(s):
164+
def int_or_flexibledelta(s):
165165
try:
166166
return int(s)
167167
except ValueError:
168168
pass
169169

170170
try:
171-
return timedelta(seconds=interval(s))
172-
except argparse.ArgumentTypeError as e:
171+
return FlexibleDelta.parse(s, fuzzyable=True)
172+
except ValueError as e:
173173
raise argparse.ArgumentTypeError(f"Value is neither an integer nor an interval: {e}")
174174

175175

src/borg/helpers/time.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,21 @@ def __init__(self, count, unit, fuzzy):
165165
def __repr__(self):
166166
return f'{self.__class__.__name__}(count={self.count}, unit="{self.unit}", fuzzy={self.fuzzy})'
167167

168+
def __eq__(self, other):
169+
if not isinstance(other, FlexibleDelta):
170+
return NotImplemented
171+
return (self.count, self.unit, self.fuzzy) == (other.count, other.unit, other.fuzzy)
172+
168173
_interval_regex = re.compile(r"^(?P<count>\d+)(?P<unit>[ymwdHMS])(?P<fuzzy>z)?$")
169174

170175
@classmethod
171176
def parse(cls, interval_string, fuzzyable=False):
172177
"""
173-
Parse interval string into
178+
Parse interval string into FlexibleDelta on a form like "1w", "25d", or "6Hz" where the unit is one of "y"
179+
(years), "m" (months), "w" (weeks), "d" (days), "H" (hours), "M" (minutes), "S" (seconds). A trailing "z"
180+
indicates a fuzzy delta (if fuzzyable is True) which is calculated as "apply delta and then take either the
181+
start or end of the current unit's interval" -- subtracting "1dz" means subtracting 1 day and then adjusting to
182+
the start of that day.
174183
"""
175184
match = cls._interval_regex.search(interval_string)
176185

0 commit comments

Comments
 (0)