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