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