예제 #1
0
    def open_ctrl_conn(self) -> bool:
        '''
        Open control connection.

        Return True if succeeded.
        '''

        if self.ping():
            if self.cli_mode:
                op = input(
                    'Already connected. Close and establish a new connection? (y/N): ',
                )
                if op.lower() != 'y':
                    return True
            else:
                return True
            self.close_ctrl_conn()

        self.ctrl_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.ctrl_conn.settimeout(self.ctrl_timeout_duration)
        err = self.ctrl_conn.connect_ex((server_host, server_port))
        if err:
            log('error', f'Connection failed, error: {err}')
            self.close_ctrl_conn()
            return False
        elif not self.check_resp(220)[0]:
            self.close_ctrl_conn()
            return False
        else:
            log('info', 'Connected to server.')
            return True
예제 #2
0
    def open_ctrl_conn(self) -> None:
        '''
        Open control connection.
        '''

        self.ctrl_conn, self.client_addr = self.ctrl_sock.accept()
        self.ctrl_conn.settimeout(self.ctrl_timeout_duration)
        log('info', f'Accept connection: {self.client_addr}')
예제 #3
0
    def close_ctrl_conn(self) -> None:
        '''
        Close control connection.
        '''

        if self.ctrl_conn:
            self.ctrl_conn.close()
            self.ctrl_conn = None
            if self.cli_mode:
                log('debug', 'Connection closed.')
예제 #4
0
    def open_data_conn(self) -> None:
        '''
        Open data connection.
        '''

        if self.data_conn:
            self.close_data_conn()
        self.data_conn, self.data_addr = self.data_sock.accept()
        self.data_conn.settimeout(self.data_timeout_duration)
        log('info', f'Data connection opened: {self.data_addr}')
        self.send_status(225)
예제 #5
0
        def _print_info(info: Tuple[str, str, str, str, str, str]) -> None:
            '''
            Print information of a file or directory.

            :param info: (file_name, file_size, file_type, mod_time, perms, owner)
            '''

            try:
                file_name, file_size, file_type, mod_time, perms, owner = info
                print('{:20} | {:>10} | {:4} | {:19} | {:10} | {:3}'.format(
                    file_name, file_size, file_type, mod_time, perms, owner))
            except ValueError as e:
                log('error', f'Invalid response: {info}, error: {e}')
예제 #6
0
    def rmdir(self, path: str, recursive: bool = False) -> None:
        '''
        Remove a directory.

        :param path: server path to the directory
        :param recursive: remove recursively if True
        '''

        src_path = self.get_server_path(path)
        log('debug', f'Removing directory: {src_path}')
        if not is_safe_path(src_path, self.server_dir):
            self.send_status(553)
            return
        try:
            if os.path.isdir(src_path):
                if recursive:
                    shutil.rmtree(src_path)
                else:
                    os.rmdir(src_path)
                log('info', f'Removed directory: {src_path}')
                self.send_status(250)
            else:
                os.remove(src_path)
                log('info', f'Deleted file: {src_path}')
        except OSError:
            log('warn', f'Failed to remove directory: {src_path}')
            self.send_status(550)
예제 #7
0
    def open_ctrl_sock(self) -> None:
        '''
        Open control socket.
        '''

        if self.ctrl_sock:
            self.close_ctrl_sock()
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((listen_host, listen_port))
        s.listen(self.max_allowed_conn)
        self.ctrl_sock = s
        self.ctrl_sock_name = s.getsockname()
        log('info', f'Server started, listening at {self.ctrl_sock_name}')
예제 #8
0
    def open_data_sock(self) -> None:
        '''
        Open data socket.
        '''

        if self.data_sock:
            self.close_data_sock()
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.settimeout(self.data_timeout_duration)
        s.bind((listen_host, 0))
        s.listen(self.max_allowed_conn)
        self.data_sock = s
        self.data_sock_name = s.getsockname()
        log('info', f'Data server started, listening at {self.data_sock_name}')
        self.send_status(227)
예제 #9
0
def main() -> None:
    print('Welcome to Naive-FTP server! Press q to exit.')

    listener = server_listener()
    listener.start()

    try:
        while True:
            if input().lower() == 'q':
                print('Bye!')
                break
    except KeyboardInterrupt:
        print('\nInterrupted.')
    finally:
        listener.close()
        log('info', 'Server stopped.')
