1 #!/usr/bin/env python2.5
2 import sys, re, errno, stat, tempfile, struct, mmap
6 INDEX_HDR = 'BUPI\0\0\0\1'
7 INDEX_SIG = '!IIIIIQ20sH'
8 ENTLEN = struct.calcsize(INDEX_SIG)
14 class IndexError(Exception):
19 def __init__(self, path):
21 self.fd = os.open(path, os.O_RDONLY|os.O_LARGEFILE|os.O_NOFOLLOW)
22 #self.st = os.fstat(self.fd)
35 def __init__(self, name, m, ofs):
39 (self.dev, self.ctime, self.mtime, self.uid, self.gid,
41 self.flags) = struct.unpack(INDEX_SIG, buffer(m, ofs, ENTLEN))
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))
50 return struct.pack(INDEX_SIG, self.dev, self.ctime, self.mtime,
51 self.uid, self.gid, self.size, self.sha,
55 self._m[self._ofs:self._ofs+ENTLEN] = self.packed()
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)
63 self.ctime = int(st.st_ctime)
64 self.mtime = int(st.st_mtime)
67 self.size = st.st_size
68 self.flags |= IX_EXISTS
70 self.flags &= ~IX_HASHVALID
76 return cmp(a.name, b.name)
80 def __init__(self, filename):
81 self.filename = filename
86 f = open(filename, 'r+')
88 if e.errno == errno.ENOENT:
93 b = f.read(len(INDEX_HDR))
95 raise IndexError('%s: header: expected %r, got %r'
96 % (filename, INDEX_HDR, b))
97 st = os.fstat(f.fileno())
99 self.m = mmap.mmap(f.fileno(), 0,
101 mmap.PROT_READ|mmap.PROT_WRITE)
102 f.close() # map will persist beyond file close
110 while ofs < len(self.m):
111 eon = self.m.find('\0', ofs)
113 yield IxEntry(buffer(self.m, ofs, eon-ofs),
115 ofs = eon + 1 + ENTLEN
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
125 def _last_writer_wins_iter(iters):
130 l.append([it.next(), it])
131 except StopIteration:
133 del iters # to avoid accidents
138 for (i,(v,it)) in enumerate(l):
145 l[i][0] = l[i][1].next()
146 except StopIteration:
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)
158 def __init__(self, filename):
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)
176 os.unlink(self.tmpname)
183 os.rename(self.tmpname, self.filename)
185 def add(self, name, st):
186 #log('ADDING %r\n' % name)
188 assert(cmp(self.lastfile, name) > 0) # reverse order only
190 data = name + '\0' + ix_encode(st, '\0'*20, IX_EXISTS|IX_HASHVALID)
193 def add_ixentry(self, e):
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()
203 def new_reader(self):
205 return IndexReader(self.tmpname)
210 saved_errors.append(e)
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):
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)
226 OsFile(name).fchdir()
228 add_error(Exception('in %s: %s' % (dir, str(e))))
233 #log('* %r: %r\n' % (name, ld))
235 add_error(Exception('in %s: %s' % (path, str(e))))
242 add_error(Exception('in %s: %s' % (path, str(e))))
244 if xdev != None and st.st_dev != xdev:
245 log('Skipping %r: different filesystem.\n'
246 % os.path.realpath(p))
248 if stat.S_ISDIR(st.st_mode):
251 for p,st in reversed(sorted(lds)):
252 dirty += handle_path(ri, wi, path, p, st, xdev)
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)
264 if ri.cur and ri.cur.name == path:
265 dirty += ri.cur.from_stat(pst)
267 #log(' --- updating %r\n' % path)
273 if opt.verbose > 1: # all files, not just dirs
274 sys.stdout.write('%s\n' % path)
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:
287 def __init__(self, l):
294 self.cur = self.i.next()
295 except StopIteration:
300 def update_index(paths):
301 ri = IndexReader(indexfile)
302 wi = IndexWriter(indexfile)
303 rig = MergeGetter(ri)
305 rpath = os.path.realpath(path)
314 (dir, name) = os.path.split(rpath)
315 if dir and dir[-1] != '/':
317 if stat.S_ISDIR(st.st_mode) and (not rpath or rpath[-1] != '/'):
319 OsFile(dir or '/').fchdir()
320 dirty = handle_path(rig, wi, dir, name, st, xdev)
322 # make sure all the parents of the updated path exist and are invalidated
325 (rpath, junk) = os.path.split(rpath)
332 while rig.cur and rig.cur.name > p:
333 #log('FINISHING: %r path=%r d=%r\n' % (rig.cur.name, p, dirty))
335 if rig.cur and rig.cur.name == p:
337 rig.cur.flags &= ~IX_HASHVALID
340 wi.add(p, os.lstat(p))
346 mi = IndexWriter(indexfile)
347 merge_indexes(mi, ri, wi.new_reader())
353 bup index [options...] <filenames...>
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)
363 o = options.Options('bup index', optspec)
364 (opt, flags, extra) = o.parse(sys.argv[1:])
366 indexfile = opt.indexfile or 'index'
370 rp = os.path.realpath(path)
372 if stat.S_ISDIR(st.st_mode) and not rp.endswith('/'):
377 for path in reversed(sorted(xpaths)):
378 if paths and path.endswith('/') and paths[-1].startswith(path):
386 if opt.fake_valid and not extra:
387 mi = IndexWriter(indexfile)
388 merge_indexes(mi, IndexReader(indexfile),
389 IndexWriter(indexfile).new_reader())
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:
397 if not ent.flags & IX_EXISTS:
398 print 'D ' + ent.name
399 elif not ent.flags & IX_HASHVALID:
400 print 'M ' + ent.name
408 log('WARNING: %d errors encountered.\n' % len(saved_errors))