Skip to content

Commit b5742a5

Browse files
committed
Add limits property to SFTPClient and update documentation
This commit exposes the SFTPLimits class and allows SFTP clients to query the limits advertised by a server. It also updates the documentation to reflect that the max read and write lengths will be chosen by default when connecting to servers that advertise these limits.
1 parent 9d63fb7 commit b5742a5

File tree

4 files changed

+55
-40
lines changed

4 files changed

+55
-40
lines changed

asyncssh/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
from .sftp import SFTPFileCorrupt, SFTPOwnerInvalid, SFTPGroupInvalid
110110
from .sftp import SFTPNoMatchingByteRangeLock
111111
from .sftp import SFTPConnectionLost, SFTPOpUnsupported
112-
from .sftp import SFTPAttrs, SFTPVFSAttrs, SFTPName
112+
from .sftp import SFTPAttrs, SFTPVFSAttrs, SFTPName, SFTPLimits
113113
from .sftp import SEEK_SET, SEEK_CUR, SEEK_END
114114

115115
from .stream import SSHSocketSessionFactory, SSHServerSessionFactory

asyncssh/sftp.py

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,8 @@ class _SFTPFSProtocol(Protocol):
234234
"""Protocol for accessing a filesystem via an SFTP server"""
235235

236236
@property
237-
def max_read_len(self) -> int:
238-
"""Maximum read length associated with this SFTP session"""
239-
240-
@property
241-
def max_write_len(self) -> int:
242-
"""Maximum write length associated with this SFTP session"""
237+
def limits(self) -> 'SFTPLimits':
238+
"""SFTP server limits associated with this SFTP session"""
243239

244240
@staticmethod
245241
def basename(path: bytes) -> bytes:
@@ -2047,7 +2043,20 @@ def decode(cls, packet: SSHPacket, sftp_version: int) -> 'SFTPName':
20472043

20482044

20492045
class SFTPLimits(Record):
2050-
"""SFTP server limits"""
2046+
"""SFTP server limits
2047+
2048+
SFTPLimits is a simple record class with the following fields:
2049+
2050+
================= ========================================= ======
2051+
Field Description Type
2052+
================= ========================================= ======
2053+
max_packet_len Max allowed size of an SFTP packet uint64
2054+
max_read_len Max allowed size of an SFTP read request uint64
2055+
max_write_len Max allowed size of an SFTP write request uint64
2056+
max_open_handles Max allowed number of open file handles uint64
2057+
================= ========================================= ======
2058+
2059+
"""
20512060

20522061
max_packet_len: int
20532062
max_read_len: int
@@ -2296,8 +2305,7 @@ def __init__(self, reader: 'SSHReader[bytes]', writer: 'SSHWriter[bytes]'):
22962305
self._writer: Optional['SSHWriter[bytes]'] = writer
22972306
self._logger = reader.logger.get_child('sftp')
22982307

2299-
self.max_read_len = SAFE_SFTP_READ_LEN
2300-
self.max_write_len = SAFE_SFTP_WRITE_LEN
2308+
self.limits = SFTPLimits(0, SAFE_SFTP_READ_LEN, SAFE_SFTP_WRITE_LEN, 0)
23012309

23022310
@property
23032311
def logger(self) -> SSHLogger:
@@ -2708,10 +2716,10 @@ async def request_limits(self) -> None:
27082716
self._log_limits(limits)
27092717

27102718
if limits.max_read_len:
2711-
self.max_read_len = limits.max_read_len
2719+
self.limits.max_read_len = limits.max_read_len
27122720

27132721
if limits.max_write_len:
2714-
self.max_write_len = limits.max_write_len
2722+
self.limits.max_write_len = limits.max_write_len
27152723

