]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - contrib/fuse/impl_a/tahoe_fuse.py
add Protovis.js-based download-status timeline visualization
[tahoe-lafs/tahoe-lafs.git] / contrib / fuse / impl_a / tahoe_fuse.py
1 #! /usr/bin/env python
2 '''
3 Tahoe thin-client fuse module.
4
5 See the accompanying README for configuration/usage details.
6
7 Goals:
8
9 - Delegate to Tahoe webapi as much as possible.
10 - Thin rather than clever.  (Even when that means clunky.)
11
12
13 Warts:
14
15 - Reads cache entire file contents, violating the thinness goal.  Can we GET spans of files?
16 - Single threaded.
17
18
19 Road-map:
20 1. Add unit tests where possible with little code modification.
21 2. Make unit tests pass for a variety of python-fuse module versions.
22 3. Modify the design to make possible unit test coverage of larger portions of code.
23
24 Wishlist:
25 - Perhaps integrate cli aliases or root_dir.cap.
26 - Research pkg_resources; see if it can replace the try-import-except-import-error pattern.
27 - Switch to logging instead of homebrew logging.
28 '''
29
30
31 #import bindann
32 #bindann.install_exception_handler()
33
34 import sys, stat, os, errno, urllib, time
35
36 try:
37     import simplejson
38 except ImportError, e:
39     raise SystemExit('''\
40 Could not import simplejson, which is bundled with Tahoe.  Please
41 update your PYTHONPATH environment variable to include the tahoe
42 "support/lib/python<VERSION>/site-packages" directory.
43
44 If you run this from the Tahoe source directory, use this command:
45 PYTHONPATH="$PYTHONPATH:./support/lib/python%d.%d/site-packages/" python %s
46 ''' % (sys.version_info[:2] + (' '.join(sys.argv),)))
47     
48
49 try:
50     import fuse
51 except ImportError, e:
52     raise SystemExit('''\
53 Could not import fuse, the pythonic fuse bindings.  This dependency
54 of tahoe-fuse.py is *not* bundled with tahoe.  Please install it.
55 On debian/ubuntu systems run: sudo apt-get install python-fuse
56 ''')
57
58 # FIXME: Check for non-working fuse versions here.
59 # FIXME: Make this work for all common python-fuse versions.
60
61 # FIXME: Currently uses the old, silly path-based (non-stateful) interface:
62 fuse.fuse_python_api = (0, 1) # Use the silly path-based api for now.
63
64
65 ### Config:
66 TahoeConfigDir = '~/.tahoe'
67 MagicDevNumber = 42
68 UnknownSize = -1
69
70
71 def main():
72     basedir = os.path.expanduser(TahoeConfigDir)
73
74     for i, arg in enumerate(sys.argv):
75         if arg == '--basedir':
76             try:
77                 basedir = sys.argv[i+1]
78                 sys.argv[i:i+2] = []
79             except IndexError:
80                 sys.argv = [sys.argv[0], '--help']
81                 
82
83     log_init(basedir)
84     log('Commandline: %r', sys.argv)
85
86     fs = TahoeFS(basedir)
87     fs.main()
88
89
90 ### Utilities for debug:
91 _logfile = None # Private to log* functions.
92
93 def log_init(confdir):
94     global _logfile
95     
96     logpath = os.path.join(confdir, 'logs', 'tahoe_fuse.log')
97     _logfile = open(logpath, 'a')
98     log('Log opened at: %s\n', time.strftime('%Y-%m-%d %H:%M:%S'))
99
100
101 def log(msg, *args):
102     _logfile.write((msg % args) + '\n')
103     _logfile.flush()
104     
105     
106 def trace_calls(m):
107     def dbmeth(self, *a, **kw):
108         pid = self.GetContext()['pid']
109         log('[%d %r]\n%s%r%r', pid, get_cmdline(pid), m.__name__, a, kw)
110         try:
111             r = m(self, *a, **kw)
112             if (type(r) is int) and (r < 0):
113                 log('-> -%s\n', errno.errorcode[-r],)
114             else:
115                 repstr = repr(r)[:256]
116                 log('-> %s\n', repstr)
117             return r
118         except:
119             sys.excepthook(*sys.exc_info())
120             
121     return dbmeth
122
123
124 def get_cmdline(pid):
125     f = open('/proc/%d/cmdline' % pid, 'r')
126     args = f.read().split('\0')
127     f.close()
128     assert args[-1] == ''
129     return args[:-1]
130
131
132 class SystemError (Exception):
133     def __init__(self, eno):
134         self.eno = eno
135         Exception.__init__(self, errno.errorcode[eno])
136
137     @staticmethod
138     def wrap_returns(meth):
139         def wrapper(*args, **kw):
140             try:
141                 return meth(*args, **kw)
142             except SystemError, e:
143                 return -e.eno
144         wrapper.__name__ = meth.__name__
145         return wrapper
146
147
148 ### Heart of the Matter:
149 class TahoeFS (fuse.Fuse):
150     def __init__(self, confdir):
151         log('Initializing with confdir = %r', confdir)
152         fuse.Fuse.__init__(self)
153         self.confdir = confdir
154         
155         self.flags = 0 # FIXME: What goes here?
156         self.multithreaded = 0
157
158         # silly path-based file handles.
159         self.filecontents = {} # {path -> contents}
160
161         self._init_url()
162         self._init_rootdir()
163
164     def _init_url(self):
165         if os.path.exists(os.path.join(self.confdir, 'node.url')):
166             self.url = file(os.path.join(self.confdir, 'node.url'), 'rb').read().strip()
167             if not self.url.endswith('/'):
168                 self.url += '/'
169         else:
170             f = open(os.path.join(self.confdir, 'webport'), 'r')
171             contents = f.read()
172             f.close()
173             fields = contents.split(':')
174             proto, port = fields[:2]
175             assert proto == 'tcp'
176             port = int(port)
177             self.url = 'http://localhost:%d' % (port,)
178
179     def _init_rootdir(self):
180         # For now we just use the same default as the CLI:
181         rootdirfn = os.path.join(self.confdir, 'private', 'root_dir.cap')
182         try:
183             f = open(rootdirfn, 'r')
184             cap = f.read().strip()
185             f.close()
186         except EnvironmentError, le:
187             # FIXME: This user-friendly help message may be platform-dependent because it checks the exception description.
188             if le.args[1].find('No such file or directory') != -1:
189                 raise SystemExit('%s requires a directory capability in %s, but it was not found.\n' % (sys.argv[0], rootdirfn))
190             else:
191                 raise le
192
193         self.rootdir = TahoeDir(self.url, canonicalize_cap(cap))
194
195     def _get_node(self, path):
196         assert path.startswith('/')
197         if path == '/':
198             return self.rootdir.resolve_path([])
199         else:
200             parts = path.split('/')[1:]
201             return self.rootdir.resolve_path(parts)
202     
203     def _get_contents(self, path):
204         contents = self.filecontents.get(path)
205         if contents is None:
206             node = self._get_node(path)
207             contents = node.open().read()
208             self.filecontents[path] = contents
209         return contents
210     
211     @trace_calls
212     @SystemError.wrap_returns
213     def getattr(self, path):
214         node = self._get_node(path)
215         return node.getattr()
216                 
217     @trace_calls
218     @SystemError.wrap_returns
219     def getdir(self, path):
220         """
221         return: [(name, typeflag), ... ]
222         """
223         node = self._get_node(path)
224         return node.getdir()
225
226     @trace_calls
227     @SystemError.wrap_returns
228     def mythread(self):
229         return -errno.ENOSYS
230
231     @trace_calls
232     @SystemError.wrap_returns
233     def chmod(self, path, mode):
234         return -errno.ENOSYS
235
236     @trace_calls
237     @SystemError.wrap_returns
238     def chown(self, path, uid, gid):
239         return -errno.ENOSYS
240
241     @trace_calls
242     @SystemError.wrap_returns
243     def fsync(self, path, isFsyncFile):
244         return -errno.ENOSYS
245
246     @trace_calls
247     @SystemError.wrap_returns
248     def link(self, target, link):
249         return -errno.ENOSYS
250
251     @trace_calls
252     @SystemError.wrap_returns
253     def mkdir(self, path, mode):
254         return -errno.ENOSYS
255
256     @trace_calls
257     @SystemError.wrap_returns
258     def mknod(self, path, mode, dev_ignored):
259         return -errno.ENOSYS
260
261     @trace_calls
262     @SystemError.wrap_returns
263     def open(self, path, mode):
264         IgnoredFlags = os.O_RDONLY | os.O_NONBLOCK | os.O_SYNC | os.O_LARGEFILE 
265         # Note: IgnoredFlags are all ignored!
266         for fname in dir(os):
267             if fname.startswith('O_'):
268                 flag = getattr(os, fname)
269                 if flag & IgnoredFlags:
270                     continue
271                 elif mode & flag:
272                     log('Flag not supported: %s', fname)
273                     raise SystemError(errno.ENOSYS)
274
275         self._get_contents(path)
276         return 0
277
278     @trace_calls
279     @SystemError.wrap_returns
280     def read(self, path, length, offset):
281         return self._get_contents(path)[offset:length]
282
283     @trace_calls
284     @SystemError.wrap_returns
285     def release(self, path):
286         del self.filecontents[path]
287         return 0
288
289     @trace_calls
290     @SystemError.wrap_returns
291     def readlink(self, path):
292         return -errno.ENOSYS
293
294     @trace_calls
295     @SystemError.wrap_returns
296     def rename(self, oldpath, newpath):
297         return -errno.ENOSYS
298
299     @trace_calls
300     @SystemError.wrap_returns
301     def rmdir(self, path):
302         return -errno.ENOSYS
303
304     #@trace_calls
305     @SystemError.wrap_returns
306     def statfs(self):
307         return -errno.ENOSYS
308
309     @trace_calls
310     @SystemError.wrap_returns
311     def symlink ( self, targetPath, linkPath ):
312         return -errno.ENOSYS
313
314     @trace_calls
315     @SystemError.wrap_returns
316     def truncate(self, path, size):
317         return -errno.ENOSYS
318
319     @trace_calls
320     @SystemError.wrap_returns
321     def unlink(self, path):
322         return -errno.ENOSYS
323
324     @trace_calls
325     @SystemError.wrap_returns
326     def utime(self, path, times):
327         return -errno.ENOSYS
328
329
330 class TahoeNode (object):
331     NextInode = 0
332     
333     @staticmethod
334     def make(baseurl, uri):
335         typefield = uri.split(':', 2)[1]
336         # FIXME: is this check correct?
337         if uri.find('URI:DIR2') != -1:
338             return TahoeDir(baseurl, uri)
339         else:
340             return TahoeFile(baseurl, uri)
341         
342     def __init__(self, baseurl, uri):
343         if not baseurl.endswith('/'):
344             baseurl += '/'
345         self.burl = baseurl
346         self.uri = uri
347         self.fullurl = '%suri/%s' % (self.burl, self.uri)
348         self.inode = TahoeNode.NextInode
349         TahoeNode.NextInode += 1
350
351     def getattr(self):
352         """
353         - st_mode (protection bits)
354         - st_ino (inode number)
355         - st_dev (device)
356         - st_nlink (number of hard links)
357         - st_uid (user ID of owner)
358         - st_gid (group ID of owner)
359         - st_size (size of file, in bytes)
360         - st_atime (time of most recent access)
361         - st_mtime (time of most recent content modification)
362         - st_ctime (platform dependent; time of most recent metadata change on Unix,
363                     or the time of creation on Windows).
364         """
365         # FIXME: Return metadata that isn't completely fabricated.
366         return (self.get_mode(),
367                 self.inode,
368                 MagicDevNumber,
369                 self.get_linkcount(),
370                 os.getuid(),
371                 os.getgid(),
372                 self.get_size(),
373                 0,
374                 0,
375                 0)
376
377     def get_metadata(self):
378         f = self.open('?t=json')
379         json = f.read()
380         f.close()
381         return simplejson.loads(json)
382         
383     def open(self, postfix=''):
384         url = self.fullurl + postfix
385         log('*** Fetching: %r', url)
386         return urllib.urlopen(url)
387
388
389 class TahoeFile (TahoeNode):
390     def __init__(self, baseurl, uri):
391         #assert uri.split(':', 2)[1] in ('CHK', 'LIT'), `uri` # fails as of 0.7.0
392         TahoeNode.__init__(self, baseurl, uri)
393
394     # nonfuse:
395     def get_mode(self):
396         return stat.S_IFREG | 0400 # Read only regular file.
397
398     def get_linkcount(self):
399         return 1
400     
401     def get_size(self):
402         rawsize = self.get_metadata()[1]['size']
403         if type(rawsize) is not int: # FIXME: What about sizes which do not fit in python int?
404             assert rawsize == u'?', `rawsize`
405             return UnknownSize
406         else:
407             return rawsize
408     
409     def resolve_path(self, path):
410         assert path == []
411         return self
412     
413
414 class TahoeDir (TahoeNode):
415     def __init__(self, baseurl, uri):
416         TahoeNode.__init__(self, baseurl, uri)
417
418         self.mode = stat.S_IFDIR | 0500 # Read only directory.
419
420     # FUSE:
421     def getdir(self):
422         d = [('.', self.get_mode()), ('..', self.get_mode())]
423         for name, child in self.get_children().items():
424             if name: # Just ignore this crazy case!
425                 d.append((name, child.get_mode()))
426         return d
427
428     # nonfuse:
429     def get_mode(self):
430         return stat.S_IFDIR | 0500 # Read only directory.
431
432     def get_linkcount(self):
433         return len(self.getdir())
434     
435     def get_size(self):
436         return 2 ** 12 # FIXME: What do we return here?  len(self.get_metadata())
437     
438     def resolve_path(self, path):
439         assert type(path) is list
440
441         if path:
442             head = path[0]
443             child = self.get_child(head)
444             return child.resolve_path(path[1:])
445         else:
446             return self
447         
448     def get_child(self, name):
449         c = self.get_children()
450         return c[name]
451
452     def get_children(self):
453         flag, md = self.get_metadata()
454         assert flag == 'dirnode'
455
456         c = {}
457         for name, (childflag, childmd) in md['children'].items():
458             if childflag == 'dirnode':
459                 cls = TahoeDir
460             else:
461                 cls = TahoeFile
462
463             c[str(name)] = cls(self.burl, childmd['ro_uri'])
464         return c
465         
466         
467 def canonicalize_cap(cap):
468     cap = urllib.unquote(cap)
469     i = cap.find('URI:')
470     assert i != -1, 'A cap must contain "URI:...", but this does not: ' + cap
471     return cap[i:]
472     
473
474 if __name__ == '__main__':
475     main()
476