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