3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
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
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
21 nullf = open(os.devnull, 'wb+')
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
32 fd = os.dup(2) # copy stderr
34 p = subprocess.Popen(argv, stdout=fd, close_fds=False)
43 p = subprocess.Popen([b'par2', b'--help'],
44 stdout=nullf, stderr=nullf, stdin=nullf)
47 log('fsck: warning: par2 not found; disabling recovery features.\n')
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')
56 canary = tmpdir + b'/canary'
57 with open(canary, 'wb') as f:
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
64 if err != b'Invalid option specified: -t1\n':
65 log('Unexpected par2 error output\n')
68 log('Assuming par2 supports parallel processing\n')
70 log('Assuming par2 does not support parallel processing\n')
77 def par2(action, args, verb_floor=0):
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:
91 def par2_generate(base):
92 return par2(b'create',
93 [b'-n1', b'-c200', b'--', base, base + b'.pack', base + b'.idx'],
96 def par2_verify(base):
97 return par2(b'verify', [b'--', base], verb_floor=3)
99 def par2_repair(base):
100 return par2(b'repair', [b'--', base], verb_floor=2)
102 def quick_verify(base):
103 f = open(base + b'.pack', 'rb')
106 assert(len(wantsum) == 20)
109 for b in chunkyreader(f, os.fstat(f.fileno()).st_size - 20):
111 if sum.digest() != wantsum:
112 raise ValueError('expected %r, got %r' % (wantsum.encode('hex'),
116 def git_verify(base):
120 except Exception as e:
121 log('error: %s\n' % e)
125 return run([b'git', b'verify-pack', b'--', base])
128 def do_pack(base, last, par2_exists, out):
130 if par2_ok and par2_exists and (opt.repair or not opt.generate):
131 vresult = par2_verify(base)
134 rresult = par2_repair(base)
136 action_result = b'failed'
137 log('%s par2 repair: failed (%d)\n' % (last, rresult))
140 action_result = b'repaired'
141 log('%s par2 repair: succeeded (0)\n' % last)
144 action_result = b'failed'
145 log('%s par2 verify: failed (%d)\n' % (last, vresult))
148 action_result = b'ok'
149 elif not opt.generate or (par2_ok and not par2_exists):
150 gresult = git_verify(base)
152 action_result = b'failed'
153 log('%s git verify: failed (%d)\n' % (last, gresult))
156 if par2_ok and opt.generate:
157 presult = par2_generate(base)
159 action_result = b'failed'
160 log('%s par2 create: failed (%d)\n' % (last, presult))
163 action_result = b'generated'
165 action_result = b'ok'
167 assert(opt.generate and (not par2_ok or par2_exists))
168 action_result = b'exists' if par2_exists else b'skipped'
170 out.write(last + b' ' + action_result + b'\n')
175 bup fsck [options...] [filenames...]
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
185 o = options.Options(optspec)
186 (opt, flags, extra) = o.parse(sys.argv[1:])
187 opt.verbose = opt.verbose or 0
192 sys.exit(0) # 'true' in sh
198 git.check_repo_or_die()
201 extra = [argv_byes(x) for x in extra]
203 debug('fsck: No filenames given: checking all packs.\n')
204 extra = glob.glob(git.repo(b'objects/pack/*.pack'))
207 out = byte_stream(sys.stdout)
212 if name.endswith(b'.pack'):
214 elif name.endswith(b'.idx'):
216 elif name.endswith(b'.par2'):
218 elif os.path.exists(name + b'.pack'):
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:
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'))
230 progress('fsck (%d/%d)\r' % (count, len(extra)))
233 nc = do_pack(base, last, par2_exists, out)
237 while len(outstanding) >= opt.jobs:
240 if pid in outstanding:
249 sys.exit(do_pack(base, last, par2_exists, out))
250 except Exception as e:
251 log('exception: %r\n' % e)
254 while len(outstanding):
257 if pid in outstanding:
262 progress('fsck (%d/%d)\r' % (count, len(extra)))
265 debug('fsck done. \n')