#
# Small ftp server for ESP8266 ans ESP32 Micropython
#
# Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky
#
# The server accepts passive mode only.
# It runs in a separated thread
#
# Start the server with:
#   from ftp import ftpserver
#   u = ftpserver()
#   u.start_thread()
#
# Copyright (c) 2016 Christopher Popp (initial ftp server framework)
# Copyright (c) 2016 Robert Hammelrath (putting the pieces together and a few extensions)
# Copyright (c) 2017 Mauro Riva (lemariva.com) (ftp class, dbg option and threading)
# Distributed under MIT License
#
import socket
import network
import uos
import gc
import _thread

DEBUG = False

def dbg(msg, msg_=""):
    global DEBUG
    if DEBUG:
        print (str(msg) + str(msg_))

class ftpserver():
    def send_list_data(self, path, dataclient, full):
        try: # whether path is a directory name
            for fname in sorted(uos.listdir(path), key = str.lower):
                dataclient.sendall(self.make_description(path, fname, full))
        except: # path may be a file name or pattern
            pattern = path.split("/")[-1]
            path = path[:-(len(pattern) + 1)]
            if path == "": path = "/"
            for fname in sorted(uos.listdir(path), key = str.lower):
                if fncmp(fname, pattern) == True:
                    dataclient.sendall(self.make_description(path, fname, full))

    def make_description(self, path, fname, full):
        if full:
            stat = uos.stat(self.get_absolute_path(path,fname))
            file_permissions = "drwxr-xr-x" if (stat[0] & 0o170000 == 0o040000) else "-rw-r--r--"
            file_size = stat[6]
            description = "{}    1 owner group {:>10} Jan 1 2000 {}\r\n".format(
                    file_permissions, file_size, fname)
        else:
            description = fname + "\r\n"
        return description

    def send_file_data(self, path, dataclient):
        with open(path, "r") as file:
            chunk = file.read(512)
            while len(chunk) > 0:
                dataclient.sendall(chunk)
                chunk = file.read(512)

    def save_file_data(self, path, dataclient):
        with open(path, "w") as file:
            chunk = dataclient.recv(512)
            while len(chunk) > 0:
                file.write(chunk)
                chunk = dataclient.recv(512)

    def get_absolute_path(self, cwd, payload):
        # Just a few special cases "..", "." and ""
        # If payload start's with /, set cwd to /
        # and consider the remainder a relative path
        if payload.startswith('/'):
            cwd = "/"
        for token in payload.split("/"):
            if token == '..':
                if cwd != '/':
                    cwd = '/'.join(cwd.split('/')[:-1])
                    if cwd == '':
                        cwd = '/'
            elif token != '.' and token != '':
                if cwd == '/':
                    cwd += token
                else:
                    cwd = cwd + '/' + token
        return cwd

    # compare fname against pattern. Pattern may contain
    # wildcards ? and *.
    def fncmp(fname, pattern):
        pi = 0
        si = 0
        while pi < len(pattern) and si < len(fname):
            if (fname[si] == pattern[pi]) or (pattern[pi] == '?'):
                si += 1
                pi += 1
            else:
                if pattern[pi] == '*': # recurse
                    if (pi + 1) == len(pattern):
                        return True
                    while si < len(fname):
                        if fncmp(fname[si:], pattern[pi+1:]) == True:
                            return True
                        else:
                            si += 1
                    return False
                else:
                    return False
        if pi == len(pattern.rstrip("*"))  and si == len(fname):
            return True
        else:
            return False

    def thread_ftp(self, arg):
        self.start()
        dbg('Thread ftp started')

    def start_thread(self):
        dbg("starting ftp server on separated thread\n")
        _thread.start_new_thread(self.thread_ftp, ('',))

    def start(self):

        DATA_PORT = 13333

        self.ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        self.ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        self.ftpsocket.bind(socket.getaddrinfo("0.0.0.0", 21)[0][4])
        self.datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4])

        self.ftpsocket.listen(1)
        self.ftpsocket.settimeout(None)
        self.datasocket.listen(1)
        self.datasocket.settimeout(None)

        msg_250_OK = '250 OK\r\n'
        msg_550_fail = '550 Failed\r\n'
        self.wlan = network.WLAN(network.AP_IF)
        if self.wlan.active():
            ifconfig = self.wlan.ifconfig()
        else:
            self.wlan = network.WLAN(network.STA_IF)
            if self.wlan.active():
                ifconfig = self.wlan.ifconfig()
            else:
                dbg("No active connection")
                return
        addr = ifconfig[0]
        print("FTP Server started on ", addr)
        try:
            dataclient = None
            fromname = None
            do_run = True
            while do_run:
                cl, remote_addr = self.ftpsocket.accept()
                cl.settimeout(300)
                cwd = '/'
                try:
                    # dbg("FTP connection from:", remote_addr)
                    cl.sendall("220 Hello, this is the ESP8266/ESP32.\r\n")
                    while True:
                        gc.collect()
                        data = cl.readline().decode("utf-8").rstrip("\r\n")
                        if len(data) <= 0:
                            dbg("Client disappeared")
                            do_run = False
                            break

                        command = data.split(" ")[0].upper()
                        payload = data[len(command):].lstrip()

                        path = self.get_absolute_path(cwd, payload)

                        dbg("Command={}, Payload={}".format(command, payload))

                        if command == "USER":
                            cl.sendall("230 Logged in.\r\n")
                        elif command == "SYST":
                            cl.sendall("215 UNIX Type: L8\r\n")
                        elif command == "NOOP":
                            cl.sendall("200 OK\r\n")
                        elif command == "FEAT":
                            cl.sendall("211 no-features\r\n")
                        elif command == "PWD":
                            cl.sendall('257 "{}"\r\n'.format(cwd))
                        elif command == "CWD":
                            try:
                                files = uos.listdir(path)
                                cwd = path
                                cl.sendall(msg_250_OK)
                            except:
                                cl.sendall(msg_550_fail)
                        elif command == "CDUP":
                            cwd = self.get_absolute_path(cwd, "..")
                            cl.sendall(msg_250_OK)
                        elif command == "TYPE":
                            # probably should switch between binary and not
                            cl.sendall('200 Transfer mode set\r\n')
                        elif command == "SIZE":
                            try:
                                size = uos.stat(path)[6]
                                cl.sendall('213 {}\r\n'.format(size))
                            except:
                                cl.sendall(msg_550_fail)
                        elif command == "QUIT":
                            cl.sendall('221 Bye.\r\n')
                            do_run = False
                            break
                        elif command == "PASV":
                            cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'.format(
                                addr.replace('.',','), DATA_PORT>>8, DATA_PORT%256))
                            dataclient, data_addr = self.datasocket.accept()
                            dbg("FTP Data connection from:", data_addr)
                        elif command == "LIST" or command == "NLST":
                            if not payload.startswith("-"):
                                place = path
                            else:
                                place = cwd
                            try:
                                self.send_list_data(place, dataclient, command == "LIST" or payload == "-l")
                                cl.sendall("150 Here comes the directory listing.\r\n")
                                cl.sendall("226 Listed.\r\n")
                            except:
                                cl.sendall(msg_550_fail)
                            if dataclient is not None:
                                dataclient.close()
                                dataclient = None
                        elif command == "RETR":
                            try:
                                self.send_file_data(path, dataclient)
                                cl.sendall("150 Opening data connection.\r\n")
                                cl.sendall("226 Transfer complete.\r\n")
                            except:
                                cl.sendall(msg_550_fail)
                            if dataclient is not None:
                                dataclient.close()
                                dataclient = None
                        elif command == "STOR":
                            try:
                                cl.sendall("150 Ok to send data.\r\n")
                                self.save_file_data(path, dataclient)
                                cl.sendall("226 Transfer complete.\r\n")
                            except:
                                cl.sendall(msg_550_fail)
                            if dataclient is not None:
                                dataclient.close()
                                dataclient = None
                        elif command == "DELE":
                            try:
                                uos.remove(path)
                                cl.sendall(msg_250_OK)
                            except:
                                cl.sendall(msg_550_fail)
                        elif command == "RMD":
                            try:
                                uos.rmdir(path)
                                cl.sendall(msg_250_OK)
                            except:
                                cl.sendall(msg_550_fail)
                        elif command == "MKD":
                            try:
                                uos.mkdir(path)
                                cl.sendall(msg_250_OK)
                            except:
                                cl.sendall(msg_550_fail)
                        elif command == "RNFR":
                                fromname = path
                                cl.sendall("350 Rename from\r\n")
                        elif command == "RNTO":
                                if fromname is not None:
                                    try:
                                        uos.rename(fromname, path)
                                        cl.sendall(msg_250_OK)
                                    except:
                                        cl.sendall(msg_550_fail)
                                else:
                                    cl.sendall(msg_550_fail)
                                fromname = None
                        elif command == "STAT":
                            if payload == "":
                                cl.sendall("211-Connected to ({})\r\n"
                                           "    Data address ({})\r\n"
                                           "211 TYPE: Binary STRU: File MODE: Stream\r\n".format(
                                           remote_addr[0], addr))
                            else:
                                cl.sendall("213-Directory listing:\r\n")
                                self.send_list_data(path, cl, True)
                                cl.sendall("213 Done.\r\n")
                        else:
                            cl.sendall("502 Unsupported command.\r\n")
                            dbg("Unsupported command {} with payload {}".format(command, payload))
                except Exception as err:
                    dbg(err)

                finally:
                    cl.close()
                    cl = None
        finally:
            self.datasocket.close()
            self.ftpsocket.close()
            if dataclient is not None:
                dataclient.close()