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