paramiko.sftp_client

Package paramiko :: Module sftp_client |
[frames] | no frames]

# Source Code for Module paramiko.sftp_client

      1  # Copyright (C) 2003-2007  Robey Pointer <robey@lag.net>
      2  #
      3  # This file is part of paramiko.
      4  #
      5  # Paramiko is free software; you can redistribute it and/or modify it under the
      6  # terms of the GNU Lesser General Public License as published by the Free
      7  # Software Foundation; either version 2.1 of the License, or (at your option)
      8  # any later version.
      9  #
     10  # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
     11  # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     12  # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
     13  # details.
     14  #
     15  # You should have received a copy of the GNU Lesser General Public License
     16  # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
     17  # 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
     18
     19  """
     20  Client-mode SFTP support.
     21  """
     22
     23  from binascii import hexlify
     24  import errno
     25  import os
     26  import threading
     27  import time
     28  import weakref
     29
     30  from paramiko.sftp import *
     31  from paramiko.sftp_attr import SFTPAttributes
     32  from paramiko.ssh_exception import SSHException
     33  from paramiko.sftp_file import SFTPFile
     34
     35
     36 -def _to_unicode(s):
     37      """
     38      decode a string as ascii or utf8 if possible (as required by the sftp
     39      protocol).  if neither works, just return a byte string because the server
     40      probably doesn't know the filename's encoding.
     41      """
     42      try:
     43          return s.encode('ascii')
     44      except UnicodeError:
     45          try:
     46              return s.decode('utf-8')
     47          except UnicodeError:
     48              return s
     49
     50
     51 -class SFTPClient (BaseSFTP):
     52      """
     53      SFTP client object.  C{SFTPClient} is used to open an sftp session across
     54      an open ssh L{Transport} and do remote file operations.
     55      """
     56
     57 -    def __init__(self, sock):
     58          """
     59          Create an SFTP client from an existing L{Channel}.  The channel
     60          should already have requested the C{"sftp"} subsystem.
     61
     62          An alternate way to create an SFTP client context is by using
     63          L{from_transport}.
     64
     65          @param sock: an open L{Channel} using the C{"sftp"} subsystem
     66          @type sock: L{Channel}
     67
     68          @raise SSHException: if there's an exception while negotiating
     69              sftp
     70          """
     71          BaseSFTP.__init__(self)
     72          self.sock = sock
     73          self.ultra_debug = False
     74          self.request_number = 1
     75          # lock for request_number
     76          self._lock = threading.Lock()
     77          self._cwd = None
     78          # request # -> SFTPFile
     79          self._expecting = weakref.WeakValueDictionary()
     80          if type(sock) is Channel:
     81              # override default logger
     82              transport = self.sock.get_transport()
     83              self.logger = util.get_logger(transport.get_log_channel() + '.sftp')
     84              self.ultra_debug = transport.get_hexdump()
     85          try:
     86              server_version = self._send_version()
     87          except EOFError, x:
     88              raise SSHException('EOF during negotiation')
     89          self._log(INFO, 'Opened sftp connection (server version %d)' % server_version)
     90
     91 -    def from_transport(cls, t):
     92          """
     93          Create an SFTP client channel from an open L{Transport}.
     94
     95          @param t: an open L{Transport} which is already authenticated
     96          @type t: L{Transport}
     97          @return: a new L{SFTPClient} object, referring to an sftp session
     98              (channel) across the transport
     99          @rtype: L{SFTPClient}
    100          """
    101          chan = t.open_session()
    102          if chan is None:
    103              return None
    104          chan.invoke_subsystem('sftp')
    105          return cls(chan)
    106      from_transport = classmethod(from_transport)
    107
    108 -    def _log(self, level, msg):
    109          super(SFTPClient, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg)
    110
    111 -    def close(self):
    112          """
    113          Close the SFTP session and its underlying channel.
    114
    115          @since: 1.4
    116          """
    117          self._log(INFO, 'sftp session closed.')
    118          self.sock.close()
    119
    120 -    def get_channel(self):
    121          """
    122          Return the underlying L{Channel} object for this SFTP session.  This
    123          might be useful for doing things like setting a timeout on the channel.
    124
    125          @return: the SSH channel
    126          @rtype: L{Channel}
    127
    128          @since: 1.7.1
    129          """
    130          return self.sock
    131
    132 -    def listdir(self, path='.'):
    133          """
    134          Return a list containing the names of the entries in the given C{path}.
    135          The list is in arbitrary order.  It does not include the special
    136          entries C{'.'} and C{'..'} even if they are present in the folder.
    137          This method is meant to mirror C{os.listdir} as closely as possible.
    138          For a list of full L{SFTPAttributes} objects, see L{listdir_attr}.
    139
    140          @param path: path to list (defaults to C{'.'})
    141          @type path: str
    142          @return: list of filenames
    143          @rtype: list of str
    144          """
    145          return [f.filename for f in self.listdir_attr(path)]
    146
    147 -    def listdir_attr(self, path='.'):
    148          """
    149          Return a list containing L{SFTPAttributes} objects corresponding to
    150          files in the given C{path}.  The list is in arbitrary order.  It does
    151          not include the special entries C{'.'} and C{'..'} even if they are
    152          present in the folder.
    153
    154          The returned L{SFTPAttributes} objects will each have an additional
    155          field: C{longname}, which may contain a formatted string of the file's
    156          attributes, in unix format.  The content of this string will probably
    157          depend on the SFTP server implementation.
    158
    159          @param path: path to list (defaults to C{'.'})
    160          @type path: str
    161          @return: list of attributes
    162          @rtype: list of L{SFTPAttributes}
    163
    164          @since: 1.2
    165          """
    166          path = self._adjust_cwd(path)
    167          self._log(DEBUG, 'listdir(%r)' % path)
    168          t, msg = self._request(CMD_OPENDIR, path)
    169          if t != CMD_HANDLE:
    170              raise SFTPError('Expected handle')
    171          handle = msg.get_string()
    172          filelist = []
    173          while True:
    174              try:
    175                  t, msg = self._request(CMD_READDIR, handle)
    176              except EOFError, e:
    177                  # done with handle
    178                  break
    179              if t != CMD_NAME:
    180                  raise SFTPError('Expected name response')
    181              count = msg.get_int()
    182              for i in range(count):
    183                  filename = _to_unicode(msg.get_string())
    184                  longname = _to_unicode(msg.get_string())
    185                  attr = SFTPAttributes._from_msg(msg, filename, longname)
    186                  if (filename != '.') and (filename != '..'):
    187                      filelist.append(attr)
    188          self._request(CMD_CLOSE, handle)
    189          return filelist
    190
    191 -    def open(self, filename, mode='r', bufsize=-1):
    192          """
    193          Open a file on the remote server.  The arguments are the same as for
    194          python's built-in C{file} (aka C{open}).  A file-like object is
    195          returned, which closely mimics the behavior of a normal python file
    196          object.
    197
    198          The mode indicates how the file is to be opened: C{'r'} for reading,
    199          C{'w'} for writing (truncating an existing file), C{'a'} for appending,
    200          C{'r+'} for reading/writing, C{'w+'} for reading/writing (truncating an
    201          existing file), C{'a+'} for reading/appending.  The python C{'b'} flag
    202          is ignored, since SSH treats all files as binary.  The C{'U'} flag is
    203          supported in a compatible way.
    204
    205          Since 1.5.2, an C{'x'} flag indicates that the operation should only
    206          succeed if the file was created and did not previously exist.  This has
    207          no direct mapping to python's file flags, but is commonly known as the
    208          C{O_EXCL} flag in posix.
    209
    210          The file will be buffered in standard python style by default, but
    211          can be altered with the C{bufsize} parameter.  C{0} turns off
    212          buffering, C{1} uses line buffering, and any number greater than 1
    213          (C{>1}) uses that specific buffer size.
    214
    215          @param filename: name of the file to open
    216          @type filename: str
    217          @param mode: mode (python-style) to open in
    218          @type mode: str
    219          @param bufsize: desired buffering (-1 = default buffer size)
    220          @type bufsize: int
    221          @return: a file object representing the open file
    222          @rtype: SFTPFile
    223
    224          @raise IOError: if the file could not be opened.
    225          """
    226          filename = self._adjust_cwd(filename)
    227          self._log(DEBUG, 'open(%r, %r)' % (filename, mode))
    228          imode = 0
    229          if ('r' in mode) or ('+' in mode):
    230              imode |= SFTP_FLAG_READ
    231          if ('w' in mode) or ('+' in mode) or ('a' in mode):
    232              imode |= SFTP_FLAG_WRITE
    233          if ('w' in mode):
    234              imode |= SFTP_FLAG_CREATE | SFTP_FLAG_TRUNC
    235          if ('a' in mode):
    236              imode |= SFTP_FLAG_CREATE | SFTP_FLAG_APPEND
    237          if ('x' in mode):
    238              imode |= SFTP_FLAG_CREATE | SFTP_FLAG_EXCL
    239          attrblock = SFTPAttributes()
    240          t, msg = self._request(CMD_OPEN, filename, imode, attrblock)
    241          if t != CMD_HANDLE:
    242              raise SFTPError('Expected handle')
    243          handle = msg.get_string()
    244          self._log(DEBUG, 'open(%r, %r) -> %s' % (filename, mode, hexlify(handle)))
    245          return SFTPFile(self, handle, mode, bufsize)
    246
    247      # python continues to vacillate about "open" vs "file"...
    248      file = open
    249
    250 -    def remove(self, path):
    251          """
    252          Remove the file at the given path.  This only works on files; for
    253          removing folders (directories), use L{rmdir}.
    254
    255          @param path: path (absolute or relative) of the file to remove
    256          @type path: str
    257
    258          @raise IOError: if the path refers to a folder (directory)
    259          """
    260          path = self._adjust_cwd(path)
    261          self._log(DEBUG, 'remove(%r)' % path)
    262          self._request(CMD_REMOVE, path)
    263
    264      unlink = remove
    265
    266 -    def rename(self, oldpath, newpath):
    267          """
    268          Rename a file or folder from C{oldpath} to C{newpath}.
    269
    270          @param oldpath: existing name of the file or folder
    271          @type oldpath: str
    272          @param newpath: new name for the file or folder
    273          @type newpath: str
    274
    275          @raise IOError: if C{newpath} is a folder, or something else goes
    276              wrong
    277          """
    278          oldpath = self._adjust_cwd(oldpath)
    279          newpath = self._adjust_cwd(newpath)
    280          self._log(DEBUG, 'rename(%r, %r)' % (oldpath, newpath))
    281          self._request(CMD_RENAME, oldpath, newpath)
    282
    283 -    def mkdir(self, path, mode=0777):
    284          """
    285          Create a folder (directory) named C{path} with numeric mode C{mode}.
    286          The default mode is 0777 (octal).  On some systems, mode is ignored.
    287          Where it is used, the current umask value is first masked out.
    288
    289          @param path: name of the folder to create
    290          @type path: str
    291          @param mode: permissions (posix-style) for the newly-created folder
    292          @type mode: int
    293          """
    294          path = self._adjust_cwd(path)
    295          self._log(DEBUG, 'mkdir(%r, %r)' % (path, mode))
    296          attr = SFTPAttributes()
    297          attr.st_mode = mode
    298          self._request(CMD_MKDIR, path, attr)
    299
    300 -    def rmdir(self, path):
    301          """
    302          Remove the folder named C{path}.
    303
    304          @param path: name of the folder to remove
    305          @type path: str
    306          """
    307          path = self._adjust_cwd(path)
    308          self._log(DEBUG, 'rmdir(%r)' % path)
    309          self._request(CMD_RMDIR, path)
    310
    311 -    def stat(self, path):
    312          """
    313          Retrieve information about a file on the remote system.  The return
    314          value is an object whose attributes correspond to the attributes of
    315          python's C{stat} structure as returned by C{os.stat}, except that it
    316          contains fewer fields.  An SFTP server may return as much or as little
    317          info as it wants, so the results may vary from server to server.
    318
    319          Unlike a python C{stat} object, the result may not be accessed as a
    320          tuple.  This is mostly due to the author's slack factor.
    321
    322          The fields supported are: C{st_mode}, C{st_size}, C{st_uid}, C{st_gid},
    323          C{st_atime}, and C{st_mtime}.
    324
    325          @param path: the filename to stat
    326          @type path: str
    327          @return: an object containing attributes about the given file
    328          @rtype: SFTPAttributes
    329          """
    330          path = self._adjust_cwd(path)
    331          self._log(DEBUG, 'stat(%r)' % path)
    332          t, msg = self._request(CMD_STAT, path)
    333          if t != CMD_ATTRS:
    334              raise SFTPError('Expected attributes')
    335          return SFTPAttributes._from_msg(msg)
    336
    337 -    def lstat(self, path):
    338          """
    339          Retrieve information about a file on the remote system, without
    340          following symbolic links (shortcuts).  This otherwise behaves exactly
    341          the same as L{stat}.
    342
    343          @param path: the filename to stat
    344          @type path: str
    345          @return: an object containing attributes about the given file
    346          @rtype: SFTPAttributes
    347          """
    348          path = self._adjust_cwd(path)
    349          self._log(DEBUG, 'lstat(%r)' % path)
    350          t, msg = self._request(CMD_LSTAT, path)
    351          if t != CMD_ATTRS:
    352              raise SFTPError('Expected attributes')
    353          return SFTPAttributes._from_msg(msg)
    354
    355 -    def symlink(self, source, dest):
    356          """
    357          Create a symbolic link (shortcut) of the C{source} path at
    358          C{destination}.
    359
    360          @param source: path of the original file
    361          @type source: str
    362          @param dest: path of the newly created symlink
    363          @type dest: str
    364          """
    365          dest = self._adjust_cwd(dest)
    366          self._log(DEBUG, 'symlink(%r, %r)' % (source, dest))
    367          if type(source) is unicode:
    368              source = source.encode('utf-8')
    369          self._request(CMD_SYMLINK, source, dest)
    370
    371 -    def chmod(self, path, mode):
    372          """
    373          Change the mode (permissions) of a file.  The permissions are
    374          unix-style and identical to those used by python's C{os.chmod}
    375          function.
    376
    377          @param path: path of the file to change the permissions of
    378          @type path: str
    379          @param mode: new permissions
    380          @type mode: int
    381          """
    382          path = self._adjust_cwd(path)
    383          self._log(DEBUG, 'chmod(%r, %r)' % (path, mode))
    384          attr = SFTPAttributes()
    385          attr.st_mode = mode
    386          self._request(CMD_SETSTAT, path, attr)
    387
    388 -    def chown(self, path, uid, gid):
    389          """
    390          Change the owner (C{uid}) and group (C{gid}) of a file.  As with
    391          python's C{os.chown} function, you must pass both arguments, so if you
    392          only want to change one, use L{stat} first to retrieve the current
    393          owner and group.
    394
    395          @param path: path of the file to change the owner and group of
    396          @type path: str
    397          @param uid: new owner's uid
    398          @type uid: int
    399          @param gid: new group id
    400          @type gid: int
    401          """
    402          path = self._adjust_cwd(path)
    403          self._log(DEBUG, 'chown(%r, %r, %r)' % (path, uid, gid))
    404          attr = SFTPAttributes()
    405          attr.st_uid, attr.st_gid = uid, gid
    406          self._request(CMD_SETSTAT, path, attr)
    407
    408 -    def utime(self, path, times):
    409          """
    410          Set the access and modified times of the file specified by C{path}.  If
    411          C{times} is C{None}, then the file's access and modified times are set
    412          to the current time.  Otherwise, C{times} must be a 2-tuple of numbers,
    413          of the form C{(atime, mtime)}, which is used to set the access and
    414          modified times, respectively.  This bizarre API is mimicked from python
    415          for the sake of consistency -- I apologize.
    416
    417          @param path: path of the file to modify
    418          @type path: str
    419          @param times: C{None} or a tuple of (access time, modified time) in
    420              standard internet epoch time (seconds since 01 January 1970 GMT)
    421          @type times: tuple(int)
    422          """
    423          path = self._adjust_cwd(path)
    424          if times is None:
    425              times = (time.time(), time.time())
    426          self._log(DEBUG, 'utime(%r, %r)' % (path, times))
    427          attr = SFTPAttributes()
    428          attr.st_atime, attr.st_mtime = times
    429          self._request(CMD_SETSTAT, path, attr)
    430
    431 -    def truncate(self, path, size):
    432          """
    433          Change the size of the file specified by C{path}.  This usually extends
    434          or shrinks the size of the file, just like the C{truncate()} method on
    435          python file objects.
    436
    437          @param path: path of the file to modify
    438          @type path: str
    439          @param size: the new size of the file
    440          @type size: int or long
    441          """
    442          path = self._adjust_cwd(path)
    443          self._log(DEBUG, 'truncate(%r, %r)' % (path, size))
    444          attr = SFTPAttributes()
    445          attr.st_size = size
    446          self._request(CMD_SETSTAT, path, attr)
    447
    448 -    def readlink(self, path):
    449          """
    450          Return the target of a symbolic link (shortcut).  You can use
    451          L{symlink} to create these.  The result may be either an absolute or
    452          relative pathname.
    453
    454          @param path: path of the symbolic link file
    455          @type path: str
    456          @return: target path
    457          @rtype: str
    458          """
    459          path = self._adjust_cwd(path)
    460          self._log(DEBUG, 'readlink(%r)' % path)
    461          t, msg = self._request(CMD_READLINK, path)
    462          if t != CMD_NAME:
    463              raise SFTPError('Expected name response')
    464          count = msg.get_int()
    465          if count == 0:
    466              return None
    467          if count != 1:
    468              raise SFTPError('Readlink returned %d results' % count)
    469          return _to_unicode(msg.get_string())
    470
    471 -    def normalize(self, path):
    472          """
    473          Return the normalized path (on the server) of a given path.  This
    474          can be used to quickly resolve symbolic links or determine what the
    475          server is considering to be the "current folder" (by passing C{'.'}
    476          as C{path}).
    477
    478          @param path: path to be normalized
    479          @type path: str
    480          @return: normalized form of the given path
    481          @rtype: str
    482
    483          @raise IOError: if the path can't be resolved on the server
    484          """
    485          path = self._adjust_cwd(path)
    486          self._log(DEBUG, 'normalize(%r)' % path)
    487          t, msg = self._request(CMD_REALPATH, path)
    488          if t != CMD_NAME:
    489              raise SFTPError('Expected name response')
    490          count = msg.get_int()
    491          if count != 1:
    492              raise SFTPError('Realpath returned %d results' % count)
    493          return _to_unicode(msg.get_string())
    494
    495 -    def chdir(self, path):
    496          """
    497          Change the "current directory" of this SFTP session.  Since SFTP
    498          doesn't really have the concept of a current working directory, this
    499          is emulated by paramiko.  Once you use this method to set a working
    500          directory, all operations on this SFTPClient object will be relative
    501          to that path.
    502
    503          @param path: new current working directory
    504          @type path: str
    505
    506          @raise IOError: if the requested path doesn't exist on the server
    507
    508          @since: 1.4
    509          """
    510          self._cwd = self.normalize(path)
    511
    512 -    def getcwd(self):
    513          """
    514          Return the "current working directory" for this SFTP session, as
    515          emulated by paramiko.  If no directory has been set with L{chdir},
    516          this method will return C{None}.
    517
    518          @return: the current working directory on the server, or C{None}
    519          @rtype: str
    520
    521          @since: 1.4
    522          """
    523          return self._cwd
    524
    525 -    def put(self, localpath, remotepath):
    526          """
    527          Copy a local file (C{localpath}) to the SFTP server as C{remotepath}.
    528          Any exception raised by operations will be passed through.  This
    529          method is primarily provided as a convenience.
    530
    531          The SFTP operations use pipelining for speed.
    532
    533          @param localpath: the local file to copy
    534          @type localpath: str
    535          @param remotepath: the destination path on the SFTP server
    536          @type remotepath: str
    537
    538          @since: 1.4
    539          """
    540          fl = file(localpath, 'rb')
    541          fr = self.file(remotepath, 'wb')
    542          fr.set_pipelined(True)
    543          size = 0
    544          while True:
    545              data = fl.read(32768)
    546              if len(data) == 0:
    547                  break
    548              fr.write(data)
    549              size += len(data)
    550          fl.close()
    551          fr.close()
    552          s = self.stat(remotepath)
    553          if s.st_size != size:
    554              raise IOError('size mismatch in put!  %d != %d' % (s.st_size, size))
    555
    556 -    def get(self, remotepath, localpath):
    557          """
    558          Copy a remote file (C{remotepath}) from the SFTP server to the local
    559          host as C{localpath}.  Any exception raised by operations will be
    560          passed through.  This method is primarily provided as a convenience.
    561
    562          @param remotepath: the remote file to copy
    563          @type remotepath: str
    564          @param localpath: the destination path on the local host
    565          @type localpath: str
    566
    567          @since: 1.4
    568          """
    569          fr = self.file(remotepath, 'rb')
    570          fr.prefetch()
    571          fl = file(localpath, 'wb')
    572          size = 0
    573          while True:
    574              data = fr.read(32768)
    575              if len(data) == 0:
    576                  break
    577              fl.write(data)
    578              size += len(data)
    579          fl.close()
    580          fr.close()
    581          s = os.stat(localpath)
    582          if s.st_size != size:
    583              raise IOError('size mismatch in get!  %d != %d' % (s.st_size, size))
    584
    585
    586      ###  internals...
    587
    588
    589 -    def _request(self, t, *arg):
    590          num = self._async_request(type(None), t, *arg)
    591          return self._read_response(num)
    592
    593 -    def _async_request(self, fileobj, t, *arg):
    594          # this method may be called from other threads (prefetch)
    595          self._lock.acquire()
    596          try:
    597              msg = Message()
    598              msg.add_int(self.request_number)
    599              for item in arg:
    600                  if type(item) is int:
    601                      msg.add_int(item)
    602                  elif type(item) is long:
    603                      msg.add_int64(item)
    604                  elif type(item) is str:
    605                      msg.add_string(item)
    606                  elif type(item) is SFTPAttributes:
    607                      item._pack(msg)
    608                  else:
    609                      raise Exception('unknown type for %r type %r' % (item, type(item)))
    610              num = self.request_number
    611              self._expecting[num] = fileobj
    612              self._send_packet(t, str(msg))
    613              self.request_number += 1
    614          finally:
    615              self._lock.release()
    616          return num
    617
    618 -    def _read_response(self, waitfor=None):
    619          while True:
    620              try:
    621                  t, data = self._read_packet()
    622              except EOFError, e:
    623                  raise SSHException('Server connection dropped: %s' % (str(e),))
    624              msg = Message(data)
    625              num = msg.get_int()
    626              if num not in self._expecting:
    627                  # might be response for a file that was closed before responses came back
    628                  self._log(DEBUG, 'Unexpected response #%d' % (num,))
    629                  if waitfor is None:
    630                      # just doing a single check
    631                      break
    632                  continue
    633              fileobj = self._expecting[num]
    634              del self._expecting[num]
    635              if num == waitfor:
    636                  # synchronous
    637                  if t == CMD_STATUS:
    638                      self._convert_status(msg)
    639                  return t, msg
    640              if fileobj is not type(None):
    641                  fileobj._async_response(t, msg)
    642              if waitfor is None:
    643                  # just doing a single check
    644                  break
    645          return (None, None)
    646
    647 -    def _finish_responses(self, fileobj):
    648          while fileobj in self._expecting.values():
    649              self._read_response()
    650              fileobj._check_exception()
    651
    652 -    def _convert_status(self, msg):
    653          """
    654          Raises EOFError or IOError on error status; otherwise does nothing.
    655          """
    656          code = msg.get_int()
    657          text = msg.get_string()
    658          if code == SFTP_OK:
    659              return
    660          elif code == SFTP_EOF:
    661              raise EOFError(text)
    662          elif code == SFTP_NO_SUCH_FILE:
    663              # clever idea from john a. meinel: map the error codes to errno
    664              raise IOError(errno.ENOENT, text)
    665          elif code == SFTP_PERMISSION_DENIED:
    666              raise IOError(errno.EACCES, text)
    667          else:
    668              raise IOError(text)
    669
    670 -    def _adjust_cwd(self, path):
    671          """
    672          Return an adjusted path if we're emulating a "current working
    673          directory" for the server.
    674          """
    675          if type(path) is unicode:
    676              path = path.encode('utf-8')
    677          if self._cwd is None:
    678              return path
    679          if (len(path) > 0) and (path[0] == '/'):
    680              # absolute path
    681              return path
    682          if self._cwd == '/':
    683              return self._cwd + path
    684          return self._cwd + '/' + path
    685
    686
    687 -class SFTP (SFTPClient):
    688      "an alias for L{SFTPClient} for backwards compatability"
    689      pass
    690