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