diff options
Diffstat (limited to 'paramiko/sftp_client.py')
-rw-r--r-- | paramiko/sftp_client.py | 229 |
1 files changed, 159 insertions, 70 deletions
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index 2fe89e9..b3d2d56 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -1,4 +1,4 @@ -# Copyright (C) 2003-2005 Robey Pointer <robey@lag.net> +# Copyright (C) 2003-2007 Robey Pointer <robey@lag.net> # # This file is part of paramiko. # @@ -20,21 +20,32 @@ Client-mode SFTP support. """ +from binascii import hexlify import errno import os import threading +import time import weakref + from paramiko.sftp import * from paramiko.sftp_attr import SFTPAttributes +from paramiko.ssh_exception import SSHException from paramiko.sftp_file import SFTPFile def _to_unicode(s): - "if a str is not ascii, decode its utf8 into unicode" + """ + decode a string as ascii or utf8 if possible (as required by the sftp + protocol). if neither works, just return a byte string because the server + probably doesn't know the filename's encoding. + """ try: return s.encode('ascii') - except: - return s.decode('utf-8') + except UnicodeError: + try: + return s.decode('utf-8') + except UnicodeError: + return s class SFTPClient (BaseSFTP): @@ -51,8 +62,11 @@ class SFTPClient (BaseSFTP): An alternate way to create an SFTP client context is by using L{from_transport}. - @param sock: an open L{Channel} using the C{"sftp"} subsystem. + @param sock: an open L{Channel} using the C{"sftp"} subsystem @type sock: L{Channel} + + @raise SSHException: if there's an exception while negotiating + sftp """ BaseSFTP.__init__(self) self.sock = sock @@ -66,31 +80,33 @@ class SFTPClient (BaseSFTP): if type(sock) is Channel: # override default logger transport = self.sock.get_transport() - self.logger = util.get_logger(transport.get_log_channel() + '.' + - self.sock.get_name() + '.sftp') + self.logger = util.get_logger(transport.get_log_channel() + '.sftp') self.ultra_debug = transport.get_hexdump() - self._send_version() - - def __del__(self): - self.close() + try: + server_version = self._send_version() + except EOFError, x: + raise SSHException('EOF during negotiation') + self._log(INFO, 'Opened sftp connection (server version %d)' % server_version) - def from_transport(selfclass, t): + def from_transport(cls, t): """ Create an SFTP client channel from an open L{Transport}. - @param t: an open L{Transport} which is already authenticated. + @param t: an open L{Transport} which is already authenticated @type t: L{Transport} @return: a new L{SFTPClient} object, referring to an sftp session - (channel) across the transport. + (channel) across the transport @rtype: L{SFTPClient} """ chan = t.open_session() if chan is None: return None - if not chan.invoke_subsystem('sftp'): - raise SFTPError('Failed to invoke sftp subsystem') - return selfclass(chan) + chan.invoke_subsystem('sftp') + return cls(chan) from_transport = classmethod(from_transport) + + def _log(self, level, msg, *args): + super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([ self.sock.get_name() ] + list(args))) def close(self): """ @@ -98,7 +114,20 @@ class SFTPClient (BaseSFTP): @since: 1.4 """ + self._log(INFO, 'sftp session closed.') self.sock.close() + + def get_channel(self): + """ + Return the underlying L{Channel} object for this SFTP session. This + might be useful for doing things like setting a timeout on the channel. + + @return: the SSH channel + @rtype: L{Channel} + + @since: 1.7.1 + """ + return self.sock def listdir(self, path='.'): """ @@ -121,6 +150,11 @@ class SFTPClient (BaseSFTP): files in the given C{path}. The list is in arbitrary order. It does not include the special entries C{'.'} and C{'..'} even if they are present in the folder. + + The returned L{SFTPAttributes} objects will each have an additional + field: C{longname}, which may contain a formatted string of the file's + attributes, in unix format. The content of this string will probably + depend on the SFTP server implementation. @param path: path to list (defaults to C{'.'}) @type path: str @@ -130,6 +164,7 @@ class SFTPClient (BaseSFTP): @since: 1.2 """ path = self._adjust_cwd(path) + self._log(DEBUG, 'listdir(%r)' % path) t, msg = self._request(CMD_OPENDIR, path) if t != CMD_HANDLE: raise SFTPError('Expected handle') @@ -147,13 +182,13 @@ class SFTPClient (BaseSFTP): for i in range(count): filename = _to_unicode(msg.get_string()) longname = _to_unicode(msg.get_string()) - attr = SFTPAttributes._from_msg(msg, filename) + attr = SFTPAttributes._from_msg(msg, filename, longname) if (filename != '.') and (filename != '..'): filelist.append(attr) self._request(CMD_CLOSE, handle) return filelist - def file(self, filename, mode='r', bufsize=-1): + def open(self, filename, mode='r', bufsize=-1): """ Open a file on the remote server. The arguments are the same as for python's built-in C{file} (aka C{open}). A file-like object is @@ -177,18 +212,19 @@ class SFTPClient (BaseSFTP): buffering, C{1} uses line buffering, and any number greater than 1 (C{>1}) uses that specific buffer size. - @param filename: name of the file to open. - @type filename: string - @param mode: mode (python-style) to open in. - @type mode: string + @param filename: name of the file to open + @type filename: str + @param mode: mode (python-style) to open in + @type mode: str @param bufsize: desired buffering (-1 = default buffer size) @type bufsize: int - @return: a file object representing the open file. + @return: a file object representing the open file @rtype: SFTPFile @raise IOError: if the file could not be opened. """ filename = self._adjust_cwd(filename) + self._log(DEBUG, 'open(%r, %r)' % (filename, mode)) imode = 0 if ('r' in mode) or ('+' in mode): imode |= SFTP_FLAG_READ @@ -205,23 +241,24 @@ class SFTPClient (BaseSFTP): if t != CMD_HANDLE: raise SFTPError('Expected handle') handle = msg.get_string() + self._log(DEBUG, 'open(%r, %r) -> %s' % (filename, mode, hexlify(handle))) return SFTPFile(self, handle, mode, bufsize) - # python has migrated toward file() instead of open(). - # and really, that's more easily identifiable. - open = file + # python continues to vacillate about "open" vs "file"... + file = open def remove(self, path): """ - Remove the file at the given path. + Remove the file at the given path. This only works on files; for + removing folders (directories), use L{rmdir}. - @param path: path (absolute or relative) of the file to remove. - @type path: string + @param path: path (absolute or relative) of the file to remove + @type path: str - @raise IOError: if the path refers to a folder (directory). Use - L{rmdir} to remove a folder. + @raise IOError: if the path refers to a folder (directory) """ path = self._adjust_cwd(path) + self._log(DEBUG, 'remove(%r)' % path) self._request(CMD_REMOVE, path) unlink = remove @@ -230,16 +267,17 @@ class SFTPClient (BaseSFTP): """ Rename a file or folder from C{oldpath} to C{newpath}. - @param oldpath: existing name of the file or folder. - @type oldpath: string - @param newpath: new name for the file or folder. - @type newpath: string + @param oldpath: existing name of the file or folder + @type oldpath: str + @param newpath: new name for the file or folder + @type newpath: str @raise IOError: if C{newpath} is a folder, or something else goes - wrong. + wrong """ oldpath = self._adjust_cwd(oldpath) newpath = self._adjust_cwd(newpath) + self._log(DEBUG, 'rename(%r, %r)' % (oldpath, newpath)) self._request(CMD_RENAME, oldpath, newpath) def mkdir(self, path, mode=0777): @@ -248,12 +286,13 @@ class SFTPClient (BaseSFTP): The default mode is 0777 (octal). On some systems, mode is ignored. Where it is used, the current umask value is first masked out. - @param path: name of the folder to create. - @type path: string - @param mode: permissions (posix-style) for the newly-created folder. + @param path: name of the folder to create + @type path: str + @param mode: permissions (posix-style) for the newly-created folder @type mode: int """ path = self._adjust_cwd(path) + self._log(DEBUG, 'mkdir(%r, %r)' % (path, mode)) attr = SFTPAttributes() attr.st_mode = mode self._request(CMD_MKDIR, path, attr) @@ -262,10 +301,11 @@ class SFTPClient (BaseSFTP): """ Remove the folder named C{path}. - @param path: name of the folder to remove. - @type path: string + @param path: name of the folder to remove + @type path: str """ path = self._adjust_cwd(path) + self._log(DEBUG, 'rmdir(%r)' % path) self._request(CMD_RMDIR, path) def stat(self, path): @@ -282,12 +322,13 @@ class SFTPClient (BaseSFTP): The fields supported are: C{st_mode}, C{st_size}, C{st_uid}, C{st_gid}, C{st_atime}, and C{st_mtime}. - @param path: the filename to stat. - @type path: string - @return: an object containing attributes about the given file. + @param path: the filename to stat + @type path: str + @return: an object containing attributes about the given file @rtype: SFTPAttributes """ path = self._adjust_cwd(path) + self._log(DEBUG, 'stat(%r)' % path) t, msg = self._request(CMD_STAT, path) if t != CMD_ATTRS: raise SFTPError('Expected attributes') @@ -299,12 +340,13 @@ class SFTPClient (BaseSFTP): following symbolic links (shortcuts). This otherwise behaves exactly the same as L{stat}. - @param path: the filename to stat. - @type path: string - @return: an object containing attributes about the given file. + @param path: the filename to stat + @type path: str + @return: an object containing attributes about the given file @rtype: SFTPAttributes """ path = self._adjust_cwd(path) + self._log(DEBUG, 'lstat(%r)' % path) t, msg = self._request(CMD_LSTAT, path) if t != CMD_ATTRS: raise SFTPError('Expected attributes') @@ -315,12 +357,13 @@ class SFTPClient (BaseSFTP): Create a symbolic link (shortcut) of the C{source} path at C{destination}. - @param source: path of the original file. - @type source: string - @param dest: path of the newly created symlink. - @type dest: string + @param source: path of the original file + @type source: str + @param dest: path of the newly created symlink + @type dest: str """ dest = self._adjust_cwd(dest) + self._log(DEBUG, 'symlink(%r, %r)' % (source, dest)) if type(source) is unicode: source = source.encode('utf-8') self._request(CMD_SYMLINK, source, dest) @@ -331,12 +374,13 @@ class SFTPClient (BaseSFTP): unix-style and identical to those used by python's C{os.chmod} function. - @param path: path of the file to change the permissions of. - @type path: string - @param mode: new permissions. + @param path: path of the file to change the permissions of + @type path: str + @param mode: new permissions @type mode: int """ path = self._adjust_cwd(path) + self._log(DEBUG, 'chmod(%r, %r)' % (path, mode)) attr = SFTPAttributes() attr.st_mode = mode self._request(CMD_SETSTAT, path, attr) @@ -348,14 +392,15 @@ class SFTPClient (BaseSFTP): only want to change one, use L{stat} first to retrieve the current owner and group. - @param path: path of the file to change the owner and group of. - @type path: string + @param path: path of the file to change the owner and group of + @type path: str @param uid: new owner's uid @type uid: int @param gid: new group id @type gid: int """ path = self._adjust_cwd(path) + self._log(DEBUG, 'chown(%r, %r, %r)' % (path, uid, gid)) attr = SFTPAttributes() attr.st_uid, attr.st_gid = uid, gid self._request(CMD_SETSTAT, path, attr) @@ -369,31 +414,50 @@ class SFTPClient (BaseSFTP): modified times, respectively. This bizarre API is mimicked from python for the sake of consistency -- I apologize. - @param path: path of the file to modify. - @type path: string + @param path: path of the file to modify + @type path: str @param times: C{None} or a tuple of (access time, modified time) in - standard internet epoch time (seconds since 01 January 1970 GMT). - @type times: tuple of int + standard internet epoch time (seconds since 01 January 1970 GMT) + @type times: tuple(int) """ path = self._adjust_cwd(path) if times is None: times = (time.time(), time.time()) + self._log(DEBUG, 'utime(%r, %r)' % (path, times)) attr = SFTPAttributes() attr.st_atime, attr.st_mtime = times self._request(CMD_SETSTAT, path, attr) + def truncate(self, path, size): + """ + Change the size of the file specified by C{path}. This usually extends + or shrinks the size of the file, just like the C{truncate()} method on + python file objects. + + @param path: path of the file to modify + @type path: str + @param size: the new size of the file + @type size: int or long + """ + path = self._adjust_cwd(path) + self._log(DEBUG, 'truncate(%r, %r)' % (path, size)) + attr = SFTPAttributes() + attr.st_size = size + self._request(CMD_SETSTAT, path, attr) + def readlink(self, path): """ Return the target of a symbolic link (shortcut). You can use L{symlink} to create these. The result may be either an absolute or relative pathname. - @param path: path of the symbolic link file. + @param path: path of the symbolic link file @type path: str - @return: target path. + @return: target path @rtype: str """ path = self._adjust_cwd(path) + self._log(DEBUG, 'readlink(%r)' % path) t, msg = self._request(CMD_READLINK, path) if t != CMD_NAME: raise SFTPError('Expected name response') @@ -411,14 +475,15 @@ class SFTPClient (BaseSFTP): server is considering to be the "current folder" (by passing C{'.'} as C{path}). - @param path: path to be normalized. + @param path: path to be normalized @type path: str - @return: normalized form of the given path. + @return: normalized form of the given path @rtype: str @raise IOError: if the path can't be resolved on the server """ path = self._adjust_cwd(path) + self._log(DEBUG, 'normalize(%r)' % path) t, msg = self._request(CMD_REALPATH, path) if t != CMD_NAME: raise SFTPError('Expected name response') @@ -457,7 +522,7 @@ class SFTPClient (BaseSFTP): """ return self._cwd - def put(self, localpath, remotepath): + def put(self, localpath, remotepath, callback=None): """ Copy a local file (C{localpath}) to the SFTP server as C{remotepath}. Any exception raised by operations will be passed through. This @@ -469,9 +534,17 @@ class SFTPClient (BaseSFTP): @type localpath: str @param remotepath: the destination path on the SFTP server @type remotepath: str + @param callback: optional callback function that accepts the bytes + transferred so far and the total bytes to be transferred + (since 1.7.4) + @type callback: function(int, int) + @return: an object containing attributes about the given file + (since 1.7.4) + @rtype: SFTPAttributes @since: 1.4 """ + file_size = os.stat(localpath).st_size fl = file(localpath, 'rb') fr = self.file(remotepath, 'wb') fr.set_pipelined(True) @@ -482,13 +555,16 @@ class SFTPClient (BaseSFTP): break fr.write(data) size += len(data) + if callback is not None: + callback(size, file_size) fl.close() fr.close() s = self.stat(remotepath) if s.st_size != size: raise IOError('size mismatch in put! %d != %d' % (s.st_size, size)) + return s - def get(self, remotepath, localpath): + def get(self, remotepath, localpath, callback=None): """ Copy a remote file (C{remotepath}) from the SFTP server to the local host as C{localpath}. Any exception raised by operations will be @@ -498,10 +574,15 @@ class SFTPClient (BaseSFTP): @type remotepath: str @param localpath: the destination path on the local host @type localpath: str + @param callback: optional callback function that accepts the bytes + transferred so far and the total bytes to be transferred + (since 1.7.4) + @type callback: function(int, int) @since: 1.4 """ fr = self.file(remotepath, 'rb') + file_size = self.stat(remotepath).st_size fr.prefetch() fl = file(localpath, 'wb') size = 0 @@ -511,6 +592,8 @@ class SFTPClient (BaseSFTP): break fl.write(data) size += len(data) + if callback is not None: + callback(size, file_size) fl.close() fr.close() s = os.stat(localpath) @@ -552,7 +635,10 @@ class SFTPClient (BaseSFTP): def _read_response(self, waitfor=None): while True: - t, data = self._read_packet() + try: + t, data = self._read_packet() + except EOFError, e: + raise SSHException('Server connection dropped: %s' % (str(e),)) msg = Message(data) num = msg.get_int() if num not in self._expecting: @@ -560,7 +646,7 @@ class SFTPClient (BaseSFTP): self._log(DEBUG, 'Unexpected response #%d' % (num,)) if waitfor is None: # just doing a single check - return + break continue fileobj = self._expecting[num] del self._expecting[num] @@ -573,7 +659,8 @@ class SFTPClient (BaseSFTP): fileobj._async_response(t, msg) if waitfor is None: # just doing a single check - return + break + return (None, None) def _finish_responses(self, fileobj): while fileobj in self._expecting.values(): @@ -610,6 +697,8 @@ class SFTPClient (BaseSFTP): if (len(path) > 0) and (path[0] == '/'): # absolute path return path + if self._cwd == '/': + return self._cwd + path return self._cwd + '/' + path |