first commit

This commit is contained in:
2026-05-10 10:56:52 +08:00
commit 9dec38db60
35 changed files with 8206 additions and 0 deletions
+281
View File
@@ -0,0 +1,281 @@
import os
gui_path = 'src/virtualbox_client/gui.py'
with open(gui_path, 'r', encoding='utf-8') as f:
content = f.read()
# ===== 1. 添加 ScriptWorker 类(放在 DownloadWorker 之后) =====
old = """class WorkspaceWorker(QObject):"""
new = """class ScriptWorker(QObject):
"""上传本地脚本到虚拟机、执行并下载日志"""
log_signal = Signal(str, str)
finished_signal = Signal(bool)
error_signal = Signal(str)
def __init__(self, config: dict, local_path: str):
super().__init__()
self.config = config
self.local_path = local_path
self.cancel_event = threading.Event()
def cancel(self):
self.cancel_event.set()
def run(self):
ssh_only = self.config.get("ssh_only", False)
_com_initialized = False
if not ssh_only:
try:
import ctypes
ctypes.windll.ole32.CoInitializeEx(None, 0)
_com_initialized = True
except Exception:
pass
try:
if ssh_only:
from virtualbox_client.utils.ssh_manager import SSHManager
vm = SSHManager(
self.config["ssh_host"], self.config["ssh_port"],
self.config["ssh_user"], self.config["ssh_pass"],
)
else:
from virtualbox_client.utils.vbox_ssh_manager import VBoxSSHManager
vm = VBoxSSHManager(
self.config["vm_name"], self.config["ssh_host"],
self.config["ssh_port"], self.config["ssh_user"],
self.config["ssh_pass"],
)
vm.set_cancel_event(self.cancel_event)
if self.cancel_event.is_set():
return
import builtins
original_print = print
def custom_print(*args, **kwargs):
msg = " ".join(str(a) for a in args)
self.log_signal.emit(msg, "info")
original_print(*args, **kwargs)
builtins.print = custom_print
try:
script_name = os.path.basename(self.local_path)
remote_path = f"/tmp/{script_name}"
log_path = f"/tmp/{script_name}.log"
local_log_path = f"{script_name}.log"
# 1. 上传脚本
self.log_signal.emit(f"📤 [1/3] 上传脚本: {self.local_path} -> {remote_path}", "info")
if not vm.upload_file(self.local_path, remote_path):
self.log_signal.emit("❌ 上传失败", "error")
return
# 2. 执行脚本
self.log_signal.emit(f"⚡ [2/3] 执行脚本: {remote_path}", "info")
cmd = (
f"chmod +x {remote_path} && "
f"echo '{self.config['ssh_pass']}' | sudo -S {remote_path} 2>&1 | "
f"tee {log_path}"
)
out, err, code = vm.exec_command(cmd, get_pty=False)
self.log_signal.emit(f"✔ 脚本执行完成,退出码: {code}", "info")
# 3. 下载日志
self.log_signal.emit(f"📥 [3/3] 下载日志: {log_path} -> {local_log_path}", "info")
if vm.download_file(log_path, local_log_path):
self.log_signal.emit(f"✔ 日志已保存到本地: {local_log_path}", "success")
else:
self.log_signal.emit("⚠ tee 日志下载失败,尝试保存终端输出", "warning")
if out:
with open(local_log_path, "w") as f:
f.write(out)
self.log_signal.emit(f"✔ 终端输出已保存到: {local_log_path}", "success")
# 4. 清理远程临时文件
vm.exec_command(f'rm -f "{remote_path}" "{log_path}"')
self.log_signal.emit("🧹 远程临时文件已清理", "info")
self.log_signal.emit("🎉 脚本执行全部完成!", "success")
self.finished_signal.emit(True)
finally:
builtins.print = original_print
except ImportError as e:
self.log_signal.emit(f"❌ 缺少依赖: {e}", "error")
self.error_signal.emit(str(e))
except Exception as e:
self.log_signal.emit(f"❌ 脚本执行错误: {str(e)}", "error")
self.error_signal.emit(str(e))
finally:
if _com_initialized:
try:
import ctypes
ctypes.windll.ole32.CoUninitialize()
except Exception:
pass
class WorkspaceWorker(QObject):"""
content = content.replace(old, new)
# ===== 2. 在 TaskPanel.setup_ui 的文件传输 tab 后面添加脚本执行 tab =====
old_file_tab_end = """ file_layout.addStretch()
self.tab_widget.addTab(file_tab, "📁 文件传输")"""
new_file_tab_end = """ file_layout.addStretch()
self.tab_widget.addTab(file_tab, "📁 文件传输")
# ====== 脚本执行 Tab ======
script_tab = QWidget()
script_layout = QVBoxLayout(script_tab)
script_layout.setContentsMargins(8, 8, 8, 8)
script_group = QGroupBox("⚡ 上传并执行脚本")
script_group_layout = QVBoxLayout(script_group)
script_path_layout = QHBoxLayout()
self.script_path_edit = QLineEdit(placeholderText="选择本地 .sh 脚本文件...")
script_path_layout.addWidget(self.script_path_edit)
self.script_browse_btn = QPushButton("📂 浏览")
self.script_browse_btn.setFixedWidth(80)
self.script_browse_btn.clicked.connect(self._browse_script)
script_path_layout.addWidget(self.script_browse_btn)
script_group_layout.addWidget(QLabel("本地脚本路径:"))
script_group_layout.addLayout(script_path_layout)
self.run_script_btn = QPushButton("⚡ 上传并执行脚本")
self.run_script_btn.setStyleSheet(\"""
QPushButton { background-color: #8e44ad; color: white; padding: 10px 30px;
font-size: 14px; font-weight: bold; border: none; border-radius: 6px; }
QPushButton:hover { background-color: #9b59b6; }
QPushButton:disabled { background-color: #555; color: #999; }
\""")
self.run_script_btn.clicked.connect(self._run_script)
script_group_layout.addWidget(self.run_script_btn)
script_layout.addWidget(script_group)
# 执行说明
desc_group = QGroupBox("📖 说明")
desc_layout = QVBoxLayout(desc_group)
desc_label = QLabel(
"1. 选择一个本地 .sh 脚本文件\\n"
"2. 点击按钮自动完成:\\n"
" 📤 上传脚本到虚拟机 /tmp/ 目录\\n"
" ⚡ 以 sudo 权限执行脚本\\n"
" 📥 下载执行日志到本地\\n"
" 🧹 清理远程临时文件"
)
desc_label.setStyleSheet("color: #aaa; font-size: 12px; line-height: 1.6;")
desc_layout.addWidget(desc_label)
script_layout.addWidget(desc_group)
script_layout.addStretch()
self.tab_widget.addTab(script_tab, "⚡ 脚本执行")"""
content = content.replace(old_file_tab_end, new_file_tab_end)
# ===== 3. 在 TaskPanel 的 _set_ui_running 中禁用脚本按钮 =====
old_running = """ def _set_ui_running(self, running: bool):
self.run_btn.setEnabled(not running)
self.cancel_btn.setEnabled(running)
self.task_input.setEnabled(not running)
if self._config_panel:
self._config_panel.set_enabled(not running)"""
new_running = """ def _set_ui_running(self, running: bool):
self.run_btn.setEnabled(not running)
self.cancel_btn.setEnabled(running)
self.task_input.setEnabled(not running)
self.run_script_btn.setEnabled(not running)
self.script_browse_btn.setEnabled(not running)
self.script_path_edit.setEnabled(not running)
if self._config_panel:
self._config_panel.set_enabled(not running)"""
content = content.replace(old_running, new_running)
# ===== 4. 在 TaskPanel 中添加 _browse_script 和 _run_script 方法 =====
# 放在 _on_download_error 之后、class MainWindow 之前
old_mainwindow = """class MainWindow(QMainWindow):"""
new_methods = """ _script_thread = None
_script_worker = None
def _cleanup_script(self):
if self._script_thread:
try:
self._script_thread.finished.disconnect()
except (TypeError, RuntimeError):
pass
if self._script_thread.isRunning():
self._script_thread.quit()
if not self._script_thread.wait(5000):
self._script_thread.terminate()
self._script_thread.wait(3000)
self._script_thread = None
if self._script_worker:
try:
self._script_worker.log_signal.disconnect()
self._script_worker.finished_signal.disconnect()
self._script_worker.error_signal.disconnect()
except (TypeError, RuntimeError):
pass
self._script_worker = None
def _browse_script(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "选择要执行的脚本", "", "Shell 脚本 (*.sh);;所有文件 (*)"
)
if file_path:
self.script_path_edit.setText(file_path)
def _run_script(self):
if not self._config_panel or not self._config_panel.validate():
return
local_path = self.script_path_edit.text().strip()
if not local_path:
QMessageBox.warning(self, "提示", "请选择要执行的脚本文件")
return
if not os.path.isfile(local_path):
QMessageBox.warning(self, "提示", f"本地脚本不存在: {local_path}")
return
self._cleanup_script()
self._set_ui_running(True)
self.tab_widget.setCurrentIndex(0) # 切换到任务日志 tab
self.log_panel.clear_log()
config = self._config_panel.get_config()
self._script_worker = ScriptWorker(config, local_path)
self._script_thread = QThread(self)
self._script_worker.moveToThread(self._script_thread)
self._script_worker.log_signal.connect(self.log_panel.append_log)
self._script_worker.finished_signal.connect(self._on_script_finished)
self._script_worker.error_signal.connect(self._on_script_error)
self._script_thread.started.connect(self._script_worker.run)
self._script_thread.finished.connect(self._cleanup_script)
self._script_thread.start()
def _on_script_finished(self, success: bool):
self._set_ui_running(False)
if success:
self.log_panel.append_log("✅ 脚本执行已完成", "success")
def _on_script_error(self, error_msg: str):
self._set_ui_running(False)
self.log_panel.append_log(f"❌ 脚本执行失败: {error_msg}", "error")
""" + old_mainwindow
content = content.replace(old_mainwindow, new_methods)
with open(gui_path, 'w', encoding='utf-8') as f:
f.write(content)
print('✅ gui.py 已集成脚本执行功能')
+275
View File
@@ -0,0 +1,275 @@
# This script adds ScriptWorker and a script execution tab to gui.py
import os
gui_path = "src/virtualbox_client/gui.py"
with open(gui_path, "r", encoding="utf-8") as f:
content = f.read()
# 1. Add ScriptWorker before WorkspaceWorker
old_worker = "class WorkspaceWorker(QObject):"
new_worker = """class ScriptWorker(QObject):
log_signal = Signal(str, str)
finished_signal = Signal(bool)
error_signal = Signal(str)
def __init__(self, config: dict, local_path: str):
super().__init__()
self.config = config
self.local_path = local_path
self.cancel_event = threading.Event()
def cancel(self):
self.cancel_event.set()
def run(self):
ssh_only = self.config.get("ssh_only", False)
_com_initialized = False
if not ssh_only:
try:
import ctypes
ctypes.windll.ole32.CoInitializeEx(None, 0)
_com_initialized = True
except Exception:
pass
try:
if ssh_only:
from virtualbox_client.utils.ssh_manager import SSHManager
vm = SSHManager(
self.config["ssh_host"], self.config["ssh_port"],
self.config["ssh_user"], self.config["ssh_pass"],
)
else:
from virtualbox_client.utils.vbox_ssh_manager import VBoxSSHManager
vm = VBoxSSHManager(
self.config["vm_name"], self.config["ssh_host"],
self.config["ssh_port"], self.config["ssh_user"],
self.config["ssh_pass"],
)
vm.set_cancel_event(self.cancel_event)
if self.cancel_event.is_set():
return
import builtins
original_print = print
def custom_print(*args, **kwargs):
msg = " ".join(str(a) for a in args)
self.log_signal.emit(msg, "info")
original_print(*args, **kwargs)
builtins.print = custom_print
try:
import os as _os
script_name = _os.path.basename(self.local_path)
remote_path = "/tmp/" + script_name
log_path = "/tmp/" + script_name + ".log"
local_log_path = script_name + ".log"
self.log_signal.emit("[1/3] Upload: " + self.local_path + " -> " + remote_path, "info")
if not vm.upload_file(self.local_path, remote_path):
self.log_signal.emit("Upload failed", "error")
return
self.log_signal.emit("[2/3] Execute: " + remote_path, "info")
cmd = (
"chmod +x " + remote_path + " && "
"echo '" + self.config["ssh_pass"] + "' | sudo -S " + remote_path + " 2>&1 | "
"tee " + log_path
)
out, err, code = vm.exec_command(cmd, get_pty=False)
self.log_signal.emit("Exit code: " + str(code), "info")
self.log_signal.emit("[3/3] Download log: " + log_path + " -> " + local_log_path, "info")
if vm.download_file(log_path, local_log_path):
self.log_signal.emit("Log saved: " + local_log_path, "success")
else:
self.log_signal.emit("Tee log download failed, saving terminal output", "warning")
if out:
with open(local_log_path, "w") as f:
f.write(out)
self.log_signal.emit("Output saved: " + local_log_path, "success")
vm.exec_command("rm -f \\"" + remote_path + "\\" \\"" + log_path + "\\"")
self.log_signal.emit("Remote temp files cleaned", "info")
self.log_signal.emit("Script execution complete!", "success")
self.finished_signal.emit(True)
finally:
builtins.print = original_print
except ImportError as e:
self.log_signal.emit("Missing dependency: " + str(e), "error")
self.error_signal.emit(str(e))
except Exception as e:
self.log_signal.emit("Script error: " + str(e), "error")
self.error_signal.emit(str(e))
finally:
if _com_initialized:
try:
import ctypes
ctypes.windll.ole32.CoUninitialize()
except Exception:
pass
""" + old_worker
content = content.replace(old_worker, new_worker)
# 2. Add script tab after file transfer tab
old_tab_end = ' file_layout.addStretch()\n self.tab_widget.addTab(file_tab, "\U0001f4c1 \u6587\u4ef6\u4f20\u8f93")'
new_tab_end = ''' file_layout.addStretch()
self.tab_widget.addTab(file_tab, "\U0001f4c1 \u6587\u4ef6\u4f20\u8f93")
# ====== Script Execution Tab ======
script_tab = QWidget()
script_layout = QVBoxLayout(script_tab)
script_layout.setContentsMargins(8, 8, 8, 8)
script_group = QGroupBox("Upload and Execute Script")
script_group_layout = QVBoxLayout(script_group)
script_path_layout = QHBoxLayout()
self.script_path_edit = QLineEdit(placeholderText="Select local .sh script file...")
script_path_layout.addWidget(self.script_path_edit)
self.script_browse_btn = QPushButton("Browse")
self.script_browse_btn.setFixedWidth(80)
self.script_browse_btn.clicked.connect(self._browse_script)
script_path_layout.addWidget(self.script_browse_btn)
script_group_layout.addWidget(QLabel("Local Script:"))
script_group_layout.addLayout(script_path_layout)
self.run_script_btn = QPushButton("Upload and Execute Script")
self.run_script_btn.setStyleSheet("""
QPushButton { background-color: #8e44ad; color: white; padding: 10px 30px;
font-size: 14px; font-weight: bold; border: none; border-radius: 6px; }
QPushButton:hover { background-color: #9b59b6; }
QPushButton:disabled { background-color: #555; color: #999; }
""")
self.run_script_btn.clicked.connect(self._run_script)
script_group_layout.addWidget(self.run_script_btn)
script_layout.addWidget(script_group)
desc_group = QGroupBox("How it works")
desc_layout = QVBoxLayout(desc_group)
desc_label = QLabel(
"1. Select a local .sh script file\\n"
"2. Click button to auto:\\n"
" Upload script to /tmp/\\n"
" Execute with sudo\\n"
" Download execution log\\n"
" Clean remote temp files"
)
desc_label.setStyleSheet("color: #aaa; font-size: 12px; line-height: 1.6;")
desc_layout.addWidget(desc_label)
script_layout.addWidget(desc_group)
script_layout.addStretch()
self.tab_widget.addTab(script_tab, "Script")'''
content = content.replace(old_tab_end, new_tab_end)
# 3. Update _set_ui_running
old_running = """ def _set_ui_running(self, running: bool):
self.run_btn.setEnabled(not running)
self.cancel_btn.setEnabled(running)
self.task_input.setEnabled(not running)
if self._config_panel:
self._config_panel.set_enabled(not running)"""
new_running = """ def _set_ui_running(self, running: bool):
self.run_btn.setEnabled(not running)
self.cancel_btn.setEnabled(running)
self.task_input.setEnabled(not running)
self.run_script_btn.setEnabled(not running)
self.script_browse_btn.setEnabled(not running)
self.script_path_edit.setEnabled(not running)
if self._config_panel:
self._config_panel.set_enabled(not running)"""
content = content.replace(old_running, new_running)
# 4. Add methods before MainWindow
old_mw = "class MainWindow(QMainWindow):"
new_mw = """ _script_thread = None
_script_worker = None
def _cleanup_script(self):
if self._script_thread:
try:
self._script_thread.finished.disconnect()
except (TypeError, RuntimeError):
pass
if self._script_thread.isRunning():
self._script_thread.quit()
if not self._script_thread.wait(5000):
self._script_thread.terminate()
self._script_thread.wait(3000)
self._script_thread = None
if self._script_worker:
try:
self._script_worker.log_signal.disconnect()
self._script_worker.finished_signal.disconnect()
self._script_worker.error_signal.disconnect()
except (TypeError, RuntimeError):
pass
self._script_worker = None
def _browse_script(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "Select script", "", "Shell scripts (*.sh);;All files (*)"
)
if file_path:
self.script_path_edit.setText(file_path)
def _run_script(self):
if not self._config_panel or not self._config_panel.validate():
return
local_path = self.script_path_edit.text().strip()
if not local_path:
QMessageBox.warning(self, "Hint", "Please select a script file")
return
if not os.path.isfile(local_path):
QMessageBox.warning(self, "Hint", "File not found: " + local_path)
return
self._cleanup_script()
self._set_ui_running(True)
self.tab_widget.setCurrentIndex(0)
self.log_panel.clear_log()
config = self._config_panel.get_config()
self._script_worker = ScriptWorker(config, local_path)
self._script_thread = QThread(self)
self._script_worker.moveToThread(self._script_thread)
self._script_worker.log_signal.connect(self.log_panel.append_log)
self._script_worker.finished_signal.connect(self._on_script_finished)
self._script_worker.error_signal.connect(self._on_script_error)
self._script_thread.started.connect(self._script_worker.run)
self._script_thread.finished.connect(self._cleanup_script)
self._script_thread.start()
def _on_script_finished(self, success: bool):
self._set_ui_running(False)
if success:
self.log_panel.append_log("Script execution completed", "success")
def _on_script_error(self, error_msg: str):
self._set_ui_running(False)
self.log_panel.append_log("Script error: " + error_msg, "error")
""" + old_mw
content = content.replace(old_mw, new_mw)
with open(gui_path, "w", encoding="utf-8") as f:
f.write(content)
print("Done")
+18
View File
@@ -0,0 +1,18 @@
import ast
import sys
files = [
"src/virtualbox_client/gui.py",
"src/virtualbox_client/utils/vbox_ssh_manager.py",
"src/virtualbox_client/utils/vm_agent.py",
]
for f in files:
try:
tree = ast.parse(open(f, encoding="utf-8").read())
print(f"OK: {f.split(chr(47))[-1]} ({len(tree.body)} nodes)")
except SyntaxError as e:
print(f"FAIL: {f.split(chr(47))[-1]}: {e}")
sys.exit(1)
print("All files pass syntax check")
+13
View File
@@ -0,0 +1,13 @@
import os
import shutil
# 需要遍历的目录
root_dir = "./"
# 遍历目录
for dirpath, dirnames, filenames in os.walk(root_dir):
if "__pycache__" in dirnames:
# 获取 __pycache__ 目录的全路径
pycache_dir = os.path.join(dirpath, "__pycache__")
# 删除目录
shutil.rmtree(pycache_dir)
print(f"Removed: {pycache_dir}")
+45
View File
@@ -0,0 +1,45 @@
import os
import sys
def generate_markdown_from_py_files(directory, output_file):
with open(output_file, "w", encoding="utf-8") as md_file:
for root, dirs, files in os.walk(directory):
# 排除 venv 目录
dirs[:] = [d for d in dirs if d != ".venv"]
dirs[:] = [d for d in dirs if d != ".vscode"]
dirs[:] = [d for d in dirs if d != "node_modules"]
dirs[:] = [d for d in dirs if d != "build"]
dirs[:] = [d for d in dirs if d != "dist"]
dirs[:] = [d for d in dirs if d != ".sdk"]
for file in files:
if (
file.endswith(".py")
or file.endswith(".rs")
or file.endswith(".toml")
or file.endswith(".md")
or file.endswith(".css")
or file.endswith(".vue")
or file.endswith(".ts")
or file.endswith(".json")
or file.endswith(".html")
or file.endswith(".sh")
or file == "Dockerfile"
):
file_path = os.path.join(root, file)
if file == "output.md" or file == "package-lock.json":
continue
md_file.write(f"`{file_path}`\n")
md_file.write("```\n")
with open(file_path, "r", encoding="utf-8") as py_file:
md_file.write(py_file.read())
md_file.write("\n```\n\n")
if __name__ == "__main__":
# 指定目录和输出文件名
target_directory = sys.argv[1] # 替换为你的目标目录
output_markdown_file = "output.md" # 输出的 Markdown 文件名
generate_markdown_from_py_files(target_directory, output_markdown_file)
print(f"Markdown 文件已生成:{output_markdown_file}")
+30
View File
@@ -0,0 +1,30 @@
content = open("src/virtualbox_client/gui.py", encoding="utf-8").read()
# 1. 修复 except 语法
content = content.replace(
"except TypeError, RuntimeError:", "except (TypeError, RuntimeError):"
)
# 2. 修复 _rename_item 中的路径分隔符
old = """ dst_dir = os.path.dirname(src_path)
dst_path = os.path.join(dst_dir, new_name.strip())"""
new = """ dst_dir = src_path.rsplit('/', 1)[0] if '/' in src_path else src_path.rsplit('\\\\', 1)[0]
dst_path = dst_dir.rstrip('/') + '/' + new_name.strip()"""
content = content.replace(old, new)
# 3. 修复 _copy_item 中的路径分隔符
old2 = """ dst_dir = os.path.dirname(src_path)
dst_path = os.path.join(dst_dir, new_name)"""
new2 = """ dst_dir = src_path.rsplit('/', 1)[0] if '/' in src_path else src_path.rsplit('\\\\', 1)[0]
dst_path = dst_dir.rstrip('/') + '/' + new_name"""
content = content.replace(old2, new2)
# 4. 修复 _upload_file 中的 remote_dir 路径
old3 = """ remote_dir = os.path.dirname(remote_path)
vm.exec_command(f'mkdir -p "{remote_dir}"')"""
new3 = """ remote_dir = remote_path.rsplit('/', 1)[0] if '/' in remote_path else remote_path.rsplit('\\\\', 1)[0]
vm.exec_command(f'mkdir -p "{remote_dir}"')"""
content = content.replace(old3, new3)
open("src/virtualbox_client/gui.py", "w", encoding="utf-8").write(content)
print("✅ 所有修复完成")
+226
View File
@@ -0,0 +1,226 @@
import os
# 1. 给 SSHManager 添加 upload_directory 方法
ssh_path = "src/virtualbox_client/utils/ssh_manager.py"
with open(ssh_path, "r", encoding="utf-8") as f:
content = f.read()
old_method = """ def upload_file(self, local_path, remote_path):
\"\"\"上传本地文件到远程服务器,返回是否成功\"\"\"
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
sftp.put(local_path, remote_path)
sftp.close()
print(f\"✔ 上传成功: {local_path} -> {remote_path}\")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f\"✘ 上传失败: {e}\")
return False
"""
new_method = """ def upload_file(self, local_path, remote_path):
\"\"\"上传本地文件到远程服务器,返回是否成功\"\"\"
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
sftp.put(local_path, remote_path)
sftp.close()
print(f\"✔ 上传成功: {local_path} -> {remote_path}\")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f\"✘ 上传失败: {e}\")
return False
def upload_directory(self, local_dir, remote_dir):
\"\"\"递归上传整个文件夹到远程服务器,返回是否成功\"\"\"
import os as _os
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
local_dir = _os.path.normpath(local_dir)
remote_dir = remote_dir.rstrip('/')
# 创建远程根目录
self.exec_command(f'mkdir -p \"{remote_dir}\"')
for root, dirs, files in _os.walk(local_dir):
# 计算相对路径
rel_path = _os.path.relpath(root, local_dir)
if rel_path == '.':
rel_path = ''
# 创建远程子目录
target_dir = remote_dir + ('/' + rel_path.replace('\\\\', '/') if rel_path else '')
self.exec_command(f'mkdir -p \"{target_dir}\"')
for fname in files:
if self._cancelled.is_set():
sftp.close()
return False
local_file = _os.path.join(root, fname)
remote_file = target_dir + '/' + fname
sftp.put(local_file, remote_file)
print(f\" ✔ {local_file} -> {remote_file}\")
sftp.close()
print(f\"✔ 文件夹上传成功: {local_dir} -> {remote_dir}\")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f\"✘ 文件夹上传失败: {e}\")
return False
"""
content = content.replace(old_method, new_method)
with open(ssh_path, "w", encoding="utf-8") as f:
f.write(content)
print("✅ SSHManager 已添加 upload_directory")
# 2. 给 VBoxSSHManager 添加 upload_directory 方法
vbox_path = "src/virtualbox_client/utils/vbox_ssh_manager.py"
with open(vbox_path, "r", encoding="utf-8") as f:
content = f.read()
content = content.replace(old_method, new_method)
with open(vbox_path, "w", encoding="utf-8") as f:
f.write(content)
print("✅ VBoxSSHManager 已添加 upload_directory")
# 3. 修改 gui.py - 添加上传文件夹功能
gui_path = "src/virtualbox_client/gui.py"
with open(gui_path, "r", encoding="utf-8") as f:
content = f.read()
# 在 WorkspaceWorker 中添加 OPERATION_UPLOAD_DIR
old_op = ' OPERATION_UPLOAD = "upload"\n'
new_op = ' OPERATION_UPLOAD = "upload"\n OPERATION_UPLOAD_DIR = "upload_dir"\n'
content = content.replace(old_op, new_op)
# 在 run 方法中添加分支
old_run = """ elif self.operation == WorkspaceWorker.OPERATION_UPLOAD:
result = self._upload_file(vm)"""
new_run = """ elif self.operation == WorkspaceWorker.OPERATION_UPLOAD:
result = self._upload_file(vm)
elif self.operation == WorkspaceWorker.OPERATION_UPLOAD_DIR:
result = self._upload_directory(vm)"""
content = content.replace(old_run, new_run)
# 在 _upload_file 之后添加 _upload_directory 方法
old_upload = """ def _upload_file(self, vm) -> bool:
local_path = self.kwargs.get("local_path", "")
remote_path = self.kwargs.get("remote_path", "")
if not local_path or not remote_path:
return False
remote_dir = remote_path.rsplit('/', 1)[0] if '/' in remote_path else remote_path.rsplit('\\\\', 1)[0]
vm.exec_command(f'mkdir -p "{remote_dir}"')
return vm.upload_file(local_path, remote_path)"""
new_upload = """ def _upload_file(self, vm) -> bool:
local_path = self.kwargs.get("local_path", "")
remote_path = self.kwargs.get("remote_path", "")
if not local_path or not remote_path:
return False
remote_dir = remote_path.rsplit('/', 1)[0] if '/' in remote_path else remote_path.rsplit('\\\\', 1)[0]
vm.exec_command(f'mkdir -p "{remote_dir}"')
return vm.upload_file(local_path, remote_path)
def _upload_directory(self, vm) -> bool:
local_dir = self.kwargs.get("local_path", "")
remote_dir = self.kwargs.get("remote_path", "")
if not local_dir or not remote_dir:
return False
return vm.upload_directory(local_dir, remote_dir)"""
content = content.replace(old_upload, new_upload)
# 修改 _on_worker_finished 添加 UPLOAD_DIR 处理
old_finish = """ elif (
self._worker and self._worker.operation == WorkspaceWorker.OPERATION_UPLOAD
):
if result:
self.log_panel.append_log("✅ 上传成功", "success")
else:
self.log_panel.append_log("❌ 上传失败", "error")
QTimer.singleShot(500, self._soft_refresh)"""
new_finish = """ elif (
self._worker
and self._worker.operation
in (WorkspaceWorker.OPERATION_UPLOAD, WorkspaceWorker.OPERATION_UPLOAD_DIR)
):
if result:
self.log_panel.append_log("✅ 上传成功", "success")
else:
self.log_panel.append_log("❌ 上传失败", "error")
QTimer.singleShot(500, self._soft_refresh)"""
content = content.replace(old_finish, new_finish)
# 在 WorkspacePanel.setup_ui 的工具栏中添加"上传文件夹"按钮
old_upload_btn = """ self.upload_btn = QPushButton("📤 上传文件")"""
new_upload_btn = """ self.upload_btn = QPushButton("📤 上传文件")"""
# 保持原样,在后面添加新按钮
# 在 upload_btn 后面添加 upload_dir_btn
old_toolbar = """ toolbar.addWidget(self.upload_btn)
self.mkdir_btn = QPushButton("📂 新建文件夹")"""
new_toolbar = """ toolbar.addWidget(self.upload_btn)
self.upload_dir_btn = QPushButton("📁 上传文件夹")
self.upload_dir_btn.setStyleSheet(\"\"\"
QPushButton { background-color: #16a085; color: white; padding: 6px 14px;
font-size: 12px; font-weight: bold; border: none; border-radius: 4px; }
QPushButton:hover { background-color: #1abc9c; }
QPushButton:disabled { background-color: #555; color: #999; }
\"\"\")
self.upload_dir_btn.clicked.connect(self._upload_folder_to_workspace)
toolbar.addWidget(self.upload_dir_btn)
self.mkdir_btn = QPushButton("📂 新建文件夹")"""
content = content.replace(old_toolbar, new_toolbar)
# 更新 _set_busy 禁用 upload_dir_btn
old_set_busy = """ self.upload_btn.setEnabled(not busy)
self.mkdir_btn.setEnabled(not busy)
self.home_btn.setEnabled(not busy)"""
new_set_busy = """ self.upload_btn.setEnabled(not busy)
self.upload_dir_btn.setEnabled(not busy)
self.mkdir_btn.setEnabled(not busy)
self.home_btn.setEnabled(not busy)"""
content = content.replace(old_set_busy, new_set_busy)
# 在 _upload_to_workspace 方法之后添加 _upload_folder_to_workspace
old_upload_workspace = """ def _upload_to_workspace(self):"""
new_upload_workspace = """ def _upload_folder_to_workspace(self):
if not self._config_panel or not self._validate_ssh_config():
return
local_dir = QFileDialog.getExistingDirectory(self, "选择要上传到工作区的文件夹")
if not local_dir:
return
folder_name = os.path.basename(local_dir.rstrip('/\\\\'))
remote_path = f"~/workspace/{folder_name}"
self._start_worker(
WorkspaceWorker.OPERATION_UPLOAD_DIR,
local_path=local_dir,
remote_path=remote_path,
)
def _upload_to_workspace(self):"""
content = content.replace(old_upload_workspace, new_upload_workspace)
with open(gui_path, "w", encoding="utf-8") as f:
f.write(content)
print("✅ gui.py 已添加上传文件夹功能")
print("\\n所有修改完成!")