一、效果展示



二、源码分享
1、工程结构

2、库安装

3、main.py
"""
功能:
- 连接/断开 ftp 服务器
- 本地/远程双栏文件浏览器
- 文件上传/下载(带进度)
- 调试控制台:实时 ftp 命令/响应日志 + 手动发送原始命令
"""
import os
import sys
import threading
import time
from datetime import datetime
from ftplib import ftp, error_perm
from pyqt6.qtcore import (
qt, qthread, pyqtsignal, qobject, qsettings
)
from pyqt6.qtgui import (
qfont, qcolor, qpalette
)
from pyqt6.qtwidgets import (
qapplication, qmainwindow, qwidget, qvboxlayout, qhboxlayout,
qgridlayout, qgroupbox, qlabel, qlineedit, qpushbutton,
qtreewidget, qtreewidgetitem, qheaderview, qtextedit,
qsplitter, qprogressbar, qfiledialog, qmessagebox,
qstatusbar, qtoolbar, qmenu, qabstractitemview, qcombobox
)
# ============================================================
# 调试 ftp 客户端 - 拦截所有命令和响应
# ============================================================
class debugftp(ftp):
"""继承 ftplib.ftp,重写 putline/getline 以捕获所有协议交互"""
def __init__(self, *args, log_callback=none, **kwargs):
self._log_callback = log_callback
super().__init__(*args, **kwargs)
def putline(self, line):
if self._log_callback:
self._log_callback("cmd", line)
super().putline(line)
def getline(self):
line = super().getline()
if self._log_callback:
self._log_callback("resp", line)
return line
# ============================================================
# ftp 信号定义
# ============================================================
class ftpsignals(qobject):
log_message = pyqtsignal(str, str) # (类别, 消息) 类别: cmd/resp/info/error/success
file_list_received = pyqtsignal(list, str) # (文件列表, 当前路径)
transfer_progress = pyqtsignal(str, int, int) # (文件名, 已传输, 总大小)
transfer_complete = pyqtsignal(str, bool, str) # (文件名, 成功, 消息)
connection_changed = pyqtsignal(bool) # 连接状态
raw_command_result = pyqtsignal(str) # 原始命令执行结果
# ============================================================
# ftp 工作线程
# ============================================================
class ftpworkerthread(qthread):
"""在后台线程执行所有 ftp 操作"""
def __init__(self, parent=none):
super().__init__(parent)
self.signals = ftpsignals()
self.ftp = none
self._connected = false
self._stop_flag = false
self._task_queue = []
self._lock = threading.lock()
def connect_to_server(self, host, port, user, password):
"""添加连接任务"""
self._enqueue("connect", {
"host": host, "port": port,
"user": user, "password": password
})
def disconnect_from_server(self):
"""添加断开连接任务"""
self._enqueue("disconnect", {})
def list_directory(self, path=""):
"""添加列目录任务"""
self._enqueue("list", {"path": path})
def change_directory(self, path):
"""添加切换目录任务"""
self._enqueue("cwd", {"path": path})
def upload_file(self, local_path, remote_name):
"""添加上传任务"""
self._enqueue("upload", {
"local_path": local_path, "remote_name": remote_name
})
def download_file(self, remote_name, local_path):
"""添加下载任务"""
self._enqueue("download", {
"remote_name": remote_name, "local_path": local_path
})
def execute_raw_command(self, command):
"""添加原始命令执行任务"""
self._enqueue("raw_cmd", {"command": command})
def delete_file(self, name):
"""添加删除文件任务"""
self._enqueue("delete_file", {"name": name})
def delete_directory(self, name):
"""添加删除目录任务"""
self._enqueue("delete_dir", {"name": name})
def _enqueue(self, task_type, params):
with self._lock:
self._task_queue.append((task_type, params))
def run(self):
while not self._stop_flag:
task = none
with self._lock:
if self._task_queue:
task = self._task_queue.pop(0)
if task is none:
self.msleep(50)
continue
task_type, params = task
try:
if task_type == "connect":
self._do_connect(**params)
elif task_type == "disconnect":
self._do_disconnect()
elif task_type == "list":
self._do_list(**params)
elif task_type == "cwd":
self._do_cwd(**params)
elif task_type == "upload":
self._do_upload(**params)
elif task_type == "download":
self._do_download(**params)
elif task_type == "raw_cmd":
self._do_raw_command(**params)
elif task_type == "delete_file":
self._do_delete_file(**params)
elif task_type == "delete_dir":
self._do_delete_dir(**params)
except exception as e:
self.signals.log_message.emit("error", f"操作失败: {str(e)}")
def stop(self):
self._stop_flag = true
if self.ftp and self._connected:
try:
self.ftp.quit()
except exception:
pass
self.wait(3000)
def _do_connect(self, host, port, user, password):
self.signals.log_message.emit("info", f"正在连接 {host}:{port} ...")
try:
self.ftp = debugftp(log_callback=self._ftp_log)
self.ftp.connect(host, int(port), timeout=10)
self.ftp.login(user, password)
self._connected = true
self.signals.log_message.emit("success", f"已连接到 {host}:{port}")
self.signals.connection_changed.emit(true)
# 连接成功后列出根目录
self._do_list(path="")
except exception as e:
self.signals.log_message.emit("error", f"连接失败: {str(e)}")
self.signals.connection_changed.emit(false)
self._connected = false
def _do_disconnect(self):
try:
if self.ftp:
self.ftp.quit()
except exception:
pass
self._connected = false
self.ftp = none
self.signals.log_message.emit("info", "已断开连接")
self.signals.connection_changed.emit(false)
def _do_list(self, path=""):
if not self._connected or not self.ftp:
return
try:
if path:
self.ftp.cwd(path)
cwd = self.ftp.pwd()
self.signals.log_message.emit("info", f"当前目录: {cwd}")
items = []
try:
# 尝试 mlsd(结构化目录列表)
for name, facts in self.ftp.mlsd():
if name == ".":
continue
if name == "..":
items.append(("..", 0, true, ""))
continue
is_dir = facts.get("type", "") == "dir"
size = int(facts.get("size", 0))
modify = facts.get("modify", "")
items.append((name, size, is_dir, modify))
except error_perm:
# 回退到 list 命令解析
lines = []
self.ftp.retrlines("list", lines.append)
for line in lines:
parsed = self._parse_list_line(line)
if parsed:
items.append(parsed)
self.signals.file_list_received.emit(items, cwd)
except exception as e:
self.signals.log_message.emit("error", f"列目录失败: {str(e)}")
def _do_cwd(self, path):
if not self._connected or not self.ftp:
return
try:
self.ftp.cwd(path)
self._do_list()
except exception as e:
self.signals.log_message.emit("error", f"切换目录失败: {str(e)}")
def _do_upload(self, local_path, remote_name):
if not self._connected or not self.ftp:
return
filename = os.path.basename(local_path)
file_size = os.path.getsize(local_path)
self.signals.log_message.emit("info", f"开始上传: {filename} ({self._format_size(file_size)})")
transferred = [0]
last_emit = [time.time()]
class trackingfile:
"""包装文件对象,在每次 read 时跟踪传输进度"""
def __init__(self, fobj):
self._f = fobj
def read(self, *args, **kwargs):
data = self._f.read(*args, **kwargs)
if data:
transferred[0] += len(data)
now = time.time()
if now - last_emit[0] > 0.1:
self.signals.transfer_progress.emit(
filename, transferred[0], file_size
)
last_emit[0] = now
return data
def __getattr__(self, name):
return getattr(self._f, name)
try:
with open(local_path, "rb") as f:
self.ftp.storbinary(f"stor {remote_name}", trackingfile(f))
self.signals.transfer_progress.emit(filename, file_size, file_size)
self.signals.transfer_complete.emit(filename, true, "上传成功")
self.signals.log_message.emit("success", f"上传完成: {filename}")
except exception as e:
self.signals.transfer_complete.emit(filename, false, str(e))
self.signals.log_message.emit("error", f"上传失败: {filename} - {str(e)}")
def _do_download(self, remote_name, local_path):
if not self._connected or not self.ftp:
return
filename = os.path.basename(remote_name)
self.signals.log_message.emit("info", f"开始下载: {filename}")
# 获取文件大小
file_size = 0
try:
file_size = self.ftp.size(remote_name) or 0
except exception:
pass
transferred = [0]
last_emit = [time.time()]
def on_data(data):
f.write(data)
transferred[0] += len(data)
now = time.time()
if now - last_emit[0] > 0.1:
self.signals.transfer_progress.emit(filename, transferred[0], file_size)
last_emit[0] = now
try:
with open(local_path, "wb") as f:
self.ftp.retrbinary(f"retr {remote_name}", on_data)
if file_size > 0:
self.signals.transfer_progress.emit(filename, file_size, file_size)
self.signals.transfer_complete.emit(filename, true, "下载成功")
self.signals.log_message.emit("success", f"下载完成: {filename}")
except exception as e:
self.signals.transfer_complete.emit(filename, false, str(e))
self.signals.log_message.emit("error", f"下载失败: {filename} - {str(e)}")
def _do_raw_command(self, command):
if not self._connected or not self.ftp:
self.signals.raw_command_result.emit("错误: 未连接到服务器")
return
try:
cmd = command.strip()
if not cmd:
return
parts = cmd.split(none, 1)
cmd_name = parts[0].upper()
cmd_args = parts[1] if len(parts) > 1 else ""
# 特殊处理一些常见命令
if cmd_name == "list":
lines = []
if cmd_args:
self.ftp.retrlines(f"list {cmd_args}", lines.append)
else:
self.ftp.retrlines("list", lines.append)
result = "\n".join(lines) if lines else "(空目录)"
elif cmd_name == "nlst":
lines = []
self.ftp.retrlines(f"nlst {cmd_args}" if cmd_args else "nlst", lines.append)
result = "\n".join(lines) if lines else "(空)"
elif cmd_name == "pwd":
result = self.ftp.pwd()
elif cmd_name == "cwd":
self.ftp.cwd(cmd_args)
result = f"当前目录: {self.ftp.pwd()}"
elif cmd_name == "size":
size = self.ftp.size(cmd_args)
result = f"{cmd_args}: {size} bytes" if size is not none else "无法获取大小"
elif cmd_name == "mkd":
result = self.ftp.mkd(cmd_args)
elif cmd_name == "rmd":
self.ftp.rmd(cmd_args)
result = f"已删除目录: {cmd_args}"
elif cmd_name == "dele":
self.ftp.delete(cmd_args)
result = f"已删除文件: {cmd_args}"
elif cmd_name == "ren":
# ren old new
ren_parts = cmd_args.split(none, 1)
if len(ren_parts) == 2:
self.ftp.rename(ren_parts[0], ren_parts[1])
result = f"已重命名: {ren_parts[0]} -> {ren_parts[1]}"
else:
result = "用法: ren <旧名> <新名>"
elif cmd_name == "help":
resp = self.ftp.sendcmd("help")
result = resp
elif cmd_name in ("quit", "bye"):
result = "请使用界面上的断开按钮"
else:
# 通用命令发送
resp = self.ftp.sendcmd(cmd)
result = resp
self.signals.raw_command_result.emit(result)
except exception as e:
self.signals.raw_command_result.emit(f"错误: {str(e)}")
def _do_delete_file(self, name):
"""删除远程文件"""
if not self._connected or not self.ftp:
return
try:
self.ftp.delete(name)
self.signals.log_message.emit("success", f"已删除文件: {name}")
# 删除后刷新目录
self._do_list()
except exception as e:
self.signals.log_message.emit("error", f"删除文件失败: {name} - {str(e)}")
def _do_delete_dir(self, name):
"""删除远程目录"""
if not self._connected or not self.ftp:
return
try:
self.ftp.rmd(name)
self.signals.log_message.emit("success", f"已删除目录: {name}")
# 删除后刷新目录
self._do_list()
except exception as e:
self.signals.log_message.emit("error", f"删除目录失败: {name} - {str(e)}")
def _ftp_log(self, category, message):
"""ftp 命令/响应日志回调"""
self.signals.log_message.emit(category, message)
def _parse_list_line(self, line):
"""解析 list 命令输出的每一行"""
# unix 格式: drwxr-xr-x 2 user group 4096 jan 01 12:00 filename
# dos 格式: 01-01-24 12:00am <dir> dirname
# 01-01-24 12:00am 1234 filename
parts = line.split(none, 8)
if len(parts) >= 9 and (parts[0][0] in ('d', '-', 'l', 'r')):
# unix 格式
is_dir = parts[0][0] == 'd'
try:
size = int(parts[4])
except (valueerror, indexerror):
size = 0
name = parts[8]
return (name, size, is_dir, "")
elif len(parts) >= 4:
# 尝试 dos 格式
try:
is_dir = parts[2] == '<dir>'
size = 0 if is_dir else int(parts[2])
name = parts[3]
return (name, size, is_dir, "")
except (valueerror, indexerror):
pass
return none
@staticmethod
def _format_size(size_bytes):
if size_bytes < 1024:
return f"{size_bytes} b"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} kb"
elif size_bytes < 1024 * 1024 * 1024:
return f"{size_bytes / (1024 * 1024):.1f} mb"
else:
return f"{size_bytes / (1024 * 1024 * 1024):.2f} gb"
# ============================================================
# 文件浏览器 widget
# ============================================================
class filebrowserwidget(qwidget):
"""文件浏览器组件,用于远程和本地文件浏览"""
file_selected = pyqtsignal(str, bool) # (文件路径, 是否为目录)
file_delete_requested = pyqtsignal(str, bool) # (名称, 是否为目录)
def __init__(self, title="文件浏览器", is_remote=false, parent=none):
super().__init__(parent)
self.is_remote = is_remote
self.current_path = ""
self.path_history = []
layout = qvboxlayout(self)
layout.setcontentsmargins(4, 4, 4, 4)
layout.setspacing(4)
# 标题和路径
header = qhboxlayout()
self.title_label = qlabel(f"<b>{title}</b>")
header.addwidget(self.title_label)
header.addstretch()
self.path_label = qlineedit()
self.path_label.setreadonly(true)
self.path_label.setplaceholdertext("路径...")
self.path_label.setminimumwidth(200)
header.addwidget(self.path_label, stretch=1)
layout.addlayout(header)
# 文件列表
self.tree = qtreewidget()
self.tree.setheaderlabels(["名称", "大小", "类型"])
self.tree.setcolumncount(3)
self.tree.setrootisdecorated(false)
self.tree.setsortingenabled(true)
self.tree.setselectionmode(qabstractitemview.selectionmode.singleselection)
self.tree.setcontextmenupolicy(qt.contextmenupolicy.customcontextmenu)
self.tree.customcontextmenurequested.connect(self._show_context_menu)
self.tree.itemdoubleclicked.connect(self._on_item_double_clicked)
header_view = self.tree.header()
header_view.setstretchlastsection(true)
header_view.setsectionresizemode(0, qheaderview.resizemode.stretch)
header_view.setsectionresizemode(1, qheaderview.resizemode.resizetocontents)
header_view.setsectionresizemode(2, qheaderview.resizemode.resizetocontents)
layout.addwidget(self.tree)
def set_file_list(self, items, current_path=""):
"""设置文件列表 items: [(name, size, is_dir, modify), ...]"""
self.tree.clear()
self.current_path = current_path
self.path_label.settext(current_path)
# 排序:目录在前,文件在后
dirs = sorted([i for i in items if i[2]], key=lambda x: x[0].lower())
files = sorted([i for i in items if not i[2]], key=lambda x: x[0].lower())
for name, size, is_dir, modify in dirs + files:
item = qtreewidgetitem()
icon = "📁" if is_dir else "📄"
if name == "..":
icon = "⬆️"
item.settext(0, f"{icon} {name}")
item.settext(1, self._format_size(size) if not is_dir else "")
item.settext(2, "文件夹" if is_dir else "文件")
item.setdata(0, qt.itemdatarole.userrole, {
"name": name, "size": size, "is_dir": is_dir
})
self.tree.addtoplevelitem(item)
def _on_item_double_clicked(self, item, column):
data = item.data(0, qt.itemdatarole.userrole)
if data:
self.file_selected.emit(data["name"], data["is_dir"])
def _show_context_menu(self, pos):
item = self.tree.itemat(pos)
if not item:
return
data = item.data(0, qt.itemdatarole.userrole)
if not data:
return
menu = qmenu(self)
if data["is_dir"]:
action = menu.addaction("打开目录")
action.triggered.connect(
lambda: self.file_selected.emit(data["name"], true)
)
if self.is_remote and data["name"] != "..":
menu.addseparator()
del_action = menu.addaction("🗑️ 删除目录")
del_action.triggered.connect(
lambda: self.file_delete_requested.emit(data["name"], true)
)
else:
if self.is_remote:
action = menu.addaction("下载")
action.triggered.connect(
lambda: self.file_selected.emit(data["name"], false)
)
menu.addseparator()
del_action = menu.addaction("删除文件")
del_action.triggered.connect(
lambda: self.file_delete_requested.emit(data["name"], false)
)
else:
action = menu.addaction("上传")
action.triggered.connect(
lambda: self.file_selected.emit(data["name"], false)
)
menu.exec(self.tree.viewport().maptoglobal(pos))
def load_local_directory(self, path):
"""加载本地目录内容"""
if not os.path.isdir(path):
return
items = []
try:
if path != os.path.dirname(path): # 非根目录,添加 ..
items.append(("..", 0, true, ""))
for name in os.listdir(path):
full_path = os.path.join(path, name)
is_dir = os.path.isdir(full_path)
try:
size = os.path.getsize(full_path) if not is_dir else 0
except oserror:
size = 0
items.append((name, size, is_dir, ""))
self.set_file_list(items, path)
except permissionerror:
pass
@staticmethod
def _format_size(size_bytes):
if size_bytes < 1024:
return f"{size_bytes} b"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} kb"
elif size_bytes < 1024 * 1024 * 1024:
return f"{size_bytes / (1024 * 1024):.1f} mb"
else:
return f"{size_bytes / (1024 * 1024 * 1024):.2f} gb"
# ============================================================
# 传输进度 widget
# ============================================================
class transferwidget(qwidget):
def __init__(self, parent=none):
super().__init__(parent)
layout = qvboxlayout(self)
layout.setcontentsmargins(4, 4, 4, 4)
header = qlabel("<b>传输队列</b>")
layout.addwidget(header)
self.transfer_list = qtreewidget()
self.transfer_list.setheaderlabels(["文件", "方向", "进度", "状态"])
self.transfer_list.setcolumncount(4)
self.transfer_list.setrootisdecorated(false)
header_view = self.transfer_list.header()
header_view.setstretchlastsection(true)
header_view.setsectionresizemode(0, qheaderview.resizemode.stretch)
header_view.setsectionresizemode(1, qheaderview.resizemode.resizetocontents)
header_view.setsectionresizemode(3, qheaderview.resizemode.resizetocontents)
layout.addwidget(self.transfer_list)
self._progress_map = {}
def add_transfer(self, filename, direction):
"""direction: 'upload' or 'download'"""
item = qtreewidgetitem()
icon = "📤" if direction == "upload" else "📥"
item.settext(0, filename)
item.settext(1, icon)
item.settext(2, "0%")
item.settext(3, "等待中")
self.transfer_list.addtoplevelitem(item)
progress = qprogressbar()
progress.setmaximum(100)
progress.setvalue(0)
self.transfer_list.setitemwidget(item, 2, progress)
self._progress_map[filename] = (item, progress)
def update_progress(self, filename, transferred, total):
if filename in self._progress_map:
item, progress = self._progress_map[filename]
if total > 0:
pct = int(transferred * 100 / total)
progress.setvalue(pct)
item.settext(2, f"{pct}%")
item.settext(3, "传输中...")
def complete_transfer(self, filename, success, message):
if filename in self._progress_map:
item, progress = self._progress_map[filename]
progress.setvalue(100 if success else 0)
item.settext(2, "完成" if success else "失败")
item.settext(3, message)
if success:
item.setforeground(3, qcolor("#2ecc71"))
else:
item.setforeground(3, qcolor("#e74c3c"))
def clear_completed(self):
items_to_remove = []
for i in range(self.transfer_list.toplevelitemcount()):
item = self.transfer_list.toplevelitem(i)
status = item.text(3)
if status in ("上传成功", "下载成功", "上传失败", "下载失败"):
items_to_remove.append(i)
for i in reversed(items_to_remove):
self.transfer_list.taketoplevelitem(i)
# ============================================================
# 调试控制台 widget
# ============================================================
class debugconsolewidget(qwidget):
command_submitted = pyqtsignal(str) # 用户输入的原始命令
def __init__(self, parent=none):
super().__init__(parent)
layout = qvboxlayout(self)
layout.setcontentsmargins(4, 4, 4, 4)
# 标题行
header = qhboxlayout()
title = qlabel("<b>调试控制台</b>")
header.addwidget(title)
header.addstretch()
clear_btn = qpushbutton("清空")
clear_btn.setfixedwidth(60)
clear_btn.clicked.connect(self.clear_log)
header.addwidget(clear_btn)
save_btn = qpushbutton("保存日志")
save_btn.setfixedwidth(80)
save_btn.clicked.connect(self.save_log)
header.addwidget(save_btn)
layout.addlayout(header)
# 日志显示区
self.log_text = qtextedit()
self.log_text.setreadonly(true)
self.log_text.setfont(qfont("consolas", 9))
layout.addwidget(self.log_text)
# 命令输入行
cmd_layout = qhboxlayout()
cmd_label = qlabel("ftp 命令:")
cmd_layout.addwidget(cmd_label)
self.cmd_input = qlineedit()
self.cmd_input.setplaceholdertext("输入 ftp 命令,如: list /, size file.txt, mkd newdir, pwd ...")
self.cmd_input.setfont(qfont("consolas", 10))
self.cmd_input.returnpressed.connect(self._on_send_command)
cmd_layout.addwidget(self.cmd_input)
send_btn = qpushbutton("发送")
send_btn.setfixedwidth(60)
send_btn.clicked.connect(self._on_send_command)
cmd_layout.addwidget(send_btn)
layout.addlayout(cmd_layout)
# 命令历史
self._cmd_history = []
self._history_idx = -1
def append_log(self, category, message):
"""添加日志消息"""
timestamp = datetime.now().strftime("%h:%m:%s")
color_map = {
"cmd": "#e67e22", # 橙色 - 发送的命令
"resp": "#3498db", # 蓝色 - 服务器响应
"info": "#95a5a6", # 灰色 - 信息
"error": "#e74c3c", # 红色 - 错误
"success": "#2ecc71", # 绿色 - 成功
}
prefix_map = {
"cmd": ">>>",
"resp": "<<<",
"info": " ℹ️",
"error": " ❌",
"success": " ✅",
}
color = color_map.get(category, "#333333")
prefix = prefix_map.get(category, " ")
html = f'<span style="color:#888;">[{timestamp}]</span> '
html += f'<span style="color:{color};font-weight:bold;">{prefix}</span> '
html += f'<span style="color:{color};">{self._escape_html(message)}</span>'
self.log_text.append(html)
# 自动滚动到底部
scrollbar = self.log_text.verticalscrollbar()
scrollbar.setvalue(scrollbar.maximum())
def clear_log(self):
self.log_text.clear()
def save_log(self):
path, _ = qfiledialog.getsavefilename(
self, "保存日志", "ftp_debug_log.txt",
"文本文件 (*.txt);;所有文件 (*)"
)
if path:
with open(path, "w", encoding="utf-8") as f:
f.write(self.log_text.toplaintext())
def _on_send_command(self):
cmd = self.cmd_input.text().strip()
if cmd:
self._cmd_history.insert(0, cmd)
self._history_idx = -1
self.command_submitted.emit(cmd)
self.cmd_input.clear()
def keypressevent(self, event):
if event.key() == qt.key.key_up:
if self._cmd_history:
self._history_idx = min(
self._history_idx + 1, len(self._cmd_history) - 1
)
self.cmd_input.settext(self._cmd_history[self._history_idx])
elif event.key() == qt.key.key_down:
if self._cmd_history and self._history_idx > 0:
self._history_idx -= 1
self.cmd_input.settext(self._cmd_history[self._history_idx])
elif self._history_idx == 0:
self._history_idx = -1
self.cmd_input.clear()
else:
super().keypressevent(event)
@staticmethod
def _escape_html(text):
return text.replace("&", "&").replace("<", "<").replace(">", ">")
# ============================================================
# 主窗口
# ============================================================
class mainwindow(qmainwindow):
def __init__(self):
super().__init__()
self.setwindowtitle("ftp 客户端调试助手")
self.setminimumsize(1100, 750)
# ftp 工作线程
self.ftp_worker = ftpworkerthread()
self.ftp_worker.signals.log_message.connect(self._on_log_message)
self.ftp_worker.signals.file_list_received.connect(self._on_file_list_received)
self.ftp_worker.signals.transfer_progress.connect(self._on_transfer_progress)
self.ftp_worker.signals.transfer_complete.connect(self._on_transfer_complete)
self.ftp_worker.signals.connection_changed.connect(self._on_connection_changed)
self.ftp_worker.signals.raw_command_result.connect(self._on_raw_command_result)
self.ftp_worker.start()
self._init_ui()
self._load_settings()
def _init_ui(self):
central = qwidget()
self.setcentralwidget(central)
main_layout = qvboxlayout(central)
main_layout.setcontentsmargins(8, 8, 8, 8)
main_layout.setspacing(6)
# ---- 1. 连接管理 + 服务器控制 ----
conn_group = qgroupbox("连接管理")
conn_layout = qgridlayout(conn_group)
conn_layout.setcontentsmargins(8, 12, 8, 8)
conn_layout.sethorizontalspacing(8)
conn_layout.setverticalspacing(6)
# 状态指示灯
self.status_indicator = qlabel("●")
self.status_indicator.setstylesheet("color: #e74c3c; font-size: 18px;")
self.status_indicator.settooltip("未连接")
conn_layout.addwidget(self.status_indicator, 0, 0)
# 主机
conn_layout.addwidget(qlabel("主机:"), 0, 1)
self.host_input = qlineedit("127.0.0.1")
self.host_input.setfixedwidth(150)
conn_layout.addwidget(self.host_input, 0, 2)
# 端口
conn_layout.addwidget(qlabel("端口:"), 0, 3)
self.port_input = qlineedit("2121")
self.port_input.setfixedwidth(70)
conn_layout.addwidget(self.port_input, 0, 4)
# 用户名
conn_layout.addwidget(qlabel("用户名:"), 0, 5)
self.user_input = qlineedit("admin")
self.user_input.setfixedwidth(100)
conn_layout.addwidget(self.user_input, 0, 6)
# 密码
conn_layout.addwidget(qlabel("密码:"), 0, 7)
self.pass_input = qlineedit("123456")
self.pass_input.setechomode(qlineedit.echomode.password)
self.pass_input.setfixedwidth(100)
conn_layout.addwidget(self.pass_input, 0, 8)
# 连接/断开按钮
self.connect_btn = qpushbutton("连接")
self.connect_btn.setfixedwidth(70)
self.connect_btn.setstylesheet(
"qpushbutton { background-color: #2ecc71; color: white; "
"font-weight: bold; border-radius: 4px; padding: 4px; }"
"qpushbutton:hover { background-color: #27ae60; }"
)
self.connect_btn.clicked.connect(self._on_connect)
conn_layout.addwidget(self.connect_btn, 0, 9)
self.disconnect_btn = qpushbutton("断开")
self.disconnect_btn.setfixedwidth(70)
self.disconnect_btn.setenabled(false)
self.disconnect_btn.setstylesheet(
"qpushbutton { background-color: #e74c3c; color: white; "
"font-weight: bold; border-radius: 4px; padding: 4px; }"
"qpushbutton:hover { background-color: #c0392b; }"
"qpushbutton:disabled { background-color: #bdc3c7; }"
)
self.disconnect_btn.clicked.connect(self._on_disconnect)
conn_layout.addwidget(self.disconnect_btn, 0, 10)
conn_layout.setcolumnstretch(11, 1)
main_layout.addwidget(conn_group)
# ---- 2. 文件浏览器(双栏) ----
file_splitter = qsplitter(qt.orientation.horizontal)
# 本地文件浏览器(左侧)
self.local_browser = filebrowserwidget("本地文件", is_remote=false)
self.local_browser.file_selected.connect(self._on_local_file_action)
self.local_browser.load_local_directory(os.path.expanduser("~"))
local_btn_layout = qhboxlayout()
browse_btn = qpushbutton("📂 浏览...")
browse_btn.clicked.connect(self._browse_local)
local_btn_layout.addwidget(browse_btn)
refresh_local_btn = qpushbutton("🔄 刷新")
refresh_local_btn.clicked.connect(
lambda: self.local_browser.load_local_directory(
self.local_browser.current_path
)
)
local_btn_layout.addwidget(refresh_local_btn)
local_btn_layout.addstretch()
local_container = qwidget()
local_container_layout = qvboxlayout(local_container)
local_container_layout.setcontentsmargins(0, 0, 0, 0)
local_container_layout.setspacing(2)
local_container_layout.addwidget(self.local_browser)
local_container_layout.addlayout(local_btn_layout)
file_splitter.addwidget(local_container)
# 远程文件浏览器(右侧)
self.remote_browser = filebrowserwidget("远程文件", is_remote=true)
self.remote_browser.file_selected.connect(self._on_remote_file_action)
self.remote_browser.file_delete_requested.connect(
self._on_remote_delete_request
)
# 上一级 + 刷新按钮
remote_btn_layout = qhboxlayout()
up_remote_btn = qpushbutton("⬆️ 上一级")
up_remote_btn.clicked.connect(self._go_up_remote)
remote_btn_layout.addwidget(up_remote_btn)
refresh_remote_btn = qpushbutton("🔄 刷新")
refresh_remote_btn.clicked.connect(self._refresh_remote)
remote_btn_layout.addwidget(refresh_remote_btn)
remote_btn_layout.addstretch()
remote_container = qwidget()
remote_container_layout = qvboxlayout(remote_container)
remote_container_layout.setcontentsmargins(0, 0, 0, 0)
remote_container_layout.setspacing(2)
remote_container_layout.addwidget(self.remote_browser)
remote_container_layout.addlayout(remote_btn_layout)
file_splitter.addwidget(remote_container)
file_splitter.setstretchfactor(0, 1)
file_splitter.setstretchfactor(1, 1)
main_layout.addwidget(file_splitter, stretch=3)
# ---- 3. 传输队列 ----
self.transfer_widget = transferwidget()
main_layout.addwidget(self.transfer_widget, stretch=1)
# ---- 4. 调试控制台 ----
self.debug_console = debugconsolewidget()
# 将调试控制台的命令提交信号连接到工作线程
self.debug_console.command_submitted.connect(
self.ftp_worker.execute_raw_command
)
main_layout.addwidget(self.debug_console, stretch=2)
# ---- 状态栏 ----
self.statusbar().showmessage("就绪")
# ---- 连接操作 ----
def _on_connect(self):
host = self.host_input.text().strip()
port = self.port_input.text().strip()
user = self.user_input.text().strip()
password = self.pass_input.text().strip()
if not host or not port:
qmessagebox.warning(self, "提示", "请输入主机地址和端口")
return
self.connect_btn.setenabled(false)
self.statusbar().showmessage(f"正在连接 {host}:{port}...")
self.ftp_worker.connect_to_server(host, port, user, password)
def _on_disconnect(self):
self.ftp_worker.disconnect_from_server()
self.remote_browser.tree.clear()
self.remote_browser.path_label.clear()
def _on_connection_changed(self, connected):
self.connect_btn.setenabled(not connected)
self.disconnect_btn.setenabled(connected)
if connected:
self.status_indicator.setstylesheet(
"color: #2ecc71; font-size: 18px;"
)
self.status_indicator.settooltip("已连接")
self.statusbar().showmessage("已连接")
else:
self.status_indicator.setstylesheet(
"color: #e74c3c; font-size: 18px;"
)
self.status_indicator.settooltip("未连接")
self.statusbar().showmessage("已断开")
# ---- 文件浏览 ----
def _on_file_list_received(self, items, current_path):
self.remote_browser.set_file_list(items, current_path)
self.statusbar().showmessage(
f"远程目录: {current_path} ({len(items)} 个项目)"
)
def _on_remote_file_action(self, name, is_dir):
if is_dir:
if name == "..":
self.ftp_worker.change_directory("..")
else:
self.ftp_worker.change_directory(name)
else:
# 文件被选中 — 弹出下载对话框
self._prompt_download(name)
def _on_remote_delete_request(self, name, is_dir):
kind = "目录" if is_dir else "文件"
reply = qmessagebox.question(
self, "确认删除",
f"确定要删除远程{kind}吗?\n\n名称: {name}\n\n"
"注意: 此操作不可恢复。",
qmessagebox.standardbutton.yes | qmessagebox.standardbutton.no
)
if reply == qmessagebox.standardbutton.yes:
if is_dir:
self.ftp_worker.delete_directory(name)
else:
self.ftp_worker.delete_file(name)
def _on_local_file_action(self, name, is_dir):
if is_dir:
if name == "..":
parent = os.path.dirname(self.local_browser.current_path)
self.local_browser.load_local_directory(parent)
else:
new_path = os.path.join(self.local_browser.current_path, name)
self.local_browser.load_local_directory(new_path)
else:
# 本地文件被选中 — 弹出上传对话框
full_path = os.path.join(self.local_browser.current_path, name)
self._prompt_upload(full_path)
def _go_up_remote(self):
self.ftp_worker.change_directory("..")
def _refresh_remote(self):
self.ftp_worker.list_directory()
def _browse_local(self):
directory = qfiledialog.getexistingdirectory(
self, "选择本地目录", self.local_browser.current_path
)
if directory:
self.local_browser.load_local_directory(directory)
# ---- 文件传输 ----
def _prompt_download(self, remote_name):
save_path, _ = qfiledialog.getsavefilename(
self, "下载文件", os.path.join(os.path.expanduser("~"), remote_name)
)
if save_path:
self.transfer_widget.add_transfer(remote_name, "download")
self.ftp_worker.download_file(remote_name, save_path)
def _prompt_upload(self, local_path):
remote_name = os.path.basename(local_path)
reply = qmessagebox.question(
self, "确认上传",
f"上传文件到远程当前目录?\n\n文件: {remote_name}",
qmessagebox.standardbutton.yes | qmessagebox.standardbutton.no
)
if reply == qmessagebox.standardbutton.yes:
self.transfer_widget.add_transfer(remote_name, "upload")
self.ftp_worker.upload_file(local_path, remote_name)
def _on_transfer_progress(self, filename, transferred, total):
self.transfer_widget.update_progress(filename, transferred, total)
def _on_transfer_complete(self, filename, success, message):
self.transfer_widget.complete_transfer(filename, success, message)
# 传输完成后刷新远程目录
if success:
self.ftp_worker.list_directory()
# ---- 日志 ----
def _on_log_message(self, category, message):
self.debug_console.append_log(category, message)
if category == "info":
self.statusbar().showmessage(message)
def _on_raw_command_result(self, result):
self.debug_console.append_log("resp", f"[命令结果] {result}")
# ---- 设置保存/加载 ----
def _load_settings(self):
settings = qsettings("ftpdebugassistant", "ftphelper")
host = settings.value("host", "127.0.0.1")
port = settings.value("port", "2121")
user = settings.value("user", "admin")
self.host_input.settext(host)
self.port_input.settext(str(port))
self.user_input.settext(user)
def _save_settings(self):
settings = qsettings("ftpdebugassistant", "ftphelper")
settings.setvalue("host", self.host_input.text())
settings.setvalue("port", self.port_input.text())
settings.setvalue("user", self.user_input.text())
def closeevent(self, event):
self._save_settings()
self.ftp_worker.stop()
event.accept()
# ============================================================
# 入口
# ============================================================
if __name__ == "__main__":
app = qapplication(sys.argv)
app.setstyle("fusion")
# 设置调色板
palette = qpalette()
palette.setcolor(qpalette.colorrole.window, qcolor(245, 245, 245))
palette.setcolor(qpalette.colorrole.windowtext, qcolor(33, 33, 33))
palette.setcolor(qpalette.colorrole.base, qcolor(255, 255, 255))
palette.setcolor(qpalette.colorrole.alternatebase, qcolor(240, 240, 240))
palette.setcolor(qpalette.colorrole.tooltipbase, qcolor(255, 255, 255))
palette.setcolor(qpalette.colorrole.tooltiptext, qcolor(33, 33, 33))
palette.setcolor(qpalette.colorrole.text, qcolor(33, 33, 33))
palette.setcolor(qpalette.colorrole.button, qcolor(240, 240, 240))
palette.setcolor(qpalette.colorrole.buttontext, qcolor(33, 33, 33))
palette.setcolor(qpalette.colorrole.highlight, qcolor(52, 152, 219))
palette.setcolor(qpalette.colorrole.highlightedtext, qcolor(255, 255, 255))
app.setpalette(palette)
# 全局样式
app.setstylesheet("""
qgroupbox {
font-weight: bold;
border: 1px solid #ddd;
border-radius: 6px;
margin-top: 10px;
padding-top: 14px;
}
qgroupbox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px;
}
qlineedit {
border: 1px solid #ccc;
border-radius: 4px;
padding: 3px 6px;
background: white;
}
qlineedit:focus {
border-color: #3498db;
}
qpushbutton {
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 12px;
background: #f0f0f0;
}
qpushbutton:hover {
background: #e0e0e0;
}
qpushbutton:pressed {
background: #d0d0d0;
}
qtreewidget {
border: 1px solid #ddd;
border-radius: 4px;
background: white;
alternate-background-color: #f8f8f8;
}
qtreewidget::item:selected {
background-color: #3498db;
color: white;
}
qheaderview::section {
background-color: #ecf0f1;
padding: 4px;
border: 1px solid #ddd;
font-weight: bold;
}
qtextedit {
border: 1px solid #ddd;
border-radius: 4px;
background: #1e1e1e;
color: #d4d4d4;
}
qprogressbar {
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
}
qprogressbar::chunk {
background-color: #3498db;
border-radius: 3px;
}
""")
window = mainwindow()
window.show()
sys.exit(app.exec())
4、ftp_server_test.py(测试使用)
"""
ftp 服务器测试例程
用于测试 ftp 客户端调试助手
"""
import os
import sys
import threading
from pyftpdlib.authorizers import dummyauthorizer
from pyftpdlib.handlers import ftphandler
from pyftpdlib.servers import ftpserver
class simpleftpserver:
"""简单的 ftp 服务器"""
def __init__(self, host="127.0.0.1", port=2121, directory="./ftp_root",
username="admin", password="123456"):
self.host = host
self.port = port
self.directory = directory
self.username = username
self.password = password
self.server = none
self.running = false
# 确保目录存在
os.makedirs(directory, exist_ok=true)
# 创建一些测试文件
self._create_test_files()
def _create_test_files(self):
"""创建测试文件"""
test_files = [
("test1.txt", "这是测试文件 1"),
("test2.txt", "这是测试文件 2"),
("readme.txt", "ftp 服务器测试文件\n用于测试客户端功能"),
]
for filename, content in test_files:
filepath = os.path.join(self.directory, filename)
if not os.path.exists(filepath):
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✓ 创建测试文件: {filename}")
# 创建子目录
subdir = os.path.join(self.directory, "subdir")
os.makedirs(subdir, exist_ok=true)
if not os.path.exists(os.path.join(subdir, "test3.txt")):
with open(os.path.join(subdir, "test3.txt"), 'w', encoding='utf-8') as f:
f.write("这是子目录中的测试文件")
print(f"✓ 创建子目录和文件: subdir/test3.txt")
def start(self):
"""启动 ftp 服务器"""
try:
# 创建授权器
authorizer = dummyauthorizer()
# 添加用户(完整权限)
authorizer.add_user(
self.username,
self.password,
self.directory,
perm="elradfmw"
)
# 添加匿名用户(只读)
authorizer.add_anonymous(self.directory, perm="elr")
# 创建 ftp 处理器
handler = ftphandler
handler.authorizer = authorizer
# 设置被动模式端口范围
handler.passive_ports = range(60000, 60100)
# 创建服务器
self.server = ftpserver((self.host, self.port), handler)
self.server.max_cons = 256
self.server.max_cons_per_ip = 5
self.running = true
print(f"\n{'='*60}")
print(f"ftp 服务器已启动")
print(f"{'='*60}")
print(f"主机: {self.host}")
print(f"端口: {self.port}")
print(f"用户名: {self.username}")
print(f"密码: {self.password}")
print(f"根目录: {self.directory}")
print(f"{'='*60}")
print(f"\n按 ctrl+c 停止服务器\n")
# 启动服务器
self.server.serve_forever()
except keyboardinterrupt:
print("\n\n正在停止服务器...")
self.stop()
except exception as e:
print(f"服务器错误: {e}")
self.running = false
def stop(self):
"""停止 ftp 服务器"""
if self.server:
self.server.close_all()
self.running = false
print("✓ 服务器已停止")
def main():
"""主函数"""
print("\n" + "="*60)
print("ftp 服务器测试例程")
print("="*60)
# 默认配置
host = "127.0.0.1"
port = 2121
directory = r"d:\user\01417804\桌面\pythonproject\image"
username = "admin"
password = "123456"
print("\n服务器配置:")
print(f" 主机: {host} (回车使用默认值)")
print(f" 端口: {port} (回车使用默认值)")
print(f" 目录: {directory} (回车使用默认值)")
print(f" 用户名: {username} (回车使用默认值)")
print(f" 密码: {password} (回车使用默认值)")
print()
# 获取用户输入
host = input("主机地址: ").strip() or host
port = int(input("端口号: ").strip() or port)
directory = input("根目录: ").strip() or directory
username = input("用户名: ").strip() or username
password = input("密码: ").strip() or password
# 创建并启动服务器
server = simpleftpserver(host, port, directory, username, password)
server.start()
if __name__ == "__main__":
main()
三、实现原理
本工具的核心设计思路是:在 ftplib.ftp 的基础上做一层“协议级”的拦截与封装,配合 qt 的信号槽机制,把耗时的网络操作放到后台线程执行,从而既保证界面流畅,又能把每一次 ftp 命令与响应实时呈现到调试控制台。 下面从几个关键点展开说明。
1、协议拦截:继承 ftp 重写 putline / getline
ftp 协议本质上是基于 tcp 的文本协议,客户端与服务器之间通过 ascii 命令和响应进行交互。ftplib.ftp 内部把“发送命令”和“读取响应”分别封装在 putline() 与 getline() 两个方法中。
因此,只要继承 ftp 并重写这两个方法,就能在不改动任何业务逻辑的前提下,捕获到客户端发出的每一条命令和服务器返回的每一条响应:
class debugftp(ftp):
def putline(self, line):
if self._log_callback:
self._log_callback("cmd", line) # 拦截发出的命令
super().putline(line)
def getline(self):
line = super().getline()
if self._log_callback:
self._log_callback("resp", line) # 拦截收到的响应
return line
putline在命令真正写入 socket 之前被调用,此时line就是即将发送的原始命令文本(如user admin、pwd、stor test.txt)。getline在从 socket 读取到一行响应后被调用,此时line就是服务器返回的原始响应(如230 login successful.、257 "/" is the current directory.)。- 通过构造函数传入的
log_callback回调,把("cmd", line)或("resp", line)交给上层,再由信号log_message转发到调试控制台。
这样做的最大好处是零侵入:ftplib 内部所有命令(login、cwd、storbinary、retrbinary、mlsd 等)最终都会走这两个方法,因此日志覆盖是完整且自动的,无需为每个命令单独埋点。
2、线程模型:qthread + 任务队列
ftp 网络操作(连接、列目录、上传、下载)都是阻塞调用,如果直接放在 gui 主线程执行,界面会卡死。因此本项目把 ftpworkerthread 作为唯一的 ftp 操作执行者,运行在后台线程中。
2.1 任务队列
工作线程内部维护一个线程安全的任务队列:
self._task_queue = []
self._lock = threading.lock()
def _enqueue(self, task_type, params):
with self._lock:
self._task_queue.append((task_type, params))
主线程(界面)通过 connect_to_server()、upload_file() 等方法向队列追加任务,工作线程在 run() 中循环取出并执行:
def run(self):
while not self._stop_flag:
task = none
with self._lock:
if self._task_queue:
task = self._task_queue.pop(0)
if task is none:
self.msleep(50)
continue
task_type, params = task
try:
# 根据 task_type 分发到对应的 _do_xxx 方法
...
except exception as e:
self.signals.log_message.emit("error", f"操作失败: {str(e)}")
- 队列为空时线程
msleep(50)让出 cpu,避免空转。 - 所有任务在同一个后台线程中串行执行,天然避免了多个 ftp 操作并发导致的协议状态错乱(ftp 是有状态协议,同一连接上并发操作会互相干扰)。
- 每个任务用
try/except包裹,任何异常都不会导致线程退出,而是通过信号上报给界面。
2.2 线程安全
- 任务入队使用
threading.lock保护,保证主线程与工作线程对队列的并发访问安全。 - 工作线程不直接操作任何界面控件,只通过
ftpsignals信号把结果发回主线程;主线程中的槽函数(如_on_file_list_received)负责更新界面。这符合 qt 的线程规则:只有主线程可以操作 gui 控件。
3、信号槽通信:ftpsignals
ftpsignals 继承自 qobject,集中定义了工作线程与主线程之间的所有通信信号:
class ftpsignals(qobject):
log_message = pyqtsignal(str, str) # (类别, 消息)
file_list_received = pyqtsignal(list, str) # (文件列表, 当前路径)
transfer_progress = pyqtsignal(str, int, int) # (文件名, 已传输, 总大小)
transfer_complete = pyqtsignal(str, bool, str) # (文件名, 成功, 消息)
connection_changed = pyqtsignal(bool) # 连接状态
raw_command_result = pyqtsignal(str) # 原始命令执行结果
log_message:携带类别(cmd/resp/info/error/success)和消息文本,调试控制台据此用不同颜色渲染。file_list_received:携带解析后的文件列表和当前路径,远程文件浏览器据此刷新。transfer_progress/transfer_complete:驱动传输队列的进度条和完成状态。connection_changed:通知主线程更新连接按钮的可用状态和状态指示灯。
由于 pyqtsignal 是跨线程安全的,工作线程 emit 的信号会自动排队到主线程的事件循环中执行对应的槽函数,从而实现了线程间安全通信。
4、目录列表解析:mlsd 优先,list 兜底
ftp 列目录有两种常见方式:
- mlsd:返回结构化数据(类型、大小、修改时间等),解析简单可靠,但需要服务器支持(rfc 3659)。
- list:返回人类可读的文本列表,格式因服务器而异(unix 风格、dos 风格等),解析较繁琐。
本工具的策略是优先尝试 mlsd,失败则回退到 list:
try:
for name, facts in self.ftp.mlsd():
is_dir = facts.get("type", "") == "dir"
size = int(facts.get("size", 0))
modify = facts.get("modify", "")
items.append((name, size, is_dir, modify))
except error_perm:
lines = []
self.ftp.retrlines("list", lines.append)
for line in lines:
parsed = self._parse_list_line(line)
if parsed:
items.append(parsed)
_parse_list_line 同时兼容 unix 和 dos 两种常见格式:
- unix 格式:
drwxr-xr-x 2 user group 4096 jan 01 12:00 filename,通过权限位首字符判断类型,取第 5 个字段为大小,第 9 个字段为文件名。 - dos 格式:
01-01-24 12:00am <dir> dirname,通过<dir>标记判断目录,否则取数字字段为大小。
5、上传/下载进度跟踪
5.1 上传:包装文件对象
上传时通过 storbinary 逐块读取本地文件并发送。为了跟踪进度,把文件对象包装一层,在每次 read() 时累计已读字节数:
class trackingfile:
def read(self, *args, **kwargs):
data = self._f.read(*args, **kwargs)
if data:
transferred[0] += len(data)
now = time.time()
if now - last_emit[0] > 0.1:
self.signals.transfer_progress.emit(filename, transferred[0], file_size)
last_emit[0] = now
return data
- 用
transferred[0]列表而非普通变量,是为了在闭包中修改外部变量。 - 通过
time.time()做节流:每 100ms 才发一次进度信号,避免高频信号刷爆界面。
5.2 下载:回调函数
下载时通过 retrbinary 的 callback 参数接收数据块,同样累计字节数并节流发送进度:
def on_data(data):
f.write(data)
transferred[0] += len(data)
now = time.time()
if now - last_emit[0] > 0.1:
self.signals.transfer_progress.emit(filename, transferred[0], file_size)
last_emit[0] = now
下载前先通过 ftp.size() 获取文件总大小,用于计算百分比;若获取失败则进度条显示为不确定状态。
6、原始命令执行
调试控制台允许用户手动输入任意 ftp 命令。_do_raw_command 对常见命令做了专门处理:
list/nlst:通过retrlines获取多行结果。pwd/cwd/size/mkd/rmd/dele/ren:调用ftplib对应方法。- 其他命令:通过
sendcmd原样发送并返回服务器响应。
else:
resp = self.ftp.sendcmd(cmd)
result = resp
这样既覆盖了常用操作,又保留了任意命令透传的能力,方便调试非标准或扩展的 ftp 命令。
7、小结
| 设计点 | 实现方式 | 解决的问题 |
|---|---|---|
| 协议拦截 | 继承 ftp 重写 putline / getline | 零侵入捕获全部命令与响应 |
| 后台执行 | qthread + 任务队列 | 避免网络阻塞卡死界面 |
| 线程通信 | pyqtsignal 信号槽 | 跨线程安全更新界面 |
| 目录解析 | mlsd 优先 + list 兜底 | 兼容不同服务器 |
| 进度跟踪 | 包装文件对象 / 回调 + 节流 | 实时且不刷爆界面 |
| 原始命令 | sendcmd 透传 | 支持任意 ftp 命令调试 |
通过以上设计,本工具既是一个可用的 ftp 客户端,更是一个协议级调试器——所有交互过程完全透明可见,非常适合学习 ftp 协议或排查服务器问题。
以上就是python基于pyqt实现ftp客户端调试助手的详细内容,更多关于python pyqt实现ftp客户端调试助手的资料请关注代码网其它相关文章!
发表评论