예제 #10
0
    def rmdir(self, path: str, recursive: bool = False) -> bool:
        '''
        Remove a directory.

        :param path: server path to the directory
        :param recursive: remove recursively if True
        '''

        if not self.ping():
            log('info', 'Please connect to server first.')
            return False

        op = 'RMDA' if recursive else 'RMD'
        self.ctrl_conn.sendall(f'{op} {path}\r\n'.encode('utf-8'))
        expected, _, resp_msg = self.check_resp(250)
        log('info' if expected else 'warn', resp_msg)
        return expected
예제 #11
0
    def send_status(self, status_code: int, *args) -> None:
        '''
        Send a response based on status code.

        :param status_code: status code
        :param *args: optional arguments
        '''

        def _parsed_addr(addr: Tuple[str, int]) -> str:
            '''
            Return a parsed host address for status code 227.

            Format: 'h1,h2,h3,h4,p1,p2'

            :param addr: the address for data connection
            '''

            try:
                return '{host},{p1},{p2}'.format(
                    host=','.join(addr[0].split('.')),
                    p1=addr[1] >> 8 & 0xFF,
                    p2=addr[1] & 0xFF,
                )
            except TypeError:
                return ''

        status_dict = {
            150: '150 File status okay; about to open data connection.\r\n',
            220: '220 Service ready for new user.\r\n',
            221: '221 Service closing control connection.\r\n',
            225: '225 Data connection open; no transfer in progress.\r\n',
            226: '226 Closing data connection. Requested file action successful.\r\n',
            227: '227 Entering Passive Mode {}.\r\n'.format(_parsed_addr(self.data_sock_name)),
            250: '250 Requested file action okay, completed.\r\n',
            257: '257 {}\r\n'.format(args[0] if len(args) else None),
            450: '450 Requested file action not taken.\r\n',
            501: '501 Syntax error in parameters or arguments.\r\n',
            550: '550 Requested action not taken. File unavailable.\r\n',
            553: '553 Requested action not taken. File name not allowed.\r\n',
        }

        status = status_dict.get(status_code)
        if status:
            self.ctrl_conn.sendall(status.encode('utf-8'))
        else:
            log('error', f'Invalid status code: {status_code}')
예제 #12
0
    def router(self, raw_cmd: str) -> None:
        '''
        Route to the associated method based on user command.

        :param raw_cmd: raw user command
        '''

        method_dict = {
            'HELP': self.help,
            'OPEN': self.open,
            'QUIT': self.close,
            'EXIT': self.close,  # alias
            'LIST': self.ls,
            'LS': self.ls,  # alias
            'RETR': self.retrieve,
            'GET': self.retrieve,  # alias
            'STOR': self.store,
            'PUT': self.store,  # alias
            'DELE': self.delete,
            'DEL': self.delete,  # alias
            'RM': self.delete,  # alias
            'CWD': self.cwd,
            'CD': self.cwd,  # alias
            'PWD': self.pwd,
            'MKD': self.mkdir,
            'MKDI': self.mkdir,  # alias
            'RMD': self.rmdir,
            'RMDI': self.rmdir,  # alias
            'RMDA': self.rmdir_all,
        }

        try:
            cmd = raw_cmd.split(None, 1)
            cmd_len = len(cmd)
            op = cmd[0]
            method = method_dict.get(op[:4].upper())
            if method:
                if cmd_len == 1:
                    method()
                else:
                    method(cmd[1])
            else:
                log('info', f'Invalid operation: {raw_cmd}')
        except TypeError as e:
            log('info', f'Invalid operation: {raw_cmd}, error: {e}')
예제 #13
0
    def pwd(self) -> str:
        '''
        Print working directory.

        Return current working directory.
        '''

        if not self.ping():
            log('info', 'Please connect to server first.')
            return None

        self.ctrl_conn.sendall(f'PWD\r\n'.encode('utf-8'))
        expected, _, resp_msg = self.check_resp(257)
        if not expected:
            log('warn', resp_msg)
            return None
        else:
            print(resp_msg)
            return resp_msg
