2
0
Эх сурвалжийг харах

Split up parsing and filtering for --keep-within (#2726)

Split up parsing and filtering for --keep-within

Fixes #2610

Parse --keep-within argument early, via new method within_range passed
to argparse type=, so that better error messages can be given.

Also swallows ValueError stacktrace per the comment in the old code that
including it wasn't desirable.
edgewood 8 жил өмнө
parent
commit
932fb9ec7a

+ 2 - 2
borg/archiver.py

@@ -21,7 +21,7 @@ import collections
 from . import __version__
 from . import __version__
 from .helpers import Error, location_validator, archivename_validator, format_line, format_time, format_file_size, \
 from .helpers import Error, location_validator, archivename_validator, format_line, format_time, format_file_size, \
     parse_pattern, PathPrefixPattern, to_localtime, timestamp, safe_timestamp, bin_to_hex, \
     parse_pattern, PathPrefixPattern, to_localtime, timestamp, safe_timestamp, bin_to_hex, \
-    get_cache_dir, prune_within, prune_split, check_python, \
+    get_cache_dir, interval, prune_within, prune_split, check_python, \
     Manifest, NoManifestError, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
     Manifest, NoManifestError, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
     dir_is_tagged, bigint_to_int, ChunkerParams, CompressionSpec, PrefixSpec, is_slow_msgpack, yes, sysinfo, \
     dir_is_tagged, bigint_to_int, ChunkerParams, CompressionSpec, PrefixSpec, is_slow_msgpack, yes, sysinfo, \
     EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, log_multi, PatternMatcher, ErrorIgnoringTextIOWrapper, set_ec, \
     EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, log_multi, PatternMatcher, ErrorIgnoringTextIOWrapper, set_ec, \
@@ -1717,7 +1717,7 @@ class Archiver:
         subparser.add_argument('--list', dest='output_list',
         subparser.add_argument('--list', dest='output_list',
                                action='store_true', default=False,
                                action='store_true', default=False,
                                help='output verbose list of archives it keeps/prunes. Requires -v/--verbose.')
                                help='output verbose list of archives it keeps/prunes. Requires -v/--verbose.')
-        subparser.add_argument('--keep-within', dest='within', type=str, metavar='WITHIN',
+        subparser.add_argument('--keep-within', dest='within', type=interval, metavar='INTERVAL',
                                help='keep all archives within this time interval')
                                help='keep all archives within this time interval')
         subparser.add_argument('-H', '--keep-hourly', dest='hourly', type=int, default=0,
         subparser.add_argument('-H', '--keep-hourly', dest='hourly', type=int, default=0,
                                help='number of hourly archives to keep')
                                help='number of hourly archives to keep')

+ 23 - 6
borg/helpers.py

@@ -279,15 +279,32 @@ class Manifest:
         return archives
         return archives
 
 
 
 
-def prune_within(archives, within):
+def interval(s):
+    """Convert a string representing a valid interval to a number of hours."""
     multiplier = {'H': 1, 'd': 24, 'w': 24 * 7, 'm': 24 * 31, 'y': 24 * 365}
     multiplier = {'H': 1, 'd': 24, 'w': 24 * 7, 'm': 24 * 31, 'y': 24 * 365}
+
+    if s.endswith(tuple(multiplier.keys())):
+        number = s[:-1]
+        suffix = s[-1]
+    else:
+        # range suffixes in ascending multiplier order
+        ranges = [k for k, v in sorted(multiplier.items(), key=lambda t: t[1])]
+        raise argparse.ArgumentTypeError(
+            'Unexpected interval time unit "%s": expected one of %r' % (s[-1], ranges))
+
     try:
     try:
-        hours = int(within[:-1]) * multiplier[within[-1]]
-    except (KeyError, ValueError):
-        # I don't like how this displays the original exception too:
-        raise argparse.ArgumentTypeError('Unable to parse --keep-within option: "%s"' % within)
+        hours = int(number) * multiplier[suffix]
+    except ValueError:
+        hours = -1
+
     if hours <= 0:
     if hours <= 0:
-        raise argparse.ArgumentTypeError('Number specified using --keep-within option must be positive')
+        raise argparse.ArgumentTypeError(
+            'Unexpected interval number "%s": expected an integer greater than 0' % number)
+
+    return hours
+
+
+def prune_within(archives, hours):
     target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600)
     target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600)
     return [a for a in archives if a.ts > target]
     return [a for a in archives if a.ts > target]
 
 

