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