Skip to content

Commit 033ef54

Browse files
committed
Add support for selecting to only allow remote copy on SFTP
This commit adds a new "remote_only" argument to the SFTPClient copy() and mcopy() functions to request that the operation only be performed if it can be done using the "remote copy" feature. It also adds a "supports_remote_copy" property to SFTPClient for an application to test if the connected SFTP server supports this function.
1 parent e72d642 commit 033ef54

File tree

3 files changed

+91
-36
lines changed

3 files changed

+91
-36
lines changed

asyncssh/sftp.py

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -811,39 +811,34 @@ async def run(self) -> None:
811811
self._progress_handler(self._srcpath, self._dstpath, 0, 0)
812812

813813
if self._srcfs == self._dstfs and \
814-
isinstance(self._srcfs, SFTPClient):
815-
try:
816-
await self._srcfs.remote_copy(
817-
cast(SFTPClientFile, self._src),
818-
cast(SFTPClientFile, self._dst))
819-
except SFTPOpUnsupported:
820-
pass
821-
else:
822-
self._bytes_copied = self._total_bytes
814+
isinstance(self._srcfs, SFTPClient) and \
815+
self._srcfs.supports_remote_copy:
816+
await self._srcfs.remote_copy(cast(SFTPClientFile, self._src),
817+
cast(SFTPClientFile, self._dst))
823818

824-
if self._progress_handler:
825-
self._progress_handler(self._srcpath, self._dstpath,
826-
self._bytes_copied,
827-
self._total_bytes)
819+
self._bytes_copied = self._total_bytes
828820

829-
return
830-
831-
async for _, datalen in self.iter():
832-
if datalen:
833-
self._bytes_copied += datalen
821+
if self._progress_handler:
822+
self._progress_handler(self._srcpath, self._dstpath,
823+
self._bytes_copied,
824+
self._total_bytes)
825+
else:
826+
async for _, datalen in self.iter():
827+
if datalen:
828+
self._bytes_copied += datalen
834829

835-
if self._progress_handler:
836-
self._progress_handler(self._srcpath, self._dstpath,
837-
self._bytes_copied,
838-
self._total_bytes)
830+
if self._progress_handler:
831+
self._progress_handler(self._srcpath, self._dstpath,
832+
self._bytes_copied,
833+
self._total_bytes)
839834

840-
if self._bytes_copied != self._total_bytes:
841-
exc = SFTPFailure('Unexpected EOF during file copy')
835+
if self._bytes_copied != self._total_bytes:
836+
exc = SFTPFailure('Unexpected EOF during file copy')
842837

843-
setattr(exc, 'filename', self._srcpath)
844-
setattr(exc, 'offset', self._bytes_copied)
838+
setattr(exc, 'filename', self._srcpath)
839+
setattr(exc, 'offset', self._bytes_copied)
845840

846-
raise exc
841+
raise exc
847842
finally:
848843
if self._src: # pragma: no branch
849844
await self._src.close()
@@ -2500,6 +2495,12 @@ def version(self) -> int:
25002495

25012496
return self._version
25022497

2498+
@property
2499+
def supports_copy_data(self) -> bool:
2500+
"""Return whether or not SFTP remote copy is supported"""
2501+
2502+
return self._supports_copy_data
2503+
25032504
async def _cleanup(self, exc: Optional[Exception]) -> None:
25042505
"""Clean up this SFTP client session"""
25052506

@@ -3678,6 +3679,12 @@ def limits(self) -> SFTPLimits:
36783679

36793680
return self._handler.limits
36803681

3682+
@property
3683+
def supports_remote_copy(self) -> bool:
3684+
"""Return whether or not SFTP remote copy is supported"""
3685+
3686+
return self._handler.supports_copy_data
3687+
36813688
@staticmethod
36823689
def basename(path: bytes) -> bytes:
36833690
"""Return the final component of a POSIX-style path"""
@@ -4116,7 +4123,8 @@ async def copy(self, srcpaths: _SFTPPaths,
41164123
follow_symlinks: bool = False, block_size: int = -1,
41174124
max_requests: int = _MAX_SFTP_REQUESTS,
41184125
progress_handler: SFTPProgressHandler = None,
4119-
error_handler: SFTPErrorHandler = None) -> None:
4126+
error_handler: SFTPErrorHandler = None,
4127+
remote_only: bool = False) -> None:
41204128
"""Copy remote files to a new location
41214129
41224130
This method copies one or more files or directories on the
@@ -4193,6 +4201,8 @@ async def copy(self, srcpaths: _SFTPPaths,
41934201
The function to call to report copy progress
41944202
:param error_handler: (optional)
41954203
The function to call when an error occurs
4204+
:param remote_only: (optional)
4205+
Whether or not to only allow this to be a remote copy
41964206
:type srcpaths:
41974207
:class:`PurePath <pathlib.PurePath>`, `str`, or `bytes`,
41984208
or a sequence of these
@@ -4205,12 +4215,16 @@ async def copy(self, srcpaths: _SFTPPaths,
42054215
:type max_requests: `int`
42064216
:type progress_handler: `callable`
42074217
:type error_handler: `callable`
4218+
:type remote_only: `bool`
42084219
42094220
:raises: | :exc:`OSError` if a local file I/O error occurs
42104221
| :exc:`SFTPError` if the server returns an error
42114222
42124223
"""
42134224

