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