]> arthur.barton.de Git - bup.git/blob - lib/bup/vfs.py
vfs: resolve absolute symlinks inside their particular backup set.
[bup.git] / lib / bup / vfs.py
1 import os, re, stat, time
2 from bup import git
3 from helpers import *
4
5 EMPTY_SHA='\0'*20
6
7 _cp = None
8 def cp():
9     global _cp
10     if not _cp:
11         _cp = git.CatPipe()
12     return _cp
13
14 class NodeError(Exception):
15     pass
16 class NoSuchFile(NodeError):
17     pass
18 class NotDir(NodeError):
19     pass
20 class NotFile(NodeError):
21     pass
22 class TooManySymlinks(NodeError):
23     pass
24
25
26 def _treeget(hash):
27     it = cp().get(hash.encode('hex'))
28     type = it.next()
29     assert(type == 'tree')
30     return git._treeparse(''.join(it))
31
32
33 def _tree_decode(hash):
34     tree = [(int(name,16),stat.S_ISDIR(int(mode,8)),sha)
35             for (mode,name,sha)
36             in _treeget(hash)]
37     assert(tree == list(sorted(tree)))
38     return tree
39
40
41 def _chunk_len(hash):
42     return sum(len(b) for b in cp().join(hash.encode('hex')))
43
44
45 def _last_chunk_info(hash):
46     tree = _tree_decode(hash)
47     assert(tree)
48     (ofs,isdir,sha) = tree[-1]
49     if isdir:
50         (subofs, sublen) = _last_chunk_info(sha)
51         return (ofs+subofs, sublen)
52     else:
53         return (ofs, _chunk_len(sha))
54
55
56 def _total_size(hash):
57     (lastofs, lastsize) = _last_chunk_info(hash)
58     return lastofs + lastsize
59
60
61 def _chunkiter(hash, startofs):
62     assert(startofs >= 0)
63     tree = _tree_decode(hash)
64
65     # skip elements before startofs
66     for i in xrange(len(tree)):
67         if i+1 >= len(tree) or tree[i+1][0] > startofs:
68             break
69     first = i
70
71     # iterate through what's left
72     for i in xrange(first, len(tree)):
73         (ofs,isdir,sha) = tree[i]
74         skipmore = startofs-ofs
75         if skipmore < 0:
76             skipmore = 0
77         if isdir:
78             for b in _chunkiter(sha, skipmore):
79                 yield b
80         else:
81             yield ''.join(cp().join(sha.encode('hex')))[skipmore:]
82
83
84 class _ChunkReader:
85     def __init__(self, hash, isdir, startofs):
86         if isdir:
87             self.it = _chunkiter(hash, startofs)
88             self.blob = None
89         else:
90             self.it = None
91             self.blob = ''.join(cp().join(hash.encode('hex')))[startofs:]
92         self.ofs = startofs
93
94     def next(self, size):
95         out = ''
96         while len(out) < size:
97             if self.it and not self.blob:
98                 try:
99                     self.blob = self.it.next()
100                 except StopIteration:
101                     self.it = None
102             if self.blob:
103                 want = size - len(out)
104                 out += self.blob[:want]
105                 self.blob = self.blob[want:]
106             if not self.it:
107                 break
108         log('next(%d) returned %d\n' % (size, len(out)))
109         self.ofs += len(out)
110         return out
111
112
113 class _FileReader:
114     def __init__(self, hash, size, isdir):
115         self.hash = hash
116         self.ofs = 0
117         self.size = size
118         self.isdir = isdir
119         self.reader = None
120
121     def seek(self, ofs):
122         if ofs > self.size:
123             self.ofs = self.size
124         elif ofs < 0:
125             self.ofs = 0
126         else:
127             self.ofs = ofs
128
129     def tell(self):
130         return self.ofs
131
132     def read(self, count = -1):
133         if count < 0:
134             count = self.size - self.ofs
135         if not self.reader or self.reader.ofs != self.ofs:
136             self.reader = _ChunkReader(self.hash, self.isdir, self.ofs)
137         try:
138             buf = self.reader.next(count)
139         except:
140             self.reader = None
141             raise  # our offsets will be all screwed up otherwise
142         self.ofs += len(buf)
143         return buf
144
145     def close(self):
146         pass
147
148
149 class Node:
150     def __init__(self, parent, name, mode, hash):
151         self.parent = parent
152         self.name = name
153         self.mode = mode
154         self.hash = hash
155         self.ctime = self.mtime = self.atime = 0
156         self._subs = None
157         
158     def __cmp__(a, b):
159         return cmp(a.name or None, b.name or None)
160     
161     def __iter__(self):
162         return iter(self.subs())
163     
164     def fullname(self):
165         if self.parent:
166             return os.path.join(self.parent.fullname(), self.name)
167         else:
168             return self.name
169     
170     def _mksubs(self):
171         self._subs = {}
172         
173     def subs(self):
174         if self._subs == None:
175             self._mksubs()
176         return sorted(self._subs.values())
177         
178     def sub(self, name):
179         if self._subs == None:
180             self._mksubs()
181         ret = self._subs.get(name)
182         if not ret:
183             raise NoSuchFile("no file %r in %r" % (name, self.name))
184         return ret
185
186     def top(self):
187         """ Return the very top node of the tree. """
188         if self.parent:
189             return self.parent.top()
190         else:
191             return self
192
193     def fs_top(self):
194         """ Return the top node of the particular backup set, or the root
195             level if this node isn't inside a backup set. """
196         if self.parent and not isinstance(self.parent, CommitList):
197             return self.parent.fs_top()
198         else:
199             return self
200
201     def _lresolve(self, parts):
202         #log('_lresolve %r in %r\n' % (parts, self.name))
203         if not parts:
204             return self
205         (first, rest) = (parts[0], parts[1:])
206         if first == '.':
207             return self._lresolve(rest)
208         elif first == '..':
209             if not self.parent:
210                 raise NoSuchFile("no parent dir for %r" % self.name)
211             return self.parent._lresolve(rest)
212         elif rest:
213             return self.sub(first)._lresolve(rest)
214         else:
215             return self.sub(first)
216
217     # walk into a given sub-path of this node.  If the last element is
218     # a symlink, leave it as a symlink, don't resolve it.  (like lstat())
219     def lresolve(self, path, stay_inside_fs=False):
220         start = self
221         if not path:
222             return start
223         if path.startswith('/'):
224             if stay_inside_fs:
225                 start = self.fs_top()
226             else:
227                 start = self.top()
228             path = path[1:]
229         parts = re.split(r'/+', path or '.')
230         if not parts[-1]:
231             parts[-1] = '.'
232         #log('parts: %r %r\n' % (path, parts))
233         return start._lresolve(parts)
234
235     # walk into the given sub-path of this node, and dereference it if it
236     # was a symlink.
237     def resolve(self, path = ''):
238         return self.lresolve(path).lresolve('.')
239
240     # like resolve(), but don't worry if the last symlink points at an
241     # invalid path.
242     # (still returns an error if any intermediate nodes were invalid)
243     def try_resolve(self, path = ''):
244         n = self.lresolve(path)
245         try:
246             n = n.lresolve('.')
247         except NoSuchFile:
248             pass
249         return n
250     
251     def nlinks(self):
252         if self._subs == None:
253             self._mksubs()
254         return 1
255
256     def size(self):
257         return 0
258
259     def open(self):
260         raise NotFile('%s is not a regular file' % self.name)
261
262
263 class File(Node):
264     def __init__(self, parent, name, mode, hash, bupmode):
265         Node.__init__(self, parent, name, mode, hash)
266         self.bupmode = bupmode
267         self._cached_size = None
268         self._filereader = None
269         
270     def open(self):
271         # You'd think FUSE might call this only once each time a file is
272         # opened, but no; it's really more of a refcount, and it's called
273         # once per read().  Thus, it's important to cache the filereader
274         # object here so we're not constantly re-seeking.
275         if not self._filereader:
276             self._filereader = _FileReader(self.hash, self.size(),
277                                            self.bupmode == git.BUP_CHUNKED)
278         self._filereader.seek(0)
279         return self._filereader
280     
281     def size(self):
282         if self._cached_size == None:
283             log('<<<<File.size() is calculating...\n')
284             if self.bupmode == git.BUP_CHUNKED:
285                 self._cached_size = _total_size(self.hash)
286             else:
287                 self._cached_size = _chunk_len(self.hash)
288             log('<<<<File.size() done.\n')
289         return self._cached_size
290
291
292 _symrefs = 0
293 class Symlink(File):
294     def __init__(self, parent, name, hash, bupmode):
295         File.__init__(self, parent, name, 0120000, hash, bupmode)
296
297     def size(self):
298         return len(self.readlink())
299
300     def readlink(self):
301         return ''.join(cp().join(self.hash.encode('hex')))
302
303     def dereference(self):
304         global _symrefs
305         if _symrefs > 100:
306             raise TooManySymlinks('too many levels of symlinks: %r'
307                                   % self.fullname())
308         _symrefs += 1
309         try:
310             return self.parent.lresolve(self.readlink(),
311                                         stay_inside_fs=True)
312         except NoSuchFile:
313             raise NoSuchFile("%s: broken symlink to %r"
314                              % (self.fullname(), self.readlink()))
315         finally:
316             _symrefs -= 1
317
318     def _lresolve(self, parts):
319         return self.dereference()._lresolve(parts)
320     
321
322 class FakeSymlink(Symlink):
323     def __init__(self, parent, name, toname):
324         Symlink.__init__(self, parent, name, EMPTY_SHA, git.BUP_NORMAL)
325         self.toname = toname
326         
327     def readlink(self):
328         return self.toname
329     
330
331 class Dir(Node):
332     def _mksubs(self):
333         self._subs = {}
334         it = cp().get(self.hash.encode('hex'))
335         type = it.next()
336         if type == 'commit':
337             del it
338             it = cp().get(self.hash.encode('hex') + ':')
339             type = it.next()
340         assert(type == 'tree')
341         for (mode,mangled_name,sha) in git._treeparse(''.join(it)):
342             mode = int(mode, 8)
343             name = mangled_name
344             (name,bupmode) = git.demangle_name(mangled_name)
345             if bupmode == git.BUP_CHUNKED:
346                 mode = 0100644
347             if stat.S_ISDIR(mode):
348                 self._subs[name] = Dir(self, name, mode, sha)
349             elif stat.S_ISLNK(mode):
350                 self._subs[name] = Symlink(self, name, sha, bupmode)
351             else:
352                 self._subs[name] = File(self, name, mode, sha, bupmode)
353                 
354
355 class CommitList(Node):
356     def __init__(self, parent, name, hash):
357         Node.__init__(self, parent, name, 040000, hash)
358         
359     def _mksubs(self):
360         self._subs = {}
361         revs = list(git.rev_list(self.hash.encode('hex')))
362         for (date, commit) in revs:
363             l = time.localtime(date)
364             ls = time.strftime('%Y-%m-%d-%H%M%S', l)
365             commithex = '.' + commit.encode('hex')
366             n1 = Dir(self, commithex, 040000, commit)
367             n2 = FakeSymlink(self, ls, commithex)
368             n1.ctime = n1.mtime = n2.ctime = n2.mtime = date
369             self._subs[commithex] = n1
370             self._subs[ls] = n2
371             latest = max(revs)
372         if latest:
373             (date, commit) = latest
374             commithex = '.' + commit.encode('hex')
375             n2 = FakeSymlink(self, 'latest', commithex)
376             n2.ctime = n2.mtime = date
377             self._subs['latest'] = n2
378
379     
380 class RefList(Node):
381     def __init__(self, parent):
382         Node.__init__(self, parent, '/', 040000, EMPTY_SHA)
383         
384     def _mksubs(self):
385         self._subs = {}
386         for (name,sha) in git.list_refs():
387             if name.startswith('refs/heads/'):
388                 name = name[11:]
389                 date = git.rev_get_date(sha.encode('hex'))
390                 n1 = CommitList(self, name, sha)
391                 n1.ctime = n1.mtime = date
392                 self._subs[name] = n1
393         
394