summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrfkelly0 <rfkelly0@67cdc799-7952-0410-af00-57a81ceafa0f>2009-06-05 05:23:13 +0000
committerrfkelly0 <rfkelly0@67cdc799-7952-0410-af00-57a81ceafa0f>2009-06-05 05:23:13 +0000
commite9c7e7684b2cbccf9d4b6c7a0b53fcd32b3c0413 (patch)
tree19bb49ea88f67be381a9b5dbe522d17e23458a32
parent9a23be35ebaac211da6a67e63a0fc46f142558b3 (diff)
downloadpyfilesystem-e9c7e7684b2cbccf9d4b6c7a0b53fcd32b3c0413.tar.gz
fs.expose.fuse: expose an FS object via FUSE
Uses a ctypes-based FUSE interface. Adapted from Will's fuseserver.py and the fuse.py example code. git-svn-id: http://pyfilesystem.googlecode.com/svn/branches/rfk-ideas@153 67cdc799-7952-0410-af00-57a81ceafa0f
-rw-r--r--fs/base.py23
-rwxr-xr-xfs/expose/fuse.py338
-rw-r--r--fs/expose/fuse/__init__.py425
-rw-r--r--fs/expose/fuse/fuse_ctypes.py603
-rw-r--r--fs/expose/sftp.py25
-rw-r--r--fs/mountfs.py2
-rw-r--r--fs/multifs.py2
-rw-r--r--fs/osfs.py2
-rw-r--r--fs/sftpfs.py2
-rw-r--r--fs/tests/__init__.py2
-rw-r--r--fs/tests/test_expose.py24
11 files changed, 1081 insertions, 367 deletions
diff --git a/fs/base.py b/fs/base.py
index 8dfeb2d..4d83bef 100644
--- a/fs/base.py
+++ b/fs/base.py
@@ -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:
diff --git a/fs/osfs.py b/fs/osfs.py
index f99b8fc..bee807c 100644
--- a/fs/osfs.py
+++ b/fs/osfs.py
@@ -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)
+