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