diff options
Diffstat (limited to 'fs/expose/fuse/__init__.py')
-rw-r--r-- | fs/expose/fuse/__init__.py | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/fs/expose/fuse/__init__.py b/fs/expose/fuse/__init__.py new file mode 100644 index 0000000..dbf25ff --- /dev/null +++ b/fs/expose/fuse/__init__.py @@ -0,0 +1,437 @@ +""" + + 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 * +from fs.xattrs import ensure_xattrs + +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. + """ + func = convert_fs_errors(func) + @wraps(func) + def wrapper(*args,**kwds): + res = func(*args,**kwds) + 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 = ensure_xattrs(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: + value = self.fs.getxattr(path,name) + except AttributeError: + raise UnsupportedError("getxattr") + else: + if value is None: + raise OSError(errno.ENOENT,"no attribute '%s'" % (name,)) + return value + + @handle_fs_errors + def link(self,target,souce): + raise UnsupportedError("link") + + @handle_fs_errors + def listxattr(self,path): + try: + return self.fs.listxattrs(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) + if os.read(r,1) != "S": + raise RuntimeError("A FUSE error occurred") + + def unmount(self): + """Cleanly unmount the FUSE filesystem, terminating this subprocess.""" + if hasattr(self,"terminate"): + self.terminate() + else: + os.kill(self.pid,signal.SIGTERM) + self.wait() + + @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 + successful = [] + def ready_callback(): + successful.append(True) + os.write(w,"S") + os.close(w) + opts["ready_callback"] = ready_callback + try: + mount(fs,path,**opts) + except Exception: + pass + if not successful: + os.write(w,"E") + + + +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) + |