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