예제 #14
0
    def run(self) -> None:
        '''
        Main function for client.
        '''

        while True:
            try:
                raw_cmd = input('> ')
                if raw_cmd:
                    self.router(raw_cmd)
            except socket.timeout:
                if self.cli_mode:
                    log('debug', f'Connection timeout.')
                self.close_ctrl_conn()
            except socket.error:
                if self.cli_mode:
                    log('debug', f'Connection closed.')
                self.close_ctrl_conn()
            except KeyboardInterrupt:
                print('\nInterrupted.')
                self.close()
                break
            except Exception as e:
                log('error', f'Unexpected exception: {e}')
                self.close()
                raise
예제 #15
0
    def open_data_conn(self) -> None:
        '''
        Open data connection.
        '''

        self.data_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.data_conn.settimeout(self.data_timeout_duration)

        # Gets data_addr
        expected, _, resp_msg = self.check_resp(227)
        if not expected:  # not under passive mode
            self.close_data_conn()
            return
        addr = re.search(
            r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)',
            resp_msg,
        )
        if not addr:  # invalid response
            log('error', f'Invalid response: {resp_msg}')
            self.close_data_conn()
            return
        self.data_addr = (
            '.'.join(addr.group(1, 2, 3, 4)),
            (int(addr.group(5)) << 8) + int(addr.group(6)),
        )
        err = self.data_conn.connect_ex(self.data_addr)
        if err:
            log('error', f'Data connection failed, error: {err}')
            self.close_data_conn()
        else:
            if self.cli_mode:
                log('debug', f'Data connection opened: {self.data_addr}')
예제 #16
0
    def mkdir(self, path: str, is_client: bool = True) -> bool:
        '''
        Make a directory recursively.

        Return True if succeeded.

        :param path: server path to the directory
        :param is_client: True for client, False for server internal use
        '''

        dst_path = self.get_server_path(path) if is_client else path
        log('debug', f'Creating directory: {dst_path}')
        if not is_safe_path(dst_path, self.server_dir):
            self.send_status(553)
            return False
        try:
            if not os.path.exists(dst_path):
                os.makedirs(dst_path)
            if os.path.isdir(dst_path):
                log('info', f'Created directory: {dst_path}')
                if is_client:
                    dir_path = dst_path[len(self.server_dir)+1:]
                    self.send_status(257, dir_path)
                return True
            else:
                raise OSError
        except OSError:
            log('warn', f'Failed to make directory: {dst_path}')
            self.send_status(550)
            return False
예제 #17
0
    def check_resp(self, code: int) -> Tuple[bool, int, str]:
        '''
        Get a response from the server, and check its status code.

        Return the check result, the responded status code and the response message.
        The result is True if the status code is expected, otherwise False.

        :param code: expected status code
        '''
        def _get_resp() -> str:
            return (self.ctrl_conn.recv(
                self.buffer_size).decode('utf-8').strip('\r\n'))

        try:
            resp = _get_resp()
        except socket.timeout:
            if self.cli_mode:
                log('debug', f'No response received, should be: {code}')
            return False, 0, None
        except socket.error as e:
            if self.cli_mode:
                log('debug', f'Connection closed: {e}, should be: {code}')
            self.close_ctrl_conn()
            return False, 0, None
        except AttributeError:
            if self.cli_mode:
                log('debug', f'Connection failed, should be: {code}')
            return False, 0, None

        try:
            resp_code, resp_msg = resp.split(None, 1)
            if resp_code != str(code):
                if self.cli_mode:
                    log('debug', f'Response: {resp_code}, should be: {code}')
                return False, resp_code, resp_msg
            return True, resp_code, resp_msg
        except ValueError as e:
            log('error', f'Invalid response: {resp}')
            self.close_ctrl_conn()
            return False, 0, None
예제 #18
0
    def cwd(self, path: str = '/') -> None:
        '''
        Change working directory.

        :param path: server path to the destination,
                     using root folder by default
        '''

        dst_path = self.get_server_path(path)
        log('debug', f'Changing working directory to {dst_path}')
        if not is_safe_path(dst_path, self.server_dir, allow_base=True):
            self.send_status(553)
            return

        if os.path.isdir(dst_path):
            self.cwd_path = dst_path[len(self.server_dir)+1:]
            if not self.cwd_path:
                self.cwd_path = '/'
            log('info', f'Changed working directory to {self.cwd_path}')
            self.send_status(257, self.cwd_path)
        else:
            self.send_status(550)