+ 37 - 4
borg/testsuite/helpers.py

@@ -1,4 +1,5 @@
 import hashlib
 import hashlib
+from argparse import ArgumentTypeError
 from time import mktime, strptime
 from time import mktime, strptime
 from datetime import datetime, timezone, timedelta
 from datetime import datetime, timezone, timedelta
 from io import StringIO
 from io import StringIO
@@ -11,7 +12,7 @@ import msgpack.fallback
 import time
 import time
 
 
 from ..helpers import Location, format_file_size, format_timedelta, format_line, PlaceholderError, make_path_safe, \
 from ..helpers import Location, format_file_size, format_timedelta, format_line, PlaceholderError, make_path_safe, \
-    prune_within, prune_split, get_cache_dir, get_keys_dir, get_security_dir, Statistics, is_slow_msgpack, \
+    interval, prune_within, prune_split, get_cache_dir, get_keys_dir, get_security_dir, Statistics, is_slow_msgpack, \
     yes, TRUISH, FALSISH, DEFAULTISH, \
     yes, TRUISH, FALSISH, DEFAULTISH, \
     StableDict, int_to_bigint, bigint_to_int, parse_timestamp, CompressionSpec, ChunkerParams, \
     StableDict, int_to_bigint, bigint_to_int, parse_timestamp, CompressionSpec, ChunkerParams, \
     ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \
     ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \
@@ -664,16 +665,48 @@ class PruneSplitTestCase(BaseTestCase):
         dotest(test_archives, 0, [], [])
         dotest(test_archives, 0, [], [])
 
 
 
 
-class PruneWithinTestCase(BaseTestCase):
+class IntervalTestCase(BaseTestCase):
+    def test_interval(self):
+        self.assert_equal(interval('1H'), 1)
+        self.assert_equal(interval('1d'), 24)
+        self.assert_equal(interval('1w'), 168)
+        self.assert_equal(interval('1m'), 744)
+        self.assert_equal(interval('1y'), 8760)
 
 
-    def test(self):
+    def test_interval_time_unit(self):
+        with pytest.raises(ArgumentTypeError) as exc:
+            interval('H')
+        self.assert_equal(
+            exc.value.args,
+            ('Unexpected interval number "": expected an integer greater than 0',))
+        with pytest.raises(ArgumentTypeError) as exc:
+            interval('-1d')
+        self.assert_equal(
+            exc.value.args,
+            ('Unexpected interval number "-1": expected an integer greater than 0',))
+        with pytest.raises(ArgumentTypeError) as exc:
+            interval('food')
+        self.assert_equal(
+            exc.value.args,
+            ('Unexpected interval number "foo": expected an integer greater than 0',))
+
+    def test_interval_number(self):
+        with pytest.raises(ArgumentTypeError) as exc:
+            interval('5')
+        self.assert_equal(
+            exc.value.args,
+            ("Unexpected interval time unit \"5\": expected one of ['H', 'd', 'w', 'm', 'y']",))
+
+
+class PruneWithinTestCase(BaseTestCase):
+    def test_prune_within(self):
 
 
         def subset(lst, indices):
         def subset(lst, indices):
             return {lst[i] for i in indices}
             return {lst[i] for i in indices}
 
 
         def dotest(test_archives, within, indices):
         def dotest(test_archives, within, indices):
             for ta in test_archives, reversed(test_archives):
             for ta in test_archives, reversed(test_archives):
-                self.assert_equal(set(prune_within(ta, within)),
+                self.assert_equal(set(prune_within(ta, interval(within))),
                                   subset(test_archives, indices))
                                   subset(test_archives, indices))
 
 
         # 1 minute, 1.5 hours, 2.5 hours, 3.5 hours, 25 hours, 49 hours
         # 1 minute, 1.5 hours, 2.5 hours, 3.5 hours, 25 hours, 49 hours