diff --git a/CloudSync/cloudsync/core/baidu_cloud.py b/CloudSync/cloudsync/core/baidu_cloud.py index a612dd5..1f8a1fa 100644 --- a/CloudSync/cloudsync/core/baidu_cloud.py +++ b/CloudSync/cloudsync/core/baidu_cloud.py @@ -83,29 +83,80 @@ class BaiduCloud(BaseCloud): return False def list_files(self, remote_path: str) -> List[str]: + """ + 获取远程目录的文件列表 + + 返回格式: + - 目录: "D folder_name" + - 文件: "F file_name size" + """ try: cmd = ["bypy", "list", remote_path] result = subprocess.run( - cmd, - capture_output=True, - text=True, + cmd, + capture_output=True, + text=True, timeout=120, encoding='utf-8', errors='replace', env=self.utf8_env ) - + if result.returncode == 0: files = [] - for line in result.stdout.split('\n'): - line = line.strip() - if line and not line.startswith('Quota'): - files.append(line) + output = result.stdout + + # 记录原始输出用于调试 + self.logger.debug(f"Baidu list raw output:\n{output}") + + # 解析bypy输出 + lines = output.split('\n') + in_file_list = False + + for line in lines: + # 跳过空行 + if not line.strip(): + continue + + # 跳过警告信息 + if '' in line or 'WARNING' in line or 'WARN' in line: + continue + + # 跳过Quota信息 + if line.strip().startswith('Quota') or 'Loading quota' in line: + continue + + # 跳过表头和分隔符 + if '($t $f $s $m $d)' in line or line.strip().startswith('/apps/bypy'): + in_file_list = True + continue + + # 跳过包含特殊字符的格式化行 + if any(x in line for x in ['', '

