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