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
13 from binascii import hexlify
15 from bup import options, git
16 from bup.compat import argv_bytes
17 from bup.helpers import Sha1, chunkyreader, istty2, log, progress
18 from bup.io import byte_stream
22 nullf = open(os.devnull, 'wb+')
29 # at least in python 2.5, using "stdout=2" or "stdout=sys.stderr" below
30 # doesn't actually work, because subprocess closes fd #2 right before
31 # execing for some reason. So we work around it by duplicating the fd
33 fd = os.dup(2) # copy stderr
35 p = subprocess.Popen(argv, stdout=fd, close_fds=False)
44 p = subprocess.Popen([b'par2', b'--help'],
45 stdout=nullf, stderr=nullf, stdin=nullf)
48 log('fsck: warning: par2 not found; disabling recovery features.\n')
52 def is_par2_parallel():
53 # A true result means it definitely allows -t1; a false result is
54 # technically inconclusive, but likely means no.
55 tmpdir = mkdtemp(prefix=b'bup-fsck')
57 canary = tmpdir + b'/canary'
58 with open(canary, 'wb') as f:
60 p = subprocess.Popen((b'par2', b'create', b'-qq', b'-t1', canary),
61 stderr=PIPE, stdin=nullf)
62 _, err = p.communicate()
63 parallel = p.returncode == 0
65 if len(err) > 0 and err != b'Invalid option specified: -t1\n':
66 log('Unexpected par2 error output\n')
69 log('Assuming par2 supports parallel processing\n')
71 log('Assuming par2 does not support parallel processing\n')
78 def par2(action, args, verb_floor=0):
80 if _par2_parallel is None:
81 _par2_parallel = is_par2_parallel()
82 cmd = [b'par2', action]
83 if opt.verbose >= verb_floor and not istty2:
92 def par2_generate(base):
93 return par2(b'create',
94 [b'-n1', b'-c200', b'--', base, base + b'.pack', base + b'.idx'],
97 def par2_verify(base):
98 return par2(b'verify', [b'--', base], verb_floor=3)
100 def par2_repair(base):
101 return par2(b'repair', [b'--', base], verb_floor=2)
103 def quick_verify(base):
104 f = open(base + b'.pack', 'rb')
107 assert(len(wantsum) == 20)
110 for b in chunkyreader(f, os.fstat(f.fileno()).st_size - 20):
112 if sum.digest() != wantsum:
113 raise ValueError('expected %r, got %r' % (hexlify(wantsum),
117 def git_verify(base):
121 except Exception as e:
122 log('error: %s\n' % e)
126 return run([b'git', b'verify-pack', b'--', base])
129 def do_pack(base, last, par2_exists, out):
131 if par2_ok and par2_exists and (opt.repair or not opt.generate):
132 vresult = par2_verify(base)
135 rresult = par2_repair(base)
137 action_result = b'failed'
138 log('%s par2 repair: failed (%d)\n' % (last, rresult))
141 action_result = b'repaired'
142 log('%s par2 repair: succeeded (0)\n' % last)
145 action_result = b'failed'
146 log('%s par2 verify: failed (%d)\n' % (last, vresult))
149 action_result = b'ok'
150 elif not opt.generate or (par2_ok and not par2_exists):
151 gresult = git_verify(base)
153 action_result = b'failed'
154 log('%s git verify: failed (%d)\n' % (last, gresult))
157 if par2_ok and opt.generate:
158 presult = par2_generate(base)
160 action_result = b'failed'
161 log('%s par2 create: failed (%d)\n' % (last, presult))
164 action_result = b'generated'
166 action_result = b'ok'
168 assert(opt.generate and (not par2_ok or par2_exists))
169 action_result = b'exists' if par2_exists else b'skipped'
171 out.write(last + b' ' + action_result + b'\n')
176 bup fsck [options...] [filenames...]
178 r,repair attempt to repair errors using par2 (dangerous!)
179 g,generate generate auto-repair information using par2
180 v,verbose increase verbosity (can be used more than once)
181 quick just check pack sha1sum, don't use git verify-pack
182 j,jobs= run 'n' jobs in parallel
183 par2-ok immediately return 0 if par2 is ok, 1 if not
184 disable-par2 ignore par2 even if it is available
186 o = options.Options(optspec)
187 (opt, flags, extra) = o.parse(sys.argv[1:])
188 opt.verbose = opt.verbose or 0
193 sys.exit(0) # 'true' in sh
199 git.check_repo_or_die()
202 extra = [argv_byes(x) for x in extra]
204 debug('fsck: No filenames given: checking all packs.\n')
205 extra = glob.glob(git.repo(b'objects/pack/*.pack'))
208 out = byte_stream(sys.stdout)
213 if name.endswith(b'.pack'):
215 elif name.endswith(b'.idx'):
217 elif name.endswith(b'.par2'):
219 elif os.path.exists(name + b'.pack'):
222 raise Exception('%r is not a pack file!' % name)
223 (dir,last) = os.path.split(base)
224 par2_exists = os.path.exists(base + b'.par2')
225 if par2_exists and os.stat(base + b'.par2').st_size == 0:
227 sys.stdout.flush() # Not sure we still need this, but it'll flush out too
228 debug('fsck: checking %r (%s)\n'
229 % (last, par2_ok and par2_exists and 'par2' or 'git'))
231 progress('fsck (%d/%d)\r' % (count, len(extra)))
234 nc = do_pack(base, last, par2_exists, out)
238 while len(outstanding) >= opt.jobs:
241 if pid in outstanding:
250 sys.exit(do_pack(base, last, par2_exists, out))
251 except Exception as e:
252 log('exception: %r\n' % e)
255 while len(outstanding):
258 if pid in outstanding:
263 progress('fsck (%d/%d)\r' % (count, len(extra)))
266 debug('fsck done. \n')