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