# Copyright (C) 2003-2007  Robey Pointer <robeypointer@gmail.com>
#
# This file is part of Paramiko.
#
# Paramiko is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 of the License, or (at your option)
# any later version.
#
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.


from binascii import hexlify
import errno
import os
import stat
import threading
import time
import weakref
from paramiko import util
from paramiko.channel import Channel
from paramiko.message import Message
from paramiko.common import INFO, DEBUG, o777
from paramiko.py3compat import bytestring, b, u, long
from paramiko.sftp import (
    BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READDIR, CMD_NAME,
    CMD_CLOSE, SFTP_FLAG_READ, SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
    SFTP_FLAG_TRUNC, SFTP_FLAG_APPEND, SFTP_FLAG_EXCL, CMD_OPEN, CMD_REMOVE,
    CMD_RENAME, CMD_MKDIR, CMD_RMDIR, CMD_STAT, CMD_ATTRS, CMD_LSTAT,
    CMD_SYMLINK, CMD_SETSTAT, CMD_READLINK, CMD_REALPATH, CMD_STATUS,
    CMD_EXTENDED, SFTP_OK, SFTP_EOF, SFTP_NO_SUCH_FILE, SFTP_PERMISSION_DENIED,
)

from paramiko.sftp_attr import SFTPAttributes
from paramiko.ssh_exception import SSHException
from paramiko.sftp_file import SFTPFile
from paramiko.util import ClosingContextManager


def _to_unicode(s):
    """
    decode a string as ascii or utf8 if possible (as required by the sftp
    protocol).  if neither works, just return a byte string because the server
    probably doesn't know the filename's encoding.
    """
    try:
        return s.encode('ascii')
    except (UnicodeError, AttributeError):
        try:
            return s.decode('utf-8')
        except UnicodeError:
            return s


b_slash = b'/'


