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