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