]> arthur.barton.de Git - bup.git/blob - cmd-index.py
A bunch of wvtests for the 'bup index' command.
[bup.git] / cmd-index.py
1 #!/usr/bin/env python2.5
2 import sys, re, errno, stat, tempfile, struct, mmap, time
3 import options, git
4 from helpers import *
5
6 EMPTY_SHA = '\0'*20
7 FAKE_SHA = '\x01'*20
8 INDEX_HDR = 'BUPI\0\0\0\1'
9 INDEX_SIG = '!IIIIIQ20sH'
10 ENTLEN = struct.calcsize(INDEX_SIG)
11
12 IX_EXISTS = 0x8000
13 IX_HASHVALID = 0x4000
14
15
16 class IndexError(Exception):
17     pass
18
19
20 class OsFile:
21     def __init__(self, path):
22         self.fd = None
23         self.fd = os.open(path, os.O_RDONLY|os.O_LARGEFILE|os.O_NOFOLLOW)
24         #self.st = os.fstat(self.fd)
25         
26     def __del__(self):
27         if self.fd:
28             fd = self.fd
29             self.fd = None
30             os.close(fd)
31
32     def fchdir(self):
33         os.fchdir(self.fd)
34
35
36 class IxEntry:
37     def __init__(self, name, m, ofs, tstart):
38         self._m = m
39         self._ofs = ofs
40         self.name = str(name)
41         self.tstart = tstart
42         (self.dev, self.ctime, self.mtime, self.uid, self.gid,
43          self.size, self.sha,
44          self.flags) = struct.unpack(INDEX_SIG, buffer(m, ofs, ENTLEN))
45
46     def __repr__(self):
47         return ("(%s,0x%04x,%d,%d,%d,%d,%d,0x%04x)" 
48                 % (self.name, self.dev,
49                    self.ctime, self.mtime, self.uid, self.gid,
50                    self.size, self.flags))
51
52     def packed(self):
53         return struct.pack(INDEX_SIG, self.dev, self.ctime, self.mtime,
54                            self.uid, self.gid, self.size, self.sha,
55                            self.flags)
56
57     def repack(self):
58         self._m[self._ofs:self._ofs+ENTLEN] = self.packed()
59
60     def from_stat(self, st):
61         old = (self.dev, self.ctime, self.mtime,
62                self.uid, self.gid, self.size, self.flags & IX_EXISTS)
63         new = (st.st_dev, int(st.st_ctime), int(st.st_mtime),
64                st.st_uid, st.st_gid, st.st_size, IX_EXISTS)
65         self.dev = st.st_dev
66         self.ctime = int(st.st_ctime)
67         self.mtime = int(st.st_mtime)
68         self.uid = st.st_uid
69         self.gid = st.st_gid
70         self.size = st.st_size
71         self.flags |= IX_EXISTS
72         if int(st.st_ctime) >= self.tstart or old != new:
73             self.flags &= ~IX_HASHVALID
74             return 1  # dirty
75         else:
76             return 0  # not dirty
77
78     def __cmp__(a, b):
79         return cmp(a.name, b.name)
80             
81
82 class IndexReader:
83     def __init__(self, filename):
84         self.filename = filename
85         self.m = ''
86         self.writable = False
87         f = None
88         try:
89             f = open(filename, 'r+')
90         except IOError, e:
91             if e.errno == errno.ENOENT:
92                 pass
93             else:
94                 raise
95         if f:
96             b = f.read(len(INDEX_HDR))
97             if b != INDEX_HDR:
98                 raise IndexError('%s: header: expected %r, got %r'
99                                  % (filename, INDEX_HDR, b))
100             st = os.fstat(f.fileno())
101             if st.st_size:
102                 self.m = mmap.mmap(f.fileno(), 0,
103                                    mmap.MAP_SHARED,
104                                    mmap.PROT_READ|mmap.PROT_WRITE)
105                 f.close()  # map will persist beyond file close
106                 self.writable = True
107
108     def __del__(self):
109         self.save()
110
111     def __iter__(self):
112         tstart = int(time.time())
113         ofs = len(INDEX_HDR)
114         while ofs < len(self.m):
115             eon = self.m.find('\0', ofs)
116             assert(eon >= 0)
117             yield IxEntry(buffer(self.m, ofs, eon-ofs),
118                           self.m, eon+1, tstart = tstart)
119             ofs = eon + 1 + ENTLEN
120
121     def save(self):
122         if self.writable:
123             self.m.flush()
124
125
126 # Read all the iters in order; when more than one iter has the same entry,
127 # the *later* iter in the list wins.  (ie. more recent iter entries replace
128 # older ones)
129 def _last_writer_wins_iter(iters):
130     l = []
131     for e in iters:
132         it = iter(e)
133         try:
134             l.append([it.next(), it])
135         except StopIteration:
136             pass
137     del iters  # to avoid accidents
138     while l:
139         l.sort()
140         mv = l[0][0]
141         mi = []
142         for (i,(v,it)) in enumerate(l):
143             #log('(%d) considering %d: %r\n' % (len(l), i, v))
144             if v > mv:
145                 mv = v
146                 mi = [i]
147             elif v == mv:
148                 mi.append(i)
149         yield mv
150         for i in mi:
151             try:
152                 l[i][0] = l[i][1].next()
153             except StopIteration:
154                 l[i] = None
155         l = filter(None, l)
156
157
158 def ix_encode(st, sha, flags):
159     return struct.pack(INDEX_SIG, st.st_dev, int(st.st_ctime),
160                        int(st.st_mtime), st.st_uid, st.st_gid,
161                        st.st_size, sha, flags)
162
163
164 class IndexWriter:
165     def __init__(self, filename):
166         self.f = None
167         self.count = 0
168         self.lastfile = None
169         self.filename = None
170         self.filename = filename = os.path.realpath(filename)
171         (dir,name) = os.path.split(filename)
172         (ffd,self.tmpname) = tempfile.mkstemp('.tmp', filename, dir)
173         self.f = os.fdopen(ffd, 'wb', 65536)
174         self.f.write(INDEX_HDR)
175
176     def __del__(self):
177         self.abort()
178
179     def abort(self):
180         f = self.f
181         self.f = None
182         if f:
183             f.close()
184             os.unlink(self.tmpname)
185
186     def close(self):
187         f = self.f
188         self.f = None
189         if f:
190             f.close()
191             os.rename(self.tmpname, self.filename)
192
193     def _write(self, data):
194         self.f.write(data)
195         self.count += 1
196
197     def add(self, name, st, hashgen=None):
198         #log('ADDING %r\n' % name)
199         if self.lastfile:
200             assert(cmp(self.lastfile, name) > 0) # reverse order only
201         self.lastfile = name
202         flags = IX_EXISTS
203         sha = None
204         if hashgen:
205             sha = hashgen(name)
206             if sha:
207                 flags |= IX_HASHVALID
208         else:
209             sha = EMPTY_SHA
210         data = name + '\0' + ix_encode(st, sha, flags)
211         self._write(data)
212
213     def add_ixentry(self, e):
214         if self.lastfile and self.lastfile <= e.name:
215             raise IndexError('%r must come before %r' 
216                              % (e.name, self.lastfile))
217         self.lastfile = e.name
218         data = e.name + '\0' + e.packed()
219         self._write(data)
220
221     def new_reader(self):
222         self.f.flush()
223         return IndexReader(self.tmpname)
224
225
226 saved_errors = []
227 def add_error(e):
228     saved_errors.append(e)
229     log('\n%s\n' % e)
230
231
232 # the use of fchdir() and lstat() are for two reasons:
233 #  - help out the kernel by not making it repeatedly look up the absolute path
234 #  - avoid race conditions caused by doing listdir() on a changing symlink
235 def handle_path(ri, wi, dir, name, pst, xdev, can_delete_siblings):
236     hashgen = None
237     if opt.fake_valid:
238         def hashgen(name):
239             return FAKE_SHA
240     
241     dirty = 0
242     path = dir + name
243     #log('handle_path(%r,%r)\n' % (dir, name))
244     if stat.S_ISDIR(pst.st_mode):
245         if opt.verbose == 1: # log dirs only
246             sys.stdout.write('%s\n' % path)
247             sys.stdout.flush()
248         try:
249             OsFile(name).fchdir()
250         except OSError, e:
251             add_error(Exception('in %s: %s' % (dir, str(e))))
252             return 0
253         try:
254             try:
255                 ld = os.listdir('.')
256                 #log('* %r: %r\n' % (name, ld))
257             except OSError, e:
258                 add_error(Exception('in %s: %s' % (path, str(e))))
259                 return 0
260             lds = []
261             for p in ld:
262                 try:
263                     st = os.lstat(p)
264                 except OSError, e:
265                     add_error(Exception('in %s: %s' % (path, str(e))))
266                     continue
267                 if xdev != None and st.st_dev != xdev:
268                     log('Skipping %r: different filesystem.\n' 
269                         % os.path.realpath(p))
270                     continue
271                 if stat.S_ISDIR(st.st_mode):
272                     p = _slashappend(p)
273                 lds.append((p, st))
274             for p,st in reversed(sorted(lds)):
275                 dirty += handle_path(ri, wi, path, p, st, xdev,
276                                      can_delete_siblings = True)
277         finally:
278             os.chdir('..')
279     #log('endloop: ri.cur:%r path:%r\n' % (ri.cur.name, path))
280     while ri.cur and ri.cur.name > path:
281         #log('ricur:%r path:%r\n' % (ri.cur, path))
282         if can_delete_siblings and dir and ri.cur.name.startswith(dir):
283             #log('    --- deleting\n')
284             ri.cur.flags &= ~(IX_EXISTS | IX_HASHVALID)
285             ri.cur.repack()
286             dirty += 1
287         ri.next()
288     if ri.cur and ri.cur.name == path:
289         dirty += ri.cur.from_stat(pst)
290         if dirty or not (ri.cur.flags & IX_HASHVALID):
291             #log('   --- updating %r\n' % path)
292             if hashgen:
293                 ri.cur.sha = hashgen(name)
294                 ri.cur.flags |= IX_HASHVALID
295             ri.cur.repack()
296         ri.next()
297     else:
298         wi.add(path, pst, hashgen = hashgen)
299         dirty += 1
300     if opt.verbose > 1:  # all files, not just dirs
301         sys.stdout.write('%s\n' % path)
302         sys.stdout.flush()
303     return dirty
304
305
306 def merge_indexes(out, r1, r2):
307     log('bup: merging indexes.\n')
308     for e in _last_writer_wins_iter([r1, r2]):
309         #if e.flags & IX_EXISTS:
310             out.add_ixentry(e)
311
312
313 class MergeGetter:
314     def __init__(self, l):
315         self.i = iter(l)
316         self.cur = None
317         self.next()
318
319     def next(self):
320         try:
321             self.cur = self.i.next()
322         except StopIteration:
323             self.cur = None
324         return self.cur
325
326
327 def _slashappend(s):
328     if s and not s.endswith('/'):
329         return s + '/'
330     else:
331         return s
332
333 def update_index(path):
334     ri = IndexReader(indexfile)
335     wi = IndexWriter(indexfile)
336     rig = MergeGetter(ri)
337     
338     rpath = os.path.realpath(path)
339     st = os.lstat(rpath)
340     if opt.xdev:
341         xdev = st.st_dev
342     else:
343         xdev = None
344     f = OsFile('.')
345     if rpath[-1] == '/':
346         rpath = rpath[:-1]
347     (dir, name) = os.path.split(rpath)
348     dir = _slashappend(dir)
349     if stat.S_ISDIR(st.st_mode) and (not rpath or rpath[-1] != '/'):
350         name += '/'
351         can_delete_siblings = True
352     else:
353         can_delete_siblings = False
354     OsFile(dir or '/').fchdir()
355     dirty = handle_path(rig, wi, dir, name, st, xdev, can_delete_siblings)
356
357     # make sure all the parents of the updated path exist and are invalidated
358     # if appropriate.
359     while 1:
360         (rpath, junk) = os.path.split(rpath)
361         if not rpath:
362             break
363         elif rpath == '/':
364             p = rpath
365         else:
366             p = rpath + '/'
367         while rig.cur and rig.cur.name > p:
368             #log('FINISHING: %r path=%r d=%r\n' % (rig.cur.name, p, dirty))
369             rig.next()
370         if rig.cur and rig.cur.name == p:
371             if dirty:
372                 rig.cur.flags &= ~IX_HASHVALID
373                 rig.cur.repack()
374         else:
375             wi.add(p, os.lstat(p))
376         if p == '/':
377             break
378     
379     f.fchdir()
380     ri.save()
381     if wi.count:
382         mi = IndexWriter(indexfile)
383         merge_indexes(mi, ri, wi.new_reader())
384         mi.close()
385     wi.abort()
386
387
388 optspec = """
389 bup index <-p|s|m|u> [options...] <filenames...>
390 --
391 p,print    print the index entries for the given names (also works with -u)
392 m,modified print only added/deleted/modified files (implies -p)
393 s,status   print each filename with a status char (A/M/D) (implies -p)
394 u,update   (recursively) update the index entries for the given filenames
395 x,xdev,one-file-system  don't cross filesystem boundaries
396 fake-valid    mark all index entries as up-to-date even if they aren't
397 f,indexfile=  the name of the index file (default 'index')
398 v,verbose  increase log output (can be used more than once)
399 """
400 o = options.Options('bup index', optspec)
401 (opt, flags, extra) = o.parse(sys.argv[1:])
402
403 if not (opt.modified or opt['print'] or opt.status or opt.update):
404     log('bup index: you must supply one or more of -p, -s, -m, or -u\n')
405     exit(97)
406 if opt.fake_valid and not opt.update:
407     log('bup index: --fake-valid is meaningless without -u\n')
408     exit(96)
409
410 git.check_repo_or_die()
411 indexfile = opt.indexfile or git.repo('bupindex')
412
413 xpaths = []
414 for path in extra:
415     rp = os.path.realpath(path)
416     st = os.lstat(rp)
417     if stat.S_ISDIR(st.st_mode):
418         rp = _slashappend(rp)
419         path = _slashappend(path)
420     xpaths.append((rp, path))
421
422 paths = []
423 for (rp, path) in reversed(sorted(xpaths)):
424     if paths and rp.endswith('/') and paths[-1][0].startswith(rp):
425         paths[-1] = (rp, path)
426     else:
427         paths.append((rp, path))
428
429 if opt.update:
430     if not paths:
431         log('bup index: update (-u) requested but no paths given\n')
432         exit(96)
433     for (rp, path) in paths:
434         update_index(rp)
435
436 if opt['print'] or opt.status or opt.modified:
437     pi = iter(paths or [(_slashappend(os.path.realpath('.')), '')])
438     (rpin, pin) = pi.next()
439     for ent in IndexReader(indexfile):
440         if ent.name < rpin:
441             try:
442                 (rpin, pin) = pi.next()
443             except StopIteration:
444                 break  # no more files can possibly match
445         elif not ent.name.startswith(rpin):
446             continue   # not interested
447         if opt.modified and ent.flags & IX_HASHVALID:
448             continue
449         name = pin + ent.name[len(rpin):]
450         if not name:
451             name = '.'
452         if opt.status:
453             if not ent.flags & IX_EXISTS:
454                 print 'D ' + name
455             elif not ent.flags & IX_HASHVALID:
456                 if ent.sha == EMPTY_SHA:
457                     print 'A ' + name
458                 else:
459                     print 'M ' + name
460             else:
461                 print '  ' + name
462         else:
463             print name
464         #print repr(ent)
465
466 if saved_errors:
467     log('WARNING: %d errors encountered.\n' % len(saved_errors))
468     exit(1)