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