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