summaryrefslogtreecommitdiff
path: root/paramiko/sftp_client.py
diff options
context:
space:
mode:
Diffstat (limited to 'paramiko/sftp_client.py')
-rw-r--r--paramiko/sftp_client.py229
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