2007-11-20 08:21:50 +00:00
#! /usr/bin/env python
'''
Tahoe thin - client fuse module .
2008-01-13 02:54:33 +00:00
See the accompanying README for configuration / usage details .
2007-11-20 20:58:42 +00:00
2007-11-20 08:21:50 +00:00
Goals :
2007-11-20 20:58:42 +00:00
2007-11-20 08:21:50 +00:00
- Delegate to Tahoe webapi as much as possible .
2007-11-20 20:58:42 +00:00
- Thin rather than clever . ( Even when that means clunky . )
2008-01-13 02:54:33 +00:00
Warts :
2007-11-20 20:58:42 +00:00
- Reads cache entire file contents , violating the thinness goal . Can we GET spans of files ?
- Single threaded .
2008-01-13 02:54:33 +00:00
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 .
2008-01-13 04:41:26 +00:00
Wishlist :
2008-06-01 02:21:42 +00:00
- Perhaps integrate cli aliases or root_dir . cap .
2008-01-13 02:54:33 +00:00
- Research pkg_resources ; see if it can replace the try - import - except - import - error pattern .
2008-06-01 02:21:42 +00:00
- Switch to logging instead of homebrew logging .
2007-11-20 08:21:50 +00:00
'''
#import bindann
#bindann.install_exception_handler()
2008-01-19 07:18:54 +00:00
import sys , stat , os , errno , urllib , time
2007-11-20 08:21:50 +00:00
2008-01-08 19:21:21 +00:00
try :
import simplejson
except ImportError , e :
2008-01-13 02:55:38 +00:00
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 .
2008-06-01 02:06:00 +00:00
If you run this from the Tahoe source directory , use this command :
PYTHONPATH = " $PYTHONPATH:./support/lib/python %d . %d /site-packages/ " python % s
''' % (sys.version_info[:2] + ( ' ' .join(sys.argv),)))
2008-01-08 19:21:21 +00:00
try :
import fuse
except ImportError , e :
2008-01-13 02:55:38 +00:00
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.
2007-11-20 08:21:50 +00:00
# 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
2008-01-13 00:06:39 +00:00
UnknownSize = - 1
2007-11-20 08:21:50 +00:00
2008-01-21 03:56:27 +00:00
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 ' ]
2008-06-01 02:22:18 +00:00
log_init ( basedir )
log ( ' Commandline: %r ' , sys . argv )
2008-01-21 03:56:27 +00:00
fs = TahoeFS ( basedir )
2007-11-20 08:21:50 +00:00
fs . main ( )
2008-01-19 07:18:54 +00:00
### Utilities for debug:
2008-06-01 02:22:18 +00:00
_logfile = None # Private to log* functions.
def log_init ( confdir ) :
2008-01-19 07:18:54 +00:00
global _logfile
2008-06-01 02:22:18 +00:00
logpath = os . path . join ( confdir , ' logs ' , ' tahoe_fuse.log ' )
_logfile = open ( logpath , ' a ' )
log ( ' Log opened at: %s \n ' , time . strftime ( ' % Y- % m- %d % H: % M: % S ' ) )
def log ( msg , * args ) :
2008-01-19 07:18:54 +00:00
_logfile . write ( ( msg % args ) + ' \n ' )
_logfile . flush ( )
2008-01-21 03:26:57 +00:00
def trace_calls ( m ) :
2007-11-20 08:21:50 +00:00
def dbmeth ( self , * a , * * kw ) :
pid = self . GetContext ( ) [ ' pid ' ]
2008-01-19 07:18:54 +00:00
log ( ' [ %d %r ] \n %s %r %r ' , pid , get_cmdline ( pid ) , m . __name__ , a , kw )
2007-11-20 08:21:50 +00:00
try :
r = m ( self , * a , * * kw )
if ( type ( r ) is int ) and ( r < 0 ) :
2008-01-19 07:18:54 +00:00
log ( ' -> - %s \n ' , errno . errorcode [ - r ] , )
2007-11-20 08:21:50 +00:00
else :
repstr = repr ( r ) [ : 256 ]
2008-01-19 07:18:54 +00:00
log ( ' -> %s \n ' , repstr )
2007-11-20 08:21:50 +00:00
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 ]
2008-01-21 03:26:57 +00:00
class SystemError ( Exception ) :
2007-11-20 08:21:50 +00:00
def __init__ ( self , eno ) :
self . eno = eno
Exception . __init__ ( self , errno . errorcode [ eno ] )
@staticmethod
2008-01-21 03:26:57 +00:00
def wrap_returns ( meth ) :
2007-11-20 08:21:50 +00:00
def wrapper ( * args , * * kw ) :
try :
return meth ( * args , * * kw )
2008-01-21 03:26:57 +00:00
except SystemError , e :
2007-11-20 08:21:50 +00:00
return - e . eno
wrapper . __name__ = meth . __name__
return wrapper
### Heart of the Matter:
class TahoeFS ( fuse . Fuse ) :
def __init__ ( self , confdir ) :
2008-01-19 07:18:54 +00:00
log ( ' Initializing with confdir = %r ' , confdir )
2007-11-20 08:21:50 +00:00
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 ( )
2007-11-20 21:00:01 +00:00
self . _init_rootdir ( )
2007-11-20 08:21:50 +00:00
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 , )
2008-01-13 00:06:39 +00:00
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. \n Please see " The CLI " in " docs/using.html " . \n ' % ( sys . argv [ 0 ] , rootdirfn ) )
else :
raise le
self . rootdir = TahoeDir ( self . url , canonicalize_cap ( cap ) )
2007-11-20 08:21:50 +00:00
def _get_node ( self , path ) :
assert path . startswith ( ' / ' )
if path == ' / ' :
2007-11-20 21:00:01 +00:00
return self . rootdir . resolve_path ( [ ] )
2007-11-20 08:21:50 +00:00
else :
parts = path . split ( ' / ' ) [ 1 : ]
2007-11-20 21:00:01 +00:00
return self . rootdir . resolve_path ( parts )
2007-11-20 08:21:50 +00:00
def _get_contents ( self , path ) :
node = self . _get_node ( path )
contents = node . open ( ) . read ( )
self . filecontents [ path ] = contents
return contents
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def getattr ( self , path ) :
node = self . _get_node ( path )
return node . getattr ( )
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def getdir ( self , path ) :
"""
return : [ ( name , typeflag ) , . . . ]
"""
node = self . _get_node ( path )
return node . getdir ( )
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def mythread ( self ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def chmod ( self , path , mode ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def chown ( self , path , uid , gid ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def fsync ( self , path , isFsyncFile ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def link ( self , target , link ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def mkdir ( self , path , mode ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def mknod ( self , path , mode , dev_ignored ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
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 :
2008-01-19 07:18:54 +00:00
log ( ' Flag not supported: %s ' , fname )
2008-01-21 03:26:57 +00:00
raise SystemError ( errno . ENOSYS )
2007-11-20 08:21:50 +00:00
self . _get_contents ( path )
return 0
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def read ( self , path , length , offset ) :
return self . _get_contents ( path ) [ offset : length ]
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def release ( self , path ) :
del self . filecontents [ path ]
return 0
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def readlink ( self , path ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def rename ( self , oldpath , newpath ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def rmdir ( self , path ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
#@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def statfs ( self ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def symlink ( self , targetPath , linkPath ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def truncate ( self , path , size ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def unlink ( self , path ) :
return - errno . ENOSYS
2008-01-21 03:26:57 +00:00
@trace_calls
@SystemError.wrap_returns
2007-11-20 08:21:50 +00:00
def utime ( self , path , times ) :
return - errno . ENOSYS
class TahoeNode ( object ) :
NextInode = 0
@staticmethod
def make ( baseurl , uri ) :
typefield = uri . split ( ' : ' , 2 ) [ 1 ]
2008-01-08 19:21:21 +00:00
# FIXME: is this check correct?
if uri . find ( ' URI:DIR2 ' ) != - 1 :
2007-11-20 08:21:50 +00:00
return TahoeDir ( baseurl , uri )
else :
return TahoeFile ( baseurl , uri )
def __init__ ( self , baseurl , uri ) :
self . burl = baseurl
self . uri = uri
2007-12-20 00:54:40 +00:00
self . fullurl = ' %s /uri/ %s ' % ( self . burl , self . uri )
2007-11-20 08:21:50 +00:00
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
2008-01-19 07:18:54 +00:00
log ( ' *** Fetching: %r ' , url )
2007-11-20 08:21:50 +00:00
return urllib . urlopen ( url )
class TahoeFile ( TahoeNode ) :
def __init__ ( self , baseurl , uri ) :
2008-01-13 00:06:39 +00:00
#assert uri.split(':', 2)[1] in ('CHK', 'LIT'), `uri` # fails as of 0.7.0
2007-11-20 08:21:50 +00:00
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 ) :
2008-01-13 00:06:39 +00:00
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
2007-11-20 08:21:50 +00:00
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
2008-01-13 00:06:39 +00:00
def canonicalize_cap ( cap ) :
2008-01-13 04:41:07 +00:00
cap = urllib . unquote ( cap )
2008-01-13 00:06:39 +00:00
i = cap . find ( ' URI: ' )
assert i != - 1 , ' A cap must contain " URI:... " , but this does not: ' + cap
return cap [ i : ]
2007-11-20 08:21:50 +00:00
if __name__ == ' __main__ ' :
main ( )