]> arthur.barton.de Git - bup.git/blob - cmd-index.py
cmd-index: correct reporting of deleted vs. added vs. modified status.
[bup.git] / cmd-index.py
1 #!/usr/bin/env python2.5
2 import sys, re, errno, stat, tempfile, struct, mmap
3 import options
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.lastfile = None
166         self.filename = None
167         self.filename = filename = os.path.realpath(filename)
168         (dir,name) = os.path.split(filename)
169         (ffd,self.tmpname) = tempfile.mkstemp('.tmp', filename, dir)
170         self.f = os.fdopen(ffd, 'wb', 65536)
171         self.f.write(INDEX_HDR)
172
173     def __del__(self):
174         self.abort()
175
176     def abort(self):
177         f = self.f
178         self.f = None
179         if f:
180             f.close()
181             os.unlink(self.tmpname)
182
183     def close(self):
184         f = self.f
185         self.f = None
186         if f:
187             f.close()
188             os.rename(self.tmpname, self.filename)
189
190     def add(self, name, st, hashgen=None):
191         #log('ADDING %r\n' % name)
192         if self.lastfile:
193             assert(cmp(self.lastfile, name) > 0) # reverse order only
194         self.lastfile = name
195         flags = IX_EXISTS
196         sha = None
197         if hashgen:
198             sha = hashgen(name)
199             if sha:
200                 flags |= IX_HASHVALID
201         else:
202             sha = EMPTY_SHA
203         data = name + '\0' + ix_encode(st, sha, flags)
204         self.f.write(data)
205
206     def add_ixentry(self, e):
207         if self.lastfile and self.lastfile <= e.name:
208             raise IndexError('%r must come before %r' 
209                              % (e.name, self.lastfile))
210         self.lastfile = e.name
211         data = e.name + '\0' + e.packed()
212         self.f.write(data)
213
214     def new_reader(self):
215         self.f.flush()
216         return IndexReader(self.tmpname)
217
218
219 saved_errors = []
220 def add_error(e):
221     saved_errors.append(e)
222     log('\n%s\n' % e)
223
224
225 # the use of fchdir() and lstat() are for two reasons:
226 #  - help out the kernel by not making it repeatedly look up the absolute path
227 #  - avoid race conditions caused by doing listdir() on a changing symlink
228 def handle_path(ri, wi, dir, name, pst, xdev, can_delete_siblings):
229     hashgen = None
230     if opt.fake_valid:
231         def hashgen(name):
232             return FAKE_SHA
233     
234     dirty = 0
235     path = dir + name
236     #log('handle_path(%r,%r)\n' % (dir, name))
237     if stat.S_ISDIR(pst.st_mode):
238         if opt.verbose == 1: # log dirs only
239             sys.stdout.write('%s\n' % path)
240             sys.stdout.flush()
241         try:
242             OsFile(name).fchdir()
243         except OSError, e:
244             add_error(Exception('in %s: %s' % (dir, str(e))))
245             return 0
246         try:
247             try:
248                 ld = os.listdir('.')
249                 #log('* %r: %r\n' % (name, ld))
250             except OSError, e:
251                 add_error(Exception('in %s: %s' % (path, str(e))))
252                 return 0
253             lds = []
254             for p in ld:
255                 try:
256                     st = os.lstat(p)
257                 except OSError, e:
258                     add_error(Exception('in %s: %s' % (path, str(e))))
259                     continue
260                 if xdev != None and st.st_dev != xdev:
261                     log('Skipping %r: different filesystem.\n' 
262                         % os.path.realpath(p))
263                     continue
264                 if stat.S_ISDIR(st.st_mode):
265                     p += '/'
266                 lds.append((p, st))
267             for p,st in reversed(sorted(lds)):
268                 dirty += handle_path(ri, wi, path, p, st, xdev,
269                                      can_delete_siblings = True)
270         finally:
271             os.chdir('..')
272     #log('endloop: ri.cur:%r path:%r\n' % (ri.cur.name, path))
273     while ri.cur and ri.cur.name > path:
274         #log('ricur:%r path:%r\n' % (ri.cur, path))
275         if can_delete_siblings and dir and ri.cur.name.startswith(dir):
276             #log('    --- deleting\n')
277             ri.cur.flags &= ~(IX_EXISTS | IX_HASHVALID)
278             ri.cur.repack()
279             dirty += 1
280         ri.next()
281     if ri.cur and ri.cur.name == path:
282         dirty += ri.cur.from_stat(pst)
283         if dirty or not (ri.cur.flags & IX_HASHVALID):
284             #log('   --- updating %r\n' % path)
285             if hashgen:
286                 ri.cur.sha = hashgen(name)
287                 ri.cur.flags |= IX_HASHVALID
288             ri.cur.repack()
289         ri.next()
290     else:
291         wi.add(path, pst, hashgen = hashgen)
292         dirty += 1
293     if opt.verbose > 1:  # all files, not just dirs
294         sys.stdout.write('%s\n' % path)
295         sys.stdout.flush()
296     return dirty
297
298
299 def merge_indexes(out, r1, r2):
300     log('Merging indexes.\n')
301     for e in _last_writer_wins_iter([r1, r2]):
302         #if e.flags & IX_EXISTS:
303             out.add_ixentry(e)
304
305
306 class MergeGetter:
307     def __init__(self, l):
308         self.i = iter(l)
309         self.cur = None
310         self.next()
311
312     def next(self):
313         try:
314             self.cur = self.i.next()
315         except StopIteration:
316             self.cur = None
317         return self.cur
318
319
320 def update_index(path):
321     ri = IndexReader(indexfile)
322     wi = IndexWriter(indexfile)
323     rig = MergeGetter(ri)
324     
325     rpath = os.path.realpath(path)
326     st = os.lstat(rpath)
327     if opt.xdev:
328         xdev = st.st_dev
329     else:
330         xdev = None
331     f = OsFile('.')
332     if rpath[-1] == '/':
333         rpath = rpath[:-1]
334     (dir, name) = os.path.split(rpath)
335     if dir and dir[-1] != '/':
336         dir += '/'
337     if stat.S_ISDIR(st.st_mode) and (not rpath or rpath[-1] != '/'):
338         name += '/'
339         can_delete_siblings = True
340     else:
341         can_delete_siblings = False
342     OsFile(dir or '/').fchdir()
343     dirty = handle_path(rig, wi, dir, name, st, xdev, can_delete_siblings)
344
345     # make sure all the parents of the updated path exist and are invalidated
346     # if appropriate.
347     while 1:
348         (rpath, junk) = os.path.split(rpath)
349         if not rpath:
350             break
351         elif rpath == '/':
352             p = rpath
353         else:
354             p = rpath + '/'
355         while rig.cur and rig.cur.name > p:
356             #log('FINISHING: %r path=%r d=%r\n' % (rig.cur.name, p, dirty))
357             rig.next()
358         if rig.cur and rig.cur.name == p:
359             if dirty:
360                 rig.cur.flags &= ~IX_HASHVALID
361                 rig.cur.repack()
362         else:
363             wi.add(p, os.lstat(p))
364         if p == '/':
365             break
366     
367     f.fchdir()
368     ri.save()
369     mi = IndexWriter(indexfile)
370     merge_indexes(mi, ri, wi.new_reader())
371     wi.abort()
372     mi.close()
373
374
375 optspec = """
376 bup index [options...] <filenames...>
377 --
378 p,print    print index after updating
379 m,modified print only modified files (implies -p)
380 x,xdev,one-file-system  don't cross filesystem boundaries
381 fake-valid    mark all index entries as up-to-date even if they aren't
382 f,indexfile=  the name of the index file (default 'index')
383 s,status   print each filename with a status char (A/M/D) (implies -p)
384 v,verbose  increase log output (can be used more than once)
385 """
386 o = options.Options('bup index', optspec)
387 (opt, flags, extra) = o.parse(sys.argv[1:])
388
389 indexfile = opt.indexfile or 'index'
390
391 xpaths = []
392 for path in extra:
393     rp = os.path.realpath(path)
394     st = os.lstat(rp)
395     if stat.S_ISDIR(st.st_mode) and not rp.endswith('/'):
396         rp += '/'
397     xpaths.append(rp)
398
399 paths = []
400 for path in reversed(sorted(xpaths)):
401     if paths and path.endswith('/') and paths[-1].startswith(path):
402         paths[-1] = path
403     else:
404         paths.append(path)
405
406 for path in paths:
407     update_index(path)
408
409 if opt.fake_valid and not extra:
410     mi = IndexWriter(indexfile)
411     merge_indexes(mi, IndexReader(indexfile),
412                   IndexWriter(indexfile).new_reader())
413     mi.close()
414
415 if opt['print'] or opt.status or opt.modified:
416     for ent in IndexReader(indexfile):
417         if opt.modified and ent.flags & IX_HASHVALID:
418             continue
419         if opt.status:
420             if not ent.flags & IX_EXISTS:
421                 print 'D ' + ent.name
422             elif not ent.flags & IX_HASHVALID:
423                 if ent.sha == EMPTY_SHA:
424                     print 'A ' + ent.name
425                 else:
426                     print 'M ' + ent.name
427             else:
428                 print '  ' + ent.name
429         else:
430             print ent.name
431         #print repr(ent)
432
433 if saved_errors:
434     log('WARNING: %d errors encountered.\n' % len(saved_errors))
435     exit(1)