diff options
-rw-r--r-- | fs/helpers.py | 15 | ||||
-rw-r--r-- | fs/sftpfs.py | 275 | ||||
-rw-r--r-- | fs/tests.py | 13 |
3 files changed, 303 insertions, 0 deletions
diff --git a/fs/helpers.py b/fs/helpers.py index e47082b..f88b17e 100644 --- a/fs/helpers.py +++ b/fs/helpers.py @@ -202,3 +202,18 @@ def issamedir(path1, path2): return pathsplit(normpath(path1))[0] == pathsplit(normpath(path2))[0] +def isprefix(path1,path2): + """Return true is path1 is a prefix of path2.""" + bits1 = path1.split("/") + bits2 = path2.split("/") + while bits1 and bits1[-1] == "": + bits1.pop() + if len(bits1) > len(bits2): + return False + for (bit1,bit2) in zip(bits1,bits2): + if bit1 != bit2: + return False + return True + + + diff --git a/fs/sftpfs.py b/fs/sftpfs.py new file mode 100644 index 0000000..79a8fee --- /dev/null +++ b/fs/sftpfs.py @@ -0,0 +1,275 @@ +""" + + fs.sftpfs: Filesystem accesing an SFTP server (via paramiko) + +""" + +import datetime + +import paramiko + +from fs.base import * +from fs.helpers import * + + +if not hasattr(paramiko.SFTPFile,"__enter__"): + paramiko.SFTPFile.__enter__ = lambda self: self + paramiko.SFTPFile.__exit__ = lambda self,et,ev,tb: self.close() and False + + +class SFTPFS(FS): + """A filesystem stored on a remote SFTP server. + + This is basically a compatability wrapper for the excellent SFTPClient + class in the paramiko module. + """ + + def __init__(self,connection,root="/",**credentials): + """SFTPFS constructor. + + The only required argument is 'connection', which must be something + from which we can construct a paramiko.SFTPClient object. Possibile + values include: + + * a hostname string + * a (hostname,port) tuple + * a paramiko.Transport instance + * a paramiko.Channel instance in "sftp" mode + + The kwd argument 'root' specifies the root directory on the remote + machine - access to files outsite this root wil be prevented. Any + other keyword arguments are assumed to be credentials to be used when + connecting the transport. + """ + self._owns_transport = False + self._credentials = credentials + if isinstance(connection,paramiko.Channel): + self.client = paramiko.SFTPClient(connection) + else: + if not isinstance(connection,paramiko.Transport): + connection = paramiko.Transport(connection) + self._owns_transport = True + if not connection.is_authenticated(): + connection.connect(**credentials) + self.client = paramiko.SFTPClient.from_transport(connection) + self.root = makeabsolute(root) + + def __del__(self): + self.close() + + def __getstate__(self): + state = super(SFTPFS,self).__getstate__() + if self._owns_transport: + state['client'] = self.client.get_channel().get_transport().getpeername() + return state + + def __setstate__(self,state): + for (k,v) in state.iteritems(): + self.__dict__[k] = v + if self._owns_transport: + t = paramiko.Transport(self.client) + t.connect(**self._credentials) + self.client = paramiko.SFTPClient.from_transport(t) + + def close(self): + """Close the connection to the remote server.""" + if getattr(self,"client",None): + if self._owns_transport: + t = self.client.get_channel().get_transport() + self.client.close() + t.close() + else: + self.client.close() + self.client = None + + def _normpath(self,path): + npath = pathjoin(self.root,makerelative(path)) + if not isprefix(self.root,npath): + raise PathError(path,msg="Path is outside root: %(path)s") + return npath + + def open(self,path,mode="r",bufsize=-1): + npath = self._normpath(path) + try: + f = self.client.open(npath,mode,bufsize) + except IOError, e: + if getattr(e,"errno",None) == 2: + raise FileNotFoundError(path) + raise OperationFailedError("open file",path=path,details=e) + if self.isdir(path): + msg = "that's a directory: %(path)s" + raise ResourceInvalidError(path,msg=msg) + return f + + def exists(self,path): + npath = self._normpath(path) + try: + self.client.stat(npath) + except IOError, e: + if getattr(e,"errno",None) == 2: + return False + raise OperationFailedError("exists",path,details=e) + else: + return True + + def isdir(self,path): + # TODO: there must be a better way to distinguish files and directories + npath = self._normpath(path) + try: + self.client.listdir(npath) + return True + except IOError, e: + if getattr(e,"errno",None) == 2: + return False + raise OperationFailedError("isdir",path,details=e) + + def isfile(self,path): + npath = self._normpath(path) + try: + self.client.listdir(npath) + return False + except IOError, e: + if getattr(e,"errno",None) == 2: + return self.exists(path) + raise OperationFailedError("isfile",path,details=e) + + def listdir(self,path="./",wildcard=None,full=False,absolute=False,dirs_only=False,files_only=False): + npath = self._normpath(path) + try: + paths = self.client.listdir(npath) + except IOError, e: + if getattr(e,"errno",None) == 2: + if self.exists(path): + raise ResourceInvalidError(path,msg="Can't list directory contents of a file: %(path)s") + raise ResourceNotFoundError(path) + raise OperationFailedError("list directory", path=path, details=e, msg="Unable to get directory listing: %(path)s - (%(details)s)") + return self._listdir_helper(path, paths, wildcard, full, absolute, dirs_only, files_only) + + def makedir(self,path,recursive=False,allow_recreate=False): + npath = self._normpath(path) + try: + self.client.mkdir(npath) + except IOError, e: + if getattr(e,"errno",None) == 2: + if recursive: + self.makedir(dirname(path),recursive=True,allow_recreate=True) + self.makedir(path,allow_recreate=allow_recreate) + else: + raise ParentDirectoryMissingError(path) + elif getattr(e,"errno",None) is not None: + raise OperationFailedError("make directory",path=path,details=e) + else: + if self.isfile(path): + raise ResourceInvalidError(path,msg="Cannot create directory, there's already a file of that name: %(path)s") + if not allow_recreate: + raise DestinationExistsError(path,msg="Can not create a directory that already exists (try allow_recreate=True): %(path)s") + + def remove(self,path): + npath = self._normpath(path) + try: + self.client.remove(npath) + except IOError, e: + if getattr(e,"errno",None) == 2: + raise FileNotFoundError(path) + elif self.isdir(path): + raise ResourceInvalidError(path,msg="Cannot use remove() on a directory: %(path)s") + raise OperationFailedError("remove file", path=path, details=e) + + def removedir(self,path,recursive=False,force=False): + npath = self._normpath(path) + if path in ("","/"): + return + if force: + for path2 in self.listdir(path,absolute=True): + try: + self.remove(path2) + except ResourceInvalidError: + self.removedir(path2,force=True) + try: + self.client.rmdir(npath) + except IOError, e: + if getattr(e,"errno",None) == 2: + if self.isfile(path): + raise ResourceInvalidError(path,msg="Can't use removedir() on a file: %(path)s") + raise DirectoryNotFoundError(path) + elif self.listdir(path): + raise DirectoryNotEmptyError(path) + raise OperationFailedError("remove directory", path=path, details=e) + if recursive: + try: + self.removedir(dirname(path),recursive=True) + except DirectoryNotEmptyError: + pass + + 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)") + nsrc = self._normpath(src) + ndst = self._normpath(dst) + try: + self.client.rename(nsrc,ndst) + except IOError, e: + if getattr(e,"errno",None) == 2: + raise FileNotFoundError(path) + raise OperationFailedError("rename resource", path=src, details=e) + + def move(self,src,dst,overwrite=False,chunk_size=16384): + nsrc = self._normpath(src) + ndst = self._normpath(dst) + if overwrite and self.isfile(dst): + self.remove(dst) + try: + self.client.rename(nsrc,ndst) + except IOError, e: + if getattr(e,"errno",None) == 2: + raise FileNotFoundError(path) + if self.exists(dst): + raise DestinationExistsError(dst) + if not self.isdir(dirname(dst)): + raise ParentDirectoryMissingError(dst,msg="Destination directory does not exist: %(path)s") + raise OperationFailedError("move file", path=src, details=e) + + def movedir(self,src,dst,overwrite=False,ignore_errors=False,chunk_size=16384): + nsrc = self._normpath(src) + ndst = self._normpath(dst) + if overwrite and self.isdir(dst): + self.removedir(dst) + try: + self.client.rename(nsrc,ndst) + except IOError, e: + if getattr(e,"errno",None) == 2: + raise DirNotFoundError(path) + if self.exists(dst): + raise DestinationExistsError(dst) + if not self.isdir(dirname(dst)): + raise ParentDirectoryMissingError(dst,msg="Destination directory does not exist: %(path)s") + raise OperationFailedError("move directory", path=src, details=e) + + def getinfo(self, path): + npath = self._normpath(path) + try: + stats = self.client.stat(npath) + except IOError, e: + raise ResourceError(path, details=e) + info = dict((k, getattr(stats, k)) for k in dir(stats) if not k.startswith('__') ) + info['size'] = info['st_size'] + ct = info.get('st_ctime', None) + if ct is not None: + info['created_time'] = datetime.datetime.fromtimestamp(ct) + at = info.get('st_atime', None) + if at is not None: + info['accessed_time'] = datetime.datetime.fromtimestamp(at) + mt = info.get('st_mtime', None) + if mt is not None: + info['modified_time'] = datetime.datetime.fromtimestamp(at) + return info + + def getsize(self, path): + npath = self._normpath(path) + try: + stats = self.client.stat(npath) + except OSError, e: + raise ResourceError(path, details=e) + return stats.st_size + + diff --git a/fs/tests.py b/fs/tests.py index d47c07e..f739fa6 100644 --- a/fs/tests.py +++ b/fs/tests.py @@ -788,6 +788,19 @@ class TestRPCFS(unittest.TestCase,FSTestCases): pass +import sftpfs +class TestSFTPFS(unittest.TestCase,FSTestCases): + + creds = dict(username="rfk",password="obviously-not-my-real-password") + + def setUp(self): + self.temp_fs = tempfs.TempFS() + self.fs = sftpfs.SFTPFS("localhost",self.temp_fs.root_path,**self.creds) + + def tearDown(self): + self.temp_fs.close() + + from fs.wrappers.xattr import SimulateXAttr class TestSimulateXAttr(unittest.TestCase,FSTestCases,XAttrTestCases): |