]> arthur.barton.de Git - bup.git/blob - cmd/prune-older-cmd.py
fe45b95c4bef564b31429e5cf101ccc8aa7a3946
[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(refnames=('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     # Foreach period, seek back from now to the period's starting time, and
50     # collect the most recent saves
51     for pstart, time_region_id in tm_ranges:
52         matches, rest = partition(lambda s: s[0] >= pstart, rest)
53         for region_id, region_saves in groupby(matches, time_region_id):
54             for action in retain_newest_in_region(list(region_saves)):
55                 yield action
56
57     for save in rest:
58         yield False, save
59
60
61 optspec = """
62 bup prune-older [options...] [BRANCH...]
63 --
64 keep-all-for=       retain all saves within the PERIOD
65 keep-dailies-for=   retain the newest save per day within the PERIOD
66 keep-monthlies-for= retain the newest save per month within the PERIOD
67 keep-yearlies-for=  retain the newest save per year within the PERIOD
68 wrt=                end all periods at this number of seconds since the epoch
69 pretend       don't prune, just report intended actions to standard output
70 gc            collect garbage after removals [1]
71 gc-threshold= only rewrite a packfile if it's over this percent garbage [10]
72 #,compress=   set compression level to # (0-9, 9 is highest) [1]
73 v,verbose     increase log output (can be used more than once)
74 unsafe        use the command even though it may be DANGEROUS
75 """
76
77 o = options.Options(optspec)
78 opt, flags, roots = o.parse(sys.argv[1:])
79
80 if not opt.unsafe:
81     o.fatal('refusing to run dangerous, experimental command without --unsafe')
82
83 now = int(time()) if not opt.wrt else opt.wrt
84 if not isinstance(now, (int, long)):
85     o.fatal('--wrt value ' + str(now) + ' is not an integer')
86
87 period_start = {}
88 for period, extent in (('all', opt.keep_all_for),
89                        ('dailies', opt.keep_dailies_for),
90                        ('monthlies', opt.keep_monthlies_for),
91                        ('yearlies', opt.keep_yearlies_for)):
92     if extent:
93         secs = period_as_secs(extent)
94         if not secs:
95             o.fatal('%r is not a valid period' % extent)
96         period_start[period] = now - secs
97
98 if not period_start:
99     o.fatal('at least one keep argument is required')
100
101 period_start = defaultdict(lambda: float('inf'), period_start)
102
103 if opt.verbose:
104     epoch_ymd = strftime('%Y-%m-%d-%H%M%S', localtime(0))
105     for kind in ['all', 'dailies', 'monthlies', 'yearlies']:
106         period_utc = period_start[kind]
107         if period_utc != float('inf'):
108             if not (period_utc > float('-inf')):
109                 log('keeping all ' + kind)
110             else:
111                 try:
112                     when = strftime('%Y-%m-%d-%H%M%S', localtime(period_utc))
113                     log('keeping ' + kind + ' since ' + when + '\n')
114                 except ValueError as ex:
115                     if period_utc < 0:
116                         log('keeping %s since %d seconds before %s\n'
117                             %(kind, abs(period_utc), epoch_ymd))
118                     elif period_utc > 0:
119                         log('keeping %s since %d seconds after %s\n'
120                             %(kind, period_utc, epoch_ymd))
121                     else:
122                         log('keeping %s since %s\n' % (kind, epoch_ymd))
123
124 git.check_repo_or_die()
125
126 # This could be more efficient, but for now just build the whole list
127 # in memory and let bup_rm() do some redundant work.
128
129 removals = []
130 for branch, branch_id in branches(roots):
131     die_if_errors()
132     saves = git.rev_list(branch_id.encode('hex'))
133     for keep_save, (utc, id) in classify_saves(saves, period_start):
134         assert(keep_save in (False, True))
135         # FIXME: base removals on hashes
136         if opt.pretend:
137             print('+' if keep_save else '-', save_name(branch, utc))
138         elif not keep_save:
139             removals.append(save_name(branch, utc))
140
141 if not opt.pretend:
142     die_if_errors()
143     bup_rm(removals, compression=opt.compress, verbosity=opt.verbose)
144     if opt.gc:
145         die_if_errors()
146         bup_gc(threshold=opt.gc_threshold,
147                compression=opt.compress,
148                verbosity=opt.verbose)
149
150 die_if_errors()