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