1# Copyright (c) 2015-2021 by Ron Frederick <ronf@timeheart.net> and others. 2# 3# This program and the accompanying materials are made available under 4# the terms of the Eclipse Public License v2.0 which accompanies this 5# distribution and is available at: 6# 7# http://www.eclipse.org/legal/epl-2.0/ 8# 9# This program may also be made available under the following secondary 10# licenses when the conditions for such availability set forth in the 11# Eclipse Public License v2.0 are satisfied: 12# 13# GNU General Public License, Version 2.0, or any later versions of 14# that license 15# 16# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later 17# 18# Contributors: 19# Ron Frederick - initial implementation, API, and documentation 20# Jonathan Slenders - proposed changes to allow SFTP server callbacks 21# to be coroutines 22 23"""SFTP handlers""" 24 25import asyncio 26import errno 27from fnmatch import fnmatch 28import inspect 29import os 30from os import SEEK_SET, SEEK_CUR, SEEK_END 31from pathlib import PurePath 32import posixpath 33import stat 34import sys 35import time 36from typing import Optional, Sequence, Tuple 37 38from .constants import DEFAULT_LANG 39 40from .constants import FXP_INIT, FXP_VERSION, FXP_OPEN, FXP_CLOSE, FXP_READ 41from .constants import FXP_WRITE, FXP_LSTAT, FXP_FSTAT, FXP_SETSTAT 42from .constants import FXP_FSETSTAT, FXP_OPENDIR, FXP_READDIR, FXP_REMOVE 43from .constants import FXP_MKDIR, FXP_RMDIR, FXP_REALPATH, FXP_STAT, FXP_RENAME 44from .constants import FXP_READLINK, FXP_SYMLINK, FXP_STATUS, FXP_HANDLE 45from .constants import FXP_DATA, FXP_NAME, FXP_ATTRS, FXP_EXTENDED 46from .constants import FXP_EXTENDED_REPLY 47 48from .constants import FXF_READ, FXF_WRITE, FXF_APPEND 49from .constants import FXF_CREAT, FXF_TRUNC, FXF_EXCL 50 51from .constants import FILEXFER_ATTR_SIZE, FILEXFER_ATTR_UIDGID 52from .constants import FILEXFER_ATTR_PERMISSIONS, FILEXFER_ATTR_ACMODTIME 53from .constants import FILEXFER_ATTR_EXTENDED, FILEXFER_ATTR_UNDEFINED 54 55from .constants import FX_OK, FX_EOF, FX_NO_SUCH_FILE, FX_PERMISSION_DENIED 56from .constants import FX_FAILURE, FX_BAD_MESSAGE, FX_NO_CONNECTION 57from .constants import FX_CONNECTION_LOST, FX_OP_UNSUPPORTED 58 59from .misc import BytesOrStr, Error, Record, async_context_manager 60from .misc import get_symbol_names, hide_empty, plural, to_hex 61 62from .packet import Byte, String, UInt32, UInt64, PacketDecodeError 63from .packet import SSHPacket, SSHPacketLogger 64 65SFTP_BLOCK_SIZE = 16384 66 67_SFTP_VERSION = 3 68_MAX_SFTP_REQUESTS = 128 69_MAX_READDIR_NAMES = 128 70 71_open_modes = { 72 'r': FXF_READ, 73 'w': FXF_WRITE | FXF_CREAT | FXF_TRUNC, 74 'a': FXF_WRITE | FXF_CREAT | FXF_APPEND, 75 'x': FXF_WRITE | FXF_CREAT | FXF_EXCL, 76 77 'r+': FXF_READ | FXF_WRITE, 78 'w+': FXF_READ | FXF_WRITE | FXF_CREAT | FXF_TRUNC, 79 'a+': FXF_READ | FXF_WRITE | FXF_CREAT | FXF_APPEND, 80 'x+': FXF_READ | FXF_WRITE | FXF_CREAT | FXF_EXCL 81} 82 83 84def _mode_to_pflags(mode): 85 """Convert open mode to SFTP open flags""" 86 87 if 'b' in mode: 88 mode = mode.replace('b', '') 89 binary = True 90 else: 91 binary = False 92 93 pflags = _open_modes.get(mode) 94 95 if not pflags: 96 raise ValueError('Invalid mode: %r' % mode) 97 98 return pflags, binary 99 100 101def _from_local_path(path): 102 """Convert local path to SFTP path""" 103 104 path = os.fsencode(path) 105 106 if sys.platform == 'win32': # pragma: no cover 107 path = path.replace(b'\\', b'/') 108 109 if path[:1] != b'/' and path[1:2] == b':': 110 path = b'/' + path 111 112 return path 113 114 115def _to_local_path(path): 116 """Convert SFTP path to local path""" 117 118 if isinstance(path, PurePath): # pragma: no branch 119 path = str(path) 120 121 if sys.platform == 'win32': # pragma: no cover 122 path = os.fsdecode(path) 123 124 if path[:1] == '/' and path[2:3] == ':': 125 path = path[1:] 126 127 path = path.replace('/', '\\') 128 129 return path 130 131 132def _setstat(path, attrs): 133 """Utility function to set file attributes""" 134 135 if attrs.size is not None: 136 os.truncate(path, attrs.size) 137 138 if attrs.uid is not None and attrs.gid is not None: 139 try: 140 os.chown(path, attrs.uid, attrs.gid) 141 except AttributeError: # pragma: no cover 142 raise NotImplementedError from None 143 144 if attrs.permissions is not None: 145 os.chmod(path, stat.S_IMODE(attrs.permissions)) 146 147 if attrs.atime is not None and attrs.mtime is not None: 148 os.utime(path, times=(attrs.atime, attrs.mtime)) 149 150 151def _split_path_by_globs(pattern): 152 """Split path grouping parts without glob pattern""" 153 154 basedir, patlist, plain = None, [], [] 155 156 for current in pattern.split(b'/'): 157 if any(c in current for c in b'*?[]'): 158 if plain: 159 if patlist: 160 patlist.append(plain) 161 else: 162 basedir = b'/'.join(plain) or b'/' 163 164 plain = [] 165 166 patlist.append(current) 167 else: 168 plain.append(current) 169 170 if plain: 171 patlist.append(plain) 172 173 return basedir, patlist 174 175 176async def _glob(fs, basedir, patlist, result): 177 """Recursively match a glob pattern""" 178 179 pattern, newpatlist = patlist[0], patlist[1:] 180 181 names = await fs.listdir(basedir or b'.') 182 183 if isinstance(pattern, list): 184 if len(pattern) == 1 and not pattern[0] and not newpatlist: 185 result.append(basedir) 186 return 187 188 for name in names: 189 if name == pattern[0]: 190 newbase = posixpath.join(basedir or b'', *pattern) 191 await fs.stat(newbase) 192 193 if not newpatlist: 194 result.append(newbase) 195 else: 196 await _glob(fs, newbase, newpatlist, result) 197 break 198 else: 199 if pattern == b'**': 200 await _glob(fs, basedir, newpatlist, result) 201 202 for name in names: 203 if name in (b'.', b'..'): 204 continue 205 206 if fnmatch(name, pattern): 207 newbase = posixpath.join(basedir or b'', name) 208 209 if not newpatlist or (len(newpatlist) == 1 and 210 not newpatlist[0]): 211 result.append(newbase) 212 else: 213 attrs = await fs.stat(newbase) 214 215 if stat.S_ISDIR(attrs.permissions): 216 if pattern == b'**': 217 await _glob(fs, newbase, patlist, result) 218 else: 219 await _glob(fs, newbase, newpatlist, result) 220 221 222async def match_glob(fs, pattern, error_handler=None): 223 """Match a glob pattern""" 224 225 names = [] 226 227 try: 228 if any(c in pattern for c in b'*?[]'): 229 basedir, patlist = _split_path_by_globs(pattern) 230 await _glob(fs, basedir, patlist, names) 231 232 if not names: 233 raise SFTPNoSuchFile('No matches found') 234 else: 235 await fs.stat(pattern) 236 names.append(pattern) 237 except (OSError, SFTPError) as exc: 238 # pylint: disable=attribute-defined-outside-init 239 exc.srcpath = pattern 240 241 if error_handler: 242 error_handler(exc) 243 else: 244 raise 245 246 return names 247 248 249class LocalFile: 250 """A coroutine wrapper around local file I/O""" 251 252 def __init__(self, f): 253 self._file = f 254 255 @classmethod 256 def basename(cls, path): 257 """Return the final component of a local file path""" 258 259 return os.path.basename(path) 260 261 @classmethod 262 def encode(cls, path): 263 """Encode path name using filesystem native encoding 264 265 This method has no effect if the path is already bytes. 266 267 """ 268 269 if isinstance(path, PurePath): # pragma: no branch 270 path = str(path) 271 272 return os.fsencode(path) 273 274 @classmethod 275 def decode(cls, path): 276 """Decode path name using filesystem native encoding 277 278 This method has no effect if the path is already a string. 279 280 """ 281 282 return os.fsdecode(path) 283 284 @classmethod 285 def compose_path(cls, path, parent=None): 286 """Compose a path 287 288 If parent is not specified, just encode the path. 289 290 """ 291 292 return posixpath.join(parent, path) if parent else path 293 294 @classmethod 295 async def open(cls, path, *args, block_size=None): 296 """Open a local file""" 297 298 # pylint: disable=unused-argument 299 300 return cls(open(_to_local_path(path), *args)) 301 302 @classmethod 303 async def stat(cls, path): 304 """Get attributes of a local file or directory, following symlinks""" 305 306 return SFTPAttrs.from_local(os.stat(_to_local_path(path))) 307 308 @classmethod 309 async def lstat(cls, path): 310 """Get attributes of a local file, directory, or symlink""" 311 312 return SFTPAttrs.from_local(os.lstat(_to_local_path(path))) 313 314 @classmethod 315 async def setstat(cls, path, attrs): 316 """Set attributes of a local file or directory""" 317 318 _setstat(_to_local_path(path), attrs) 319 320 @classmethod 321 async def exists(cls, path): 322 """Return if the local path exists and isn't a broken symbolic link""" 323 324 return os.path.exists(_to_local_path(path)) 325 326 @classmethod 327 async def isdir(cls, path): 328 """Return if the local path refers to a directory""" 329 330 return os.path.isdir(_to_local_path(path)) 331 332 @classmethod 333 async def listdir(cls, path): 334 """Read the names of the files in a local directory""" 335 336 files = os.listdir(_to_local_path(path)) 337 338 if sys.platform == 'win32': # pragma: no cover 339 files = [os.fsencode(f) for f in files] 340 341 return files 342 343 @classmethod 344 async def mkdir(cls, path): 345 """Create a local directory with the specified attributes""" 346 347 os.mkdir(_to_local_path(path)) 348 349 @classmethod 350 async def readlink(cls, path): 351 """Return the target of a local symbolic link""" 352 353 return _from_local_path(os.readlink(_to_local_path(path))) 354 355 @classmethod 356 async def symlink(cls, oldpath, newpath): 357 """Create a local symbolic link""" 358 359 os.symlink(_to_local_path(oldpath), _to_local_path(newpath)) 360 361 async def read(self, size, offset): 362 """Read data from the local file""" 363 364 self._file.seek(offset) 365 return self._file.read(size) 366 367 async def write(self, data, offset): 368 """Write data to the local file""" 369 370 self._file.seek(offset) 371 return self._file.write(data) 372 373 async def close(self): 374 """Close the local file""" 375 376 self._file.close() 377 378 379class _SFTPParallelIO: 380 """Parallelize I/O requests on files 381 382 This class issues parallel read and wite requests on files. 383 384 """ 385 386 def __init__(self, block_size, max_requests, offset, size): 387 self._block_size = block_size 388 self._max_requests = max_requests 389 self._offset = offset 390 self._bytes_left = size 391 self._pending = set() 392 393 def _start_tasks(self): 394 """Create parallel file I/O tasks""" 395 396 while self._bytes_left and len(self._pending) < self._max_requests: 397 size = min(self._bytes_left, self._block_size) 398 399 task = asyncio.ensure_future(self.run_task(self._offset, size)) 400 self._pending.add(task) 401 402 self._offset += size 403 self._bytes_left -= size 404 405 async def start(self): 406 """Start parallel I/O""" 407 408 async def run_task(self, offset, size): 409 """Perform file I/O on a particular byte range""" 410 411 raise NotImplementedError 412 413 async def finish(self): 414 """Finish parallel I/O""" 415 416 async def cleanup(self): 417 """Clean up parallel I/O""" 418 419 async def run(self): 420 """Perform all file I/O and return result or exception""" 421 422 try: 423 await self.start() 424 425 self._start_tasks() 426 427 while self._pending: 428 done, self._pending = await asyncio.wait( 429 self._pending, return_when=asyncio.FIRST_COMPLETED) 430 431 exceptions = [] 432 433 for task in done: 434 exc = task.exception() 435 436 if exc and not isinstance(exc, SFTPEOFError): 437 exceptions.append(exc) 438 439 if exceptions: 440 for task in self._pending: 441 task.cancel() 442 443 raise exceptions[0] 444 445 self._start_tasks() 446 447 return await self.finish() 448 finally: 449 await self.cleanup() 450 451 452class _SFTPFileReader(_SFTPParallelIO): 453 """Parallelized SFTP file reader""" 454 455 def __init__(self, block_size, max_requests, handler, handle, offset, size): 456 super().__init__(block_size, max_requests, offset, size) 457 458 self._handler = handler 459 self._handle = handle 460 self._start = offset 461 self._data = bytearray() 462 463 async def run_task(self, offset, size): 464 """Read a block of the file""" 465 466 while size: 467 data = await self._handler.read(self._handle, offset, size) 468 469 pos = offset - self._start 470 pad = pos - len(self._data) 471 472 if pad > 0: 473 self._data += pad * b'\0' 474 475 datalen = len(data) 476 self._data[pos:pos+datalen] = data 477 478 offset += datalen 479 size -= datalen 480 481 async def finish(self): 482 """Finish parallel read""" 483 484 return bytes(self._data) 485 486 487class _SFTPFileWriter(_SFTPParallelIO): 488 """Parallelized SFTP file writer""" 489 490 def __init__(self, block_size, max_requests, handler, handle, offset, data): 491 super().__init__(block_size, max_requests, offset, len(data)) 492 493 self._handler = handler 494 self._handle = handle 495 self._start = offset 496 self._data = data 497 498 async def run_task(self, offset, size): 499 """Write a block to the file""" 500 501 pos = offset - self._start 502 await self._handler.write(self._handle, offset, 503 self._data[pos:pos+size]) 504 505 506class _SFTPFileCopier(_SFTPParallelIO): 507 """SFTP file copier 508 509 This class parforms an SFTP file copy, initiating multiple 510 read and write requests to copy chunks of the file in parallel. 511 512 """ 513 514 def __init__(self, block_size, max_requests, offset, total_bytes, 515 srcfs, dstfs, srcpath, dstpath, progress_handler): 516 super().__init__(block_size, max_requests, offset, total_bytes) 517 518 self._srcfs = srcfs 519 self._dstfs = dstfs 520 521 self._srcpath = srcpath 522 self._dstpath = dstpath 523 524 self._src = None 525 self._dst = None 526 527 self._bytes_copied = 0 528 self._total_bytes = total_bytes 529 self._progress_handler = progress_handler 530 531 async def start(self): 532 """Start parallel copy""" 533 534 self._src = await self._srcfs.open(self._srcpath, 'rb', block_size=None) 535 self._dst = await self._dstfs.open(self._dstpath, 'wb', block_size=None) 536 537 if self._progress_handler and self._total_bytes == 0: 538 self._progress_handler(self._srcpath, self._dstpath, 0, 0) 539 540 async def run_task(self, offset, size): 541 """Copy the next block of the file""" 542 543 while size: 544 data = await self._src.read(size, offset) 545 546 if not data: 547 exc = SFTPFailure('Unexpected EOF during file copy') 548 549 # pylint: disable=attribute-defined-outside-init 550 exc.filename = self._srcpath 551 exc.offset = offset 552 553 raise exc 554 555 await self._dst.write(data, offset) 556 557 datalen = len(data) 558 559 if self._progress_handler: 560 self._bytes_copied += datalen 561 self._progress_handler(self._srcpath, self._dstpath, 562 self._bytes_copied, self._total_bytes) 563 564 offset += datalen 565 size -= datalen 566 567 async def cleanup(self): 568 """Clean up parallel copy""" 569 570 try: 571 if self._src: # pragma: no branch 572 await self._src.close() 573 finally: 574 if self._dst: # pragma: no branch 575 await self._dst.close() 576 577 578class SFTPError(Error): 579 """SFTP error 580 581 This exception is raised when an error occurs while processing 582 an SFTP request. Exception codes should be taken from 583 :ref:`SFTP error codes <SFTPErrorCodes>`. 584 585 :param code: 586 Disconnect reason, taken from :ref:`disconnect reason 587 codes <DisconnectReasons>` 588 :param reason: 589 A human-readable reason for the disconnect 590 :param lang: (optional) 591 The language the reason is in 592 :type code: `int` 593 :type reason: `str` 594 :type lang: `str` 595 596 """ 597 598 599class SFTPEOFError(SFTPError): 600 """SFTP EOF error 601 602 This exception is raised when end of file is reached when 603 reading a file or directory. 604 605 :param reason: (optional) 606 Details about the EOF 607 :param lang: (optional) 608 The language the reason is in 609 :type reason: `str` 610 :type lang: `str` 611 612 """ 613 614 def __init__(self, reason='', lang=DEFAULT_LANG): 615 super().__init__(FX_EOF, reason, lang) 616 617 618class SFTPNoSuchFile(SFTPError): 619 """SFTP no such file 620 621 This exception is raised when the requested file is not found. 622 623 :param reason: 624 Details about the missing file 625 :param lang: (optional) 626 The language the reason is in 627 :type reason: `str` 628 :type lang: `str` 629 630 """ 631 632 def __init__(self, reason, lang=DEFAULT_LANG): 633 super().__init__(FX_NO_SUCH_FILE, reason, lang) 634 635 636class SFTPPermissionDenied(SFTPError): 637 """SFTP permission denied 638 639 This exception is raised when the permissions are not available 640 to perform the requested operation. 641 642 :param reason: 643 Details about the invalid permissions 644 :param lang: (optional) 645 The language the reason is in 646 :type reason: `str` 647 :type lang: `str` 648 649 """ 650 651 def __init__(self, reason, lang=DEFAULT_LANG): 652 super().__init__(FX_PERMISSION_DENIED, reason, lang) 653 654 655class SFTPFailure(SFTPError): 656 """SFTP failure 657 658 This exception is raised when an unexpected SFTP failure occurs. 659 660 :param reason: 661 Details about the failure 662 :param lang: (optional) 663 The language the reason is in 664 :type reason: `str` 665 :type lang: `str` 666 667 """ 668 669 def __init__(self, reason, lang=DEFAULT_LANG): 670 super().__init__(FX_FAILURE, reason, lang) 671 672 673class SFTPBadMessage(SFTPError): 674 """SFTP bad message 675 676 This exception is raised when an invalid SFTP message is 677 received. 678 679 :param reason: 680 Details about the invalid message 681 :param lang: (optional) 682 The language the reason is in 683 :type reason: `str` 684 :type lang: `str` 685 686 """ 687 688 def __init__(self, reason, lang=DEFAULT_LANG): 689 super().__init__(FX_BAD_MESSAGE, reason, lang) 690 691 692class SFTPNoConnection(SFTPError): 693 """SFTP no connection 694 695 This exception is raised when an SFTP request is made on a 696 closed SSH connection. 697 698 :param reason: 699 Details about the closed connection 700 :param lang: (optional) 701 The language the reason is in 702 :type reason: `str` 703 :type lang: `str` 704 705 """ 706 707 def __init__(self, reason, lang=DEFAULT_LANG): 708 super().__init__(FX_NO_CONNECTION, reason, lang) 709 710 711class SFTPConnectionLost(SFTPError): 712 """SFTP connection lost 713 714 This exception is raised when the SSH connection is lost or 715 closed while making an SFTP request. 716 717 :param reason: 718 Details about the connection failure 719 :param lang: (optional) 720 The language the reason is in 721 :type reason: `str` 722 :type lang: `str` 723 724 """ 725 726 def __init__(self, reason, lang=DEFAULT_LANG): 727 super().__init__(FX_CONNECTION_LOST, reason, lang) 728 729 730class SFTPOpUnsupported(SFTPError): 731 """SFTP operation unsupported 732 733 This exception is raised when the requested SFTP operation 734 is not supported. 735 736 :param reason: 737 Details about the unsupported operation 738 :param lang: (optional) 739 The language the reason is in 740 :type reason: `str` 741 :type lang: `str` 742 743 """ 744 745 def __init__(self, reason, lang=DEFAULT_LANG): 746 super().__init__(FX_OP_UNSUPPORTED, reason, lang) 747 748 749_sftp_error_map = { 750 FX_EOF: SFTPEOFError, 751 FX_NO_SUCH_FILE: SFTPNoSuchFile, 752 FX_PERMISSION_DENIED: SFTPPermissionDenied, 753 FX_FAILURE: SFTPFailure, 754 FX_BAD_MESSAGE: SFTPBadMessage, 755 FX_NO_CONNECTION: SFTPNoConnection, 756 FX_CONNECTION_LOST: SFTPConnectionLost, 757 FX_OP_UNSUPPORTED: SFTPOpUnsupported 758} 759 760 761def _construct_sftp_error(code, reason, lang): 762 """Map SFTP error code to appropriate SFTPError exception""" 763 764 try: 765 return _sftp_error_map[code](reason, lang) 766 except KeyError: 767 return SFTPError(code, '%s (error %d)' % (reason, code), lang) 768 769 770class SFTPAttrs(Record): 771 """SFTP file attributes 772 773 SFTPAttrs is a simple record class with the following fields: 774 775 ============ =========================================== ====== 776 Field Description Type 777 ============ =========================================== ====== 778 size File size in bytes uint64 779 uid User id of file owner uint32 780 gid Group id of file owner uint32 781 permissions Bit mask of POSIX file permissions, uint32 782 atime Last access time, UNIX epoch seconds uint32 783 mtime Last modification time, UNIX epoch seconds uint32 784 ============ =========================================== ====== 785 786 In addition to the above, an `nlink` field is provided which 787 stores the number of links to this file, but it is not encoded 788 in the SFTP protocol. It's included here only so that it can be 789 used to create the default `longname` string in :class:`SFTPName` 790 objects. 791 792 Extended attributes can also be added via a field named 793 `extended` which is a list of string name/value pairs. 794 795 When setting attributes using an :class:`SFTPAttrs`, only fields 796 which have been initialized will be changed on the selected file. 797 798 """ 799 800 size: Optional[int] 801 uid: Optional[int] 802 gid: Optional[int] 803 permissions: Optional[int] 804 atime: Optional[int] 805 mtime: Optional[int] 806 nlink: Optional[int] 807 extended: Sequence[Tuple[bytes, bytes]] = () 808 809 def _format(self, k, v): 810 """Convert attributes to more readable values""" 811 812 if v is None or k == 'extended' and not v: 813 return None 814 815 if k == 'permissions': 816 return '{:06o}'.format(v) 817 elif k in ('atime', 'mtime'): 818 return time.ctime(v) 819 else: 820 return str(v) 821 822 def encode(self): 823 """Encode SFTP attributes as bytes in an SSH packet""" 824 825 flags = 0 826 attrs = [] 827 828 if self.size is not None: 829 flags |= FILEXFER_ATTR_SIZE 830 attrs.append(UInt64(self.size)) 831 832 if self.uid is not None and self.gid is not None: 833 flags |= FILEXFER_ATTR_UIDGID 834 attrs.append(UInt32(self.uid) + UInt32(self.gid)) 835 836 if self.permissions is not None: 837 flags |= FILEXFER_ATTR_PERMISSIONS 838 attrs.append(UInt32(self.permissions)) 839 840 if self.atime is not None and self.mtime is not None: 841 flags |= FILEXFER_ATTR_ACMODTIME 842 attrs.append(UInt32(int(self.atime)) + UInt32(int(self.mtime))) 843 844 if self.extended: 845 flags |= FILEXFER_ATTR_EXTENDED 846 attrs.append(UInt32(len(self.extended))) 847 attrs.extend(String(type) + String(data) 848 for type, data in self.extended) 849 850 return UInt32(flags) + b''.join(attrs) 851 852 @classmethod 853 def decode(cls, packet): 854 """Decode bytes in an SSH packet as SFTP attributes""" 855 856 flags = packet.get_uint32() 857 attrs = cls() 858 859 if flags & FILEXFER_ATTR_UNDEFINED: 860 raise SFTPBadMessage('Unsupported attribute flags') 861 862 if flags & FILEXFER_ATTR_SIZE: 863 attrs.size = packet.get_uint64() 864 865 if flags & FILEXFER_ATTR_UIDGID: 866 attrs.uid = packet.get_uint32() 867 attrs.gid = packet.get_uint32() 868 869 if flags & FILEXFER_ATTR_PERMISSIONS: 870 attrs.permissions = packet.get_uint32() & 0xffff 871 872 if flags & FILEXFER_ATTR_ACMODTIME: 873 attrs.atime = packet.get_uint32() 874 attrs.mtime = packet.get_uint32() 875 876 if flags & FILEXFER_ATTR_EXTENDED: 877 count = packet.get_uint32() 878 attrs.extended = [] 879 880 for _ in range(count): 881 attr = packet.get_string() 882 data = packet.get_string() 883 attrs.extended.append((attr, data)) 884 885 return attrs 886 887 @classmethod 888 def from_local(cls, result): 889 """Convert from local stat attributes""" 890 891 return cls(result.st_size, result.st_uid, result.st_gid, 892 result.st_mode, result.st_atime, result.st_mtime, 893 result.st_nlink) 894 895 896class SFTPVFSAttrs(Record): 897 """SFTP file system attributes 898 899 SFTPVFSAttrs is a simple record class with the following fields: 900 901 ============ =========================================== ====== 902 Field Description Type 903 ============ =========================================== ====== 904 bsize File system block size (I/O size) uint64 905 frsize Fundamental block size (allocation size) uint64 906 blocks Total data blocks (in frsize units) uint64 907 bfree Free data blocks uint64 908 bavail Available data blocks (for non-root) uint64 909 files Total file inodes uint64 910 ffree Free file inodes uint64 911 favail Available file inodes (for non-root) uint64 912 fsid File system id uint64 913 flags File system flags (read-only, no-setuid) uint64 914 namemax Maximum filename length uint64 915 ============ =========================================== ====== 916 917 """ 918 919 bsize: int = 0 920 frsize: int = 0 921 blocks: int = 0 922 bfree: int = 0 923 bavail: int = 0 924 files: int = 0 925 ffree: int = 0 926 favail: int = 0 927 fsid: int = 0 928 flags: int = 0 929 namemax: int = 0 930 931 def encode(self): 932 """Encode SFTP statvfs attributes as bytes in an SSH packet""" 933 934 return b''.join((UInt64(self.bsize), UInt64(self.frsize), 935 UInt64(self.blocks), UInt64(self.bfree), 936 UInt64(self.bavail), UInt64(self.files), 937 UInt64(self.ffree), UInt64(self.favail), 938 UInt64(self.fsid), UInt64(self.flags), 939 UInt64(self.namemax))) 940 941 @classmethod 942 def decode(cls, packet): 943 """Decode bytes in an SSH packet as SFTP statvfs attributes""" 944 945 vfsattrs = cls() 946 947 vfsattrs.bsize = packet.get_uint64() 948 vfsattrs.frsize = packet.get_uint64() 949 vfsattrs.blocks = packet.get_uint64() 950 vfsattrs.bfree = packet.get_uint64() 951 vfsattrs.bavail = packet.get_uint64() 952 vfsattrs.files = packet.get_uint64() 953 vfsattrs.ffree = packet.get_uint64() 954 vfsattrs.favail = packet.get_uint64() 955 vfsattrs.fsid = packet.get_uint64() 956 vfsattrs.flags = packet.get_uint64() 957 vfsattrs.namemax = packet.get_uint64() 958 959 return vfsattrs 960 961 @classmethod 962 def from_local(cls, result): 963 """Convert from local statvfs attributes""" 964 965 return cls(result.f_bsize, result.f_frsize, result.f_blocks, 966 result.f_bfree, result.f_bavail, result.f_files, 967 result.f_ffree, result.f_favail, 0, result.f_flag, 968 result.f_namemax) 969 970 971class SFTPName(Record): 972 """SFTP file name and attributes 973 974 SFTPName is a simple record class with the following fields: 975 976 ========= ================================== ================== 977 Field Description Type 978 ========= ================================== ================== 979 filename Filename `str` or `bytes` 980 longname Expanded form of filename & attrs `str` or `bytes` 981 attrs File attributes :class:`SFTPAttrs` 982 ========= ================================== ================== 983 984 A list of these is returned by :meth:`readdir() <SFTPClient.readdir>` 985 in :class:`SFTPClient` when retrieving the contents of a directory. 986 987 """ 988 989 filename: BytesOrStr = '' 990 longname: BytesOrStr = '' 991 attrs: SFTPAttrs = SFTPAttrs() 992 993 def _format(self, k, v): 994 """Convert name fields to more readable values""" 995 996 if isinstance(v, bytes): 997 v = v.decode('utf-8', errors='replace') 998 999 return str(v) or None 1000 1001 def encode(self): 1002 """Encode an SFTP name as bytes in an SSH packet""" 1003 1004 1005 # pylint: disable=no-member 1006 return (String(self.filename) + String(self.longname) + 1007 self.attrs.encode()) 1008 1009 @classmethod 1010 def decode(cls, packet): 1011 """Decode bytes in an SSH packet as an SFTP name""" 1012 1013 1014 filename = packet.get_string() 1015 longname = packet.get_string() 1016 attrs = SFTPAttrs.decode(packet) 1017 1018 return cls(filename, longname, attrs) 1019 1020 1021class SFTPHandler(SSHPacketLogger): 1022 """SFTP session handler""" 1023 1024 _data_pkttypes = {FXP_WRITE, FXP_DATA} 1025 1026 _handler_names = get_symbol_names(globals(), 'FXP_') 1027 1028 # SFTP implementations with broken order for SYMLINK arguments 1029 _nonstandard_symlink_impls = ['OpenSSH', 'paramiko'] 1030 1031 # Return types by message -- unlisted entries always return FXP_STATUS, 1032 # those below return FXP_STATUS on error 1033 _return_types = { 1034 FXP_OPEN: FXP_HANDLE, 1035 FXP_READ: FXP_DATA, 1036 FXP_LSTAT: FXP_ATTRS, 1037 FXP_FSTAT: FXP_ATTRS, 1038 FXP_OPENDIR: FXP_HANDLE, 1039 FXP_READDIR: FXP_NAME, 1040 FXP_REALPATH: FXP_NAME, 1041 FXP_STAT: FXP_ATTRS, 1042 FXP_READLINK: FXP_NAME, 1043 b'statvfs@openssh.com': FXP_EXTENDED_REPLY, 1044 b'fstatvfs@openssh.com': FXP_EXTENDED_REPLY 1045 } 1046 1047 def __init__(self, reader, writer): 1048 self._reader = reader 1049 self._writer = writer 1050 1051 self._logger = reader.logger.get_child('sftp') 1052 1053 @property 1054 def logger(self): 1055 """A logger associated with this SFTP handler""" 1056 1057 return self._logger 1058 1059 async def _cleanup(self, exc): 1060 """Clean up this SFTP session""" 1061 1062 # pylint: disable=unused-argument 1063 1064 if self._writer: # pragma: no branch 1065 self._writer.close() 1066 self._reader = None 1067 self._writer = None 1068 1069 async def _process_packet(self, pkttype, pktid, packet): 1070 """Abstract method for processing SFTP packets""" 1071 1072 raise NotImplementedError 1073 1074 def send_packet(self, pkttype, pktid, *args): 1075 """Send an SFTP packet""" 1076 1077 payload = Byte(pkttype) + b''.join(args) 1078 1079 try: 1080 self._writer.write(UInt32(len(payload)) + payload) 1081 except ConnectionError as exc: 1082 raise SFTPConnectionLost(str(exc)) from None 1083 1084 self.log_sent_packet(pkttype, pktid, payload) 1085 1086 async def recv_packet(self): 1087 """Receive an SFTP packet""" 1088 1089 pktlen = await self._reader.readexactly(4) 1090 pktlen = int.from_bytes(pktlen, 'big') 1091 1092 packet = await self._reader.readexactly(pktlen) 1093 return SSHPacket(packet) 1094 1095 async def recv_packets(self): 1096 """Receive and process SFTP packets""" 1097 1098 try: 1099 while self._reader: # pragma: no branch 1100 packet = await self.recv_packet() 1101 1102 pkttype = packet.get_byte() 1103 pktid = packet.get_uint32() 1104 1105 self.log_received_packet(pkttype, pktid, packet) 1106 1107 await self._process_packet(pkttype, pktid, packet) 1108 except PacketDecodeError as exc: 1109 await self._cleanup(SFTPBadMessage(str(exc))) 1110 except EOFError: 1111 await self._cleanup(None) 1112 except (OSError, Error) as exc: 1113 await self._cleanup(exc) 1114 1115 1116class SFTPClientHandler(SFTPHandler): 1117 """An SFTP client session handler""" 1118 1119 _extensions = [] 1120 1121 def __init__(self, loop, reader, writer): 1122 super().__init__(reader, writer) 1123 1124 self._loop = loop 1125 self._version = None 1126 self._next_pktid = 0 1127 self._requests = {} 1128 self._nonstandard_symlink = False 1129 self._supports_posix_rename = False 1130 self._supports_statvfs = False 1131 self._supports_fstatvfs = False 1132 self._supports_hardlink = False 1133 self._supports_fsync = False 1134 1135 async def _cleanup(self, exc): 1136 """Clean up this SFTP client session""" 1137 1138 req_exc = exc or SFTPConnectionLost('Connection closed') 1139 1140 for waiter in list(self._requests.values()): 1141 if not waiter.cancelled(): # pragma: no branch 1142 waiter.set_exception(req_exc) 1143 1144 self._requests = {} 1145 1146 self.logger.info('SFTP client exited%s', ': ' + str(exc) if exc else '') 1147 1148 await super()._cleanup(exc) 1149 1150 async def _process_packet(self, pkttype, pktid, packet): 1151 """Process incoming SFTP responses""" 1152 1153 try: 1154 waiter = self._requests.pop(pktid) 1155 except KeyError: 1156 await self._cleanup(SFTPBadMessage('Invalid response id')) 1157 else: 1158 if not waiter.cancelled(): # pragma: no branch 1159 waiter.set_result((pkttype, packet)) 1160 1161 def _send_request(self, pkttype, args, waiter): 1162 """Send an SFTP request""" 1163 1164 if not self._writer: 1165 raise SFTPNoConnection('Connection not open') 1166 1167 pktid = self._next_pktid 1168 self._next_pktid = (self._next_pktid + 1) & 0xffffffff 1169 1170 self._requests[pktid] = waiter 1171 1172 if isinstance(pkttype, bytes): 1173 hdr = UInt32(pktid) + String(pkttype) 1174 pkttype = FXP_EXTENDED 1175 else: 1176 hdr = UInt32(pktid) 1177 1178 self.send_packet(pkttype, pktid, hdr, *args) 1179 1180 async def _make_request(self, pkttype, *args): 1181 """Make an SFTP request and wait for a response""" 1182 1183 waiter = self._loop.create_future() 1184 self._send_request(pkttype, args, waiter) 1185 resptype, resp = await waiter 1186 1187 return_type = self._return_types.get(pkttype) 1188 1189 if resptype not in (FXP_STATUS, return_type): 1190 raise SFTPBadMessage('Unexpected response type: %s' % resptype) 1191 1192 result = self._packet_handlers[resptype](self, resp) 1193 1194 if result is not None or return_type is None: 1195 return result 1196 else: 1197 raise SFTPBadMessage('Unexpected FX_OK response') 1198 1199 def _process_status(self, packet): 1200 """Process an incoming SFTP status response""" 1201 1202 code = packet.get_uint32() 1203 1204 if packet: 1205 try: 1206 reason = packet.get_string().decode('utf-8') 1207 lang = packet.get_string().decode('ascii') 1208 except UnicodeDecodeError: 1209 raise SFTPBadMessage('Invalid status message') from None 1210 else: 1211 # Some servers may not always send reason and lang (usually 1212 # when responding with FX_OK). Tolerate this, automatically 1213 # filling in empty strings for them if they're not present. 1214 1215 reason = '' 1216 lang = '' 1217 1218 packet.check_end() 1219 1220 if code == FX_OK: 1221 self.logger.debug1('Received OK') 1222 return None 1223 else: 1224 raise _construct_sftp_error(code, reason, lang) 1225 1226 def _process_handle(self, packet): 1227 """Process an incoming SFTP handle response""" 1228 1229 handle = packet.get_string() 1230 packet.check_end() 1231 1232 self.logger.debug1('Received handle %s', to_hex(handle)) 1233 1234 return handle 1235 1236 def _process_data(self, packet): 1237 """Process an incoming SFTP data response""" 1238 1239 data = packet.get_string() 1240 packet.check_end() 1241 1242 self.logger.debug1('Received %s', plural(len(data), 'data byte')) 1243 1244 return data 1245 1246 def _process_name(self, packet): 1247 """Process an incoming SFTP name response""" 1248 1249 count = packet.get_uint32() 1250 names = [SFTPName.decode(packet) for i in range(count)] 1251 packet.check_end() 1252 1253 self.logger.debug1('Received %s', plural(len(names), 'name')) 1254 1255 for name in names: 1256 self.logger.debug1(' %s', name) 1257 1258 return names 1259 1260 def _process_attrs(self, packet): 1261 """Process an incoming SFTP attributes response""" 1262 1263 attrs = SFTPAttrs().decode(packet) 1264 packet.check_end() 1265 1266 self.logger.debug1('Received %s', attrs) 1267 1268 return attrs 1269 1270 def _process_extended_reply(self, packet): 1271 """Process an incoming SFTP extended reply response""" 1272 1273 # pylint: disable=no-self-use 1274 1275 # Let the caller do the decoding for extended replies 1276 return packet 1277 1278 _packet_handlers = { 1279 FXP_STATUS: _process_status, 1280 FXP_HANDLE: _process_handle, 1281 FXP_DATA: _process_data, 1282 FXP_NAME: _process_name, 1283 FXP_ATTRS: _process_attrs, 1284 FXP_EXTENDED_REPLY: _process_extended_reply 1285 } 1286 1287 async def start(self): 1288 """Start an SFTP client""" 1289 1290 self.logger.debug1('Sending init, version=%d%s', _SFTP_VERSION, 1291 ', extensions:' if self._extensions else '') 1292 1293 for name, data in self._extensions: # pragma: no cover 1294 self.logger.debug1(' %s: %s', name, data) 1295 1296 extensions = (String(name) + String(data) 1297 for name, data in self._extensions) 1298 1299 self.send_packet(FXP_INIT, None, UInt32(_SFTP_VERSION), *extensions) 1300 1301 try: 1302 resp = await self.recv_packet() 1303 1304 resptype = resp.get_byte() 1305 1306 self.log_received_packet(resptype, None, resp) 1307 1308 if resptype != FXP_VERSION: 1309 raise SFTPBadMessage('Expected version message') 1310 1311 version = resp.get_uint32() 1312 1313 if version != _SFTP_VERSION: 1314 raise SFTPBadMessage('Unsupported version: %d' % version) 1315 1316 self._version = version 1317 1318 extensions = [] 1319 1320 while resp: 1321 name = resp.get_string() 1322 data = resp.get_string() 1323 extensions.append((name, data)) 1324 except PacketDecodeError as exc: 1325 raise SFTPBadMessage(str(exc)) from None 1326 except (asyncio.IncompleteReadError, Error) as exc: 1327 raise SFTPFailure(str(exc)) from None 1328 1329 self.logger.debug1('Received version=%d%s', version, 1330 ', extensions:' if extensions else '') 1331 1332 for name, data in extensions: 1333 self.logger.debug1(' %s: %s', name, data) 1334 1335 if name == b'posix-rename@openssh.com' and data == b'1': 1336 self._supports_posix_rename = True 1337 elif name == b'statvfs@openssh.com' and data == b'2': 1338 self._supports_statvfs = True 1339 elif name == b'fstatvfs@openssh.com' and data == b'2': 1340 self._supports_fstatvfs = True 1341 elif name == b'hardlink@openssh.com' and data == b'1': 1342 self._supports_hardlink = True 1343 elif name == b'fsync@openssh.com' and data == b'1': 1344 self._supports_fsync = True 1345 1346 if version == 3: 1347 # Check if the server has a buggy SYMLINK implementation 1348 1349 server_version = self._reader.get_extra_info('server_version', '') 1350 if any(name in server_version 1351 for name in self._nonstandard_symlink_impls): 1352 self.logger.debug1('Adjusting for non-standard symlink ' 1353 'implementation') 1354 self._nonstandard_symlink = True 1355 1356 async def open(self, filename, pflags, attrs): 1357 """Make an SFTP open request""" 1358 1359 self.logger.debug1('Sending open for %s, mode 0x%02x%s', 1360 filename, pflags, hide_empty(attrs)) 1361 1362 return await self._make_request(FXP_OPEN, String(filename), 1363 UInt32(pflags), attrs.encode()) 1364 1365 async def close(self, handle): 1366 """Make an SFTP close request""" 1367 1368 self.logger.debug1('Sending close for handle %s', to_hex(handle)) 1369 1370 if self._writer: 1371 await self._make_request(FXP_CLOSE, String(handle)) 1372 1373 async def read(self, handle, offset, length): 1374 """Make an SFTP read request""" 1375 1376 self.logger.debug1('Sending read for %s at offset %d in handle %s', 1377 plural(length, 'byte'), offset, to_hex(handle)) 1378 1379 return await self._make_request(FXP_READ, String(handle), 1380 UInt64(offset), UInt32(length)) 1381 1382 async def write(self, handle, offset, data): 1383 """Make an SFTP write request""" 1384 1385 self.logger.debug1('Sending write for %s at offset %d in handle %s', 1386 plural(len(data), 'byte'), offset, to_hex(handle)) 1387 1388 return await self._make_request(FXP_WRITE, String(handle), 1389 UInt64(offset), String(data)) 1390 1391 async def stat(self, path): 1392 """Make an SFTP stat request""" 1393 1394 self.logger.debug1('Sending stat for %s', path) 1395 1396 return await self._make_request(FXP_STAT, String(path)) 1397 1398 async def lstat(self, path): 1399 """Make an SFTP lstat request""" 1400 1401 self.logger.debug1('Sending lstat for %s', path) 1402 1403 return await self._make_request(FXP_LSTAT, String(path)) 1404 1405 async def fstat(self, handle): 1406 """Make an SFTP fstat request""" 1407 1408 self.logger.debug1('Sending fstat for handle %s', to_hex(handle)) 1409 1410 return await self._make_request(FXP_FSTAT, String(handle)) 1411 1412 async def setstat(self, path, attrs): 1413 """Make an SFTP setstat request""" 1414 1415 self.logger.debug1('Sending setstat for %s%s', path, hide_empty(attrs)) 1416 1417 return await self._make_request(FXP_SETSTAT, String(path), 1418 attrs.encode()) 1419 1420 async def fsetstat(self, handle, attrs): 1421 """Make an SFTP fsetstat request""" 1422 1423 self.logger.debug1('Sending fsetstat for handle %s%s', 1424 to_hex(handle), hide_empty(attrs)) 1425 1426 return await self._make_request(FXP_FSETSTAT, String(handle), 1427 attrs.encode()) 1428 1429 async def statvfs(self, path): 1430 """Make an SFTP statvfs request""" 1431 1432 if self._supports_statvfs: 1433 self.logger.debug1('Sending statvfs for %s', path) 1434 1435 packet = await self._make_request(b'statvfs@openssh.com', 1436 String(path)) 1437 vfsattrs = SFTPVFSAttrs.decode(packet) 1438 packet.check_end() 1439 1440 self.logger.debug1('Received %s', vfsattrs) 1441 1442 return vfsattrs 1443 else: 1444 raise SFTPOpUnsupported('statvfs not supported') 1445 1446 async def fstatvfs(self, handle): 1447 """Make an SFTP fstatvfs request""" 1448 1449 if self._supports_fstatvfs: 1450 self.logger.debug1('Sending fstatvfs for handle %s', to_hex(handle)) 1451 1452 packet = await self._make_request(b'fstatvfs@openssh.com', 1453 String(handle)) 1454 vfsattrs = SFTPVFSAttrs.decode(packet) 1455 packet.check_end() 1456 1457 self.logger.debug1('Received %s', vfsattrs) 1458 1459 return vfsattrs 1460 else: 1461 raise SFTPOpUnsupported('fstatvfs not supported') 1462 1463 async def remove(self, path): 1464 """Make an SFTP remove request""" 1465 1466 self.logger.debug1('Sending remove for %s', path) 1467 1468 return await self._make_request(FXP_REMOVE, String(path)) 1469 1470 async def rename(self, oldpath, newpath): 1471 """Make an SFTP rename request""" 1472 1473 self.logger.debug1('Sending rename request from %s to %s', 1474 oldpath, newpath) 1475 1476 return await self._make_request(FXP_RENAME, String(oldpath), 1477 String(newpath)) 1478 1479 async def posix_rename(self, oldpath, newpath): 1480 """Make an SFTP POSIX rename request""" 1481 1482 if self._supports_posix_rename: 1483 self.logger.debug1('Sending POSIX rename request from %s to %s', 1484 oldpath, newpath) 1485 1486 return await self._make_request(b'posix-rename@openssh.com', 1487 String(oldpath), String(newpath)) 1488 else: 1489 raise SFTPOpUnsupported('POSIX rename not supported') 1490 1491 async def opendir(self, path): 1492 """Make an SFTP opendir request""" 1493 1494 self.logger.debug1('Sending opendir for %s', path) 1495 1496 return await self._make_request(FXP_OPENDIR, String(path)) 1497 1498 async def readdir(self, handle): 1499 """Make an SFTP readdir request""" 1500 1501 self.logger.debug1('Sending readdir for handle %s', to_hex(handle)) 1502 1503 return await self._make_request(FXP_READDIR, String(handle)) 1504 1505 async def mkdir(self, path, attrs): 1506 """Make an SFTP mkdir request""" 1507 1508 self.logger.debug1('Sending mkdir for %s', path) 1509 1510 return await self._make_request(FXP_MKDIR, String(path), attrs.encode()) 1511 1512 async def rmdir(self, path): 1513 """Make an SFTP rmdir request""" 1514 1515 self.logger.debug1('Sending rmdir for %s', path) 1516 1517 return await self._make_request(FXP_RMDIR, String(path)) 1518 1519 async def realpath(self, path): 1520 """Make an SFTP realpath request""" 1521 1522 self.logger.debug1('Sending realpath for %s', path) 1523 1524 return await self._make_request(FXP_REALPATH, String(path)) 1525 1526 async def readlink(self, path): 1527 """Make an SFTP readlink request""" 1528 1529 self.logger.debug1('Sending readlink for %s', path) 1530 1531 return await self._make_request(FXP_READLINK, String(path)) 1532 1533 async def symlink(self, oldpath, newpath): 1534 """Make an SFTP symlink request""" 1535 1536 self.logger.debug1('Sending symlink request from %s to %s', 1537 oldpath, newpath) 1538 1539 if self._nonstandard_symlink: 1540 args = String(oldpath) + String(newpath) 1541 else: 1542 args = String(newpath) + String(oldpath) 1543 1544 return await self._make_request(FXP_SYMLINK, args) 1545 1546 async def link(self, oldpath, newpath): 1547 """Make an SFTP link request""" 1548 1549 if self._supports_hardlink: 1550 self.logger.debug1('Sending hardlink request from %s to %s', 1551 oldpath, newpath) 1552 1553 return await self._make_request(b'hardlink@openssh.com', 1554 String(oldpath), String(newpath)) 1555 else: 1556 raise SFTPOpUnsupported('link not supported') 1557 1558 async def fsync(self, handle): 1559 """Make an SFTP fsync request""" 1560 1561 if self._supports_fsync: 1562 self.logger.debug1('Sending fsync for handle %s', to_hex(handle)) 1563 1564 return await self._make_request(b'fsync@openssh.com', 1565 String(handle)) 1566 else: 1567 raise SFTPOpUnsupported('fsync not supported') 1568 1569 def exit(self): 1570 """Handle a request to close the SFTP session""" 1571 1572 if self._writer: 1573 self._writer.write_eof() 1574 1575 async def wait_closed(self): 1576 """Wait for this SFTP session to close""" 1577 1578 if self._writer: 1579 await self._writer.channel.wait_closed() 1580 1581 1582class SFTPClientFile: 1583 """SFTP client remote file object 1584 1585 This class represents an open file on a remote SFTP server. It 1586 is opened with the :meth:`open() <SFTPClient.open>` method on the 1587 :class:`SFTPClient` class and provides methods to read and write 1588 data and get and set attributes on the open file. 1589 1590 """ 1591 1592 def __init__(self, handler, handle, appending, encoding, errors, 1593 block_size, max_requests): 1594 self._handler = handler 1595 self._handle = handle 1596 self._appending = appending 1597 self._encoding = encoding 1598 self._errors = errors 1599 self._block_size = block_size 1600 self._max_requests = max_requests 1601 self._offset = None if appending else 0 1602 1603 async def __aenter__(self): 1604 """Allow SFTPClientFile to be used as an async context manager""" 1605 1606 return self 1607 1608 async def __aexit__(self, *exc_info): 1609 """Wait for file close when used as an async context manager""" 1610 1611 await self.close() 1612 1613 async def _end(self): 1614 """Return the offset of the end of the file""" 1615 1616 attrs = await self.stat() 1617 return attrs.size 1618 1619 async def read(self, size=-1, offset=None): 1620 """Read data from the remote file 1621 1622 This method reads and returns up to `size` bytes of data 1623 from the remote file. If size is negative, all data up to 1624 the end of the file is returned. 1625 1626 If offset is specified, the read will be performed starting 1627 at that offset rather than the current file position. This 1628 argument should be provided if you want to issue parallel 1629 reads on the same file, since the file position is not 1630 predictable in that case. 1631 1632 Data will be returned as a string if an encoding was set when 1633 the file was opened. Otherwise, data is returned as bytes. 1634 1635 An empty `str` or `bytes` object is returned when at EOF. 1636 1637 :param size: 1638 The number of bytes to read 1639 :param offset: (optional) 1640 The offset from the beginning of the file to begin reading 1641 :type size: `int` 1642 :type offset: `int` 1643 1644 :returns: data read from the file, as a `str` or `bytes` 1645 1646 :raises: | :exc:`ValueError` if the file has been closed 1647 | :exc:`UnicodeDecodeError` if the data can't be 1648 decoded using the requested encoding 1649 | :exc:`SFTPError` if the server returns an error 1650 1651 """ 1652 1653 if self._handle is None: 1654 raise ValueError('I/O operation on closed file') 1655 1656 if offset is None: 1657 offset = self._offset 1658 1659 # If self._offset is None, we're appending and haven't seeked 1660 # backward in the file since the last write, so there's no 1661 # data to return 1662 1663 data = b'' 1664 1665 if offset is not None: 1666 if size is None or size < 0: 1667 size = (await self._end()) - offset 1668 1669 try: 1670 if self._block_size and size > self._block_size: 1671 data = await _SFTPFileReader( 1672 self._block_size, self._max_requests, self._handler, 1673 self._handle, offset, size).run() 1674 else: 1675 data = await self._handler.read(self._handle, offset, size) 1676 self._offset = offset + len(data) 1677 except SFTPEOFError: 1678 pass 1679 1680 if self._encoding: 1681 data = data.decode(self._encoding, self._errors) 1682 1683 return data 1684 1685 async def write(self, data, offset=None): 1686 """Write data to the remote file 1687 1688 This method writes the specified data at the current 1689 position in the remote file. 1690 1691 :param data: 1692 The data to write to the file 1693 :param offset: (optional) 1694 The offset from the beginning of the file to begin writing 1695 :type data: `str` or `bytes` 1696 :type offset: `int` 1697 1698 If offset is specified, the write will be performed starting 1699 at that offset rather than the current file position. This 1700 argument should be provided if you want to issue parallel 1701 writes on the same file, since the file position is not 1702 predictable in that case. 1703 1704 :returns: number of bytes written 1705 1706 :raises: | :exc:`ValueError` if the file has been closed 1707 | :exc:`UnicodeEncodeError` if the data can't be 1708 encoded using the requested encoding 1709 | :exc:`SFTPError` if the server returns an error 1710 1711 """ 1712 1713 if self._handle is None: 1714 raise ValueError('I/O operation on closed file') 1715 1716 if offset is None: 1717 # Offset is ignored when appending, so fill in an offset of 0 1718 # if we don't have a current file position 1719 offset = self._offset or 0 1720 1721 if self._encoding: 1722 data = data.encode(self._encoding, self._errors) 1723 1724 datalen = len(data) 1725 1726 if self._block_size and datalen > self._block_size: 1727 await _SFTPFileWriter( 1728 self._block_size, self._max_requests, self._handler, 1729 self._handle, offset, data).run() 1730 else: 1731 await self._handler.write(self._handle, offset, data) 1732 1733 self._offset = None if self._appending else offset + datalen 1734 return datalen 1735 1736 async def seek(self, offset, from_what=SEEK_SET): 1737 """Seek to a new position in the remote file 1738 1739 This method changes the position in the remote file. The 1740 `offset` passed in is treated as relative to the beginning 1741 of the file if `from_what` is set to `SEEK_SET` (the 1742 default), relative to the current file position if it is 1743 set to `SEEK_CUR`, or relative to the end of the file 1744 if it is set to `SEEK_END`. 1745 1746 :param offset: 1747 The amount to seek 1748 :param from_what: (optional) 1749 The reference point to use 1750 :type offset: `int` 1751 :type from_what: `SEEK_SET`, `SEEK_CUR`, or `SEEK_END` 1752 1753 :returns: The new byte offset from the beginning of the file 1754 1755 """ 1756 1757 if self._handle is None: 1758 raise ValueError('I/O operation on closed file') 1759 1760 if from_what == SEEK_SET: 1761 self._offset = offset 1762 elif from_what == SEEK_CUR: 1763 self._offset += offset 1764 elif from_what == SEEK_END: 1765 self._offset = (await self._end()) + offset 1766 else: 1767 raise ValueError('Invalid reference point') 1768 1769 return self._offset 1770 1771 async def tell(self): 1772 """Return the current position in the remote file 1773 1774 This method returns the current position in the remote file. 1775 1776 :returns: The current byte offset from the beginning of the file 1777 1778 """ 1779 1780 if self._handle is None: 1781 raise ValueError('I/O operation on closed file') 1782 1783 if self._offset is None: 1784 self._offset = await self._end() 1785 1786 return self._offset 1787 1788 async def stat(self): 1789 """Return file attributes of the remote file 1790 1791 This method queries file attributes of the currently open file. 1792 1793 :returns: An :class:`SFTPAttrs` containing the file attributes 1794 1795 :raises: :exc:`SFTPError` if the server returns an error 1796 1797 """ 1798 1799 if self._handle is None: 1800 raise ValueError('I/O operation on closed file') 1801 1802 return await self._handler.fstat(self._handle) 1803 1804 async def setstat(self, attrs): 1805 """Set attributes of the remote file 1806 1807 This method sets file attributes of the currently open file. 1808 1809 :param attrs: 1810 File attributes to set on the file 1811 :type attrs: :class:`SFTPAttrs` 1812 1813 :raises: :exc:`SFTPError` if the server returns an error 1814 1815 """ 1816 1817 if self._handle is None: 1818 raise ValueError('I/O operation on closed file') 1819 1820 await self._handler.fsetstat(self._handle, attrs) 1821 1822 async def statvfs(self): 1823 """Return file system attributes of the remote file 1824 1825 This method queries attributes of the file system containing 1826 the currently open file. 1827 1828 :returns: An :class:`SFTPVFSAttrs` containing the file system 1829 attributes 1830 1831 :raises: :exc:`SFTPError` if the server doesn't support this 1832 extension or returns an error 1833 1834 """ 1835 1836 if self._handle is None: 1837 raise ValueError('I/O operation on closed file') 1838 1839 return await self._handler.fstatvfs(self._handle) 1840 1841 async def truncate(self, size=None): 1842 """Truncate the remote file to the specified size 1843 1844 This method changes the remote file's size to the specified 1845 value. If a size is not provided, the current file position 1846 is used. 1847 1848 :param size: (optional) 1849 The desired size of the file, in bytes 1850 :type size: `int` 1851 1852 :raises: :exc:`SFTPError` if the server returns an error 1853 1854 """ 1855 1856 if size is None: 1857 size = self._offset 1858 1859 await self.setstat(SFTPAttrs(size=size)) 1860 1861 async def chown(self, uid, gid): 1862 """Change the owner user and group id of the remote file 1863 1864 This method changes the user and group id of the 1865 currently open file. 1866 1867 :param uid: 1868 The new user id to assign to the file 1869 :param gid: 1870 The new group id to assign to the file 1871 :type uid: `int` 1872 :type gid: `int` 1873 1874 :raises: :exc:`SFTPError` if the server returns an error 1875 1876 """ 1877 1878 await self.setstat(SFTPAttrs(uid=uid, gid=gid)) 1879 1880 async def chmod(self, mode): 1881 """Change the file permissions of the remote file 1882 1883 This method changes the permissions of the currently 1884 open file. 1885 1886 :param mode: 1887 The new file permissions, expressed as an int 1888 :type mode: `int` 1889 1890 :raises: :exc:`SFTPError` if the server returns an error 1891 1892 """ 1893 1894 await self.setstat(SFTPAttrs(permissions=mode)) 1895 1896 async def utime(self, times=None): 1897 """Change the access and modify times of the remote file 1898 1899 This method changes the access and modify times of the 1900 currently open file. If `times` is not provided, 1901 the times will be changed to the current time. 1902 1903 :param times: (optional) 1904 The new access and modify times, as seconds relative to 1905 the UNIX epoch 1906 :type times: tuple of two `int` or `float` values 1907 1908 :raises: :exc:`SFTPError` if the server returns an error 1909 1910 """ 1911 1912 if times is None: 1913 atime = mtime = time.time() 1914 else: 1915 atime, mtime = times 1916 1917 await self.setstat(SFTPAttrs(atime=atime, mtime=mtime)) 1918 1919 async def fsync(self): 1920 """Force the remote file data to be written to disk""" 1921 1922 if self._handle is None: 1923 raise ValueError('I/O operation on closed file') 1924 1925 await self._handler.fsync(self._handle) 1926 1927 async def close(self): 1928 """Close the remote file""" 1929 1930 if self._handle: 1931 await self._handler.close(self._handle) 1932 self._handle = None 1933 1934 1935class SFTPClient: 1936 """SFTP client 1937 1938 This class represents the client side of an SFTP session. It is 1939 started by calling the :meth:`start_sftp_client() 1940 <SSHClientConnection.start_sftp_client>` method on the 1941 :class:`SSHClientConnection` class. 1942 1943 """ 1944 1945 def __init__(self, handler, path_encoding, path_errors): 1946 self._handler = handler 1947 self._path_encoding = path_encoding 1948 self._path_errors = path_errors 1949 self._cwd = None 1950 1951 async def __aenter__(self): 1952 """Allow SFTPClient to be used as an async context manager""" 1953 1954 return self 1955 1956 async def __aexit__(self, *exc_info): 1957 """Wait for client close when used as an async context manager""" 1958 1959 self.exit() 1960 await self.wait_closed() 1961 1962 @property 1963 def logger(self): 1964 """A logger associated with this SFTP client""" 1965 1966 return self._handler.logger 1967 1968 def basename(self, path): 1969 """Return the final component of a POSIX-style path""" 1970 1971 # pylint: disable=no-self-use 1972 1973 return posixpath.basename(path) 1974 1975 def encode(self, path): 1976 """Encode path name using configured path encoding 1977 1978 This method has no effect if the path is already bytes. 1979 1980 """ 1981 1982 if isinstance(path, PurePath): # pragma: no branch 1983 path = str(path) 1984 1985 if isinstance(path, str): 1986 if self._path_encoding: 1987 path = path.encode(self._path_encoding, self._path_errors) 1988 else: 1989 raise SFTPBadMessage('Path must be bytes when ' 1990 'encoding is not set') 1991 1992 return path 1993 1994 def decode(self, path, want_string=True): 1995 """Decode path name using configured path encoding 1996 1997 This method has no effect if want_string is set to `False`. 1998 1999 """ 2000 2001 if want_string and self._path_encoding: 2002 try: 2003 path = path.decode(self._path_encoding, self._path_errors) 2004 except UnicodeDecodeError: 2005 raise SFTPBadMessage('Unable to decode name') from None 2006 2007 return path 2008 2009 def compose_path(self, path, parent=...): 2010 """Compose a path 2011 2012 If parent is not specified, return a path relative to the 2013 current remote working directory. 2014 2015 """ 2016 2017 if parent is ...: 2018 parent = self._cwd 2019 2020 path = self.encode(path) 2021 2022 return posixpath.join(parent, path) if parent else path 2023 2024 async def _mode(self, path, statfunc=None): 2025 """Return the mode of a remote path, or 0 if it can't be accessed""" 2026 2027 if statfunc is None: 2028 statfunc = self.stat 2029 2030 try: 2031 return (await statfunc(path)).permissions 2032 except (SFTPNoSuchFile, SFTPPermissionDenied): 2033 return 0 2034 2035 async def _glob(self, fs, patterns, error_handler): 2036 """Begin a new glob pattern match""" 2037 2038 # pylint: disable=no-self-use 2039 2040 if isinstance(patterns, (str, bytes, PurePath)): 2041 patterns = [patterns] 2042 2043 result = [] 2044 2045 for pattern in patterns: 2046 if not pattern: 2047 continue 2048 2049 names = await match_glob(fs, fs.encode(pattern), error_handler) 2050 2051 if isinstance(pattern, (str, PurePath)): 2052 names = [fs.decode(name) for name in names] 2053 2054 result.extend(names) 2055 2056 return result 2057 2058 async def _copy(self, srcfs, dstfs, srcpath, dstpath, preserve, 2059 recurse, follow_symlinks, block_size, max_requests, 2060 progress_handler, error_handler): 2061 """Copy a file, directory, or symbolic link""" 2062 2063 try: 2064 if follow_symlinks: 2065 srcattrs = await srcfs.stat(srcpath) 2066 else: 2067 srcattrs = await srcfs.lstat(srcpath) 2068 2069 if stat.S_ISDIR(srcattrs.permissions): 2070 if not recurse: 2071 raise SFTPFailure('%s is a directory' % 2072 srcpath.decode('utf-8', errors='replace')) 2073 2074 self.logger.info(' Starting copy of directory %s to %s', 2075 srcpath, dstpath) 2076 2077 if not await dstfs.isdir(dstpath): 2078 await dstfs.mkdir(dstpath) 2079 2080 names = await srcfs.listdir(srcpath) 2081 2082 for name in names: 2083 if name in (b'.', b'..'): 2084 continue 2085 2086 srcfile = posixpath.join(srcpath, name) 2087 dstfile = posixpath.join(dstpath, name) 2088 2089 await self._copy(srcfs, dstfs, srcfile, dstfile, 2090 preserve, recurse, follow_symlinks, 2091 block_size, max_requests, 2092 progress_handler, error_handler) 2093 2094 self.logger.info(' Finished copy of directory %s to %s', 2095 srcpath, dstpath) 2096 2097 elif stat.S_ISLNK(srcattrs.permissions): 2098 targetpath = await srcfs.readlink(srcpath) 2099 2100 self.logger.info(' Copying symlink %s to %s', srcpath, dstpath) 2101 self.logger.info(' Target path: %s', targetpath) 2102 2103 await dstfs.symlink(targetpath, dstpath) 2104 else: 2105 self.logger.info(' Copying file %s to %s', srcpath, dstpath) 2106 2107 await _SFTPFileCopier(block_size, max_requests, 0, 2108 srcattrs.size, srcfs, dstfs, srcpath, 2109 dstpath, progress_handler).run() 2110 2111 if preserve: 2112 attrs = await srcfs.stat(srcpath) 2113 2114 attrs = SFTPAttrs(permissions=attrs.permissions, 2115 atime=attrs.atime, mtime=attrs.mtime) 2116 2117 self.logger.info(' Preserving attrs: %s', attrs) 2118 2119 await dstfs.setstat(dstpath, attrs) 2120 except (OSError, SFTPError) as exc: 2121 # pylint: disable=attribute-defined-outside-init 2122 exc.srcpath = srcpath 2123 exc.dstpath = dstpath 2124 2125 if error_handler: 2126 error_handler(exc) 2127 else: 2128 raise 2129 2130 async def _begin_copy(self, srcfs, dstfs, srcpaths, dstpath, copy_type, 2131 expand_glob, preserve, recurse, follow_symlinks, 2132 block_size, max_requests, progress_handler, 2133 error_handler): 2134 """Begin a new file upload, download, or copy""" 2135 2136 if isinstance(srcpaths, tuple): 2137 srcpaths = list(srcpaths) 2138 2139 self.logger.info('Starting SFTP %s of %s to %s', 2140 copy_type, srcpaths, dstpath) 2141 2142 if expand_glob: 2143 srcpaths = await self._glob(srcfs, srcpaths, error_handler) 2144 2145 dst_isdir = dstpath is None or (await dstfs.isdir(dstpath)) 2146 2147 if dstpath: 2148 dstpath = dstfs.encode(dstpath) 2149 2150 if isinstance(srcpaths, (str, bytes, PurePath)): 2151 srcpaths = [srcpaths] 2152 elif not dst_isdir: 2153 raise SFTPFailure('%s must be a directory' % 2154 dstpath.decode('utf-8', errors='replace')) 2155 2156 for srcfile in srcpaths: 2157 srcfile = srcfs.encode(srcfile) 2158 filename = srcfs.basename(srcfile) 2159 2160 if dstpath is None: 2161 dstfile = filename 2162 elif dst_isdir: 2163 dstfile = dstfs.compose_path(filename, parent=dstpath) 2164 else: 2165 dstfile = dstpath 2166 2167 await self._copy(srcfs, dstfs, srcfile, dstfile, preserve, 2168 recurse, follow_symlinks, block_size, 2169 max_requests, progress_handler, error_handler) 2170 2171 async def get(self, remotepaths, localpath=None, *, preserve=False, 2172 recurse=False, follow_symlinks=False, 2173 block_size=SFTP_BLOCK_SIZE, max_requests=_MAX_SFTP_REQUESTS, 2174 progress_handler=None, error_handler=None): 2175 """Download remote files 2176 2177 This method downloads one or more files or directories from 2178 the remote system. Either a single remote path or a sequence 2179 of remote paths to download can be provided. 2180 2181 When downloading a single file or directory, the local path can 2182 be either the full path to download data into or the path to an 2183 existing directory where the data should be placed. In the 2184 latter case, the base file name from the remote path will be 2185 used as the local name. 2186 2187 When downloading multiple files, the local path must refer to 2188 an existing directory. 2189 2190 If no local path is provided, the file is downloaded 2191 into the current local working directory. 2192 2193 If preserve is `True`, the access and modification times 2194 and permissions of the original file are set on the 2195 downloaded file. 2196 2197 If recurse is `True` and the remote path points at a 2198 directory, the entire subtree under that directory is 2199 downloaded. 2200 2201 If follow_symlinks is set to `True`, symbolic links found 2202 on the remote system will have the contents of their target 2203 downloaded rather than creating a local symbolic link. When 2204 using this option during a recursive download, one needs to 2205 watch out for links that result in loops. 2206 2207 The block_size argument specifies the size of read and write 2208 requests issued when downloading the files, defaulting to 16 KB. 2209 2210 The max_requests argument specifies the maximum number of 2211 parallel read or write requests issued, defaulting to 128. 2212 2213 If progress_handler is specified, it will be called after 2214 each block of a file is successfully downloaded. The arguments 2215 passed to this handler will be the source path, destination 2216 path, bytes downloaded so far, and total bytes in the file 2217 being downloaded. If multiple source paths are provided or 2218 recurse is set to `True`, the progress_handler will be 2219 called consecutively on each file being downloaded. 2220 2221 If error_handler is specified and an error occurs during 2222 the download, this handler will be called with the exception 2223 instead of it being raised. This is intended to primarily be 2224 used when multiple remote paths are provided or when recurse 2225 is set to `True`, to allow error information to be collected 2226 without aborting the download of the remaining files. The 2227 error handler can raise an exception if it wants the download 2228 to completely stop. Otherwise, after an error, the download 2229 will continue starting with the next file. 2230 2231 :param remotepaths: 2232 The paths of the remote files or directories to download 2233 :param localpath: (optional) 2234 The path of the local file or directory to download into 2235 :param preserve: (optional) 2236 Whether or not to preserve the original file attributes 2237 :param recurse: (optional) 2238 Whether or not to recursively copy directories 2239 :param follow_symlinks: (optional) 2240 Whether or not to follow symbolic links 2241 :param block_size: (optional) 2242 The block size to use for file reads and writes 2243 :param max_requests: (optional) 2244 The maximum number of parallel read or write requests 2245 :param progress_handler: (optional) 2246 The function to call to report download progress 2247 :param error_handler: (optional) 2248 The function to call when an error occurs 2249 :type remotepaths: 2250 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes`, 2251 or a sequence of these 2252 :type localpath: 2253 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2254 :type preserve: `bool` 2255 :type recurse: `bool` 2256 :type follow_symlinks: `bool` 2257 :type block_size: `int` 2258 :type max_requests: `int` 2259 :type progress_handler: `callable` 2260 :type error_handler: `callable` 2261 2262 :raises: | :exc:`OSError` if a local file I/O error occurs 2263 | :exc:`SFTPError` if the server returns an error 2264 2265 """ 2266 2267 await self._begin_copy(self, LocalFile, remotepaths, localpath, 'get', 2268 False, preserve, recurse, follow_symlinks, 2269 block_size, max_requests, progress_handler, 2270 error_handler) 2271 2272 async def put(self, localpaths, remotepath=None, *, preserve=False, 2273 recurse=False, follow_symlinks=False, 2274 block_size=SFTP_BLOCK_SIZE, max_requests=_MAX_SFTP_REQUESTS, 2275 progress_handler=None, error_handler=None): 2276 """Upload local files 2277 2278 This method uploads one or more files or directories to the 2279 remote system. Either a single local path or a sequence of 2280 local paths to upload can be provided. 2281 2282 When uploading a single file or directory, the remote path can 2283 be either the full path to upload data into or the path to an 2284 existing directory where the data should be placed. In the 2285 latter case, the base file name from the local path will be 2286 used as the remote name. 2287 2288 When uploading multiple files, the remote path must refer to 2289 an existing directory. 2290 2291 If no remote path is provided, the file is uploaded into the 2292 current remote working directory. 2293 2294 If preserve is `True`, the access and modification times 2295 and permissions of the original file are set on the 2296 uploaded file. 2297 2298 If recurse is `True` and the local path points at a 2299 directory, the entire subtree under that directory is 2300 uploaded. 2301 2302 If follow_symlinks is set to `True`, symbolic links found 2303 on the local system will have the contents of their target 2304 uploaded rather than creating a remote symbolic link. When 2305 using this option during a recursive upload, one needs to 2306 watch out for links that result in loops. 2307 2308 The block_size argument specifies the size of read and write 2309 requests issued when uploading the files, defaulting to 16 KB. 2310 2311 The max_requests argument specifies the maximum number of 2312 parallel read or write requests issued, defaulting to 128. 2313 2314 If progress_handler is specified, it will be called after 2315 each block of a file is successfully uploaded. The arguments 2316 passed to this handler will be the source path, destination 2317 path, bytes uploaded so far, and total bytes in the file 2318 being uploaded. If multiple source paths are provided or 2319 recurse is set to `True`, the progress_handler will be 2320 called consecutively on each file being uploaded. 2321 2322 If error_handler is specified and an error occurs during 2323 the upload, this handler will be called with the exception 2324 instead of it being raised. This is intended to primarily be 2325 used when multiple local paths are provided or when recurse 2326 is set to `True`, to allow error information to be collected 2327 without aborting the upload of the remaining files. The 2328 error handler can raise an exception if it wants the upload 2329 to completely stop. Otherwise, after an error, the upload 2330 will continue starting with the next file. 2331 2332 :param localpaths: 2333 The paths of the local files or directories to upload 2334 :param remotepath: (optional) 2335 The path of the remote file or directory to upload into 2336 :param preserve: (optional) 2337 Whether or not to preserve the original file attributes 2338 :param recurse: (optional) 2339 Whether or not to recursively copy directories 2340 :param follow_symlinks: (optional) 2341 Whether or not to follow symbolic links 2342 :param block_size: (optional) 2343 The block size to use for file reads and writes 2344 :param max_requests: (optional) 2345 The maximum number of parallel read or write requests 2346 :param progress_handler: (optional) 2347 The function to call to report upload progress 2348 :param error_handler: (optional) 2349 The function to call when an error occurs 2350 :type localpaths: 2351 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes`, 2352 or a sequence of these 2353 :type remotepath: 2354 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2355 :type preserve: `bool` 2356 :type recurse: `bool` 2357 :type follow_symlinks: `bool` 2358 :type block_size: `int` 2359 :type max_requests: `int` 2360 :type progress_handler: `callable` 2361 :type error_handler: `callable` 2362 2363 :raises: | :exc:`OSError` if a local file I/O error occurs 2364 | :exc:`SFTPError` if the server returns an error 2365 2366 """ 2367 2368 await self._begin_copy(LocalFile, self, localpaths, remotepath, 'put', 2369 False, preserve, recurse, follow_symlinks, 2370 block_size, max_requests, progress_handler, 2371 error_handler) 2372 2373 async def copy(self, srcpaths, dstpath=None, *, preserve=False, 2374 recurse=False, follow_symlinks=False, 2375 block_size=SFTP_BLOCK_SIZE, max_requests=_MAX_SFTP_REQUESTS, 2376 progress_handler=None, error_handler=None): 2377 """Copy remote files to a new location 2378 2379 This method copies one or more files or directories on the 2380 remote system to a new location. Either a single source path 2381 or a sequence of source paths to copy can be provided. 2382 2383 When copying a single file or directory, the destination path 2384 can be either the full path to copy data into or the path to 2385 an existing directory where the data should be placed. In the 2386 latter case, the base file name from the source path will be 2387 used as the destination name. 2388 2389 When copying multiple files, the destination path must refer 2390 to an existing remote directory. 2391 2392 If no destination path is provided, the file is copied into 2393 the current remote working directory. 2394 2395 If preserve is `True`, the access and modification times 2396 and permissions of the original file are set on the 2397 copied file. 2398 2399 If recurse is `True` and the source path points at a 2400 directory, the entire subtree under that directory is 2401 copied. 2402 2403 If follow_symlinks is set to `True`, symbolic links found 2404 in the source will have the contents of their target copied 2405 rather than creating a copy of the symbolic link. When 2406 using this option during a recursive copy, one needs to 2407 watch out for links that result in loops. 2408 2409 The block_size argument specifies the size of read and write 2410 requests issued when copying the files, defaulting to 16 KB. 2411 2412 The max_requests argument specifies the maximum number of 2413 parallel read or write requests issued, defaulting to 128. 2414 2415 If progress_handler is specified, it will be called after 2416 each block of a file is successfully copied. The arguments 2417 passed to this handler will be the source path, destination 2418 path, bytes copied so far, and total bytes in the file 2419 being copied. If multiple source paths are provided or 2420 recurse is set to `True`, the progress_handler will be 2421 called consecutively on each file being copied. 2422 2423 If error_handler is specified and an error occurs during 2424 the copy, this handler will be called with the exception 2425 instead of it being raised. This is intended to primarily be 2426 used when multiple source paths are provided or when recurse 2427 is set to `True`, to allow error information to be collected 2428 without aborting the copy of the remaining files. The error 2429 handler can raise an exception if it wants the copy to 2430 completely stop. Otherwise, after an error, the copy will 2431 continue starting with the next file. 2432 2433 :param srcpaths: 2434 The paths of the remote files or directories to copy 2435 :param dstpath: (optional) 2436 The path of the remote file or directory to copy into 2437 :param preserve: (optional) 2438 Whether or not to preserve the original file attributes 2439 :param recurse: (optional) 2440 Whether or not to recursively copy directories 2441 :param follow_symlinks: (optional) 2442 Whether or not to follow symbolic links 2443 :param block_size: (optional) 2444 The block size to use for file reads and writes 2445 :param max_requests: (optional) 2446 The maximum number of parallel read or write requests 2447 :param progress_handler: (optional) 2448 The function to call to report copy progress 2449 :param error_handler: (optional) 2450 The function to call when an error occurs 2451 :type srcpaths: 2452 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes`, 2453 or a sequence of these 2454 :type dstpath: 2455 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2456 :type preserve: `bool` 2457 :type recurse: `bool` 2458 :type follow_symlinks: `bool` 2459 :type block_size: `int` 2460 :type max_requests: `int` 2461 :type progress_handler: `callable` 2462 :type error_handler: `callable` 2463 2464 :raises: | :exc:`OSError` if a local file I/O error occurs 2465 | :exc:`SFTPError` if the server returns an error 2466 2467 """ 2468 2469 await self._begin_copy(self, self, srcpaths, dstpath, 'remote copy', 2470 False, preserve, recurse, follow_symlinks, 2471 block_size, max_requests, progress_handler, 2472 error_handler) 2473 2474 async def mget(self, remotepaths, localpath=None, *, preserve=False, 2475 recurse=False, follow_symlinks=False, 2476 block_size=SFTP_BLOCK_SIZE, max_requests=_MAX_SFTP_REQUESTS, 2477 progress_handler=None, error_handler=None): 2478 """Download remote files with glob pattern match 2479 2480 This method downloads files and directories from the remote 2481 system matching one or more glob patterns. 2482 2483 The arguments to this method are identical to the :meth:`get` 2484 method, except that the remote paths specified can contain 2485 wildcard patterns. 2486 2487 """ 2488 2489 await self._begin_copy(self, LocalFile, remotepaths, localpath, 'mget', 2490 True, preserve, recurse, follow_symlinks, 2491 block_size, max_requests, progress_handler, 2492 error_handler) 2493 2494 async def mput(self, localpaths, remotepath=None, *, preserve=False, 2495 recurse=False, follow_symlinks=False, 2496 block_size=SFTP_BLOCK_SIZE, max_requests=_MAX_SFTP_REQUESTS, 2497 progress_handler=None, error_handler=None): 2498 """Upload local files with glob pattern match 2499 2500 This method uploads files and directories to the remote 2501 system matching one or more glob patterns. 2502 2503 The arguments to this method are identical to the :meth:`put` 2504 method, except that the local paths specified can contain 2505 wildcard patterns. 2506 2507 """ 2508 2509 await self._begin_copy(LocalFile, self, localpaths, remotepath, 'mput', 2510 True, preserve, recurse, follow_symlinks, 2511 block_size, max_requests, progress_handler, 2512 error_handler) 2513 2514 async def mcopy(self, srcpaths, dstpath=None, *, preserve=False, 2515 recurse=False, follow_symlinks=False, 2516 block_size=SFTP_BLOCK_SIZE, max_requests=_MAX_SFTP_REQUESTS, 2517 progress_handler=None, error_handler=None): 2518 """Download remote files with glob pattern match 2519 2520 This method copies files and directories on the remote 2521 system matching one or more glob patterns. 2522 2523 The arguments to this method are identical to the :meth:`copy` 2524 method, except that the source paths specified can contain 2525 wildcard patterns. 2526 2527 """ 2528 2529 await self._begin_copy(self, self, srcpaths, dstpath, 'remote mcopy', 2530 True, preserve, recurse, follow_symlinks, 2531 block_size, max_requests, progress_handler, 2532 error_handler) 2533 2534 async def glob(self, patterns, error_handler=None): 2535 """Match remote files against glob patterns 2536 2537 This method matches remote files against one or more glob 2538 patterns. Either a single pattern or a sequence of patterns 2539 can be provided to match against. 2540 2541 Supported wildcard characters include '*', '?', and 2542 character ranges in square brackets. In addition, '**' 2543 can be used to trigger a recursive directory search at 2544 that point in the pattern, and a trailing slash can be 2545 used to request that only directories get returned. 2546 2547 If error_handler is specified and an error occurs during 2548 the match, this handler will be called with the exception 2549 instead of it being raised. This is intended to primarily be 2550 used when multiple patterns are provided to allow error 2551 information to be collected without aborting the match 2552 against the remaining patterns. The error handler can raise 2553 an exception if it wants to completely abort the match. 2554 Otherwise, after an error, the match will continue starting 2555 with the next pattern. 2556 2557 An error will be raised if any of the patterns completely 2558 fail to match, and this can either stop the match against 2559 the remaining patterns or be handled by the error_handler 2560 just like other errors. 2561 2562 :param patterns: 2563 Glob patterns to try and match remote files against 2564 :param error_handler: (optional) 2565 The function to call when an error occurs 2566 :type patterns: 2567 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes`, 2568 or a sequence of these 2569 :type error_handler: `callable` 2570 2571 :raises: :exc:`SFTPError` if the server returns an error 2572 or no match is found 2573 2574 """ 2575 2576 return await self._glob(self, patterns, error_handler) 2577 2578 async def makedirs(self, path, attrs=SFTPAttrs(), exist_ok=False): 2579 """Create a remote directory with the specified attributes 2580 2581 This method creates a remote directory at the specified path 2582 similar to :meth:`mkdir`, but it will also create any 2583 intermediate directories which don't yet exist. 2584 2585 If the target directory already exists and exist_ok is set 2586 to `False`, this method will raise an error. 2587 2588 :param path: 2589 The path of where the new remote directory should be created 2590 :param attrs: (optional) 2591 The file attributes to use when creating the directory or 2592 any intermediate directories 2593 :param exist_ok: (optional) 2594 Whether or not to raise an error if thet target directory 2595 already exists 2596 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2597 :type attrs: :class:`SFTPAttrs` 2598 :type exist_ok: `bool` 2599 2600 :raises: :exc:`SFTPError` if the server returns an error 2601 2602 """ 2603 2604 path = self.encode(path) 2605 curpath = b'/' if posixpath.isabs(path) else (self._cwd or b'') 2606 exists = True 2607 2608 for part in path.split(b'/'): 2609 curpath = posixpath.join(curpath, part) 2610 2611 try: 2612 await self.mkdir(curpath, attrs) 2613 exists = False 2614 except SFTPFailure: 2615 mode = await self._mode(curpath) 2616 2617 if not stat.S_ISDIR(mode): 2618 path = curpath.decode('utf-8', errors='replace') 2619 raise SFTPFailure('%s is not a directory' % path) from None 2620 2621 if exists and not exist_ok: 2622 raise SFTPFailure('%s already exists' % 2623 curpath.decode('utf-8', errors='replace')) 2624 2625 async def rmtree(self, path, ignore_errors=False, onerror=None): 2626 """Recursively delete a directory tree 2627 2628 This method removes all the files in a directory tree. 2629 2630 If ignore_errors is set, errors are ignored. Otherwise, 2631 if onerror is set, it will be called with arguments of 2632 the function which failed, the path it failed on, and 2633 exception information returns by :func:`sys.exc_info()`. 2634 2635 If follow_symlinks is set, files or directories pointed at by 2636 symlinks (and their subdirectories, if any) will be removed 2637 in addition to the links pointing at them. 2638 2639 :param path: 2640 The path of the parent directory to remove 2641 :param ignore_errors: (optional) 2642 Whether or not to ignore errors during the remove 2643 :param onerror: (optional) 2644 A function to call when errors occur 2645 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2646 :type ignore_errors: `bool` 2647 :type onerror: `callable` 2648 2649 :raises: :exc:`SFTPError` if the server returns an error 2650 2651 """ 2652 2653 async def _unlink(path): 2654 """Internal helper for unlinking non-directories""" 2655 2656 try: 2657 await self.unlink(path) 2658 except SFTPError: 2659 onerror(self.unlink, path, sys.exc_info()) 2660 2661 async def _rmtree(path): 2662 """Internal helper for rmtree recursion""" 2663 2664 tasks = [] 2665 2666 try: 2667 async with sem: 2668 async for entry in self.scandir(path): 2669 if entry.filename in (b'.', b'..'): 2670 continue 2671 2672 mode = entry.attrs.permissions 2673 entry = posixpath.join(path, entry.filename) 2674 2675 if stat.S_ISDIR(mode): 2676 task = _rmtree(entry) 2677 else: 2678 task = _unlink(entry) 2679 2680 tasks.append(asyncio.ensure_future(task)) 2681 except SFTPError: 2682 onerror(self.scandir, path, sys.exc_info()) 2683 2684 results = await asyncio.gather(*tasks, return_exceptions=True) 2685 exc = next((result for result in results 2686 if isinstance(result, Exception)), None) 2687 2688 if exc: 2689 raise exc 2690 2691 try: 2692 await self.rmdir(path) 2693 except SFTPError: 2694 onerror(self.rmdir, path, sys.exc_info()) 2695 2696 # pylint: disable=function-redefined 2697 if ignore_errors: 2698 def onerror(*_args): 2699 pass 2700 elif onerror is None: 2701 def onerror(*_args): 2702 raise # pylint: disable=misplaced-bare-raise 2703 # pylint: enable=function-redefined 2704 2705 path = self.encode(path) 2706 sem = asyncio.Semaphore(_MAX_SFTP_REQUESTS) 2707 2708 try: 2709 if await self.islink(path): 2710 raise SFTPNoSuchFile('%s must not be a symlink' % 2711 path.decode('utf-8', errors='replace')) 2712 except SFTPError: 2713 onerror(self.islink, path, sys.exc_info()) 2714 return 2715 2716 await _rmtree(path) 2717 2718 @async_context_manager 2719 async def open(self, path, pflags_or_mode=FXF_READ, attrs=SFTPAttrs(), 2720 encoding='utf-8', errors='strict', 2721 block_size=SFTP_BLOCK_SIZE, max_requests=_MAX_SFTP_REQUESTS): 2722 """Open a remote file 2723 2724 This method opens a remote file and returns an 2725 :class:`SFTPClientFile` object which can be used to read and 2726 write data and get and set file attributes. 2727 2728 The path can be either a `str` or `bytes` value. If it is a 2729 str, it will be encoded using the file encoding specified 2730 when the :class:`SFTPClient` was started. 2731 2732 The following open mode flags are supported: 2733 2734 ========== ====================================================== 2735 Mode Description 2736 ========== ====================================================== 2737 FXF_READ Open the file for reading. 2738 FXF_WRITE Open the file for writing. If both this and FXF_READ 2739 are set, open the file for both reading and writing. 2740 FXF_APPEND Force writes to append data to the end of the file 2741 regardless of seek position. 2742 FXF_CREAT Create the file if it doesn't exist. Without this, 2743 attempts to open a non-existent file will fail. 2744 FXF_TRUNC Truncate the file to zero length if it already exists. 2745 FXF_EXCL Return an error when trying to open a file which 2746 already exists. 2747 ========== ====================================================== 2748 2749 By default, file data is read and written as strings in UTF-8 2750 format with strict error checking, but this can be changed 2751 using the `encoding` and `errors` parameters. To read and 2752 write data as bytes in binary format, an `encoding` value of 2753 `None` can be used. 2754 2755 Instead of these flags, a Python open mode string can also be 2756 provided. Python open modes map to the above flags as follows: 2757 2758 ==== ============================================= 2759 Mode Flags 2760 ==== ============================================= 2761 r FXF_READ 2762 w FXF_WRITE | FXF_CREAT | FXF_TRUNC 2763 a FXF_WRITE | FXF_CREAT | FXF_APPEND 2764 x FXF_WRITE | FXF_CREAT | FXF_EXCL 2765 2766 r+ FXF_READ | FXF_WRITE 2767 w+ FXF_READ | FXF_WRITE | FXF_CREAT | FXF_TRUNC 2768 a+ FXF_READ | FXF_WRITE | FXF_CREAT | FXF_APPEND 2769 x+ FXF_READ | FXF_WRITE | FXF_CREAT | FXF_EXCL 2770 ==== ============================================= 2771 2772 Including a 'b' in the mode causes the `encoding` to be set 2773 to `None`, forcing all data to be read and written as bytes 2774 in binary format. 2775 2776 The attrs argument is used to set initial attributes of the 2777 file if it needs to be created. Otherwise, this argument is 2778 ignored. 2779 2780 The block_size argument specifies the size of parallel read and 2781 write requests issued on the file. If set to `None`, each read 2782 or write call will become a single request to the SFTP server. 2783 Otherwise, read or write calls larger than this size will be 2784 turned into parallel requests to the server of the requested 2785 size, defaulting to 16 KB. 2786 2787 .. note:: The OpenSSH SFTP server will close the connection 2788 if it receives a message larger than 256 KB, and 2789 limits read requests to returning no more than 2790 64 KB. So, when connecting to an OpenSSH SFTP 2791 server, it is recommended that the block_size be 2792 set below these sizes. 2793 2794 The max_requests argument specifies the maximum number of 2795 parallel read or write requests issued, defaulting to 128. 2796 2797 :param path: 2798 The name of the remote file to open 2799 :param pflags_or_mode: (optional) 2800 The access mode to use for the remote file (see above) 2801 :param attrs: (optional) 2802 File attributes to use if the file needs to be created 2803 :param encoding: (optional) 2804 The Unicode encoding to use for data read and written 2805 to the remote file 2806 :param errors: (optional) 2807 The error-handling mode if an invalid Unicode byte 2808 sequence is detected, defaulting to 'strict' which 2809 raises an exception 2810 :param block_size: (optional) 2811 The block size to use for read and write requests 2812 :param max_requests: (optional) 2813 The maximum number of parallel read or write requests 2814 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2815 :type pflags_or_mode: `int` or `str` 2816 :type attrs: :class:`SFTPAttrs` 2817 :type encoding: `str` 2818 :type errors: `str` 2819 :type block_size: `int` or `None` 2820 :type max_requests: `int` 2821 2822 :returns: An :class:`SFTPClientFile` to use to access the file 2823 2824 :raises: | :exc:`ValueError` if the mode is not valid 2825 | :exc:`SFTPError` if the server returns an error 2826 2827 """ 2828 2829 if isinstance(pflags_or_mode, str): 2830 pflags, binary = _mode_to_pflags(pflags_or_mode) 2831 2832 if binary: 2833 encoding = None 2834 else: 2835 pflags = pflags_or_mode 2836 2837 path = self.compose_path(path) 2838 handle = await self._handler.open(path, pflags, attrs) 2839 2840 return SFTPClientFile(self._handler, handle, pflags & FXF_APPEND, 2841 encoding, errors, block_size, max_requests) 2842 2843 async def stat(self, path): 2844 """Get attributes of a remote file or directory, following symlinks 2845 2846 This method queries the attributes of a remote file or 2847 directory. If the path provided is a symbolic link, the 2848 returned attributes will correspond to the target of the 2849 link. 2850 2851 :param path: 2852 The path of the remote file or directory to get attributes for 2853 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2854 2855 :returns: An :class:`SFTPAttrs` containing the file attributes 2856 2857 :raises: :exc:`SFTPError` if the server returns an error 2858 2859 """ 2860 2861 path = self.compose_path(path) 2862 return await self._handler.stat(path) 2863 2864 async def lstat(self, path): 2865 """Get attributes of a remote file, directory, or symlink 2866 2867 This method queries the attributes of a remote file, 2868 directory, or symlink. Unlike :meth:`stat`, this method 2869 returns the attributes of a symlink itself rather than 2870 the target of that link. 2871 2872 :param path: 2873 The path of the remote file, directory, or link to get 2874 attributes for 2875 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2876 2877 :returns: An :class:`SFTPAttrs` containing the file attributes 2878 2879 :raises: :exc:`SFTPError` if the server returns an error 2880 2881 """ 2882 2883 path = self.compose_path(path) 2884 return await self._handler.lstat(path) 2885 2886 async def setstat(self, path, attrs): 2887 """Set attributes of a remote file or directory 2888 2889 This method sets attributes of a remote file or directory. 2890 If the path provided is a symbolic link, the attributes 2891 will be set on the target of the link. A subset of the 2892 fields in `attrs` can be initialized and only those 2893 attributes will be changed. 2894 2895 :param path: 2896 The path of the remote file or directory to set attributes for 2897 :param attrs: 2898 File attributes to set 2899 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2900 :type attrs: :class:`SFTPAttrs` 2901 2902 :raises: :exc:`SFTPError` if the server returns an error 2903 2904 """ 2905 2906 path = self.compose_path(path) 2907 await self._handler.setstat(path, attrs) 2908 2909 async def statvfs(self, path): 2910 """Get attributes of a remote file system 2911 2912 This method queries the attributes of the file system containing 2913 the specified path. 2914 2915 :param path: 2916 The path of the remote file system to get attributes for 2917 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2918 2919 :returns: An :class:`SFTPVFSAttrs` containing the file system 2920 attributes 2921 2922 :raises: :exc:`SFTPError` if the server doesn't support this 2923 extension or returns an error 2924 2925 """ 2926 2927 path = self.compose_path(path) 2928 return await self._handler.statvfs(path) 2929 2930 async def truncate(self, path, size): 2931 """Truncate a remote file to the specified size 2932 2933 This method truncates a remote file to the specified size. 2934 If the path provided is a symbolic link, the target of 2935 the link will be truncated. 2936 2937 :param path: 2938 The path of the remote file to be truncated 2939 :param size: 2940 The desired size of the file, in bytes 2941 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2942 :type size: `int` 2943 2944 :raises: :exc:`SFTPError` if the server returns an error 2945 2946 """ 2947 2948 await self.setstat(path, SFTPAttrs(size=size)) 2949 2950 async def chown(self, path, uid, gid): 2951 """Change the owner user and group id of a remote file or directory 2952 2953 This method changes the user and group id of a remote 2954 file or directory. If the path provided is a symbolic 2955 link, the target of the link will be changed. 2956 2957 :param path: 2958 The path of the remote file to change 2959 :param uid: 2960 The new user id to assign to the file 2961 :param gid: 2962 The new group id to assign to the file 2963 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2964 :type uid: `int` 2965 :type gid: `int` 2966 2967 :raises: :exc:`SFTPError` if the server returns an error 2968 2969 """ 2970 2971 await self.setstat(path, SFTPAttrs(uid=uid, gid=gid)) 2972 2973 async def chmod(self, path, mode): 2974 """Change the file permissions of a remote file or directory 2975 2976 This method changes the permissions of a remote file or 2977 directory. If the path provided is a symbolic link, the 2978 target of the link will be changed. 2979 2980 :param path: 2981 The path of the remote file to change 2982 :param mode: 2983 The new file permissions, expressed as an int 2984 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 2985 :type mode: `int` 2986 2987 :raises: :exc:`SFTPError` if the server returns an error 2988 2989 """ 2990 2991 await self.setstat(path, SFTPAttrs(permissions=mode)) 2992 2993 async def utime(self, path, times=None): 2994 """Change the access and modify times of a remote file or directory 2995 2996 This method changes the access and modify times of a 2997 remote file or directory. If `times` is not provided, 2998 the times will be changed to the current time. If the 2999 path provided is a symbolic link, the target of the link 3000 will be changed. 3001 3002 :param path: 3003 The path of the remote file to change 3004 :param times: (optional) 3005 The new access and modify times, as seconds relative to 3006 the UNIX epoch 3007 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3008 :type times: tuple of two `int` or `float` values 3009 3010 :raises: :exc:`SFTPError` if the server returns an error 3011 3012 """ 3013 3014 if times is None: 3015 atime = mtime = time.time() 3016 else: 3017 atime, mtime = times 3018 3019 await self.setstat(path, SFTPAttrs(atime=atime, mtime=mtime)) 3020 3021 async def exists(self, path): 3022 """Return if the remote path exists and isn't a broken symbolic link 3023 3024 :param path: 3025 The remote path to check 3026 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3027 3028 :raises: :exc:`SFTPError` if the server returns an error 3029 3030 """ 3031 3032 return bool((await self._mode(path))) 3033 3034 async def lexists(self, path): 3035 """Return if the remote path exists, without following symbolic links 3036 3037 :param path: 3038 The remote path to check 3039 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3040 3041 :raises: :exc:`SFTPError` if the server returns an error 3042 3043 """ 3044 3045 return bool((await self._mode(path, statfunc=self.lstat))) 3046 3047 async def getatime(self, path): 3048 """Return the last access time of a remote file or directory 3049 3050 :param path: 3051 The remote path to check 3052 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3053 3054 :raises: :exc:`SFTPError` if the server returns an error 3055 3056 """ 3057 3058 return (await self.stat(path)).atime 3059 3060 async def getmtime(self, path): 3061 """Return the last modification time of a remote file or directory 3062 3063 :param path: 3064 The remote path to check 3065 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3066 3067 :raises: :exc:`SFTPError` if the server returns an error 3068 3069 """ 3070 3071 return (await self.stat(path)).mtime 3072 3073 async def getsize(self, path): 3074 """Return the size of a remote file or directory 3075 3076 :param path: 3077 The remote path to check 3078 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3079 3080 :raises: :exc:`SFTPError` if the server returns an error 3081 3082 """ 3083 3084 return (await self.stat(path)).size 3085 3086 async def isdir(self, path): 3087 """Return if the remote path refers to a directory 3088 3089 :param path: 3090 The remote path to check 3091 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3092 3093 :raises: :exc:`SFTPError` if the server returns an error 3094 3095 """ 3096 3097 return stat.S_ISDIR((await self._mode(path))) 3098 3099 async def isfile(self, path): 3100 """Return if the remote path refers to a regular file 3101 3102 :param path: 3103 The remote path to check 3104 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3105 3106 :raises: :exc:`SFTPError` if the server returns an error 3107 3108 """ 3109 3110 return stat.S_ISREG((await self._mode(path))) 3111 3112 async def islink(self, path): 3113 """Return if the remote path refers to a symbolic link 3114 3115 :param path: 3116 The remote path to check 3117 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3118 3119 :raises: :exc:`SFTPError` if the server returns an error 3120 3121 """ 3122 3123 return stat.S_ISLNK((await self._mode(path, statfunc=self.lstat))) 3124 3125 async def remove(self, path): 3126 """Remove a remote file 3127 3128 This method removes a remote file or symbolic link. 3129 3130 :param path: 3131 The path of the remote file or link to remove 3132 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3133 3134 :raises: :exc:`SFTPError` if the server returns an error 3135 3136 """ 3137 3138 path = self.compose_path(path) 3139 await self._handler.remove(path) 3140 3141 async def unlink(self, path): 3142 """Remove a remote file (see :meth:`remove`)""" 3143 3144 await self.remove(path) 3145 3146 async def rename(self, oldpath, newpath): 3147 """Rename a remote file, directory, or link 3148 3149 This method renames a remote file, directory, or link. 3150 3151 .. note:: This requests the standard SFTP version of rename 3152 which will not overwrite the new path if it already 3153 exists. To request POSIX behavior where the new 3154 path is removed before the rename, use 3155 :meth:`posix_rename`. 3156 3157 :param oldpath: 3158 The path of the remote file, directory, or link to rename 3159 :param newpath: 3160 The new name for this file, directory, or link 3161 :type oldpath: 3162 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3163 :type newpath: 3164 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3165 3166 :raises: :exc:`SFTPError` if the server returns an error 3167 3168 """ 3169 3170 oldpath = self.compose_path(oldpath) 3171 newpath = self.compose_path(newpath) 3172 await self._handler.rename(oldpath, newpath) 3173 3174 async def posix_rename(self, oldpath, newpath): 3175 """Rename a remote file, directory, or link with POSIX semantics 3176 3177 This method renames a remote file, directory, or link, 3178 removing the prior instance of new path if it previously 3179 existed. 3180 3181 This method may not be supported by all SFTP servers. 3182 3183 :param oldpath: 3184 The path of the remote file, directory, or link to rename 3185 :param newpath: 3186 The new name for this file, directory, or link 3187 :type oldpath: 3188 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3189 :type newpath: 3190 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3191 3192 :raises: :exc:`SFTPError` if the server doesn't support this 3193 extension or returns an error 3194 3195 """ 3196 3197 oldpath = self.compose_path(oldpath) 3198 newpath = self.compose_path(newpath) 3199 await self._handler.posix_rename(oldpath, newpath) 3200 3201 async def scandir(self, path='.'): 3202 """Return an async iterator of the contents of a remote directory 3203 3204 This method reads the contents of a directory, returning 3205 the names and attributes of what is contained there as an 3206 async iterator. If no path is provided, it defaults to the 3207 current remote working directory. 3208 3209 :param path: (optional) 3210 The path of the remote directory to read 3211 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3212 3213 :returns: An async iterator of :class:`SFTPName` entries, with 3214 path names matching the type used to pass in the path 3215 3216 :raises: :exc:`SFTPError` if the server returns an error 3217 3218 """ 3219 3220 dirpath = self.compose_path(path) 3221 handle = await self._handler.opendir(dirpath) 3222 3223 try: 3224 while True: 3225 for entry in await self._handler.readdir(handle): 3226 if isinstance(path, (str, PurePath)): 3227 entry.filename = self.decode(entry.filename) 3228 entry.longname = self.decode(entry.longname) 3229 3230 yield entry 3231 except SFTPEOFError: 3232 pass 3233 finally: 3234 await self._handler.close(handle) 3235 3236 async def readdir(self, path='.'): 3237 """Read the contents of a remote directory 3238 3239 This method reads the contents of a directory, returning 3240 the names and attributes of what is contained there. If no 3241 path is provided, it defaults to the current remote working 3242 directory. 3243 3244 :param path: (optional) 3245 The path of the remote directory to read 3246 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3247 3248 :returns: A list of :class:`SFTPName` entries, with path 3249 names matching the type used to pass in the path 3250 3251 :raises: :exc:`SFTPError` if the server returns an error 3252 3253 """ 3254 3255 return [entry async for entry in self.scandir(path)] 3256 3257 async def listdir(self, path='.'): 3258 """Read the names of the files in a remote directory 3259 3260 This method reads the names of files and subdirectories 3261 in a remote directory. If no path is provided, it defaults 3262 to the current remote working directory. 3263 3264 :param path: (optional) 3265 The path of the remote directory to read 3266 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3267 3268 :returns: A list of file/subdirectory names, matching the 3269 type used to pass in the path 3270 3271 :raises: :exc:`SFTPError` if the server returns an error 3272 3273 """ 3274 3275 names = await self.readdir(path) 3276 return [name.filename for name in names] 3277 3278 async def mkdir(self, path, attrs=SFTPAttrs()): 3279 """Create a remote directory with the specified attributes 3280 3281 This method creates a new remote directory at the 3282 specified path with the requested attributes. 3283 3284 :param path: 3285 The path of where the new remote directory should be created 3286 :param attrs: (optional) 3287 The file attributes to use when creating the directory 3288 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3289 :type attrs: :class:`SFTPAttrs` 3290 3291 :raises: :exc:`SFTPError` if the server returns an error 3292 3293 """ 3294 3295 path = self.compose_path(path) 3296 await self._handler.mkdir(path, attrs) 3297 3298 async def rmdir(self, path): 3299 """Remove a remote directory 3300 3301 This method removes a remote directory. The directory 3302 must be empty for the removal to succeed. 3303 3304 :param path: 3305 The path of the remote directory to remove 3306 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3307 3308 :raises: :exc:`SFTPError` if the server returns an error 3309 3310 """ 3311 3312 path = self.compose_path(path) 3313 await self._handler.rmdir(path) 3314 3315 async def realpath(self, path): 3316 """Return the canonical version of a remote path 3317 3318 This method returns a canonical version of the requested path. 3319 3320 :param path: (optional) 3321 The path of the remote directory to canonicalize 3322 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3323 3324 :returns: The canonical path as a `str` or `bytes`, matching 3325 the type used to pass in the path 3326 3327 :raises: :exc:`SFTPError` if the server returns an error 3328 3329 """ 3330 3331 fullpath = self.compose_path(path) 3332 names = await self._handler.realpath(fullpath) 3333 3334 if len(names) > 1: 3335 raise SFTPBadMessage('Too many names returned') 3336 3337 return self.decode(names[0].filename, isinstance(path, (str, PurePath))) 3338 3339 async def getcwd(self): 3340 """Return the current remote working directory 3341 3342 :returns: The current remote working directory, decoded using 3343 the specified path encoding 3344 3345 :raises: :exc:`SFTPError` if the server returns an error 3346 3347 """ 3348 3349 if self._cwd is None: 3350 self._cwd = await self.realpath(b'.') 3351 3352 return self.decode(self._cwd) 3353 3354 async def chdir(self, path): 3355 """Change the current remote working directory 3356 3357 :param path: 3358 The path to set as the new remote working directory 3359 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3360 3361 :raises: :exc:`SFTPError` if the server returns an error 3362 3363 """ 3364 3365 self._cwd = await self.realpath(self.encode(path)) 3366 3367 async def readlink(self, path): 3368 """Return the target of a remote symbolic link 3369 3370 This method returns the target of a symbolic link. 3371 3372 :param path: 3373 The path of the remote symbolic link to follow 3374 :type path: :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3375 3376 :returns: The target path of the link as a `str` or `bytes` 3377 3378 :raises: :exc:`SFTPError` if the server returns an error 3379 3380 """ 3381 3382 linkpath = self.compose_path(path) 3383 names = await self._handler.readlink(linkpath) 3384 3385 if len(names) > 1: 3386 raise SFTPBadMessage('Too many names returned') 3387 3388 return self.decode(names[0].filename, isinstance(path, (str, PurePath))) 3389 3390 async def symlink(self, oldpath, newpath): 3391 """Create a remote symbolic link 3392 3393 This method creates a symbolic link. The argument order here 3394 matches the standard Python :meth:`os.symlink` call. The 3395 argument order sent on the wire is automatically adapted 3396 depending on the version information sent by the server, as 3397 a number of servers (OpenSSH in particular) did not follow 3398 the SFTP standard when implementing this call. 3399 3400 :param oldpath: 3401 The path the link should point to 3402 :param newpath: 3403 The path of where to create the remote symbolic link 3404 :type oldpath: 3405 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3406 :type newpath: 3407 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3408 3409 :raises: :exc:`SFTPError` if the server returns an error 3410 3411 """ 3412 3413 oldpath = self.compose_path(oldpath) 3414 newpath = self.encode(newpath) 3415 await self._handler.symlink(oldpath, newpath) 3416 3417 async def link(self, oldpath, newpath): 3418 """Create a remote hard link 3419 3420 This method creates a hard link to the remote file specified 3421 by oldpath at the location specified by newpath. 3422 3423 This method may not be supported by all SFTP servers. 3424 3425 :param oldpath: 3426 The path of the remote file the hard link should point to 3427 :param newpath: 3428 The path of where to create the remote hard link 3429 :type oldpath: 3430 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3431 :type newpath: 3432 :class:`PurePath <pathlib.PurePath>`, `str`, or `bytes` 3433 3434 :raises: :exc:`SFTPError` if the server doesn't support this 3435 extension or returns an error 3436 3437 """ 3438 3439 oldpath = self.compose_path(oldpath) 3440 newpath = self.compose_path(newpath) 3441 await self._handler.link(oldpath, newpath) 3442 3443 def exit(self): 3444 """Exit the SFTP client session 3445 3446 This method exits the SFTP client session, closing the 3447 corresponding channel opened on the server. 3448 3449 """ 3450 3451 self._handler.exit() 3452 3453 async def wait_closed(self): 3454 """Wait for this SFTP client session to close""" 3455 3456 await self._handler.wait_closed() 3457 3458 3459class SFTPServerHandler(SFTPHandler): 3460 """An SFTP server session handler""" 3461 3462 _extensions = [(b'posix-rename@openssh.com', b'1'), 3463 (b'hardlink@openssh.com', b'1'), 3464 (b'fsync@openssh.com', b'1')] 3465 3466 if hasattr(os, 'statvfs'): # pragma: no branch 3467 _extensions += [(b'statvfs@openssh.com', b'2'), 3468 (b'fstatvfs@openssh.com', b'2')] 3469 3470 def __init__(self, server, reader, writer): 3471 super().__init__(reader, writer) 3472 3473 self._server = server 3474 self._version = None 3475 self._nonstandard_symlink = False 3476 self._next_handle = 0 3477 self._file_handles = {} 3478 self._dir_handles = {} 3479 3480 async def _cleanup(self, exc): 3481 """Clean up this SFTP server session""" 3482 3483 if self._server: # pragma: no branch 3484 for file_obj in list(self._file_handles.values()): 3485 result = self._server.close(file_obj) 3486 3487 if inspect.isawaitable(result): 3488 await result 3489 3490 self._server.exit() 3491 3492 self._server = None 3493 self._file_handles = [] 3494 self._dir_handles = [] 3495 3496 self.logger.info('SFTP server exited%s', ': ' + str(exc) if exc else '') 3497 3498 await super()._cleanup(exc) 3499 3500 def _get_next_handle(self): 3501 """Get the next available unique file handle number""" 3502 3503 while True: 3504 handle = self._next_handle.to_bytes(4, 'big') 3505 self._next_handle = (self._next_handle + 1) & 0xffffffff 3506 3507 if (handle not in self._file_handles and 3508 handle not in self._dir_handles): 3509 return handle 3510 3511 async def _process_packet(self, pkttype, pktid, packet): 3512 """Process incoming SFTP requests""" 3513 3514 # pylint: disable=broad-except 3515 try: 3516 if pkttype == FXP_EXTENDED: 3517 pkttype = packet.get_string() 3518 3519 handler = self._packet_handlers.get(pkttype) 3520 if not handler: 3521 raise SFTPOpUnsupported('Unsupported request type: %s' % 3522 pkttype) 3523 3524 return_type = self._return_types.get(pkttype, FXP_STATUS) 3525 result = await handler(self, packet) 3526 3527 if return_type == FXP_STATUS: 3528 self.logger.debug1('Sending OK') 3529 3530 result = UInt32(FX_OK) + String('') + String('') 3531 elif return_type == FXP_HANDLE: 3532 self.logger.debug1('Sending handle %s', to_hex(result)) 3533 3534 result = String(result) 3535 elif return_type == FXP_DATA: 3536 self.logger.debug1('Sending %s', plural(len(result), 3537 'data byte')) 3538 3539 result = String(result) 3540 elif return_type == FXP_NAME: 3541 self.logger.debug1('Sending %s', plural(len(result), 'name')) 3542 3543 for name in result: 3544 self.logger.debug1(' %s', name) 3545 3546 result = (UInt32(len(result)) + 3547 b''.join(name.encode() for name in result)) 3548 else: 3549 if isinstance(result, os.stat_result): 3550 result = SFTPAttrs.from_local(result) 3551 elif isinstance(result, os.statvfs_result): 3552 result = SFTPVFSAttrs.from_local(result) 3553 3554 if isinstance(result, SFTPAttrs): 3555 self.logger.debug1('Sending %s', result) 3556 elif isinstance(result, SFTPVFSAttrs): # pragma: no branch 3557 self.logger.debug1('Sending %s', result) 3558 3559 result = result.encode() 3560 except PacketDecodeError as exc: 3561 return_type = FXP_STATUS 3562 3563 self.logger.debug1('Sending bad message error: %s', str(exc)) 3564 3565 result = (UInt32(FX_BAD_MESSAGE) + String(str(exc)) + 3566 String(DEFAULT_LANG)) 3567 except SFTPError as exc: 3568 return_type = FXP_STATUS 3569 3570 if exc.code == FX_EOF: 3571 self.logger.debug1('Sending EOF') 3572 else: 3573 self.logger.debug1('Sending error: %s', str(exc.reason)) 3574 3575 result = UInt32(exc.code) + String(exc.reason) + String(exc.lang) 3576 except NotImplementedError as exc: 3577 return_type = FXP_STATUS 3578 name = handler.__name__[9:] 3579 3580 self.logger.debug1('Sending operation not supported: %s', name) 3581 3582 result = (UInt32(FX_OP_UNSUPPORTED) + 3583 String('Operation not supported: %s' % name) + 3584 String(DEFAULT_LANG)) 3585 except OSError as exc: 3586 return_type = FXP_STATUS 3587 reason = exc.strerror or str(exc) 3588 3589 if exc.errno in (errno.ENOENT, errno.ENOTDIR): 3590 self.logger.debug1('Sending no such file error: %s', reason) 3591 3592 code = FX_NO_SUCH_FILE 3593 elif exc.errno == errno.EACCES: 3594 self.logger.debug1('Sending permission denied: %s', reason) 3595 3596 code = FX_PERMISSION_DENIED 3597 else: 3598 self.logger.debug1('Sending failure: %s', reason) 3599 3600 code = FX_FAILURE 3601 3602 result = UInt32(code) + String(reason) + String(DEFAULT_LANG) 3603 except Exception as exc: # pragma: no cover 3604 return_type = FXP_STATUS 3605 reason = 'Uncaught exception: %s' % str(exc) 3606 3607 self.logger.debug1('Sending failure: %s', reason) 3608 3609 result = UInt32(FX_FAILURE) + String(reason) + String(DEFAULT_LANG) 3610 3611 self.send_packet(return_type, pktid, UInt32(pktid), result) 3612 3613 async def _process_open(self, packet): 3614 """Process an incoming SFTP open request""" 3615 3616 path = packet.get_string() 3617 pflags = packet.get_uint32() 3618 attrs = SFTPAttrs.decode(packet) 3619 packet.check_end() 3620 3621 self.logger.debug1('Received open request for %s, mode 0x%02x%s', 3622 path, pflags, hide_empty(attrs)) 3623 3624 result = self._server.open(path, pflags, attrs) 3625 3626 if inspect.isawaitable(result): 3627 result = await result 3628 3629 handle = self._get_next_handle() 3630 self._file_handles[handle] = result 3631 return handle 3632 3633 async def _process_close(self, packet): 3634 """Process an incoming SFTP close request""" 3635 3636 handle = packet.get_string() 3637 packet.check_end() 3638 3639 self.logger.debug1('Received close for handle %s', to_hex(handle)) 3640 3641 file_obj = self._file_handles.pop(handle, None) 3642 if file_obj: 3643 result = self._server.close(file_obj) 3644 3645 if inspect.isawaitable(result): 3646 await result 3647 3648 return 3649 3650 if self._dir_handles.pop(handle, None) is not None: 3651 return 3652 3653 raise SFTPFailure('Invalid file handle') 3654 3655 async def _process_read(self, packet): 3656 """Process an incoming SFTP read request""" 3657 3658 handle = packet.get_string() 3659 offset = packet.get_uint64() 3660 length = packet.get_uint32() 3661 packet.check_end() 3662 3663 self.logger.debug1('Received read for %s at offset %d in handle %s', 3664 plural(length, 'byte'), offset, to_hex(handle)) 3665 3666 file_obj = self._file_handles.get(handle) 3667 3668 if file_obj: 3669 result = self._server.read(file_obj, offset, length) 3670 3671 if inspect.isawaitable(result): 3672 result = await result 3673 3674 if result: 3675 return result 3676 else: 3677 raise SFTPEOFError 3678 else: 3679 raise SFTPFailure('Invalid file handle') 3680 3681 async def _process_write(self, packet): 3682 """Process an incoming SFTP write request""" 3683 3684 handle = packet.get_string() 3685 offset = packet.get_uint64() 3686 data = packet.get_string() 3687 packet.check_end() 3688 3689 self.logger.debug1('Received write for %s at offset %d in handle %s', 3690 plural(len(data), 'byte'), offset, to_hex(handle)) 3691 3692 file_obj = self._file_handles.get(handle) 3693 3694 if file_obj: 3695 result = self._server.write(file_obj, offset, data) 3696 3697 if inspect.isawaitable(result): 3698 result = await result 3699 3700 return result 3701 else: 3702 raise SFTPFailure('Invalid file handle') 3703 3704 async def _process_lstat(self, packet): 3705 """Process an incoming SFTP lstat request""" 3706 3707 path = packet.get_string() 3708 packet.check_end() 3709 3710 self.logger.debug1('Received lstat for %s', path) 3711 3712 result = self._server.lstat(path) 3713 3714 if inspect.isawaitable(result): 3715 result = await result 3716 3717 return result 3718 3719 async def _process_fstat(self, packet): 3720 """Process an incoming SFTP fstat request""" 3721 3722 handle = packet.get_string() 3723 packet.check_end() 3724 3725 self.logger.debug1('Received fstat for handle %s', to_hex(handle)) 3726 3727 file_obj = self._file_handles.get(handle) 3728 3729 if file_obj: 3730 result = self._server.fstat(file_obj) 3731 3732 if inspect.isawaitable(result): 3733 result = await result 3734 3735 return result 3736 else: 3737 raise SFTPFailure('Invalid file handle') 3738 3739 async def _process_setstat(self, packet): 3740 """Process an incoming SFTP setstat request""" 3741 3742 path = packet.get_string() 3743 attrs = SFTPAttrs.decode(packet) 3744 packet.check_end() 3745 3746 self.logger.debug1('Received setstat for %s%s', path, hide_empty(attrs)) 3747 3748 result = self._server.setstat(path, attrs) 3749 3750 if inspect.isawaitable(result): 3751 result = await result 3752 3753 return result 3754 3755 async def _process_fsetstat(self, packet): 3756 """Process an incoming SFTP fsetstat request""" 3757 3758 handle = packet.get_string() 3759 attrs = SFTPAttrs.decode(packet) 3760 packet.check_end() 3761 3762 self.logger.debug1('Received fsetstat for handle %s%s', 3763 to_hex(handle), hide_empty(attrs)) 3764 3765 file_obj = self._file_handles.get(handle) 3766 3767 if file_obj: 3768 result = self._server.fsetstat(file_obj, attrs) 3769 3770 if inspect.isawaitable(result): 3771 result = await result 3772 3773 return result 3774 else: 3775 raise SFTPFailure('Invalid file handle') 3776 3777 async def _process_opendir(self, packet): 3778 """Process an incoming SFTP opendir request""" 3779 3780 path = packet.get_string() 3781 packet.check_end() 3782 3783 self.logger.debug1('Received opendir for %s', path) 3784 3785 listdir_result = self._server.listdir(path) 3786 3787 if inspect.isawaitable(listdir_result): 3788 listdir_result = await listdir_result 3789 3790 for i, name in enumerate(listdir_result): 3791 if isinstance(name, bytes): 3792 name = SFTPName(name) 3793 listdir_result[i] = name 3794 3795 # pylint: disable=no-member 3796 filename = os.path.join(path, name.filename) 3797 attr_result = self._server.lstat(filename) 3798 3799 if inspect.isawaitable(attr_result): 3800 attr_result = await attr_result 3801 3802 if isinstance(attr_result, os.stat_result): 3803 attr_result = SFTPAttrs.from_local(attr_result) 3804 3805 # pylint: disable=attribute-defined-outside-init 3806 name.attrs = attr_result 3807 3808 if not name.longname: 3809 longname_result = self._server.format_longname(name) 3810 3811 if inspect.isawaitable(longname_result): 3812 await longname_result 3813 3814 handle = self._get_next_handle() 3815 self._dir_handles[handle] = listdir_result 3816 return handle 3817 3818 async def _process_readdir(self, packet): 3819 """Process an incoming SFTP readdir request""" 3820 3821 handle = packet.get_string() 3822 packet.check_end() 3823 3824 self.logger.debug1('Received readdir for handle %s', to_hex(handle)) 3825 3826 names = self._dir_handles.get(handle) 3827 if names: 3828 result = names[:_MAX_READDIR_NAMES] 3829 del names[:_MAX_READDIR_NAMES] 3830 return result 3831 else: 3832 raise SFTPEOFError 3833 3834 async def _process_remove(self, packet): 3835 """Process an incoming SFTP remove request""" 3836 3837 path = packet.get_string() 3838 packet.check_end() 3839 3840 self.logger.debug1('Received remove for %s', path) 3841 3842 result = self._server.remove(path) 3843 3844 if inspect.isawaitable(result): 3845 result = await result 3846 3847 return result 3848 3849 async def _process_mkdir(self, packet): 3850 """Process an incoming SFTP mkdir request""" 3851 3852 path = packet.get_string() 3853 attrs = SFTPAttrs.decode(packet) 3854 packet.check_end() 3855 3856 self.logger.debug1('Received mkdir for %s', path) 3857 3858 result = self._server.mkdir(path, attrs) 3859 3860 if inspect.isawaitable(result): 3861 result = await result 3862 3863 return result 3864 3865 async def _process_rmdir(self, packet): 3866 """Process an incoming SFTP rmdir request""" 3867 3868 path = packet.get_string() 3869 packet.check_end() 3870 3871 self.logger.debug1('Received rmdir for %s', path) 3872 3873 result = self._server.rmdir(path) 3874 3875 if inspect.isawaitable(result): 3876 result = await result 3877 3878 return result 3879 3880 async def _process_realpath(self, packet): 3881 """Process an incoming SFTP realpath request""" 3882 3883 path = packet.get_string() 3884 packet.check_end() 3885 3886 self.logger.debug1('Received realpath for %s', path) 3887 3888 result = self._server.realpath(path) 3889 3890 if inspect.isawaitable(result): 3891 result = await result 3892 3893 return [SFTPName(result)] 3894 3895 async def _process_stat(self, packet): 3896 """Process an incoming SFTP stat request""" 3897 3898 path = packet.get_string() 3899 packet.check_end() 3900 3901 self.logger.debug1('Received stat for %s', path) 3902 3903 result = self._server.stat(path) 3904 3905 if inspect.isawaitable(result): 3906 result = await result 3907 3908 return result 3909 3910 async def _process_rename(self, packet): 3911 """Process an incoming SFTP rename request""" 3912 3913 oldpath = packet.get_string() 3914 newpath = packet.get_string() 3915 packet.check_end() 3916 3917 self.logger.debug1('Received rename request from %s to %s', 3918 oldpath, newpath) 3919 3920 result = self._server.rename(oldpath, newpath) 3921 3922 if inspect.isawaitable(result): 3923 result = await result 3924 3925 return result 3926 3927 async def _process_readlink(self, packet): 3928 """Process an incoming SFTP readlink request""" 3929 3930 path = packet.get_string() 3931 packet.check_end() 3932 3933 self.logger.debug1('Received readlink for %s', path) 3934 3935 result = self._server.readlink(path) 3936 3937 if inspect.isawaitable(result): 3938 result = await result 3939 3940 return [SFTPName(result)] 3941 3942 async def _process_symlink(self, packet): 3943 """Process an incoming SFTP symlink request""" 3944 3945 if self._nonstandard_symlink: 3946 oldpath = packet.get_string() 3947 newpath = packet.get_string() 3948 else: 3949 newpath = packet.get_string() 3950 oldpath = packet.get_string() 3951 3952 packet.check_end() 3953 3954 self.logger.debug1('Received symlink request from %s to %s', 3955 oldpath, newpath) 3956 3957 result = self._server.symlink(oldpath, newpath) 3958 3959 if inspect.isawaitable(result): 3960 result = await result 3961 3962 return result 3963 3964 async def _process_posix_rename(self, packet): 3965 """Process an incoming SFTP POSIX rename request""" 3966 3967 oldpath = packet.get_string() 3968 newpath = packet.get_string() 3969 packet.check_end() 3970 3971 self.logger.debug1('Received POSIX rename request from %s to %s', 3972 oldpath, newpath) 3973 3974 result = self._server.posix_rename(oldpath, newpath) 3975 3976 if inspect.isawaitable(result): 3977 result = await result 3978 3979 return result 3980 3981 async def _process_statvfs(self, packet): 3982 """Process an incoming SFTP statvfs request""" 3983 3984 path = packet.get_string() 3985 packet.check_end() 3986 3987 self.logger.debug1('Received statvfs for %s', path) 3988 3989 result = self._server.statvfs(path) 3990 3991 if inspect.isawaitable(result): 3992 result = await result 3993 3994 return result 3995 3996 async def _process_fstatvfs(self, packet): 3997 """Process an incoming SFTP fstatvfs request""" 3998 3999 handle = packet.get_string() 4000 packet.check_end() 4001 4002 self.logger.debug1('Received fstatvfs for handle %s', to_hex(handle)) 4003 4004 file_obj = self._file_handles.get(handle) 4005 4006 if file_obj: 4007 result = self._server.fstatvfs(file_obj) 4008 4009 if inspect.isawaitable(result): 4010 result = await result 4011 4012 return result 4013 else: 4014 raise SFTPFailure('Invalid file handle') 4015 4016 async def _process_link(self, packet): 4017 """Process an incoming SFTP hard link request""" 4018 4019 oldpath = packet.get_string() 4020 newpath = packet.get_string() 4021 packet.check_end() 4022 4023 self.logger.debug1('Received hardlink request from %s to %s', 4024 oldpath, newpath) 4025 4026 result = self._server.link(oldpath, newpath) 4027 4028 if inspect.isawaitable(result): 4029 result = await result 4030 4031 return result 4032 4033 async def _process_fsync(self, packet): 4034 """Process an incoming SFTP fsync request""" 4035 4036 handle = packet.get_string() 4037 packet.check_end() 4038 4039 self.logger.debug1('Received fsync for handle %s', to_hex(handle)) 4040 4041 file_obj = self._file_handles.get(handle) 4042 4043 if file_obj: 4044 result = self._server.fsync(file_obj) 4045 4046 if inspect.isawaitable(result): 4047 result = await result 4048 4049 return result 4050 else: 4051 raise SFTPFailure('Invalid file handle') 4052 4053 _packet_handlers = { 4054 FXP_OPEN: _process_open, 4055 FXP_CLOSE: _process_close, 4056 FXP_READ: _process_read, 4057 FXP_WRITE: _process_write, 4058 FXP_LSTAT: _process_lstat, 4059 FXP_FSTAT: _process_fstat, 4060 FXP_SETSTAT: _process_setstat, 4061 FXP_FSETSTAT: _process_fsetstat, 4062 FXP_OPENDIR: _process_opendir, 4063 FXP_READDIR: _process_readdir, 4064 FXP_REMOVE: _process_remove, 4065 FXP_MKDIR: _process_mkdir, 4066 FXP_RMDIR: _process_rmdir, 4067 FXP_REALPATH: _process_realpath, 4068 FXP_STAT: _process_stat, 4069 FXP_RENAME: _process_rename, 4070 FXP_READLINK: _process_readlink, 4071 FXP_SYMLINK: _process_symlink, 4072 b'posix-rename@openssh.com': _process_posix_rename, 4073 b'statvfs@openssh.com': _process_statvfs, 4074 b'fstatvfs@openssh.com': _process_fstatvfs, 4075 b'hardlink@openssh.com': _process_link, 4076 b'fsync@openssh.com': _process_fsync 4077 } 4078 4079 async def run(self): 4080 """Run an SFTP server""" 4081 4082 try: 4083 packet = await self.recv_packet() 4084 4085 pkttype = packet.get_byte() 4086 4087 self.log_received_packet(pkttype, None, packet) 4088 4089 version = packet.get_uint32() 4090 4091 extensions = [] 4092 4093 while packet: 4094 name = packet.get_string() 4095 data = packet.get_string() 4096 extensions.append((name, data)) 4097 except PacketDecodeError as exc: 4098 await self._cleanup(SFTPBadMessage(str(exc))) 4099 return 4100 except Error as exc: 4101 await self._cleanup(exc) 4102 return 4103 4104 if pkttype != FXP_INIT: 4105 await self._cleanup(SFTPBadMessage('Expected init message')) 4106 return 4107 4108 self.logger.debug1('Received init, version=%d%s', version, 4109 ', extensions:' if extensions else '') 4110 4111 for name, data in extensions: 4112 self.logger.debug1(' %s: %s', name, data) 4113 4114 reply_version = min(version, _SFTP_VERSION) 4115 4116 self.logger.debug1('Sending version=%d%s', reply_version, 4117 ', extensions:' if self._extensions else '') 4118 4119 for name, data in self._extensions: 4120 self.logger.debug1(' %s: %s', name, data) 4121 4122 extensions = (String(name) + String(data) 4123 for name, data in self._extensions) 4124 4125 try: 4126 self.send_packet(FXP_VERSION, None, UInt32(reply_version), 4127 *extensions) 4128 except SFTPError as exc: 4129 await self._cleanup(exc) 4130 return 4131 4132 if reply_version == 3: 4133 # Check if the server has a buggy SYMLINK implementation 4134 4135 client_version = self._reader.get_extra_info('client_version', '') 4136 if any(name in client_version 4137 for name in self._nonstandard_symlink_impls): 4138 self.logger.debug1('Adjusting for non-standard symlink ' 4139 'implementation') 4140 self._nonstandard_symlink = True 4141 4142 await self.recv_packets() 4143 4144 4145class SFTPServer: 4146 """SFTP server 4147 4148 Applications should subclass this when implementing an SFTP 4149 server. The methods listed below should be implemented to 4150 provide the desired application behavior. 4151 4152 .. note:: Any method can optionally be defined as a 4153 coroutine if that method needs to perform 4154 blocking opertions to determine its result. 4155 4156 The `chan` object provided here is the :class:`SSHServerChannel` 4157 instance this SFTP server is associated with. It can be queried to 4158 determine which user the client authenticated as, environment 4159 variables set on the channel when it was opened, and key and 4160 certificate options or permissions associated with this session. 4161 4162 .. note:: In AsyncSSH 1.x, this first argument was an 4163 :class:`SSHServerConnection`, not an 4164 :class:`SSHServerChannel`. When moving to AsyncSSH 4165 2.x, subclasses of :class:`SFTPServer` which 4166 implement an __init__ method will need to be 4167 updated to account for this change, and pass this 4168 through to the parent. 4169 4170 If the `chroot` argument is specified when this object is 4171 created, the default :meth:`map_path` and :meth:`reverse_map_path` 4172 methods will enforce a virtual root directory starting in that 4173 location, limiting access to only files within that directory 4174 tree. This will also affect path names returned by the 4175 :meth:`realpath` and :meth:`readlink` methods. 4176 4177 """ 4178 4179 # The default implementation of a number of these methods don't need self 4180 # pylint: disable=no-self-use 4181 4182 def __init__(self, chan, chroot=None): 4183 # pylint: disable=unused-argument 4184 4185 self._chan = chan 4186 4187 if chroot: 4188 self._chroot = _from_local_path(os.path.realpath(chroot)) 4189 else: 4190 self._chroot = None 4191 4192 @property 4193 def channel(self): 4194 """The channel associated with this SFTP server session""" 4195 4196 return self._chan 4197 4198 @property 4199 def connection(self): 4200 """The channel associated with this SFTP server session""" 4201 4202 return self._chan.get_connection() 4203 4204 @property 4205 def env(self): 4206 """The environment associated with this SFTP server session 4207 4208 This method returns the environment set by the client 4209 when this SFTP session was opened. 4210 4211 :returns: A dictionary containing the environment variables 4212 set by the client 4213 4214 """ 4215 4216 4217 return self._chan.get_environment() 4218 4219 @property 4220 def logger(self): 4221 """A logger associated with this SFTP server""" 4222 4223 return self._chan.logger 4224 4225 def format_user(self, uid): 4226 """Return the user name associated with a uid 4227 4228 This method returns a user name string to insert into 4229 the `longname` field of an :class:`SFTPName` object. 4230 4231 By default, it calls the Python :func:`pwd.getpwuid` 4232 function if it is available, or returns the numeric 4233 uid as a string if not. If there is no uid, it returns 4234 an empty string. 4235 4236 :param uid: 4237 The uid value to look up 4238 :type uid: `int` or `None` 4239 4240 :returns: The formatted user name string 4241 4242 """ 4243 4244 if uid is not None: 4245 try: 4246 # pylint: disable=import-outside-toplevel 4247 import pwd 4248 user = pwd.getpwuid(uid).pw_name 4249 except (ImportError, KeyError): 4250 user = str(uid) 4251 else: 4252 user = '' 4253 4254 return user 4255 4256 4257 def format_group(self, gid): 4258 """Return the group name associated with a gid 4259 4260 This method returns a group name string to insert into 4261 the `longname` field of an :class:`SFTPName` object. 4262 4263 By default, it calls the Python :func:`grp.getgrgid` 4264 function if it is available, or returns the numeric 4265 gid as a string if not. If there is no gid, it returns 4266 an empty string. 4267 4268 :param gid: 4269 The gid value to look up 4270 :type gid: `int` or `None` 4271 4272 :returns: The formatted group name string 4273 4274 """ 4275 4276 if gid is not None: 4277 try: 4278 # pylint: disable=import-outside-toplevel 4279 import grp 4280 group = grp.getgrgid(gid).gr_name 4281 except (ImportError, KeyError): 4282 group = str(gid) 4283 else: 4284 group = '' 4285 4286 return group 4287 4288 4289 def format_longname(self, name): 4290 """Format the long name associated with an SFTP name 4291 4292 This method fills in the `longname` field of a 4293 :class:`SFTPName` object. By default, it generates 4294 something similar to UNIX "ls -l" output. The `filename` 4295 and `attrs` fields of the :class:`SFTPName` should 4296 already be filled in before this method is called. 4297 4298 :param name: 4299 The :class:`SFTPName` instance to format the long name for 4300 :type name: :class:`SFTPName` 4301 4302 """ 4303 4304 if name.attrs.permissions is not None: 4305 mode = stat.filemode(name.attrs.permissions) 4306 else: 4307 mode = '' 4308 4309 nlink = str(name.attrs.nlink) if name.attrs.nlink else '' 4310 4311 user = self.format_user(name.attrs.uid) 4312 group = self.format_group(name.attrs.gid) 4313 4314 size = str(name.attrs.size) if name.attrs.size is not None else '' 4315 4316 if name.attrs.mtime is not None: 4317 now = time.time() 4318 mtime = time.localtime(name.attrs.mtime) 4319 modtime = time.strftime('%b ', mtime) 4320 4321 try: 4322 modtime += time.strftime('%e', mtime) 4323 except ValueError: 4324 modtime += time.strftime('%d', mtime) 4325 4326 if now - 365*24*60*60/2 < name.attrs.mtime <= now: 4327 modtime += time.strftime(' %H:%M', mtime) 4328 else: 4329 modtime += time.strftime(' %Y', mtime) 4330 else: 4331 modtime = '' 4332 4333 detail = '{:10s} {:>4s} {:8s} {:8s} {:>8s} {:12s} '.format( 4334 mode, nlink, user, group, size, modtime) 4335 4336 name.longname = detail.encode('utf-8') + name.filename 4337 4338 def map_path(self, path): 4339 """Map the path requested by the client to a local path 4340 4341 This method can be overridden to provide a custom mapping 4342 from path names requested by the client to paths in the local 4343 filesystem. By default, it will enforce a virtual "chroot" 4344 if one was specified when this server was created. Otherwise, 4345 path names are left unchanged, with relative paths being 4346 interpreted based on the working directory of the currently 4347 running process. 4348 4349 :param path: 4350 The path name to map 4351 :type path: `bytes` 4352 4353 :returns: bytes containing the local path name to operate on 4354 4355 """ 4356 4357 if self._chroot: 4358 normpath = posixpath.normpath(posixpath.join(b'/', path)) 4359 return posixpath.join(self._chroot, normpath[1:]) 4360 else: 4361 return path 4362 4363 def reverse_map_path(self, path): 4364 """Reverse map a local path into the path reported to the client 4365 4366 This method can be overridden to provide a custom reverse 4367 mapping for the mapping provided by :meth:`map_path`. By 4368 default, it hides the portion of the local path associated 4369 with the virtual "chroot" if one was specified. 4370 4371 :param path: 4372 The local path name to reverse map 4373 :type path: `bytes` 4374 4375 :returns: bytes containing the path name to report to the client 4376 4377 """ 4378 4379 if self._chroot: 4380 if path == self._chroot: 4381 return b'/' 4382 elif path.startswith(self._chroot + b'/'): 4383 return path[len(self._chroot):] 4384 else: 4385 raise SFTPNoSuchFile('File not found') 4386 else: 4387 return path 4388 4389 def open(self, path, pflags, attrs): 4390 """Open a file to serve to a remote client 4391 4392 This method returns a file object which can be used to read 4393 and write data and get and set file attributes. 4394 4395 The possible open mode flags and their meanings are: 4396 4397 ========== ====================================================== 4398 Mode Description 4399 ========== ====================================================== 4400 FXF_READ Open the file for reading. If neither FXF_READ nor 4401 FXF_WRITE are set, this is the default. 4402 FXF_WRITE Open the file for writing. If both this and FXF_READ 4403 are set, open the file for both reading and writing. 4404 FXF_APPEND Force writes to append data to the end of the file 4405 regardless of seek position. 4406 FXF_CREAT Create the file if it doesn't exist. Without this, 4407 attempts to open a non-existent file will fail. 4408 FXF_TRUNC Truncate the file to zero length if it already exists. 4409 FXF_EXCL Return an error when trying to open a file which 4410 already exists. 4411 ========== ====================================================== 4412 4413 The attrs argument is used to set initial attributes of the 4414 file if it needs to be created. Otherwise, this argument is 4415 ignored. 4416 4417 :param path: 4418 The name of the file to open 4419 :param pflags: 4420 The access mode to use for the file (see above) 4421 :param attrs: 4422 File attributes to use if the file needs to be created 4423 :type path: `bytes` 4424 :type pflags: `int` 4425 :type attrs: :class:`SFTPAttrs` 4426 4427 :returns: A file object to use to access the file 4428 4429 :raises: :exc:`SFTPError` to return an error to the client 4430 4431 """ 4432 4433 if pflags & FXF_EXCL: 4434 mode = 'xb' 4435 elif pflags & FXF_APPEND: 4436 mode = 'ab' 4437 elif pflags & FXF_WRITE and not pflags & FXF_READ: 4438 mode = 'wb' 4439 else: 4440 mode = 'rb' 4441 4442 if pflags & FXF_READ and pflags & FXF_WRITE: 4443 mode += '+' 4444 flags = os.O_RDWR 4445 elif pflags & FXF_WRITE: 4446 flags = os.O_WRONLY 4447 else: 4448 flags = os.O_RDONLY 4449 4450 if pflags & FXF_APPEND: 4451 flags |= os.O_APPEND 4452 4453 if pflags & FXF_CREAT: 4454 flags |= os.O_CREAT 4455 4456 if pflags & FXF_TRUNC: 4457 flags |= os.O_TRUNC 4458 4459 if pflags & FXF_EXCL: 4460 flags |= os.O_EXCL 4461 4462 flags |= getattr(os, 'O_BINARY', 0) 4463 4464 perms = 0o666 if attrs.permissions is None else attrs.permissions 4465 return open(_to_local_path(self.map_path(path)), mode, buffering=0, 4466 opener=lambda path, _: os.open(path, flags, perms)) 4467 4468 def close(self, file_obj): 4469 """Close an open file or directory 4470 4471 :param file_obj: 4472 The file or directory object to close 4473 :type file_obj: file 4474 4475 :raises: :exc:`SFTPError` to return an error to the client 4476 4477 """ 4478 4479 file_obj.close() 4480 4481 def read(self, file_obj, offset, size): 4482 """Read data from an open file 4483 4484 :param file_obj: 4485 The file to read from 4486 :param offset: 4487 The offset from the beginning of the file to begin reading 4488 :param size: 4489 The number of bytes to read 4490 :type file_obj: file 4491 :type offset: `int` 4492 :type size: `int` 4493 4494 :returns: bytes read from the file 4495 4496 :raises: :exc:`SFTPError` to return an error to the client 4497 4498 """ 4499 4500 file_obj.seek(offset) 4501 return file_obj.read(size) 4502 4503 def write(self, file_obj, offset, data): 4504 """Write data to an open file 4505 4506 :param file_obj: 4507 The file to write to 4508 :param offset: 4509 The offset from the beginning of the file to begin writing 4510 :param data: 4511 The data to write to the file 4512 :type file_obj: file 4513 :type offset: `int` 4514 :type data: `bytes` 4515 4516 :returns: number of bytes written 4517 4518 :raises: :exc:`SFTPError` to return an error to the client 4519 4520 """ 4521 4522 file_obj.seek(offset) 4523 return file_obj.write(data) 4524 4525 def lstat(self, path): 4526 """Get attributes of a file, directory, or symlink 4527 4528 This method queries the attributes of a file, directory, 4529 or symlink. Unlike :meth:`stat`, this method should 4530 return the attributes of a symlink itself rather than 4531 the target of that link. 4532 4533 :param path: 4534 The path of the file, directory, or link to get attributes for 4535 :type path: `bytes` 4536 4537 :returns: An :class:`SFTPAttrs` or an os.stat_result containing 4538 the file attributes 4539 4540 :raises: :exc:`SFTPError` to return an error to the client 4541 4542 """ 4543 4544 return os.lstat(_to_local_path(self.map_path(path))) 4545 4546 def fstat(self, file_obj): 4547 """Get attributes of an open file 4548 4549 :param file_obj: 4550 The file to get attributes for 4551 :type file_obj: file 4552 4553 :returns: An :class:`SFTPAttrs` or an os.stat_result containing 4554 the file attributes 4555 4556 :raises: :exc:`SFTPError` to return an error to the client 4557 4558 """ 4559 4560 file_obj.flush() 4561 return os.fstat(file_obj.fileno()) 4562 4563 def setstat(self, path, attrs): 4564 """Set attributes of a file or directory 4565 4566 This method sets attributes of a file or directory. If 4567 the path provided is a symbolic link, the attributes 4568 should be set on the target of the link. A subset of the 4569 fields in `attrs` can be initialized and only those 4570 attributes should be changed. 4571 4572 :param path: 4573 The path of the remote file or directory to set attributes for 4574 :param attrs: 4575 File attributes to set 4576 :type path: `bytes` 4577 :type attrs: :class:`SFTPAttrs` 4578 4579 :raises: :exc:`SFTPError` to return an error to the client 4580 4581 """ 4582 4583 _setstat(_to_local_path(self.map_path(path)), attrs) 4584 4585 def fsetstat(self, file_obj, attrs): 4586 """Set attributes of an open file 4587 4588 :param file_obj: 4589 The file to set attributes for 4590 :param attrs: 4591 File attributes to set on the file 4592 :type file_obj: file 4593 :type attrs: :class:`SFTPAttrs` 4594 4595 :raises: :exc:`SFTPError` to return an error to the client 4596 4597 """ 4598 4599 file_obj.flush() 4600 4601 if sys.platform == 'win32': # pragma: no cover 4602 _setstat(file_obj.name, attrs) 4603 else: 4604 _setstat(file_obj.fileno(), attrs) 4605 4606 def listdir(self, path): 4607 """List the contents of a directory 4608 4609 :param path: 4610 The path of the directory to open 4611 :type path: `bytes` 4612 4613 :returns: A list of names of files in the directory 4614 4615 :raises: :exc:`SFTPError` to return an error to the client 4616 4617 """ 4618 4619 files = os.listdir(_to_local_path(self.map_path(path))) 4620 4621 if sys.platform == 'win32': # pragma: no cover 4622 files = [os.fsencode(f) for f in files] 4623 4624 return [b'.', b'..'] + files 4625 4626 def remove(self, path): 4627 """Remove a file or symbolic link 4628 4629 :param path: 4630 The path of the file or link to remove 4631 :type path: `bytes` 4632 4633 :raises: :exc:`SFTPError` to return an error to the client 4634 4635 """ 4636 4637 os.remove(_to_local_path(self.map_path(path))) 4638 4639 def mkdir(self, path, attrs): 4640 """Create a directory with the specified attributes 4641 4642 :param path: 4643 The path of where the new directory should be created 4644 :param attrs: 4645 The file attributes to use when creating the directory 4646 :type path: `bytes` 4647 :type attrs: :class:`SFTPAttrs` 4648 4649 :raises: :exc:`SFTPError` to return an error to the client 4650 4651 """ 4652 4653 mode = 0o777 if attrs.permissions is None else attrs.permissions 4654 os.mkdir(_to_local_path(self.map_path(path)), mode) 4655 4656 def rmdir(self, path): 4657 """Remove a directory 4658 4659 :param path: 4660 The path of the directory to remove 4661 :type path: `bytes` 4662 4663 :raises: :exc:`SFTPError` to return an error to the client 4664 4665 """ 4666 4667 os.rmdir(_to_local_path(self.map_path(path))) 4668 4669 def realpath(self, path): 4670 """Return the canonical version of a path 4671 4672 :param path: 4673 The path of the directory to canonicalize 4674 :type path: `bytes` 4675 4676 :returns: bytes containing the canonical path 4677 4678 :raises: :exc:`SFTPError` to return an error to the client 4679 4680 """ 4681 4682 path = os.path.realpath(_to_local_path(self.map_path(path))) 4683 return self.reverse_map_path(_from_local_path(path)) 4684 4685 def stat(self, path): 4686 """Get attributes of a file or directory, following symlinks 4687 4688 This method queries the attributes of a file or directory. 4689 If the path provided is a symbolic link, the returned 4690 attributes should correspond to the target of the link. 4691 4692 :param path: 4693 The path of the remote file or directory to get attributes for 4694 :type path: `bytes` 4695 4696 :returns: An :class:`SFTPAttrs` or an os.stat_result containing 4697 the file attributes 4698 4699 :raises: :exc:`SFTPError` to return an error to the client 4700 4701 """ 4702 4703 return os.stat(_to_local_path(self.map_path(path))) 4704 4705 def rename(self, oldpath, newpath): 4706 """Rename a file, directory, or link 4707 4708 This method renames a file, directory, or link. 4709 4710 .. note:: This is a request for the standard SFTP version 4711 of rename which will not overwrite the new path 4712 if it already exists. The :meth:`posix_rename` 4713 method will be called if the client requests the 4714 POSIX behavior where an existing instance of the 4715 new path is removed before the rename. 4716 4717 :param oldpath: 4718 The path of the file, directory, or link to rename 4719 :param newpath: 4720 The new name for this file, directory, or link 4721 :type oldpath: `bytes` 4722 :type newpath: `bytes` 4723 4724 :raises: :exc:`SFTPError` to return an error to the client 4725 4726 """ 4727 4728 oldpath = _to_local_path(self.map_path(oldpath)) 4729 newpath = _to_local_path(self.map_path(newpath)) 4730 4731 if os.path.exists(newpath): 4732 raise SFTPFailure('File already exists') 4733 4734 os.rename(oldpath, newpath) 4735 4736 def readlink(self, path): 4737 """Return the target of a symbolic link 4738 4739 :param path: 4740 The path of the symbolic link to follow 4741 :type path: `bytes` 4742 4743 :returns: bytes containing the target path of the link 4744 4745 :raises: :exc:`SFTPError` to return an error to the client 4746 4747 """ 4748 4749 path = os.readlink(_to_local_path(self.map_path(path))) 4750 return self.reverse_map_path(_from_local_path(path)) 4751 4752 def symlink(self, oldpath, newpath): 4753 """Create a symbolic link 4754 4755 :param oldpath: 4756 The path the link should point to 4757 :param newpath: 4758 The path of where to create the symbolic link 4759 :type oldpath: `bytes` 4760 :type newpath: `bytes` 4761 4762 :raises: :exc:`SFTPError` to return an error to the client 4763 4764 """ 4765 4766 if posixpath.isabs(oldpath): 4767 oldpath = self.map_path(oldpath) 4768 else: 4769 newdir = posixpath.dirname(newpath) 4770 abspath1 = self.map_path(posixpath.join(newdir, oldpath)) 4771 4772 mapped_newdir = self.map_path(newdir) 4773 abspath2 = os.path.join(mapped_newdir, oldpath) 4774 4775 # Make sure the symlink doesn't point outside the chroot 4776 if os.path.realpath(abspath1) != os.path.realpath(abspath2): 4777 oldpath = os.path.relpath(abspath1, start=mapped_newdir) 4778 4779 newpath = self.map_path(newpath) 4780 4781 os.symlink(_to_local_path(oldpath), _to_local_path(newpath)) 4782 4783 def posix_rename(self, oldpath, newpath): 4784 """Rename a file, directory, or link with POSIX semantics 4785 4786 This method renames a file, directory, or link, removing 4787 the prior instance of new path if it previously existed. 4788 4789 :param oldpath: 4790 The path of the file, directory, or link to rename 4791 :param newpath: 4792 The new name for this file, directory, or link 4793 :type oldpath: `bytes` 4794 :type newpath: `bytes` 4795 4796 :raises: :exc:`SFTPError` to return an error to the client 4797 4798 """ 4799 4800 oldpath = _to_local_path(self.map_path(oldpath)) 4801 newpath = _to_local_path(self.map_path(newpath)) 4802 4803 os.replace(oldpath, newpath) 4804 4805 def statvfs(self, path): 4806 """Get attributes of the file system containing a file 4807 4808 :param path: 4809 The path of the file system to get attributes for 4810 :type path: `bytes` 4811 4812 :returns: An :class:`SFTPVFSAttrs` or an os.statvfs_result 4813 containing the file system attributes 4814 4815 :raises: :exc:`SFTPError` to return an error to the client 4816 4817 """ 4818 4819 try: 4820 return os.statvfs(_to_local_path(self.map_path(path))) 4821 except AttributeError: # pragma: no cover 4822 raise SFTPOpUnsupported('statvfs not supported') from None 4823 4824 def fstatvfs(self, file_obj): 4825 """Return attributes of the file system containing an open file 4826 4827 :param file_obj: 4828 The open file to get file system attributes for 4829 :type file_obj: file 4830 4831 :returns: An :class:`SFTPVFSAttrs` or an os.statvfs_result 4832 containing the file system attributes 4833 4834 :raises: :exc:`SFTPError` to return an error to the client 4835 4836 """ 4837 4838 try: 4839 return os.statvfs(file_obj.fileno()) 4840 except AttributeError: # pragma: no cover 4841 raise SFTPOpUnsupported('fstatvfs not supported') from None 4842 4843 def link(self, oldpath, newpath): 4844 """Create a hard link 4845 4846 :param oldpath: 4847 The path of the file the hard link should point to 4848 :param newpath: 4849 The path of where to create the hard link 4850 :type oldpath: `bytes` 4851 :type newpath: `bytes` 4852 4853 :raises: :exc:`SFTPError` to return an error to the client 4854 4855 """ 4856 4857 oldpath = _to_local_path(self.map_path(oldpath)) 4858 newpath = _to_local_path(self.map_path(newpath)) 4859 4860 os.link(oldpath, newpath) 4861 4862 def fsync(self, file_obj): 4863 """Force file data to be written to disk 4864 4865 :param file_obj: 4866 The open file containing the data to flush to disk 4867 :type file_obj: file 4868 4869 :raises: :exc:`SFTPError` to return an error to the client 4870 4871 """ 4872 4873 os.fsync(file_obj.fileno()) 4874 4875 def exit(self): 4876 """Shut down this SFTP server""" 4877 4878 4879class SFTPServerFile: 4880 """A wrapper around SFTPServer used to access files it manages""" 4881 4882 def __init__(self, server): 4883 self._server = server 4884 self._file_obj = None 4885 4886 @classmethod 4887 def basename(cls, path): 4888 """Return the final component of a POSIX-style path""" 4889 4890 return posixpath.basename(path) 4891 4892 async def stat(self, path): 4893 """Get attributes of a file""" 4894 4895 attrs = self._server.stat(path) 4896 4897 if inspect.isawaitable(attrs): 4898 attrs = await attrs 4899 4900 if isinstance(attrs, os.stat_result): 4901 attrs = SFTPAttrs.from_local(attrs) 4902 4903 return attrs 4904 4905 async def setstat(self, path, attrs): 4906 """Set attributes of a file or directory""" 4907 4908 result = self._server.setstat(path, attrs) 4909 4910 if inspect.isawaitable(result): 4911 attrs = await result 4912 4913 async def _mode(self, path): 4914 """Return the file mode of a path, or 0 if it can't be accessed""" 4915 4916 try: 4917 return (await self.stat(path)).permissions 4918 except OSError as exc: 4919 if exc.errno in (errno.ENOENT, errno.EACCES): 4920 return 0 4921 else: 4922 raise 4923 except (SFTPNoSuchFile, SFTPPermissionDenied): 4924 return 0 4925 4926 async def exists(self, path): 4927 """Return if a path exists""" 4928 4929 return (await self._mode(path)) != 0 4930 4931 async def isdir(self, path): 4932 """Return if the path refers to a directory""" 4933 4934 return stat.S_ISDIR((await self._mode(path))) 4935 4936 async def mkdir(self, path): 4937 """Create a directory""" 4938 4939 result = self._server.mkdir(path, SFTPAttrs()) 4940 4941 if inspect.isawaitable(result): 4942 await result 4943 4944 async def listdir(self, path): 4945 """List the contents of a directory""" 4946 4947 files = self._server.listdir(path) 4948 4949 if inspect.isawaitable(files): 4950 files = await files 4951 4952 return files 4953 4954 async def open(self, path, mode='rb'): 4955 """Open a file""" 4956 4957 pflags, _ = _mode_to_pflags(mode) 4958 file_obj = self._server.open(path, pflags, SFTPAttrs()) 4959 4960 if inspect.isawaitable(file_obj): 4961 file_obj = await file_obj 4962 4963 self._file_obj = file_obj 4964 return self 4965 4966 async def read(self, size, offset): 4967 """Read bytes from the file""" 4968 4969 data = self._server.read(self._file_obj, offset, size) 4970 4971 if inspect.isawaitable(data): 4972 data = await data 4973 4974 return data 4975 4976 async def write(self, data, offset): 4977 """Write bytes to the file""" 4978 4979 size = self._server.write(self._file_obj, offset, data) 4980 4981 if inspect.isawaitable(size): 4982 size = await size 4983 4984 return size 4985 4986 async def close(self): 4987 """Close a file managed by the associated SFTPServer""" 4988 4989 result = self._server.close(self._file_obj) 4990 4991 if inspect.isawaitable(result): 4992 await result 4993 4994 4995async def start_sftp_client(conn, loop, reader, writer, 4996 path_encoding, path_errors): 4997 """Start an SFTP client""" 4998 4999 handler = SFTPClientHandler(loop, reader, writer) 5000 5001 handler.logger.info('Starting SFTP client') 5002 5003 await handler.start() 5004 5005 conn.create_task(handler.recv_packets(), handler.logger) 5006 5007 return SFTPClient(handler, path_encoding, path_errors) 5008 5009 5010def run_sftp_server(sftp_server, reader, writer): 5011 """Return a handler for an SFTP server session""" 5012 5013 handler = SFTPServerHandler(sftp_server, reader, writer) 5014 5015 handler.logger.info('Starting SFTP server') 5016 5017 return handler.run() 5018