4225+
if remote_only and not self.supports_remote_copy:
4226+
raise SFTPOpUnsupported('Remote copy not supported')
4227+
42144228
await self._begin_copy(self, self, srcpaths, dstpath, 'remote copy',
42154229
False, preserve, recurse, follow_symlinks,
42164230
block_size, max_requests, progress_handler,
@@ -4268,8 +4282,9 @@ async def mcopy(self, srcpaths: _SFTPPaths,
42684282
follow_symlinks: bool = False, block_size: int = -1,
42694283
max_requests: int = _MAX_SFTP_REQUESTS,
42704284
progress_handler: SFTPProgressHandler = None,
4271-
error_handler: SFTPErrorHandler = None) -> None:
4272-
"""Download remote files with glob pattern match
4285+
error_handler: SFTPErrorHandler = None,
4286+
remote_only: bool = False) -> None:
4287+
"""Copy remote files with glob pattern match
42734288
42744289
This method copies files and directories on the remote
42754290
system matching one or more glob patterns.
@@ -4280,6 +4295,9 @@ async def mcopy(self, srcpaths: _SFTPPaths,
42804295
42814296
"""
42824297

4298+
if remote_only and not self.supports_remote_copy:
4299+
raise SFTPOpUnsupported('Remote copy not supported')
4300+
42834301
await self._begin_copy(self, self, srcpaths, dstpath, 'remote mcopy',
42844302
True, preserve, recurse, follow_symlinks,
42854303
block_size, max_requests, progress_handler,

docs/api.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,13 +1069,14 @@ SFTP Support
10691069

10701070
.. autoclass:: SFTPClient()
10711071

1072-
======================================================================= =
1072+
======================================= =
10731073
SFTP client attributes
1074-
======================================================================= =
1074+
======================================= =
10751075
.. autoattribute:: logger
10761076
.. autoattribute:: version
10771077
.. autoattribute:: limits
1078-
======================================================================= =
1078+
.. autoattribute:: supports_remote_copy
1079+
======================================= =
10791080

10801081
=========================== =
10811082
File transfer methods

tests/test_sftp.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -755,11 +755,11 @@ def test_copy_non_remote(self):
755755
async def _test_copy_non_remote(self, sftp):
756756
"""Test copying without using remote_copy function"""
757757

758-
for src in ('src', b'src', Path('src')):
759-
with self.subTest(src=type(src)):
758+
for method in ('copy', 'mcopy'):
759+
with self.subTest(method=method):
760760
try:
761761
self._create_file('src')
762-
await sftp.copy(src, 'dst')
762+
await sftp.copy('src', 'dst')
763763
self._check_file('src', 'dst')
764764
finally:
765765
remove('src dst')
@@ -768,6 +768,23 @@ async def _test_copy_non_remote(self, sftp):
768768
# pylint: disable=no-value-for-parameter
769769
_test_copy_non_remote(self)
770770

771+
def test_copy_remote_only(self):
772+
"""Test copying while allowing only remote copy"""
773+
774+
@sftp_test
775+
async def _test_copy_remote_only(self, sftp):
776+
"""Test copying with only remote copy allowed"""
777+
778+
for method in ('copy', 'mcopy'):
779+
with self.subTest(method=method):
780+
with self.assertRaises(SFTPOpUnsupported):
781+
await getattr(sftp, method)('src', 'dst',
782+
remote_only=True)
783+
784+
with patch('asyncssh.sftp.SFTPServerHandler._extensions', []):
785+
# pylint: disable=no-value-for-parameter
786+
_test_copy_remote_only(self)
787+
771788
@sftp_test
772789
async def test_copy_progress(self, sftp):
773790
"""Test copying a file over SFTP with progress reporting"""
@@ -1152,6 +1169,25 @@ def err_handler(exc):
11521169
finally:
11531170
remove('src1 src2 dst')
11541171

1172+
def test_remote_copy_unsupported(self):
1173+
"""Test remote copy on a server which doesn't support it"""
1174+
1175+
@sftp_test
1176+
async def _test_remote_copy_unsupported(self, sftp):
1177+
"""Test remote copy not being supported"""
1178+
1179+
try:
1180+
self._create_file('src')
1181+
1182+
with self.assertRaises(SFTPOpUnsupported):
1183+
await sftp.remote_copy('src', 'dst')
1184+
finally:
1185+
remove('src')
1186+
1187+
with patch('asyncssh.sftp.SFTPServerHandler._extensions', []):
1188+
# pylint: disable=no-value-for-parameter
1189+
_test_remote_copy_unsupported(self)
1190+
11551191
@sftp_test
11561192
async def test_remote_copy_arguments(self, sftp):
11571193
"""Test remote copy arguments"""

0 commit comments

Comments
 (0)