text: """
  fs.expose.sftp
  ==============
  
  Expose an FS object over SFTP (via paramiko).
  
  This module provides the necessary interfaces to expose an FS object over
  SFTP, plugging into the infrastructure provided by the 'paramiko' module.
  
  For simple usage, the class 'BaseSFTPServer' provides an all-in-one server
  class based on the standard SocketServer module.  Use it like so::
  
      server = BaseSFTPServer((hostname,port),fs)
      server.serve_forever()
  
  Note that the base class allows UNAUTHENTICATED ACCESS by default.  For more
  serious work you will probably want to subclass it and override methods such
  as check_auth_password() and get_allowed_auths().
  
  To integrate this module into an existing server framework based on paramiko,
  the 'SFTPServerInterface' class provides a concrete implementation of the
  paramiko.SFTPServerInterface protocol.  If you don't understand what this
  is, you probably don't want to use it.
  
  """
  
  
  
  import os
  import stat as statinfo
  import time
  import socketserver
  import threading
  
  import paramiko
  
  from fs.base import flags_to_mode
  from fs.path import *
  from fs.errors import *
  from fs.local_functools import wraps
  from fs.filelike import StringIO
  from fs.utils import isdir
  
  
  # Default host key used by BaseSFTPServer
  #
  DEFAULT_HOST_KEY = paramiko.RSAKey.from_private_key(StringIO(
      "-----BEGIN RSA PRIVATE KEY-----\n" \
      "MIICXgIBAAKCAIEAl7sAF0x2O/HwLhG68b1uG8KHSOTqe3Cdlj5i/1RhO7E2BJ4B\n" \
      "3jhKYDYtupRnMFbpu7fb21A24w3Y3W5gXzywBxR6dP2HgiSDVecoDg2uSYPjnlDk\n" \
      "HrRuviSBG3XpJ/awn1DObxRIvJP4/sCqcMY8Ro/3qfmid5WmMpdCZ3EBeC0CAwEA\n" \
      "AQKCAIBSGefUs5UOnr190C49/GiGMN6PPP78SFWdJKjgzEHI0P0PxofwPLlSEj7w\n" \
      "RLkJWR4kazpWE7N/bNC6EK2pGueMN9Ag2GxdIRC5r1y8pdYbAkuFFwq9Tqa6j5B0\n" \
      "GkkwEhrcFNBGx8UfzHESXe/uE16F+e8l6xBMcXLMJVo9Xjui6QJBAL9MsJEx93iO\n" \
      "zwjoRpSNzWyZFhiHbcGJ0NahWzc3wASRU6L9M3JZ1VkabRuWwKNuEzEHNK8cLbRl\n" \
      "TyH0mceWXcsCQQDLDEuWcOeoDteEpNhVJFkXJJfwZ4Rlxu42MDsQQ/paJCjt2ONU\n" \
      "WBn/P6iYDTvxrt/8+CtLfYc+QQkrTnKn3cLnAkEAk3ixXR0h46Rj4j/9uSOfyyow\n" \
      "qHQunlZ50hvNz8GAm4TU7v82m96449nFZtFObC69SLx/VsboTPsUh96idgRrBQJA\n" \
      "QBfGeFt1VGAy+YTLYLzTfnGnoFQcv7+2i9ZXnn/Gs9N8M+/lekdBFYgzoKN0y4pG\n" \
      "2+Q+Tlr2aNlAmrHtkT13+wJAJVgZATPI5X3UO0Wdf24f/w9+OY+QxKGl86tTQXzE\n" \
      "4bwvYtUGufMIHiNeWP66i6fYCucXCMYtx6Xgu2hpdZZpFw==\n" \
      "-----END RSA PRIVATE KEY-----\n"
  ))
  
  
  def report_sftp_errors(func):
      """Decorator to catch and report FS errors as SFTP error codes.
  
      Any FSError exceptions are caught and translated into an appropriate
      return code, while other exceptions are passed through untouched.
      """
      @wraps(func)
      def wrapper(*args,**kwds):
          try:
              return func(*args, **kwds)
          except ResourceNotFoundError as e:
              return paramiko.SFTP_NO_SUCH_FILE
          except UnsupportedError as e:
              return paramiko.SFTP_OP_UNSUPPORTED
          except FSError as e:
              return paramiko.SFTP_FAILURE
      return wrapper
  
  
  class SFTPServerInterface(paramiko.SFTPServerInterface):
      """SFTPServerInterface 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 subsystem
      handler like so::
  
        t.set_subsystem_handler("sftp",SFTPServer,SFTPServerInterface,fs)
  
      If this all looks too complicated, you might consider the BaseSFTPServer
      class also provided by this module - it automatically creates the enclosing
      paramiko server infrastructure.
      """
  
      def __init__(self, server, fs, encoding=None, *args, **kwds):
          self.fs = fs
          if encoding is None:
              encoding = "utf8"
          self.encoding = encoding
          super(SFTPServerInterface,self).__init__(server, *args, **kwds)
  
      def close(self):
          # Close the pyfs file system and dereference it.
          self.fs.close()
          self.fs = None
  
      @report_sftp_errors
      def open(self, path, flags, attr):
          return SFTPHandle(self, path, flags)
  
      @report_sftp_errors
      def list_folder(self, path):
          if not isinstance(path, str):
              path = path.decode(self.encoding)
          stats = []
          for entry in self.fs.listdir(path, absolute=True):
              stat = self.stat(entry)
              if not isinstance(stat, int):
                  stats.append(stat)
          return stats
  
      @report_sftp_errors
      def stat(self, path):
          if not isinstance(path, str):
              path = path.decode(self.encoding)
  
          info = self.fs.getinfo(path)
  
          stat = paramiko.SFTPAttributes()
          stat.filename = basename(path).encode(self.encoding)
          stat.st_size = info.get("size")
  
          if 'st_atime' in info:
              stat.st_atime = info.get('st_atime')
          elif 'accessed_time' in info:
              stat.st_atime = time.mktime(info.get("accessed_time").timetuple())
  
          if 'st_mtime' in info:
              stat.st_mtime = info.get('st_mtime')
          else:
              if 'modified_time' in info:
                  stat.st_mtime = time.mktime(info.get("modified_time").timetuple())
  
          if isdir(self.fs, path, info):
              stat.st_mode = 0o777 | statinfo.S_IFDIR
          else:
              stat.st_mode = 0o777 | statinfo.S_IFREG
          return stat
  
      def lstat(self, path):
          return self.stat(path)
  
      @report_sftp_errors
      def remove(self, path):
          if not isinstance(path, str):
              path = path.decode(self.encoding)
          self.fs.remove(path)
          return paramiko.SFTP_OK
  
      @report_sftp_errors
      def rename(self, oldpath, newpath):
          if not isinstance(oldpath, str):
              oldpath = oldpath.decode(self.encoding)
          if not isinstance(newpath, str):
              newpath = newpath.decode(self.encoding)
          if self.fs.isfile(oldpath):
              self.fs.move(oldpath, newpath)
          else:
              self.fs.movedir(oldpath, newpath)
          return paramiko.SFTP_OK
  
      @report_sftp_errors
      def mkdir(self, path, attr):
          if not isinstance(path, str):
              path = path.decode(self.encoding)
          self.fs.makedir(path)
          return paramiko.SFTP_OK
  
      @report_sftp_errors
      def rmdir(self, path):
          if not isinstance(path, str):
              path = path.decode(self.encoding)
          self.fs.removedir(path)
          return paramiko.SFTP_OK
  
      def canonicalize(self, path):
          try:
              return abspath(normpath(path)).encode(self.encoding)
          except BackReferenceError:
              # If the client tries to use backrefs to escape root, gently
              # nudge them back to /.
              return '/'
  
      @report_sftp_errors
      def chattr(self, path, attr):
          #  f.truncate() is implemented by setting the size attr.
          #  Any other attr requests fail out.
          if attr._flags:
              if attr._flags != attr.FLAG_SIZE:
                  raise UnsupportedError
              with self.fs.open(path,"r+") as f:
                  f.truncate(attr.st_size)
          return paramiko.SFTP_OK
  
      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.
  
      This is a simple file wrapper for SFTPServerInterface, passing read
      and write requests directly through the to underlying file from the FS.
      """
  
      def __init__(self, owner, path, flags):
          super(SFTPHandle, self).__init__(flags)
          mode = flags_to_mode(flags)
          self.owner = owner
          if not isinstance(path, str):
              path = path.decode(self.owner.encoding)
          self.path = path
          self._file = owner.fs.open(path, 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(data)
          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 SFTPServer(paramiko.SFTPServer):
      """
      An SFTPServer class that closes the filesystem when done.
      """
  
      def finish_subsystem(self):
          # Close the SFTPServerInterface, it will close the pyfs file system.
          self.server.close()
          super(SFTPServer, self).finish_subsystem()
  
  
  class SFTPRequestHandler(socketserver.BaseRequestHandler):
      """SocketServer RequestHandler subclass for BaseSFTPServer.
  
      This RequestHandler subclass creates a paramiko Transport, sets up the
      sftp subsystem, and hands off to the transport's own request handling
      thread.
      """
      timeout = 60
      auth_timeout = 60
  
      def setup(self):
          """
          Creates the SSH transport. Sets security options.
          """
          self.transport = paramiko.Transport(self.request)
          self.transport.load_server_moduli()
          so = self.transport.get_security_options()
          so.digests = ('hmac-sha1', )
          so.compression = ('zlib@openssh.com', 'none')
          self.transport.add_server_key(self.server.host_key)
          self.transport.set_subsystem_handler("sftp", SFTPServer, SFTPServerInterface, self.server.fs, encoding=self.server.encoding)
  
      def handle(self):
          """
          Start the paramiko server, this will start a thread to handle the connection.
          """
          self.transport.start_server(server=BaseServerInterface())
          # TODO: I like the code below _in theory_ but it does not work as I expected.
          # Figure out how to actually time out a new client if they fail to auth in a
          # certain amount of time.
          #chan = self.transport.accept(self.auth_timeout)
          #if chan is None:
          #    self.transport.close()
  
      def handle_timeout(self):
          try:
              self.transport.close()
          finally:
              super(SFTPRequestHandler, self).handle_timeout()
  
  
  
  class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
      pass
  
  
  class BaseSFTPServer(ThreadedTCPServer):
      """SocketServer.TCPServer subclass exposing an FS via SFTP.
  
      Operation is in the standard SocketServer style.  The target FS object
      can be passed into the constructor, or set as an attribute on the server::
  
          server = BaseSFTPServer((hostname,port),fs)
          server.serve_forever()
  
      It is also possible to specify the host key used by the sever by setting
      the 'host_key' attribute.  If this is not specified, it will default to
      the key found in the DEFAULT_HOST_KEY variable.
      """
      # If the server stops/starts quickly, don't fail because of
      # "port in use" error.
      allow_reuse_address = True
  
      def __init__(self, address, fs=None, encoding=None, host_key=None, RequestHandlerClass=None):
          self.fs = fs
          self.encoding = encoding
          if host_key is None:
              host_key = DEFAULT_HOST_KEY
          self.host_key = host_key
          if RequestHandlerClass is None:
              RequestHandlerClass = SFTPRequestHandler
          socketserver.TCPServer.__init__(self, address, RequestHandlerClass)
  
      def shutdown_request(self, request):
          # Prevent TCPServer from closing the connection prematurely
          return
  
      def close_request(self, request):
          # Prevent TCPServer from closing the connection prematurely
          return
  
  
  class BaseServerInterface(paramiko.ServerInterface):
      """
      Paramiko ServerInterface implementation that performs user authentication.
  
      Note that this base class allows UNAUTHENTICATED ACCESS to the exposed
      FS.  This is intentional, since we can't guess what your authentication
      needs are.  To protect the exposed FS, override the following methods:
  
          * get_allowed_auths Determine the allowed auth modes
          * check_auth_none Check auth with no credentials
          * check_auth_password Check auth with a password
          * check_auth_publickey Check auth with a public key
      """
  
      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):
          """Check whether the user can proceed without authentication."""
          return paramiko.AUTH_SUCCESSFUL
  
      def check_auth_publickey(self, username,key):
          """Check whether the given public key is valid for authentication."""
          return paramiko.AUTH_FAILED
  
      def check_auth_password(self, username, password):
          """Check whether the given password is valid for authentication."""
          return paramiko.AUTH_FAILED
  
      def get_allowed_auths(self,username):
          """Return string containing a comma separated list of allowed auth modes.
  
          The available modes are  "node", "password" and "publickey".
          """
          return "none"
  
  
  #  When called from the command-line, expose a TempFS for testing purposes
  if __name__ == "__main__":
      from fs.tempfs import TempFS
      server = BaseSFTPServer(("localhost",8022),TempFS())
      try:
          #import rpdb2; rpdb2.start_embedded_debugger('password')
          server.serve_forever()
      except (SystemExit,KeyboardInterrupt):
          server.server_close()
  
  