예제 #19
0
    def store(self, path: str) -> None:
        '''
        Store a file to server.

        :param path: local path to the file
        '''

        dst_path = self.get_server_path(os.path.basename(path))
        log('debug', f'Storing file: {dst_path}')
        if not is_safe_path(dst_path, self.server_dir):
            self.send_status(553)
            return
        dir_name, file_name = dst_path.rsplit(os.sep, 1)
        if not os.path.isdir(dir_name):
            if not self.mkdir(dir_name, is_client=False):  # failed to make directory
                return
        if not file_name:  # make directory only
            return

        try:
            with open(dst_path, 'wb') as dst_file:
                self.send_status(150)
                if not self.data_sock:
                    self.open_data_sock()
                self.open_data_conn()
                while self.data_conn:
                    data = self.data_conn.recv(self.buffer_size)
                    if not data:
                        break
                    dst_file.write(data)
            log('info', f'Stored file: {dst_path}')
        except OSError as e:
            log('warn', f'System error: {e}')
            self.send_status(550)
        except socket.timeout:
            log('warn', f'Data connection timeout: {self.data_addr}')
            self.send_status(426)
        except socket.error:
            pass
        finally:
            self.close_data_sock()
예제 #20
0
    def retrieve(self, path: str) -> None:
        '''
        Retrieve a file from server.

        :param path: server path to the file
        '''

        src_path = self.get_server_path(path)
        log('debug', f'Sending file: {src_path}')
        if not is_safe_path(src_path, self.server_dir):
            self.send_status(553)
            return
        if not os.path.exists(src_path):
            self.send_status(550)
            return

        try:
            with open(src_path, 'rb') as src_file:
                self.send_status(150)
                if not self.data_sock:
                    self.open_data_sock()
                self.open_data_conn()
                while True:
                    data = src_file.read(self.buffer_size)
                    if not data:
                        break
                    self.data_conn.sendall(data)
            log('info', f'Sent file {src_path}')
        except OSError as e:
            log('warn', f'System error: {e}')
            self.send_status(550)
        except socket.timeout:
            log('warn', f'Data connection timeout: {self.data_addr}')
            self.send_status(426)
        except socket.error:
            pass
        finally:
            self.close_data_sock()
예제 #21
0
    def delete(self, path: str) -> bool:
        '''
        Delete a file from server.

        Return True if succeeded.

        :param path: server path to the file
        '''

        if not self.ping():
            log('info', 'Please connect to server first.')
            return False

        log('info', f'Deleting file: {path}')

        self.ctrl_conn.sendall(f'DELE {path}\r\n'.encode('utf-8'))

        expected, _, resp_msg = self.check_resp(250)
        log('info' if expected else 'warn', resp_msg)
        return expected
예제 #22
0
    def mkdir(self, path: str) -> bool:
        '''
        Make a directory recursively.

        Return True if succeeded.

        :param path: server path to the directory
        '''

        if not self.ping():
            log('info', 'Please connect to server first.')
            return False

        self.ctrl_conn.sendall(f'MKD {path}\r\n'.encode('utf-8'))
        expected, _, resp_msg = self.check_resp(257)
        if not expected:
            log('warn', resp_msg)
        else:
            log('info', f'Created directory: {resp_msg}')
        return expected
예제 #23
0
    def cwd(self, path: str = '/') -> bool:
        '''
        Change working directory.

        Return True if succeeded.

        :param path: server path to the destination,
                     using root folder by default
        '''

        if not self.ping():
            log('info', 'Please connect to server first.')
            return False

        self.ctrl_conn.sendall(f'CWD {path}\r\n'.encode('utf-8'))
        expected, _, resp_msg = self.check_resp(257)
        if not expected:
            log('warn', resp_msg)
            return False
        else:
            log('info', f'Changed directory to: {resp_msg}')
            return True
예제 #24
0
    def delete(self, path: str) -> None:
        '''
        Delete a file from server.

        :param path: server path to the file
        '''

        src_path = self.get_server_path(path)
        log('debug', f'Deleting file: {src_path}')
        if not is_safe_path(src_path, self.server_dir):
            self.send_status(553)
            return
        if not os.path.isfile(src_path):
            self.send_status(550)
            return

        try:
            os.remove(src_path)
            log('info', f'Deleted file: {src_path}')
            self.send_status(250)
        except OSError:
            log('warn', f'Failed to delete file: {src_path}')
            self.send_status(550)
