]> arthur.barton.de Git - bup.git/blob - lib/bup/vfs.py
91566da1dcc29a5fad934a055259694094b13c8e
[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 class FileReader:
27     def __init__(self, node):
28         self.n = node
29         self.ofs = 0
30         self.size = self.n.size()
31
32     def seek(self, ofs):
33         if ofs > self.size:
34             self.ofs = self.size
35         elif ofs < 0:
36             self.ofs = 0
37         else:
38             self.ofs = ofs
39
40     def tell(self):
41         return self.ofs
42
43     def read(self, count = -1):
44         if count < 0:
45             count = self.size - self.ofs
46         buf = self.n.readbytes(self.ofs, count)
47         self.ofs += len(buf)
48         return buf
49
50
51 class Node:
52     def __init__(self, parent, name, mode, hash):
53         self.parent = parent
54         self.name = name
55         self.mode = mode
56         self.hash = hash
57         self.ctime = self.mtime = self.atime = 0
58         self._subs = None
59         
60     def __cmp__(a, b):
61         return cmp(a.name or None, b.name or None)
62     
63     def __iter__(self):
64         return iter(self.subs())
65     
66     def fullname(self):
67         if self.parent:
68             return os.path.join(self.parent.fullname(), self.name)
69         else:
70             return self.name
71     
72     def _mksubs(self):
73         self._subs = {}
74         
75     def subs(self):
76         if self._subs == None:
77             self._mksubs()
78         return sorted(self._subs.values())
79         
80     def sub(self, name):
81         if self._subs == None:
82             self._mksubs()
83         ret = self._subs.get(name)
84         if not ret:
85             raise NoSuchFile("no file %r in %r" % (name, self.name))
86         return ret
87
88     def top(self):
89         if self.parent:
90             return self.parent.top()
91         else:
92             return self
93
94     def _lresolve(self, parts):
95         #log('_lresolve %r in %r\n' % (parts, self.name))
96         if not parts:
97             return self
98         (first, rest) = (parts[0], parts[1:])
99         if first == '.':
100             return self._lresolve(rest)
101         elif first == '..':
102             if not self.parent:
103                 raise NoSuchFile("no parent dir for %r" % self.name)
104             return self.parent._lresolve(rest)
105         elif rest:
106             return self.sub(first)._lresolve(rest)
107         else:
108             return self.sub(first)
109
110     def lresolve(self, path):
111         start = self
112         if path.startswith('/'):
113             start = self.top()
114             path = path[1:]
115         parts = re.split(r'/+', path or '.')
116         if not parts[-1]:
117             parts[-1] = '.'
118         #log('parts: %r %r\n' % (path, parts))
119         return start._lresolve(parts)
120
121     def resolve(self, path):
122         return self.lresolve(path).lresolve('')
123     
124     def nlinks(self):
125         if self._subs == None:
126             self._mksubs()
127         return 1
128
129     def size(self):
130         return 0
131
132     def open(self):
133         raise NotFile('%s is not a regular file' % self.name)
134     
135     def readbytes(self, ofs, count):
136         raise NotFile('%s is not a regular file' % self.name)
137     
138     def read(self, num = -1):
139         if num < 0:
140             num = self.size()
141         return self.readbytes(0, num)
142     
143     
144 class File(Node):
145     def __init__(self, parent, name, mode, hash):
146         Node.__init__(self, parent, name, mode, hash)
147         self._cached_size = None
148         
149     def _content(self):
150         return cp().join(self.hash.encode('hex'))
151
152     def open(self):
153         return FileReader(self)
154     
155     def size(self):
156         # FIXME inefficient.  If a file is chunked, we could just check
157         # the offset + size of the very last chunk.
158         if self._cached_size == None:
159             self._cached_size = sum(len(blob) for blob in self._content())
160         return self._cached_size
161     
162     def readbytes(self, ofs, count):
163         # FIXME inefficient.  If a file is chunked, we could choose to
164         # read only the required chunks rather than joining the whole thing.
165         buf = ''.join(self._content())
166         return buf[ofs:ofs+count]
167     
168
169 _symrefs = 0
170 class Symlink(File):
171     def __init__(self, parent, name, hash):
172         File.__init__(self, parent, name, 0120000, hash)
173
174     def readlink(self):
175         return self.read(1024)
176
177     def dereference(self):
178         global _symrefs
179         if _symrefs > 100:
180             raise TooManySymlinks('too many levels of symlinks: %r'
181                                   % self.fullname())
182         _symrefs += 1
183         try:
184             return self.parent.lresolve(self.readlink())
185         finally:
186             _symrefs -= 1
187
188     def _lresolve(self, parts):
189         return self.dereference()._lresolve(parts)
190     
191
192 class FakeSymlink(Symlink):
193     def __init__(self, parent, name, toname):
194         Symlink.__init__(self, parent, name, EMPTY_SHA)
195         self.toname = toname
196         
197     def _content(self):
198         return self.toname
199     
200
201 class Dir(Node):
202     def _mksubs(self):
203         self._subs = {}
204         it = cp().get(self.hash.encode('hex'))
205         type = it.next()
206         if type == 'commit':
207             del it
208             it = cp().get(self.hash.encode('hex') + ':')
209             type = it.next()
210         assert(type == 'tree')
211         for (mode,mangled_name,sha) in git._treeparse(''.join(it)):
212             mode = int(mode, 8)
213             name = mangled_name
214             (name,bupmode) = git.demangle_name(mangled_name)
215             if bupmode == git.BUP_CHUNKED:
216                 mode = 0100644
217             if stat.S_ISDIR(mode):
218                 self._subs[name] = Dir(self, name, mode, sha)
219             elif stat.S_ISLNK(mode):
220                 self._subs[name] = Symlink(self, name, sha)
221             else:
222                 self._subs[name] = File(self, name, mode, sha)
223                 
224
225 class CommitList(Node):
226     def __init__(self, parent, name, hash):
227         Node.__init__(self, parent, name, 040000, hash)
228         
229     def _mksubs(self):
230         self._subs = {}
231         revs = list(git.rev_list(self.hash.encode('hex')))
232         for (date, commit) in revs:
233             l = time.localtime(date)
234             ls = time.strftime('%Y-%m-%d-%H%M%S', l)
235             commithex = '.' + commit.encode('hex')
236             n1 = Dir(self, commithex, 040000, commit)
237             n2 = FakeSymlink(self, ls, commithex)
238             n1.ctime = n1.mtime = n2.ctime = n2.mtime = date
239             self._subs[commithex] = n1
240             self._subs[ls] = n2
241             latest = max(revs)
242         if latest:
243             (date, commit) = latest
244             commithex = '.' + commit.encode('hex')
245             n2 = FakeSymlink(self, 'latest', commithex)
246             n2.ctime = n2.mtime = date
247             self._subs['latest'] = n2
248
249     
250 class RefList(Node):
251     def __init__(self, parent):
252         Node.__init__(self, parent, '/', 040000, EMPTY_SHA)
253         
254     def _mksubs(self):
255         self._subs = {}
256         for (name,sha) in git.list_refs():
257             if name.startswith('refs/heads/'):
258                 name = name[11:]
259                 date = git.rev_get_date(sha.encode('hex'))
260                 n1 = CommitList(self, name, sha)
261                 n1.ctime = n1.mtime = date
262                 self._subs[name] = n1
263         
264