#! /usr/bin/env python ''' Tahoe thin-client fuse module. Usage Notes: This is a proof-of-concept, and not production quality. It uses the FUSE interface, and where bugs or unimplemented features are encountered the file system may become confused. In my experience with ubuntu's linux version 2.6.20-16-generic, and python-fuse version 2.5-5build1, the worst behavior is that processes which are accessing the fuse filesystem when some bugs occur hang. Also, the filesystem is currently single-threaded and blocking, so one bug interrupts all filesystem client processes. The rest of my system seems stable even in these cases (the rest of the filesystem and other processes function). The current design caches EACH FILE ENTIRELY IN MEMORY as long as any process has that file open. Expect horrible memory usage. (But also, subsequent reads after the first should be fast. ;-) Goals: - Delegate to Tahoe webapi as much as possible. - Thin rather than clever. (Even when that means clunky.) Status Quo: - Reads cache entire file contents, violating the thinness goal. Can we GET spans of files? - Single threaded. ''' #import bindann #bindann.install_exception_handler() import sys, stat, os, errno, urllib try: import simplejson except ImportError, e: raise SystemExit('Could not import simplejson, which is bundled with Tahoe. Please update your PYTHONPATH environment variable to include the tahoe "support/lib/python/site-packages" directory.') try: import fuse except ImportError, e: raise SystemExit('Could not import fuse, the pythonic fuse bindings. This dependency of tahoe-fuse.py is *not* bundled with tahoe. Please install it. On debian/ubuntu systems run: sudo apt-get install python-fuse') # FIXME: Currently uses the old, silly path-based (non-stateful) interface: fuse.fuse_python_api = (0, 1) # Use the silly path-based api for now. ### Config: TahoeConfigDir = '~/.tahoe' MagicDevNumber = 42 def main(args = sys.argv[1:]): if not args: raise SystemExit("Usage: %s MOUNTPOINT\n\nThe argument MOUNTPOINT is an empty directory where you want to mount a tahoe filesystem.\n" % (sys.argv[0],)) fs = TahoeFS(os.path.expanduser(TahoeConfigDir)) fs.main() ### Utilities just for debug: def debugdeco(m): def dbmeth(self, *a, **kw): pid = self.GetContext()['pid'] print '[%d %r]\n%s%r%r' % (pid, get_cmdline(pid), m.__name__, a, kw) try: r = m(self, *a, **kw) if (type(r) is int) and (r < 0): print '-> -%s\n' % (errno.errorcode[-r],) else: repstr = repr(r)[:256] print '-> %s\n' % (repstr,) return r except: sys.excepthook(*sys.exc_info()) return dbmeth def get_cmdline(pid): f = open('/proc/%d/cmdline' % pid, 'r') args = f.read().split('\0') f.close() assert args[-1] == '' return args[:-1] class ErrnoExc (Exception): def __init__(self, eno): self.eno = eno Exception.__init__(self, errno.errorcode[eno]) @staticmethod def wrapped(meth): def wrapper(*args, **kw): try: return meth(*args, **kw) except ErrnoExc, e: return -e.eno wrapper.__name__ = meth.__name__ return wrapper ### Heart of the Matter: class TahoeFS (fuse.Fuse): def __init__(self, confdir): fuse.Fuse.__init__(self) self.confdir = confdir self.flags = 0 # FIXME: What goes here? self.multithreaded = 0 # silly path-based file handles. self.filecontents = {} # {path -> contents} self._init_url() self._init_bookmarks() def _init_url(self): f = open(os.path.join(self.confdir, 'webport'), 'r') contents = f.read() f.close() fields = contents.split(':') proto, port = fields[:2] assert proto == 'tcp' port = int(port) self.url = 'http://localhost:%d' % (port,) def _init_bookmarks(self): rootdirfn = os.path.join(self.confdir, 'private', 'root-dir.cap') try: f = open(rootdirfn, 'r') uri = f.read().strip() f.close() except EnvironmentError, le: # FIXME: This user-friendly help message may be platform-dependent because it checks the exception description. if le.args[1].find('No such file or directory') != -1: raise SystemExit('%s requires a directory capability in %s, but when it was not found.\nPlease see "The CLI" in "docs/using.html".\n' % (sys.argv[0], rootdirfn)) else: raise le self.bookmarks = TahoeDir(self.url, uri) def _get_node(self, path): assert path.startswith('/') if path == '/': return self.bookmarks.resolve_path([]) else: parts = path.split('/')[1:] return self.bookmarks.resolve_path(parts) def _get_contents(self, path): node = self._get_node(path) contents = node.open().read() self.filecontents[path] = contents return contents @debugdeco @ErrnoExc.wrapped def getattr(self, path): node = self._get_node(path) return node.getattr() @debugdeco @ErrnoExc.wrapped def getdir(self, path): """ return: [(name, typeflag), ... ] """ node = self._get_node(path) return node.getdir() @debugdeco @ErrnoExc.wrapped def mythread(self): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def chmod(self, path, mode): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def chown(self, path, uid, gid): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def fsync(self, path, isFsyncFile): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def link(self, target, link): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def mkdir(self, path, mode): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def mknod(self, path, mode, dev_ignored): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def open(self, path, mode): IgnoredFlags = os.O_RDONLY | os.O_NONBLOCK | os.O_SYNC | os.O_LARGEFILE # Note: IgnoredFlags are all ignored! for fname in dir(os): if fname.startswith('O_'): flag = getattr(os, fname) if flag & IgnoredFlags: continue elif mode & flag: print 'Flag not supported:', fname raise ErrnoExc(errno.ENOSYS) self._get_contents(path) return 0 @debugdeco @ErrnoExc.wrapped def read(self, path, length, offset): return self._get_contents(path)[offset:length] @debugdeco @ErrnoExc.wrapped def release(self, path): del self.filecontents[path] return 0 @debugdeco @ErrnoExc.wrapped def readlink(self, path): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def rename(self, oldpath, newpath): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def rmdir(self, path): return -errno.ENOSYS #@debugdeco @ErrnoExc.wrapped def statfs(self): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def symlink ( self, targetPath, linkPath ): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def truncate(self, path, size): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def unlink(self, path): return -errno.ENOSYS @debugdeco @ErrnoExc.wrapped def utime(self, path, times): return -errno.ENOSYS class TahoeNode (object): NextInode = 0 @staticmethod def make(baseurl, uri): typefield = uri.split(':', 2)[1] # FIXME: is this check correct? if uri.find('URI:DIR2') != -1: return TahoeDir(baseurl, uri) else: return TahoeFile(baseurl, uri) def __init__(self, baseurl, uri): self.burl = baseurl self.uri = uri self.fullurl = '%s/uri/%s' % (self.burl, self.uri) self.inode = TahoeNode.NextInode TahoeNode.NextInode += 1 def getattr(self): """ - st_mode (protection bits) - st_ino (inode number) - st_dev (device) - st_nlink (number of hard links) - st_uid (user ID of owner) - st_gid (group ID of owner) - st_size (size of file, in bytes) - st_atime (time of most recent access) - st_mtime (time of most recent content modification) - st_ctime (platform dependent; time of most recent metadata change on Unix, or the time of creation on Windows). """ # FIXME: Return metadata that isn't completely fabricated. return (self.get_mode(), self.inode, MagicDevNumber, self.get_linkcount(), os.getuid(), os.getgid(), self.get_size(), 0, 0, 0) def get_metadata(self): f = self.open('?t=json') json = f.read() f.close() return simplejson.loads(json) def open(self, postfix=''): url = self.fullurl + postfix print '*** Fetching:', `url` return urllib.urlopen(url) class TahoeFile (TahoeNode): def __init__(self, baseurl, uri): assert uri.split(':', 2)[1] in ('CHK', 'LIT'), `uri` TahoeNode.__init__(self, baseurl, uri) # nonfuse: def get_mode(self): return stat.S_IFREG | 0400 # Read only regular file. def get_linkcount(self): return 1 def get_size(self): return self.get_metadata()[1]['size'] def resolve_path(self, path): assert type(path) is list assert path == [] return self class TahoeDir (TahoeNode): def __init__(self, baseurl, uri): #assert uri.split(':', 2)[1] in ('DIR2', 'DIR2-RO'), `uri` TahoeNode.__init__(self, baseurl, uri) self.mode = stat.S_IFDIR | 0500 # Read only directory. # FUSE: def getdir(self): d = [('.', self.get_mode()), ('..', self.get_mode())] for name, child in self.get_children().items(): if name: # Just ignore this crazy case! d.append((name, child.get_mode())) return d # nonfuse: def get_mode(self): return stat.S_IFDIR | 0500 # Read only directory. def get_linkcount(self): return len(self.getdir()) def get_size(self): return 2 ** 12 # FIXME: What do we return here? len(self.get_metadata()) def resolve_path(self, path): assert type(path) is list if path: head = path[0] child = self.get_child(head) return child.resolve_path(path[1:]) else: return self def get_child(self, name): c = self.get_children() return c[name] def get_children(self): flag, md = self.get_metadata() assert flag == 'dirnode' c = {} for name, (childflag, childmd) in md['children'].items(): if childflag == 'dirnode': cls = TahoeDir else: cls = TahoeFile c[str(name)] = cls(self.burl, childmd['ro_uri']) return c if __name__ == '__main__': main()