Add a --within option to the prune command.

This commit is contained in:
Dan Christensen 2014-02-08 15:37:27 -05:00
parent 2252616f9e
commit b5483b79a4
3 changed files with 62 additions and 8 deletions

View File

@ -13,7 +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
get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \
Manifest, remove_surrogates, is_a_terminal
from attic.remote import RepositoryServer, RemoteRepository
@ -311,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:
@ -486,7 +489,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.'''
@ -495,6 +503,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,

View File

@ -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 = []

View File

@ -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])