tahoe-lafs/contrib/tahoe-fuse.py

423 lines
12 KiB
Python
Raw Normal View History

#! /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<VERSION>/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()