第二次提交,添加本地文件管理模块

This commit is contained in:
taiyi 2025-10-11 23:35:09 +08:00
parent 27f4ce314a
commit 8231566051

View File

@ -9,6 +9,7 @@ from typing import List
from ..core.base_cloud import BaseCloud
from ..core.sync_orchestrator import SyncOrchestrator
from ..utils.logger import get_logger
from ..utils.folder_manager import get_folder_manager
from .quick_auth import create_quick_auth_panel
@ -18,85 +19,173 @@ class SyncTab:
self.parent = parent
self.clouds = clouds
self.logger = get_logger(__name__)
self.folder_manager = get_folder_manager()
self.orchestrator = None
self.is_syncing = False
self.log_queue = queue.Queue()
self.frame = ttk.Frame(parent)
self._setup_ui()
self._start_log_monitor()
# 加载历史文件夹
self._load_folder_history()
def _setup_ui(self):
main_frame = ttk.Frame(self.frame, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.frame.columnconfigure(0, weight=1)
self.frame.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(2, weight=1)
ttk.Label(main_frame, text="本地目录:").grid(row=0, column=0, sticky=tk.W, pady=(0, 5))
dir_frame = ttk.Frame(main_frame)
dir_frame.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=(0, 5))
dir_frame.columnconfigure(0, weight=1)
self.local_dir_var = tk.StringVar()
self.local_dir_entry = ttk.Entry(dir_frame, textvariable=self.local_dir_var)
self.local_dir_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
# 文件夹管理区域
folder_mgmt_frame = ttk.LabelFrame(main_frame, text="📁 本地文件夹管理", padding="10")
folder_mgmt_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
folder_mgmt_frame.columnconfigure(0, weight=1)
# 当前选中的文件夹显示
current_folder_frame = ttk.Frame(folder_mgmt_frame)
current_folder_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
current_folder_frame.columnconfigure(1, weight=1)
ttk.Label(current_folder_frame, text="当前文件夹:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
self.current_folder_var = tk.StringVar()
current_folder_entry = ttk.Entry(current_folder_frame, textvariable=self.current_folder_var, state='readonly')
current_folder_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 5))
ttk.Button(
dir_frame,
text="浏览",
command=self._browse_directory
).grid(row=0, column=1, sticky=tk.E)
ttk.Label(main_frame, text="远程目录:").grid(row=1, column=0, sticky=tk.W, pady=(0, 5))
current_folder_frame,
text="浏览",
command=self._browse_and_add_folder
).grid(row=0, column=2)
# 远程目录设置
remote_dir_frame = ttk.Frame(folder_mgmt_frame)
remote_dir_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
remote_dir_frame.columnconfigure(1, weight=1)
ttk.Label(remote_dir_frame, text="远程目录:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
self.remote_dir_var = tk.StringVar(value="/CloudSync")
self.remote_dir_entry = ttk.Entry(main_frame, textvariable=self.remote_dir_var)
self.remote_dir_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=(0, 5))
paned_window = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
paned_window.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0))
local_frame = ttk.LabelFrame(paned_window, text="本地文件", padding="5")
paned_window.add(local_frame, weight=1)
ttk.Entry(remote_dir_frame, textvariable=self.remote_dir_var).grid(row=0, column=1, sticky=(tk.W, tk.E))
# 文件夹列表
list_frame = ttk.Frame(folder_mgmt_frame)
list_frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
list_frame.columnconfigure(0, weight=1)
list_frame.rowconfigure(0, weight=1)
# 创建Treeview显示文件夹列表
columns = ('path', 'remote', 'enabled', 'last_accessed')
self.folder_tree = ttk.Treeview(list_frame, columns=columns, show='headings', height=6)
# 设置列标题
self.folder_tree.heading('path', text='本地路径')
self.folder_tree.heading('remote', text='远程路径')
self.folder_tree.heading('enabled', text='状态')
self.folder_tree.heading('last_accessed', text='最后访问')
# 设置列宽
self.folder_tree.column('path', width=300)
self.folder_tree.column('remote', width=150)
self.folder_tree.column('enabled', width=60)
self.folder_tree.column('last_accessed', width=150)
# 滚动条
folder_scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.folder_tree.yview)
self.folder_tree.configure(yscrollcommand=folder_scrollbar.set)
self.folder_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
folder_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
# 绑定选择事件
self.folder_tree.bind('<<TreeviewSelect>>', self._on_folder_select)
# 文件夹操作按钮
folder_btn_frame = ttk.Frame(folder_mgmt_frame)
folder_btn_frame.grid(row=3, column=0, sticky=tk.W, pady=(0, 5))
ttk.Button(
folder_btn_frame,
text=" 添加文件夹",
command=self._browse_and_add_folder
).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(
folder_btn_frame,
text=" 移除选中",
command=self._remove_selected_folder
).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(
folder_btn_frame,
text="🔄 刷新列表",
command=self._refresh_folder_list
).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(
folder_btn_frame,
text="🧹 清理无效",
command=self._clean_invalid_folders
).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(
folder_btn_frame,
text="📤 导出配置",
command=self._export_folder_config
).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(
folder_btn_frame,
text="📥 导入配置",
command=self._import_folder_config
).pack(side=tk.LEFT)
# 文件预览区域
preview_paned = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
preview_paned.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0))
local_frame = ttk.LabelFrame(preview_paned, text="本地文件", padding="5")
preview_paned.add(local_frame, weight=1)
self.local_tree = ttk.Treeview(local_frame, show="tree")
local_scrollbar = ttk.Scrollbar(local_frame, orient=tk.VERTICAL, command=self.local_tree.yview)
self.local_tree.configure(yscrollcommand=local_scrollbar.set)
self.local_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
local_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
local_frame.columnconfigure(0, weight=1)
local_frame.rowconfigure(0, weight=1)
remote_frame = ttk.LabelFrame(paned_window, text="远程文件", padding="5")
paned_window.add(remote_frame, weight=1)
remote_frame = ttk.LabelFrame(preview_paned, text="远程文件", padding="5")
preview_paned.add(remote_frame, weight=1)
self.remote_tree = ttk.Treeview(remote_frame, show="tree")
remote_scrollbar = ttk.Scrollbar(remote_frame, orient=tk.VERTICAL, command=self.remote_tree.yview)
self.remote_tree.configure(yscrollcommand=remote_scrollbar.set)
self.remote_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
remote_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
remote_frame.columnconfigure(0, weight=1)
remote_frame.rowconfigure(0, weight=1)
# 控制区域
control_frame = ttk.Frame(main_frame)
control_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
control_frame.columnconfigure(1, weight=1)
control_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
control_frame.columnconfigure(2, weight=1)
self.sync_button = ttk.Button(
control_frame,
text="开始同步",
control_frame,
text="开始同步",
command=self._start_sync
)
self.sync_button.grid(row=0, column=0, sticky=tk.W)
self.watch_var = tk.BooleanVar()
self.watch_checkbox = ttk.Checkbutton(
control_frame,
@ -105,50 +194,200 @@ class SyncTab:
command=self._toggle_watch
)
self.watch_checkbox.grid(row=0, column=1, sticky=tk.W, padx=(10, 0))
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(
control_frame,
variable=self.progress_var,
maximum=100
maximum=100,
length=200
)
self.progress_bar.grid(row=0, column=2, sticky=tk.E, padx=(10, 0))
# 日志区域
log_frame = ttk.LabelFrame(main_frame, text="同步日志", padding="5")
log_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0))
log_frame.grid(row=4, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0))
self.log_text = tk.Text(log_frame, height=8, state=tk.DISABLED)
log_scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
self.log_text.configure(yscrollcommand=log_scrollbar.set)
self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
log_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1)
main_frame.rowconfigure(4, weight=1)
# 授权面板(仅在未授权时显示)
self.auth_panel = None
self._check_and_show_auth_panel()
def _browse_directory(self):
directory = filedialog.askdirectory()
def _load_folder_history(self):
"""加载历史文件夹"""
self._refresh_folder_list()
# 如果有文件夹,选中最近使用的
recent_folders = self.folder_manager.get_recent_folders(1)
if recent_folders:
self.current_folder_var.set(recent_folders[0]['path'])
self.remote_dir_var.set(recent_folders[0]['remote_path'])
self._refresh_local_tree(recent_folders[0]['path'])
def _refresh_folder_list(self):
"""刷新文件夹列表"""
# 清空现有项目
for item in self.folder_tree.get_children():
self.folder_tree.delete(item)
# 获取所有文件夹
folders = self.folder_manager.get_all_folders()
# 添加到树形视图
for folder in folders:
enabled_text = "✅ 启用" if folder.get('enabled', True) else "❌ 禁用"
last_accessed = folder.get('last_accessed', 'N/A')
if last_accessed != 'N/A':
try:
from datetime import datetime
dt = datetime.fromisoformat(last_accessed)
last_accessed = dt.strftime("%Y-%m-%d %H:%M")
except:
pass
self.folder_tree.insert('', 'end', values=(
folder['path'],
folder['remote_path'],
enabled_text,
last_accessed
))
def _on_folder_select(self, event):
"""文件夹选择事件"""
selection = self.folder_tree.selection()
if selection:
item = self.folder_tree.item(selection[0])
values = item['values']
if values:
folder_path = values[0]
remote_path = values[1]
# 更新当前选中的文件夹
self.current_folder_var.set(folder_path)
self.remote_dir_var.set(remote_path)
# 刷新文件预览
self._refresh_local_tree(folder_path)
# 更新访问记录
self.folder_manager.update_folder_access(folder_path)
def _browse_and_add_folder(self):
"""浏览并添加文件夹"""
directory = filedialog.askdirectory(title="选择本地同步文件夹")
if directory:
self.local_dir_var.set(directory)
self._refresh_local_tree()
def _refresh_local_tree(self):
local_dir = self.local_dir_var.get()
# 添加到文件夹管理器
remote_path = self.remote_dir_var.get()
if self.folder_manager.add_folder(directory, remote_path):
self._refresh_folder_list()
self.current_folder_var.set(directory)
self._refresh_local_tree(directory)
self.logger.info(f"Added folder: {directory}")
else:
messagebox.showerror("错误", f"添加文件夹失败: {directory}")
def _remove_selected_folder(self):
"""移除选中的文件夹"""
selection = self.folder_tree.selection()
if not selection:
messagebox.showwarning("提示", "请先选择要移除的文件夹")
return
item = self.folder_tree.item(selection[0])
folder_path = item['values'][0]
# 确认删除
result = messagebox.askyesno(
"确认移除",
f"确定要从同步列表中移除此文件夹吗?\n\n{folder_path}\n\n注意: 这不会删除本地文件"
)
if result:
if self.folder_manager.remove_folder(folder_path):
self._refresh_folder_list()
self.current_folder_var.set("")
self.local_tree.delete(*self.local_tree.get_children())
self.logger.info(f"Removed folder: {folder_path}")
else:
messagebox.showerror("错误", f"移除文件夹失败: {folder_path}")
def _clean_invalid_folders(self):
"""清理无效的文件夹"""
removed_count = self.folder_manager.clean_invalid_folders()
if removed_count > 0:
self._refresh_folder_list()
messagebox.showinfo("清理完成", f"已清理 {removed_count} 个无效文件夹")
else:
messagebox.showinfo("清理完成", "没有发现无效文件夹")
def _export_folder_config(self):
"""导出文件夹配置"""
export_path = filedialog.asksaveasfilename(
title="导出文件夹配置",
defaultextension=".json",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if export_path:
if self.folder_manager.export_config(export_path):
messagebox.showinfo("导出成功", f"配置已导出到:\n{export_path}")
else:
messagebox.showerror("导出失败", "导出配置时出错")
def _import_folder_config(self):
"""导入文件夹配置"""
import_path = filedialog.askopenfilename(
title="导入文件夹配置",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if import_path:
# 询问是合并还是替换
result = messagebox.askyesnocancel(
"导入选项",
"是否合并到现有配置?\n\n" +
"点击 '': 合并配置(保留现有文件夹)\n" +
"点击 '': 替换配置(清空现有文件夹)\n" +
"点击 '取消': 取消导入"
)
if result is not None:
merge = result
if self.folder_manager.import_config(import_path, merge=merge):
self._refresh_folder_list()
messagebox.showinfo("导入成功", "配置已导入")
else:
messagebox.showerror("导入失败", "导入配置时出错")
def _refresh_local_tree(self, local_dir=None):
"""刷新本地文件树"""
if local_dir is None:
local_dir = self.current_folder_var.get()
if not local_dir or not os.path.exists(local_dir):
return
self.local_tree.delete(*self.local_tree.get_children())
try:
self._populate_tree(self.local_tree, local_dir, "")
except Exception as e:
self.logger.error(f"Error refreshing local tree: {e}")
def _browse_directory(self):
"""向后兼容的方法"""
self._browse_and_add_folder()
def _populate_tree(self, tree, path, parent):
try:
@ -166,53 +405,68 @@ class SyncTab:
def _start_sync(self):
if self.is_syncing:
return
local_dir = self.local_dir_var.get()
remote_dir = self.remote_dir_var.get()
if not local_dir:
messagebox.showerror("错误", "请选择本地目录")
# 获取所有启用的文件夹
enabled_folders = self.folder_manager.get_all_folders(enabled_only=True)
if not enabled_folders:
messagebox.showerror("错误", "没有可同步的文件夹\n\n请先添加并启用至少一个文件夹")
return
if not os.path.exists(local_dir):
messagebox.showerror("错误", "本地目录不存在")
return
self.is_syncing = True
self.sync_button.config(text="同步中...", state="disabled")
def sync_worker():
try:
self.orchestrator = SyncOrchestrator(
self.clouds,
local_dir,
remote_dir
)
def progress_callback(message, current, total):
progress = (current / total) * 100 if total > 0 else 0
self.log_queue.put(f"进度: {message} ({current}/{total})")
self.progress_var.set(progress)
self.log_queue.put("开始全量同步...")
sync_results = self.orchestrator.full_sync(progress_callback)
self.log_queue.put("同步完成!")
for cloud_name, files in sync_results.items():
self.log_queue.put(f"{cloud_name}: 同步 {len(files)} 个文件")
if self.watch_var.get():
total_folders = len(enabled_folders)
for idx, folder_info in enumerate(enabled_folders, 1):
local_dir = folder_info['path']
remote_dir = folder_info['remote_path']
# 检查文件夹是否存在
if not os.path.exists(local_dir):
self.log_queue.put(f"⚠️ 跳过不存在的文件夹: {local_dir}")
continue
self.log_queue.put(f"📁 同步文件夹 ({idx}/{total_folders}): {local_dir}")
self.orchestrator = SyncOrchestrator(
self.clouds,
local_dir,
remote_dir
)
def progress_callback(message, current, total):
folder_progress = ((idx - 1) / total_folders) * 100
sync_progress = (current / total) * (100 / total_folders) if total > 0 else 0
total_progress = folder_progress + sync_progress
self.log_queue.put(f" {message} ({current}/{total})")
self.progress_var.set(total_progress)
sync_results = self.orchestrator.full_sync(progress_callback)
for cloud_name, files in sync_results.items():
self.log_queue.put(f"{cloud_name}: 同步 {len(files)} 个文件")
# 更新访问记录
self.folder_manager.update_folder_access(local_dir)
self.log_queue.put("✅ 所有文件夹同步完成!")
# 如果启用了实时监控
if self.watch_var.get() and self.orchestrator:
self.orchestrator.start_watching()
self.log_queue.put("开始实时监控...")
self.log_queue.put("👁️ 开始实时监控...")
except Exception as e:
self.log_queue.put(f"同步失败: {e}")
self.log_queue.put(f"同步失败: {e}")
self.logger.error(f"Sync failed: {e}")
finally:
self.is_syncing = False
self.sync_button.config(text="开始同步", state="normal")
self.progress_var.set(0)
threading.Thread(target=sync_worker, daemon=True).start()
def _toggle_watch(self):