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