class SFTPClient(BaseSFTP, ClosingContextManager):
    """
    SFTP client object.

    Used to open an SFTP session across an open SSH `.Transport` and perform
    remote file operations.

    Instances of this class may be used as context managers.
    """
    def __init__(self, sock):
        """
        Create an SFTP client from an existing `.Channel`.  The channel
        should already have requested the ``"sftp"`` subsystem.

        An alternate way to create an SFTP client context is by using
        `from_transport`.

        :param .Channel sock: an open `.Channel` using the ``"sftp"`` subsystem

        :raises:
            `.SSHException` -- if there's an exception while negotiating sftp
        """
        BaseSFTP.__init__(self)
        self.sock = sock
        self.ultra_debug = False
        self.request_number = 1
        # lock for request_number
        self._lock = threading.Lock()
        self._cwd = None
        # request # -> SFTPFile
        self._expecting = weakref.WeakValueDictionary()
        if type(sock) is Channel:
            # override default logger
            transport = self.sock.get_transport()
            self.logger = util.get_logger(
                transport.get_log_channel() + '.sftp')
            self.ultra_debug = transport.get_hexdump()
        try:
            server_version = self._send_version()
        except EOFError:
            raise SSHException('EOF during negotiation')
        self._log(
            INFO,
            'Opened sftp connection (server version {})'.format(server_version)
        )

    @classmethod
    def from_transport(cls, t, window_size=None, max_packet_size=None):
        """
        Create an SFTP client channel from an open `.Transport`.

        Setting the window and packet sizes might affect the transfer speed.
        The default settings in the `.Transport` class are the same as in
        OpenSSH and should work adequately for both files transfers and
        interactive sessions.

        :param .Transport t: an open `.Transport` which is already
            authenticated
        :param int window_size:
            optional window size for the `.SFTPClient` session.
        :param int max_packet_size:
            optional max packet size for the `.SFTPClient` session..

        :return:
            a new `.SFTPClient` object, referring to an sftp session (channel)
            across the transport

        .. versionchanged:: 1.15
            Added the ``window_size`` and ``max_packet_size`` arguments.
        """
        chan = t.open_session(window_size=window_size,
                              max_packet_size=max_packet_size)
        if chan is None:
            return None
        chan.invoke_subsystem('sftp')
        return cls(chan)

    def _log(self, level, msg, *args):
        if isinstance(msg, list):
            for m in msg:
                self._log(level, m, *args)
        else:
            # NOTE: these bits MUST continue using %-style format junk because
            # logging.Logger.log() explicitly requires it. Grump.
            # escape '%' in msg (they could come from file or directory names)
            # before logging
            msg = msg.replace('%', '%%')
            super(SFTPClient, self)._log(
                level,
                "[chan %s] " + msg, *([self.sock.get_name()] + list(args)))

    def close(self):
        """
        Close the SFTP session and its underlying channel.

        .. versionadded:: 1.4
        """
        self._log(INFO, 'sftp session closed.')
        self.sock.close()

    def get_channel(self):
        """
        Return the underlying `.Channel` object for this SFTP session.  This
        might be useful for doing things like setting a timeout on the channel.

        .. versionadded:: 1.7.1
        """
        return self.sock

    def listdir(self, path='.'):
        """
        Return a list containing the names of the entries in the given
        ``path``.

        The list is in arbitrary order.  It does not include the special
        entries ``'.'`` and ``'..'`` even if they are present in the folder.
        This method is meant to mirror ``os.listdir`` as closely as possible.
        For a list of full `.SFTPAttributes` objects, see `listdir_attr`.

        :param str path: path to list (defaults to ``'.'``)
        """
        return [f.filename for f in self.listdir_attr(path)]

    def listdir_attr(self, path='.'):
        """
        Return a list containing `.SFTPAttributes` objects corresponding to
        files in the given ``path``.  The list is in arbitrary order.  It does
        not include the special entries ``'.'`` and ``'..'`` even if they are
        present in the folder.

        The returned `.SFTPAttributes` objects will each have an additional
        field: ``longname``, which may contain a formatted string of the file's
        attributes, in unix format.  The content of this string will probably
        depend on the SFTP server implementation.

        :param str path: path to list (defaults to ``'.'``)
        :return: list of `.SFTPAttributes` objects

        .. versionadded:: 1.2
        """
        path = self._adjust_cwd(path)
        self._log(DEBUG, 'listdir({!r})'.format(path))
        t, msg = self._request(CMD_OPENDIR, path)
        if t != CMD_HANDLE:
            raise SFTPError('Expected handle')
        handle = msg.get_binary()
        filelist = []
        while True:
            try:
                t, msg = self._request(CMD_READDIR, handle)
            except EOFError:
                # done with handle
                break
            if t != CMD_NAME:
                raise SFTPError('Expected name response')
            count = msg.get_int()
            for i in range(count):
                filename = msg.get_text()
                longname = msg.get_text()
                attr = SFTPAttributes._from_msg(msg, filename, longname)
                if (filename != '.') and (filename != '..'):
                    filelist.append(attr)
        self._request(CMD_CLOSE, handle)
        return filelist

    def listdir_iter(self, path='.', read_aheads=50):
        """
        Generator version of `.listdir_attr`.

        See the API docs for `.listdir_attr` for overall details.

        This function adds one more kwarg on top of `.listdir_attr`:
        ``read_aheads``, an integer controlling how many
        ``SSH_FXP_READDIR`` requests are made to the server. The default of 50
        should suffice for most file listings as each request/response cycle
        may contain multiple files (dependent on server implementation.)

        .. versionadded:: 1.15
        """
        path = self._adjust_cwd(path)
        self._log(DEBUG, 'listdir({!r})'.format(path))
        t, msg = self._request(CMD_OPENDIR, path)

        if t != CMD_HANDLE:
            raise SFTPError('Expected handle')

        handle = msg.get_string()

        nums = list()
        while True:
            try:
                # Send out a bunch of readdir requests so that we can read the
                # responses later on Section 6.7 of the SSH file transfer RFC
                # explains this
                # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
                for i in range(read_aheads):
                    num = self._async_request(type(None), CMD_READDIR, handle)
                    nums.append(num)


                # For each of our sent requests
                # Read and parse the corresponding packets
                # If we're at the end of our queued requests, then fire off
                # some more requests
                # Exit the loop when we've reached the end of the directory
                # handle
                for num in nums:
                    t, pkt_data = self._read_packet()
                    msg = Message(pkt_data)
                    new_num = msg.get_int()
                    if num == new_num:
                        if t == CMD_STATUS:
                            self._convert_status(msg)
                    count = msg.get_int()
                    for i in range(count):
                        filename = msg.get_text()
                        longname = msg.get_text()
                        attr = SFTPAttributes._from_msg(
                            msg, filename, longname)
                        if (filename != '.') and (filename != '..'):
                            yield attr

                # If we've hit the end of our queued requests, reset nums.
                nums = list()

            except EOFError:
                self._request(CMD_CLOSE, handle)
                return


    def open(self, filename, mode='r', bufsize=-1):
        """
        Open a file on the remote server.  The arguments are the same as for