]> arthur.barton.de Git - bup.git/blob - lib/bup/vfs.py
vfs: try_lresolve() was a bad idea. Create try_resolve() instead.
[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         if self.parent:
188             return self.parent.top()
189         else:
190             return self
191
192     def _lresolve(self, parts):
193         #log('_lresolve %r in %r\n' % (parts, self.name))
194         if not parts:
195             return self
196         (first, rest) = (parts[0], parts[1:])
197         if first == '.':
198             return self._lresolve(rest)
199         elif first == '..':
200             if not self.parent:
201                 raise NoSuchFile("no parent dir for %r" % self.name)
202             return self.parent._lresolve(rest)
203         elif rest:
204             return self.sub(first)._lresolve(rest)
205         else:
206             return self.sub(first)
207
208     # walk into a given sub-path of this node.  If the last element is
209     # a symlink, leave it as a symlink, don't resolve it.  (like lstat())
210     def lresolve(self, path):
211         start = self
212         if not path:
213             return start
214         if path.startswith('/'):
215             start = self.top()
216             path = path[1:]
217         parts = re.split(r'/+', path or '.')
218         if not parts[-1]:
219             parts[-1] = '.'
220         #log('parts: %r %r\n' % (path, parts))
221         return start._lresolve(parts)
222
223     # walk into the given sub-path of this node, and dereference it if it
224     # was a symlink.
225     def resolve(self, path = ''):
226         return self.lresolve(path).lresolve('.')
227
228     # like resolve(), but don't worry if the last symlink points at an
229     # invalid path.
230     # (still returns an error if any intermediate nodes were invalid)
231     def try_resolve(self, path = ''):
232         n = self.lresolve(path)
233         try:
234             n = n.lresolve('.')
235         except NoSuchFile:
236             pass
237         return n
238     
239     def nlinks(self):
240         if self._subs == None:
241             self._mksubs()
242         return 1
243
244     def size(self):
245         return 0
246
247     def open(self):
248         raise NotFile('%s is not a regular file' % self.name)
249
250
251 class File(Node):
252     def __init__(self, parent, name, mode, hash, bupmode):
253         Node.__init__(self, parent, name, mode, hash)
254         self.bupmode = bupmode
255         self._cached_size = None
256         self._filereader = None
257         
258     def open(self):
259         # You'd think FUSE might call this only once each time a file is
260         # opened, but no; it's really more of a refcount, and it's called
261         # once per read().  Thus, it's important to cache the filereader
262         # object here so we're not constantly re-seeking.
263         if not self._filereader:
264             self._filereader = _FileReader(self.hash, self.size(),
265                                            self.bupmode == git.BUP_CHUNKED)
266         self._filereader.seek(0)
267         return self._filereader
268     
269     def size(self):
270         if self._cached_size == None:
271             log('<<<<File.size() is calculating...\n')
272             if self.bupmode == git.BUP_CHUNKED:
273                 self._cached_size = _total_size(self.hash)
274             else:
275                 self._cached_size = _chunk_len(self.hash)
276             log('<<<<File.size() done.\n')
277         return self._cached_size
278
279
280 _symrefs = 0
281 class Symlink(File):
282     def __init__(self, parent, name, hash, bupmode):
283         File.__init__(self, parent, name, 0120000, hash, bupmode)
284
285     def size(self):
286         return len(self.readlink())
287
288     def readlink(self):
289         return ''.join(cp().join(self.hash.encode('hex')))
290
291     def dereference(self):
292         global _symrefs
293         if _symrefs > 100:
294             raise TooManySymlinks('too many levels of symlinks: %r'
295                                   % self.fullname())
296         _symrefs += 1
297         try:
298             return self.parent.lresolve(self.readlink())
299         except NoSuchFile:
300             raise NoSuchFile("%s: broken symlink to %r"
301                              % (self.fullname(), self.readlink()))
302         finally:
303             _symrefs -= 1
304
305     def _lresolve(self, parts):
306         return self.dereference()._lresolve(parts)
307     
308
309 class FakeSymlink(Symlink):
310     def __init__(self, parent, name, toname):
311         Symlink.__init__(self, parent, name, EMPTY_SHA, git.BUP_NORMAL)
312         self.toname = toname
313         
314     def readlink(self):
315         return self.toname
316     
317
318 class Dir(Node):
319     def _mksubs(self):
320         self._subs = {}
321         it = cp().get(self.hash.encode('hex'))
322         type = it.next()
323         if type == 'commit':
324             del it
325             it = cp().get(self.hash.encode('hex') + ':')
326             type = it.next()
327         assert(type == 'tree')
328         for (mode,mangled_name,sha) in git._treeparse(''.join(it)):
329             mode = int(mode, 8)
330             name = mangled_name
331             (name,bupmode) = git.demangle_name(mangled_name)
332             if bupmode == git.BUP_CHUNKED:
333                 mode = 0100644
334             if stat.S_ISDIR(mode):
335                 self._subs[name] = Dir(self, name, mode, sha)
336             elif stat.S_ISLNK(mode):
337                 self._subs[name] = Symlink(self, name, sha, bupmode)
338             else:
339                 self._subs[name] = File(self, name, mode, sha, bupmode)
340                 
341
342 class CommitList(Node):
343     def __init__(self, parent, name, hash):
344         Node.__init__(self, parent, name, 040000, hash)
345         
346     def _mksubs(self):
347         self._subs = {}
348         revs = list(git.rev_list(self.hash.encode('hex')))
349         for (date, commit) in revs:
350             l = time.localtime(date)
351             ls = time.strftime('%Y-%m-%d-%H%M%S', l)
352             commithex = '.' + commit.encode('hex')
353             n1 = Dir(self, commithex, 040000, commit)
354             n2 = FakeSymlink(self, ls, commithex)
355             n1.ctime = n1.mtime = n2.ctime = n2.mtime = date
356             self._subs[commithex] = n1
357             self._subs[ls] = n2
358             latest = max(revs)
359         if latest:
360             (date, commit) = latest
361             commithex = '.' + commit.encode('hex')
362             n2 = FakeSymlink(self, 'latest', commithex)
363             n2.ctime = n2.mtime = date
364             self._subs['latest'] = n2
365
366     
367 class RefList(Node):
368     def __init__(self, parent):
369         Node.__init__(self, parent, '/', 040000, EMPTY_SHA)
370         
371     def _mksubs(self):
372         self._subs = {}
373         for (name,sha) in git.list_refs():
374             if name.startswith('refs/heads/'):
375                 name = name[11:]
376                 date = git.rev_get_date(sha.encode('hex'))
377                 n1 = CommitList(self, name, sha)
378                 n1.ctime = n1.mtime = date
379                 self._subs[name] = n1
380         
381