Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
sftp_file.py595 linesDownload Raw Back to paramiko
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 the6# terms of the GNU Lesser General Public License as published by the Free7# 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 ANY11# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR12# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more13# details.14#15# You should have received a copy of the GNU Lesser General Public License16# along with Paramiko; if not, write to the Free Software Foundation, Inc.,17# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.18 19"""20SFTP file object21"""22 23 24from binascii import hexlify25from collections import deque26import socket27import threading28import time29from paramiko.common import DEBUG, io_sleep30 31from paramiko.file import BufferedFile32from paramiko.util import u33from paramiko.sftp import (34    CMD_CLOSE,35    CMD_READ,36    CMD_DATA,37    SFTPError,38    CMD_WRITE,39    CMD_STATUS,40    CMD_FSTAT,41    CMD_ATTRS,42    CMD_FSETSTAT,43    CMD_EXTENDED,44    int64,45)46from paramiko.sftp_attr import SFTPAttributes47 48 49class SFTPFile(BufferedFile):50    """51    Proxy object for a file on the remote server, in client mode SFTP.52 53    Instances of this class may be used as context managers in the same way54    that built-in Python file objects are.55    """56 57    # Some sftp servers will choke if you send read/write requests larger than58    # this size.59    MAX_REQUEST_SIZE = 3276860 61    def __init__(self, sftp, handle, mode="r", bufsize=-1):62        BufferedFile.__init__(self)63        self.sftp = sftp64        self.handle = handle65        BufferedFile._set_mode(self, mode, bufsize)66        self.pipelined = False67        self._prefetching = False68        self._prefetch_done = False69        self._prefetch_data = {}70        self._prefetch_extents = {}71        self._prefetch_lock = threading.Lock()72        self._saved_exception = None73        self._reqs = deque()74 75    def __del__(self):76        self._close(async_=True)77 78    def close(self):79        """80        Close the file.81        """82        self._close(async_=False)83 84    def _close(self, async_=False):85        # We allow double-close without signaling an error, because real86        # Python file objects do.  However, we must protect against actually87        # sending multiple CMD_CLOSE packets, because after we close our88        # handle, the same handle may be re-allocated by the server, and we89        # may end up mysteriously closing some random other file.  (This is90        # especially important because we unconditionally call close() from91        # __del__.)92        if self._closed:93            return94        self.sftp._log(DEBUG, "close({})".format(u(hexlify(self.handle))))95        if self.pipelined:96            self.sftp._finish_responses(self)97        BufferedFile.close(self)98        try:99            if async_:100                # GC'd file handle could be called from an arbitrary thread101                # -- don't wait for a response102                self.sftp._async_request(type(None), CMD_CLOSE, self.handle)103            else:104                self.sftp._request(CMD_CLOSE, self.handle)105        except EOFError:106            # may have outlived the Transport connection107            pass108        except (IOError, socket.error):109            # may have outlived the Transport connection110            pass111 112    def _data_in_prefetch_requests(self, offset, size):113        k = [114            x for x in list(self._prefetch_extents.values()) if x[0] <= offset115        ]116        if len(k) == 0:117            return False118        k.sort(key=lambda x: x[0])119        buf_offset, buf_size = k[-1]120        if buf_offset + buf_size <= offset:121            # prefetch request ends before this one begins122            return False123        if buf_offset + buf_size >= offset + size:124            # inclusive125            return True126        # well, we have part of the request.  see if another chunk has127        # the rest.128        return self._data_in_prefetch_requests(129            buf_offset + buf_size, offset + size - buf_offset - buf_size130        )131 132    def _data_in_prefetch_buffers(self, offset):133        """134        if a block of data is present in the prefetch buffers, at the given135        offset, return the offset of the relevant prefetch buffer.  otherwise,136        return None.  this guarantees nothing about the number of bytes137        collected in the prefetch buffer so far.138        """139        k = [i for i in self._prefetch_data.keys() if i <= offset]140        if len(k) == 0:141            return None142        index = max(k)143        buf_offset = offset - index144        if buf_offset >= len(self._prefetch_data[index]):145            # it's not here146            return None147        return index148 149    def _read_prefetch(self, size):150        """151        read data out of the prefetch buffer, if possible.  if the data isn't152        in the buffer, return None.  otherwise, behaves like a normal read.153        """154        # while not closed, and haven't fetched past the current position,155        # and haven't reached EOF...156        while True:157            offset = self._data_in_prefetch_buffers(self._realpos)158            if offset is not None:159                break160            if self._prefetch_done or self._closed:161                break162            self.sftp._read_response()163            self._check_exception()164        if offset is None:165            self._prefetching = False166            return None167        prefetch = self._prefetch_data[offset]168        del self._prefetch_data[offset]169 170        buf_offset = self._realpos - offset171        if buf_offset > 0:172            self._prefetch_data[offset] = prefetch[:buf_offset]173            prefetch = prefetch[buf_offset:]174        if size < len(prefetch):175            self._prefetch_data[self._realpos + size] = prefetch[size:]176            prefetch = prefetch[:size]177        return prefetch178 179    def _read(self, size):180        size = min(size, self.MAX_REQUEST_SIZE)181        if self._prefetching:182            data = self._read_prefetch(size)183            if data is not None:184                return data185        t, msg = self.sftp._request(186            CMD_READ, self.handle, int64(self._realpos), int(size)187        )188        if t != CMD_DATA:189            raise SFTPError("Expected data")190        return msg.get_string()191 192    def _write(self, data):193        # may write less than requested if it would exceed max packet size194        chunk = min(len(data), self.MAX_REQUEST_SIZE)195        sftp_async_request = self.sftp._async_request(196            type(None),197            CMD_WRITE,198            self.handle,199            int64(self._realpos),200            data[:chunk],201        )202        self._reqs.append(sftp_async_request)203        if not self.pipelined or (204            len(self._reqs) > 100 and self.sftp.sock.recv_ready()205        ):206            while len(self._reqs):207                req = self._reqs.popleft()208                t, msg = self.sftp._read_response(req)209                if t != CMD_STATUS:210                    raise SFTPError("Expected status")211                # convert_status already called212        return chunk213 214    def settimeout(self, timeout):215        """216        Set a timeout on read/write operations on the underlying socket or217        ssh `.Channel`.218 219        :param float timeout:220            seconds to wait for a pending read/write operation before raising221            ``socket.timeout``, or ``None`` for no timeout222 223        .. seealso:: `.Channel.settimeout`224        """225        self.sftp.sock.settimeout(timeout)226 227    def gettimeout(self):228        """229        Returns the timeout in seconds (as a `float`) associated with the230        socket or ssh `.Channel` used for this file.231 232        .. seealso:: `.Channel.gettimeout`233        """234        return self.sftp.sock.gettimeout()235 236    def setblocking(self, blocking):237        """238        Set blocking or non-blocking mode on the underiying socket or ssh239        `.Channel`.240 241        :param int blocking:242            0 to set non-blocking mode; non-0 to set blocking mode.243 244        .. seealso:: `.Channel.setblocking`245        """246        self.sftp.sock.setblocking(blocking)247 248    def seekable(self):249        """250        Check if the file supports random access.251 252        :return:253            `True` if the file supports random access. If `False`,254            :meth:`seek` will raise an exception255        """256        return True257 258    def seek(self, offset, whence=0):259        """260        Set the file's current position.261 262        See `file.seek` for details.263        """264        self.flush()265        if whence == self.SEEK_SET:266            self._realpos = self._pos = offset267        elif whence == self.SEEK_CUR:268            self._pos += offset269            self._realpos = self._pos270        else:271            self._realpos = self._pos = self._get_size() + offset272        self._rbuffer = bytes()273 274    def stat(self):275        """276        Retrieve information about this file from the remote system.  This is277        exactly like `.SFTPClient.stat`, except that it operates on an278        already-open file.279 280        :returns:281            an `.SFTPAttributes` object containing attributes about this file.282        """283        t, msg = self.sftp._request(CMD_FSTAT, self.handle)284        if t != CMD_ATTRS:285            raise SFTPError("Expected attributes")286        return SFTPAttributes._from_msg(msg)287 288    def chmod(self, mode):289        """290        Change the mode (permissions) of this file.  The permissions are291        unix-style and identical to those used by Python's `os.chmod`292        function.293 294        :param int mode: new permissions295        """296        self.sftp._log(297            DEBUG, "chmod({}, {!r})".format(hexlify(self.handle), mode)298        )299        attr = SFTPAttributes()300        attr.st_mode = mode301        self.sftp._request(CMD_FSETSTAT, self.handle, attr)302 303    def chown(self, uid, gid):304        """305        Change the owner (``uid``) and group (``gid``) of this file.  As with306        Python's `os.chown` function, you must pass both arguments, so if you307        only want to change one, use `stat` first to retrieve the current308        owner and group.309 310        :param int uid: new owner's uid311        :param int gid: new group id312        """313        self.sftp._log(314            DEBUG,315            "chown({}, {!r}, {!r})".format(hexlify(self.handle), uid, gid),316        )317        attr = SFTPAttributes()318        attr.st_uid, attr.st_gid = uid, gid319        self.sftp._request(CMD_FSETSTAT, self.handle, attr)320 321    def utime(self, times):322        """323        Set the access and modified times of this file.  If324        ``times`` is ``None``, then the file's access and modified times are325        set to the current time.  Otherwise, ``times`` must be a 2-tuple of326        numbers, of the form ``(atime, mtime)``, which is used to set the327        access and modified times, respectively.  This bizarre API is mimicked328        from Python for the sake of consistency -- I apologize.329 330        :param tuple times:331            ``None`` or a tuple of (access time, modified time) in standard332            internet epoch time (seconds since 01 January 1970 GMT)333        """334        if times is None:335            times = (time.time(), time.time())336        self.sftp._log(337            DEBUG, "utime({}, {!r})".format(hexlify(self.handle), times)338        )339        attr = SFTPAttributes()340        attr.st_atime, attr.st_mtime = times341        self.sftp._request(CMD_FSETSTAT, self.handle, attr)342 343    def truncate(self, size):344        """345        Change the size of this file.  This usually extends346        or shrinks the size of the file, just like the ``truncate()`` method on347        Python file objects.348 349        :param size: the new size of the file350        """351        self.sftp._log(352            DEBUG, "truncate({}, {!r})".format(hexlify(self.handle), size)353        )354        attr = SFTPAttributes()355        attr.st_size = size356        self.sftp._request(CMD_FSETSTAT, self.handle, attr)357 358    def check(self, hash_algorithm, offset=0, length=0, block_size=0):359        """360        Ask the server for a hash of a section of this file.  This can be used361        to verify a successful upload or download, or for various rsync-like362        operations.363 364        The file is hashed from ``offset``, for ``length`` bytes.365        If ``length`` is 0, the remainder of the file is hashed.  Thus, if both366        ``offset`` and ``length`` are zero, the entire file is hashed.367 368        Normally, ``block_size`` will be 0 (the default), and this method will369        return a byte string representing the requested hash (for example, a370        string of length 16 for MD5, or 20 for SHA-1).  If a non-zero371        ``block_size`` is given, each chunk of the file (from ``offset`` to372        ``offset + length``) of ``block_size`` bytes is computed as a separate373        hash.  The hash results are all concatenated and returned as a single374        string.375 376        For example, ``check('sha1', 0, 1024, 512)`` will return a string of377        length 40.  The first 20 bytes will be the SHA-1 of the first 512 bytes378        of the file, and the last 20 bytes will be the SHA-1 of the next 512379        bytes.380 381        :param str hash_algorithm:382            the name of the hash algorithm to use (normally ``"sha1"`` or383            ``"md5"``)384        :param offset:385            offset into the file to begin hashing (0 means to start from the386            beginning)387        :param length:388            number of bytes to hash (0 means continue to the end of the file)389        :param int block_size:390            number of bytes to hash per result (must not be less than 256; 0391            means to compute only one hash of the entire segment)392        :return:393            `str` of bytes representing the hash of each block, concatenated394            together395 396        :raises:397            ``IOError`` -- if the server doesn't support the "check-file"398            extension, or possibly doesn't support the hash algorithm requested399 400        .. note:: Many (most?) servers don't support this extension yet.401 402        .. versionadded:: 1.4403        """404        t, msg = self.sftp._request(405            CMD_EXTENDED,406            "check-file",407            self.handle,408            hash_algorithm,409            int64(offset),410            int64(length),411            block_size,412        )413        msg.get_text()  # ext414        msg.get_text()  # alg415        data = msg.get_remainder()416        return data417 418    def set_pipelined(self, pipelined=True):419        """420        Turn on/off the pipelining of write operations to this file.  When421        pipelining is on, paramiko won't wait for the server response after422        each write operation.  Instead, they're collected as they come in. At423        the first non-write operation (including `.close`), all remaining424        server responses are collected.  This means that if there was an error425        with one of your later writes, an exception might be thrown from within426        `.close` instead of `.write`.427 428        By default, files are not pipelined.429 430        :param bool pipelined:431            ``True`` if pipelining should be turned on for this file; ``False``432            otherwise433 434        .. versionadded:: 1.5435        """436        self.pipelined = pipelined437 438    def prefetch(self, file_size=None, max_concurrent_requests=None):439        """440        Pre-fetch the remaining contents of this file in anticipation of future441        `.read` calls.  If reading the entire file, pre-fetching can442        dramatically improve the download speed by avoiding roundtrip latency.443        The file's contents are incrementally buffered in a background thread.444 445        The prefetched data is stored in a buffer until read via the `.read`446        method.  Once data has been read, it's removed from the buffer.  The447        data may be read in a random order (using `.seek`); chunks of the448        buffer that haven't been read will continue to be buffered.449 450        :param int file_size:451            When this is ``None`` (the default), this method calls `stat` to452            determine the remote file size. In some situations, doing so can453            cause exceptions or hangs (see `#562454            <https://github.com/paramiko/paramiko/pull/562>`_); as a455            workaround, one may call `stat` explicitly and pass its value in456            via this parameter.457        :param int max_concurrent_requests:458            The maximum number of concurrent read requests to prefetch. See459            `.SFTPClient.get` (its ``max_concurrent_prefetch_requests`` param)460            for details.461 462        .. versionadded:: 1.5.1463        .. versionchanged:: 1.16.0464            The ``file_size`` parameter was added (with no default value).465        .. versionchanged:: 1.16.1466            The ``file_size`` parameter was made optional for backwards467            compatibility.468        .. versionchanged:: 3.3469            Added ``max_concurrent_requests``.470        """471        if file_size is None:472            file_size = self.stat().st_size473 474        # queue up async reads for the rest of the file475        chunks = []476        n = self._realpos477        while n < file_size:478            chunk = min(self.MAX_REQUEST_SIZE, file_size - n)479            chunks.append((n, chunk))480            n += chunk481        if len(chunks) > 0:482            self._start_prefetch(chunks, max_concurrent_requests)483 484    def readv(self, chunks, max_concurrent_prefetch_requests=None):485        """486        Read a set of blocks from the file by (offset, length).  This is more487        efficient than doing a series of `.seek` and `.read` calls, since the488        prefetch machinery is used to retrieve all the requested blocks at489        once.490 491        :param chunks:492            a list of ``(offset, length)`` tuples indicating which sections of493            the file to read494        :param int max_concurrent_prefetch_requests:495            The maximum number of concurrent read requests to prefetch. See496            `.SFTPClient.get` (its ``max_concurrent_prefetch_requests`` param)497            for details.498        :return: a list of blocks read, in the same order as in ``chunks``499 500        .. versionadded:: 1.5.4501        .. versionchanged:: 3.3502            Added ``max_concurrent_prefetch_requests``.503        """504        self.sftp._log(505            DEBUG, "readv({}, {!r})".format(hexlify(self.handle), chunks)506        )507 508        read_chunks = []509        for offset, size in chunks:510            # don't fetch data that's already in the prefetch buffer511            if self._data_in_prefetch_buffers(512                offset513            ) or self._data_in_prefetch_requests(offset, size):514                continue515 516            # break up anything larger than the max read size517            while size > 0:518                chunk_size = min(size, self.MAX_REQUEST_SIZE)519                read_chunks.append((offset, chunk_size))520                offset += chunk_size521                size -= chunk_size522 523        self._start_prefetch(read_chunks, max_concurrent_prefetch_requests)524        # now we can just devolve to a bunch of read()s :)525        for x in chunks:526            self.seek(x[0])527            yield self.read(x[1])528 529    # ...internals...530 531    def _get_size(self):532        try:533            return self.stat().st_size534        except:535            return 0536 537    def _start_prefetch(self, chunks, max_concurrent_requests=None):538        self._prefetching = True539        self._prefetch_done = False540 541        t = threading.Thread(542            target=self._prefetch_thread,543            args=(chunks, max_concurrent_requests),544        )545        t.daemon = True546        t.start()547 548    def _prefetch_thread(self, chunks, max_concurrent_requests):549        # do these read requests in a temporary thread because there may be550        # a lot of them, so it may block.551        for offset, length in chunks:552            # Limit the number of concurrent requests in a busy-loop553            if max_concurrent_requests is not None:554                while True:555                    with self._prefetch_lock:556                        pf_len = len(self._prefetch_extents)557                        if pf_len < max_concurrent_requests:558                            break559                    time.sleep(io_sleep)560 561            num = self.sftp._async_request(562                self, CMD_READ, self.handle, int64(offset), int(length)563            )564            with self._prefetch_lock:565                self._prefetch_extents[num] = (offset, length)566 567    def _async_response(self, t, msg, num):568        if t == CMD_STATUS:569            # save exception and re-raise it on next file operation570            try:571                self.sftp._convert_status(msg)572            except Exception as e:573                self._saved_exception = e574            return575        if t != CMD_DATA:576            raise SFTPError("Expected data")577        data = msg.get_string()578        while True:579            with self._prefetch_lock:580                # spin if in race with _prefetch_thread581                if num in self._prefetch_extents:582                    offset, length = self._prefetch_extents[num]583                    self._prefetch_data[offset] = data584                    del self._prefetch_extents[num]585                    if len(self._prefetch_extents) == 0:586                        self._prefetch_done = True587                    break588 589    def _check_exception(self):590        """if there's a saved exception, raise & clear it"""591        if self._saved_exception is not None:592            x = self._saved_exception593            self._saved_exception = None594            raise x595 
codekingpro/portable-devtools · Team Ai