예제 #25
0
    def router(self, raw_cmd: str) -> None:
        '''
        Route to the associated method based on client command.

        :param raw_cmd: raw client command
        '''

        method_dict = {
            'PING': self.pong,
            'LIST': self.ls,
            'RETR': self.retrieve,
            'STOR': self.store,
            'DELE': self.delete,
            'CWD': self.cwd,
            'PWD': self.pwd,
            'MKD': self.mkdir,
            'RMD': self.rmdir,
            'RMDA': self.rmdir_all,
        }

        try:
            log('debug', f'Operation: {raw_cmd}')
            cmd = raw_cmd.split(None, 1)
            cmd_len = len(cmd)
            op = cmd[0]
            method = method_dict.get(op[:4].upper())
            if method:
                if cmd_len == 1:
                    method()
                else:
                    method(cmd[1])
            else:
                log('warn', f'Invalid client operation: {raw_cmd}')
        except TypeError as e:
            log('warn', f'Invalid client operation: {raw_cmd}, error: {e}')
            self.send_status(501)
예제 #26
0
    def ls(self, path: str = '.') -> None:
        '''
        List information of a file or directory.

        :param path: server path to the file or directory
        '''

        def _parse_stat(raw_stat: os.stat_result) -> str:
            '''
            Parse the raw stat_result to a string for response.

            Return file size, file mode, last modified time, permissions and owner id.

            :param raw_stat: stat_result of a file or directory
            '''

            s = []
            s.append(raw_stat.st_size)   # file size
            s.append(raw_stat.st_mode)   # file mode
            s.append(raw_stat.st_mtime)  # last modified time
            s.append(raw_stat.st_uid)    # owner id
            return ' '.join([str(i) for i in s])

        def _send_info(file_name: str, info: str) -> None:
            '''
            Send file information to client.

            :param file_name: file name
            :param info: file information
            '''

            self.data_conn.sendall(f'{file_name} {info}\r\n'.encode('utf-8'))

        src_path = self.get_server_path(path)
        log('debug', f'Listing information of {src_path}')
        if not is_safe_path(src_path, self.server_dir, allow_base=True):
            self.send_status(553)
            return
        if not os.path.exists(src_path):
            self.send_status(550)
            return

        try:
            self.send_status(150)
            if not self.data_sock:
                self.open_data_sock()
            self.open_data_conn()
            if os.path.isdir(src_path):
                with os.scandir(src_path) as it:
                    for file in it:
                        if not file.name.startswith('.'):
                            file_name = file.name.replace(' ', '%20')
                            info = _parse_stat(file.stat())
                            _send_info(file_name, info)
            else:
                file_name = os.path.basename(src_path)
                if not file_name.startswith('.'):
                    file_name = file_name.replace(' ', '%20')
                    info = _parse_stat(os.stat(src_path))
                    _send_info(file_name, info)
            log('info', f'Finished listing information of {src_path}')
        except OSError as e:
            log('warn', f'System error: {e}')
            self.send_status(550)
        except socket.timeout:
            log('warn', f'Data connection timeout: {self.data_addr}')
            self.send_status(426)
        except socket.error:
            pass
        finally:
            self.close_data_sock()
예제 #27
0
    def retrieve(self, path: str) -> str:
        '''
        Retrieve a file from server.

        Return the location of the downloaded file if succeeded, otherwise return None.

        :param path: server path to the file
        '''

        if not self.ping():
            log('info', 'Please connect to server first.')
            return None

        dst_path = self.get_client_path(os.path.basename(path))
        log('info', f'Downloading file: {dst_path}')

        self.ctrl_conn.sendall(f'RETR {path}\r\n'.encode('utf-8'))

        expected, _, resp_msg = self.check_resp(150)
        if not expected:
            log('warn', resp_msg)
            return None
        self.open_data_conn()
        if not self.check_resp(225)[0]:
            self.close_data_conn()
            return None

        try:
            with open(dst_path, 'wb') as dst_file:
                while True:
                    data = self.data_conn.recv(self.buffer_size)
                    if not data:
                        break
                    dst_file.write(data)
        except OSError as e:
            log('warn', f'System error: {e}')
            return None
        except socket.error:
            if self.cli_mode:
                log('debug', 'Data connection closed.')
            return None
        else:
            log('info', 'File successfully downloaded.')
            return dst_path
        finally:
            self.close_data_conn()
