]> arthur.barton.de Git - bup.git/blob - lib/bup/vfs.py
vfs: correctly handle reading small files.
[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
93     def next(self, size):
94         out = ''
95         while len(out) < size:
96             if self.it and not self.blob:
97                 try:
98                     self.blob = self.it.next()
99                 except StopIteration:
100                     self.it = None
101             if self.blob:
102                 want = size - len(out)
103                 out += self.blob[:want]
104                 self.blob = self.blob[want:]
105             if not self.it:
106                 break
107         log('next(%d) returned %d\n' % (size, len(out)))
108         return out
109
110
111 class _FileReader:
112     def __init__(self, hash, size, isdir):
113         self.hash = hash
114         self.ofs = 0
115         self.size = size
116         self.isdir = isdir
117         self.reader = None
118
119     def seek(self, ofs):
120         if self.ofs == ofs:
121             return
122         self.reader = None
123         if ofs > self.size:
124             self.ofs = self.size
125         elif ofs < 0:
126             self.ofs = 0
127         else:
128             self.ofs = ofs
129
130     def tell(self):
131         return self.ofs
132
133     def read(self, count = -1):
134         if count < 0:
135             count = self.size - self.ofs
136         if not self.reader:
137             self.reader = _ChunkReader(self.hash, self.isdir, self.ofs)
138         try:
139             buf = self.reader.next(count)
140         except:
141             self.reader = None
142             raise  # our offsets will be all screwed up otherwise
143         self.ofs += len(buf)
144         return buf
145
146
147 class Node:
148     def __init__(self, parent, name, mode, hash):
149         self.parent = parent
150         self.name = name
151         self.mode = mode
152         self.hash = hash
153         self.ctime = self.mtime = self.atime = 0
154         self._subs = None
155         
156     def __cmp__(a, b):
157         return cmp(a.name or None, b.name or None)
158     
159     def __iter__(self):
160         return iter(self.subs())
161     
162     def fullname(self):
163         if self.parent:
164             return os.path.join(self.parent.fullname(), self.name)
165         else:
166             return self.name
167     
168     def _mksubs(self):
169         self._subs = {}
170         
171     def subs(self):
172         if self._subs == None:
173             self._mksubs()
174         return sorted(self._subs.values())
175         
176     def sub(self, name):
177         if self._subs == None:
178             self._mksubs()
179         ret = self._subs.get(name)
180         if not ret:
181             raise NoSuchFile("no file %r in %r" % (name, self.name))
182         return ret
183
184     def top(self):
185         if self.parent:
186             return self.parent.top()
187         else:
188             return self
189
190     def _lresolve(self, parts):
191         #log('_lresolve %r in %r\n' % (parts, self.name))
192         if not parts:
193             return self
194         (first, rest) = (parts[0], parts[1:])
195         if first == '.':
196             return self._lresolve(rest)
197         elif first == '..':
198             if not self.parent:
199                 raise NoSuchFile("no parent dir for %r" % self.name)
200             return self.parent._lresolve(rest)
201         elif rest:
202             return self.sub(first)._lresolve(rest)
203         else:
204             return self.sub(first)
205
206     def lresolve(self, path):
207         start = self
208         if path.startswith('/'):
209             start = self.top()
210             path = path[1:]
211         parts = re.split(r'/+', path or '.')
212         if not parts[-1]:
213             parts[-1] = '.'
214         #log('parts: %r %r\n' % (path, parts))
215         return start._lresolve(parts)
216
217     def resolve(self, path):
218         return self.lresolve(path).lresolve('')
219     
220     def nlinks(self):
221         if self._subs == None:
222             self._mksubs()
223         return 1
224
225     def size(self):
226         return 0
227
228     def open(self):
229         raise NotFile('%s is not a regular file' % self.name)
230
231
232 class File(Node):
233     def __init__(self, parent, name, mode, hash, bupmode):
234         Node.__init__(self, parent, name, mode, hash)
235         self.bupmode = bupmode
236         self._cached_size = None
237         self._filereader = None
238         
239     def open(self):
240         # You'd think FUSE might call this only once each time a file is
241         # opened, but no; it's really more of a refcount, and it's called
242         # once per read().  Thus, it's important to cache the filereader
243         # object here so we're not constantly re-seeking.
244         if not self._filereader:
245             self._filereader = _FileReader(self.hash, self.size(),
246                                            self.bupmode == git.BUP_CHUNKED)
247         return self._filereader
248     
249     def size(self):
250         if self._cached_size == None:
251             log('<<<<File.size() is calculating\n')
252             if self.bupmode == git.BUP_CHUNKED:
253                 self._cached_size = _total_size(self.hash)
254             else:
255                 self._cached_size = _chunk_len(self.hash)
256         return self._cached_size
257
258
259 _symrefs = 0
260 class Symlink(File):
261     def __init__(self, parent, name, hash, bupmode):
262         File.__init__(self, parent, name, 0120000, hash, bupmode)
263
264     def size(self):
265         return len(self.readlink())
266
267     def readlink(self):
268         return ''.join(cp().join(self.hash.encode('hex')))
269
270     def dereference(self):
271         global _symrefs
272         if _symrefs > 100:
273             raise TooManySymlinks('too many levels of symlinks: %r'
274                                   % self.fullname())
275         _symrefs += 1
276         try:
277             return self.parent.lresolve(self.readlink())
278         finally:
279             _symrefs -= 1
280
281     def _lresolve(self, parts):
282         return self.dereference()._lresolve(parts)
283     
284
285 class FakeSymlink(Symlink):
286     def __init__(self, parent, name, toname):
287         Symlink.__init__(self, parent, name, EMPTY_SHA, git.BUP_NORMAL)
288         self.toname = toname
289         
290     def readlink(self):
291         return self.toname
292     
293
294 class Dir(Node):
295     def _mksubs(self):
296         self._subs = {}
297         it = cp().get(self.hash.encode('hex'))
298         type = it.next()
299         if type == 'commit':
300             del it
301             it = cp().get(self.hash.encode('hex') + ':')
302             type = it.next()
303         assert(type == 'tree')
304         for (mode,mangled_name,sha) in git._treeparse(''.join(it)):
305             mode = int(mode, 8)
306             name = mangled_name
307             (name,bupmode) = git.demangle_name(mangled_name)
308             if bupmode == git.BUP_CHUNKED:
309                 mode = 0100644
310             if stat.S_ISDIR(mode):
311                 self._subs[name] = Dir(self, name, mode, sha)
312             elif stat.S_ISLNK(mode):
313                 self._subs[name] = Symlink(self, name, sha, bupmode)
314             else:
315                 self._subs[name] = File(self, name, mode, sha, bupmode)
316                 
317
318 class CommitList(Node):
319     def __init__(self, parent, name, hash):
320         Node.__init__(self, parent, name, 040000, hash)
321         
322     def _mksubs(self):
323         self._subs = {}
324         revs = list(git.rev_list(self.hash.encode('hex')))
325         for (date, commit) in revs:
326             l = time.localtime(date)
327             ls = time.strftime('%Y-%m-%d-%H%M%S', l)
328             commithex = '.' + commit.encode('hex')
329             n1 = Dir(self, commithex, 040000, commit)
330             n2 = FakeSymlink(self, ls, commithex)
331             n1.ctime = n1.mtime = n2.ctime = n2.mtime = date
332             self._subs[commithex] = n1
333             self._subs[ls] = n2
334             latest = max(revs)
335         if latest:
336             (date, commit) = latest
337             commithex = '.' + commit.encode('hex')
338             n2 = FakeSymlink(self, 'latest', commithex)
339             n2.ctime = n2.mtime = date
340             self._subs['latest'] = n2
341
342     
343 class RefList(Node):
344     def __init__(self, parent):
345         Node.__init__(self, parent, '/', 040000, EMPTY_SHA)
346         
347     def _mksubs(self):
348         self._subs = {}
349         for (name,sha) in git.list_refs():
350             if name.startswith('refs/heads/'):
351                 name = name[11:]
352                 date = git.rev_get_date(sha.encode('hex'))
353                 n1 = CommitList(self, name, sha)
354                 n1.ctime = n1.mtime = date
355                 self._subs[name] = n1
356         
357