Bladeren bron

Merge remote-tracking branch 'jdchristensen/prune-within'

Conflicts:
	attic/archiver.py
Jonas Borgström 11 jaren geleden
bovenliggende
commit
5898b3b935
3 gewijzigde bestanden met toevoegingen van 62 en 9 verwijderingen
  1. 15 6
      attic/archiver.py
  2. 14 1
      attic/helpers.py
  3. 33 2
      attic/testsuite/helpers.py

+ 15 - 6
attic/archiver.py

@@ -13,8 +13,8 @@ from attic.cache import Cache
 from attic.key import key_creator
 from attic.helpers import Error, location_validator, format_time, \
     format_file_mode, ExcludePattern, exclude_path, adjust_patterns, to_localtime, \
-    get_cache_dir, get_keys_dir, format_timedelta, prune_split, Manifest, remove_surrogates, \
-    is_a_terminal, update_excludes
+    get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \
+    Manifest, remove_surrogates, is_a_terminal, update_excludes
 from attic.remote import RepositoryServer, RemoteRepository
 
 
@@ -312,15 +312,17 @@ class Archiver:
         cache = Cache(repository, key, manifest)
         archives = list(sorted(Archive.list_archives(repository, key, manifest, cache),
                                key=attrgetter('ts'), reverse=True))
-        if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0:
-            self.print_error('At least one of the "hourly", "daily", "weekly", "monthly" or "yearly" '
+        if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0 and args.within is None:
+            self.print_error('At least one of the "within", "hourly", "daily", "weekly", "monthly" or "yearly" '
                              'settings must be specified')
             return 1
         if args.prefix:
             archives = [archive for archive in archives if archive.name.startswith(args.prefix)]
         keep = []
+        if args.within:
+            keep += prune_within(archives, args.within)
         if args.hourly:
-            keep += prune_split(archives, '%Y-%m-%d %H', args.hourly)
+            keep += prune_split(archives, '%Y-%m-%d %H', args.hourly, keep)
         if args.daily:
             keep += prune_split(archives, '%Y-%m-%d', args.daily, keep)
         if args.weekly:
@@ -496,7 +498,12 @@ class Archiver:
         are applied from hourly to yearly, and backups selected by previous rules do
         not count towards those of later rules. Dates and times are interpreted in
         the local timezone, and weeks go from Monday to Sunday. Specifying a
-        negative number of archives to keep means that there is no limit. If a
+        negative number of archives to keep means that there is no limit.
+        The "--within" option takes an argument of the form "<int><char>",
+        where char is "H", "d", "w", "m", "y". For example, "--within 2d" means
+        to keep all archives that were created within the past 48 hours.
+        "1m" is taken to mean "31d". The archives kept with this option do not
+        count towards the totals specified by any other options. If a
         prefix is set with -p, then only archives that start with the prefix are
         considered for deletion and only those archives count towards the totals
         specified by the rules.'''
@@ -505,6 +512,8 @@ class Archiver:
                                           description=self.do_prune.__doc__,
                                           epilog=prune_epilog)
         subparser.set_defaults(func=self.do_prune)
+        subparser.add_argument('--within', dest='within', type=str, metavar='WITHIN',
+                               help='keep all archives within this time interval')
         subparser.add_argument('-H', '--hourly', dest='hourly', type=int, default=0,
                                help='number of hourly archives to keep')
         subparser.add_argument('-d', '--daily', dest='daily', type=int, default=0,

+ 14 - 1
attic/helpers.py

@@ -8,7 +8,7 @@ import re
 import stat
 import sys
 import time
-from datetime import datetime, timezone
+from datetime import datetime, timezone, timedelta
 from fnmatch import translate
 from operator import attrgetter
 import fcntl
@@ -91,6 +91,19 @@ class Manifest:
         self.repository.put(self.MANIFEST_ID, self.key.encrypt(data))
 
 
+def prune_within(archives, within):
+    multiplier = {'H': 1, 'd': 24, 'w': 24*7, 'm': 24*31, 'y': 24*365}
+    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 --within option: "%s"' % within)
+    if hours <= 0:
+        raise argparse.ArgumentTypeError('Number specified using --within option must be positive')
+    target = datetime.now(timezone.utc) - timedelta(seconds=hours*60*60)
+    return [a for a in archives if a.ts > target]
+
+
 def prune_split(archives, pattern, n, skip=[]):
     last = None
     keep = []

+ 33 - 2
attic/testsuite/helpers.py

@@ -1,9 +1,9 @@
 from time import mktime, strptime
-from datetime import datetime, timezone
+from datetime import datetime, timezone, timedelta
 import os
 import tempfile
 import unittest
-from attic.helpers import adjust_patterns, exclude_path, Location, format_timedelta, IncludePattern, ExcludePattern, make_path_safe, UpgradableLock, prune_split, to_localtime
+from attic.helpers import adjust_patterns, exclude_path, Location, format_timedelta, IncludePattern, ExcludePattern, make_path_safe, UpgradableLock, prune_within, prune_split, to_localtime
 from attic.testsuite import AtticTestCase
 
 
@@ -145,3 +145,34 @@ class PruneSplitTestCase(AtticTestCase):
         dotest(test_archives, 3, [test_archives[5]], [6, 2, 0])
         dotest(test_archives, 3, [test_archives[4]], [6, 5, 2])
         dotest(test_archives, 0, [], [])
+
+
+class PruneWithinTestCase(AtticTestCase):
+
+    def test(self):
+
+        def subset(lst, indices):
+            return {lst[i] for i in indices}
+
+        def dotest(test_archives, within, indices):
+            for ta in test_archives, reversed(test_archives):
+                self.assert_equal(set(prune_within(ta, within)),
+                                  subset(test_archives, indices))
+            
+        # 1 minute, 1.5 hours, 2.5 hours, 3.5 hours, 25 hours, 49 hours
+        test_offsets = [60, 90*60, 150*60, 210*60, 25*60*60, 49*60*60]
+        now = datetime.now(timezone.utc)
+        test_dates = [now - timedelta(seconds=s) for s in test_offsets]
+        test_archives = [MockArchive(date) for date in test_dates]
+
+        dotest(test_archives, '1H',  [0])
+        dotest(test_archives, '2H',  [0, 1])
+        dotest(test_archives, '3H',  [0, 1, 2])
+        dotest(test_archives, '24H', [0, 1, 2, 3])
+        dotest(test_archives, '26H', [0, 1, 2, 3, 4])
+        dotest(test_archives, '2d',  [0, 1, 2, 3, 4])
+        dotest(test_archives, '50H', [0, 1, 2, 3, 4, 5])
+        dotest(test_archives, '3d',  [0, 1, 2, 3, 4, 5])
+        dotest(test_archives, '1w',  [0, 1, 2, 3, 4, 5])
+        dotest(test_archives, '1m',  [0, 1, 2, 3, 4, 5])
+        dotest(test_archives, '1y',  [0, 1, 2, 3, 4, 5])