]> arthur.barton.de Git - bup.git/blob - lib/cmd/index-cmd.py
Bypass Python 3 glibc argv problems by routing args through env
[bup.git] / lib / cmd / index-cmd.py
1 #!/bin/sh
2 """": # -*-python-*-
3 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
4 export "BUP_ARGV_0"="$0"
5 arg_i=1
6 for arg in "$@"; do
7     export "BUP_ARGV_${arg_i}"="$arg"
8     shift
9     arg_i=$((arg_i + 1))
10 done
11 # Here to end of preamble replaced during install
12 bup_python="$(dirname "$0")/bup-python" || exit $?
13 exec "$bup_python" "$0"
14 """
15 # end of bup preamble
16
17 from __future__ import absolute_import, print_function
18 from binascii import hexlify
19 import sys, stat, time, os, errno, re
20
21 from bup import compat, metadata, options, git, index, drecurse, hlinkdb
22 from bup.compat import argv_bytes
23 from bup.drecurse import recursive_dirlist
24 from bup.hashsplit import GIT_MODE_TREE, GIT_MODE_FILE
25 from bup.helpers import (add_error, handle_ctrl_c, log, parse_excludes, parse_rx_excludes,
26                          progress, qprogress, saved_errors)
27 from bup.io import byte_stream, path_msg
28
29
30 class IterHelper:
31     def __init__(self, l):
32         self.i = iter(l)
33         self.cur = None
34         self.next()
35
36     def __next__(self):
37         self.cur = next(self.i, None)
38         return self.cur
39
40     next = __next__
41
42 def check_index(reader):
43     try:
44         log('check: checking forward iteration...\n')
45         e = None
46         d = {}
47         for e in reader.forward_iter():
48             if e.children_n:
49                 if opt.verbose:
50                     log('%08x+%-4d %r\n' % (e.children_ofs, e.children_n,
51                                             path_msg(e.name)))
52                 assert(e.children_ofs)
53                 assert e.name.endswith(b'/')
54                 assert(not d.get(e.children_ofs))
55                 d[e.children_ofs] = 1
56             if e.flags & index.IX_HASHVALID:
57                 assert(e.sha != index.EMPTY_SHA)
58                 assert(e.gitmode)
59         assert not e or bytes(e.name) == b'/'  # last entry is *always* /
60         log('check: checking normal iteration...\n')
61         last = None
62         for e in reader:
63             if last:
64                 assert(last > e.name)
65             last = e.name
66     except:
67         log('index error! at %r\n' % e)
68         raise
69     log('check: passed.\n')
70
71
72 def clear_index(indexfile):
73     indexfiles = [indexfile, indexfile + b'.meta', indexfile + b'.hlink']
74     for indexfile in indexfiles:
75         path = git.repo(indexfile)
76         try:
77             os.remove(path)
78             if opt.verbose:
79                 log('clear: removed %s\n' % path_msg(path))
80         except OSError as e:
81             if e.errno != errno.ENOENT:
82                 raise
83
84
85 def update_index(top, excluded_paths, exclude_rxs, xdev_exceptions, out=None):
86     # tmax and start must be epoch nanoseconds.
87     tmax = (time.time() - 1) * 10**9
88     ri = index.Reader(indexfile)
89     msw = index.MetaStoreWriter(indexfile + b'.meta')
90     wi = index.Writer(indexfile, msw, tmax)
91     rig = IterHelper(ri.iter(name=top))
92     tstart = int(time.time()) * 10**9
93
94     hlinks = hlinkdb.HLinkDB(indexfile + b'.hlink')
95
96     fake_hash = None
97     if opt.fake_valid:
98         def fake_hash(name):
99             return (GIT_MODE_FILE, index.FAKE_SHA)
100
101     total = 0
102     bup_dir = os.path.abspath(git.repo())
103     index_start = time.time()
104     for path, pst in recursive_dirlist([top],
105                                        xdev=opt.xdev,
106                                        bup_dir=bup_dir,
107                                        excluded_paths=excluded_paths,
108                                        exclude_rxs=exclude_rxs,
109                                        xdev_exceptions=xdev_exceptions):
110         if opt.verbose>=2 or (opt.verbose==1 and stat.S_ISDIR(pst.st_mode)):
111             out.write(b'%s\n' % path)
112             out.flush()
113             elapsed = time.time() - index_start
114             paths_per_sec = total / elapsed if elapsed else 0
115             qprogress('Indexing: %d (%d paths/s)\r' % (total, paths_per_sec))
116         elif not (total % 128):
117             elapsed = time.time() - index_start
118             paths_per_sec = total / elapsed if elapsed else 0
119             qprogress('Indexing: %d (%d paths/s)\r' % (total, paths_per_sec))
120         total += 1
121
122         while rig.cur and rig.cur.name > path:  # deleted paths
123             if rig.cur.exists():
124                 rig.cur.set_deleted()
125                 rig.cur.repack()
126                 if rig.cur.nlink > 1 and not stat.S_ISDIR(rig.cur.mode):
127                     hlinks.del_path(rig.cur.name)
128             rig.next()
129
130         if rig.cur and rig.cur.name == path:    # paths that already existed
131             need_repack = False
132             if(rig.cur.stale(pst, tstart, check_device=opt.check_device)):
133                 try:
134                     meta = metadata.from_path(path, statinfo=pst)
135                 except (OSError, IOError) as e:
136                     add_error(e)
137                     rig.next()
138                     continue
139                 if not stat.S_ISDIR(rig.cur.mode) and rig.cur.nlink > 1:
140                     hlinks.del_path(rig.cur.name)
141                 if not stat.S_ISDIR(pst.st_mode) and pst.st_nlink > 1:
142                     hlinks.add_path(path, pst.st_dev, pst.st_ino)
143                 # Clear these so they don't bloat the store -- they're
144                 # already in the index (since they vary a lot and they're
145                 # fixed length).  If you've noticed "tmax", you might
146                 # wonder why it's OK to do this, since that code may
147                 # adjust (mangle) the index mtime and ctime -- producing
148                 # fake values which must not end up in a .bupm.  However,
149                 # it looks like that shouldn't be possible:  (1) When
150                 # "save" validates the index entry, it always reads the
151                 # metadata from the filesytem. (2) Metadata is only
152                 # read/used from the index if hashvalid is true. (3)
153                 # "faked" entries will be stale(), and so we'll invalidate
154                 # them below.
155                 meta.ctime = meta.mtime = meta.atime = 0
156                 meta_ofs = msw.store(meta)
157                 rig.cur.update_from_stat(pst, meta_ofs)
158                 rig.cur.invalidate()
159                 need_repack = True
160             if not (rig.cur.flags & index.IX_HASHVALID):
161                 if fake_hash:
162                     if rig.cur.sha == index.EMPTY_SHA:
163                         rig.cur.gitmode, rig.cur.sha = fake_hash(path)
164                     rig.cur.flags |= index.IX_HASHVALID
165                     need_repack = True
166             if opt.fake_invalid:
167                 rig.cur.invalidate()
168                 need_repack = True
169             if need_repack:
170                 rig.cur.repack()
171             rig.next()
172         else:  # new paths
173             try:
174                 meta = metadata.from_path(path, statinfo=pst)
175             except (OSError, IOError) as e:
176                 add_error(e)
177                 continue
178             # See same assignment to 0, above, for rationale.
179             meta.atime = meta.mtime = meta.ctime = 0
180             meta_ofs = msw.store(meta)
181             wi.add(path, pst, meta_ofs, hashgen=fake_hash)
182             if not stat.S_ISDIR(pst.st_mode) and pst.st_nlink > 1:
183                 hlinks.add_path(path, pst.st_dev, pst.st_ino)
184
185     elapsed = time.time() - index_start
186     paths_per_sec = total / elapsed if elapsed else 0
187     progress('Indexing: %d, done (%d paths/s).\n' % (total, paths_per_sec))
188
189     hlinks.prepare_save()
190
191     if ri.exists():
192         ri.save()
193         wi.flush()
194         if wi.count:
195             wr = wi.new_reader()
196             if opt.check:
197                 log('check: before merging: oldfile\n')
198                 check_index(ri)
199                 log('check: before merging: newfile\n')
200                 check_index(wr)
201             mi = index.Writer(indexfile, msw, tmax)
202
203             for e in index.merge(ri, wr):
204                 # FIXME: shouldn't we remove deleted entries eventually?  When?
205                 mi.add_ixentry(e)
206
207             ri.close()
208             mi.close()
209             wr.close()
210         wi.abort()
211     else:
212         wi.close()
213
214     msw.close()
215     hlinks.commit_save()
216
217
218 optspec = """
219 bup index <-p|-m|-s|-u|--clear|--check> [options...] <filenames...>
220 --
221  Modes:
222 p,print    print the index entries for the given names (also works with -u)
223 m,modified print only added/deleted/modified files (implies -p)
224 s,status   print each filename with a status char (A/M/D) (implies -p)
225 u,update   recursively update the index entries for the given file/dir names (default if no mode is specified)
226 check      carefully check index file integrity
227 clear      clear the default index
228  Options:
229 H,hash     print the hash for each object next to its name
230 l,long     print more information about each file
231 no-check-device don't invalidate an entry if the containing device changes
232 fake-valid mark all index entries as up-to-date even if they aren't
233 fake-invalid mark all index entries as invalid
234 f,indexfile=  the name of the index file (normally BUP_DIR/bupindex)
235 exclude= a path to exclude from the backup (may be repeated)
236 exclude-from= skip --exclude paths in file (may be repeated)
237 exclude-rx= skip paths matching the unanchored regex (may be repeated)
238 exclude-rx-from= skip --exclude-rx patterns in file (may be repeated)
239 v,verbose  increase log output (can be used more than once)
240 x,xdev,one-file-system  don't cross filesystem boundaries
241 """
242 o = options.Options(optspec)
243 opt, flags, extra = o.parse(compat.argv[1:])
244
245 if not (opt.modified or \
246         opt['print'] or \
247         opt.status or \
248         opt.update or \
249         opt.check or \
250         opt.clear):
251     opt.update = 1
252 if (opt.fake_valid or opt.fake_invalid) and not opt.update:
253     o.fatal('--fake-{in,}valid are meaningless without -u')
254 if opt.fake_valid and opt.fake_invalid:
255     o.fatal('--fake-valid is incompatible with --fake-invalid')
256 if opt.clear and opt.indexfile:
257     o.fatal('cannot clear an external index (via -f)')
258
259 # FIXME: remove this once we account for timestamp races, i.e. index;
260 # touch new-file; index.  It's possible for this to happen quickly
261 # enough that new-file ends up with the same timestamp as the first
262 # index, and then bup will ignore it.
263 tick_start = time.time()
264 time.sleep(1 - (tick_start - int(tick_start)))
265
266 git.check_repo_or_die()
267
268 handle_ctrl_c()
269
270 if opt.verbose is None:
271     opt.verbose = 0
272
273 if opt.indexfile:
274     indexfile = argv_bytes(opt.indexfile)
275 else:
276     indexfile = git.repo(b'bupindex')
277
278 if opt.check:
279     log('check: starting initial check.\n')
280     check_index(index.Reader(indexfile))
281
282 if opt.clear:
283     log('clear: clearing index.\n')
284     clear_index(indexfile)
285
286 sys.stdout.flush()
287 out = byte_stream(sys.stdout)
288
289 if opt.update:
290     if not extra:
291         o.fatal('update mode (-u) requested but no paths given')
292     extra = [argv_bytes(x) for x in extra]
293     excluded_paths = parse_excludes(flags, o.fatal)
294     exclude_rxs = parse_rx_excludes(flags, o.fatal)
295     xexcept = index.unique_resolved_paths(extra)
296     for rp, path in index.reduce_paths(extra):
297         update_index(rp, excluded_paths, exclude_rxs, xdev_exceptions=xexcept,
298                      out=out)
299
300 if opt['print'] or opt.status or opt.modified:
301     extra = [argv_bytes(x) for x in extra]
302     for name, ent in index.Reader(indexfile).filter(extra or [b'']):
303         if (opt.modified 
304             and (ent.is_valid() or ent.is_deleted() or not ent.mode)):
305             continue
306         line = b''
307         if opt.status:
308             if ent.is_deleted():
309                 line += b'D '
310             elif not ent.is_valid():
311                 if ent.sha == index.EMPTY_SHA:
312                     line += b'A '
313                 else:
314                     line += b'M '
315             else:
316                 line += b'  '
317         if opt.hash:
318             line += hexlify(ent.sha) + b' '
319         if opt.long:
320             line += b'%7s %7s ' % (oct(ent.mode), oct(ent.gitmode))
321         out.write(line + (name or b'./') + b'\n')
322
323 if opt.check and (opt['print'] or opt.status or opt.modified or opt.update):
324     log('check: starting final check.\n')
325     check_index(index.Reader(indexfile))
326
327 if saved_errors:
328     log('WARNING: %d errors encountered.\n' % len(saved_errors))
329     sys.exit(1)