3 Tahoe thin-client fuse module.
5 See the accompanying README for configuration/usage details.
9 - Delegate to Tahoe webapi as much as possible.
10 - Thin rather than clever. (Even when that means clunky.)
15 - Reads cache entire file contents, violating the thinness goal. Can we GET spans of files?
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.
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.
32 #bindann.install_exception_handler()
34 import sys, stat, os, errno, urllib, time
38 except ImportError, e:
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.
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),)))
51 except ImportError, e:
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
58 # FIXME: Check for non-working fuse versions here.
59 # FIXME: Make this work for all common python-fuse versions.
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.
66 TahoeConfigDir = '~/.tahoe'
72 basedir = os.path.expanduser(TahoeConfigDir)
74 for i, arg in enumerate(sys.argv):
75 if arg == '--basedir':
77 basedir = sys.argv[i+1]
80 sys.argv = [sys.argv[0], '--help']
84 log('Commandline: %r', sys.argv)
90 ### Utilities for debug:
91 _logfile = None # Private to log* functions.
93 def log_init(confdir):
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'))
102 _logfile.write((msg % args) + '\n')
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)
111 r = m(self, *a, **kw)
112 if (type(r) is int) and (r < 0):
113 log('-> -%s\n', errno.errorcode[-r],)
115 repstr = repr(r)[:256]
116 log('-> %s\n', repstr)
119 sys.excepthook(*sys.exc_info())
124 def get_cmdline(pid):
125 f = open('/proc/%d/cmdline' % pid, 'r')
126 args = f.read().split('\0')
128 assert args[-1] == ''
132 class SystemError (Exception):
133 def __init__(self, eno):
135 Exception.__init__(self, errno.errorcode[eno])
138 def wrap_returns(meth):
139 def wrapper(*args, **kw):
141 return meth(*args, **kw)
142 except SystemError, e:
144 wrapper.__name__ = meth.__name__
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
155 self.flags = 0 # FIXME: What goes here?
156 self.multithreaded = 0
158 # silly path-based file handles.
159 self.filecontents = {} # {path -> contents}
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('/'):
170 f = open(os.path.join(self.confdir, 'webport'), 'r')
173 fields = contents.split(':')
174 proto, port = fields[:2]
175 assert proto == 'tcp'
177 self.url = 'http://localhost:%d' % (port,)
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')
183 f = open(rootdirfn, 'r')
184 cap = f.read().strip()
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))
193 self.rootdir = TahoeDir(self.url, canonicalize_cap(cap))
195 def _get_node(self, path):
196 assert path.startswith('/')
198 return self.rootdir.resolve_path([])
200 parts = path.split('/')[1:]
201 return self.rootdir.resolve_path(parts)
203 def _get_contents(self, path):
204 contents = self.filecontents.get(path)
206 node = self._get_node(path)
207 contents = node.open().read()
208 self.filecontents[path] = contents
212 @SystemError.wrap_returns
213 def getattr(self, path):
214 node = self._get_node(path)
215 return node.getattr()
218 @SystemError.wrap_returns
219 def getdir(self, path):
221 return: [(name, typeflag), ... ]
223 node = self._get_node(path)
227 @SystemError.wrap_returns
232 @SystemError.wrap_returns
233 def chmod(self, path, mode):
237 @SystemError.wrap_returns
238 def chown(self, path, uid, gid):
242 @SystemError.wrap_returns
243 def fsync(self, path, isFsyncFile):
247 @SystemError.wrap_returns
248 def link(self, target, link):
252 @SystemError.wrap_returns
253 def mkdir(self, path, mode):
257 @SystemError.wrap_returns
258 def mknod(self, path, mode, dev_ignored):
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:
272 log('Flag not supported: %s', fname)
273 raise SystemError(errno.ENOSYS)
275 self._get_contents(path)
279 @SystemError.wrap_returns
280 def read(self, path, length, offset):
281 return self._get_contents(path)[offset:length]
284 @SystemError.wrap_returns
285 def release(self, path):
286 del self.filecontents[path]
290 @SystemError.wrap_returns
291 def readlink(self, path):
295 @SystemError.wrap_returns
296 def rename(self, oldpath, newpath):
300 @SystemError.wrap_returns
301 def rmdir(self, path):
305 @SystemError.wrap_returns
310 @SystemError.wrap_returns
311 def symlink ( self, targetPath, linkPath ):
315 @SystemError.wrap_returns
316 def truncate(self, path, size):
320 @SystemError.wrap_returns
321 def unlink(self, path):
325 @SystemError.wrap_returns
326 def utime(self, path, times):
330 class TahoeNode (object):
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)
340 return TahoeFile(baseurl, uri)
342 def __init__(self, baseurl, uri):
343 if not baseurl.endswith('/'):
347 self.fullurl = '%suri/%s' % (self.burl, self.uri)
348 self.inode = TahoeNode.NextInode
349 TahoeNode.NextInode += 1
353 - st_mode (protection bits)
354 - st_ino (inode number)
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).
365 # FIXME: Return metadata that isn't completely fabricated.
366 return (self.get_mode(),
369 self.get_linkcount(),
377 def get_metadata(self):
378 f = self.open('?t=json')
381 return simplejson.loads(json)
383 def open(self, postfix=''):
384 url = self.fullurl + postfix
385 log('*** Fetching: %r', url)
386 return urllib.urlopen(url)
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)
396 return stat.S_IFREG | 0400 # Read only regular file.
398 def get_linkcount(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`
409 def resolve_path(self, path):
414 class TahoeDir (TahoeNode):
415 def __init__(self, baseurl, uri):
416 TahoeNode.__init__(self, baseurl, uri)
418 self.mode = stat.S_IFDIR | 0500 # Read only directory.
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()))
430 return stat.S_IFDIR | 0500 # Read only directory.
432 def get_linkcount(self):
433 return len(self.getdir())
436 return 2 ** 12 # FIXME: What do we return here? len(self.get_metadata())
438 def resolve_path(self, path):
439 assert type(path) is list
443 child = self.get_child(head)
444 return child.resolve_path(path[1:])
448 def get_child(self, name):
449 c = self.get_children()
452 def get_children(self):
453 flag, md = self.get_metadata()
454 assert flag == 'dirnode'
457 for name, (childflag, childmd) in md['children'].items():
458 if childflag == 'dirnode':
463 c[str(name)] = cls(self.burl, childmd['ro_uri'])
467 def canonicalize_cap(cap):
468 cap = urllib.unquote(cap)
470 assert i != -1, 'A cap must contain "URI:...", but this does not: ' + cap
474 if __name__ == '__main__':