]> arthur.barton.de Git - bup.git/blob - lib/bup/vfs.py
7964c9825f2bde56de7a64a10a2ea69cdd707eb8
[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 _content(self):
146         return cp().join(self.hash.encode('hex'))
147
148     def open(self):
149         return FileReader(self)
150     
151     def size(self):
152         # FIXME inefficient.  If a file is chunked, we could just check
153         # the offset + size of the very last chunk.
154         return sum(len(blob) for blob in self._content())
155     
156     def readbytes(self, ofs, count):
157         # FIXME inefficient.  If a file is chunked, we could choose to
158         # read only the required chunks rather than joining the whole thing.
159         buf = ''.join(self._content())
160         return buf[ofs:ofs+count]
161     
162
163 _symrefs = 0
164 class Symlink(File):
165     def __init__(self, parent, name, hash):
166         File.__init__(self, parent, name, 0120000, hash)
167
168     def readlink(self):
169         return self.read(1024)
170
171     def dereference(self):
172         global _symrefs
173         if _symrefs > 100:
174             raise TooManySymlinks('too many levels of symlinks: %r'
175                                   % self.fullname())
176         _symrefs += 1
177         try:
178             return self.parent.lresolve(self.readlink())
179         finally:
180             _symrefs -= 1
181
182     def _lresolve(self, parts):
183         return self.dereference()._lresolve(parts)
184     
185
186 class FakeSymlink(Symlink):
187     def __init__(self, parent, name, toname):
188         Symlink.__init__(self, parent, name, EMPTY_SHA)
189         self.toname = toname
190         
191     def _content(self):
192         return self.toname
193     
194
195 class Dir(Node):
196     def _mksubs(self):
197         self._subs = {}
198         it = cp().get(self.hash.encode('hex'))
199         type = it.next()
200         if type == 'commit':
201             del it
202             it = cp().get(self.hash.encode('hex') + ':')
203             type = it.next()
204         assert(type == 'tree')
205         for (mode,mangled_name,sha) in git._treeparse(''.join(it)):
206             mode = int(mode, 8)
207             name = mangled_name
208             (name,bupmode) = git.demangle_name(mangled_name)
209             if bupmode == git.BUP_CHUNKED:
210                 mode = 0100644
211             if stat.S_ISDIR(mode):
212                 self._subs[name] = Dir(self, name, mode, sha)
213             elif stat.S_ISLNK(mode):
214                 self._subs[name] = Symlink(self, name, sha)
215             else:
216                 self._subs[name] = File(self, name, mode, sha)
217                 
218
219 class CommitList(Node):
220     def __init__(self, parent, name, hash):
221         Node.__init__(self, parent, name, 040000, hash)
222         
223     def _mksubs(self):
224         self._subs = {}
225         revs = list(git.rev_list(self.hash.encode('hex')))
226         for (date, commit) in revs:
227             l = time.localtime(date)
228             ls = time.strftime('%Y-%m-%d-%H%M%S', l)
229             commithex = '.' + commit.encode('hex')
230             n1 = Dir(self, commithex, 040000, commit)
231             n2 = FakeSymlink(self, ls, commithex)
232             n1.ctime = n1.mtime = n2.ctime = n2.mtime = date
233             self._subs[commithex] = n1
234             self._subs[ls] = n2
235             latest = max(revs)
236         if latest:
237             (date, commit) = latest
238             commithex = '.' + commit.encode('hex')
239             n2 = FakeSymlink(self, 'latest', commithex)
240             n2.ctime = n2.mtime = date
241             self._subs['latest'] = n2
242
243     
244 class RefList(Node):
245     def __init__(self, parent):
246         Node.__init__(self, parent, '/', 040000, EMPTY_SHA)
247         
248     def _mksubs(self):
249         self._subs = {}
250         for (name,sha) in git.list_refs():
251             if name.startswith('refs/heads/'):
252                 name = name[11:]
253                 date = git.rev_get_date(sha.encode('hex'))
254                 n1 = CommitList(self, name, sha)
255                 n1.ctime = n1.mtime = date
256                 self._subs[name] = n1
257         
258