', '', '', '!0!0!', '>"!!']): + continue + + # 如果已经进入文件列表区域,处理文件信息 + if in_file_list: + line = line.strip() + + # bypy的输出可能是多列格式,需要解析 + # 典型格式: "D folder_name" 或 "F file_name (size)" + if line.startswith('D ') or line.startswith('F '): + files.append(line) + elif line and not line.startswith('-') and not line.startswith('='): + # 尝试将行解析为文件信息 + # 如果行包含文件名,尝试识别类型 + # 简单判断:如果没有扩展名,可能是目录 + if '.' not in line or line.count(' ') > 0: + # 可能需要更复杂的解析,暂时标记为文件 + files.append(f"F {line}") + + self.logger.info(f"Parsed {len(files)} files from Baidu: {remote_path}") return files else: self.logger.error(f"Baidu list failed: {result.stderr}") return [] - + except subprocess.TimeoutExpired: self.logger.error(f"Baidu list timeout: {remote_path}") return [] diff --git a/CloudSync/cloudsync/ui/sync_tab.py b/CloudSync/cloudsync/ui/sync_tab.py index a5e53e3..109f93c 100644 --- a/CloudSync/cloudsync/ui/sync_tab.py +++ b/CloudSync/cloudsync/ui/sync_tab.py @@ -164,7 +164,13 @@ class SyncTab: 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") + # 创建带隐藏列的Treeview用于存储元数据 + self.remote_tree = ttk.Treeview(remote_frame, show="tree", columns=("path", "type", "cloud")) + # 隐藏columns(宽度设为0) + self.remote_tree.column("path", width=0, stretch=False) + self.remote_tree.column("type", width=0, stretch=False) + self.remote_tree.column("cloud", width=0, stretch=False) + remote_scrollbar = ttk.Scrollbar(remote_frame, orient=tk.VERTICAL, command=self.remote_tree.yview) self.remote_tree.configure(yscrollcommand=remote_scrollbar.set) @@ -174,6 +180,12 @@ class SyncTab: remote_frame.columnconfigure(0, weight=1) remote_frame.rowconfigure(0, weight=1) + # 绑定双击事件以展开文件夹 + self.remote_tree.bind('', self._on_remote_item_double_click) + + # 用于跟踪当前远程目录路径(用于导航) + self.current_remote_path_stack = [] + # 控制区域 control_frame = ttk.Frame(main_frame) control_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(10, 0)) @@ -402,6 +414,9 @@ class SyncTab: # 显示加载提示 self.log_queue.put(f"🔄 正在加载远程文件: {remote_path}") + # 保存当前路径 + self.current_remote_path = remote_path + def load_worker(): try: # 获取所有可用的云盘服务 @@ -424,6 +439,14 @@ class SyncTab: # 在UI线程中更新树形视图 def update_tree(): + # 如果不是根目录,添加"返回上级"选项 + if remote_path != self.remote_dir_var.get() and '/' in remote_path: + parent_path = '/'.join(remote_path.rstrip('/').split('/')[:-1]) + if not parent_path: + parent_path = self.remote_dir_var.get() + back_item = self.remote_tree.insert("", "end", text="⬆️ 返回上级目录", + values=(parent_path, 'back', '')) + if not all_files: # 显示"无文件"提示 self.remote_tree.insert("", "end", text="📭 远程目录为空或不存在") @@ -431,31 +454,57 @@ class SyncTab: # 为每个云盘创建一个分类节点 for cloud_name, files in all_files.items(): - cloud_node = self.remote_tree.insert("", "end", text=f"☁️ {cloud_name}", open=True) + # 如果只有一个云盘且文件不多,可以不创建云盘节点 + if len(self.clouds) == 1 and len(files) < 50: + parent_node = "" + else: + cloud_node = self.remote_tree.insert("", "end", text=f"☁️ {cloud_name}", + values=('', 'cloud', cloud_name), open=True) + parent_node = cloud_node - # 解析文件列表并添加到树中 + # 分类存储文件夹和文件 + folders = [] + regular_files = [] + + # 解析文件列表并分类 for file_info in files: - # bypy的输出格式可能是 "D 文件夹名" 或 "F 文件名 (大小)" file_info = file_info.strip() - if not file_info: continue - # 尝试解析文件类型和名称 + # 解析文件类型和名称 if file_info.startswith('D '): # 目录 name = file_info[2:].strip() - self.remote_tree.insert(cloud_node, "end", text=f"📁 {name}") + folders.append(name) elif file_info.startswith('F '): # 文件 parts = file_info[2:].split('(') name = parts[0].strip() size = parts[1].rstrip(')').strip() if len(parts) > 1 else "" - display_text = f"📄 {name}" + (f" ({size})" if size else "") - self.remote_tree.insert(cloud_node, "end", text=display_text) + regular_files.append((name, size)) else: - # 未知格式,直接显示 - self.remote_tree.insert(cloud_node, "end", text=f"📄 {file_info}") + # 未知格式,尝试直接显示 + regular_files.append((file_info, "")) + + # 先显示文件夹(按名称排序) + for name in sorted(folders): + full_path = f"{remote_path.rstrip('/')}/{name}" + folder_item = self.remote_tree.insert( + parent_node, "end", + text=f"📁 {name}", + values=(full_path, 'folder', cloud_name) + ) + + # 再显示文件(按名称排序) + for name, size in sorted(regular_files): + display_text = f"📄 {name}" + (f" ({size})" if size else "") + full_path = f"{remote_path.rstrip('/')}/{name}" + file_item = self.remote_tree.insert( + parent_node, "end", + text=display_text, + values=(full_path, 'file', cloud_name) + ) self.log_queue.put("✅ 远程文件列表加载完成") @@ -473,6 +522,51 @@ class SyncTab: # 在后台线程中加载 threading.Thread(target=load_worker, daemon=True).start() + def _on_remote_item_double_click(self, event): + """处理远程文件树的双击事件""" + selection = self.remote_tree.selection() + if not selection: + return + + item = selection[0] + item_data = self.remote_tree.item(item) + item_text = item_data.get('text', '') + item_values = item_data.get('values', []) + + # 从values中获取路径和类型 (path, type, cloud) + if item_values and len(item_values) >= 2: + item_path = item_values[0] + item_type = item_values[1] + else: + # 如果没有设置values,尝试从文本判断 + if item_text.startswith('📁 '): + # 这是一个文件夹 + folder_name = item_text[2:].strip() + current_path = getattr(self, 'current_remote_path', self.remote_dir_var.get()) + item_path = f"{current_path.rstrip('/')}/{folder_name}" + item_type = 'folder' + elif item_text.startswith('⬆️'): + # 返回上级 + item_type = 'back' + current_path = getattr(self, 'current_remote_path', self.remote_dir_var.get()) + item_path = '/'.join(current_path.rstrip('/').split('/')[:-1]) or self.remote_dir_var.get() + else: + # 文件或其他,不处理 + self.log_queue.put(f"ℹ️ 点击的项目不可操作: {item_text}") + return + + # 如果是文件夹或返回上级,进入该目录 + if item_type == 'folder' or item_type == 'back': + self.log_queue.put(f"📂 进入目录: {item_path}") + self._refresh_remote_tree(item_path) + elif item_type == 'file': + # 文件双击暂不处理,可以在未来添加下载功能 + self.log_queue.put(f"ℹ️ 文件双击功能待实现: {item_path}") + elif item_type == 'cloud': + # 双击云盘节点,不做任何操作 + pass + + def _browse_directory(self): """向后兼容的方法""" self._browse_and_add_folder()