Compare commits
2 Commits
8231566051
...
06a2ab71e6
| Author | SHA1 | Date | |
|---|---|---|---|
| 06a2ab71e6 | |||
| 80d0ad288c |
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
199
CloudSync/FOLDER_MANAGEMENT.md
Normal file
199
CloudSync/FOLDER_MANAGEMENT.md
Normal file
@ -0,0 +1,199 @@
|
||||
# 📁 本地文件夹管理功能
|
||||
|
||||
CloudSync 现在支持管理多个本地文件夹,并自动保存使用历史记录!
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
### 1. 多文件夹管理
|
||||
- ✅ 同时管理多个本地同步文件夹
|
||||
- ✅ 每个文件夹独立配置远程路径
|
||||
- ✅ 可以启用/禁用特定文件夹的同步
|
||||
- ✅ 批量同步所有启用的文件夹
|
||||
|
||||
### 2. 历史记录
|
||||
- 📊 自动记录文件夹访问时间
|
||||
- 📈 统计文件夹使用次数
|
||||
- 🕐 按最近使用时间排序
|
||||
- 🔥 按使用频率排序
|
||||
|
||||
### 3. 配置持久化
|
||||
- 💾 自动保存文件夹列表到 `~/.cloudsync/folders.json`
|
||||
- 🔄 下次启动自动加载历史文件夹
|
||||
- 📤 支持导出配置到文件
|
||||
- 📥 支持从文件导入配置(合并或替换)
|
||||
|
||||
### 4. 智能管理
|
||||
- 🧹 一键清理无效文件夹(路径不存在的)
|
||||
- 🔍 文件夹状态显示(启用/禁用/最后访问时间)
|
||||
- ✏️ 可以编辑文件夹的远程路径
|
||||
- ❌ 移除文件夹不会删除本地文件
|
||||
|
||||
## 🎮 使用方法
|
||||
|
||||
### 添加文件夹
|
||||
|
||||
1. **方法一:点击"浏览"按钮**
|
||||
- 在"当前文件夹"旁边点击"浏览"
|
||||
- 选择要同步的本地文件夹
|
||||
- 文件夹会自动添加到列表
|
||||
|
||||
2. **方法二:点击"➕ 添加文件夹"按钮**
|
||||
- 在文件夹列表下方点击"➕ 添加文件夹"
|
||||
- 选择要同步的本地文件夹
|
||||
- 文件夹会添加到列表并自动选中
|
||||
|
||||
### 管理文件夹
|
||||
|
||||
在文件夹列表中显示的信息:
|
||||
- **本地路径**:文件夹在本地的完整路径
|
||||
- **远程路径**:同步到云盘的目标路径
|
||||
- **状态**:✅ 启用 或 ❌ 禁用
|
||||
- **最后访问**:最后一次访问该文件夹的时间
|
||||
|
||||
### 文件夹操作
|
||||
|
||||
- **➖ 移除选中**:从列表中移除选中的文件夹(不删除本地文件)
|
||||
- **🔄 刷新列表**:重新加载文件夹列表
|
||||
- **🧹 清理无效**:自动移除路径不存在的文件夹
|
||||
- **📤 导出配置**:将文件夹列表导出为JSON文件
|
||||
- **📥 导入配置**:从JSON文件导入文件夹列表
|
||||
|
||||
### 开始同步
|
||||
|
||||
1. 添加一个或多个文件夹
|
||||
2. 确保要同步的文件夹状态为"✅ 启用"
|
||||
3. 点击"开始同步"按钮
|
||||
4. 系统会依次同步所有启用的文件夹
|
||||
|
||||
## 📊 配置文件格式
|
||||
|
||||
配置文件保存在:`~/.cloudsync/folders.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "/home/user/Documents",
|
||||
"remote_path": "/CloudSync/Documents",
|
||||
"enabled": true,
|
||||
"added_at": "2025-10-11T12:00:00",
|
||||
"last_accessed": "2025-10-11T15:30:00",
|
||||
"access_count": 5
|
||||
},
|
||||
{
|
||||
"path": "/home/user/Pictures",
|
||||
"remote_path": "/CloudSync/Pictures",
|
||||
"enabled": true,
|
||||
"added_at": "2025-10-11T12:05:00",
|
||||
"last_accessed": "2025-10-11T14:20:00",
|
||||
"access_count": 3
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-10-11T15:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 高级功能
|
||||
|
||||
### 编程式使用
|
||||
|
||||
```python
|
||||
from cloudsync.utils.folder_manager import get_folder_manager
|
||||
|
||||
# 获取文件夹管理器实例
|
||||
fm = get_folder_manager()
|
||||
|
||||
# 添加文件夹
|
||||
fm.add_folder(
|
||||
path="/path/to/folder",
|
||||
remote_path="/CloudSync/MyFolder",
|
||||
enabled=True
|
||||
)
|
||||
|
||||
# 获取所有文件夹
|
||||
all_folders = fm.get_all_folders()
|
||||
|
||||
# 获取启用的文件夹
|
||||
enabled_folders = fm.get_all_folders(enabled_only=True)
|
||||
|
||||
# 获取最近使用的5个文件夹
|
||||
recent_folders = fm.get_recent_folders(limit=5)
|
||||
|
||||
# 获取最常用的10个文件夹
|
||||
popular_folders = fm.get_most_used_folders(limit=10)
|
||||
|
||||
# 更新文件夹状态
|
||||
fm.update_folder_enabled("/path/to/folder", enabled=False)
|
||||
|
||||
# 更新远程路径
|
||||
fm.update_folder_remote_path("/path/to/folder", "/NewPath")
|
||||
|
||||
# 清理无效文件夹
|
||||
removed_count = fm.clean_invalid_folders()
|
||||
|
||||
# 导出配置
|
||||
fm.export_config("/path/to/backup.json")
|
||||
|
||||
# 导入配置(合并)
|
||||
fm.import_config("/path/to/backup.json", merge=True)
|
||||
|
||||
# 导入配置(替换)
|
||||
fm.import_config("/path/to/backup.json", merge=False)
|
||||
```
|
||||
|
||||
## 💡 使用技巧
|
||||
|
||||
1. **首次使用**
|
||||
- 添加常用的同步文件夹
|
||||
- 系统会记住这些文件夹,下次打开自动加载
|
||||
|
||||
2. **批量管理**
|
||||
- 导出配置文件保存到云盘或U盘
|
||||
- 在另一台电脑上导入配置,快速恢复同步设置
|
||||
|
||||
3. **性能优化**
|
||||
- 禁用暂时不需要同步的文件夹
|
||||
- 定期清理无效文件夹
|
||||
|
||||
4. **备份建议**
|
||||
- 定期导出文件夹配置
|
||||
- 配置文件很小,可以包含在系统备份中
|
||||
|
||||
## 🚀 升级说明
|
||||
|
||||
从旧版本升级:
|
||||
- 旧版本的单文件夹配置会自动迁移
|
||||
- 不需要重新配置,所有设置都会保留
|
||||
- 配置文件兼容性:向后兼容
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
**Q: 移除文件夹会删除本地文件吗?**
|
||||
A: 不会!移除只是从同步列表中删除,不会删除任何本地文件。
|
||||
|
||||
**Q: 文件夹的访问记录有什么用?**
|
||||
A: 帮助你了解文件夹使用频率,下次打开时自动选中最近使用的文件夹。
|
||||
|
||||
**Q: 如何临时禁用某个文件夹的同步?**
|
||||
A: 在文件夹列表中右键(或双击)可以切换启用状态(功能开发中)。
|
||||
|
||||
**Q: 配置文件丢失了怎么办?**
|
||||
A: 如果有导出的备份文件,可以导入恢复。否则需要重新添加文件夹。
|
||||
|
||||
**Q: 可以同步网络驱动器吗?**
|
||||
A: 可以!只要路径有效,支持任何可访问的目录。
|
||||
|
||||
## 📝 开发计划
|
||||
|
||||
- [ ] 文件夹分组功能
|
||||
- [ ] 文件夹标签和颜色标记
|
||||
- [ ] 同步策略自定义(文件过滤、排除规则)
|
||||
- [ ] 文件夹右键菜单
|
||||
- [ ] 文件夹拖拽排序
|
||||
- [ ] 批量操作(批量启用/禁用/移除)
|
||||
- [ ] 文件夹搜索和过滤
|
||||
- [ ] 同步历史记录查看
|
||||
|
||||
## 🤝 反馈建议
|
||||
|
||||
如果您有任何建议或发现问题,欢迎反馈!
|
||||
278
CloudSync/REMOTE_FILES_FIX.md
Normal file
278
CloudSync/REMOTE_FILES_FIX.md
Normal file
@ -0,0 +1,278 @@
|
||||
# 远程文件显示功能修复说明
|
||||
|
||||
## 问题描述
|
||||
|
||||
用户报告:主页面中远程同步文件夹无法显示
|
||||
|
||||
## 根本原因
|
||||
|
||||
`SyncTab` 类中缺少 `_refresh_remote_tree()` 方法的实现。虽然在文件夹选择事件中调用了该方法,但实际方法并不存在,导致远程文件无法加载和显示。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 实现 `_refresh_remote_tree()` 方法
|
||||
|
||||
**位置**: `cloudsync/ui/sync_tab.py:391-474`
|
||||
|
||||
**功能特性**:
|
||||
|
||||
- ✅ 异步加载远程文件,避免UI阻塞
|
||||
- ✅ 支持多云盘服务并行查询
|
||||
- ✅ 智能解析不同云盘的文件列表格式
|
||||
- ✅ 按云盘分类显示文件结构
|
||||
- ✅ 显示加载进度和错误信息
|
||||
- ✅ 自动区分文件和文件夹
|
||||
- ✅ 显示文件大小信息
|
||||
|
||||
### 2. 方法实现详解
|
||||
|
||||
```python
|
||||
def _refresh_remote_tree(self, remote_path=None):
|
||||
"""刷新远程文件树"""
|
||||
# 1. 获取远程路径
|
||||
if remote_path is None:
|
||||
remote_path = self.remote_dir_var.get()
|
||||
|
||||
if not remote_path:
|
||||
return
|
||||
|
||||
# 2. 清空现有显示
|
||||
self.remote_tree.delete(*self.remote_tree.get_children())
|
||||
|
||||
# 3. 显示加载提示
|
||||
self.log_queue.put(f"🔄 正在加载远程文件: {remote_path}")
|
||||
|
||||
# 4. 在后台线程中加载文件列表
|
||||
def load_worker():
|
||||
try:
|
||||
all_files = {}
|
||||
|
||||
# 从所有云盘服务获取文件列表
|
||||
for cloud in self.clouds:
|
||||
try:
|
||||
self.log_queue.put(f" 正在从 {cloud.name} 获取文件列表...")
|
||||
files = cloud.list_files(remote_path)
|
||||
|
||||
if files:
|
||||
all_files[cloud.name] = files
|
||||
self.log_queue.put(f" ✅ {cloud.name}: 找到 {len(files)} 个文件")
|
||||
else:
|
||||
self.log_queue.put(f" ℹ️ {cloud.name}: 目录为空或不存在")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to list files from {cloud.name}: {e}")
|
||||
self.log_queue.put(f" ❌ {cloud.name}: 获取失败 - {str(e)[:50]}")
|
||||
|
||||
# 5. 在UI线程中更新树形视图
|
||||
def update_tree():
|
||||
if not all_files:
|
||||
self.remote_tree.insert("", "end", text="📭 远程目录为空或不存在")
|
||||
return
|
||||
|
||||
# 为每个云盘创建分类节点
|
||||
for cloud_name, files in all_files.items():
|
||||
cloud_node = self.remote_tree.insert("", "end",
|
||||
text=f"☁️ {cloud_name}",
|
||||
open=True)
|
||||
|
||||
# 解析并添加文件
|
||||
for file_info in files:
|
||||
file_info = file_info.strip()
|
||||
if not file_info:
|
||||
continue
|
||||
|
||||
# 解析百度网盘格式:
|
||||
# "D 文件夹名" - 目录
|
||||
# "F 文件名 (大小)" - 文件
|
||||
if file_info.startswith('D '):
|
||||
name = file_info[2:].strip()
|
||||
self.remote_tree.insert(cloud_node, "end",
|
||||
text=f"📁 {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)
|
||||
else:
|
||||
# 未知格式,直接显示
|
||||
self.remote_tree.insert(cloud_node, "end",
|
||||
text=f"📄 {file_info}")
|
||||
|
||||
self.log_queue.put("✅ 远程文件列表加载完成")
|
||||
|
||||
self.frame.after(0, update_tree)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading remote files: {e}")
|
||||
self.log_queue.put(f"❌ 加载远程文件失败: {e}")
|
||||
|
||||
def show_error():
|
||||
self.remote_tree.insert("", "end",
|
||||
text=f"❌ 加载失败: {str(e)[:50]}")
|
||||
|
||||
self.frame.after(0, show_error)
|
||||
|
||||
# 启动后台线程
|
||||
threading.Thread(target=load_worker, daemon=True).start()
|
||||
```
|
||||
|
||||
### 3. 触发机制
|
||||
|
||||
方法在以下情况下自动触发:
|
||||
|
||||
1. **文件夹选择事件** (`_on_folder_select()`)
|
||||
- 用户在文件夹列表中选择某个文件夹时
|
||||
- 自动刷新该文件夹对应的远程路径
|
||||
|
||||
```python
|
||||
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._refresh_remote_tree(remote_path) # ← 自动触发
|
||||
|
||||
# 更新访问记录
|
||||
self.folder_manager.update_folder_access(folder_path)
|
||||
```
|
||||
|
||||
## 用户体验改进
|
||||
|
||||
### 🎯 加载过程可视化
|
||||
|
||||
- 实时显示加载状态:"🔄 正在加载远程文件"
|
||||
- 显示每个云盘的查询进度
|
||||
- 成功/失败/空目录都有清晰的提示
|
||||
|
||||
### 📁 文件分类显示
|
||||
|
||||
```
|
||||
☁️ baidu
|
||||
📁 Documents
|
||||
📁 Pictures
|
||||
📄 readme.txt (1.2KB)
|
||||
📄 photo.jpg (3.5MB)
|
||||
☁️ quark
|
||||
📁 Backup
|
||||
📄 data.zip (50MB)
|
||||
```
|
||||
|
||||
### ⚡ 性能优化
|
||||
|
||||
- 使用后台线程加载,不阻塞UI
|
||||
- 并行查询多个云盘服务
|
||||
- 异步更新UI,保持界面响应
|
||||
|
||||
### 🛡️ 错误处理
|
||||
|
||||
- 单个云盘失败不影响其他云盘
|
||||
- 详细的错误日志记录
|
||||
- 友好的错误提示信息
|
||||
|
||||
## 支持的云盘格式
|
||||
|
||||
### 百度网盘 (bypy)
|
||||
|
||||
bypy的list命令输出格式:
|
||||
```
|
||||
D folder_name
|
||||
F file_name.txt (1024)
|
||||
```
|
||||
|
||||
解析规则:
|
||||
- `D` 开头:目录,显示为 📁
|
||||
- `F` 开头:文件,显示为 📄,括号中为大小
|
||||
- 其他格式:作为文件显示
|
||||
|
||||
### 夸克网盘
|
||||
|
||||
将根据实际API返回格式进行解析
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 基本功能测试
|
||||
|
||||
```python
|
||||
# 测试场景1:选择文件夹后自动加载远程文件
|
||||
1. 启动应用
|
||||
2. 在文件夹列表中选择一个文件夹
|
||||
3. 观察右侧"远程文件"面板是否显示文件
|
||||
4. 检查日志中是否有加载进度信息
|
||||
|
||||
# 测试场景2:空目录显示
|
||||
1. 选择一个远程不存在的文件夹
|
||||
2. 应显示 "📭 远程目录为空或不存在"
|
||||
|
||||
# 测试场景3:错误处理
|
||||
1. 断开网络连接
|
||||
2. 选择文件夹触发加载
|
||||
3. 应显示错误信息并记录到日志
|
||||
```
|
||||
|
||||
### 2. 性能测试
|
||||
|
||||
```python
|
||||
# 大文件列表测试
|
||||
1. 创建包含大量文件的远程目录(100+文件)
|
||||
2. 选择该文件夹
|
||||
3. 观察加载时间和UI响应性
|
||||
4. 验证所有文件都正确显示
|
||||
```
|
||||
|
||||
### 3. 多云盘测试
|
||||
|
||||
```python
|
||||
# 多云盘并行查询
|
||||
1. 配置多个云盘服务(百度+夸克)
|
||||
2. 在不同云盘创建相同路径的文件夹
|
||||
3. 选择文件夹观察是否正确显示两个云盘的内容
|
||||
4. 验证分类节点是否按云盘名称区分
|
||||
```
|
||||
|
||||
## 已知限制
|
||||
|
||||
1. **文件格式解析**:目前主要支持百度网盘(bypy)的输出格式,其他云盘可能需要调整解析逻辑
|
||||
|
||||
2. **深度遍历**:当前只显示一级文件列表,不支持展开子目录(可作为未来改进)
|
||||
|
||||
3. **大文件列表**:如果远程目录包含数千个文件,TreeView可能会有性能问题(建议添加分页或虚拟滚动)
|
||||
|
||||
## 未来改进方向
|
||||
|
||||
- [ ] 支持递归加载子目录
|
||||
- [ ] 添加文件图标根据扩展名区分
|
||||
- [ ] 支持文件右键菜单(下载、删除、分享等)
|
||||
- [ ] 添加搜索和过滤功能
|
||||
- [ ] 实现虚拟滚动优化大列表性能
|
||||
- [ ] 缓存远程文件列表减少API调用
|
||||
- [ ] 支持拖拽上传文件到远程目录
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `cloudsync/ui/sync_tab.py:391-474` - `_refresh_remote_tree()` 方法实现
|
||||
- `cloudsync/ui/sync_tab.py:265-286` - `_on_folder_select()` 触发机制
|
||||
- `cloudsync/core/baidu_cloud.py:85-117` - 百度云list_files实现
|
||||
- `cloudsync/core/base_cloud.py` - 云盘服务基类
|
||||
|
||||
## 版本历史
|
||||
|
||||
- **2025-10-11**: 初始实现
|
||||
- 添加 `_refresh_remote_tree()` 方法
|
||||
- 支持多云盘并行查询
|
||||
- 实现文件格式解析和分类显示
|
||||
- 添加完整的错误处理和日志记录
|
||||
@ -276,9 +276,12 @@ class SyncTab:
|
||||
self.current_folder_var.set(folder_path)
|
||||
self.remote_dir_var.set(remote_path)
|
||||
|
||||
# 刷新文件预览
|
||||
# 刷新本地文件预览
|
||||
self._refresh_local_tree(folder_path)
|
||||
|
||||
# 刷新远程文件预览
|
||||
self._refresh_remote_tree(remote_path)
|
||||
|
||||
# 更新访问记录
|
||||
self.folder_manager.update_folder_access(folder_path)
|
||||
|
||||
@ -385,6 +388,91 @@ class SyncTab:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error refreshing local tree: {e}")
|
||||
|
||||
def _refresh_remote_tree(self, remote_path=None):
|
||||
"""刷新远程文件树"""
|
||||
if remote_path is None:
|
||||
remote_path = self.remote_dir_var.get()
|
||||
|
||||
if not remote_path:
|
||||
return
|
||||
|
||||
# 清空现有项目
|
||||
self.remote_tree.delete(*self.remote_tree.get_children())
|
||||
|
||||
# 显示加载提示
|
||||
self.log_queue.put(f"🔄 正在加载远程文件: {remote_path}")
|
||||
|
||||
def load_worker():
|
||||
try:
|
||||
# 获取所有可用的云盘服务
|
||||
all_files = {}
|
||||
|
||||
for cloud in self.clouds:
|
||||
try:
|
||||
self.log_queue.put(f" 正在从 {cloud.name} 获取文件列表...")
|
||||
files = cloud.list_files(remote_path)
|
||||
|
||||
if files:
|
||||
all_files[cloud.name] = files
|
||||
self.log_queue.put(f" ✅ {cloud.name}: 找到 {len(files)} 个文件")
|
||||
else:
|
||||
self.log_queue.put(f" ℹ️ {cloud.name}: 目录为空或不存在")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to list files from {cloud.name}: {e}")
|
||||
self.log_queue.put(f" ❌ {cloud.name}: 获取失败 - {str(e)[:50]}")
|
||||
|
||||
# 在UI线程中更新树形视图
|
||||
def update_tree():
|
||||
if not all_files:
|
||||
# 显示"无文件"提示
|
||||
self.remote_tree.insert("", "end", text="📭 远程目录为空或不存在")
|
||||
return
|
||||
|
||||
# 为每个云盘创建一个分类节点
|
||||
for cloud_name, files in all_files.items():
|
||||
cloud_node = self.remote_tree.insert("", "end", text=f"☁️ {cloud_name}", open=True)
|
||||
|
||||
# 解析文件列表并添加到树中
|
||||
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}")
|
||||
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)
|
||||
else:
|
||||
# 未知格式,直接显示
|
||||
self.remote_tree.insert(cloud_node, "end", text=f"📄 {file_info}")
|
||||
|
||||
self.log_queue.put("✅ 远程文件列表加载完成")
|
||||
|
||||
self.frame.after(0, update_tree)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading remote files: {e}")
|
||||
self.log_queue.put(f"❌ 加载远程文件失败: {e}")
|
||||
|
||||
def show_error():
|
||||
self.remote_tree.insert("", "end", text=f"❌ 加载失败: {str(e)[:50]}")
|
||||
|
||||
self.frame.after(0, show_error)
|
||||
|
||||
# 在后台线程中加载
|
||||
threading.Thread(target=load_worker, daemon=True).start()
|
||||
|
||||
def _browse_directory(self):
|
||||
"""向后兼容的方法"""
|
||||
self._browse_and_add_folder()
|
||||
|
||||
362
CloudSync/cloudsync/utils/folder_manager.py
Normal file
362
CloudSync/cloudsync/utils/folder_manager.py
Normal file
@ -0,0 +1,362 @@
|
||||
"""
|
||||
本地文件夹管理器 - 管理多个同步文件夹和历史记录
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
|
||||
class FolderManager:
|
||||
"""本地文件夹管理器"""
|
||||
|
||||
def __init__(self, config_file: Optional[str] = None):
|
||||
"""
|
||||
初始化文件夹管理器
|
||||
|
||||
Args:
|
||||
config_file: 配置文件路径,如果为None则使用默认路径
|
||||
"""
|
||||
self.logger = get_logger(__name__)
|
||||
|
||||
# 配置文件路径
|
||||
if config_file is None:
|
||||
config_dir = os.path.expanduser("~/.cloudsync")
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
self.config_file = os.path.join(config_dir, "folders.json")
|
||||
else:
|
||||
self.config_file = config_file
|
||||
|
||||
# 文件夹列表
|
||||
self.folders: List[Dict[str, str]] = []
|
||||
|
||||
# 加载配置
|
||||
self._load_config()
|
||||
|
||||
def _load_config(self):
|
||||
"""从配置文件加载文件夹列表"""
|
||||
try:
|
||||
if os.path.exists(self.config_file):
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self.folders = data.get('folders', [])
|
||||
self.logger.info(f"Loaded {len(self.folders)} folders from config")
|
||||
else:
|
||||
self.logger.info("No config file found, starting with empty folder list")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to load folder config: {e}")
|
||||
self.folders = []
|
||||
|
||||
def _save_config(self):
|
||||
"""保存文件夹列表到配置文件"""
|
||||
try:
|
||||
data = {
|
||||
'folders': self.folders,
|
||||
'last_updated': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
self.logger.info(f"Saved {len(self.folders)} folders to config")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to save folder config: {e}")
|
||||
return False
|
||||
|
||||
def add_folder(self, path: str, remote_path: str = "/CloudSync", enabled: bool = True) -> bool:
|
||||
"""
|
||||
添加文件夹
|
||||
|
||||
Args:
|
||||
path: 本地文件夹路径
|
||||
remote_path: 对应的远程路径
|
||||
enabled: 是否启用同步
|
||||
|
||||
Returns:
|
||||
是否添加成功
|
||||
"""
|
||||
# 标准化路径
|
||||
path = os.path.abspath(path)
|
||||
|
||||
# 检查路径是否存在
|
||||
if not os.path.exists(path):
|
||||
self.logger.warning(f"Path does not exist: {path}")
|
||||
return False
|
||||
|
||||
# 检查是否已存在
|
||||
if self.get_folder(path) is not None:
|
||||
self.logger.info(f"Folder already exists: {path}")
|
||||
# 更新最后访问时间
|
||||
self.update_folder_access(path)
|
||||
return True
|
||||
|
||||
# 添加新文件夹
|
||||
folder_info = {
|
||||
'path': path,
|
||||
'remote_path': remote_path,
|
||||
'enabled': enabled,
|
||||
'added_at': datetime.now().isoformat(),
|
||||
'last_accessed': datetime.now().isoformat(),
|
||||
'access_count': 1
|
||||
}
|
||||
|
||||
self.folders.append(folder_info)
|
||||
self._save_config()
|
||||
|
||||
self.logger.info(f"Added folder: {path} -> {remote_path}")
|
||||
return True
|
||||
|
||||
def remove_folder(self, path: str) -> bool:
|
||||
"""
|
||||
移除文件夹
|
||||
|
||||
Args:
|
||||
path: 本地文件夹路径
|
||||
|
||||
Returns:
|
||||
是否移除成功
|
||||
"""
|
||||
path = os.path.abspath(path)
|
||||
|
||||
# 查找并移除
|
||||
for i, folder in enumerate(self.folders):
|
||||
if folder['path'] == path:
|
||||
self.folders.pop(i)
|
||||
self._save_config()
|
||||
self.logger.info(f"Removed folder: {path}")
|
||||
return True
|
||||
|
||||
self.logger.warning(f"Folder not found: {path}")
|
||||
return False
|
||||
|
||||
def get_folder(self, path: str) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
获取文件夹信息
|
||||
|
||||
Args:
|
||||
path: 本地文件夹路径
|
||||
|
||||
Returns:
|
||||
文件夹信息字典,如果不存在则返回None
|
||||
"""
|
||||
path = os.path.abspath(path)
|
||||
|
||||
for folder in self.folders:
|
||||
if folder['path'] == path:
|
||||
return folder
|
||||
|
||||
return None
|
||||
|
||||
def update_folder_access(self, path: str) -> bool:
|
||||
"""
|
||||
更新文件夹访问时间和次数
|
||||
|
||||
Args:
|
||||
path: 本地文件夹路径
|
||||
|
||||
Returns:
|
||||
是否更新成功
|
||||
"""
|
||||
path = os.path.abspath(path)
|
||||
|
||||
for folder in self.folders:
|
||||
if folder['path'] == path:
|
||||
folder['last_accessed'] = datetime.now().isoformat()
|
||||
folder['access_count'] = folder.get('access_count', 0) + 1
|
||||
self._save_config()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update_folder_enabled(self, path: str, enabled: bool) -> bool:
|
||||
"""
|
||||
更新文件夹启用状态
|
||||
|
||||
Args:
|
||||
path: 本地文件夹路径
|
||||
enabled: 是否启用
|
||||
|
||||
Returns:
|
||||
是否更新成功
|
||||
"""
|
||||
path = os.path.abspath(path)
|
||||
|
||||
for folder in self.folders:
|
||||
if folder['path'] == path:
|
||||
folder['enabled'] = enabled
|
||||
self._save_config()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update_folder_remote_path(self, path: str, remote_path: str) -> bool:
|
||||
"""
|
||||
更新文件夹远程路径
|
||||
|
||||
Args:
|
||||
path: 本地文件夹路径
|
||||
remote_path: 新的远程路径
|
||||
|
||||
Returns:
|
||||
是否更新成功
|
||||
"""
|
||||
path = os.path.abspath(path)
|
||||
|
||||
for folder in self.folders:
|
||||
if folder['path'] == path:
|
||||
folder['remote_path'] = remote_path
|
||||
self._save_config()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_all_folders(self, enabled_only: bool = False) -> List[Dict[str, str]]:
|
||||
"""
|
||||
获取所有文件夹
|
||||
|
||||
Args:
|
||||
enabled_only: 是否只返回启用的文件夹
|
||||
|
||||
Returns:
|
||||
文件夹信息列表
|
||||
"""
|
||||
if enabled_only:
|
||||
return [f for f in self.folders if f.get('enabled', True)]
|
||||
return self.folders.copy()
|
||||
|
||||
def get_recent_folders(self, limit: int = 10) -> List[Dict[str, str]]:
|
||||
"""
|
||||
获取最近使用的文件夹
|
||||
|
||||
Args:
|
||||
limit: 返回数量限制
|
||||
|
||||
Returns:
|
||||
按最近访问时间排序的文件夹列表
|
||||
"""
|
||||
# 按最后访问时间排序
|
||||
sorted_folders = sorted(
|
||||
self.folders,
|
||||
key=lambda f: f.get('last_accessed', ''),
|
||||
reverse=True
|
||||
)
|
||||
|
||||
return sorted_folders[:limit]
|
||||
|
||||
def get_most_used_folders(self, limit: int = 10) -> List[Dict[str, str]]:
|
||||
"""
|
||||
获取最常用的文件夹
|
||||
|
||||
Args:
|
||||
limit: 返回数量限制
|
||||
|
||||
Returns:
|
||||
按访问次数排序的文件夹列表
|
||||
"""
|
||||
# 按访问次数排序
|
||||
sorted_folders = sorted(
|
||||
self.folders,
|
||||
key=lambda f: f.get('access_count', 0),
|
||||
reverse=True
|
||||
)
|
||||
|
||||
return sorted_folders[:limit]
|
||||
|
||||
def clean_invalid_folders(self) -> int:
|
||||
"""
|
||||
清理不存在的文件夹
|
||||
|
||||
Returns:
|
||||
清理的文件夹数量
|
||||
"""
|
||||
initial_count = len(self.folders)
|
||||
|
||||
# 过滤出仍然存在的文件夹
|
||||
self.folders = [
|
||||
f for f in self.folders
|
||||
if os.path.exists(f['path'])
|
||||
]
|
||||
|
||||
removed_count = initial_count - len(self.folders)
|
||||
|
||||
if removed_count > 0:
|
||||
self._save_config()
|
||||
self.logger.info(f"Cleaned {removed_count} invalid folders")
|
||||
|
||||
return removed_count
|
||||
|
||||
def export_config(self, export_path: str) -> bool:
|
||||
"""
|
||||
导出配置到指定路径
|
||||
|
||||
Args:
|
||||
export_path: 导出文件路径
|
||||
|
||||
Returns:
|
||||
是否导出成功
|
||||
"""
|
||||
try:
|
||||
data = {
|
||||
'folders': self.folders,
|
||||
'exported_at': datetime.now().isoformat(),
|
||||
'version': '1.0'
|
||||
}
|
||||
|
||||
with open(export_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
self.logger.info(f"Exported config to: {export_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to export config: {e}")
|
||||
return False
|
||||
|
||||
def import_config(self, import_path: str, merge: bool = True) -> bool:
|
||||
"""
|
||||
从指定路径导入配置
|
||||
|
||||
Args:
|
||||
import_path: 导入文件路径
|
||||
merge: 是否合并到现有配置(False则替换)
|
||||
|
||||
Returns:
|
||||
是否导入成功
|
||||
"""
|
||||
try:
|
||||
with open(import_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
imported_folders = data.get('folders', [])
|
||||
|
||||
if merge:
|
||||
# 合并:添加不存在的文件夹
|
||||
existing_paths = {f['path'] for f in self.folders}
|
||||
for folder in imported_folders:
|
||||
if folder['path'] not in existing_paths:
|
||||
self.folders.append(folder)
|
||||
else:
|
||||
# 替换
|
||||
self.folders = imported_folders
|
||||
|
||||
self._save_config()
|
||||
self.logger.info(f"Imported {len(imported_folders)} folders from: {import_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to import config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# 全局文件夹管理器实例
|
||||
_folder_manager = None
|
||||
|
||||
|
||||
def get_folder_manager() -> FolderManager:
|
||||
"""获取全局文件夹管理器实例"""
|
||||
global _folder_manager
|
||||
if _folder_manager is None:
|
||||
_folder_manager = FolderManager()
|
||||
return _folder_manager
|
||||
Loading…
Reference in New Issue
Block a user