예제 #28
0
    def store(self, path: str) -> bool:
        '''
        Store a file to server.

        Return True if succeeded.

        :param path: local path to the file
        '''

        if not self.ping():
            log('info', 'Please connect to server first.')
            return False

        src_path = self.get_client_path(path)
        if not os.path.isfile(src_path):
            log('info', 'File not found.')
            return False
        log('info', f'Uploading file: {src_path}')

        self.ctrl_conn.sendall(f'STOR {path}\r\n'.encode('utf-8'))

        expected, _, resp_msg = self.check_resp(150)
        if not expected:
            log('info', resp_msg)
            return False
        self.open_data_conn()
        if not self.check_resp(225)[0]:
            self.close_data_conn()
            return False

        try:
            with open(src_path, 'rb') as src_file:
                while True:
                    data = src_file.read(self.buffer_size)
                    if not data:
                        break
                    self.data_conn.sendall(data)
        except OSError as e:
            log('warn', f'System error: {e}')
        except socket.error:
            if self.cli_mode:
                log('debug', 'Data connection closed.')
        else:
            log('info', 'File successfully uploaded.')
            return True
        finally:
            self.close_data_conn()
예제 #29
0
    def ls(self, path: str = '.') -> list[dict]:
        '''
        List information of a file or directory.

        Return the parsed file information list.

        :param path: server path to the file or directory,
                     using current path by default
        '''
        def _parse_stat(resp: str) -> Tuple[str, str, str, str, str, str]:
            '''
            Parse a line of server response to a human readable list for output.

            Return file name, file size, file type, last modified time,
            permissions and owner.

            :param resp: a line of response
            '''
            def _parse_size(st_size: int) -> str:
                '''
                Interpret st_size to a human readable size.

                Return file size with a proper unit prefix.

                :param st_size: file size
                '''

                size = float(st_size)
                for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB']:
                    if abs(size) < 1024.0:
                        return f'{size:4.1f} {unit}'
                    size /= 1024.0
                return f'{size:.1f} YB'

            def _parse_type(st_mode: int) -> str:
                '''
                Interpret the result of st_mode to file type.

                Return file type.

                :param st_mode: file mode
                '''

                type_dict = {
                    stat.S_ISREG: 'File',
                    stat.S_ISDIR: 'Dir',
                    stat.S_ISLNK: 'Link',
                }
                for check in type_dict:
                    if check(st_mode):
                        return type_dict[check]
                return 'Unkn'

            def _parse_perms(st_mode: int) -> str:
                '''
                Interpret the result of st_mode to permissions.

                Return a Unix-like permission string.

                :param st_mode: file mode
                '''

                perm_dict = {
                    stat.S_IRUSR: '******',
                    stat.S_IWUSR: '******',
                    stat.S_IXUSR: '******',
                    stat.S_IRGRP: 'r',
                    stat.S_IWGRP: 'w',
                    stat.S_IXGRP: 'x',
                    stat.S_IROTH: 'r',
                    stat.S_IWOTH: 'w',
                    stat.S_IXOTH: 'x',
                }
                perms = 'd' if stat.S_ISDIR(st_mode) else '-'
                for perm in perm_dict:
                    perms += perm_dict[perm] if st_mode & perm else '-'
                return perms

            if not resp:
                return None
            try:
                file_name, st_size, st_mode, st_mtime, st_uid = resp.split(' ')
                file_name = file_name.replace('%20', ' ')
                file_size = _parse_size(int(st_size))
                file_type = _parse_type(int(st_mode))
                if file_type == 'Dir':
                    file_size = ''
                mod_time = (datetime.fromtimestamp(
                    float(st_mtime)).strftime('%Y-%m-%d %H:%M:%S'))
                perms = _parse_perms(int(st_mode))
                owner = st_uid
            except (ValueError, TypeError) as e:
                log('error', f'Invalid response: {resp}, error: {e}')
                return None
            else:
                return file_name, file_size, file_type, mod_time, perms, owner

        def _print_info(info: Tuple[str, str, str, str, str, str]) -> None:
            '''
            Print information of a file or directory.

            :param info: (file_name, file_size, file_type, mod_time, perms, owner)
            '''

            try:
                file_name, file_size, file_type, mod_time, perms, owner = info
                print('{:20} | {:>10} | {:4} | {:19} | {:10} | {:3}'.format(
                    file_name, file_size, file_type, mod_time, perms, owner))
            except ValueError as e:
                log('error', f'Invalid response: {info}, error: {e}')

        def _to_dict(info: Tuple[str, str, str, str, str, str]) -> dict:
            '''
            Convert an information entry into key-value pairs in JSON syntax.

            Return a dictionary version of info.

            :param info: (file_name, file_size, file_type, mod_time, perms, owner)
            '''

            return {
                'fileName': info[0],
                'fileSize': info[1],
                'fileType': info[2],
                'modTime': info[3],
                'perms': info[4],
                'owner': info[5],
            }

        if not self.ping():
            log('info', 'Please connect to server first.')
            return None

        self.ctrl_conn.sendall(f'LIST {path}\r\n'.encode('utf-8'))

        expected, _, resp_msg = self.check_resp(150)
        if not expected:
            log('warn', resp_msg)
            return None
        self.open_data_conn()
        if not self.check_resp(225)[0]:
            self.close_data_conn()
            return None

        try:
            raw_resp = b''
            while True:
                data = self.data_conn.recv(self.buffer_size)
                if not data:
                    break
                raw_resp += data
            try:
                resp_list: list[str] = (
                    raw_resp.decode('utf-8').strip('\r\n').split('\r\n'))
            except (ValueError, TypeError) as e:
                log('error', f'Invalid response: {raw_resp}, error: {e}')
                return None
            else:
                infos = []
                for resp in resp_list:
                    info = _parse_stat(resp)
                    if info:
                        if self.cli_mode:
                            _print_info(info)
                        info_dict = _to_dict(info)
                        infos.append(info_dict)
                return infos
        except socket.error:
            if self.cli_mode:
                log('debug', 'Data connection closed.')
            return None
        finally:
            self.close_data_conn()
