123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930 |
- # Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
- #
- # This file is part of Paramiko.
- #
- # Paramiko is free software; you can redistribute it and/or modify it under the
- # terms of the GNU Lesser General Public License as published by the Free
- # Software Foundation; either version 2.1 of the License, or (at your option)
- # any later version.
- #
- # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
- # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
- # details.
- #
- # You should have received a copy of the GNU Lesser General Public License
- # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
- # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- from binascii import hexlify
- import errno
- import os
- import stat
- import threading
- import time
- import weakref
- from paramiko import util
- from paramiko.channel import Channel
- from paramiko.message import Message
- from paramiko.common import INFO, DEBUG, o777
- from paramiko.py3compat import b, u, long
- from paramiko.sftp import (
- BaseSFTP,
- CMD_OPENDIR,
- CMD_HANDLE,
- SFTPError,
- CMD_READDIR,
- CMD_NAME,
- CMD_CLOSE,
- SFTP_FLAG_READ,
- SFTP_FLAG_WRITE,
- SFTP_FLAG_CREATE,
- SFTP_FLAG_TRUNC,
- SFTP_FLAG_APPEND,
- SFTP_FLAG_EXCL,
- CMD_OPEN,
- CMD_REMOVE,
- CMD_RENAME,
- CMD_MKDIR,
- CMD_RMDIR,
- CMD_STAT,
- CMD_ATTRS,
- CMD_LSTAT,
- CMD_SYMLINK,
- CMD_SETSTAT,
- CMD_READLINK,
- CMD_REALPATH,
- CMD_STATUS,
- CMD_EXTENDED,
- SFTP_OK,
- SFTP_EOF,
- SFTP_NO_SUCH_FILE,
- SFTP_PERMISSION_DENIED,
- )
- from paramiko.sftp_attr import SFTPAttributes
- from paramiko.ssh_exception import SSHException
- from paramiko.sftp_file import SFTPFile
- from paramiko.util import ClosingContextManager
- def _to_unicode(s):
- """
- 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 (UnicodeError, AttributeError):
- try:
- return s.decode("utf-8")
- except UnicodeError:
- return s
- b_slash = b"/"
- class SFTPClient(BaseSFTP, ClosingContextManager):
- """
- SFTP client object.
- Used to open an SFTP session across an open SSH `.Transport` and perform
- remote file operations.
- Instances of this class may be used as context managers.
- """
- def __init__(self, sock):
- """
- Create an SFTP client from an existing `.Channel`. The channel
- should already have requested the ``"sftp"`` subsystem.
- An alternate way to create an SFTP client context is by using
- `from_transport`.
- :param .Channel sock: an open `.Channel` using the ``"sftp"`` subsystem
- :raises:
- `.SSHException` -- if there's an exception while negotiating sftp
- """
- BaseSFTP.__init__(self)
- self.sock = sock
- self.ultra_debug = False
- self.request_number = 1
- # lock for request_number
- self._lock = threading.Lock()
- self._cwd = None
- # request # -> SFTPFile
- self._expecting = weakref.WeakValueDictionary()
- if type(sock) is Channel:
- # override default logger
- transport = self.sock.get_transport()
- self.logger = util.get_logger(
- transport.get_log_channel() + ".sftp"
- )
- self.ultra_debug = transport.get_hexdump()
- try:
- server_version = self._send_version()
- except EOFError:
- raise SSHException("EOF during negotiation")
- self._log(
- INFO,
- "Opened sftp connection (server version {})".format(
- server_version
- ),
- )
- @classmethod
- def from_transport(cls, t, window_size=None, max_packet_size=None):
- """
- Create an SFTP client channel from an open `.Transport`.
- Setting the window and packet sizes might affect the transfer speed.
- The default settings in the `.Transport` class are the same as in
- OpenSSH and should work adequately for both files transfers and
- interactive sessions.
- :param .Transport t: an open `.Transport` which is already
- authenticated
- :param int window_size:
- optional window size for the `.SFTPClient` session.
- :param int max_packet_size:
- optional max packet size for the `.SFTPClient` session..
- :return:
- a new `.SFTPClient` object, referring to an sftp session (channel)
- across the transport
- .. versionchanged:: 1.15
- Added the ``window_size`` and ``max_packet_size`` arguments.
- """
- chan = t.open_session(
- window_size=window_size, max_packet_size=max_packet_size
- )
- if chan is None:
- return None
- chan.invoke_subsystem("sftp")
- return cls(chan)
- def _log(self, level, msg, *args):
- if isinstance(msg, list):
- for m in msg:
- self._log(level, m, *args)
- else:
- # NOTE: these bits MUST continue using %-style format junk because
- # logging.Logger.log() explicitly requires it. Grump.
- # escape '%' in msg (they could come from file or directory names)
- # before logging
- msg = msg.replace("%", "%%")
- super(SFTPClient, self)._log(
- level,
- "[chan %s] " + msg,
- *([self.sock.get_name()] + list(args))
- )
- def close(self):
- """
- Close the SFTP session and its underlying channel.
- .. versionadded:: 1.4
- """
- self._log(INFO, "sftp session closed.")
- self.sock.close()
- def get_channel(self):
- """
- Return the underlying `.Channel` object for this SFTP session. This
- might be useful for doing things like setting a timeout on the channel.
- .. versionadded:: 1.7.1
- """
- return self.sock
- def listdir(self, path="."):
- """
- Return a list containing the names of the entries in the given
- ``path``.
- The list is in arbitrary order. It does not include the special
- entries ``'.'`` and ``'..'`` even if they are present in the folder.
- This method is meant to mirror ``os.listdir`` as closely as possible.
- For a list of full `.SFTPAttributes` objects, see `listdir_attr`.
- :param str path: path to list (defaults to ``'.'``)
- """
- return [f.filename for f in self.listdir_attr(path)]
- def listdir_attr(self, path="."):
- """
- Return a list containing `.SFTPAttributes` objects corresponding to
- files in the given ``path``. The list is in arbitrary order. It does
- not include the special entries ``'.'`` and ``'..'`` even if they are
- present in the folder.
- The returned `.SFTPAttributes` objects will each have an additional
- field: ``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 str path: path to list (defaults to ``'.'``)
- :return: list of `.SFTPAttributes` objects
- .. versionadded:: 1.2
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "listdir({!r})".format(path))
- t, msg = self._request(CMD_OPENDIR, path)
- if t != CMD_HANDLE:
- raise SFTPError("Expected handle")
- handle = msg.get_binary()
- filelist = []
- while True:
- try:
- t, msg = self._request(CMD_READDIR, handle)
- except EOFError:
- # done with handle
- break
- if t != CMD_NAME:
- raise SFTPError("Expected name response")
- count = msg.get_int()
- for i in range(count):
- filename = msg.get_text()
- longname = msg.get_text()
- attr = SFTPAttributes._from_msg(msg, filename, longname)
- if (filename != ".") and (filename != ".."):
- filelist.append(attr)
- self._request(CMD_CLOSE, handle)
- return filelist
- def listdir_iter(self, path=".", read_aheads=50):
- """
- Generator version of `.listdir_attr`.
- See the API docs for `.listdir_attr` for overall details.
- This function adds one more kwarg on top of `.listdir_attr`:
- ``read_aheads``, an integer controlling how many
- ``SSH_FXP_READDIR`` requests are made to the server. The default of 50
- should suffice for most file listings as each request/response cycle
- may contain multiple files (dependent on server implementation.)
- .. versionadded:: 1.15
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "listdir({!r})".format(path))
- t, msg = self._request(CMD_OPENDIR, path)
- if t != CMD_HANDLE:
- raise SFTPError("Expected handle")
- handle = msg.get_string()
- nums = list()
- while True:
- try:
- # Send out a bunch of readdir requests so that we can read the
- # responses later on Section 6.7 of the SSH file transfer RFC
- # explains this
- # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
- for i in range(read_aheads):
- num = self._async_request(type(None), CMD_READDIR, handle)
- nums.append(num)
- # For each of our sent requests
- # Read and parse the corresponding packets
- # If we're at the end of our queued requests, then fire off
- # some more requests
- # Exit the loop when we've reached the end of the directory
- # handle
- for num in nums:
- t, pkt_data = self._read_packet()
- msg = Message(pkt_data)
- new_num = msg.get_int()
- if num == new_num:
- if t == CMD_STATUS:
- self._convert_status(msg)
- count = msg.get_int()
- for i in range(count):
- filename = msg.get_text()
- longname = msg.get_text()
- attr = SFTPAttributes._from_msg(
- msg, filename, longname
- )
- if (filename != ".") and (filename != ".."):
- yield attr
- # If we've hit the end of our queued requests, reset nums.
- nums = list()
- except EOFError:
- self._request(CMD_CLOSE, handle)
- return
- 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 `python:file` (aka `python:open`). A file-like
- object is returned, which closely mimics the behavior of a normal
- Python file object, including the ability to be used as a context
- manager.
- The mode indicates how the file is to be opened: ``'r'`` for reading,
- ``'w'`` for writing (truncating an existing file), ``'a'`` for
- appending, ``'r+'`` for reading/writing, ``'w+'`` for reading/writing
- (truncating an existing file), ``'a+'`` for reading/appending. The
- Python ``'b'`` flag is ignored, since SSH treats all files as binary.
- The ``'U'`` flag is supported in a compatible way.
- Since 1.5.2, an ``'x'`` flag indicates that the operation should only
- succeed if the file was created and did not previously exist. This has
- no direct mapping to Python's file flags, but is commonly known as the
- ``O_EXCL`` flag in posix.
- The file will be buffered in standard Python style by default, but
- can be altered with the ``bufsize`` parameter. ``<=0`` turns off
- buffering, ``1`` uses line buffering, and any number greater than 1
- (``>1``) uses that specific buffer size.
- :param str filename: name of the file to open
- :param str mode: mode (Python-style) to open in
- :param int bufsize: desired buffering (default: ``-1``)
- :return: an `.SFTPFile` object representing the open file
- :raises: ``IOError`` -- if the file could not be opened.
- """
- filename = self._adjust_cwd(filename)
- self._log(DEBUG, "open({!r}, {!r})".format(filename, mode))
- imode = 0
- if ("r" in mode) or ("+" in mode):
- imode |= SFTP_FLAG_READ
- if ("w" in mode) or ("+" in mode) or ("a" in mode):
- imode |= SFTP_FLAG_WRITE
- if "w" in mode:
- imode |= SFTP_FLAG_CREATE | SFTP_FLAG_TRUNC
- if "a" in mode:
- imode |= SFTP_FLAG_CREATE | SFTP_FLAG_APPEND
- if "x" in mode:
- imode |= SFTP_FLAG_CREATE | SFTP_FLAG_EXCL
- attrblock = SFTPAttributes()
- t, msg = self._request(CMD_OPEN, filename, imode, attrblock)
- if t != CMD_HANDLE:
- raise SFTPError("Expected handle")
- handle = msg.get_binary()
- self._log(
- DEBUG,
- "open({!r}, {!r}) -> {}".format(
- filename, mode, u(hexlify(handle))
- ),
- )
- return SFTPFile(self, handle, mode, bufsize)
- # Python continues to vacillate about "open" vs "file"...
- file = open
- def remove(self, path):
- """
- Remove the file at the given path. This only works on files; for
- removing folders (directories), use `rmdir`.
- :param str path: path (absolute or relative) of the file to remove
- :raises: ``IOError`` -- if the path refers to a folder (directory)
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "remove({!r})".format(path))
- self._request(CMD_REMOVE, path)
- unlink = remove
- def rename(self, oldpath, newpath):
- """
- Rename a file or folder from ``oldpath`` to ``newpath``.
- .. note::
- This method implements 'standard' SFTP ``RENAME`` behavior; those
- seeking the OpenSSH "POSIX rename" extension behavior should use
- `posix_rename`.
- :param str oldpath:
- existing name of the file or folder
- :param str newpath:
- new name for the file or folder, must not exist already
- :raises:
- ``IOError`` -- if ``newpath`` is a folder, or something else goes
- wrong
- """
- oldpath = self._adjust_cwd(oldpath)
- newpath = self._adjust_cwd(newpath)
- self._log(DEBUG, "rename({!r}, {!r})".format(oldpath, newpath))
- self._request(CMD_RENAME, oldpath, newpath)
- def posix_rename(self, oldpath, newpath):
- """
- Rename a file or folder from ``oldpath`` to ``newpath``, following
- posix conventions.
- :param str oldpath: existing name of the file or folder
- :param str newpath: new name for the file or folder, will be
- overwritten if it already exists
- :raises:
- ``IOError`` -- if ``newpath`` is a folder, posix-rename is not
- supported by the server or something else goes wrong
- :versionadded: 2.2
- """
- oldpath = self._adjust_cwd(oldpath)
- newpath = self._adjust_cwd(newpath)
- self._log(DEBUG, "posix_rename({!r}, {!r})".format(oldpath, newpath))
- self._request(
- CMD_EXTENDED, "posix-rename@openssh.com", oldpath, newpath
- )
- def mkdir(self, path, mode=o777):
- """
- Create a folder (directory) named ``path`` with numeric mode ``mode``.
- 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 str path: name of the folder to create
- :param int mode: permissions (posix-style) for the newly-created folder
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "mkdir({!r}, {!r})".format(path, mode))
- attr = SFTPAttributes()
- attr.st_mode = mode
- self._request(CMD_MKDIR, path, attr)
- def rmdir(self, path):
- """
- Remove the folder named ``path``.
- :param str path: name of the folder to remove
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "rmdir({!r})".format(path))
- self._request(CMD_RMDIR, path)
- def stat(self, path):
- """
- Retrieve information about a file on the remote system. The return
- value is an object whose attributes correspond to the attributes of
- Python's ``stat`` structure as returned by ``os.stat``, except that it
- contains fewer fields. An SFTP server may return as much or as little
- info as it wants, so the results may vary from server to server.
- Unlike a Python `python:stat` object, the result may not be accessed as
- a tuple. This is mostly due to the author's slack factor.
- The fields supported are: ``st_mode``, ``st_size``, ``st_uid``,
- ``st_gid``, ``st_atime``, and ``st_mtime``.
- :param str path: the filename to stat
- :return:
- an `.SFTPAttributes` object containing attributes about the given
- file
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "stat({!r})".format(path))
- t, msg = self._request(CMD_STAT, path)
- if t != CMD_ATTRS:
- raise SFTPError("Expected attributes")
- return SFTPAttributes._from_msg(msg)
- def lstat(self, path):
- """
- Retrieve information about a file on the remote system, without
- following symbolic links (shortcuts). This otherwise behaves exactly
- the same as `stat`.
- :param str path: the filename to stat
- :return:
- an `.SFTPAttributes` object containing attributes about the given
- file
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "lstat({!r})".format(path))
- t, msg = self._request(CMD_LSTAT, path)
- if t != CMD_ATTRS:
- raise SFTPError("Expected attributes")
- return SFTPAttributes._from_msg(msg)
- def symlink(self, source, dest):
- """
- Create a symbolic link to the ``source`` path at ``destination``.
- :param str source: path of the original file
- :param str dest: path of the newly created symlink
- """
- dest = self._adjust_cwd(dest)
- self._log(DEBUG, "symlink({!r}, {!r})".format(source, dest))
- source = b(source)
- self._request(CMD_SYMLINK, source, dest)
- def chmod(self, path, mode):
- """
- Change the mode (permissions) of a file. The permissions are
- unix-style and identical to those used by Python's `os.chmod`
- function.
- :param str path: path of the file to change the permissions of
- :param int mode: new permissions
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "chmod({!r}, {!r})".format(path, mode))
- attr = SFTPAttributes()
- attr.st_mode = mode
- self._request(CMD_SETSTAT, path, attr)
- def chown(self, path, uid, gid):
- """
- Change the owner (``uid``) and group (``gid``) of a file. As with
- Python's `os.chown` function, you must pass both arguments, so if you
- only want to change one, use `stat` first to retrieve the current
- owner and group.
- :param str path: path of the file to change the owner and group of
- :param int uid: new owner's uid
- :param int gid: new group id
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "chown({!r}, {!r}, {!r})".format(path, uid, gid))
- attr = SFTPAttributes()
- attr.st_uid, attr.st_gid = uid, gid
- self._request(CMD_SETSTAT, path, attr)
- def utime(self, path, times):
- """
- Set the access and modified times of the file specified by ``path``.
- If ``times`` is ``None``, then the file's access and modified times
- are set to the current time. Otherwise, ``times`` must be a 2-tuple
- of numbers, of the form ``(atime, mtime)``, which is used to set the
- access and modified times, respectively. This bizarre API is mimicked
- from Python for the sake of consistency -- I apologize.
- :param str path: path of the file to modify
- :param tuple times:
- ``None`` or a tuple of (access time, modified time) in standard
- internet epoch time (seconds since 01 January 1970 GMT)
- """
- path = self._adjust_cwd(path)
- if times is None:
- times = (time.time(), time.time())
- self._log(DEBUG, "utime({!r}, {!r})".format(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 ``path``. This usually
- extends or shrinks the size of the file, just like the `~file.truncate`
- method on Python file objects.
- :param str path: path of the file to modify
- :param int size: the new size of the file
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "truncate({!r}, {!r})".format(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
- `symlink` to create these. The result may be either an absolute or
- relative pathname.
- :param str path: path of the symbolic link file
- :return: target path, as a `str`
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "readlink({!r})".format(path))
- t, msg = self._request(CMD_READLINK, path)
- if t != CMD_NAME:
- raise SFTPError("Expected name response")
- count = msg.get_int()
- if count == 0:
- return None
- if count != 1:
- raise SFTPError("Readlink returned {} results".format(count))
- return _to_unicode(msg.get_string())
- def normalize(self, path):
- """
- Return the normalized path (on the server) of a given path. This
- can be used to quickly resolve symbolic links or determine what the
- server is considering to be the "current folder" (by passing ``'.'``
- as ``path``).
- :param str path: path to be normalized
- :return: normalized form of the given path (as a `str`)
- :raises: ``IOError`` -- if the path can't be resolved on the server
- """
- path = self._adjust_cwd(path)
- self._log(DEBUG, "normalize({!r})".format(path))
- t, msg = self._request(CMD_REALPATH, path)
- if t != CMD_NAME:
- raise SFTPError("Expected name response")
- count = msg.get_int()
- if count != 1:
- raise SFTPError("Realpath returned {} results".format(count))
- return msg.get_text()
- def chdir(self, path=None):
- """
- Change the "current directory" of this SFTP session. Since SFTP
- doesn't really have the concept of a current working directory, this is
- emulated by Paramiko. Once you use this method to set a working
- directory, all operations on this `.SFTPClient` object will be relative
- to that path. You can pass in ``None`` to stop using a current working
- directory.
- :param str path: new current working directory
- :raises:
- ``IOError`` -- if the requested path doesn't exist on the server
- .. versionadded:: 1.4
- """
- if path is None:
- self._cwd = None
- return
- if not stat.S_ISDIR(self.stat(path).st_mode):
- code = errno.ENOTDIR
- raise SFTPError(code, "{}: {}".format(os.strerror(code), path))
- self._cwd = b(self.normalize(path))
- def getcwd(self):
- """
- Return the "current working directory" for this SFTP session, as
- emulated by Paramiko. If no directory has been set with `chdir`,
- this method will return ``None``.
- .. versionadded:: 1.4
- """
- # TODO: make class initialize with self._cwd set to self.normalize('.')
- return self._cwd and u(self._cwd)
- def _transfer_with_callback(self, reader, writer, file_size, callback):
- size = 0
- while True:
- data = reader.read(32768)
- writer.write(data)
- size += len(data)
- if len(data) == 0:
- break
- if callback is not None:
- callback(size, file_size)
- return size
- def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True):
- """
- Copy the contents of an open file object (``fl``) to the SFTP server as
- ``remotepath``. Any exception raised by operations will be passed
- through.
- The SFTP operations use pipelining for speed.
- :param fl: opened file or file-like object to copy
- :param str remotepath: the destination path on the SFTP server
- :param int file_size:
- optional size parameter passed to callback. If none is specified,
- size defaults to 0
- :param callable callback:
- optional callback function (form: ``func(int, int)``) that accepts
- the bytes transferred so far and the total bytes to be transferred
- (since 1.7.4)
- :param bool confirm:
- whether to do a stat() on the file afterwards to confirm the file
- size (since 1.7.7)
- :return:
- an `.SFTPAttributes` object containing attributes about the given
- file.
- .. versionadded:: 1.10
- """
- with self.file(remotepath, "wb") as fr:
- fr.set_pipelined(True)
- size = self._transfer_with_callback(
- reader=fl, writer=fr, file_size=file_size, callback=callback
- )
- if confirm:
- s = self.stat(remotepath)
- if s.st_size != size:
- raise IOError(
- "size mismatch in put! {} != {}".format(s.st_size, size)
- )
- else:
- s = SFTPAttributes()
- return s
- def put(self, localpath, remotepath, callback=None, confirm=True):
- """
- Copy a local file (``localpath``) to the SFTP server as ``remotepath``.
- Any exception raised by operations will be passed through. This
- method is primarily provided as a convenience.
- The SFTP operations use pipelining for speed.
- :param str localpath: the local file to copy
- :param str remotepath: the destination path on the SFTP server. Note
- that the filename should be included. Only specifying a directory
- may result in an error.
- :param callable callback:
- optional callback function (form: ``func(int, int)``) that accepts
- the bytes transferred so far and the total bytes to be transferred
- :param bool confirm:
- whether to do a stat() on the file afterwards to confirm the file
- size
- :return: an `.SFTPAttributes` object containing attributes about the
- given file
- .. versionadded:: 1.4
- .. versionchanged:: 1.7.4
- ``callback`` and rich attribute return value added.
- .. versionchanged:: 1.7.7
- ``confirm`` param added.
- """
- file_size = os.stat(localpath).st_size
- with open(localpath, "rb") as fl:
- return self.putfo(fl, remotepath, file_size, callback, confirm)
- def getfo(self, remotepath, fl, callback=None, prefetch=True):
- """
- Copy a remote file (``remotepath``) from the SFTP server and write to
- an open file or file-like object, ``fl``. Any exception raised by
- operations will be passed through. This method is primarily provided
- as a convenience.
- :param object remotepath: opened file or file-like object to copy to
- :param str fl:
- the destination path on the local host or open file object
- :param callable callback:
- optional callback function (form: ``func(int, int)``) that accepts
- the bytes transferred so far and the total bytes to be transferred
- :param bool prefetch:
- controls whether prefetching is performed (default: True)
- :return: the `number <int>` of bytes written to the opened file object
- .. versionadded:: 1.10
- .. versionchanged:: 2.8
- Added the ``prefetch`` keyword argument.
- """
- file_size = self.stat(remotepath).st_size
- with self.open(remotepath, "rb") as fr:
- if prefetch:
- fr.prefetch(file_size)
- return self._transfer_with_callback(
- reader=fr, writer=fl, file_size=file_size, callback=callback
- )
- def get(self, remotepath, localpath, callback=None, prefetch=True):
- """
- Copy a remote file (``remotepath``) from the SFTP server to the local
- host as ``localpath``. Any exception raised by operations will be
- passed through. This method is primarily provided as a convenience.
- :param str remotepath: the remote file to copy
- :param str localpath: the destination path on the local host
- :param callable callback:
- optional callback function (form: ``func(int, int)``) that accepts
- the bytes transferred so far and the total bytes to be transferred
- :param bool prefetch:
- controls whether prefetching is performed (default: True)
- .. versionadded:: 1.4
- .. versionchanged:: 1.7.4
- Added the ``callback`` param
- .. versionchanged:: 2.8
- Added the ``prefetch`` keyword argument.
- """
- with open(localpath, "wb") as fl:
- size = self.getfo(remotepath, fl, callback, prefetch)
- s = os.stat(localpath)
- if s.st_size != size:
- raise IOError(
- "size mismatch in get! {} != {}".format(s.st_size, size)
- )
- # ...internals...
- def _request(self, t, *arg):
- num = self._async_request(type(None), t, *arg)
- return self._read_response(num)
- def _async_request(self, fileobj, t, *arg):
- # this method may be called from other threads (prefetch)
- self._lock.acquire()
- try:
- msg = Message()
- msg.add_int(self.request_number)
- for item in arg:
- if isinstance(item, long):
- msg.add_int64(item)
- elif isinstance(item, int):
- msg.add_int(item)
- elif isinstance(item, SFTPAttributes):
- item._pack(msg)
- else:
- # For all other types, rely on as_string() to either coerce
- # to bytes before writing or raise a suitable exception.
- msg.add_string(item)
- num = self.request_number
- self._expecting[num] = fileobj
- self.request_number += 1
- finally:
- self._lock.release()
- self._send_packet(t, msg)
- return num
- def _read_response(self, waitfor=None):
- while True:
- try:
- t, data = self._read_packet()
- except EOFError as e:
- raise SSHException("Server connection dropped: {}".format(e))
- msg = Message(data)
- num = msg.get_int()
- self._lock.acquire()
- try:
- if num not in self._expecting:
- # might be response for a file that was closed before
- # responses came back
- self._log(DEBUG, "Unexpected response #{}".format(num))
- if waitfor is None:
- # just doing a single check
- break
- continue
- fileobj = self._expecting[num]
- del self._expecting[num]
- finally:
- self._lock.release()
- if num == waitfor:
- # synchronous
- if t == CMD_STATUS:
- self._convert_status(msg)
- return t, msg
- # can not rewrite this to deal with E721, either as a None check
- # nor as not an instance of None or NoneType
- if fileobj is not type(None): # noqa
- fileobj._async_response(t, msg, num)
- if waitfor is None:
- # just doing a single check
- break
- return None, None
- def _finish_responses(self, fileobj):
- while fileobj in self._expecting.values():
- self._read_response()
- fileobj._check_exception()
- def _convert_status(self, msg):
- """
- Raises EOFError or IOError on error status; otherwise does nothing.
- """
- code = msg.get_int()
- text = msg.get_text()
- if code == SFTP_OK:
- return
- elif code == SFTP_EOF:
- raise EOFError(text)
- elif code == SFTP_NO_SUCH_FILE:
- # clever idea from john a. meinel: map the error codes to errno
- raise IOError(errno.ENOENT, text)
- elif code == SFTP_PERMISSION_DENIED:
- raise IOError(errno.EACCES, text)
- else:
- raise IOError(text)
- def _adjust_cwd(self, path):
- """
- Return an adjusted path if we're emulating a "current working
- directory" for the server.
- """
- path = b(path)
- if self._cwd is None:
- return path
- if len(path) and path[0:1] == b_slash:
- # absolute path
- return path
- if self._cwd == b_slash:
- return self._cwd + path
- return self._cwd + b_slash + path
- class SFTP(SFTPClient):
- """
- An alias for `.SFTPClient` for backwards compatibility.
- """
- pass
|