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