]> arthur.barton.de Git - bup.git/blob - cmd/fsck-cmd.py
fsck: stop using .encode('hex')
[bup.git] / cmd / fsck-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 absolute_import, print_function
9 import sys, os, glob, subprocess
10 from shutil import rmtree
11 from subprocess import PIPE, Popen
12 from tempfile import mkdtemp
13 from binascii import hexlify
14
15 from bup import options, git
16 from bup.compat import argv_bytes
17 from bup.helpers import Sha1, chunkyreader, istty2, log, progress
18 from bup.io import byte_stream
19
20
21 par2_ok = 0
22 nullf = open(os.devnull, 'wb+')
23
24 def debug(s):
25     if opt.verbose > 1:
26         log(s)
27
28 def run(argv):
29     # at least in python 2.5, using "stdout=2" or "stdout=sys.stderr" below
30     # doesn't actually work, because subprocess closes fd #2 right before
31     # execing for some reason.  So we work around it by duplicating the fd
32     # first.
33     fd = os.dup(2)  # copy stderr
34     try:
35         p = subprocess.Popen(argv, stdout=fd, close_fds=False)
36         return p.wait()
37     finally:
38         os.close(fd)
39
40 def par2_setup():
41     global par2_ok
42     rv = 1
43     try:
44         p = subprocess.Popen([b'par2', b'--help'],
45                              stdout=nullf, stderr=nullf, stdin=nullf)
46         rv = p.wait()
47     except OSError:
48         log('fsck: warning: par2 not found; disabling recovery features.\n')
49     else:
50         par2_ok = 1
51
52 def is_par2_parallel():
53     # A true result means it definitely allows -t1; a false result is
54     # technically inconclusive, but likely means no.
55     tmpdir = mkdtemp(prefix=b'bup-fsck')
56     try:
57         canary = tmpdir + b'/canary'
58         with open(canary, 'wb') as f:
59             f.write(b'canary\n')
60         p = subprocess.Popen((b'par2', b'create', b'-qq', b'-t1', canary),
61                              stderr=PIPE, stdin=nullf)
62         _, err = p.communicate()
63         parallel = p.returncode == 0
64         if opt.verbose:
65             if len(err) > 0 and err != b'Invalid option specified: -t1\n':
66                 log('Unexpected par2 error output\n')
67                 log(repr(err) + '\n')
68             if parallel:
69                 log('Assuming par2 supports parallel processing\n')
70             else:
71                 log('Assuming par2 does not support parallel processing\n')
72         return parallel
73     finally:
74         rmtree(tmpdir)
75
76 _par2_parallel = None
77
78 def par2(action, args, verb_floor=0):
79     global _par2_parallel
80     if _par2_parallel is None:
81         _par2_parallel = is_par2_parallel()
82     cmd = [b'par2', action]
83     if opt.verbose >= verb_floor and not istty2:
84         cmd.append(b'-q')
85     else:
86         cmd.append(b'-qq')
87     if _par2_parallel:
88         cmd.append(b'-t1')
89     cmd.extend(args)
90     return run(cmd)
91
92 def par2_generate(base):
93     return par2(b'create',
94                 [b'-n1', b'-c200', b'--', base, base + b'.pack', base + b'.idx'],
95                 verb_floor=2)
96
97 def par2_verify(base):
98     return par2(b'verify', [b'--', base], verb_floor=3)
99
100 def par2_repair(base):
101     return par2(b'repair', [b'--', base], verb_floor=2)
102
103 def quick_verify(base):
104     f = open(base + b'.pack', 'rb')
105     f.seek(-20, 2)
106     wantsum = f.read(20)
107     assert(len(wantsum) == 20)
108     f.seek(0)
109     sum = Sha1()
110     for b in chunkyreader(f, os.fstat(f.fileno()).st_size - 20):
111         sum.update(b)
112     if sum.digest() != wantsum:
113         raise ValueError('expected %r, got %r' % (hexlify(wantsum),
114                                                   sum.hexdigest()))
115         
116
117 def git_verify(base):
118     if opt.quick:
119         try:
120             quick_verify(base)
121         except Exception as e:
122             log('error: %s\n' % e)
123             return 1
124         return 0
125     else:
126         return run([b'git', b'verify-pack', b'--', base])
127     
128     
129 def do_pack(base, last, par2_exists, out):
130     code = 0
131     if par2_ok and par2_exists and (opt.repair or not opt.generate):
132         vresult = par2_verify(base)
133         if vresult != 0:
134             if opt.repair:
135                 rresult = par2_repair(base)
136                 if rresult != 0:
137                     action_result = b'failed'
138                     log('%s par2 repair: failed (%d)\n' % (last, rresult))
139                     code = rresult
140                 else:
141                     action_result = b'repaired'
142                     log('%s par2 repair: succeeded (0)\n' % last)
143                     code = 100
144             else:
145                 action_result = b'failed'
146                 log('%s par2 verify: failed (%d)\n' % (last, vresult))
147                 code = vresult
148         else:
149             action_result = b'ok'
150     elif not opt.generate or (par2_ok and not par2_exists):
151         gresult = git_verify(base)
152         if gresult != 0:
153             action_result = b'failed'
154             log('%s git verify: failed (%d)\n' % (last, gresult))
155             code = gresult
156         else:
157             if par2_ok and opt.generate:
158                 presult = par2_generate(base)
159                 if presult != 0:
160                     action_result = b'failed'
161                     log('%s par2 create: failed (%d)\n' % (last, presult))
162                     code = presult
163                 else:
164                     action_result = b'generated'
165             else:
166                 action_result = b'ok'
167     else:
168         assert(opt.generate and (not par2_ok or par2_exists))
169         action_result = b'exists' if par2_exists else b'skipped'
170     if opt.verbose:
171         out.write(last + b' ' +  action_result + b'\n')
172     return code
173
174
175 optspec = """
176 bup fsck [options...] [filenames...]
177 --
178 r,repair    attempt to repair errors using par2 (dangerous!)
179 g,generate  generate auto-repair information using par2
180 v,verbose   increase verbosity (can be used more than once)
181 quick       just check pack sha1sum, don't use git verify-pack
182 j,jobs=     run 'n' jobs in parallel
183 par2-ok     immediately return 0 if par2 is ok, 1 if not
184 disable-par2  ignore par2 even if it is available
185 """
186 o = options.Options(optspec)
187 (opt, flags, extra) = o.parse(sys.argv[1:])
188 opt.verbose = opt.verbose or 0
189
190 par2_setup()
191 if opt.par2_ok:
192     if par2_ok:
193         sys.exit(0)  # 'true' in sh
194     else:
195         sys.exit(1)
196 if opt.disable_par2:
197     par2_ok = 0
198
199 git.check_repo_or_die()
200
201 if extra:
202     extra = [argv_byes(x) for x in extra]
203 else:
204     debug('fsck: No filenames given: checking all packs.\n')
205     extra = glob.glob(git.repo(b'objects/pack/*.pack'))
206
207 sys.stdout.flush()
208 out = byte_stream(sys.stdout)
209 code = 0
210 count = 0
211 outstanding = {}
212 for name in extra:
213     if name.endswith(b'.pack'):
214         base = name[:-5]
215     elif name.endswith(b'.idx'):
216         base = name[:-4]
217     elif name.endswith(b'.par2'):
218         base = name[:-5]
219     elif os.path.exists(name + b'.pack'):
220         base = name
221     else:
222         raise Exception('%r is not a pack file!' % name)
223     (dir,last) = os.path.split(base)
224     par2_exists = os.path.exists(base + b'.par2')
225     if par2_exists and os.stat(base + b'.par2').st_size == 0:
226         par2_exists = 0
227     sys.stdout.flush()  # Not sure we still need this, but it'll flush out too
228     debug('fsck: checking %r (%s)\n'
229           % (last, par2_ok and par2_exists and 'par2' or 'git'))
230     if not opt.verbose:
231         progress('fsck (%d/%d)\r' % (count, len(extra)))
232     
233     if not opt.jobs:
234         nc = do_pack(base, last, par2_exists, out)
235         code = code or nc
236         count += 1
237     else:
238         while len(outstanding) >= opt.jobs:
239             (pid,nc) = os.wait()
240             nc >>= 8
241             if pid in outstanding:
242                 del outstanding[pid]
243                 code = code or nc
244                 count += 1
245         pid = os.fork()
246         if pid:  # parent
247             outstanding[pid] = 1
248         else: # child
249             try:
250                 sys.exit(do_pack(base, last, par2_exists, out))
251             except Exception as e:
252                 log('exception: %r\n' % e)
253                 sys.exit(99)
254                 
255 while len(outstanding):
256     (pid,nc) = os.wait()
257     nc >>= 8
258     if pid in outstanding:
259         del outstanding[pid]
260         code = code or nc
261         count += 1
262     if not opt.verbose:
263         progress('fsck (%d/%d)\r' % (count, len(extra)))
264
265 if istty2:
266     debug('fsck done.           \n')
267 sys.exit(code)