summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrfkelly0 <rfkelly0@67cdc799-7952-0410-af00-57a81ceafa0f>2009-06-03 05:30:56 +0000
committerrfkelly0 <rfkelly0@67cdc799-7952-0410-af00-57a81ceafa0f>2009-06-03 05:30:56 +0000
commit76a0b2b4a2883c0f494d593806a01c4939d87522 (patch)
treea6d067f2e705308360ec212d6d525ef39d576781
parent469e5d63f69dc42a28df9e17e55caf3029c9eaac (diff)
downloadpyfilesystem-76a0b2b4a2883c0f494d593806a01c4939d87522.tar.gz
Added SFTPFS class, for accessing SFTP servers via paramiko
git-svn-id: http://pyfilesystem.googlecode.com/svn/branches/rfk-ideas@145 67cdc799-7952-0410-af00-57a81ceafa0f
-rw-r--r--fs/helpers.py15
-rw-r--r--fs/sftpfs.py275
-rw-r--r--fs/tests.py13
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):