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