Skip to content

Commit 8cb57a0

Browse files
authored
Merge pull request #664 from Jakuje/sftp-chunk
Increase SFTP chunk size to increase the SFTP throughput in both directions
2 parents adfd06b + 5d6f78b commit 8cb57a0

File tree

5 files changed

+82
-24
lines changed

5 files changed

+82
-24
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved performance of SFTP transfers by using larger transfer chunks -- by :user:`Jakuje`.

src/pylibsshext/includes/libssh.pxd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ cdef extern from "libssh/libssh.h" nogil:
2727
pass
2828
ctypedef ssh_session_struct* ssh_session
2929

30+
cdef struct ssh_string_struct:
31+
pass
32+
ctypedef ssh_string_struct* ssh_string
33+
3034
cdef struct ssh_key_struct:
3135
pass
3236
ctypedef ssh_key_struct* ssh_key

src/pylibsshext/includes/sftp.pxd

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
#
1818
from posix.types cimport mode_t
1919

20-
from pylibsshext.includes.libssh cimport ssh_channel, ssh_session
20+
from libc cimport stdint
21+
22+
from pylibsshext.includes.libssh cimport ssh_channel, ssh_session, ssh_string
2123

2224

2325
cdef extern from "libssh/sftp.h" nogil:
@@ -30,6 +32,31 @@ cdef extern from "libssh/sftp.h" nogil:
3032
pass
3133
ctypedef sftp_file_struct * sftp_file
3234

35+
struct sftp_attributes_struct:
36+
char *name
37+
char *longname
38+
stdint.uint32_t flags
39+
stdint.uint8_t type
40+
stdint.uint64_t size
41+
stdint.uint32_t uid
42+
stdint.uint32_t gid
43+
char *owner
44+
char *group
45+
stdint.uint32_t permissions
46+
stdint.uint64_t atime64
47+
stdint.uint32_t atime
48+
stdint.uint32_t atime_nseconds
49+
stdint.uint64_t createtime
50+
stdint.uint32_t createtime_nseconds
51+
stdint.uint64_t mtime64
52+
stdint.uint32_t mtime
53+
stdint.uint32_t mtime_nseconds
54+
ssh_string acl
55+
stdint.uint32_t extended_count
56+
ssh_string extended_type
57+
ssh_string extended_data
58+
ctypedef sftp_attributes_struct * sftp_attributes
59+
3360
cdef int SSH_FX_OK
3461
cdef int SSH_FX_EOF
3562
cdef int SSH_FX_NO_SUCH_FILE
@@ -55,5 +82,8 @@ cdef extern from "libssh/sftp.h" nogil:
5582
ssize_t sftp_read(sftp_file file, const void *buf, size_t count)
5683
int sftp_get_error(sftp_session sftp)
5784

85+
sftp_attributes sftp_stat(sftp_session session, const char *path)
86+
87+
5888
cdef extern from "sys/stat.h" nogil:
5989
cdef int S_IRWXU

src/pylibsshext/sftp.pyx

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
from posix.fcntl cimport O_CREAT, O_RDONLY, O_TRUNC, O_WRONLY
1919

2020
from cpython.bytes cimport PyBytes_AS_STRING
21+
from cpython.mem cimport PyMem_Free, PyMem_Malloc
2122

2223
from pylibsshext.errors cimport LibsshSFTPException
2324
from pylibsshext.session cimport get_libssh_session
2425

2526

27+
SFTP_MAX_CHUNK = 32_768 # 32kB
28+
29+
2630
MSG_MAP = {
2731
sftp.SSH_FX_OK: "No error",
2832
sftp.SSH_FX_EOF: "End-of-file encountered",
@@ -63,7 +67,7 @@ cdef class SFTP:
6367
rf = sftp.sftp_open(self._libssh_sftp_session, remote_file_b, O_WRONLY | O_CREAT | O_TRUNC, sftp.S_IRWXU)
6468
if rf is NULL:
6569
raise LibsshSFTPException("Opening remote file [%s] for write failed with error [%s]" % (remote_file, self._get_sftp_error_str()))
66-
buffer = f.read(1024)
70+
buffer = f.read(SFTP_MAX_CHUNK)
6771

6872
while buffer != b"":
6973
length = len(buffer)
@@ -76,38 +80,54 @@ cdef class SFTP:
7680
self._get_sftp_error_str(),
7781
)
7882
)
79-
buffer = f.read(1024)
83+
buffer = f.read(SFTP_MAX_CHUNK)
8084
sftp.sftp_close(rf)
8185

