Skip to content

Latest commit

 

History

History
342 lines (256 loc) · 11.6 KB

File metadata and controls

342 lines (256 loc) · 11.6 KB

syncmac 设计文档

概述

syncmac 是一个基于 rsync 的个人文件同步工具,使用 Python 实现。设计目标是提供高效、可靠、易用的文件同步解决方案,特别针对 macOS 环境优化。

核心设计原则

  1. 简单性 - 最小化依赖,仅使用 Python 标准库和 ANSI 转义码
  2. 性能 - 优化文件扫描和传输效率
  3. 安全性 - 默认 dry-run 模式,路径验证
  4. 可扩展性 - 易于添加新目标和配置

架构

┌─────────────────────────────────────────────────────────────┐
│                         CLI 层                                │
│  ┌─────────────┐   ┌─────────────┐                          │
│  │ Argument     │   │ 别名检测      │                          │
│  │ Parser       │   └──────┬──────┘                          │
│  └──────┬──────┘             │                                │
└─────────┼─────────────────────┼───────────────────────────────┘
          │                     │
          ▼                     ▼
┌─────────────────────────────────────────────────────────────┐
│                      主同步流程                               │
│   main()                                                     │
│   1. 检查依赖 (rsync)                                        │
│   2. 解析参数                                                │
│   3. 验证目标                                                │
│   4. 执行同步                                                │
└─────────────────────────────────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────────────┐
│                   配置管理层                                  │
│   get_target_config()   # 单个目标配置                       │
│   get_composite_target()  # 组合目标配置                     │
│   is_valid_target()     # 目标验证                           │
│   list_targets()          # 列出所有目标                     │
└─────────────────────────────────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────────────┐
│                   同步执行层                                  │
│   sync_target()      # 单个目标同步(含 DOTFILES 特殊处理)   │
│   run_composite_target()  # 组合目标递归执行                 │
└─────────────────────────────────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────────────┐
│                   进度显示层                                  │
│   ProgressDisplay                                            │
│   - ANSI 转义码渲染                                          │
│   - 固定位置更新(扫描:单行 \r)                            │
│   - 固定位置更新(同步:清除+重绘)                          │
│   - 最近文件队列                                             │
└─────────────────────────────────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────────────┐
│                     rsync 进程                               │
└─────────────────────────────────────────────────────────────┘

核心组件

1. ANSI 转义码层

class ANSI:
    RESET = "\033[0m"
    CLEAR_LINE = "\033[2K"
    UP = "\033[1A"
    GREEN = "\033[32m"
    YELLOW = "\033[33m"
    CYAN = "\033[36m"
    DIM = "\033[2m"
    BOLD = "\033[1m"

提供终端控制能力:清除行/移动光标、颜色显示、样式控制。

2. 配置管理

目标配置格式

"source|destination|excludes"
  • source: 源路径(相对或绝对)
  • destination: 目标路径(可选)
  • excludes: 排除规则(可选)

特殊标记

  • DOTFILES: 特殊模式,仅同步隐藏文件
  • 空目标: 使用默认路径

3. DOTFILES 模式

设计目标:仅同步用户主目录下的隐藏文件和目录,避免遍历整个目录树。

实现:scan_dotfiles_directory() 函数

  • 递归扫描 .* 目录
  • 非递归跳过普通目录
  • 保留软链接(不添加尾斜杠)
  • 创建临时文件列表供 --files-from 使用

软链接处理:

  • 软链接:不添加尾斜杠,rsync 保留链接本身
  • 普通目录:添加尾斜杠,rsync 递归同步内容

优化效果:扫描时间从 ~14 秒降至 ~0.2 秒(约 70 倍提升)。

4. 进度显示系统

扫描阶段:单行动态更新

使用 \r(回车符)实现单行覆盖,光标始终在同一行:

# 扫描阶段
print(f"\r\033[K{lines[0]}", end="", flush=True)
self._lines_printed = 1  # 固定 1 行

优势:简单可靠,永远不会出现多行残留。

同步阶段:清除+重绘

同步阶段内容可能变化(文件列表出现/消失),使用清除+重绘方案:

# 同步阶段
clear_count = max(self._lines_printed, self._max_lines_printed)
for _ in range(clear_count):
    print("\033[1A\033[2K", end="", flush=True)
# 重新打印
print(lines[0], flush=True)
if self.recent_synced:
    print("  最近同步:")
    for f in self.recent_synced[-5:]:
        print(f"     {f}")

关键设计:max(_lines_printed, _max_lines_printed)

  • _lines_printed: 当前打印的行数(可能因文件列表消失而变小)
  • _max_lines_printed: 历史最大打印行数(单调递增)
  • 使用最大值确保清除足够行,避免残留

