~davide-cst00/uftp/0.2

« back to all changes in this revision

Viewing changes to paramiko/sftp_client.py

  • Committer: Davide Costa
  • Date: 2018-09-14 12:21:37 UTC
  • Revision ID: davide.cst00@gmail.com-20180914122137-j0ycqb4tk9z2r1k5
Xenial release 0.2.1

Show diffs side-by-side

added added

removed removed

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