mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-22 14:22:25 +00:00
de022b2115
The commandline option handling of the version of python-fuse I use is arcane. This is an ugly hack.
464 lines
12 KiB
Python
464 lines
12 KiB
Python
#! /usr/bin/env python
|
|
'''
|
|
Tahoe thin-client fuse module.
|
|
|
|
See the accompanying README for configuration/usage details.
|
|
|
|
Goals:
|
|
|
|
- Delegate to Tahoe webapi as much as possible.
|
|
- Thin rather than clever. (Even when that means clunky.)
|
|
|
|
|
|
Warts:
|
|
|
|
- Reads cache entire file contents, violating the thinness goal. Can we GET spans of files?
|
|
- Single threaded.
|
|
|
|
|
|
Road-map:
|
|
|
|
1. Add unit tests where possible with little code modification.
|
|
2. Make unit tests pass for a variety of python-fuse module versions.
|
|
3. Modify the design to make possible unit test coverage of larger portions of code.
|
|
|
|
In parallel:
|
|
*. Make system tests which launch a client, mount a fuse interface, and excercise the whole stack.
|
|
|
|
Wishlist:
|
|
- Reuse allmydata.uri to check/canonicalize uris.
|
|
- Research pkg_resources; see if it can replace the try-import-except-import-error pattern.
|
|
'''
|
|
|
|
|
|
#import bindann
|
|
#bindann.install_exception_handler()
|
|
|
|
import sys, stat, os, errno, urllib, time
|
|
|
|
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: Check for non-working fuse versions here.
|
|
# FIXME: Make this work for all common python-fuse versions.
|
|
|
|
# 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
|
|
UnknownSize = -1
|
|
|
|
|
|
def main():
|
|
basedir = os.path.expanduser(TahoeConfigDir)
|
|
|
|
for i, arg in enumerate(sys.argv):
|
|
if arg == '--basedir':
|
|
try:
|
|
basedir = sys.argv[i+1]
|
|
sys.argv[i:i+2] = []
|
|
except IndexError:
|
|
sys.argv = [sys.argv[0], '--help']
|
|
|
|
print 'DEBUG:', sys.argv
|
|
|
|
fs = TahoeFS(basedir)
|
|
fs.main()
|
|
|
|
|
|
### Utilities for debug:
|
|
_logfile = None
|
|
def log(msg, *args):
|
|
global _logfile
|
|
if _logfile is None:
|
|
confdir = os.path.expanduser(TahoeConfigDir)
|
|
path = os.path.join(confdir, 'logs', 'tahoe_fuse.log')
|
|
_logfile = open(path, 'a')
|
|
_logfile.write('Log opened at: %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'),))
|
|
_logfile.write((msg % args) + '\n')
|
|
_logfile.flush()
|
|
|
|
|
|
def trace_calls(m):
|
|
def dbmeth(self, *a, **kw):
|
|
pid = self.GetContext()['pid']
|
|
log('[%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):
|
|
log('-> -%s\n', errno.errorcode[-r],)
|
|
else:
|
|
repstr = repr(r)[:256]
|
|
log('-> %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 SystemError (Exception):
|
|
def __init__(self, eno):
|
|
self.eno = eno
|
|
Exception.__init__(self, errno.errorcode[eno])
|
|
|
|
@staticmethod
|
|
def wrap_returns(meth):
|
|
def wrapper(*args, **kw):
|
|
try:
|
|
return meth(*args, **kw)
|
|
except SystemError, e:
|
|
return -e.eno
|
|
wrapper.__name__ = meth.__name__
|
|
return wrapper
|
|
|
|
|
|
### Heart of the Matter:
|
|
class TahoeFS (fuse.Fuse):
|
|
def __init__(self, confdir):
|
|
log('Initializing with confdir = %r', 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_rootdir()
|
|
|
|
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_rootdir(self):
|
|
# For now we just use the same default as the CLI:
|
|
rootdirfn = os.path.join(self.confdir, 'private', 'root_dir.cap')
|
|
try:
|
|
f = open(rootdirfn, 'r')
|
|
cap = 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 it was not found.\nPlease see "The CLI" in "docs/using.html".\n' % (sys.argv[0], rootdirfn))
|
|
else:
|
|
raise le
|
|
|
|
self.rootdir = TahoeDir(self.url, canonicalize_cap(cap))
|
|
|
|
def _get_node(self, path):
|
|
assert path.startswith('/')
|
|
if path == '/':
|
|
return self.rootdir.resolve_path([])
|
|
else:
|
|
parts = path.split('/')[1:]
|
|
return self.rootdir.resolve_path(parts)
|
|
|
|
def _get_contents(self, path):
|
|
node = self._get_node(path)
|
|
contents = node.open().read()
|
|
self.filecontents[path] = contents
|
|
return contents
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def getattr(self, path):
|
|
node = self._get_node(path)
|
|
return node.getattr()
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def getdir(self, path):
|
|
"""
|
|
return: [(name, typeflag), ... ]
|
|
"""
|
|
node = self._get_node(path)
|
|
return node.getdir()
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def mythread(self):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def chmod(self, path, mode):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def chown(self, path, uid, gid):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def fsync(self, path, isFsyncFile):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def link(self, target, link):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def mkdir(self, path, mode):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def mknod(self, path, mode, dev_ignored):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
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:
|
|
log('Flag not supported: %s', fname)
|
|
raise SystemError(errno.ENOSYS)
|
|
|
|
self._get_contents(path)
|
|
return 0
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def read(self, path, length, offset):
|
|
return self._get_contents(path)[offset:length]
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def release(self, path):
|
|
del self.filecontents[path]
|
|
return 0
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def readlink(self, path):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def rename(self, oldpath, newpath):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def rmdir(self, path):
|
|
return -errno.ENOSYS
|
|
|
|
#@trace_calls
|
|
@SystemError.wrap_returns
|
|
def statfs(self):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def symlink ( self, targetPath, linkPath ):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def truncate(self, path, size):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
def unlink(self, path):
|
|
return -errno.ENOSYS
|
|
|
|
@trace_calls
|
|
@SystemError.wrap_returns
|
|
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
|
|
log('*** Fetching: %r', url)
|
|
return urllib.urlopen(url)
|
|
|
|
|
|
class TahoeFile (TahoeNode):
|
|
def __init__(self, baseurl, uri):
|
|
#assert uri.split(':', 2)[1] in ('CHK', 'LIT'), `uri` # fails as of 0.7.0
|
|
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):
|
|
rawsize = self.get_metadata()[1]['size']
|
|
if type(rawsize) is not int: # FIXME: What about sizes which do not fit in python int?
|
|
assert rawsize == u'?', `rawsize`
|
|
return UnknownSize
|
|
else:
|
|
return rawsize
|
|
|
|
def resolve_path(self, path):
|
|
assert path == []
|
|
return self
|
|
|
|
|
|
class TahoeDir (TahoeNode):
|
|
def __init__(self, baseurl, 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
|
|
|
|
|
|
def canonicalize_cap(cap):
|
|
cap = urllib.unquote(cap)
|
|
i = cap.find('URI:')
|
|
assert i != -1, 'A cap must contain "URI:...", but this does not: ' + cap
|
|
return cap[i:]
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|