v2.5.0 修复:此前使用 max(_lines_printed, len(lines)) 时,当文件列表从有变无, _lines_printed 从 6 变为 1,下次只清除 1 行,导致之前的 5 行文件列表残留。 引入 _max_lines_printed 后,即使文件列表消失,仍会清除历史最大行数。

显示内容

  1. 扫描阶段:

    • 动画 spinner
    • 已扫描文件数
  2. 同步阶段:

    • 动画 spinner
    • 已同步文件数 / 传输速度
    • 无传输时显示"已检查 X.Xs"(v2.5.x 移除了最近同步文件列表显示)

线程安全

使用 threading.Lock 保护共享状态,后台线程每 150ms 触发一次 spinner 更新。

5. 同步执行流程

单目标同步

sync_target(name, home, from_path, to_path)
  1. 获取目标配置
  2. 解析源/目标路径
  3. DOTFILES 特殊处理(扫描 + 临时文件列表)
  4. 构建排除规则
  5. 创建进度显示器
  6. 构建 rsync 命令
  7. 执行同步(含进度解析)
  8. 处理退出码

组合目标同步

run_composite_target(name, home, from_path, to_path)
  1. 获取子目标列表
  2. 遍历子目标
  3. 递归处理嵌套组合
  4. 执行单个目标同步

6. rsync 命令构建

默认选项

DEFAULT_RSYNC_OPTS = "-aKvz --delete --itemize-changes --progress"
  • -a: 归档模式(-rlptgoD)
  • -K: 保留指向目录的软链接
  • -v: 详细输出
  • -z: 压缩传输
  • --delete: 删除目标中多余文件
  • --itemize-changes: 输出每个文件的变更详情
  • --progress: 显示传输进度(用于大文件实时显示)

进度解析

解析 rsync 的 --itemize-changes 和 --progress 输出:

  • >f..t......: 文件传输
  • <f..t......: 文件接收
  • *deleting: 文件删除
  • XX% XXMB/s: 传输进度

根据解析结果实时更新 sync_count 和 recent_synced 队列。

7. 路径处理

# 绝对路径:base + path
if config_path.startswith('/'):
    full_src = f"{from_path}{config_path}"
# 相对路径:base/path
else:
    full_src = f"{base_path}/{config_path}"

8. 全局排除规则

GLOBAL_EXCLUDES = ["Caches", "__pycache__", "node_modules", ".venv"]

所有任务自动应用,无需逐个配置。

错误处理

rsync 退出码

代码 含义 处理
0 成功 正常完成
23 部分文件无法传输 警告 + 显示失败文件
24 部分文件传输错误 警告
其他 错误 显示错误

临时文件清理

finally:
    if dotfiles_temp_path:
        os.unlink(dotfiles_temp_path)

确保临时文件列表始终被删除。

性能优化

1. DOTFILES 扫描优化

问题:传统方法需要遍历整个目录树。

解决方案:仅扫描 .* 条目,直接跳过普通目录。

效果:扫描速度提升约 70 倍(从 ~14 秒降至 ~0.2 秒)。

2. 固定位置更新

问题:频繁的屏幕重绘导致性能下降。

解决方案:仅在内容变化时更新显示(哈希检测)。

实现:_get_display_hash() 计算显示内容哈希,相同则跳过。

3. 最小更新间隔

  • spinner 更新:100ms 间隔
  • 完整更新:200ms 间隔

避免过于频繁的终端刷新。

扩展性

添加新目标

def get_target_config(target: str, home: str) -> str:
    configs = {
         "new_target": f"{home}/path|{home}|excludes",
    }
    return configs.get(actual_target, "")

添加组合目标

def get_composite_target(target: str) -> str:
    composite_targets = {
         "new_composite": "target1 target2 target3",
    }
    return composite_targets.get(target, "")

安全考虑

  1. 路径验证:本地源路径在同步前验证存在性
  2. Dry-run 模式:支持预演,避免意外数据损失
  3. 临时文件清理:确保临时文件列表被删除
  4. 远程源跳过验证:避免不必要的网络请求

依赖管理

最小依赖原则

  • 仅依赖 Python 标准库
  • 使用 ANSI 转义码替代 Rich 等第三方 UI 库
  • rsync 作为唯一外部依赖

依赖检查

def check_dependencies() -> bool:
    try:
        subprocess.run(["rsync", "--version"],
                      stdout=subprocess.DEVNULL,
                      stderr=subprocess.DEVNULL,
                      check=True)
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        return False