27162724
async def open(self, filename: bytes, pflags: int,
27172725
attrs: SFTPAttrs) -> bytes:
@@ -3114,9 +3122,9 @@ def __init__(self, handler: SFTPClientHandler, handle: bytes,
31143122
self._offset = None if appending else 0
31153123

31163124
self.read_len = \
3117-
handler.max_read_len if block_size == -1 else block_size
3125+
handler.limits.max_read_len if block_size == -1 else block_size
31183126
self.write_len = \
3119-
handler.max_write_len if block_size == -1 else block_size
3127+
handler.limits.max_write_len if block_size == -1 else block_size
31203128

31213129
async def __aenter__(self) -> Self:
31223130
"""Allow SFTPClientFile to be used as an async context manager"""
@@ -3189,8 +3197,8 @@ async def read(self, size: int = -1,
31893197
size = (await self._end()) - offset
31903198

31913199
try:
3192-
if self.read_len and size > min(self.read_len,
3193-
self._handler.max_read_len):
3200+
if self.read_len and size > \
3201+
min(self.read_len, self._handler.limits.max_read_len):
31943202
data = await _SFTPFileReader(
31953203
self.read_len, self._max_requests, self._handler,
31963204
self._handle, offset, size).run()
@@ -3610,16 +3618,10 @@ def version(self) -> int:
36103618
return self._handler.version
36113619

36123620
@property
3613-
def max_read_len(self) -> int:
3614-
"""Maximum read length associated with this SFTP session"""
3615-
3616-
return self._handler.max_read_len
3621+
def limits(self) -> SFTPLimits:
3622+
""":class:`SFTPLimits` associated with this SFTP session"""
36173623

3618-
@property
3619-
def max_write_len(self) -> int:
3620-
"""Maximum write length associated with this SFTP session"""
3621-
3622-
return self._handler.max_write_len
3624+
return self._handler.limits
36233625

36243626
@staticmethod
36253627
def basename(path: bytes) -> bytes:
@@ -3786,7 +3788,8 @@ async def _begin_copy(self, srcfs: _SFTPFSProtocol, dstfs: _SFTPFSProtocol,
37863788
"""Begin a new file upload, download, or copy"""
37873789

37883790
if block_size == -1:
3789-
block_size = min(srcfs.max_read_len, dstfs.max_write_len)
3791+
block_size = min(srcfs.limits.max_read_len,
3792+
dstfs.limits.max_write_len)
37903793

37913794
if isinstance(srcpaths, (bytes, str, PurePath)):
37923795
srcpaths = [srcpaths]
@@ -3880,7 +3883,9 @@ async def get(self, remotepaths: _SFTPPaths,
38803883
watch out for links that result in loops.
38813884
38823885
The block_size argument specifies the size of read and write
3883-
requests issued when downloading the files, defaulting to 16 KB.
3886+
requests issued when downloading the files, defaulting to
3887+
the maximum allowed by the server, or 16 KB if the server
3888+
doesn't advertise limits.
38843889
38853890
The max_requests argument specifies the maximum number of
38863891
parallel read or write requests issued, defaulting to 128.
@@ -3984,7 +3989,9 @@ async def put(self, localpaths: _SFTPPaths,
39843989
watch out for links that result in loops.
39853990
39863991
The block_size argument specifies the size of read and write
3987-
requests issued when uploading the files, defaulting to 16 KB.
3992+
requests issued when downloading the files, defaulting to
3993+
the maximum allowed by the server, or 16 KB if the server
3994+
doesn't advertise limits.
39883995
39893996
The max_requests argument specifies the maximum number of
39903997
parallel read or write requests issued, defaulting to 128.
@@ -4088,7 +4095,9 @@ async def copy(self, srcpaths: _SFTPPaths,
40884095
watch out for links that result in loops.
40894096
40904097
The block_size argument specifies the size of read and write
4091-
requests issued when copying the files, defaulting to 16 KB.
4098+
requests issued when downloading the files, defaulting to
4099+
the maximum allowed by the server, or 16 KB if the server
4100+
doesn't advertise limits.
40924101
40934102
The max_requests argument specifies the maximum number of
40944103
parallel read or write requests issued, defaulting to 128.
@@ -4528,14 +4537,14 @@ async def open(self, path: _SFTPPath,
45284537
or write call will become a single request to the SFTP server.
45294538
Otherwise, read or write calls larger than this size will be
45304539
turned into parallel requests to the server of the requested
4531-
size, defaulting to 16 KB.
4540+
size, defaulting to the maximum allowed by the server, or 16 KB
4541+
if the server doesn't advertise limits.
45324542
45334543
.. note:: The OpenSSH SFTP server will close the connection
4534-
if it receives a message larger than 256 KB, and
4535-
limits read requests to returning no more than
4536-
64 KB. So, when connecting to an OpenSSH SFTP
4537-
server, it is recommended that the block_size be
4538-
set below these sizes.
4544+
if it receives a message larger than 256 KB. So,
4545+
when connecting to an OpenSSH SFTP server, it is
4546+
recommended that the block_size be left at its
4547+
default of using the server-advertised limits.
45394548
45404549
The max_requests argument specifies the maximum number of
45414550
parallel read or write requests issued, defaulting to 128.
@@ -6419,8 +6428,10 @@ async def _process_limits(self, packet: SSHPacket) -> SFTPLimits:
64196428

64206429
packet.check_end()
64216430

6431+
nfiles = os.sysconf('SC_OPEN_MAX') - 5 if hasattr(os, 'sysconf') else 0
6432+
64226433
return SFTPLimits(MAX_SFTP_PACKET_LEN, MAX_SFTP_READ_LEN,
6423-
MAX_SFTP_WRITE_LEN, 0)
6434+
MAX_SFTP_WRITE_LEN, nfiles)
64246435

64256436
_packet_handlers: Dict[Union[int, bytes], _SFTPPacketHandler] = {
64266437
FXP_OPEN: _process_open,
@@ -7529,8 +7540,7 @@ async def close(self) -> None:
75297540
class LocalFS:
75307541
"""An async wrapper around local filesystem access"""
75317542

7532-
max_read_len = MAX_SFTP_READ_LEN
7533-
max_write_len = MAX_SFTP_WRITE_LEN
7543+
limits = SFTPLimits(0, MAX_SFTP_READ_LEN, MAX_SFTP_WRITE_LEN, 0)
75347544

75357545
@staticmethod
75367546
def basename(path: bytes) -> bytes:

docs/api.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,7 @@ SFTP Support
10741074
======================================================================= =
10751075
.. autoattribute:: logger
10761076
.. autoattribute:: version
1077+
.. autoattribute:: limits
10771078
======================================================================= =
10781079

10791080
===================== =
@@ -1243,6 +1244,8 @@ SFTP Support
12431244

12441245
.. autoclass:: SFTPName()
12451246

1247+
.. autoclass:: SFTPLimits()
1248+
12461249
.. index:: Public key and certificate support
12471250
.. _PublicKeySupport:
12481251

tests/test_sftp.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3726,8 +3726,10 @@ async def _send_zero_read_write_len(self, packet):
37263726
{b'limits@openssh.com': _send_zero_read_write_len}):
37273727
async with self.connect() as conn:
37283728
async with conn.start_sftp_client() as sftp:
3729-
self.assertEqual(sftp.max_read_len, SAFE_SFTP_READ_LEN)
3730-
self.assertEqual(sftp.max_write_len, SAFE_SFTP_WRITE_LEN)
3729+
self.assertEqual(sftp.limits.max_read_len,
3730+
SAFE_SFTP_READ_LEN)
3731+
self.assertEqual(sftp.limits.max_write_len,
3732+
SAFE_SFTP_WRITE_LEN)
37313733

37323734
def test_write_close(self):
37333735
"""Test session cleanup in the middle of a write request"""

0 commit comments

Comments
 (0)