1 import os, re, stat, time
14 class NodeError(Exception):
16 class NoSuchFile(NodeError):
18 class NotDir(NodeError):
20 class NotFile(NodeError):
22 class TooManySymlinks(NodeError):
27 it = cp().get(hash.encode('hex'))
29 assert(type == 'tree')
30 return git._treeparse(''.join(it))
33 def _tree_decode(hash):
34 tree = [(int(name,16),stat.S_ISDIR(int(mode,8)),sha)
37 assert(tree == list(sorted(tree)))
42 return sum(len(b) for b in cp().join(hash.encode('hex')))
45 def _last_chunk_info(hash):
46 tree = _tree_decode(hash)
48 (ofs,isdir,sha) = tree[-1]
50 (subofs, sublen) = _last_chunk_info(sha)
51 return (ofs+subofs, sublen)
53 return (ofs, _chunk_len(sha))
56 def _total_size(hash):
57 (lastofs, lastsize) = _last_chunk_info(hash)
58 return lastofs + lastsize
61 def _chunkiter(hash, startofs):
63 tree = _tree_decode(hash)
65 # skip elements before startofs
66 for i in xrange(len(tree)):
67 if i+1 >= len(tree) or tree[i+1][0] > startofs:
71 # iterate through what's left
72 for i in xrange(first, len(tree)):
73 (ofs,isdir,sha) = tree[i]
74 skipmore = startofs-ofs
78 for b in _chunkiter(sha, skipmore):
81 yield ''.join(cp().join(sha.encode('hex')))[skipmore:]
85 def __init__(self, hash, isdir, startofs):
87 self.it = _chunkiter(hash, startofs)
91 self.blob = ''.join(cp().join(hash.encode('hex')))[startofs:]
96 while len(out) < size:
97 if self.it and not self.blob:
99 self.blob = self.it.next()
100 except StopIteration:
103 want = size - len(out)
104 out += self.blob[:want]
105 self.blob = self.blob[want:]
108 log('next(%d) returned %d\n' % (size, len(out)))
114 def __init__(self, hash, size, isdir):
132 def read(self, count = -1):
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)
138 buf = self.reader.next(count)
141 raise # our offsets will be all screwed up otherwise
150 def __init__(self, parent, name, mode, hash):
155 self.ctime = self.mtime = self.atime = 0
159 return cmp(a.name or None, b.name or None)
162 return iter(self.subs())
166 return os.path.join(self.parent.fullname(), self.name)
174 if self._subs == None:
176 return sorted(self._subs.values())
179 if self._subs == None:
181 ret = self._subs.get(name)
183 raise NoSuchFile("no file %r in %r" % (name, self.name))
187 """ Return the very top node of the tree. """
189 return self.parent.top()
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()
201 def _lresolve(self, parts):
202 #log('_lresolve %r in %r\n' % (parts, self.name))
205 (first, rest) = (parts[0], parts[1:])
207 return self._lresolve(rest)
210 raise NoSuchFile("no parent dir for %r" % self.name)
211 return self.parent._lresolve(rest)
213 return self.sub(first)._lresolve(rest)
215 return self.sub(first)
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):
223 if path.startswith('/'):
225 start = self.fs_top()
229 parts = re.split(r'/+', path or '.')
232 #log('parts: %r %r\n' % (path, parts))
233 return start._lresolve(parts)
235 # walk into the given sub-path of this node, and dereference it if it
237 def resolve(self, path = ''):
238 return self.lresolve(path).lresolve('.')
240 # like resolve(), but don't worry if the last symlink points at an
242 # (still returns an error if any intermediate nodes were invalid)
243 def try_resolve(self, path = ''):
244 n = self.lresolve(path)
252 if self._subs == None:
260 raise NotFile('%s is not a regular file' % self.name)
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
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
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)
287 self._cached_size = _chunk_len(self.hash)
288 log('<<<<File.size() done.\n')
289 return self._cached_size
294 def __init__(self, parent, name, hash, bupmode):
295 File.__init__(self, parent, name, 0120000, hash, bupmode)
298 return len(self.readlink())
301 return ''.join(cp().join(self.hash.encode('hex')))
303 def dereference(self):
306 raise TooManySymlinks('too many levels of symlinks: %r'
310 return self.parent.lresolve(self.readlink(),
313 raise NoSuchFile("%s: broken symlink to %r"
314 % (self.fullname(), self.readlink()))
318 def _lresolve(self, parts):
319 return self.dereference()._lresolve(parts)
322 class FakeSymlink(Symlink):
323 def __init__(self, parent, name, toname):
324 Symlink.__init__(self, parent, name, EMPTY_SHA, git.BUP_NORMAL)
334 it = cp().get(self.hash.encode('hex'))
338 it = cp().get(self.hash.encode('hex') + ':')
340 assert(type == 'tree')
341 for (mode,mangled_name,sha) in git._treeparse(''.join(it)):
344 (name,bupmode) = git.demangle_name(mangled_name)
345 if bupmode == git.BUP_CHUNKED:
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)
352 self._subs[name] = File(self, name, mode, sha, bupmode)
355 class CommitList(Node):
356 def __init__(self, parent, name, hash):
357 Node.__init__(self, parent, name, 040000, hash)
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
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
381 def __init__(self, parent):
382 Node.__init__(self, parent, '/', 040000, EMPTY_SHA)
386 for (name,sha) in git.list_refs():
387 if name.startswith('refs/heads/'):
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