summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.rst2
-rw-r--r--paramiko/file.py64
-rw-r--r--paramiko/sftp_file.py64
-rw-r--r--sites/www/changelog.rst9
-rw-r--r--tests/test_client.py39
-rwxr-xr-xtests/test_file.py27
-rwxr-xr-xtests/test_sftp.py1
7 files changed, 153 insertions, 53 deletions
diff --git a/README.rst b/README.rst
index 0dcf30e5..8e8c6186 100644
--- a/README.rst
+++ b/README.rst
@@ -11,7 +11,7 @@ Paramiko
:Paramiko: Python SSH module
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
-:Copyright: Copyright (c) 2013-2015 Jeff Forcier <jeff@bitprophet.org>
+:Copyright: Copyright (c) 2013-2016 Jeff Forcier <jeff@bitprophet.org>
:License: `LGPL <https://www.gnu.org/copyleft/lesser.html>`_
:Homepage: http://www.paramiko.org/
:API docs: http://docs.paramiko.org
diff --git a/paramiko/file.py b/paramiko/file.py
index e3b0a16a..05f2d6e6 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
@@ -97,7 +97,7 @@ class BufferedFile (ClosingContextManager):
:raises StopIteration: when the end of the file is reached.
- :return: a line (`str`) read from the file.
+ :returns: a line (`str`) read from the file.
"""
line = self.readline()
if not line:
@@ -119,6 +119,48 @@ class BufferedFile (ClosingContextManager):
raise StopIteration
return line
+ def readable(self):
+ """
+ Check if the file can be read from.
+
+ :returns:
+ `True` if the file can be read from. If `False`, `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.
+
+ :returns:
+ `True` if the file can be written to. If `False`, `write` will
+ raise an exception.
+ """
+ return (self._flags & self.FLAG_WRITE) == self.FLAG_WRITE
+
+ def seekable(self):
+ """
+ Check if the file supports random access.
+
+ :returns:
+ `True` if the file supports random access. If `False`, `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.
+
+ :returns:
+ 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
@@ -132,7 +174,7 @@ class BufferedFile (ClosingContextManager):
text data.
:param int size: maximum number of bytes to read
- :return:
+ :returns:
data read from the file (as bytes), or an empty string if EOF was
encountered immediately
"""
@@ -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):
"""
@@ -192,7 +234,7 @@ class BufferedFile (ClosingContextManager):
characters (``'\\0'``) if they occurred in the input.
:param int size: maximum length of returned string.
- :return:
+ :returns:
next line of the file, or an empty string if the end of the
file has been reached.
@@ -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:]
@@ -277,7 +319,7 @@ class BufferedFile (ClosingContextManager):
after rounding up to an internal buffer size) are read.
:param int sizehint: desired maximum number of bytes to read.
- :return: `list` of lines read from the file.
+ :returns: `list` of lines read from the file.
"""
lines = []
byte_count = 0
@@ -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:
@@ -317,7 +359,7 @@ class BufferedFile (ClosingContextManager):
useful if the underlying file doesn't support random access, or was
opened in append mode.
- :return: file position (`number <int>` of bytes).
+ :returns: file position (`number <int>` of bytes).
"""
return self._pos
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 32b5752b..9ec970c7 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -23,6 +23,9 @@ Changelog
* :bug:`652` Fix behavior of ``gssapi-with-mic`` auth requests so they fail
gracefully (allowing followup via other auth methods) instead of raising an
exception. Patch courtesy of ``@jamercee``.
+* :feature:`588` Add missing file-like object methods for
+ `~paramiko.file.BufferedFile` and `~paramiko.sftp_file.SFTPFile`. Thanks to
+ Adam Meily for the patch.
* :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>`
@@ -65,7 +68,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.
@@ -270,7 +273,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.
@@ -293,7 +296,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_client.py b/tests/test_client.py
index f71efd5a..d39febac 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -87,6 +87,12 @@ class SSHClientTest (unittest.TestCase):
self.sockl.bind(('localhost', 0))
self.sockl.listen(1)
self.addr, self.port = self.sockl.getsockname()
+ self.connect_kwargs = dict(
+ hostname=self.addr,
+ port=self.port,
+ username='slowdive',
+ look_for_keys=False,
+ )
self.event = threading.Event()
def tearDown(self):
@@ -124,7 +130,7 @@ class SSHClientTest (unittest.TestCase):
self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key)
# Actual connection
- self.tc.connect(self.addr, self.port, username='slowdive', **kwargs)
+ self.tc.connect(**dict(self.connect_kwargs, **kwargs))
# Authentication successful?
self.event.wait(1.0)
@@ -229,7 +235,7 @@ class SSHClientTest (unittest.TestCase):
self.tc = paramiko.SSHClient()
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.assertEqual(0, len(self.tc.get_host_keys()))
- self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
+ self.tc.connect(password='pygmalion', **self.connect_kwargs)
self.event.wait(1.0)
self.assertTrue(self.event.is_set())
@@ -284,7 +290,7 @@ class SSHClientTest (unittest.TestCase):
self.tc = paramiko.SSHClient()
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.assertEqual(0, len(self.tc.get_host_keys()))
- self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
+ self.tc.connect(**dict(self.connect_kwargs, password='pygmalion'))
self.event.wait(1.0)
self.assertTrue(self.event.is_set())
@@ -319,7 +325,7 @@ class SSHClientTest (unittest.TestCase):
self.tc = tc
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.assertEquals(0, len(self.tc.get_host_keys()))
- self.tc.connect(self.addr, self.port, username='slowdive', password='pygmalion')
+ self.tc.connect(**dict(self.connect_kwargs, password='pygmalion'))
self.event.wait(1.0)
self.assertTrue(self.event.is_set())
@@ -341,12 +347,29 @@ class SSHClientTest (unittest.TestCase):
self.tc = paramiko.SSHClient()
self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port), 'ssh-rsa', public_host_key)
# Connect with a half second banner timeout.
+ kwargs = dict(self.connect_kwargs, banner_timeout=0.5)
self.assertRaises(
paramiko.SSHException,
self.tc.connect,
- self.addr,
- self.port,
- username='slowdive',
+ **kwargs
+ )
+
+ def test_8_auth_trickledown(self):
+ """
+ Failed key auth doesn't prevent subsequent pw auth from succeeding
+ """
+ # NOTE: re #387, re #394
+ # If pkey module used within Client._auth isn't correctly handling auth
+ # errors (e.g. if it allows things like ValueError to bubble up as per
+ # midway thru #394) client.connect() will fail (at key load step)
+ # instead of succeeding (at password step)
+ kwargs = dict(
+ # Password-protected key whose passphrase is not 'pygmalion' (it's
+ # 'television' as per tests/test_pkey.py). NOTE: must use
+ # key_filename, loading the actual key here with PKey will except
+ # immediately; we're testing the try/except crap within Client.
+ key_filename=[test_path('test_rsa_password.key')],
+ # Actual password for default 'slowdive' user
password='pygmalion',
- banner_timeout=0.5
)
+ self._test_connection(**kwargs)
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 ff146ade..e4c2c3a3 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)