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