From 2d20d1f09bd0069cfea8b2dde716c35676ef39ba Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Fri, 30 Aug 2024 12:01:52 +0200 Subject: [PATCH 1/7] sftp: Fix downloading files Fixes: #341 Signed-off-by: Jakub Jelen --- src/pylibsshext/sftp.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pylibsshext/sftp.pyx b/src/pylibsshext/sftp.pyx index 6331c529d..98077a00b 100644 --- a/src/pylibsshext/sftp.pyx +++ b/src/pylibsshext/sftp.pyx @@ -100,7 +100,7 @@ cdef class SFTP: raise LibsshSFTPException("Reading data from remote file [%s] failed with error [%s]" % (remote_file, self._get_sftp_error_str())) - with open(local_file, 'wb+') as f: + with open(local_file, 'ab') as f: bytes_written = f.write(read_buffer[:file_data]) if bytes_written and file_data != bytes_written: sftp.sftp_close(rf) From 068a2e722964f7538cd76af0be91b167e8492044 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Fri, 30 Aug 2024 11:20:00 +0200 Subject: [PATCH 2/7] tests: Reproducer for #341 Signed-off-by: Jakub Jelen --- tests/unit/sftp_test.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/unit/sftp_test.py b/tests/unit/sftp_test.py index fc70c4512..88add8a56 100644 --- a/tests/unit/sftp_test.py +++ b/tests/unit/sftp_test.py @@ -2,6 +2,8 @@ """Tests suite for sftp.""" +import random +import string import uuid import pytest @@ -63,3 +65,29 @@ def test_get(dst_path, src_path, sftp_session, transmit_payload): """Check that SFTP file download works.""" sftp_session.get(str(src_path), str(dst_path)) assert dst_path.read_bytes() == transmit_payload + + +@pytest.fixture +def large_payload(): + """Generate a large 1025 byte (1024 + 1B) test payload.""" + payload_len = 1024 + 1 + random_bytes = [ord(random.choice(string.printable)) for _ in range(payload_len)] + return bytes(random_bytes) + + +@pytest.fixture +def src_path_large(tmp_path, large_payload): + """Return a remote path to a 1025 byte-sized file. + + The pylibssh chunk size is 1024 so the test needs a file that would + execute at least two loops. + """ + path = tmp_path / 'large.txt' + path.write_bytes(large_payload) + return path + + +def test_get_large(dst_path, src_path_large, sftp_session, large_payload): + """Check that SFTP can download large file.""" + sftp_session.get(str(src_path_large), str(dst_path)) + assert dst_path.read_bytes() == large_payload From dd1c3aff85f173c4f0072821e2e66b8d60ff1c14 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Fri, 30 Aug 2024 11:23:57 +0200 Subject: [PATCH 3/7] tests: Symmetric reproducer for upload Signed-off-by: Jakub Jelen --- tests/unit/sftp_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/sftp_test.py b/tests/unit/sftp_test.py index 88add8a56..1572c0d66 100644 --- a/tests/unit/sftp_test.py +++ b/tests/unit/sftp_test.py @@ -87,6 +87,12 @@ def src_path_large(tmp_path, large_payload): return path +def test_put_large(dst_path, src_path_large, sftp_session, large_payload): + """Check that SFTP can upload large file.""" + sftp_session.put(str(src_path_large), str(dst_path)) + assert dst_path.read_bytes() == large_payload + + def test_get_large(dst_path, src_path_large, sftp_session, large_payload): """Check that SFTP can download large file.""" sftp_session.get(str(src_path_large), str(dst_path)) From 5ae4af853ce6c0cd9cf6814d612076a9890758e9 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Fri, 15 Nov 2024 14:08:48 +0100 Subject: [PATCH 4/7] sftp: Fix overwriting existing files while downloading Thanks @kucharskim for the report and testing! Signed-off-by: Jakub Jelen --- src/pylibsshext/sftp.pyx | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pylibsshext/sftp.pyx b/src/pylibsshext/sftp.pyx index 98077a00b..0b42ff8f4 100644 --- a/src/pylibsshext/sftp.pyx +++ b/src/pylibsshext/sftp.pyx @@ -89,18 +89,19 @@ cdef class SFTP: rf = sftp.sftp_open(self._libssh_sftp_session, remote_file_b, O_RDONLY, sftp.S_IRWXU) if rf is NULL: - raise LibsshSFTPException("Opening remote file [%s] for read failed with error [%s]" % (remote_file, self._get_sftp_error_str())) - - while True: - file_data = sftp.sftp_read(rf, read_buffer, sizeof(char) * 1024) - if file_data == 0: - break - elif file_data < 0: - sftp.sftp_close(rf) - raise LibsshSFTPException("Reading data from remote file [%s] failed with error [%s]" - % (remote_file, self._get_sftp_error_str())) - - with open(local_file, 'ab') as f: + raise LibsshSFTPException("Opening remote file [%s] for read failed with error [%s]" + % (remote_file, self._get_sftp_error_str())) + + with open(local_file, 'wb') as f: + while True: + file_data = sftp.sftp_read(rf, read_buffer, sizeof(char) * 1024) + if file_data == 0: + break + elif file_data < 0: + sftp.sftp_close(rf) + raise LibsshSFTPException("Reading data from remote file [%s] failed with error [%s]" + % (remote_file, self._get_sftp_error_str())) + bytes_written = f.write(read_buffer[:file_data]) if bytes_written and file_data != bytes_written: sftp.sftp_close(rf) From 9d64dcc9ac64e0dcd5622f2ab3e0cbc260ae1830 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Fri, 15 Nov 2024 14:09:43 +0100 Subject: [PATCH 5/7] tests: Reproduer for overriding existing files Signed-off-by: Jakub Jelen --- tests/unit/sftp_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/unit/sftp_test.py b/tests/unit/sftp_test.py index 1572c0d66..93446bd9f 100644 --- a/tests/unit/sftp_test.py +++ b/tests/unit/sftp_test.py @@ -50,6 +50,22 @@ def dst_path(file_paths_pair): return path +@pytest.fixture +def other_payload(): + """Generate a binary test payload.""" + uuid_name = uuid.uuid4() + return 'Original content: {name!s}'.format(name=uuid_name).encode() + + +@pytest.fixture +def dst_exists_path(file_paths_pair, other_payload): + """Return a data destination path.""" + path = file_paths_pair[1] + path.write_bytes(other_payload) + assert path.exists() + return path + + def test_make_sftp(sftp_session): """Smoke-test SFTP instance creation.""" assert sftp_session @@ -67,6 +83,12 @@ def test_get(dst_path, src_path, sftp_session, transmit_payload): assert dst_path.read_bytes() == transmit_payload +def test_get_existing(dst_exists_path, src_path, sftp_session, transmit_payload): + """Check that SFTP file download works when target file exists.""" + sftp_session.get(str(src_path), str(dst_exists_path)) + assert dst_exists_path.read_bytes() == transmit_payload + + @pytest.fixture def large_payload(): """Generate a large 1025 byte (1024 + 1B) test payload.""" From cbbf5d0d3f30ed1393879950cf462b6033347497 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Fri, 15 Nov 2024 14:11:09 +0100 Subject: [PATCH 6/7] tests: Symetric test for upload to override existing file --- tests/unit/sftp_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/sftp_test.py b/tests/unit/sftp_test.py index 93446bd9f..1822522dc 100644 --- a/tests/unit/sftp_test.py +++ b/tests/unit/sftp_test.py @@ -89,6 +89,12 @@ def test_get_existing(dst_exists_path, src_path, sftp_session, transmit_payload) assert dst_exists_path.read_bytes() == transmit_payload +def test_put_existing(dst_exists_path, src_path, sftp_session, transmit_payload): + """Check that SFTP file download works when target file exists.""" + sftp_session.put(str(src_path), str(dst_exists_path)) + assert dst_exists_path.read_bytes() == transmit_payload + + @pytest.fixture def large_payload(): """Generate a large 1025 byte (1024 + 1B) test payload.""" From 0407ea204b39893735db393fdbf6eb6737bd4801 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Fri, 30 Aug 2024 13:21:44 +0200 Subject: [PATCH 7/7] Add history fragment Signed-off-by: Jakub Jelen --- docs/changelog-fragments/638.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changelog-fragments/638.bugfix.rst diff --git a/docs/changelog-fragments/638.bugfix.rst b/docs/changelog-fragments/638.bugfix.rst new file mode 100644 index 000000000..4c4849c70 --- /dev/null +++ b/docs/changelog-fragments/638.bugfix.rst @@ -0,0 +1 @@ +Fixed large sftp reads and proper overriding existing files -- by :user:`Jakuje`.