feat(synctool): 增加同步功能

This commit is contained in:
2025-12-21 07:49:19 +08:00
parent 0a365b568a
commit 1efe034a59
12 changed files with 1062 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import pathlib
import time
from textual.app import ComposeResult
from textual.containers import Horizontal, ScrollableContainer
@@ -18,22 +19,304 @@ class SyncScreen(Screen):
def __init__(self, nucleons: list = [], desc: str = ""):
super().__init__(name=None, id=None, classes=None)
self.sync_service = None
self.sync_config = {}
self.is_syncing = False
self.is_paused = False
self.log_messages = []
self.max_log_lines = 50
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
with ScrollableContainer(id="sync_container"):
pass
# 标题和连接状态
yield Static("WebDAV 同步工具", classes="title")
yield Static("", id="status_label", classes="status")
# 配置信息
yield Static("服务器配置", classes="section_title")
with Horizontal(classes="config_info"):
yield Static("URL:", classes="config_label")
yield Static("", id="server_url", classes="config_value")
with Horizontal(classes="config_info"):
yield Static("远程路径:", classes="config_label")
yield Static("", id="remote_path", classes="config_value")
with Horizontal(classes="config_info"):
yield Static("同步模式:", classes="config_label")
yield Static("", id="sync_mode", classes="config_value")
# 控制按钮
yield Static("控制面板", classes="section_title")
with Horizontal(classes="control_buttons"):
yield Button("测试连接", id="test_connection", variant="primary")
yield Button("开始同步", id="start_sync", variant="success")
yield Button("暂停", id="pause_sync", variant="warning", disabled=True)
yield Button("取消", id="cancel_sync", variant="error", disabled=True)
# 进度显示
yield Static("同步进度", classes="section_title")
yield ProgressBar(id="progress_bar", show_percentage=True, total=100)
yield Static("", id="progress_label", classes="progress_text")
# 日志输出
yield Static("同步日志", classes="section_title")
yield Static("", id="log_output", classes="log_output")
yield Footer()
def on_mount(self):
"""挂载时初始化状态"""
self.load_config()
self.update_ui_from_config()
self.log_message("同步工具已启动")
def load_config(self):
"""从配置文件加载同步设置"""
try:
from heurams.context import config_var
config_data = config_var.get().data
self.sync_config = config_data.get('sync', {}).get('webdav', {})
# 创建同步服务实例
from heurams.services.sync_service import create_sync_service_from_config
self.sync_service = create_sync_service_from_config()
except Exception as e:
self.log_message(f"加载配置失败: {e}", is_error=True)
self.sync_config = {}
def update_ui_from_config(self):
"""更新 UI 显示配置信息"""
try:
# 更新服务器 URL
url = self.sync_config.get('url', '未配置')
url_widget = self.query_one("#server_url")
url_widget.update(url if url else '未配置') # type: ignore
# 更新远程路径
remote_path = self.sync_config.get('remote_path', '/heurams/')
path_widget = self.query_one("#remote_path")
path_widget.update(remote_path) # type: ignore
# 更新同步模式
sync_mode = self.sync_config.get('sync_mode', 'bidirectional')
mode_widget = self.query_one("#sync_mode")
mode_map = {
'bidirectional': '双向同步',
'upload_only': '仅上传',
'download_only': '仅下载',
}
mode_widget.update(mode_map.get(sync_mode, sync_mode)) # type: ignore
# 更新状态标签
status_widget = self.query_one("#status_label")
if self.sync_service and self.sync_service.client:
status_widget.update("✅ 同步服务已就绪") # type: ignore
status_widget.add_class("ready")
else:
status_widget.update("❌ 同步服务未配置或未启用") # type: ignore
status_widget.add_class("error")
except Exception as e:
self.log_message(f"更新 UI 失败: {e}", is_error=True)
def update_status(self, status, current_item="", progress=None):
"""更新状态显示"""
try:
status_widget = self.query_one("#status_label")
status_widget.update(status) # type: ignore
if progress is not None:
progress_bar = self.query_one("#progress_bar")
progress_bar.progress = progress # type: ignore
progress_label = self.query_one("#progress_label")
progress_label.update(f"{progress}% - {current_item}" if current_item else f"{progress}%") # type: ignore
except Exception as e:
self.log_message(f"更新状态失败: {e}", is_error=True)
def log_message(self, message: str, is_error: bool = False):
"""添加日志消息并更新显示"""
timestamp = time.strftime("%H:%M:%S")
prefix = "[ERROR]" if is_error else "[INFO]"
log_line = f"{timestamp} {prefix} {message}"
self.log_messages.append(log_line)
# 保持日志行数不超过最大值
if len(self.log_messages) > self.max_log_lines:
self.log_messages = self.log_messages[-self.max_log_lines:]
# 更新日志显示
try:
log_widget = self.query_one("#log_output")
log_widget.update("\n".join(self.log_messages)) # type: ignore
except Exception:
pass # 如果组件未就绪,忽略错误
def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件"""
button_id = event.button.id
if button_id == "test_connection":
self.test_connection()
elif button_id == "start_sync":
self.start_sync()
elif button_id == "pause_sync":
self.pause_sync()
elif button_id == "cancel_sync":
self.cancel_sync()
event.stop()
def test_connection(self):
"""测试 WebDAV 服务器连接"""
if not self.sync_service:
self.log_message("同步服务未初始化,请检查配置", is_error=True)
self.update_status("❌ 同步服务未初始化")
return
self.log_message("正在测试 WebDAV 连接...")
self.update_status("正在测试连接...")
try:
success = self.sync_service.test_connection()
if success:
self.log_message("连接测试成功")
self.update_status("✅ 连接正常")
else:
self.log_message("连接测试失败", is_error=True)
self.update_status("❌ 连接失败")
except Exception as e:
self.log_message(f"连接测试异常: {e}", is_error=True)
self.update_status("❌ 连接异常")
def start_sync(self):
"""开始同步"""
if not self.sync_service:
self.log_message("同步服务未初始化,无法开始同步", is_error=True)
return
if self.is_syncing:
self.log_message("同步已在进行中", is_error=True)
return
self.is_syncing = True
self.is_paused = False
self.update_button_states()
self.log_message("开始同步数据...")
self.update_status("正在同步...", progress=0)
# 启动后台同步任务
self.run_worker(self.perform_sync, thread=True)
def perform_sync(self):
"""执行同步任务(在后台线程中运行)"""
worker = get_current_worker()
try:
# 获取需要同步的本地目录
from heurams.context import config_var
config = config_var.get()
paths = config.get('paths', {})
# 同步 nucleon 目录
nucleon_dir = pathlib.Path(paths.get('nucleon_dir', './data/nucleon'))
if nucleon_dir.exists():
self.log_message(f"同步 nucleon 目录: {nucleon_dir}")
self.update_status(f"同步 nucleon 目录...", progress=10)
result = self.sync_service.sync_directory(nucleon_dir) # type: ignore
if result.get('success'):
self.log_message(f"nucleon 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)}")
else:
self.log_message(f"nucleon 同步失败: {result.get('error', '未知错误')}", is_error=True)
# 同步 electron 目录
electron_dir = pathlib.Path(paths.get('electron_dir', './data/electron'))
if electron_dir.exists():
self.log_message(f"同步 electron 目录: {electron_dir}")
self.update_status(f"同步 electron 目录...", progress=60)
result = self.sync_service.sync_directory(electron_dir) # type: ignore
if result.get('success'):
self.log_message(f"electron 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)}")
else:
self.log_message(f"electron 同步失败: {result.get('error', '未知错误')}", is_error=True)
# 同步 orbital 目录(如果存在)
orbital_dir = pathlib.Path(paths.get('orbital_dir', './data/orbital'))
if orbital_dir.exists():
self.log_message(f"同步 orbital 目录: {orbital_dir}")
self.update_status(f"同步 orbital 目录...", progress=80)
result = self.sync_service.sync_directory(orbital_dir) # type: ignore
if result.get('success'):
self.log_message(f"orbital 同步完成: 上传 {result.get('uploaded', 0)} 个, 下载 {result.get('downloaded', 0)}")
else:
self.log_message(f"orbital 同步失败: {result.get('error', '未知错误')}", is_error=True)
# 同步完成
self.update_status("同步完成", progress=100)
self.log_message("所有目录同步完成")
except Exception as e:
self.log_message(f"同步过程中发生错误: {e}", is_error=True)
self.update_status("同步失败")
finally:
# 重置同步状态
self.is_syncing = False
self.is_paused = False
self.update_button_states() # type: ignore
def pause_sync(self):
"""暂停同步"""
if not self.is_syncing:
return
self.is_paused = not self.is_paused
self.update_button_states()
if self.is_paused:
self.log_message("同步已暂停")
self.update_status("同步已暂停")
else:
self.log_message("同步已恢复")
self.update_status("正在同步...")
def cancel_sync(self):
"""取消同步"""
if not self.is_syncing:
return
self.is_syncing = False
self.is_paused = False
self.update_button_states()
self.log_message("同步已取消")
self.update_status("同步已取消")
def update_button_states(self):
"""更新按钮状态"""
try:
start_button = self.query_one("#start_sync")
pause_button = self.query_one("#pause_sync")
cancel_button = self.query_one("#cancel_sync")
if self.is_syncing:
start_button.disabled = True
pause_button.disabled = False
cancel_button.disabled = False
pause_button.label = "继续" if self.is_paused else "暂停" # type: ignore
else:
start_button.disabled = False
pause_button.disabled = True
cancel_button.disabled = True
except Exception as e:
self.log_message(f"更新按钮状态失败: {e}", is_error=True)
def action_go_back(self):
self.app.pop_screen()