8286
def get(self, remote_file, local_file):
8387
cdef sftp.sftp_file rf
84-
cdef char read_buffer[1024]
88+
cdef char *read_buffer = NULL
89+
cdef sftp.sftp_attributes attrs
8590

8691
remote_file_b = remote_file
8792
if isinstance(remote_file_b, unicode):
8893
remote_file_b = remote_file.encode("utf-8")
8994

95+
attrs = sftp.sftp_stat(self._libssh_sftp_session, remote_file_b)
96+
if attrs is NULL:
97+
raise LibsshSFTPException("Failed to stat the remote file [%s]. Error: [%s]"
98+
% (remote_file, self._get_sftp_error_str()))
99+
file_size = attrs.size
100+
90101
rf = sftp.sftp_open(self._libssh_sftp_session, remote_file_b, O_RDONLY, sftp.S_IRWXU)
91102
if rf is NULL:
92103
raise LibsshSFTPException("Opening remote file [%s] for read failed with error [%s]" % (remote_file, self._get_sftp_error_str()))
93104

94-
with open(local_file, 'wb') as f:
95-
while True:
96-
file_data = sftp.sftp_read(rf, <void *>read_buffer, sizeof(char) * 1024)
97-
if file_data == 0:
98-
break
99-
elif file_data < 0:
100-
sftp.sftp_close(rf)
101-
raise LibsshSFTPException("Reading data from remote file [%s] failed with error [%s]"
102-
% (remote_file, self._get_sftp_error_str()))
103-
104-
bytes_written = f.write(read_buffer[:file_data])
105-
if bytes_written and file_data != bytes_written:
106-
sftp.sftp_close(rf)
107-
raise LibsshSFTPException("Number of bytes [%s] read from remote file [%s]"
108-
" does not match number of bytes [%s] written to local file [%s]"
109-
" due to error [%s]"
110-
% (file_data, remote_file, bytes_written, local_file, self._get_sftp_error_str()))
105+
try:
106+
with open(local_file, 'wb') as f:
107+
buffer_size = min(SFTP_MAX_CHUNK, file_size)
108+
read_buffer = <char *>PyMem_Malloc(buffer_size)
109+
if read_buffer is NULL:
110+
raise LibsshSFTPException("Memory allocation error")
111+
112+
while True:
113+
file_data = sftp.sftp_read(rf, <void *>read_buffer, sizeof(char) * buffer_size)
114+
if file_data == 0:
115+
break
116+
elif file_data < 0:
117+
sftp.sftp_close(rf)
118+
raise LibsshSFTPException("Reading data from remote file [%s] failed with error [%s]"
119+
% (remote_file, self._get_sftp_error_str()))
120+
121+
bytes_written = f.write(read_buffer[:file_data])
122+
if bytes_written and file_data != bytes_written:
123+
sftp.sftp_close(rf)
124+
raise LibsshSFTPException("Number of bytes [%s] read from remote file [%s]"
125+
" does not match number of bytes [%s] written to local file [%s]"
126+
" due to error [%s]"
127+
% (file_data, remote_file, bytes_written, local_file, self._get_sftp_error_str()))
128+
finally:
129+
if read_buffer is not NULL:
130+
PyMem_Free(read_buffer)
111131
sftp.sftp_close(rf)
112132

113133
def close(self):

tests/unit/sftp_test.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import pytest
1010

11+
from pylibsshext.sftp import SFTP_MAX_CHUNK
12+
1113

1214
@pytest.fixture
1315
def sftp_session(ssh_client_session):
@@ -21,16 +23,17 @@ def sftp_session(ssh_client_session):
2123

2224

2325
@pytest.fixture(
24-
params=(32, 1024 + 1),
26+
params=(32, SFTP_MAX_CHUNK + 1),
2527
ids=('small-payload', 'large-payload'),
2628
)
2729
def transmit_payload(request: pytest.FixtureRequest) -> bytes:
2830
"""Generate binary test payloads of assorted sizes.
2931
3032
The choice 32 is arbitrary small value.
3133
32-
The choice 1024 + 1 is meant to be 1B larger than the chunk size used in
33-
:file:`sftp.pyx` to make sure we excercise at least two rounds of reading/writing.
34+
The choice SFTP_MAX_CHUNK + 1 (32kB + 1B) is meant to be 1B larger than the chunk
35+
size used in :file:`sftp.pyx` to make sure we excercise at least two rounds of
36+
reading/writing.
3437
"""
3538
payload_len = request.param
3639
random_bytes = [ord(random.choice(string.printable)) for _ in range(payload_len)]

0 commit comments

Comments
 (0)