예제 #30
0
        def _parse_stat(resp: str) -> Tuple[str, str, str, str, str, str]:
            '''
            Parse a line of server response to a human readable list for output.

            Return file name, file size, file type, last modified time,
            permissions and owner.

            :param resp: a line of response
            '''
            def _parse_size(st_size: int) -> str:
                '''
                Interpret st_size to a human readable size.

                Return file size with a proper unit prefix.

                :param st_size: file size
                '''

                size = float(st_size)
                for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB']:
                    if abs(size) < 1024.0:
                        return f'{size:4.1f} {unit}'
                    size /= 1024.0
                return f'{size:.1f} YB'

            def _parse_type(st_mode: int) -> str:
                '''
                Interpret the result of st_mode to file type.

                Return file type.

                :param st_mode: file mode
                '''

                type_dict = {
                    stat.S_ISREG: 'File',
                    stat.S_ISDIR: 'Dir',
                    stat.S_ISLNK: 'Link',
                }
                for check in type_dict:
                    if check(st_mode):
                        return type_dict[check]
                return 'Unkn'

            def _parse_perms(st_mode: int) -> str:
                '''
                Interpret the result of st_mode to permissions.

                Return a Unix-like permission string.

                :param st_mode: file mode
                '''

                perm_dict = {
                    stat.S_IRUSR: '******',
                    stat.S_IWUSR: '******',
                    stat.S_IXUSR: '******',
                    stat.S_IRGRP: 'r',
                    stat.S_IWGRP: 'w',
                    stat.S_IXGRP: 'x',
                    stat.S_IROTH: 'r',
                    stat.S_IWOTH: 'w',
                    stat.S_IXOTH: 'x',
                }
                perms = 'd' if stat.S_ISDIR(st_mode) else '-'
                for perm in perm_dict:
                    perms += perm_dict[perm] if st_mode & perm else '-'
                return perms

            if not resp:
                return None
            try:
                file_name, st_size, st_mode, st_mtime, st_uid = resp.split(' ')
                file_name = file_name.replace('%20', ' ')
                file_size = _parse_size(int(st_size))
                file_type = _parse_type(int(st_mode))
                if file_type == 'Dir':
                    file_size = ''
                mod_time = (datetime.fromtimestamp(
                    float(st_mtime)).strftime('%Y-%m-%d %H:%M:%S'))
                perms = _parse_perms(int(st_mode))
                owner = st_uid
            except (ValueError, TypeError) as e:
                log('error', f'Invalid response: {resp}, error: {e}')
                return None
            else:
                return file_name, file_size, file_type, mod_time, perms, owner