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