]> arthur.barton.de Git - bup.git/blob - lib/bup/index.py
c3d405d7cfb2bfb33254cdd42294cc1053c7035a
[bup.git] / lib / bup / index.py
1 import metadata, os, stat, struct, tempfile
2 from bup import xstat
3 from bup.helpers import *
4
5 EMPTY_SHA = '\0'*20
6 FAKE_SHA = '\x01'*20
7
8 INDEX_HDR = 'BUPI\0\0\0\5'
9
10 # Time values are handled as integer nanoseconds since the epoch in
11 # memory, but are written as xstat/metadata timespecs.  This behavior
12 # matches the existing metadata/xstat/.bupm code.
13
14 # Record times (mtime, ctime, atime) as xstat/metadata timespecs, and
15 # store all of the times in the index so they won't interfere with the
16 # forthcoming metadata cache.
17 INDEX_SIG =  '!QQQqQqQqQIIQII20sHIIQ'
18
19 ENTLEN = struct.calcsize(INDEX_SIG)
20 FOOTER_SIG = '!Q'
21 FOOTLEN = struct.calcsize(FOOTER_SIG)
22
23 IX_EXISTS = 0x8000        # file exists on filesystem
24 IX_HASHVALID = 0x4000     # the stored sha1 matches the filesystem
25 IX_SHAMISSING = 0x2000    # the stored sha1 object doesn't seem to exist
26
27 class Error(Exception):
28     pass
29
30
31 class MetaStoreReader:
32     def __init__(self, filename):
33         self._file = open(filename, 'rb')
34
35     def close(self):
36         if self._file:
37             self._file.close()
38             self._file = None
39
40     def __del__(self):
41         self.close()
42
43     def metadata_at(self, ofs):
44         self._file.seek(ofs)
45         return metadata.Metadata.read(self._file)
46
47
48 class MetaStoreWriter:
49     # For now, we just append to the file, and try to handle any
50     # truncation or corruption somewhat sensibly.
51
52     def __init__(self, filename):
53         # Map metadata hashes to bupindex.meta offsets.
54         self._offsets = {}
55         self._filename = filename
56         # FIXME: see how slow this is; does it matter?
57         m_file = open(filename, 'ab+')
58         try:
59             m_file.seek(0)
60             try:
61                 m_off = m_file.tell()
62                 m = metadata.Metadata.read(m_file)
63                 while m:
64                     m_encoded = m.encode()
65                     self._offsets[m_encoded] = m_off
66                     m_off = m_file.tell()
67                     m = metadata.Metadata.read(m_file)
68             except EOFError:
69                 pass
70             except:
71                 log('index metadata in %r appears to be corrupt' % filename)
72                 raise
73         finally:
74             m_file.close()
75         self._file = open(filename, 'ab')
76
77     def close(self):
78         if self._file:
79             self._file.close()
80             self._file = None
81
82     def __del__(self):
83         # Be optimistic.
84         self.close()
85
86     def store(self, metadata):
87         meta_encoded = metadata.encode(include_path=False)
88         ofs = self._offsets.get(meta_encoded)
89         if ofs:
90             return ofs
91         ofs = self._file.tell()
92         self._file.write(meta_encoded)
93         self._offsets[meta_encoded] = ofs
94         return ofs
95
96
97 class Level:
98     def __init__(self, ename, parent):
99         self.parent = parent
100         self.ename = ename
101         self.list = []
102         self.count = 0
103
104     def write(self, f):
105         (ofs,n) = (f.tell(), len(self.list))
106         if self.list:
107             count = len(self.list)
108             #log('popping %r with %d entries\n' 
109             #    % (''.join(self.ename), count))
110             for e in self.list:
111                 e.write(f)
112             if self.parent:
113                 self.parent.count += count + self.count
114         return (ofs,n)
115
116
117 def _golevel(level, f, ename, newentry, metastore, tmax):
118     # close nodes back up the tree
119     assert(level)
120     default_meta_ofs = metastore.store(metadata.Metadata())
121     while ename[:len(level.ename)] != level.ename:
122         n = BlankNewEntry(level.ename[-1], default_meta_ofs, tmax)
123         n.flags |= IX_EXISTS
124         (n.children_ofs,n.children_n) = level.write(f)
125         level.parent.list.append(n)
126         level = level.parent
127
128     # create nodes down the tree
129     while len(level.ename) < len(ename):
130         level = Level(ename[:len(level.ename)+1], level)
131
132     # are we in precisely the right place?
133     assert(ename == level.ename)
134     n = newentry or \
135         BlankNewEntry(ename and level.ename[-1] or None, default_meta_ofs, tmax)
136     (n.children_ofs,n.children_n) = level.write(f)
137     if level.parent:
138         level.parent.list.append(n)
139     level = level.parent
140
141     return level
142
143
144 class Entry:
145     def __init__(self, basename, name, meta_ofs, tmax):
146         self.basename = str(basename)
147         self.name = str(name)
148         self.meta_ofs = meta_ofs
149         self.tmax = tmax
150         self.children_ofs = 0
151         self.children_n = 0
152
153     def __repr__(self):
154         return ("(%s,0x%04x,%d,%d,%d,%d,%d,%d,%d,%d,%s/%s,0x%04x,%d,0x%08x/%d)"
155                 % (self.name, self.dev, self.ino, self.nlink,
156                    self.ctime, self.mtime, self.atime, self.uid, self.gid,
157                    self.size, self.mode, self.gitmode,
158                    self.flags, self.meta_ofs,
159                    self.children_ofs, self.children_n))
160
161     def packed(self):
162         try:
163             ctime = xstat.nsecs_to_timespec(self.ctime)
164             mtime = xstat.nsecs_to_timespec(self.mtime)
165             atime = xstat.nsecs_to_timespec(self.atime)
166             return struct.pack(INDEX_SIG,
167                                self.dev, self.ino, self.nlink,
168                                ctime[0], ctime[1],
169                                mtime[0], mtime[1],
170                                atime[0], atime[1],
171                                self.uid, self.gid, self.size, self.mode,
172                                self.gitmode, self.sha, self.flags,
173                                self.children_ofs, self.children_n,
174                                self.meta_ofs)
175         except (DeprecationWarning, struct.error), e:
176             log('pack error: %s (%r)\n' % (e, self))
177             raise
178
179     def from_stat(self, st, meta_ofs, tstart, check_device=True):
180         old = (self.dev if check_device else 0,
181                self.ino, self.nlink, self.ctime, self.mtime,
182                self.uid, self.gid, self.size, self.flags & IX_EXISTS)
183         new = (st.st_dev if check_device else 0,
184                st.st_ino, st.st_nlink, st.st_ctime, st.st_mtime,
185                st.st_uid, st.st_gid, st.st_size, IX_EXISTS)
186         self.dev = st.st_dev
187         self.ino = st.st_ino
188         self.nlink = st.st_nlink
189         self.ctime = st.st_ctime
190         self.mtime = st.st_mtime
191         self.atime = st.st_atime
192         self.uid = st.st_uid
193         self.gid = st.st_gid
194         self.size = st.st_size
195         self.mode = st.st_mode
196         self.flags |= IX_EXISTS
197         self.meta_ofs = meta_ofs
198         # Check that the ctime's "second" is at or after tstart's.
199         ctime_sec_in_ns = xstat.fstime_floor_secs(st.st_ctime) * 10**9
200         if ctime_sec_in_ns >= tstart or old != new \
201               or self.sha == EMPTY_SHA or not self.gitmode:
202             self.invalidate()
203         self._fixup()
204         
205     def _fixup(self):
206         if self.uid < 0:
207             self.uid += 0x100000000
208         if self.gid < 0:
209             self.gid += 0x100000000
210         assert(self.uid >= 0)
211         assert(self.gid >= 0)
212         self.mtime = self._fixup_time(self.mtime)
213         self.ctime = self._fixup_time(self.ctime)
214
215     def _fixup_time(self, t):
216         if self.tmax != None and t > self.tmax:
217             return self.tmax
218         else:
219             return t
220
221     def is_valid(self):
222         f = IX_HASHVALID|IX_EXISTS
223         return (self.flags & f) == f
224
225     def invalidate(self):
226         self.flags &= ~IX_HASHVALID
227
228     def validate(self, gitmode, sha):
229         assert(sha)
230         assert(gitmode)
231         assert(gitmode+0 == gitmode)
232         self.gitmode = gitmode
233         self.sha = sha
234         self.flags |= IX_HASHVALID|IX_EXISTS
235
236     def exists(self):
237         return not self.is_deleted()
238
239     def sha_missing(self):
240         return (self.flags & IX_SHAMISSING) or not (self.flags & IX_HASHVALID)
241
242     def is_deleted(self):
243         return (self.flags & IX_EXISTS) == 0
244
245     def set_deleted(self):
246         if self.flags & IX_EXISTS:
247             self.flags &= ~(IX_EXISTS | IX_HASHVALID)
248
249     def is_real(self):
250         return not self.is_fake()
251
252     def is_fake(self):
253         return not self.ctime
254
255     def __cmp__(a, b):
256         return (cmp(b.name, a.name)
257                 or cmp(a.is_valid(), b.is_valid())
258                 or cmp(a.is_fake(), b.is_fake()))
259
260     def write(self, f):
261         f.write(self.basename + '\0' + self.packed())
262
263
264 class NewEntry(Entry):
265     def __init__(self, basename, name, tmax, dev, ino, nlink,
266                  ctime, mtime, atime,
267                  uid, gid, size, mode, gitmode, sha, flags, meta_ofs,
268                  children_ofs, children_n):
269         Entry.__init__(self, basename, name, meta_ofs, tmax)
270         (self.dev, self.ino, self.nlink, self.ctime, self.mtime, self.atime,
271          self.uid, self.gid, self.size, self.mode, self.gitmode, self.sha,
272          self.flags, self.children_ofs, self.children_n
273          ) = (dev, ino, nlink, ctime, mtime, atime, uid, gid,
274               size, mode, gitmode, sha, flags, children_ofs, children_n)
275         self._fixup()
276
277
278 class BlankNewEntry(NewEntry):
279     def __init__(self, basename, meta_ofs, tmax):
280         NewEntry.__init__(self, basename, basename, tmax,
281                           0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
282                           0, EMPTY_SHA, 0, meta_ofs, 0, 0)
283
284
285 class ExistingEntry(Entry):
286     def __init__(self, parent, basename, name, m, ofs):
287         Entry.__init__(self, basename, name, None, None)
288         self.parent = parent
289         self._m = m
290         self._ofs = ofs
291         (self.dev, self.ino, self.nlink,
292          self.ctime, ctime_ns, self.mtime, mtime_ns, self.atime, atime_ns,
293          self.uid, self.gid, self.size, self.mode, self.gitmode, self.sha,
294          self.flags, self.children_ofs, self.children_n, self.meta_ofs
295          ) = struct.unpack(INDEX_SIG, str(buffer(m, ofs, ENTLEN)))
296         self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
297         self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
298         self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
299
300     # effectively, we don't bother messing with IX_SHAMISSING if
301     # not IX_HASHVALID, since it's redundant, and repacking is more
302     # expensive than not repacking.
303     # This is implemented by having sha_missing() check IX_HASHVALID too.
304     def set_sha_missing(self, val):
305         val = val and 1 or 0
306         oldval = self.sha_missing() and 1 or 0
307         if val != oldval:
308             flag = val and IX_SHAMISSING or 0
309             newflags = (self.flags & (~IX_SHAMISSING)) | flag
310             self.flags = newflags
311             self.repack()
312
313     def unset_sha_missing(self, flag):
314         if self.flags & IX_SHAMISSING:
315             self.flags &= ~IX_SHAMISSING
316             self.repack()
317
318     def repack(self):
319         self._m[self._ofs:self._ofs+ENTLEN] = self.packed()
320         if self.parent and not self.is_valid():
321             self.parent.invalidate()
322             self.parent.repack()
323
324     def iter(self, name=None, wantrecurse=None):
325         dname = name
326         if dname and not dname.endswith('/'):
327             dname += '/'
328         ofs = self.children_ofs
329         assert(ofs <= len(self._m))
330         assert(self.children_n < 1000000)
331         for i in xrange(self.children_n):
332             eon = self._m.find('\0', ofs)
333             assert(eon >= 0)
334             assert(eon >= ofs)
335             assert(eon > ofs)
336             basename = str(buffer(self._m, ofs, eon-ofs))
337             child = ExistingEntry(self, basename, self.name + basename,
338                                   self._m, eon+1)
339             if (not dname
340                  or child.name.startswith(dname)
341                  or child.name.endswith('/') and dname.startswith(child.name)):
342                 if not wantrecurse or wantrecurse(child):
343                     for e in child.iter(name=name, wantrecurse=wantrecurse):
344                         yield e
345             if not name or child.name == name or child.name.startswith(dname):
346                 yield child
347             ofs = eon + 1 + ENTLEN
348
349     def __iter__(self):
350         return self.iter()
351             
352
353 class Reader:
354     def __init__(self, filename):
355         self.filename = filename
356         self.m = ''
357         self.writable = False
358         self.count = 0
359         f = None
360         try:
361             f = open(filename, 'r+')
362         except IOError, e:
363             if e.errno == errno.ENOENT:
364                 pass
365             else:
366                 raise
367         if f:
368             b = f.read(len(INDEX_HDR))
369             if b != INDEX_HDR:
370                 log('warning: %s: header: expected %r, got %r\n'
371                                  % (filename, INDEX_HDR, b))
372             else:
373                 st = os.fstat(f.fileno())
374                 if st.st_size:
375                     self.m = mmap_readwrite(f)
376                     self.writable = True
377                     self.count = struct.unpack(FOOTER_SIG,
378                           str(buffer(self.m, st.st_size-FOOTLEN, FOOTLEN)))[0]
379
380     def __del__(self):
381         self.close()
382
383     def __len__(self):
384         return int(self.count)
385
386     def forward_iter(self):
387         ofs = len(INDEX_HDR)
388         while ofs+ENTLEN <= len(self.m)-FOOTLEN:
389             eon = self.m.find('\0', ofs)
390             assert(eon >= 0)
391             assert(eon >= ofs)
392             assert(eon > ofs)
393             basename = str(buffer(self.m, ofs, eon-ofs))
394             yield ExistingEntry(None, basename, basename, self.m, eon+1)
395             ofs = eon + 1 + ENTLEN
396
397     def iter(self, name=None, wantrecurse=None):
398         if len(self.m) > len(INDEX_HDR)+ENTLEN:
399             dname = name
400             if dname and not dname.endswith('/'):
401                 dname += '/'
402             root = ExistingEntry(None, '/', '/',
403                                  self.m, len(self.m)-FOOTLEN-ENTLEN)
404             for sub in root.iter(name=name, wantrecurse=wantrecurse):
405                 yield sub
406             if not dname or dname == root.name:
407                 yield root
408
409     def __iter__(self):
410         return self.iter()
411
412     def exists(self):
413         return self.m
414
415     def save(self):
416         if self.writable and self.m:
417             self.m.flush()
418
419     def close(self):
420         self.save()
421         if self.writable and self.m:
422             self.m.close()
423             self.m = None
424             self.writable = False
425
426     def filter(self, prefixes, wantrecurse=None):
427         for (rp, path) in reduce_paths(prefixes):
428             for e in self.iter(rp, wantrecurse=wantrecurse):
429                 assert(e.name.startswith(rp))
430                 name = path + e.name[len(rp):]
431                 yield (name, e)
432
433
434 # FIXME: this function isn't very generic, because it splits the filename
435 # in an odd way and depends on a terminating '/' to indicate directories.
436 def pathsplit(p):
437     """Split a path into a list of elements of the file system hierarchy."""
438     l = p.split('/')
439     l = [i+'/' for i in l[:-1]] + l[-1:]
440     if l[-1] == '':
441         l.pop()  # extra blank caused by terminating '/'
442     return l
443
444
445 class Writer:
446     def __init__(self, filename, metastore, tmax):
447         self.rootlevel = self.level = Level([], None)
448         self.f = None
449         self.count = 0
450         self.lastfile = None
451         self.filename = None
452         self.filename = filename = realpath(filename)
453         self.metastore = metastore
454         self.tmax = tmax
455         (dir,name) = os.path.split(filename)
456         (ffd,self.tmpname) = tempfile.mkstemp('.tmp', filename, dir)
457         self.f = os.fdopen(ffd, 'wb', 65536)
458         self.f.write(INDEX_HDR)
459
460     def __del__(self):
461         self.abort()
462
463     def abort(self):
464         f = self.f
465         self.f = None
466         if f:
467             f.close()
468             os.unlink(self.tmpname)
469
470     def flush(self):
471         if self.level:
472             self.level = _golevel(self.level, self.f, [], None,
473                                   self.metastore, self.tmax)
474             self.count = self.rootlevel.count
475             if self.count:
476                 self.count += 1
477             self.f.write(struct.pack(FOOTER_SIG, self.count))
478             self.f.flush()
479         assert(self.level == None)
480
481     def close(self):
482         self.flush()
483         f = self.f
484         self.f = None
485         if f:
486             f.close()
487             os.rename(self.tmpname, self.filename)
488
489     def _add(self, ename, entry):
490         if self.lastfile and self.lastfile <= ename:
491             raise Error('%r must come before %r' 
492                              % (''.join(e.name), ''.join(self.lastfile)))
493         self.lastfile = ename
494         self.level = _golevel(self.level, self.f, ename, entry,
495                               self.metastore, self.tmax)
496
497     def add(self, name, st, meta_ofs, hashgen = None):
498         endswith = name.endswith('/')
499         ename = pathsplit(name)
500         basename = ename[-1]
501         #log('add: %r %r\n' % (basename, name))
502         flags = IX_EXISTS
503         sha = None
504         if hashgen:
505             (gitmode, sha) = hashgen(name)
506             flags |= IX_HASHVALID
507         else:
508             (gitmode, sha) = (0, EMPTY_SHA)
509         if st:
510             isdir = stat.S_ISDIR(st.st_mode)
511             assert(isdir == endswith)
512             e = NewEntry(basename, name, self.tmax,
513                          st.st_dev, st.st_ino, st.st_nlink,
514                          st.st_ctime, st.st_mtime, st.st_atime,
515                          st.st_uid, st.st_gid,
516                          st.st_size, st.st_mode, gitmode, sha, flags,
517                          meta_ofs, 0, 0)
518         else:
519             assert(endswith)
520             meta_ofs = self.metastore.store(metadata.Metadata())
521             e = BlankNewEntry(basename, meta_ofs, self.tmax)
522             e.gitmode = gitmode
523             e.sha = sha
524             e.flags = flags
525         self._add(ename, e)
526
527     def add_ixentry(self, e):
528         e.children_ofs = e.children_n = 0
529         self._add(pathsplit(e.name), e)
530
531     def new_reader(self):
532         self.flush()
533         return Reader(self.tmpname)
534
535
536 def reduce_paths(paths):
537     xpaths = []
538     for p in paths:
539         rp = realpath(p)
540         try:
541             st = os.lstat(rp)
542             if stat.S_ISDIR(st.st_mode):
543                 rp = slashappend(rp)
544                 p = slashappend(p)
545             xpaths.append((rp, p))
546         except OSError, e:
547             add_error('reduce_paths: %s' % e)
548     xpaths.sort()
549
550     paths = []
551     prev = None
552     for (rp, p) in xpaths:
553         if prev and (prev == rp 
554                      or (prev.endswith('/') and rp.startswith(prev))):
555             continue # already superceded by previous path
556         paths.append((rp, p))
557         prev = rp
558     paths.sort(reverse=True)
559     return paths
560
561 def merge(*iters):
562     def pfunc(count, total):
563         qprogress('bup: merging indexes (%d/%d)\r' % (count, total))
564     def pfinal(count, total):
565         progress('bup: merging indexes (%d/%d), done.\n' % (count, total))
566     return merge_iter(iters, 1024, pfunc, pfinal, key='name')