1# Copyright (C) 2003-2007  Robey Pointer <robeypointer@gmail.com>
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 distributed 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
20from binascii import hexlify
21import errno
22import os
23import stat
24import threading
25import time
26import weakref
27from paramiko import util
28from paramiko.channel import Channel
29from paramiko.message import Message
30from paramiko.common import INFO, DEBUG, o777
31from paramiko.py3compat import b, u, long
32from paramiko.sftp import (
33    BaseSFTP,
34    CMD_OPENDIR,
35    CMD_HANDLE,
36    SFTPError,
37    CMD_READDIR,
38    CMD_NAME,
39    CMD_CLOSE,
40    SFTP_FLAG_READ,
41    SFTP_FLAG_WRITE,
42    SFTP_FLAG_CREATE,
43    SFTP_FLAG_TRUNC,
44    SFTP_FLAG_APPEND,
45    SFTP_FLAG_EXCL,
46    CMD_OPEN,
47    CMD_REMOVE,
48    CMD_RENAME,
49    CMD_MKDIR,
50    CMD_RMDIR,
51    CMD_STAT,
52    CMD_ATTRS,
53    CMD_LSTAT,
54    CMD_SYMLINK,
55    CMD_SETSTAT,
56    CMD_READLINK,
57    CMD_REALPATH,
58    CMD_STATUS,
59    CMD_EXTENDED,
60    SFTP_OK,
61    SFTP_EOF,
62    SFTP_NO_SUCH_FILE,
63    SFTP_PERMISSION_DENIED,
64)
65
66from paramiko.sftp_attr import SFTPAttributes
67from paramiko.ssh_exception import SSHException
68from paramiko.sftp_file import SFTPFile
69from paramiko.util import ClosingContextManager
70
71
72def _to_unicode(s):
73    """
74    decode a string as ascii or utf8 if possible (as required by the sftp
75    protocol).  if neither works, just return a byte string because the server
76    probably doesn't know the filename's encoding.
77    """
78    try:
79        return s.encode("ascii")
80    except (UnicodeError, AttributeError):
81        try:
82            return s.decode("utf-8")
83        except UnicodeError:
84            return s
85
86
87b_slash = b"/"
88
89
90class SFTPClient(BaseSFTP, ClosingContextManager):
91    """
92    SFTP client object.
93
94    Used to open an SFTP session across an open SSH `.Transport` and perform
95    remote file operations.
96
97    Instances of this class may be used as context managers.
98    """
99
100    def __init__(self, sock):
101        """
102        Create an SFTP client from an existing `.Channel`.  The channel
103        should already have requested the ``"sftp"`` subsystem.
104
105        An alternate way to create an SFTP client context is by using
106        `from_transport`.
107
108        :param .Channel sock: an open `.Channel` using the ``"sftp"`` subsystem
109
110        :raises:
111            `.SSHException` -- if there's an exception while negotiating sftp
112        """
113        BaseSFTP.__init__(self)
114        self.sock = sock
115        self.ultra_debug = False
116        self.request_number = 1
117        # lock for request_number
118        self._lock = threading.Lock()
119        self._cwd = None
120        # request # -> SFTPFile
121        self._expecting = weakref.WeakValueDictionary()
122        if type(sock) is Channel:
123            # override default logger
124            transport = self.sock.get_transport()
125            self.logger = util.get_logger(
126                transport.get_log_channel() + ".sftp"
127            )
128            self.ultra_debug = transport.get_hexdump()
129        try:
130            server_version = self._send_version()
131        except EOFError:
132            raise SSHException("EOF during negotiation")
133        self._log(
134            INFO,
135            "Opened sftp connection (server version {})".format(
136                server_version
137            ),
138        )
139
140    @classmethod
141    def from_transport(cls, t, window_size=None, max_packet_size=None):
142        """
143        Create an SFTP client channel from an open `.Transport`.
144
145        Setting the window and packet sizes might affect the transfer speed.
146        The default settings in the `.Transport` class are the same as in
147        OpenSSH and should work adequately for both files transfers and
148        interactive sessions.
149
150        :param .Transport t: an open `.Transport` which is already
151            authenticated
152        :param int window_size:
153            optional window size for the `.SFTPClient` session.
154        :param int max_packet_size:
155            optional max packet size for the `.SFTPClient` session..
156
157        :return:
158            a new `.SFTPClient` object, referring to an sftp session (channel)
159            across the transport
160
161        .. versionchanged:: 1.15
162            Added the ``window_size`` and ``max_packet_size`` arguments.
163        """
164        chan = t.open_session(
165            window_size=window_size, max_packet_size=max_packet_size
166        )
167        if chan is None:
168            return None
169        chan.invoke_subsystem("sftp")
170        return cls(chan)
171
172    def _log(self, level, msg, *args):
173        if isinstance(msg, list):
174            for m in msg:
175                self._log(level, m, *args)
176        else:
177            # NOTE: these bits MUST continue using %-style format junk because
178            # logging.Logger.log() explicitly requires it. Grump.
179            # escape '%' in msg (they could come from file or directory names)
180            # before logging
181            msg = msg.replace("%", "%%")
182            super(SFTPClient, self)._log(
183                level,
184                "[chan %s] " + msg,
185                *([self.sock.get_name()] + list(args))
186            )
187
188    def close(self):
189        """
190        Close the SFTP session and its underlying channel.
191
192        .. versionadded:: 1.4
193        """
194        self._log(INFO, "sftp session closed.")
195        self.sock.close()
196
197    def get_channel(self):
198        """
199        Return the underlying `.Channel` object for this SFTP session.  This
200        might be useful for doing things like setting a timeout on the channel.
201
202        .. versionadded:: 1.7.1
203        """
204        return self.sock
205
206    def listdir(self, path="."):
207        """
208        Return a list containing the names of the entries in the given
209        ``path``.
210
211        The list is in arbitrary order.  It does not include the special
212        entries ``'.'`` and ``'..'`` even if they are present in the folder.
213        This method is meant to mirror ``os.listdir`` as closely as possible.
214        For a list of full `.SFTPAttributes` objects, see `listdir_attr`.
215
216        :param str path: path to list (defaults to ``'.'``)
217        """
218        return [f.filename for f in self.listdir_attr(path)]
219
220    def listdir_attr(self, path="."):
221        """
222        Return a list containing `.SFTPAttributes` objects corresponding to
223        files in the given ``path``.  The list is in arbitrary order.  It does
224        not include the special entries ``'.'`` and ``'..'`` even if they are
225        present in the folder.
226
227        The returned `.SFTPAttributes` objects will each have an additional
228        field: ``longname``, which may contain a formatted string of the file's
229        attributes, in unix format.  The content of this string will probably
230        depend on the SFTP server implementation.
231
232        :param str path: path to list (defaults to ``'.'``)
233        :return: list of `.SFTPAttributes` objects
234
235        .. versionadded:: 1.2
236        """
237        path = self._adjust_cwd(path)
238        self._log(DEBUG, "listdir({!r})".format(path))
239        t, msg = self._request(CMD_OPENDIR, path)
240        if t != CMD_HANDLE:
241            raise SFTPError("Expected handle")
242        handle = msg.get_binary()
243        filelist = []
244        while True:
245            try:
246                t, msg = self._request(CMD_READDIR, handle)
247            except EOFError:
248                # done with handle
249                break
250            if t != CMD_NAME:
251                raise SFTPError("Expected name response")
252            count = msg.get_int()
253            for i in range(count):
254                filename = msg.get_text()
255                longname = msg.get_text()
256                attr = SFTPAttributes._from_msg(msg, filename, longname)
257                if (filename != ".") and (filename != ".."):
258                    filelist.append(attr)
259        self._request(CMD_CLOSE, handle)
260        return filelist
261
262    def listdir_iter(self, path=".", read_aheads=50):
263        """
264        Generator version of `.listdir_attr`.
265
266        See the API docs for `.listdir_attr` for overall details.
267
268        This function adds one more kwarg on top of `.listdir_attr`:
269        ``read_aheads``, an integer controlling how many
270        ``SSH_FXP_READDIR`` requests are made to the server. The default of 50
271        should suffice for most file listings as each request/response cycle
272        may contain multiple files (dependent on server implementation.)
273
274        .. versionadded:: 1.15
275        """
276        path = self._adjust_cwd(path)
277        self._log(DEBUG, "listdir({!r})".format(path))
278        t, msg = self._request(CMD_OPENDIR, path)
279
280        if t != CMD_HANDLE:
281            raise SFTPError("Expected handle")
282
283        handle = msg.get_string()
284
285        nums = list()
286        while True:
287            try:
288                # Send out a bunch of readdir requests so that we can read the
289                # responses later on Section 6.7 of the SSH file transfer RFC
290                # explains this
291                # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
292                for i in range(read_aheads):
293                    num = self._async_request(type(None), CMD_READDIR, handle)
294                    nums.append(num)
295
296                # For each of our sent requests
297                # Read and parse the corresponding packets
298                # If we're at the end of our queued requests, then fire off
299                # some more requests
300                # Exit the loop when we've reached the end of the directory
301                # handle
302                for num in nums:
303                    t, pkt_data = self._read_packet()
304                    msg = Message(pkt_data)
305                    new_num = msg.get_int()
306                    if num == new_num:
307                        if t == CMD_STATUS:
308                            self._convert_status(msg)
309                    count = msg.get_int()
310                    for i in range(count):
311                        filename = msg.get_text()
312                        longname = msg.get_text()
313                        attr = SFTPAttributes._from_msg(
314                            msg, filename, longname
315                        )
316                        if (filename != ".") and (filename != ".."):
317                            yield attr
318
319                # If we've hit the end of our queued requests, reset nums.
320                nums = list()
321
322            except EOFError:
323                self._request(CMD_CLOSE, handle)
324                return
325
326    def open(self, filename, mode="r", bufsize=-1):
327        """
328        Open a file on the remote server.  The arguments are the same as for
329        Python's built-in `python:file` (aka `python:open`).  A file-like
330        object is returned, which closely mimics the behavior of a normal
331        Python file object, including the ability to be used as a context
332        manager.
333
334        The mode indicates how the file is to be opened: ``'r'`` for reading,
335        ``'w'`` for writing (truncating an existing file), ``'a'`` for
336        appending, ``'r+'`` for reading/writing, ``'w+'`` for reading/writing
337        (truncating an existing file), ``'a+'`` for reading/appending.  The
338        Python ``'b'`` flag is ignored, since SSH treats all files as binary.
339        The ``'U'`` flag is supported in a compatible way.
340
341        Since 1.5.2, an ``'x'`` flag indicates that the operation should only
342        succeed if the file was created and did not previously exist.  This has
343        no direct mapping to Python's file flags, but is commonly known as the
344        ``O_EXCL`` flag in posix.
345
346        The file will be buffered in standard Python style by default, but
347        can be altered with the ``bufsize`` parameter.  ``0`` turns off
348        buffering, ``1`` uses line buffering, and any number greater than 1
349        (``>1``) uses that specific buffer size.
350
351        :param str filename: name of the file to open
352        :param str mode: mode (Python-style) to open in
353        :param int bufsize: desired buffering (-1 = default buffer size)
354        :return: an `.SFTPFile` object representing the open file
355
356        :raises: ``IOError`` -- if the file could not be opened.
357        """
358        filename = self._adjust_cwd(filename)
359        self._log(DEBUG, "open({!r}, {!r})".format(filename, mode))
360        imode = 0
361        if ("r" in mode) or ("+" in mode):
362            imode |= SFTP_FLAG_READ
363        if ("w" in mode) or ("+" in mode) or ("a" in mode):
364            imode |= SFTP_FLAG_WRITE
365        if "w" in mode:
366            imode |= SFTP_FLAG_CREATE | SFTP_FLAG_TRUNC
367        if "a" in mode:
368            imode |= SFTP_FLAG_CREATE | SFTP_FLAG_APPEND
369        if "x" in mode:
370            imode |= SFTP_FLAG_CREATE | SFTP_FLAG_EXCL
371        attrblock = SFTPAttributes()
372        t, msg = self._request(CMD_OPEN, filename, imode, attrblock)
373        if t != CMD_HANDLE:
374            raise SFTPError("Expected handle")
375        handle = msg.get_binary()
376        self._log(
377            DEBUG,
378            "open({!r}, {!r}) -> {}".format(
379                filename, mode, u(hexlify(handle))
380            ),
381        )
382        return SFTPFile(self, handle, mode, bufsize)
383
384    # Python continues to vacillate about "open" vs "file"...
385    file = open
386
387    def remove(self, path):
388        """
389        Remove the file at the given path.  This only works on files; for
390        removing folders (directories), use `rmdir`.
391
392        :param str path: path (absolute or relative) of the file to remove
393
394        :raises: ``IOError`` -- if the path refers to a folder (directory)
395        """
396        path = self._adjust_cwd(path)
397        self._log(DEBUG, "remove({!r})".format(path))
398        self._request(CMD_REMOVE, path)
399
400    unlink = remove
401
402    def rename(self, oldpath, newpath):
403        """
404        Rename a file or folder from ``oldpath`` to ``newpath``.
405
406        .. note::
407            This method implements 'standard' SFTP ``RENAME`` behavior; those
408            seeking the OpenSSH "POSIX rename" extension behavior should use
409            `posix_rename`.
410
411        :param str oldpath:
412            existing name of the file or folder
413        :param str newpath:
414            new name for the file or folder, must not exist already
415
416        :raises:
417            ``IOError`` -- if ``newpath`` is a folder, or something else goes
418            wrong
419        """
420        oldpath = self._adjust_cwd(oldpath)
421        newpath = self._adjust_cwd(newpath)
422        self._log(DEBUG, "rename({!r}, {!r})".format(oldpath, newpath))
423        self._request(CMD_RENAME, oldpath, newpath)
424
425    def posix_rename(self, oldpath, newpath):
426        """
427        Rename a file or folder from ``oldpath`` to ``newpath``, following
428        posix conventions.
429
430        :param str oldpath: existing name of the file or folder
431        :param str newpath: new name for the file or folder, will be
432            overwritten if it already exists
433
434        :raises:
435            ``IOError`` -- if ``newpath`` is a folder, posix-rename is not
436            supported by the server or something else goes wrong
437
438        :versionadded: 2.2
439        """
440        oldpath = self._adjust_cwd(oldpath)
441        newpath = self._adjust_cwd(newpath)
442        self._log(DEBUG, "posix_rename({!r}, {!r})".format(oldpath, newpath))
443        self._request(
444            CMD_EXTENDED, "posix-rename@openssh.com", oldpath, newpath
445        )
446
447    def mkdir(self, path, mode=o777):
448        """
449        Create a folder (directory) named ``path`` with numeric mode ``mode``.
450        The default mode is 0777 (octal).  On some systems, mode is ignored.
451        Where it is used, the current umask value is first masked out.
452
453        :param str path: name of the folder to create
454        :param int mode: permissions (posix-style) for the newly-created folder
455        """
456        path = self._adjust_cwd(path)
457        self._log(DEBUG, "mkdir({!r}, {!r})".format(path, mode))
458        attr = SFTPAttributes()
459        attr.st_mode = mode
460        self._request(CMD_MKDIR, path, attr)
461
462    def rmdir(self, path):
463        """
464        Remove the folder named ``path``.
465
466        :param str path: name of the folder to remove
467        """
468        path = self._adjust_cwd(path)
469        self._log(DEBUG, "rmdir({!r})".format(path))
470        self._request(CMD_RMDIR, path)
471
472    def stat(self, path):
473        """
474        Retrieve information about a file on the remote system.  The return
475        value is an object whose attributes correspond to the attributes of
476        Python's ``stat`` structure as returned by ``os.stat``, except that it
477        contains fewer fields.  An SFTP server may return as much or as little
478        info as it wants, so the results may vary from server to server.
479
480        Unlike a Python `python:stat` object, the result may not be accessed as
481        a tuple.  This is mostly due to the author's slack factor.
482
483        The fields supported are: ``st_mode``, ``st_size``, ``st_uid``,
484        ``st_gid``, ``st_atime``, and ``st_mtime``.
485
486        :param str path: the filename to stat
487        :return:
488            an `.SFTPAttributes` object containing attributes about the given
489            file
490        """
491        path = self._adjust_cwd(path)
492        self._log(DEBUG, "stat({!r})".format(path))
493        t, msg = self._request(CMD_STAT, path)
494        if t != CMD_ATTRS:
495            raise SFTPError("Expected attributes")
496        return SFTPAttributes._from_msg(msg)
497
498    def lstat(self, path):
499        """
500        Retrieve information about a file on the remote system, without
501        following symbolic links (shortcuts).  This otherwise behaves exactly
502        the same as `stat`.
503
504        :param str path: the filename to stat
505        :return:
506            an `.SFTPAttributes` object containing attributes about the given
507            file
508        """
509        path = self._adjust_cwd(path)
510        self._log(DEBUG, "lstat({!r})".format(path))
511        t, msg = self._request(CMD_LSTAT, path)
512        if t != CMD_ATTRS:
513            raise SFTPError("Expected attributes")
514        return SFTPAttributes._from_msg(msg)
515
516    def symlink(self, source, dest):
517        """
518        Create a symbolic link to the ``source`` path at ``destination``.
519
520        :param str source: path of the original file
521        :param str dest: path of the newly created symlink
522        """
523        dest = self._adjust_cwd(dest)
524        self._log(DEBUG, "symlink({!r}, {!r})".format(source, dest))
525        source = b(source)
526        self._request(CMD_SYMLINK, source, dest)
527
528    def chmod(self, path, mode):
529        """
530        Change the mode (permissions) of a file.  The permissions are
531        unix-style and identical to those used by Python's `os.chmod`
532        function.
533
534        :param str path: path of the file to change the permissions of
535        :param int mode: new permissions
536        """
537        path = self._adjust_cwd(path)
538        self._log(DEBUG, "chmod({!r}, {!r})".format(path, mode))
539        attr = SFTPAttributes()
540        attr.st_mode = mode
541        self._request(CMD_SETSTAT, path, attr)
542
543    def chown(self, path, uid, gid):
544        """
545        Change the owner (``uid``) and group (``gid``) of a file.  As with
546        Python's `os.chown` function, you must pass both arguments, so if you
547        only want to change one, use `stat` first to retrieve the current
548        owner and group.
549
550        :param str path: path of the file to change the owner and group of
551        :param int uid: new owner's uid
552        :param int gid: new group id
553        """
554        path = self._adjust_cwd(path)
555        self._log(DEBUG, "chown({!r}, {!r}, {!r})".format(path, uid, gid))
556        attr = SFTPAttributes()
557        attr.st_uid, attr.st_gid = uid, gid
558        self._request(CMD_SETSTAT, path, attr)
559
560    def utime(self, path, times):
561        """
562        Set the access and modified times of the file specified by ``path``.
563        If ``times`` is ``None``, then the file's access and modified times
564        are set to the current time.  Otherwise, ``times`` must be a 2-tuple
565        of numbers, of the form ``(atime, mtime)``, which is used to set the
566        access and modified times, respectively.  This bizarre API is mimicked
567        from Python for the sake of consistency -- I apologize.
568
569        :param str path: path of the file to modify
570        :param tuple times:
571            ``None`` or a tuple of (access time, modified time) in standard
572            internet epoch time (seconds since 01 January 1970 GMT)
573        """
574        path = self._adjust_cwd(path)
575        if times is None:
576            times = (time.time(), time.time())
577        self._log(DEBUG, "utime({!r}, {!r})".format(path, times))
578        attr = SFTPAttributes()
579        attr.st_atime, attr.st_mtime = times
580        self._request(CMD_SETSTAT, path, attr)
581
582    def truncate(self, path, size):
583        """
584        Change the size of the file specified by ``path``.  This usually
585        extends or shrinks the size of the file, just like the `~file.truncate`
586        method on Python file objects.
587
588        :param str path: path of the file to modify
589        :param int size: the new size of the file
590        """
591        path = self._adjust_cwd(path)
592        self._log(DEBUG, "truncate({!r}, {!r})".format(path, size))
593        attr = SFTPAttributes()
594        attr.st_size = size
595        self._request(CMD_SETSTAT, path, attr)
596
597    def readlink(self, path):
598        """
599        Return the target of a symbolic link (shortcut).  You can use
600        `symlink` to create these.  The result may be either an absolute or
601        relative pathname.
602
603        :param str path: path of the symbolic link file
604        :return: target path, as a `str`
605        """
606        path = self._adjust_cwd(path)
607        self._log(DEBUG, "readlink({!r})".format(path))
608        t, msg = self._request(CMD_READLINK, path)
609        if t != CMD_NAME:
610            raise SFTPError("Expected name response")
611        count = msg.get_int()
612        if count == 0:
613            return None
614        if count != 1:
615            raise SFTPError("Readlink returned {} results".format(count))
616        return _to_unicode(msg.get_string())
617
618    def normalize(self, path):
619        """
620        Return the normalized path (on the server) of a given path.  This
621        can be used to quickly resolve symbolic links or determine what the
622        server is considering to be the "current folder" (by passing ``'.'``
623        as ``path``).
624
625        :param str path: path to be normalized
626        :return: normalized form of the given path (as a `str`)
627
628        :raises: ``IOError`` -- if the path can't be resolved on the server
629        """
630        path = self._adjust_cwd(path)
631        self._log(DEBUG, "normalize({!r})".format(path))
632        t, msg = self._request(CMD_REALPATH, path)
633        if t != CMD_NAME:
634            raise SFTPError("Expected name response")
635        count = msg.get_int()
636        if count != 1:
637            raise SFTPError("Realpath returned {} results".format(count))
638        return msg.get_text()
639
640    def chdir(self, path=None):
641        """
642        Change the "current directory" of this SFTP session.  Since SFTP
643        doesn't really have the concept of a current working directory, this is
644        emulated by Paramiko.  Once you use this method to set a working
645        directory, all operations on this `.SFTPClient` object will be relative
646        to that path. You can pass in ``None`` to stop using a current working
647        directory.
648
649        :param str path: new current working directory
650
651        :raises:
652            ``IOError`` -- if the requested path doesn't exist on the server
653
654        .. versionadded:: 1.4
655        """
656        if path is None:
657            self._cwd = None
658            return
659        if not stat.S_ISDIR(self.stat(path).st_mode):
660            code = errno.ENOTDIR
661            raise SFTPError(code, "{}: {}".format(os.strerror(code), path))
662        self._cwd = b(self.normalize(path))
663
664    def getcwd(self):
665        """
666        Return the "current working directory" for this SFTP session, as
667        emulated by Paramiko.  If no directory has been set with `chdir`,
668        this method will return ``None``.
669
670        .. versionadded:: 1.4
671        """
672        # TODO: make class initialize with self._cwd set to self.normalize('.')
673        return self._cwd and u(self._cwd)
674
675    def _transfer_with_callback(self, reader, writer, file_size, callback):
676        size = 0
677        while True:
678            data = reader.read(32768)
679            writer.write(data)
680            size += len(data)
681            if len(data) == 0:
682                break
683            if callback is not None:
684                callback(size, file_size)
685        return size
686
687    def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True):
688        """
689        Copy the contents of an open file object (``fl``) to the SFTP server as
690        ``remotepath``. Any exception raised by operations will be passed
691        through.
692
693        The SFTP operations use pipelining for speed.
694
695        :param fl: opened file or file-like object to copy
696        :param str remotepath: the destination path on the SFTP server
697        :param int file_size:
698            optional size parameter passed to callback. If none is specified,
699            size defaults to 0
700        :param callable callback:
701            optional callback function (form: ``func(int, int)``) that accepts
702            the bytes transferred so far and the total bytes to be transferred
703            (since 1.7.4)
704        :param bool confirm:
705            whether to do a stat() on the file afterwards to confirm the file
706            size (since 1.7.7)
707
708        :return:
709            an `.SFTPAttributes` object containing attributes about the given
710            file.
711
712        .. versionadded:: 1.10
713        """
714        with self.file(remotepath, "wb") as fr:
715            fr.set_pipelined(True)
716            size = self._transfer_with_callback(
717                reader=fl, writer=fr, file_size=file_size, callback=callback
718            )
719        if confirm:
720            s = self.stat(remotepath)
721            if s.st_size != size:
722                raise IOError(
723                    "size mismatch in put!  {} != {}".format(s.st_size, size)
724                )
725        else:
726            s = SFTPAttributes()
727        return s
728
729    def put(self, localpath, remotepath, callback=None, confirm=True):
730        """
731        Copy a local file (``localpath``) to the SFTP server as ``remotepath``.
732        Any exception raised by operations will be passed through.  This
733        method is primarily provided as a convenience.
734
735        The SFTP operations use pipelining for speed.
736
737        :param str localpath: the local file to copy
738        :param str remotepath: the destination path on the SFTP server. Note
739            that the filename should be included. Only specifying a directory
740            may result in an error.
741        :param callable callback:
742            optional callback function (form: ``func(int, int)``) that accepts
743            the bytes transferred so far and the total bytes to be transferred
744        :param bool confirm:
745            whether to do a stat() on the file afterwards to confirm the file
746            size
747
748        :return: an `.SFTPAttributes` object containing attributes about the
749            given file
750
751        .. versionadded:: 1.4
752        .. versionchanged:: 1.7.4
753            ``callback`` and rich attribute return value added.
754        .. versionchanged:: 1.7.7
755            ``confirm`` param added.
756        """
757        file_size = os.stat(localpath).st_size
758        with open(localpath, "rb") as fl:
759            return self.putfo(fl, remotepath, file_size, callback, confirm)
760
761    def getfo(self, remotepath, fl, callback=None):
762        """
763        Copy a remote file (``remotepath``) from the SFTP server and write to
764        an open file or file-like object, ``fl``.  Any exception raised by
765        operations will be passed through.  This method is primarily provided
766        as a convenience.
767
768        :param object remotepath: opened file or file-like object to copy to
769        :param str fl:
770            the destination path on the local host or open file object
771        :param callable callback:
772            optional callback function (form: ``func(int, int)``) that accepts
773            the bytes transferred so far and the total bytes to be transferred
774        :return: the `number <int>` of bytes written to the opened file object
775
776        .. versionadded:: 1.10
777        """
778        file_size = self.stat(remotepath).st_size
779        with self.open(remotepath, "rb") as fr:
780            fr.prefetch(file_size)
781            return self._transfer_with_callback(
782                reader=fr, writer=fl, file_size=file_size, callback=callback
783            )
784
785    def get(self, remotepath, localpath, callback=None):
786        """
787        Copy a remote file (``remotepath``) from the SFTP server to the local
788        host as ``localpath``.  Any exception raised by operations will be
789        passed through.  This method is primarily provided as a convenience.
790
791        :param str remotepath: the remote file to copy
792        :param str localpath: the destination path on the local host
793        :param callable callback:
794            optional callback function (form: ``func(int, int)``) that accepts
795            the bytes transferred so far and the total bytes to be transferred
796
797        .. versionadded:: 1.4
798        .. versionchanged:: 1.7.4
799            Added the ``callback`` param
800        """
801        with open(localpath, "wb") as fl:
802            size = self.getfo(remotepath, fl, callback)
803        s = os.stat(localpath)
804        if s.st_size != size:
805            raise IOError(
806                "size mismatch in get!  {} != {}".format(s.st_size, size)
807            )
808
809    # ...internals...
810
811    def _request(self, t, *arg):
812        num = self._async_request(type(None), t, *arg)
813        return self._read_response(num)
814
815    def _async_request(self, fileobj, t, *arg):
816        # this method may be called from other threads (prefetch)
817        self._lock.acquire()
818        try:
819            msg = Message()
820            msg.add_int(self.request_number)
821            for item in arg:
822                if isinstance(item, long):
823                    msg.add_int64(item)
824                elif isinstance(item, int):
825                    msg.add_int(item)
826                elif isinstance(item, SFTPAttributes):
827                    item._pack(msg)
828                else:
829                    # For all other types, rely on as_string() to either coerce
830                    # to bytes before writing or raise a suitable exception.
831                    msg.add_string(item)
832            num = self.request_number
833            self._expecting[num] = fileobj
834            self.request_number += 1
835        finally:
836            self._lock.release()
837        self._send_packet(t, msg)
838        return num
839
840    def _read_response(self, waitfor=None):
841        while True:
842            try:
843                t, data = self._read_packet()
844            except EOFError as e:
845                raise SSHException("Server connection dropped: {}".format(e))
846            msg = Message(data)
847            num = msg.get_int()
848            self._lock.acquire()
849            try:
850                if num not in self._expecting:
851                    # might be response for a file that was closed before
852                    # responses came back
853                    self._log(DEBUG, "Unexpected response #{}".format(num))
854                    if waitfor is None:
855                        # just doing a single check
856                        break
857                    continue
858                fileobj = self._expecting[num]
859                del self._expecting[num]
860            finally:
861                self._lock.release()
862            if num == waitfor:
863                # synchronous
864                if t == CMD_STATUS:
865                    self._convert_status(msg)
866                return t, msg
867
868            # can not rewrite this to deal with E721, either as a None check
869            # nor as not an instance of None or NoneType
870            if fileobj is not type(None):  # noqa
871                fileobj._async_response(t, msg, num)
872            if waitfor is None:
873                # just doing a single check
874                break
875        return None, None
876
877    def _finish_responses(self, fileobj):
878        while fileobj in self._expecting.values():
879            self._read_response()
880            fileobj._check_exception()
881
882    def _convert_status(self, msg):
883        """
884        Raises EOFError or IOError on error status; otherwise does nothing.
885        """
886        code = msg.get_int()
887        text = msg.get_text()
888        if code == SFTP_OK:
889            return
890        elif code == SFTP_EOF:
891            raise EOFError(text)
892        elif code == SFTP_NO_SUCH_FILE:
893            # clever idea from john a. meinel: map the error codes to errno
894            raise IOError(errno.ENOENT, text)
895        elif code == SFTP_PERMISSION_DENIED:
896            raise IOError(errno.EACCES, text)
897        else:
898            raise IOError(text)
899
900    def _adjust_cwd(self, path):
901        """
902        Return an adjusted path if we're emulating a "current working
903        directory" for the server.
904        """
905        path = b(path)
906        if self._cwd is None:
907            return path
908        if len(path) and path[0:1] == b_slash:
909            # absolute path
910            return path
911        if self._cwd == b_slash:
912            return self._cwd + path
913        return self._cwd + b_slash + path
914
915
916class SFTP(SFTPClient):
917    """
918    An alias for `.SFTPClient` for backwards compatibility.
919    """
920
921    pass
922