]> arthur.barton.de Git - bup.git/blob - cmd/prune-older-cmd.py
Update base_version to 0.34~ for 0.34 development
[bup.git] / cmd / prune-older-cmd.py
1 #!/bin/sh
2 """": # -*-python-*-
3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
5 """
6 # end of bup preamble
7
8 from __future__ import absolute_import, print_function
9 from collections import defaultdict
10 from itertools import groupby
11 from sys import stderr
12 from time import localtime, strftime, time
13 import re, sys
14
15 from bup import git, options
16 from bup.compat import int_types
17 from bup.gc import bup_gc
18 from bup.helpers import die_if_errors, log, partition, period_as_secs
19 from bup.repo import LocalRepo
20 from bup.rm import bup_rm
21
22
23 def branches(refnames=()):
24     return ((name[11:], sha.encode('hex')) for (name,sha)
25             in git.list_refs(patterns=('refs/heads/' + n for n in refnames),
26                              limit_to_heads=True))
27
28 def save_name(branch, utc):
29     return branch + '/' + strftime('%Y-%m-%d-%H%M%S', localtime(utc))
30
31 def classify_saves(saves, period_start):
32     """For each (utc, id) in saves, yield (True, (utc, id)) if the save
33     should be kept and (False, (utc, id)) if the save should be removed.
34     The ids are binary hashes.
35     """
36
37     def retain_newest_in_region(region):
38         for save in region[0:1]:
39             yield True, save
40         for save in region[1:]:
41             yield False, save
42
43     matches, rest = partition(lambda s: s[0] >= period_start['all'], saves)
44     for save in matches:
45         yield True, save
46
47     tm_ranges = ((period_start['dailies'], lambda s: localtime(s[0]).tm_yday),
48                  (period_start['monthlies'], lambda s: localtime(s[0]).tm_mon),
49                  (period_start['yearlies'], lambda s: localtime(s[0]).tm_year))
50
51     # Break the decreasing utc sorted saves up into the respective
52     # period ranges (dailies, monthlies, ...).  Within each range,
53     # group the saves by the period scale (days, months, ...), and
54     # then yield a "keep" action (True, utc) for the newest save in
55     # each group, and a "drop" action (False, utc) for the rest.
56     for pstart, time_region_id in tm_ranges:
57         matches, rest = partition(lambda s: s[0] >= pstart, rest)
58         for region_id, region_saves in groupby(matches, time_region_id):
59             for action in retain_newest_in_region(list(region_saves)):
60                 yield action
61
62     # Finally, drop any saves older than the specified periods
63     for save in rest:
64         yield False, save
65
66
67 optspec = """
68 bup prune-older [options...] [BRANCH...]
69 --
70 keep-all-for=       retain all saves within the PERIOD
71 keep-dailies-for=   retain the newest save per day within the PERIOD
72 keep-monthlies-for= retain the newest save per month within the PERIOD
73 keep-yearlies-for=  retain the newest save per year within the PERIOD
74 wrt=                end all periods at this number of seconds since the epoch
75 pretend       don't prune, just report intended actions to standard output
76 gc            collect garbage after removals [1]
77 gc-threshold= only rewrite a packfile if it's over this percent garbage [10]
78 #,compress=   set compression level to # (0-9, 9 is highest) [1]
79 v,verbose     increase log output (can be used more than once)
80 unsafe        use the command even though it may be DANGEROUS
81 """
82
83 o = options.Options(optspec)
84 opt, flags, roots = o.parse(sys.argv[1:])
85
86 if not opt.unsafe:
87     o.fatal('refusing to run dangerous, experimental command without --unsafe')
88
89 now = int(time()) if opt.wrt is None else opt.wrt
90 if not isinstance(now, int_types):
91     o.fatal('--wrt value ' + str(now) + ' is not an integer')
92
93 period_start = {}
94 for period, extent in (('all', opt.keep_all_for),
95                        ('dailies', opt.keep_dailies_for),
96                        ('monthlies', opt.keep_monthlies_for),
97                        ('yearlies', opt.keep_yearlies_for)):
98     if extent:
99         secs = period_as_secs(extent)
100         if not secs:
101             o.fatal('%r is not a valid period' % extent)
102         period_start[period] = now - secs
103
104 if not period_start:
105     o.fatal('at least one keep argument is required')
106
107 period_start = defaultdict(lambda: float('inf'), period_start)
108
109 if opt.verbose:
110     epoch_ymd = strftime('%Y-%m-%d-%H%M%S', localtime(0))
111     for kind in ['all', 'dailies', 'monthlies', 'yearlies']:
112         period_utc = period_start[kind]
113         if period_utc != float('inf'):
114             if not (period_utc > float('-inf')):
115                 log('keeping all ' + kind)
116             else:
117                 try:
118                     when = strftime('%Y-%m-%d-%H%M%S', localtime(period_utc))
119                     log('keeping ' + kind + ' since ' + when + '\n')
120                 except ValueError as ex:
121                     if period_utc < 0:
122                         log('keeping %s since %d seconds before %s\n'
123                             %(kind, abs(period_utc), epoch_ymd))
124                     elif period_utc > 0:
125                         log('keeping %s since %d seconds after %s\n'
126                             %(kind, period_utc, epoch_ymd))
127                     else:
128                         log('keeping %s since %s\n' % (kind, epoch_ymd))
129
130 git.check_repo_or_die()
131
132 # This could be more efficient, but for now just build the whole list
133 # in memory and let bup_rm() do some redundant work.
134
135 def parse_info(f):
136     author_secs = f.readline().strip()
137     return int(author_secs)
138
139 removals = []
140 for branch, branch_id in branches(roots):
141     die_if_errors()
142     saves = ((utc, oidx.decode('hex')) for (oidx, utc) in
143              git.rev_list(branch_id, format='%at', parse=parse_info))
144     for keep_save, (utc, id) in classify_saves(saves, period_start):
145         assert(keep_save in (False, True))
146         # FIXME: base removals on hashes
147         if opt.pretend:
148             print('+' if keep_save else '-', save_name(branch, utc))
149         elif not keep_save:
150             removals.append(save_name(branch, utc))
151
152 if not opt.pretend:
153     die_if_errors()
154     repo = LocalRepo()
155     bup_rm(repo, removals, compression=opt.compress, verbosity=opt.verbose)
156     if opt.gc:
157         die_if_errors()
158         bup_gc(threshold=opt.gc_threshold,
159                compression=opt.compress,
160                verbosity=opt.verbose)
161
162 die_if_errors()