]> arthur.barton.de Git - bup.git/blob - cmd/fsck-cmd.py
fsck: Hide error when probing par2 capabilities
[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         p = subprocess.Popen(['par2', 'create', '-qq', '-t1', canary],
57                              stdout=nullf, stderr=nullf, stdin=nullf)
58         rc = p.wait()
59         return rc == 0
60     finally:
61         rmtree(tmpdir)
62
63 _par2_parallel = None
64
65 def par2(action, args, verb_floor=0):
66     global _par2_parallel
67     if _par2_parallel is None:
68         _par2_parallel = is_par2_parallel()
69     cmd = ['par2', action]
70     if opt.verbose >= verb_floor and not istty2:
71         cmd.append('-q')
72     else:
73         cmd.append('-qq')
74     if _par2_parallel:
75         cmd.append('-t1')
76     cmd.extend(args)
77     return run(cmd)
78
79 def par2_generate(base):
80     return par2('create',
81                 ['-n1', '-c200', '--', base, base + '.pack', base + '.idx'],
82                 verb_floor=2)
83
84 def par2_verify(base):
85     return par2('verify', ['--', base], verb_floor=3)
86
87 def par2_repair(base):
88     return par2('repair', ['--', base], verb_floor=2)
89
90 def quick_verify(base):
91     f = open(base + '.pack', 'rb')
92     f.seek(-20, 2)
93     wantsum = f.read(20)
94     assert(len(wantsum) == 20)
95     f.seek(0)
96     sum = Sha1()
97     for b in chunkyreader(f, os.fstat(f.fileno()).st_size - 20):
98         sum.update(b)
99     if sum.digest() != wantsum:
100         raise ValueError('expected %r, got %r' % (wantsum.encode('hex'),
101                                                   sum.hexdigest()))
102         
103
104 def git_verify(base):
105     if opt.quick:
106         try:
107             quick_verify(base)
108         except Exception as e:
109             log('error: %s\n' % e)
110             return 1
111         return 0
112     else:
113         return run(['git', 'verify-pack', '--', base])
114     
115     
116 def do_pack(base, last, par2_exists):
117     code = 0
118     if par2_ok and par2_exists and (opt.repair or not opt.generate):
119         vresult = par2_verify(base)
120         if vresult != 0:
121             if opt.repair:
122                 rresult = par2_repair(base)
123                 if rresult != 0:
124                     action_result = 'failed'
125                     log('%s par2 repair: failed (%d)\n' % (last, rresult))
126                     code = rresult
127                 else:
128                     action_result = 'repaired'
129                     log('%s par2 repair: succeeded (0)\n' % last)
130                     code = 100
131             else:
132                 action_result = 'failed'
133                 log('%s par2 verify: failed (%d)\n' % (last, vresult))
134                 code = vresult
135         else:
136             action_result = 'ok'
137     elif not opt.generate or (par2_ok and not par2_exists):
138         gresult = git_verify(base)
139         if gresult != 0:
140             action_result = 'failed'
141             log('%s git verify: failed (%d)\n' % (last, gresult))
142             code = gresult
143         else:
144             if par2_ok and opt.generate:
145                 presult = par2_generate(base)
146                 if presult != 0:
147                     action_result = 'failed'
148                     log('%s par2 create: failed (%d)\n' % (last, presult))
149                     code = presult
150                 else:
151                     action_result = 'generated'
152             else:
153                 action_result = 'ok'
154     else:
155         assert(opt.generate and (not par2_ok or par2_exists))
156         action_result = 'exists' if par2_exists else 'skipped'
157     if opt.verbose:
158         print(last, action_result)
159     return code
160
161
162 optspec = """
163 bup fsck [options...] [filenames...]
164 --
165 r,repair    attempt to repair errors using par2 (dangerous!)
166 g,generate  generate auto-repair information using par2
167 v,verbose   increase verbosity (can be used more than once)
168 quick       just check pack sha1sum, don't use git verify-pack
169 j,jobs=     run 'n' jobs in parallel
170 par2-ok     immediately return 0 if par2 is ok, 1 if not
171 disable-par2  ignore par2 even if it is available
172 """
173 o = options.Options(optspec)
174 (opt, flags, extra) = o.parse(sys.argv[1:])
175
176 par2_setup()
177 if opt.par2_ok:
178     if par2_ok:
179         sys.exit(0)  # 'true' in sh
180     else:
181         sys.exit(1)
182 if opt.disable_par2:
183     par2_ok = 0
184
185 git.check_repo_or_die()
186
187 if not extra:
188     debug('fsck: No filenames given: checking all packs.\n')
189     extra = glob.glob(git.repo('objects/pack/*.pack'))
190
191 code = 0
192 count = 0
193 outstanding = {}
194 for name in extra:
195     if name.endswith('.pack'):
196         base = name[:-5]
197     elif name.endswith('.idx'):
198         base = name[:-4]
199     elif name.endswith('.par2'):
200         base = name[:-5]
201     elif os.path.exists(name + '.pack'):
202         base = name
203     else:
204         raise Exception('%s is not a pack file!' % name)
205     (dir,last) = os.path.split(base)
206     par2_exists = os.path.exists(base + '.par2')
207     if par2_exists and os.stat(base + '.par2').st_size == 0:
208         par2_exists = 0
209     sys.stdout.flush()
210     debug('fsck: checking %s (%s)\n' 
211           % (last, par2_ok and par2_exists and 'par2' or 'git'))
212     if not opt.verbose:
213         progress('fsck (%d/%d)\r' % (count, len(extra)))
214     
215     if not opt.jobs:
216         nc = do_pack(base, last, par2_exists)
217         code = code or nc
218         count += 1
219     else:
220         while len(outstanding) >= opt.jobs:
221             (pid,nc) = os.wait()
222             nc >>= 8
223             if pid in outstanding:
224                 del outstanding[pid]
225                 code = code or nc
226                 count += 1
227         pid = os.fork()
228         if pid:  # parent
229             outstanding[pid] = 1
230         else: # child
231             try:
232                 sys.exit(do_pack(base, last, par2_exists))
233             except Exception as e:
234                 log('exception: %r\n' % e)
235                 sys.exit(99)
236                 
237 while len(outstanding):
238     (pid,nc) = os.wait()
239     nc >>= 8
240     if pid in outstanding:
241         del outstanding[pid]
242         code = code or nc
243         count += 1
244     if not opt.verbose:
245         progress('fsck (%d/%d)\r' % (count, len(extra)))
246
247 if istty2:
248     debug('fsck done.           \n')
249 sys.exit(code)