syncmac 是一个基于 rsync 的个人文件同步工具,使用 Python 实现。设计目标是提供高效、可靠、易用的文件同步解决方案,特别针对 macOS 环境优化。
- 简单性 - 最小化依赖,仅使用 Python 标准库和 ANSI 转义码
- 性能 - 优化文件扫描和传输效率
- 安全性 - 默认 dry-run 模式,路径验证
- 可扩展性 - 易于添加新目标和配置
┌─────────────────────────────────────────────────────────────┐
│ 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 进程 │
└─────────────────────────────────────────────────────────────┘
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"提供终端控制能力:清除行/移动光标、颜色显示、样式控制。
"source|destination|excludes"source: 源路径(相对或绝对)destination: 目标路径(可选)excludes: 排除规则(可选)
DOTFILES: 特殊模式,仅同步隐藏文件- 空目标: 使用默认路径
设计目标:仅同步用户主目录下的隐藏文件和目录,避免遍历整个目录树。
实现:scan_dotfiles_directory() 函数
- 递归扫描
.*目录 - 非递归跳过普通目录
- 保留软链接(不添加尾斜杠)
- 创建临时文件列表供
--files-from使用
软链接处理:
- 软链接:不添加尾斜杠,rsync 保留链接本身
- 普通目录:添加尾斜杠,rsync 递归同步内容
优化效果:扫描时间从 ~14 秒降至 ~0.2 秒(约 70 倍提升)。
使用 \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 后,即使文件列表消失,仍会清除历史最大行数。
-
扫描阶段:
- 动画 spinner
- 已扫描文件数
-
同步阶段:
- 动画 spinner
- 已同步文件数 / 传输速度
- 无传输时显示"已检查 X.Xs"(v2.5.x 移除了最近同步文件列表显示)
使用 threading.Lock 保护共享状态,后台线程每 150ms 触发一次 spinner 更新。
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. 执行单个目标同步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 队列。
# 绝对路径:base + path
if config_path.startswith('/'):
full_src = f"{from_path}{config_path}"
# 相对路径:base/path
else:
full_src = f"{base_path}/{config_path}"GLOBAL_EXCLUDES = ["Caches", "__pycache__", "node_modules", ".venv"]所有任务自动应用,无需逐个配置。
| 代码 | 含义 | 处理 |
|---|---|---|
| 0 | 成功 | 正常完成 |
| 23 | 部分文件无法传输 | 警告 + 显示失败文件 |
| 24 | 部分文件传输错误 | 警告 |
| 其他 | 错误 | 显示错误 |
finally:
if dotfiles_temp_path:
os.unlink(dotfiles_temp_path)确保临时文件列表始终被删除。
问题:传统方法需要遍历整个目录树。
解决方案:仅扫描 .* 条目,直接跳过普通目录。
效果:扫描速度提升约 70 倍(从 ~14 秒降至 ~0.2 秒)。
问题:频繁的屏幕重绘导致性能下降。
解决方案:仅在内容变化时更新显示(哈希检测)。
实现:_get_display_hash() 计算显示内容哈希,相同则跳过。
- 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, "")- 路径验证:本地源路径在同步前验证存在性
- Dry-run 模式:支持预演,避免意外数据损失
- 临时文件清理:确保临时文件列表被删除
- 远程源跳过验证:避免不必要的网络请求
- 仅依赖 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