diff options
-rw-r--r-- | paramiko/file.py | 54 | ||||
-rw-r--r-- | paramiko/sftp_file.py | 64 | ||||
-rw-r--r-- | sites/www/changelog.rst | 7 | ||||
-rwxr-xr-x | tests/test_file.py | 27 | ||||
-rwxr-xr-x | tests/test_sftp.py | 1 |
5 files changed, 114 insertions, 39 deletions
diff --git a/paramiko/file.py b/paramiko/file.py index e3b0a16a..43d1c008 100644 --- a/paramiko/file.py +++ b/paramiko/file.py @@ -59,7 +59,7 @@ class BufferedFile (ClosingContextManager): def __del__(self): self.close() - + def __iter__(self): """ Returns an iterator that can be used to iterate over the lines in this @@ -119,6 +119,48 @@ class BufferedFile (ClosingContextManager): raise StopIteration return line + def readable(self): + """ + Check if the file can be read from. + + :return: + `True` if the file can be read from. If `False`, :meth`read` will + raise an exception + """ + return (self._flags & self.FLAG_READ) == self.FLAG_READ + + def writable(self): + """ + Check if the file can be written to. + + :return: + `True` if the file can be written to. If `False`, :meth:`write` + will raise an exception + """ + return (self._flags & self.FLAG_WRITE) == self.FLAG_WRITE + + def seekable(self): + """ + Check if the file supports random access. + + :return: + `True` if the file supports random access. If `False`, + :meth:`seek` will raise an exception + """ + return False + + def readinto(self, buff): + """ + Read up to ``len(buff)`` bytes into :class:`bytearray` *buff* and + return the number of bytes read. + + :return: + the number of bytes read + """ + data = self.read(len(buff)) + buff[:len(data)] = data + return len(data) + def read(self, size=None): """ Read at most ``size`` bytes from the file (less if we hit the end of the @@ -155,12 +197,12 @@ class BufferedFile (ClosingContextManager): result += new_data self._realpos += len(new_data) self._pos += len(new_data) - return result + return result if size <= len(self._rbuffer): result = self._rbuffer[:size] self._rbuffer = self._rbuffer[size:] self._pos += len(result) - return result + return result while len(self._rbuffer) < size: read_size = size - len(self._rbuffer) if self._flags & self.FLAG_BUFFERED: @@ -176,7 +218,7 @@ class BufferedFile (ClosingContextManager): result = self._rbuffer[:size] self._rbuffer = self._rbuffer[size:] self._pos += len(result) - return result + return result def readline(self, size=None): """ @@ -254,7 +296,7 @@ class BufferedFile (ClosingContextManager): xpos = pos + 1 if (line[pos] == cr_byte_value) and (xpos < len(line)) and (line[xpos] == linefeed_byte_value): xpos += 1 - # if the string was truncated, _rbuffer needs to have the string after + # if the string was truncated, _rbuffer needs to have the string after # the newline character plus the truncated part of the line we stored # earlier in _rbuffer self._rbuffer = line[xpos:] + self._rbuffer if truncated else line[xpos:] @@ -300,7 +342,7 @@ class BufferedFile (ClosingContextManager): If a file is opened in append mode (``'a'`` or ``'a+'``), any seek operations will be undone at the next write (as the file position will move back to the end of the file). - + :param int offset: position to move to within the file, relative to ``whence``. :param int whence: diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index c5b65488..3b584dbe 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -64,13 +64,13 @@ class SFTPFile (BufferedFile): def __del__(self): self._close(async=True) - + def close(self): """ Close the file. """ self._close(async=False) - + def _close(self, async=False): # We allow double-close without signaling an error, because real # Python file objects do. However, we must protect against actually @@ -112,7 +112,7 @@ class SFTPFile (BufferedFile): return True # well, we have part of the request. see if another chunk has the rest. return self._data_in_prefetch_requests(buf_offset + buf_size, offset + size - buf_offset - buf_size) - + def _data_in_prefetch_buffers(self, offset): """ if a block of data is present in the prefetch buffers, at the given @@ -129,7 +129,7 @@ class SFTPFile (BufferedFile): # it's not here return None return index - + def _read_prefetch(self, size): """ read data out of the prefetch buffer, if possible. if the data isn't @@ -149,7 +149,7 @@ class SFTPFile (BufferedFile): return None prefetch = self._prefetch_data[offset] del self._prefetch_data[offset] - + buf_offset = self._realpos - offset if buf_offset > 0: self._prefetch_data[offset] = prefetch[:buf_offset] @@ -158,7 +158,7 @@ class SFTPFile (BufferedFile): self._prefetch_data[self._realpos + size] = prefetch[size:] prefetch = prefetch[:size] return prefetch - + def _read(self, size): size = min(size, self.MAX_REQUEST_SIZE) if self._prefetching: @@ -217,6 +217,16 @@ class SFTPFile (BufferedFile): """ self.sftp.sock.setblocking(blocking) + def seekable(self): + """ + Check if the file supports random access. + + :return: + `True` if the file supports random access. If `False`, + :meth:`seek` will raise an exception + """ + return True + def seek(self, offset, whence=0): self.flush() if whence == self.SEEK_SET: @@ -253,7 +263,7 @@ class SFTPFile (BufferedFile): attr = SFTPAttributes() attr.st_mode = mode self.sftp._request(CMD_FSETSTAT, self.handle, attr) - + def chown(self, uid, gid): """ Change the owner (``uid``) and group (``gid``) of this file. As with @@ -294,7 +304,7 @@ class SFTPFile (BufferedFile): Change the size of this file. This usually extends or shrinks the size of the file, just like the ``truncate()`` method on Python file objects. - + :param size: the new size of the file :type size: int or long """ @@ -302,17 +312,17 @@ class SFTPFile (BufferedFile): attr = SFTPAttributes() attr.st_size = size self.sftp._request(CMD_FSETSTAT, self.handle, attr) - + def check(self, hash_algorithm, offset=0, length=0, block_size=0): """ Ask the server for a hash of a section of this file. This can be used to verify a successful upload or download, or for various rsync-like operations. - + The file is hashed from ``offset``, for ``length`` bytes. If ``length`` is 0, the remainder of the file is hashed. Thus, if both ``offset`` and ``length`` are zero, the entire file is hashed. - + Normally, ``block_size`` will be 0 (the default), and this method will return a byte string representing the requested hash (for example, a string of length 16 for MD5, or 20 for SHA-1). If a non-zero @@ -320,12 +330,12 @@ class SFTPFile (BufferedFile): ``offset + length``) of ``block_size`` bytes is computed as a separate hash. The hash results are all concatenated and returned as a single string. - + For example, ``check('sha1', 0, 1024, 512)`` will return a string of length 40. The first 20 bytes will be the SHA-1 of the first 512 bytes of the file, and the last 20 bytes will be the SHA-1 of the next 512 bytes. - + :param str hash_algorithm: the name of the hash algorithm to use (normally ``"sha1"`` or ``"md5"``) @@ -343,13 +353,13 @@ class SFTPFile (BufferedFile): :return: `str` of bytes representing the hash of each block, concatenated together - + :raises IOError: if the server doesn't support the "check-file" extension, or possibly doesn't support the hash algorithm requested - + .. note:: Many (most?) servers don't support this extension yet. - + .. versionadded:: 1.4 """ t, msg = self.sftp._request(CMD_EXTENDED, 'check-file', self.handle, @@ -358,7 +368,7 @@ class SFTPFile (BufferedFile): alg = msg.get_text() data = msg.get_remainder() return data - + def set_pipelined(self, pipelined=True): """ Turn on/off the pipelining of write operations to this file. When @@ -368,24 +378,24 @@ class SFTPFile (BufferedFile): server responses are collected. This means that if there was an error with one of your later writes, an exception might be thrown from within `.close` instead of `.write`. - + By default, files are not pipelined. - + :param bool pipelined: ``True`` if pipelining should be turned on for this file; ``False`` otherwise - + .. versionadded:: 1.5 """ self.pipelined = pipelined - + def prefetch(self, file_size): """ Pre-fetch the remaining contents of this file in anticipation of future `.read` calls. If reading the entire file, pre-fetching can dramatically improve the download speed by avoiding roundtrip latency. The file's contents are incrementally buffered in a background thread. - + The prefetched data is stored in a buffer until read via the `.read` method. Once data has been read, it's removed from the buffer. The data may be read in a random order (using `.seek`); chunks of the @@ -402,20 +412,20 @@ class SFTPFile (BufferedFile): n += chunk if len(chunks) > 0: self._start_prefetch(chunks) - + def readv(self, chunks): """ Read a set of blocks from the file by (offset, length). This is more efficient than doing a series of `.seek` and `.read` calls, since the prefetch machinery is used to retrieve all the requested blocks at once. - + :param chunks: a list of (offset, length) tuples indicating which sections of the file to read :type chunks: list(tuple(long, int)) :return: a list of blocks read, in the same order as in ``chunks`` - + .. versionadded:: 1.5.4 """ self.sftp._log(DEBUG, 'readv(%s, %r)' % (hexlify(self.handle), chunks)) @@ -454,7 +464,7 @@ class SFTPFile (BufferedFile): t = threading.Thread(target=self._prefetch_thread, args=(chunks,)) t.setDaemon(True) t.start() - + def _prefetch_thread(self, chunks): # do these read requests in a temporary thread because there may be # a lot of them, so it may block. @@ -480,7 +490,7 @@ class SFTPFile (BufferedFile): del self._prefetch_extents[num] if len(self._prefetch_extents) == 0: self._prefetch_done = True - + def _check_exception(self): """if there's a saved exception, raise & clear it""" if self._saved_exception is not None: diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index b66dd0db..9357c164 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* Add missing file-like object methods for BufferedFile and SFTPFile. * :support:`636` Clean up and enhance the README (and rename it to ``README.rst`` from just ``README``). Thanks to ``@LucasRMehl``. * :release:`1.16.0 <2015-11-04>` @@ -44,7 +45,7 @@ Changelog :issue:`581`, and a bunch of other duplicates besides) Add support for SHA-2 based key exchange (kex) algorithm ``diffie-hellman-group-exchange-sha256`` and (H)MAC algorithms ``hmac-sha2-256`` and ``hmac-sha2-512``. - + This change includes tweaks to debug-level logging regarding algorithm-selection handshakes; the old all-in-one log line is now multiple easier-to-read, printed-at-handshake-time log lines. @@ -248,7 +249,7 @@ Changelog Plugaru. * :bug:`-` Fix logging error in sftp_client for filenames containing the '%' character. Thanks to Antoine Brenner. -* :bug:`308` Fix regression in dsskey.py that caused sporadic signature +* :bug:`308` Fix regression in dsskey.py that caused sporadic signature verification failures. Thanks to Chris Rose. * :support:`299` Use deterministic signatures for ECDSA keys for improved security. Thanks to Alex Gaynor. @@ -271,7 +272,7 @@ Changelog * :feature:`16` **Python 3 support!** Our test suite passes under Python 3, and it (& Fabric's test suite) continues to pass under Python 2. **Python 2.5 is no longer supported with this change!** - + The merged code was built on many contributors' efforts, both code & feedback. In no particular order, we thank Daniel Goertzen, Ivan Kolodyazhny, Tomi Pieviläinen, Jason R. Coombs, Jan N. Schulze, ``@Lazik``, Dorian Pula, diff --git a/tests/test_file.py b/tests/test_file.py index a6ff69e9..7fab6985 100755 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -70,9 +70,9 @@ class BufferedFileTest (unittest.TestCase): def test_2_readline(self): f = LoopbackFile('r+U') - f.write(b'First line.\nSecond line.\r\nThird line.\n' + + f.write(b'First line.\nSecond line.\r\nThird line.\n' + b'Fourth line.\nFinal line non-terminated.') - + self.assertEqual(f.readline(), 'First line.\n') # universal newline mode should convert this linefeed: self.assertEqual(f.readline(), 'Second line.\n') @@ -165,7 +165,28 @@ class BufferedFileTest (unittest.TestCase): f.write(buffer(b'Too small.')) f.close() + def test_9_readable(self): + f = LoopbackFile('r') + self.assertTrue(f.readable()) + self.assertFalse(f.writable()) + self.assertFalse(f.seekable()) + f.close() + + def test_A_writable(self): + f = LoopbackFile('w') + self.assertTrue(f.writable()) + self.assertFalse(f.readable()) + self.assertFalse(f.seekable()) + f.close() + + def test_B_readinto(self): + data = bytearray(5) + f = LoopbackFile('r+') + f._write(b"hello") + f.readinto(data) + self.assertEqual(data, b'hello') + f.close() + if __name__ == '__main__': from unittest import main main() - diff --git a/tests/test_sftp.py b/tests/test_sftp.py index 131b8abf..53b73ee0 100755 --- a/tests/test_sftp.py +++ b/tests/test_sftp.py @@ -429,6 +429,7 @@ class SFTPTest (unittest.TestCase): line_number += 1 pos_list.append(loc) loc = f.tell() + self.assertTrue(f.seekable()) f.seek(pos_list[6], f.SEEK_SET) self.assertEqual(f.readline(), 'Nouzilly, France.\n') f.seek(pos_list[17], f.SEEK_SET) |