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
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}')
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.')
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)
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 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)
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}')
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)
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.')
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
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}')
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}')
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
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
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}')
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
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
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)
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()
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()
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
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
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
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)
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)
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()
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()
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()
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()
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