From 4f71e99e388b0cd6cb4fea2503719d0e00958bb3 Mon Sep 17 00:00:00 2001 From: rfkelly0 Date: Wed, 3 Jun 2009 09:03:17 +0000 Subject: adding basic support to expose an FS over SFTP git-svn-id: http://pyfilesystem.googlecode.com/svn/branches/rfk-ideas@146 67cdc799-7952-0410-af00-57a81ceafa0f --- fs/expose/__init__.py | 0 fs/expose/fuse.py | 338 ++++++++++++++++++++++++++++++++++++++++++++++++++ fs/expose/sftp.py | 252 +++++++++++++++++++++++++++++++++++++ fs/fuseserver.py | 338 -------------------------------------------------- 4 files changed, 590 insertions(+), 338 deletions(-) create mode 100644 fs/expose/__init__.py create mode 100755 fs/expose/fuse.py create mode 100644 fs/expose/sftp.py delete mode 100755 fs/fuseserver.py diff --git a/fs/expose/__init__.py b/fs/expose/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fs/expose/fuse.py b/fs/expose/fuse.py new file mode 100755 index 0000000..d96c4ae --- /dev/null +++ b/fs/expose/fuse.py @@ -0,0 +1,338 @@ +#!/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 + +""" +::*'''open(path, flags)''' + +::*'''create(path, flags, mode)''' + +::*'''read(path, length, offset, fh=None)''' + +::*'''write(path, buf, offset, fh=None)''' + +::*'''fgetattr(path, fh=None)''' + +::*'''ftruncate(path, len, fh=None)''' + +::*'''flush(path, fh=None)''' + +::*'''release(path, fh=None)''' + +::*'''fsync(path, fdatasync, fh=None)''' + +""" + + +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/sftp.py b/fs/expose/sftp.py new file mode 100644 index 0000000..e81fcff --- /dev/null +++ b/fs/expose/sftp.py @@ -0,0 +1,252 @@ +""" + + fs.expose.sftp: expose an FS object via SFTP, using paramiko. + +""" + +import os +import stat as statinfo +import time +import SocketServer as ss +import threading + +import paramiko + +from fs.errors import * +from fs.helpers import * + +try: + from functools import wraps +except ImportError: + def wraps(f): + return f + +def debug(func): + @wraps(func) + def wrapper(*args,**kwds): + print func, args[1:], kwds + try: + res = func(*args,**kwds) + except Exception, e: + print "EXC:", e + raise + print "RES:", res + return res + return wrapper + + +def report_sftp_errors(func): + """Decorator to catch and report FS errors as SFTP error codes.""" + @debug + @wraps(func) + def wrapper(*args,**kwds): + try: + return func(*args,**kwds) + except ResourceNotFoundError: + return paramiko.SFTP_NO_SUCH_FILE + except UnsupportedError: + return paramiko.SFTP_OP_UNSUPPORTED + except FSError: + return paramiko.SFTP_FAILURE + return wrapper + + +class SFTPServerInterface(paramiko.SFTPServerInterface): + """SFTPServerInferface implementation that exposes an FS object. + + This SFTPServerInterface subclass expects a single additional argument, + the fs object to be exposed. Use it to set up a transport like so: + + t.set_subsystem_handler("sftp",SFTPServer,SFTPServerInterface,fs) + + """ + + def __init__(self,server,fs,*args,**kwds): + self.fs = fs + super(SFTPServerInterface,self).__init__(server,*args,**kwds) + + @report_sftp_errors + def open(self,path,flags,attr): + return SFTPHandle(self,path,flags) + + @report_sftp_errors + def list_folder(self,path): + stats = [] + for entry in self.fs.listdir(path,absolute=True): + stats.append(self.stat(entry)) + return stats + + @report_sftp_errors + def stat(self,path): + info = self.fs.getinfo(path) + stat = paramiko.SFTPAttributes() + stat.filename = resourcename(path) + stat.st_size = info.get("size") + stat.st_atime = time.mktime(info.get("accessed_time").timetuple()) + stat.st_mtime = time.mktime(info.get("modified_time").timetuple()) + if self.fs.isdir(path): + stat.st_mode = 0777 | statinfo.S_IFDIR + else: + stat.st_mode = 0777 | statinfo.S_IFREG + return stat + + def lstat(self,path): + return self.stat(path) + + @report_sftp_errors + def remove(self,path): + self.fs.remove(path) + return paramiko.SFTP_OK + + @report_sftp_errors + def rename(self,oldpath,newpath): + if self.fs.isfile(path): + self.fs.move(oldpath,newpath) + else: + self.fs.movedir(oldpath,newpath) + return paramiko.SFTP_OK + + @report_sftp_errors + def mkdir(self,path,attr): + self.fs.makedir(path) + return paramiko.SFTP_OK + + @report_sftp_errors + def rmdir(self,path): + self.fs.removedir(path) + return paramiko.SFTP_OK + + def canonicalize(self,path): + return makeabsolute(path) + + def chattr(self,path,attr): + return paramiko.SFTP_OP_UNSUPPORTED + + def readlink(self,path): + return paramiko.SFTP_OP_UNSUPPORTED + + def symlink(self,path): + return paramiko.SFTP_OP_UNSUPPORTED + + +class SFTPHandle(paramiko.SFTPHandle): + """SFTP file handler pointing to a file in an FS object.""" + + def __init__(self,owner,path,flags): + super(SFTPHandle,self).__init__(flags) + mode = self._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() + return paramiko.SFTP_OK + + @report_sftp_errors + def read(self,offset,length): + self._file.seek(offset) + return self._file.read(length) + + @report_sftp_errors + def write(self,offset,data): + self._file.seek(offset) + self._file.write(length) + return paramiko.SFTP_OK + + def stat(self): + return self.owner.stat(self.path) + + def chattr(self,attr): + return self.owner.chattr(self.path,attr) + + + +class SFTPRequestHandler(ss.StreamRequestHandler): + """SockerServer RequestHandler subclass for our SFTP server.""" + + def handle(self): + t = paramiko.Transport(self.request) + t.add_server_key(self.server.host_key) + t.set_subsystem_handler("sftp",paramiko.SFTPServer,SFTPServerInterface,self.server.fs) + # Careful - this actually spawns a new thread to handle the requests + t.start_server(server=self.server) + + +class BaseSFTPServer(ss.TCPServer,paramiko.ServerInterface): + """SocketServer.TCPServer subclass exposing an FS via SFTP.""" + + def __init__(self,address,fs=None,host_key=None,RequestHandlerClass=None): + self.fs = fs + if host_key is None: + self.host_key = paramiko.RSAKey.generate(1024) + else: + self.host_key = host_key + if RequestHandlerClass is None: + RequestHandlerClass = SFTPRequestHandler + ss.TCPServer.__init__(self,address,RequestHandlerClass) + + def close_request(self,request): + # paramiko.Transport closes itself when finished, + # so there's no need for us to do it. + pass + + def check_channel_request(self,kind,chanid): + if kind == 'session': + return paramiko.OPEN_SUCCEEDED + return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + + def check_auth_none(self,username): + if True: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def check_auth_publickey(self,username,key): + if True: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def check_auth_password(self,username,password): + if True: + return paramiko.AUTH_SUCCESSFUL + return paramiko.AUTH_FAILED + + def get_allowed_auths(self,username): + return ("none","publickey","password") + + + +def serve(addr,fs,host_key=None): + """Serve the given FS on the given address.""" + server = BaseSFTPServer(addr,fs) + server.serve_forever() + + +if __name__ == "__main__": + from fs.tempfs import TempFS + serve(("localhost",8023),TempFS()) + + diff --git a/fs/fuseserver.py b/fs/fuseserver.py deleted file mode 100755 index d96c4ae..0000000 --- a/fs/fuseserver.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 - -""" -::*'''open(path, flags)''' - -::*'''create(path, flags, mode)''' - -::*'''read(path, length, offset, fh=None)''' - -::*'''write(path, buf, offset, fh=None)''' - -::*'''fgetattr(path, fh=None)''' - -::*'''ftruncate(path, len, fh=None)''' - -::*'''flush(path, fh=None)''' - -::*'''release(path, fh=None)''' - -::*'''fsync(path, fdatasync, fh=None)''' - -""" - - -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 -- cgit v1.2.1