diff options
-rw-r--r-- | fs/base.py | 23 | ||||
-rwxr-xr-x | fs/expose/fuse.py | 338 | ||||
-rw-r--r-- | fs/expose/fuse/__init__.py | 425 | ||||
-rw-r--r-- | fs/expose/fuse/fuse_ctypes.py | 603 | ||||
-rw-r--r-- | fs/expose/sftp.py | 25 | ||||
-rw-r--r-- | fs/mountfs.py | 2 | ||||
-rw-r--r-- | fs/multifs.py | 2 | ||||
-rw-r--r-- | fs/osfs.py | 2 | ||||
-rw-r--r-- | fs/sftpfs.py | 2 | ||||
-rw-r--r-- | fs/tests/__init__.py | 2 | ||||
-rw-r--r-- | fs/tests/test_expose.py | 24 |
11 files changed, 1081 insertions, 367 deletions
@@ -738,3 +738,26 @@ class SubFS(FS): def rename(self, src, dst): return self.parent.rename(self._delegate(src), self._delegate(dst)) + +def flags_to_mode(flags): + """Convert an os.O_* bitmask into an FS mode string.""" + if flags & os.O_EXCL: + raise UnsupportedError("open",msg="O_EXCL is not supported") + if flags & os.O_WRONLY: + if flags & os.O_TRUNC: + mode = "w" + elif flags & os.O_APPEND: + mode = "a" + else: + mode = "r+" + elif flags & os.O_RDWR: + if flags & os.O_TRUNC: + mode = "w+" + elif flags & os.O_APPEND: + mode = "a+" + else: + mode = "r+" + else: + mode = "r" + return mode + diff --git a/fs/expose/fuse.py b/fs/expose/fuse.py deleted file mode 100755 index d96c4ae..0000000 --- a/fs/expose/fuse.py +++ /dev/null @@ -1,338 +0,0 @@ -#!/usr/bin/env python - - -import base - -import fuse -fuse.fuse_python_api = (0, 2) - - -from datetime import datetime -import time -from os import errno - -import sys -from stat import * - -def showtb(f): - - def run(*args, **kwargs): - print - print "-"*80 - print f, args, kwargs - try: - ret = f(*args, **kwargs) - print "\tReturned:", repr(ret) - return ret - except Exception, e: - print e - raise - print "-"*80 - print - return run - -""" -::*'''<code>open(path, flags)</code>''' - -::*'''<code>create(path, flags, mode)</code>''' - -::*'''<code>read(path, length, offset, fh=None)</code>''' - -::*'''<code>write(path, buf, offset, fh=None)</code>''' - -::*'''<code>fgetattr(path, fh=None)</code>''' - -::*'''<code>ftruncate(path, len, fh=None)</code>''' - -::*'''<code>flush(path, fh=None)</code>''' - -::*'''<code>release(path, fh=None)</code>''' - -::*'''<code>fsync(path, fdatasync, fh=None)</code>''' - -""" - - -class FuseFile(object): - - def __init__(self, f): - self.f = f - - - - - -_run_t = time.time() -class FSFUSE(fuse.Fuse): - - def __init__(self, fs, *args, **kwargs): - fuse.Fuse.__init__(self, *args, **kwargs) - self._fs = fs - - @showtb - def fsinit(self): - return 0 - - def __getattr__(self, name): - print name - raise AttributeError - - #@showtb - def getattr(self, path): - - if not self._fs.exists(path): - return -errno.ENOENT - - class Stat(fuse.Stat): - def __init__(self, context, fs, path): - fuse.Stat.__init__(self) - info = fs.getinfo(path) - isdir = fs.isdir(path) - - fsize = fs.getsize(path) or 1024 - self.st_ino = 0 - self.st_dev = 0 - self.st_nlink = 2 if isdir else 1 - self.st_blksize = fsize - self.st_mode = info.get('st_mode', S_IFDIR | 0755 if isdir else S_IFREG | 0666) - print self.st_mode - self.st_uid = context['uid'] - self.st_gid = context['gid'] - self.st_rdev = 0 - self.st_size = fsize - self.st_blocks = 1 - - for key, value in info.iteritems(): - if not key.startswith('_'): - setattr(self, key, value) - - def do_time(attr, key): - if not hasattr(self, attr): - if key in info: - info_t = info[key] - setattr(self, attr, time.mktime(info_t.timetuple())) - else: - setattr(self, attr, _run_t) - - do_time('st_atime', 'accessed_time') - do_time('st_mtime', 'modified_time') - do_time('st_ctime', 'created_time') - - #for v in dir(self): - # if not v.startswith('_'): - # print v, getattr(self, v) - - return Stat(self.GetContext(), self._fs, path) - - @showtb - def chmod(self, path, mode): - return 0 - - @showtb - def chown(self, path, user, group): - return 0 - - @showtb - def utime(self, path, times): - return 0 - - @showtb - def utimens(self, path, times): - return 0 - - @showtb - def fsyncdir(self): - pass - - @showtb - def bmap(self): - return 0 - - @showtb - def ftruncate(self, path, flags, fh): - if fh is not None: - fh.truncate() - fh.flush() - return 0 - - def fsdestroy(self): - return 0 - - @showtb - def statfs(self): - return (0, 0, 0, 0, 0, 0, 0) - - - - #def setattr - # - # - #@showtb - #def getdir(self, path, offset): - # paths = ['.', '..'] - # paths += self._fs.listdir(path) - # print repr(paths) - # - # for p in paths: - # yield fuse.Direntry(p) - - @showtb - def opendir(self, path): - return 0 - - @showtb - def getxattr(self, path, name, default): - return self._fs.getattr(path, name, default) - - @showtb - def setxattr(self, path, name, value): - self._fs.setattr(path, name) - return 0 - - @showtb - def removeattr(self, path, name): - self._fs.removeattr(path, name) - return 0 - - @showtb - def listxattr(self, path, something): - return self._fs.listattrs(path) - - @showtb - def open(self, path, flags): - return self._fs.open(path, flags=flags) - - @showtb - def create(self, path, flags, mode): - return self._fs.open(path, "w") - - @showtb - def read(self, path, length, offset, fh=None): - if fh: - fh.seek(offset) - return fh.read(length) - - @showtb - def write(self, path, buf, offset, fh=None): - if fh: - fh.seek(offset) - # FUSE seems to expect a return value of the number of bytes written, - # but Python file objects don't return that information, - # so we will assume all bytes are written... - bytes_written = fh.write(buf) or len(buf) - return bytes_written - - @showtb - def release(self, path, flags, fh=None): - if fh: - fh.close() - return 0 - - @showtb - def flush(self, path, fh=None): - if fh: - try: - fh.flush() - except base.FSError: - return 0 - return 0 - - @showtb - def access(self, path, *args, **kwargs): - return 0 - - - #@showtb - def readdir(self, path, offset): - paths = ['.', '..'] - paths += self._fs.listdir(path) - return [fuse.Direntry(p) for p in paths] - - #@showtb - #def fgetattr(self, path, fh=None): - # fh.flush() - # return self.getattr(path) - - @showtb - def readlink(self, path): - return path - - @showtb - def symlink(self, path, path1): - return 0 - - - @showtb - def mknod(self, path, mode, rdev): - f = None - try: - f = self._fs.open(path, mode) - finally: - f.close() - return 0 - - @showtb - def mkdir(self, path, mode): - self._fs.mkdir(path, mode) - return 0 - - @showtb - def rmdir(self, path): - self._fs.removedir(path, True) - return 0 - - @showtb - def unlink(self, path): - try: - self._fs.remove(path) - except base.FSError: - return 0 - return 0 - - #symlink(target, name) - - @showtb - def rename(self, old, new): - self._fs.rename(old, new) - return 0 - - - - #@showtb - #def read(self, path, size, offset): - # pass - - - -def main(fs): - usage=""" - FSFS: Exposes an FS - """ + fuse.Fuse.fusage - - server = FSFUSE(fs, version="%prog 0.1", - usage=usage, dash_s_do='setsingle') - - #server.readdir('.', 0) - - server.parse(errex=1) - server.main() - - -if __name__ == "__main__": - - import memoryfs - import osfs - mem_fs = memoryfs.MemoryFS() - mem_fs.makedir("test") - mem_fs.createfile("a.txt", "This is a test") - mem_fs.createfile("test/b.txt", "This is in a sub-dir") - - - #fs = osfs.OSFS('/home/will/fusetest/') - #main(fs) - - main(mem_fs) - - # To run do ./fuserserver.py -d -f testfs - # This will map a fs.memoryfs to testfs/ on the local filesystem under tests/fs - # To unmouont, do fusermount -u testfs
\ No newline at end of file diff --git a/fs/expose/fuse/__init__.py b/fs/expose/fuse/__init__.py new file mode 100644 index 0000000..cae3a1b --- /dev/null +++ b/fs/expose/fuse/__init__.py @@ -0,0 +1,425 @@ +""" + + fs.expose.fuse: expose an FS object to the native filesystem via FUSE + +This module provides the necessay interfaces to mount an FS object into +the local filesystem via FUSE: + + http://fuse.sourceforge.net/ + +For simple usage, the function 'mount' takes an FS object and a local path, +and exposes the given FS at that path: + + >>> from fs.memoryfs import MemoryFS + >>> from fs.expose import fuse + >>> fs = MemoryFS() + >>> mp = fuse.mount(fs,"/mnt/my-memory-fs") + >>> mp.path + '/mnt/my-memory-fs' + >>> mp.unmount() + +The above spawns a new background process to manage the FUSE event loop, which +can be controlled through the returned subprocess.Popen object. To avoid +spawning a new process, set the 'foreground' option: + + >>> # This will block until the filesystem is unmounted + >>> fuse.mount(fs,"/mnt/my-memory-fs",foreground=True) + +Any additional options for the FUSE process can be passed as keyword arguments +to the 'mount' function. + +If you require finer control over the creation of the FUSE process, you can +instantiate the MountProcess class directly. It accepts all options available +to subprocess.Popen: + + >>> from subprocess import PIPE + >>> mp = fuse.MountProcess(fs,"/mnt/my-memory-fs",stderr=PIPE) + >>> fuse_errors = mp.communicate()[1] + +The binding to FUSE is created via ctypes, using a custom version of the +fuse.py code from Giorgos Verigakis: + + http://code.google.com/p/fusepy/ + +""" + +import os +import sys +import signal +import errno +import time +import stat as statinfo +import subprocess +import pickle + +from fs.base import flags_to_mode +from fs.errors import * +from fs.path import * + +import fuse_ctypes as fuse +try: + fuse._libfuse.fuse_get_context +except AttributeError: + raise ImportError("could not locate FUSE library") + + +FUSE = fuse.FUSE +Operations = fuse.Operations +fuse_get_context = fuse.fuse_get_context + +STARTUP_TIME = time.time() + + +def handle_fs_errors(func): + """Method decorator to report FS errors in the appropriate way. + + This decorator catches all FS errors and translates them into an + equivalent OSError. It also makes the function return zero instead + of None as an indication of successful execution. + """ + def wrapper(*args,**kwds): + try: + res = func(*args,**kwds) + except ResourceNotFoundError, e: + raise OSError(errno.ENOENT,str(e)) + except FSError, e: + raise OSError(errno.EFAULT,str(e)) + except Exception, e: + raise + if res is None: + return 0 + return res + return wrapper + + +def get_stat_dict(fs,path): + """Build a 'stat' dictionary for the given file.""" + uid, gid, pid = fuse_get_context() + info = fs.getinfo(path) + private_keys = [k for k in info if k.startswith("_")] + for k in private_keys: + del info[k] + # Basic stuff that is constant for all paths + info.setdefault("st_ino",0) + info.setdefault("st_dev",0) + info.setdefault("st_uid",uid) + info.setdefault("st_gid",gid) + info.setdefault("st_rdev",0) + info.setdefault("st_blksize",1024) + info.setdefault("st_blocks",1) + # The interesting stuff + info.setdefault("st_size",info.get("size",1024)) + info.setdefault("st_mode",info.get('st_mode',0700)) + if fs.isdir(path): + info["st_mode"] = info["st_mode"] | statinfo.S_IFDIR + info.setdefault("st_nlink",2) + else: + info["st_mode"] = info["st_mode"] | statinfo.S_IFREG + info.setdefault("st_nlink",1) + for (key1,key2) in [("st_atime","accessed_time"),("st_mtime","modified_time"),("st_ctime","created_time")]: + if key1 not in info: + if key2 in info: + info[key1] = time.mktime(info[key2].timetuple()) + else: + info[key1] = STARTUP_TIME + return info + + +class FSOperations(Operations): + """FUSE Operations interface delegating all activities to an FS object.""" + + def __init__(self,fs,on_init=None,on_destroy=None): + self.fs = fs + self._fhmap = {} + self._on_init = on_init + self._on_destroy = on_destroy + + def _get_file(self,fh): + try: + return self._fhmap[fh] + except KeyError: + raise FSError("invalid file handle") + + def _reg_file(self,f): + # TODO: a better handle-generation routine + fh = int(time.time()*1000) + self._fhmap.setdefault(fh,f) + if self._fhmap[fh] is not f: + return self._reg_file(f) + return fh + + def init(self,conn): + if self._on_init: + self._on_init() + + def destroy(self,data): + if self._on_destroy: + self._on_destroy() + + @handle_fs_errors + def chmod(self,path,mode): + raise UnsupportedError("chmod") + + @handle_fs_errors + def chown(self,path,uid,gid): + raise UnsupportedError("chown") + + @handle_fs_errors + def create(self,path,mode,fi=None): + if fi is not None: + raise UnsupportedError("raw_fi") + return self._reg_file(self.fs.open(path,"w")) + + @handle_fs_errors + def flush(self,path,fh): + self._get_file(fh).flush() + + @handle_fs_errors + def getattr(self,path,fh=None): + return get_stat_dict(self.fs,path) + + @handle_fs_errors + def getxattr(self,path,name,position=0): + try: + return self.fs.getxattr(path,name) + except AttributeError: + raise UnsupportedError("getxattr") + + @handle_fs_errors + def link(self,target,souce): + raise UnsupportedError("link") + + @handle_fs_errors + def listxattr(self,path): + try: + return self.fs.xattrs(path) + except AttributeError: + raise UnsupportedError("listxattr") + + @handle_fs_errors + def mkdir(self,path,mode): + try: + self.fs.makedir(path,mode) + except TypeError: + self.fs.makedir(path) + + @handle_fs_errors + def mknod(self,path,mode,dev): + raise UnsupportedError("mknod") + + @handle_fs_errors + def open(self,path,flags): + mode = flags_to_mode(flags) + return self._reg_file(self.fs.open(path,mode)) + + @handle_fs_errors + def read(self,path,size,offset,fh): + f = self._get_file(fh) + f.seek(offset) + return f.read(size) + + @handle_fs_errors + def readdir(self,path,fh=None): + return ['.', '..'] + self.fs.listdir(path) + + @handle_fs_errors + def readlink(self,path): + raise UnsupportedError("readlink") + + @handle_fs_errors + def release(self,path,fh): + self._get_file(fh).close() + del self._fhmap[fh] + + @handle_fs_errors + def removexattr(self,path,name): + try: + return self.fs.delxattr(path,name) + except AttributeError: + raise UnsupportedError("removexattr") + + @handle_fs_errors + def rename(self,old,new): + if issamedir(old,new): + self.fs.rename(old,new) + else: + if self.fs.isdir(old): + self.fs.movedir(old,new) + else: + self.fs.move(old,new) + + @handle_fs_errors + def rmdir(self, path): + self.fs.removedir(path) + + @handle_fs_errors + def setxattr(self,path,name,value,options,position=0): + try: + return self.fs.setxattr(path,name,value) + except AttributeError: + raise UnsupportedError("setxattr") + + @handle_fs_errors + def symlink(self, target, source): + raise UnsupportedError("symlink") + + @handle_fs_errors + def truncate(self, path, length, fh=None): + if fh is None and length == 0: + self.fs.open(path,"w").close() + else: + if fh is None: + f = self.fs.open(path,"w+") + else: + f = self._get_file(fh) + if not hasattr(f,"truncate"): + raise UnsupportedError("trunace") + f.truncate(length) + + @handle_fs_errors + def unlink(self, path): + self.fs.remove(path) + + @handle_fs_errors + def utimens(self, path, times=None): + raise UnsupportedError("utimens") + + @handle_fs_errors + def write(self, path, data, offset, fh): + f = self._get_file(fh) + f.seek(offset) + f.write(data) + return len(data) + + +def mount(fs,path,foreground=False,ready_callback=None,**kwds): + """Mount the given FS at the given path, using FUSE. + + By default, this function spawns a new background process to manage the + FUSE event loop. The return value in this case is an instance of the + 'MountProcess' class, a subprocess.Popen subclass. + + If the keyword argument 'foreground' is given, we instead run the FUSE + main loop in the current process. In this case the function will block + until the filesystem is unmounted, then return None. + + If the keyword argument 'ready_callback' is provided, it will be called + when the filesystem has been mounted and is ready for use. Any additional + keyword arguments will be passed through as options to the underlying + FUSE class. Some interesting options include: + + * nothreads: switch off threading in the FUSE event loop + * fsname: name to display in the mount info table + + """ + if foreground: + ops = FSOperations(fs,on_init=ready_callback) + return FUSE(ops,path,foreground=foreground,**kwds) + else: + mp = MountProcess(fs,path,kwds) + if ready_callback: + ready_callback() + return mp + + +def unmount(path): + """Unmount the given mount point. + + This function shells out to the 'fusermount' program to unmount a + FUSE filesystem. It works, but it would probably be better to use the + 'unmount' method on the MountProcess class if you have it. + """ + if os.system("fusermount -u '" + path + "'"): + raise OSError("filesystem could not be unmounted: " + path) + + +class MountProcess(subprocess.Popen): + """subprocess.Popen subclass managing a FUSE mount. + + This is a subclass of subprocess.Popen, designed for easy management of + a FUSE mount in a background process. Rather than specifying the command + to execute, pass in the FS object to be mounted, the target mount point + and a dictionary of options for the underlying FUSE class. + + In order to be passed successfully to the new process, the FS object + must be pickleable. This restriction may be lifted in the future. + + This class has an extra attribute 'path' giving the path to the mounted + filesystem, and an extra method 'unmount' that will cleanly unmount it + and terminate the process. + + By default, the spawning process will block until it receives notification + that the filesystem has been mounted. Since this notification is sent + by writing to a pipe, using the 'close_fds' option on this class will + prevent it from being sent. You can also pass in the keyword argument + 'nowait' to continue without waiting for notification. + + """ + + # This works by spawning a new python interpreter and passing it the + # pickled (fs,path,opts) tuple on the command-line. Something like this: + # + # python -c "import MountProcess; MountProcess._do_mount('..data..') + # + # It would be more efficient to do a straight os.fork() here, and would + # remove the need to pickle the FS. But API wise, I think it's much + # better for mount() to return a Popen instance than just a pid. + # + # In the future this class could implement its own forking logic and + # just copy the relevant bits of the Popen interface. For now, this + # spawn-a-new-interpreter solution is the easiest to get up and running. + + def __init__(self,fs,path,fuse_opts={},nowait=False,**kwds): + self.path = path + if nowait or kwds.get("close_fds",False): + cmd = 'from fs.expose.fuse import MountProcess; ' + cmd = cmd + 'MountProcess._do_mount_nowait(%s)' + cmd = cmd % (pickle.dumps((fs,path,fuse_opts)),) + cmd = cmd % (repr(pickle.dumps((fs,path,fuse_opts),-1)),) + cmd = [sys.executable,"-c",cmd] + super(MountProcess,self).__init__(cmd,**kwds) + else: + (r,w) = os.pipe() + cmd = 'from fs.expose.fuse import MountProcess; ' + cmd = cmd + 'MountProcess._do_mount_wait(%s)' + cmd = cmd % (repr(pickle.dumps((fs,path,fuse_opts,r,w),-1)),) + cmd = [sys.executable,"-c",cmd] + super(MountProcess,self).__init__(cmd,**kwds) + os.close(w) + print os.getpid(), "WAITING" + os.read(r,1) + + def unmount(self): + """Cleanly unmount the FUSE filesystem, terminating this subprocess.""" + if hasattr(self,"terminate"): + self.terminate() + else: + os.kill(self.pid,signal.SIGTERM) + + @staticmethod + def _do_mount_nowait(data): + """Perform the specified mount, return without waiting.""" + (fs,path,opts) = pickle.loads(data) + opts["foreground"] = True + mount(fs,path,*opts) + + @staticmethod + def _do_mount_wait(data): + """Perform the specified mount, signalling when ready.""" + (fs,path,opts,r,w) = pickle.loads(data) + os.close(r) + opts["foreground"] = True + opts["ready_callback"] = lambda: os.close(w) + mount(fs,path,**opts) + + +if __name__ == "__main__": + import os, os.path + from fs.tempfs import TempFS + mount_point = os.path.join(os.environ["HOME"],"fs.expose.fuse") + if not os.path.exists(mount_point): + os.makedirs(mount_point) + def ready_callback(): + print "READY" + mount(TempFS(),mount_point,foreground=True,ready_callback=ready_callback) + diff --git a/fs/expose/fuse/fuse_ctypes.py b/fs/expose/fuse/fuse_ctypes.py new file mode 100644 index 0000000..a9e09cd --- /dev/null +++ b/fs/expose/fuse/fuse_ctypes.py @@ -0,0 +1,603 @@ +# +# [rfk,05/06/09] I've patched this to add support for the init() and +# destroy() callbacks and will submit the patch upstream +# sometime soon... +# +# Copyright (c) 2008 Giorgos Verigakis <verigak@gmail.com> +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import division + +from ctypes import * +from ctypes.util import find_library +from errno import EFAULT +from functools import partial +from platform import machine, system +from traceback import print_exc + + +class c_timespec(Structure): + _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)] + +class c_utimbuf(Structure): + _fields_ = [('actime', c_timespec), ('modtime', c_timespec)] + +class c_stat(Structure): + pass # Platform dependent + +_system = system() +if _system == 'Darwin': + _libiconv = CDLL(find_library("iconv"), RTLD_GLOBAL) # libfuse dependency + ENOTSUP = 45 + c_dev_t = c_int32 + c_fsblkcnt_t = c_ulong + c_fsfilcnt_t = c_ulong + c_gid_t = c_uint32 + c_mode_t = c_uint16 + c_off_t = c_int64 + c_pid_t = c_int32 + c_uid_t = c_uint32 + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), + c_size_t, c_int, c_uint32) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), + c_size_t, c_uint32) + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_uint32), + ('st_mode', c_mode_t), + ('st_nlink', c_uint16), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_size', c_off_t), + ('st_blocks', c_int64), + ('st_blksize', c_int32)] +elif _system == 'Linux': + ENOTSUP = 95 + c_dev_t = c_ulonglong + c_fsblkcnt_t = c_ulonglong + c_fsfilcnt_t = c_ulonglong + c_gid_t = c_uint + c_mode_t = c_uint + c_off_t = c_longlong + c_pid_t = c_int + c_uid_t = c_uint + setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int) + getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t) + + _machine = machine() + if _machine == 'i686': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('__pad1', c_ushort), + ('__st_ino', c_ulong), + ('st_mode', c_mode_t), + ('st_nlink', c_uint), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('st_rdev', c_dev_t), + ('__pad2', c_ushort), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_longlong), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec), + ('st_ino', c_ulonglong)] + elif machine() == 'x86_64': + c_stat._fields_ = [ + ('st_dev', c_dev_t), + ('st_ino', c_ulong), + ('st_nlink', c_ulong), + ('st_mode', c_mode_t), + ('st_uid', c_uid_t), + ('st_gid', c_gid_t), + ('__pad0', c_int), + ('st_rdev', c_dev_t), + ('st_size', c_off_t), + ('st_blksize', c_long), + ('st_blocks', c_long), + ('st_atimespec', c_timespec), + ('st_mtimespec', c_timespec), + ('st_ctimespec', c_timespec)] + else: + raise NotImplementedError('Linux %s is not supported.' % _machine) +else: + raise NotImplementedError('%s is not supported.' % _system) + + +class c_statvfs(Structure): + _fields_ = [ + ('f_bsize', c_ulong), + ('f_frsize', c_ulong), + ('f_blocks', c_fsblkcnt_t), + ('f_bfree', c_fsblkcnt_t), + ('f_bavail', c_fsblkcnt_t), + ('f_files', c_fsfilcnt_t), + ('f_ffree', c_fsfilcnt_t), + ('f_favail', c_fsfilcnt_t)] + +class fuse_file_info(Structure): + _fields_ = [ + ('flags', c_int), + ('fh_old', c_ulong), + ('writepage', c_int), + ('direct_io', c_uint, 1), + ('keep_cache', c_uint, 1), + ('flush', c_uint, 1), + ('padding', c_uint, 29), + ('fh', c_uint64), + ('lock_owner', c_uint64)] + +class fuse_context(Structure): + _fields_ = [ + ('fuse', c_voidp), + ('uid', c_uid_t), + ('gid', c_gid_t), + ('pid', c_pid_t), + ('private_data', c_voidp)] + +class fuse_conn_info(Structure): + _fields_ = [ + ('proto_major', c_uint), + ('proto_minor', c_uint), + ('async_read', c_uint), + ('max_write', c_uint), + ('max_readahead', c_uint), + ('capable', c_uint), + ('want', c_uint), + ('reserved', c_uint*25)] + +class fuse_operations(Structure): + _fields_ = [ + ('getattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat))), + ('readlink', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), + ('getdir', c_voidp), # Deprecated, use readdir + ('mknod', CFUNCTYPE(c_int, c_char_p, c_mode_t, c_dev_t)), + ('mkdir', CFUNCTYPE(c_int, c_char_p, c_mode_t)), + ('unlink', CFUNCTYPE(c_int, c_char_p)), + ('rmdir', CFUNCTYPE(c_int, c_char_p)), + ('symlink', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('rename', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('link', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('chmod', CFUNCTYPE(c_int, c_char_p, c_mode_t)), + ('chown', CFUNCTYPE(c_int, c_char_p, c_uid_t, c_gid_t)), + ('truncate', CFUNCTYPE(c_int, c_char_p, c_off_t)), + ('utime', c_voidp), # Deprecated, use utimens + ('open', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('read', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, + POINTER(fuse_file_info))), + ('write', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, + POINTER(fuse_file_info))), + ('statfs', CFUNCTYPE(c_int, c_char_p, POINTER(c_statvfs))), + ('flush', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('release', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('fsync', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), + ('setxattr', setxattr_t), + ('getxattr', getxattr_t), + ('listxattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)), + ('removexattr', CFUNCTYPE(c_int, c_char_p, c_char_p)), + ('opendir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('readdir', CFUNCTYPE(c_int, c_char_p, c_voidp, CFUNCTYPE(c_int, c_voidp, + c_char_p, POINTER(c_stat), c_off_t), c_off_t, POINTER(fuse_file_info))), + ('releasedir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))), + ('fsyncdir', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))), + ('init', CFUNCTYPE(c_voidp, POINTER(fuse_conn_info))), + ('destroy', CFUNCTYPE(None, c_voidp)), + ('access', CFUNCTYPE(c_int, c_char_p, c_int)), + ('create', CFUNCTYPE(c_int, c_char_p, c_mode_t, POINTER(fuse_file_info))), + ('ftruncate', CFUNCTYPE(c_int, c_char_p, c_off_t, POINTER(fuse_file_info))), + ('fgetattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat), + POINTER(fuse_file_info))), + ('lock', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info), c_int, c_voidp)), + ('utimens', CFUNCTYPE(c_int, c_char_p, POINTER(c_utimbuf))), + ('bmap', CFUNCTYPE(c_int, c_char_p, c_size_t, POINTER(c_ulonglong)))] + + +def time_of_timespec(ts): + return ts.tv_sec + 1.0 * ts.tv_nsec / 10 ** 9 + +def set_st_attrs(st, attrs): + for key, val in attrs.items(): + if key in ('st_atime', 'st_mtime', 'st_ctime'): + timespec = getattr(st, key + 'spec') + timespec.tv_sec = int(val) + timespec.tv_nsec = int((val - timespec.tv_sec) * 10 ** 9) + elif hasattr(st, key): + setattr(st, key, val) + +def _operation_wrapper(func, *args, **kwargs): + """Decorator for the methods of class FUSE""" + try: + return func(*args, **kwargs) or 0 + except OSError, e: + return -(e.errno or EFAULT) + except: + print_exc() + return -EFAULT + +_libfuse = CDLL(find_library("fuse")) + + +def fuse_get_context(): + """Returns a (uid, gid, pid) tuple""" + p = _libfuse.fuse_get_context() + ctx = cast(p, POINTER(fuse_context)).contents + return ctx.uid, ctx.gid, ctx.pid + + +class FUSE(object): + """This class is the lower level interface and should not be subclassed + under normal use. Its methods are called by fuse. + Assumes API version 2.6 or later.""" + + def __init__(self, operations, mountpoint, raw_fi=False, **kwargs): + """Setting raw_fi to True will cause FUSE to pass the fuse_file_info + class as is to Operations, instead of just the fh field. + This gives you access to direct_io, keep_cache, etc.""" + + self.operations = operations + self.raw_fi = raw_fi + args = ['fuse'] + if kwargs.pop('foreground', False): + args.append('-f') + if kwargs.pop('debug', False): + args.append('-d') + if kwargs.pop('nothreads', False): + args.append('-s') + kwargs.setdefault('fsname', operations.__class__.__name__) + args.append('-o') + args.append(','.join(key if val == True else '%s=%s' % (key, val) + for key, val in kwargs.items())) + args.append(mountpoint) + argv = (c_char_p * len(args))(*args) + + fuse_ops = fuse_operations() + for name, prototype in fuse_operations._fields_: + if prototype != c_voidp and getattr(operations, name, None): + op = partial(_operation_wrapper, getattr(self, name)) + setattr(fuse_ops, name, prototype(op)) + _libfuse.fuse_main_real(len(args), argv, pointer(fuse_ops), + sizeof(fuse_ops), None) + del self.operations # Invoke the destructor + + def init(self,conn): + return self.operations("init",conn) + + def destroy(self,data): + return self.operations("destroy",data) + + def getattr(self, path, buf): + return self.fgetattr(path, buf, None) + + def readlink(self, path, buf, bufsize): + ret = self.operations('readlink', path) + memmove(buf, create_string_buffer(ret), bufsize) + return 0 + + def mknod(self, path, mode, dev): + return self.operations('mknod', path, mode, dev) + + def mkdir(self, path, mode): + return self.operations('mkdir', path, mode) + + def unlink(self, path): + return self.operations('unlink', path) + + def rmdir(self, path): + return self.operations('rmdir', path) + + def symlink(self, source, target): + return self.operations('symlink', target, source) + + def rename(self, old, new): + return self.operations('rename', old, new) + + def link(self, source, target): + return self.operations('link', target, source) + + def chmod(self, path, mode): + return self.operations('chmod', path, mode) + + def chown(self, path, uid, gid): + return self.operations('chown', path, uid, gid) + + def truncate(self, path, length): + return self.operations('truncate', path, length) + + def open(self, path, fip): + fi = fip.contents + if self.raw_fi: + return self.operations('open', path, fi) + else: + fi.fh = self.operations('open', path, fi.flags) + return 0 + + def read(self, path, buf, size, offset, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + ret = self.operations('read', path, size, offset, fh) + if ret: + memmove(buf, create_string_buffer(ret), size) + return len(ret) + + def write(self, path, buf, size, offset, fip): + data = string_at(buf, size) + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('write', path, data, offset, fh) + + def statfs(self, path, buf): + stv = buf.contents + attrs = self.operations('statfs', path) + for key, val in attrs.items(): + if hasattr(stv, key): + setattr(stv, key, val) + return 0 + + def flush(self, path, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('flush', path, fh) + + def release(self, path, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('release', path, fh) + + def fsync(self, path, datasync, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('fsync', path, datasync, fh) + + def setxattr(self, path, name, value, size, options, *args): + s = string_at(value, size) + return self.operations('setxattr', path, name, s, options, *args) + + def getxattr(self, path, name, value, size, *args): + ret = self.operations('getxattr', path, name, *args) + buf = create_string_buffer(ret) + if bool(value): + memmove(value, buf, size) + return len(ret) + + def listxattr(self, path, namebuf, size): + ret = self.operations('listxattr', path) + if not ret: + return 0 + buf = create_string_buffer('\x00'.join(ret)) + if bool(namebuf): + memmove(namebuf, buf, size) + return len(buf) + + def removexattr(self, path, name): + return self.operations('removexattr', path, name) + + def opendir(self, path, fip): + # Ignore raw_fi + fip.contents.fh = self.operations('opendir', path) + return 0 + + def readdir(self, path, buf, filler, offset, fip): + # Ignore raw_fi + for item in self.operations('readdir', path, fip.contents.fh): + if isinstance(item, str): + name, st, offset = item, None, 0 + else: + name, attrs, offset = item + if attrs: + st = c_stat() + set_st_attrs(st, attrs) + else: + st = None + filler(buf, name, st, offset) + return 0 + + def releasedir(self, path, fip): + # Ignore raw_fi + return self.operations('releasedir', path, fip.contents.fh) + + def fsyncdir(self, path, datasync, fip): + # Ignore raw_fi + return self.operations('fsyncdir', path, datasync, fip.contents.fh) + + def access(self, path, amode): + return self.operations('access', path, amode) + + def create(self, path, mode, fip): + fi = fip.contents + if self.raw_fi: + return self.operations('create', path, mode, fi) + else: + fi.fh = self.operations('create', path, mode) + return 0 + + def ftruncate(self, path, length, fip): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('truncate', path, length, fh) + + def fgetattr(self, path, buf, fip): + memset(buf, 0, sizeof(c_stat)) + st = buf.contents + fh = fip and (fip.contents if self.raw_fi else fip.contents.fh) + attrs = self.operations('getattr', path, fh) + set_st_attrs(st, attrs) + return 0 + + def lock(self, path, fip, cmd, lock): + fh = fip.contents if self.raw_fi else fip.contents.fh + return self.operations('lock', path, fh, cmd, lock) + + def utimens(self, path, buf): + if buf: + atime = time_of_timespec(buf.contents.actime) + mtime = time_of_timespec(buf.contents.modtime) + times = (atime, mtime) + else: + times = None + return self.operations('utimens', path, times) + + def bmap(self, path, blocksize, idx): + return self.operations('bmap', path, blocksize, idx) + + +from errno import EACCES, ENOENT +from stat import S_IFDIR + +class Operations: + """This class should be subclassed and passed as an argument to FUSE on + initialization. All operations should raise an OSError exception on + error. + + When in doubt of what an operation should do, check the FUSE header + file or the corresponding system call man page.""" + + def __call__(self, op, *args): + if not hasattr(self, op): + raise OSError(EFAULT, '') + return getattr(self, op)(*args) + + def on_init(self,conn): + pass + + def on_destroy(self,data): + pass + + def access(self, path, amode): + return 0 + + bmap = None + + def chmod(self, path, mode): + raise OSError(EACCES, '') + + def chown(self, path, uid, gid): + raise OSError(EACCES, '') + + def create(self, path, mode, fi=None): + """When raw_fi is False (default case), fi is None and create should + return a numerical file handle. + When raw_fi is True the file handle should be set directly by create + and return 0.""" + raise OSError(EACCES, '') + + def flush(self, path, fh): + return 0 + + def fsync(self, path, datasync, fh): + return 0 + + def fsyncdir(self, path, datasync, fh): + return 0 + + def getattr(self, path, fh=None): + """Returns a dictionary with keys identical to the stat C structure + of stat(2). + st_atime, st_mtime and st_ctime should be floats.""" + if path != '/': + raise OSError(ENOENT, '') + return dict(st_mode=(S_IFDIR | 0755), st_nlink=2) + + def getxattr(self, path, name, position=0): + raise OSError(ENOTSUP, '') + + def link(self, target, source): + raise OSError(EACCES, '') + + def listxattr(self, path): + return [] + + lock = None + + def mkdir(self, path, mode): + raise OSError(EACCES, '') + + def mknod(self, path, mode, dev): + raise OSError(EACCES, '') + + def open(self, path, flags): + """When raw_fi is False (default case), open should return a numerical + file handle. + When raw_fi is True the signature of open becomes: + open(self, path, fi) + and the file handle should be set directly.""" + return 0 + + def opendir(self, path): + """Returns a numerical file handle.""" + return 0 + + def read(self, path, size, offset, fh): + """Returns a string containing the data requested.""" + raise OSError(EACCES, '') + + def readdir(self, path, fh): + """Can return either a list of names, or a list of (name, attrs, offset) + tuples. attrs is a dict as in getattr.""" + return ['.', '..'] + + def readlink(self, path): + raise OSError(EACCES, '') + + def release(self, path, fh): + return 0 + + def releasedir(self, path, fh): + return 0 + + def removexattr(self, path, name): + raise OSError(ENOTSUP, '') + + def rename(self, old, new): + raise OSError(EACCES, '') + + def rmdir(self, path): + raise OSError(EACCES, '') + + def setxattr(self, path, name, value, options, position=0): + raise OSError(ENOTSUP, '') + + def statfs(self, path): + """Returns a dictionary with keys identical to the statvfs C structure + of statvfs(3). The f_frsize, f_favail, f_fsid and f_flag fields are + ignored by FUSE though.""" + return {} + + def symlink(self, target, source): + raise OSError(EACCES, '') + + def truncate(self, path, length, fh=None): + raise OSError(EACCES, '') + + def unlink(self, path): + raise OSError(EACCES, '') + + def utimens(self, path, times=None): + """Times is a (atime, mtime) tuple. If None use current time.""" + return 0 + + def write(self, path, data, offset, fh): + raise OSError(EACCES, '') + + +class LoggingMixIn: + def __call__(self, op, path, *args): + print '->', op, path, repr(args) + ret = '[Unknown Error]' + try: + ret = getattr(self, op)(path, *args) + return ret + except OSError, e: + ret = str(e) + raise + finally: + print '<-', op, repr(ret) diff --git a/fs/expose/sftp.py b/fs/expose/sftp.py index 0042ee8..526fd84 100644 --- a/fs/expose/sftp.py +++ b/fs/expose/sftp.py @@ -31,6 +31,7 @@ from StringIO import StringIO import paramiko +from fs.base import flags_to_mode from fs.path import * from fs.errors import * @@ -157,33 +158,11 @@ class SFTPHandle(paramiko.SFTPHandle): def __init__(self,owner,path,flags): super(SFTPHandle,self).__init__(flags) - mode = self._flags_to_mode(flags) + mode = flags_to_mode(flags) self.owner = owner self.path = path self._file = owner.fs.open(path,mode) - def _flags_to_mode(self,flags): - """Convert an os.O_* bitmask into an FS mode string.""" - if flags & os.O_EXCL: - raise UnsupportedError("open",msg="O_EXCL is not supported") - if flags & os.O_WRONLY: - if flags & os.O_TRUNC: - mode = "w" - elif flags & os.O_APPEND: - mode = "a" - else: - mode = "r+" - elif flags & os.O_RDWR: - if flags & os.O_TRUNC: - mode = "w+" - elif flags & os.O_APPEND: - mode = "a+" - else: - mode = "r+" - else: - mode = "r" - return mode - @report_sftp_errors def close(self): self._file.close() diff --git a/fs/mountfs.py b/fs/mountfs.py index 777dfd3..2bef74d 100644 --- a/fs/mountfs.py +++ b/fs/mountfs.py @@ -223,7 +223,7 @@ class MountFS(FS): def rename(self, src, dst): if not issamedir(src, dst): - raise ValueError("Destination path must the same directory (user the move method for moving to a different directory)") + raise ValueError("Destination path must the same directory (use the move method for moving to a different directory)") self._lock.acquire() try: diff --git a/fs/multifs.py b/fs/multifs.py index 837f667..0e1e357 100644 --- a/fs/multifs.py +++ b/fs/multifs.py @@ -205,7 +205,7 @@ class MultiFS(FS): def rename(self, src, dst): if not issamedir(src, dst): - raise ValueError("Destination path must the same directory (user the move method for moving to a different directory)") + raise ValueError("Destination path must the same directory (use the move method for moving to a different directory)") self._lock.acquire() try: for fs in self: @@ -131,7 +131,7 @@ class OSFS(FS): def rename(self, src, dst): if not issamedir(src, dst): - raise ValueError("Destination path must the same directory (user the move method for moving to a different directory)") + raise ValueError("Destination path must the same directory (use the move method for moving to a different directory)") path_src = self.getsyspath(src) path_dst = self.getsyspath(dst) try: diff --git a/fs/sftpfs.py b/fs/sftpfs.py index 6429791..f1eeac5 100644 --- a/fs/sftpfs.py +++ b/fs/sftpfs.py @@ -211,7 +211,7 @@ class SFTPFS(FS): def rename(self,src,dst): if not issamedir(src, dst): - raise ValueError("Destination path must the same directory (user the move method for moving to a different directory)") + raise ValueError("Destination path must the same directory (use the move method for moving to a different directory)") nsrc = self._normpath(src) ndst = self._normpath(dst) try: diff --git a/fs/tests/__init__.py b/fs/tests/__init__.py index 3d2c309..9fa69c6 100644 --- a/fs/tests/__init__.py +++ b/fs/tests/__init__.py @@ -13,10 +13,10 @@ logging.basicConfig(level=logging.ERROR,stream=sys.stdout) from fs.base import * +import os, os.path import pickle - class FSTestCases: """Base suite of testcases for filesystem implementations. diff --git a/fs/tests/test_expose.py b/fs/tests/test_expose.py index 88f5095..a76353d 100644 --- a/fs/tests/test_expose.py +++ b/fs/tests/test_expose.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ fs.tests.test_expose: testcases for fs.expose and associated FS classes @@ -6,11 +5,15 @@ """ import unittest +import sys +import os, os.path import socket import threading +import time from fs.tests import FSTestCases from fs.tempfs import TempFS +from fs.path import * from fs import rpcfs from fs.expose.xmlrpc import RPCFSServer @@ -93,3 +96,22 @@ class TestSFTPFS(TestRPCFS): pass +from fs.expose import fuse +from fs.osfs import OSFS +class TestFUSE(unittest.TestCase,FSTestCases): + + def setUp(self): + self.temp_fs = TempFS() + self.temp_fs.makedir("root") + self.temp_fs.makedir("mount") + self.mounted_fs = self.temp_fs.opendir("root") + self.mount_point = self.temp_fs.getsyspath("mount") + self.fs = self.temp_fs.opendir("mount") + self.mount_proc = fuse.mount(self.mounted_fs,self.mount_point) + + def tearDown(self): + self.mount_proc.unmount() + + def check(self,p): + return self.mounted_fs.exists(p) + |