Skip to content

Commit 80bccfe

Browse files
committed
Merge PRs #661, #693, #695 and #696 into devel
5 parents 4250532 + d1ed4b7 + 3ea4733 + 152f831 + 2617d39 commit 80bccfe

File tree

5 files changed

+40
-47
lines changed

5 files changed

+40
-47
lines changed
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Uploading large files over SCP no longer fails -- by :user:`Jakuje`.

docs/requirements.txt

+6-6
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,9 @@ requests==2.32.3 \
304304
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
305305
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
306306
# via sphinx
307-
setuptools-scm==8.2.1 \
308-
--hash=sha256:0d234a45ca9e4cb7379886b827558d952d1d8f730c3957e7f217b2ec77fe4402 \
309-
--hash=sha256:51cfdd1deefc9b8c08d1a61e940a59c4dec39eb6c285d33fa2f1b4be26c7874d
307+
setuptools-scm==8.2.0 \
308+
--hash=sha256:136e2b1d393d709d2bcf26f275b8dec06c48b811154167b0fd6bb002aad17d6d \
309+
--hash=sha256:a18396a1bc0219c974d1a74612b11f9dce0d5bd8b1dc55c65f6ac7fd609e8c28
310310
# via -r docs/requirements.in
311311
snowballstemmer==2.2.0 \
312312
--hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
@@ -419,9 +419,9 @@ zipp==3.21.0 \
419419
# importlib-resources
420420

421421
# The following packages are considered to be unsafe in a requirements file:
422-
setuptools==75.8.0 \
423-
--hash=sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6 \
424-
--hash=sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3
422+
setuptools==77.0.3 \
423+
--hash=sha256:583b361c8da8de57403743e756609670de6fb2345920e36dc5c2d914c319c945 \
424+
--hash=sha256:67122e78221da5cf550ddd04cf8742c8fe12094483749a792d56cd669d6cf58c
425425
# via
426426
# incremental
427427
# setuptools-scm

src/pylibsshext/scp.pyx

+15-5
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ from pylibsshext.errors cimport LibsshSCPException
2424
from pylibsshext.session cimport get_libssh_session
2525

2626

27-
SCP_MAX_CHUNK = 65536
27+
SCP_MAX_CHUNK = 65_536 # 64kB
2828

2929

3030
cdef class SCP:
@@ -74,15 +74,25 @@ cdef class SCP:
7474
)
7575

7676
try:
77+
# Read buffer
78+
read_buffer_size = min(file_size, SCP_MAX_CHUNK)
79+
7780
# Begin to send to the file
7881
rc = libssh.ssh_scp_push_file(scp, filename_b, file_size, file_mode)
7982
if rc != libssh.SSH_OK:
8083
raise LibsshSCPException("Can't open remote file: %s" % self._get_ssh_error_str())
8184

82-
# Write to the open file
83-
rc = libssh.ssh_scp_write(scp, PyBytes_AS_STRING(f.read()), file_size)
84-
if rc != libssh.SSH_OK:
85-
raise LibsshSCPException("Can't write to remote file: %s" % self._get_ssh_error_str())
85+
remaining_bytes_to_read = file_size
86+
while remaining_bytes_to_read > 0:
87+
# Read the chunk from local file
88+
read_bytes = min(remaining_bytes_to_read, read_buffer_size)
89+
read_buffer = f.read(read_bytes)
90+
91+
# Write to the open file
92+
rc = libssh.ssh_scp_write(scp, PyBytes_AS_STRING(read_buffer), read_bytes)
93+
if rc != libssh.SSH_OK:
94+
raise LibsshSCPException("Can't write to remote file: %s" % self._get_ssh_error_str())
95+
remaining_bytes_to_read -= read_bytes
8696
finally:
8797
libssh.ssh_scp_close(scp)
8898
libssh.ssh_scp_free(scp)

tests/unit/channel_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def test_send_signal(ssh_channel):
109109
ssh_channel.poll(timeout=POLL_TIMEOUT)
110110
output += ssh_channel.recv().decode('utf-8')
111111

112-
# Send SIGINT
112+
# Send SIGUSR1
113113
ssh_channel.send_signal(signal.SIGUSR1)
114114

115115
rc = -1

tests/unit/scp_test.py

+17-35
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
import os
66
import random
77
import string
8-
import uuid
98

109
import pytest
1110

1211
from pylibsshext.errors import LibsshSCPException
12+
from pylibsshext.scp import SCP_MAX_CHUNK
1313

1414

1515
@pytest.fixture
@@ -22,11 +22,22 @@ def ssh_scp(ssh_client_session):
2222
del scp # noqa: WPS420
2323

2424

25-
@pytest.fixture
26-
def transmit_payload():
27-
"""Generate a binary test payload."""
28-
uuid_name = uuid.uuid4()
29-
return 'Hello, {name!s}'.format(name=uuid_name).encode()
25+
@pytest.fixture(
26+
params=(32, SCP_MAX_CHUNK + 1),
27+
ids=('small-payload', 'large-payload'),
28+
)
29+
def transmit_payload(request: pytest.FixtureRequest):
30+
"""Generate a binary test payload.
31+
32+
The choice 32 is arbitrary small value.
33+
34+
The choice SCP_CHUNK_SIZE + 1 (64kB + 1B) is meant to be 1B larger than the chunk
35+
size used in :file:`scp.pyx` to make sure we excercise at least two rounds of
36+
reading/writing.
37+
"""
38+
payload_len = request.param
39+
random_bytes = [ord(random.choice(string.printable)) for _ in range(payload_len)]
40+
return bytes(random_bytes)
3041

3142

3243
@pytest.fixture
@@ -91,32 +102,3 @@ def test_get_existing_local(pre_existing_file_path, src_path, ssh_scp, transmit_
91102
"""Check that SCP file download works and overwrites local file if it exists."""
92103
ssh_scp.get(str(src_path), str(pre_existing_file_path))
93104
assert pre_existing_file_path.read_bytes() == transmit_payload
94-
95-
96-
@pytest.fixture
97-
def large_payload():
98-
"""Generate a large 65537 byte (64kB+1B) test payload."""
99-
random_char_kilobyte = [ord(random.choice(string.printable)) for _ in range(1024)]
100-
full_bytes_number = 64
101-
a_64kB_chunk = bytes(random_char_kilobyte * full_bytes_number)
102-
the_last_byte = random.choice(random_char_kilobyte).to_bytes(length=1, byteorder='big')
103-
return a_64kB_chunk + the_last_byte
104-
105-
106-
@pytest.fixture
107-
def src_path_large(tmp_path, large_payload):
108-
"""Return a remote path that to a 65537 byte-sized file.
109-
110-
Typical single-read chunk size is 64kB in ``libssh`` so
111-
the test needs a file that would overflow that to trigger
112-
the read loop.
113-
"""
114-
path = tmp_path / 'large.txt'
115-
path.write_bytes(large_payload)
116-
return path
117-
118-
119-
def test_get_large(dst_path, src_path_large, ssh_scp, large_payload):
120-
"""Check that SCP file download gets over 64kB of data."""
121-
ssh_scp.get(str(src_path_large), str(dst_path))
122-
assert dst_path.read_bytes() == large_payload

0 commit comments

Comments
 (0)