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