161
161
MAX_SFTP_WRITE_LEN = 4 * 1024 * 1024 # 4 MiB
162
162
MAX_SFTP_PACKET_LEN = MAX_SFTP_WRITE_LEN + 1024
163
163
164
+ _COPY_DATA_BLOCK_SIZE = 256 * 1024 # 256 KiB
165
+
164
166
_MAX_SFTP_REQUESTS = 128
165
167
_MAX_READDIR_NAMES = 128
166
168
@@ -806,6 +808,24 @@ async def run(self) -> None:
806
808
if self ._progress_handler and self ._total_bytes == 0 :
807
809
self ._progress_handler (self ._srcpath , self ._dstpath , 0 , 0 )
808
810
811
+ if self ._srcfs == self ._dstfs and \
812
+ isinstance (self ._srcfs , SFTPClient ):
813
+ try :
814
+ await self ._srcfs .remote_copy (
815
+ cast (SFTPClientFile , self ._src ),
816
+ cast (SFTPClientFile , self ._dst ))
817
+ except SFTPOpUnsupported :
818
+ pass
819
+ else :
820
+ self ._bytes_copied = self ._total_bytes
821
+
822
+ if self ._progress_handler :
823
+ self ._progress_handler (self ._srcpath , self ._dstpath ,
824
+ self ._bytes_copied ,
825
+ self ._total_bytes )
826
+
827
+ return
828
+
809
829
async for _ , datalen in self .iter ():
810
830
if datalen :
811
831
self ._bytes_copied += datalen
@@ -822,8 +842,6 @@ async def run(self) -> None:
822
842
setattr (exc , 'offset' , self ._bytes_copied )
823
843
824
844
raise exc
825
-
826
-
827
845
finally :
828
846
if self ._src : # pragma: no branch
829
847
await self ._src .close ()
@@ -2472,6 +2490,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop,
2472
2490
self ._supports_fsync = False
2473
2491
self ._supports_lsetstat = False
2474
2492
self ._supports_limits = False
2493
+ self ._supports_copy_data = False
2475
2494
2476
2495
@property
2477
2496
def version (self ) -> int :
@@ -2692,6 +2711,8 @@ async def start(self) -> None:
2692
2711
self ._supports_lsetstat = True
2693
2712
elif name == b'limits@openssh.com' and data == b'1' :
2694
2713
self ._supports_limits = True
2714
+ elif name == b'copy-data' and data == b'1' :
2715
+ self ._supports_copy_data = True
2695
2716
2696
2717
if version == 3 :
2697
2718
# Check if the server has a buggy SYMLINK implementation
@@ -3090,6 +3111,26 @@ async def fsync(self, handle: bytes) -> None:
3090
3111
else :
3091
3112
raise SFTPOpUnsupported ('fsync not supported' )
3092
3113
3114
+ async def copy_data (self , read_from_handle : bytes , read_from_offset : int ,
3115
+ read_from_length : int , write_to_handle : bytes ,
3116
+ write_to_offset : int ) -> None :
3117
+ """Make an SFTP copy data request"""
3118
+
3119
+ if self ._supports_copy_data :
3120
+ self .logger .debug1 ('Sending copy-data from handle %s, '
3121
+ 'offset %d, length %d to handle %s, '
3122
+ 'offset %d' , read_from_handle .hex (),
3123
+ read_from_offset , read_from_length ,
3124
+ write_to_handle .hex (), write_to_offset )
3125
+
3126
+ await self ._make_request (b'copy-data' , String (read_from_handle ),
3127
+ UInt64 (read_from_offset ),
3128
+ UInt64 (read_from_length ),
3129
+ String (write_to_handle ),
3130
+ UInt64 (write_to_offset ))
3131
+ else :
3132
+ raise SFTPOpUnsupported ('copy-data not supported' )
3133
+
3093
3134
def exit (self ) -> None :
3094
3135
"""Handle a request to close the SFTP session"""
3095
3136
@@ -3142,6 +3183,15 @@ async def __aexit__(self, _exc_type: Optional[Type[BaseException]],
3142
3183
await self .close ()
3143
3184
return False
3144
3185
3186
+ @property
3187
+ def handle (self ) -> bytes :
3188
+ """Return handle or raise an error if clsoed"""
3189
+
3190
+ if self ._handle is None :
3191
+ raise ValueError ('I/O operation on closed file' )
3192
+
3193
+ return self ._handle
3194
+
3145
3195
async def _end (self ) -> int :
3146
3196
"""Return the offset of the end of the file"""
3147
3197
@@ -4233,6 +4283,35 @@ async def mcopy(self, srcpaths: _SFTPPaths,
4233
4283
block_size , max_requests , progress_handler ,
4234
4284
error_handler )
4235
4285
4286
+ async def remote_copy (self , src : SFTPClientFile , dst : SFTPClientFile ,
4287
+ src_offset : int = 0 , src_length : int = 0 ,
4288
+ dst_offset : int = 0 ) -> None :
4289
+ """Copy data between remote files
4290
+
4291
+ :param src:
4292
+ The remote file object to read data from
4293
+ :param dst:
4294
+ The remote file object to write data to
4295
+ :param src_offset: (optional)
4296
+ The offset to begin reading data from
4297
+ :param src_length: (optional)
4298
+ The number of bytes to attempt to copy
4299
+ :param dst_offset: (optional)
4300
+ The offset to begin writing data to
4301
+ :type src: :class:`SSHClientFile`
4302
+ :type dst: :class:`SSHClientFile`
4303
+ :type src_offset: `int`
4304
+ :type src_length: `int`
4305
+ :type dst_offset: `int`
4306
+
4307
+ :raises: :exc:`SFTPError` if the server doesn't support this
4308
+ extension or returns an error
4309
+
4310
+ """
4311
+
4312
+ await self ._handler .copy_data (src .handle , src_offset , src_length ,
4313
+ dst .handle , dst_offset )
4314
+
4236
4315
async def glob (self , patterns : _SFTPPaths ,
4237
4316
error_handler : SFTPErrorHandler = None ) -> \
4238
4317
Sequence [BytesOrStr ]:
@@ -5583,7 +5662,8 @@ class SFTPServerHandler(SFTPHandler):
5583
5662
(b'hardlink@openssh.com' , b'1' ),
5584
5663
(b'fsync@openssh.com' , b'1' ),
5585
5664
(b'lsetstat@openssh.com' , b'1' ),
5586
- (b'limits@openssh.com' , b'1' )]
5665
+ (b'limits@openssh.com' , b'1' ),
5666
+ (b'copy-data' , b'1' )]
5587
5667
5588
5668
_attrib_extensions : List [bytes ] = []
5589
5669
@@ -6437,6 +6517,55 @@ async def _process_limits(self, packet: SSHPacket) -> SFTPLimits:
6437
6517
return SFTPLimits (MAX_SFTP_PACKET_LEN , MAX_SFTP_READ_LEN ,
6438
6518
MAX_SFTP_WRITE_LEN , nfiles )
6439
6519
6520
+ async def _process_copy_data (self , packet : SSHPacket ) -> None :
6521
+ """Process an incoming copy data request"""
6522
+
6523
+ read_from_handle = packet .get_string ()
6524
+ read_from_offset = packet .get_uint64 ()
6525
+ read_from_length = packet .get_uint64 ()
6526
+ write_to_handle = packet .get_string ()
6527
+ write_to_offset = packet .get_uint64 ()
6528
+ packet .check_end ()
6529
+
6530
+ self .logger .debug1 ('Received copy-data from handle %s, '
6531
+ 'offset %d, length %d to handle %s, '
6532
+ 'offset %d' , read_from_handle .hex (),
6533
+ read_from_offset , read_from_length ,
6534
+ write_to_handle .hex (), write_to_offset )
6535
+
6536
+ src = self ._file_handles .get (read_from_handle )
6537
+ dst = self ._file_handles .get (write_to_handle )
6538
+
6539
+ if src and dst :
6540
+ read_to_end = read_from_length == 0
6541
+
6542
+ while read_to_end or read_from_length :
6543
+ if read_to_end :
6544
+ size = _COPY_DATA_BLOCK_SIZE
6545
+ else :
6546
+ size = min (read_from_length , _COPY_DATA_BLOCK_SIZE )
6547
+
6548
+ data = self ._server .read (src , read_from_offset , size )
6549
+
6550
+ if inspect .isawaitable (data ):
6551
+ data = await cast (Awaitable [bytes ], data )
6552
+
6553
+ result = self ._server .write (dst , write_to_offset , data )
6554
+
6555
+ if inspect .isawaitable (result ):
6556
+ await result
6557
+
6558
+ if len (data ) < size :
6559
+ break
6560
+
6561
+ read_from_offset += size
6562
+ write_to_offset += size
6563
+
6564
+ if not read_to_end :
6565
+ read_from_length -= size
6566
+ else :
6567
+ raise SFTPInvalidHandle ('Invalid file handle' )
6568
+
6440
6569
_packet_handlers : Dict [Union [int , bytes ], _SFTPPacketHandler ] = {
6441
6570
FXP_OPEN : _process_open ,
6442
6571
FXP_CLOSE : _process_close ,
@@ -6465,7 +6594,8 @@ async def _process_limits(self, packet: SSHPacket) -> SFTPLimits:
6465
6594
b'hardlink@openssh.com' : _process_openssh_link ,
6466
6595
b'fsync@openssh.com' : _process_fsync ,
6467
6596
b'lsetstat@openssh.com' : _process_lsetstat ,
6468
- b'limits@openssh.com' : _process_limits
6597
+ b'limits@openssh.com' : _process_limits ,
6598
+ b'copy-data' : _process_copy_data
6469
6599
}
6470
6600
6471
6601
async def run (self ) -> None :
0 commit comments