我给自己写了一个本地文件夹同步备份工具:先预览,再增量复制
目录
- 缘起:复制整个文件夹为什么仍然不放心
- 需求拆解:同步和备份不是一回事
- 技术选型:标准库就能完成增量备份
- 核心实现一:生成可检查的同步计划
- 核心实现二:安全复制与旧版本保留
- UI 定制:任何写入都先展示影响范围
- 三个备份工具不能回避的坑
- 结尾:没有验证过的备份,只是一份希望
1. 缘起:复制整个文件夹为什么仍然不放心
我有几个目录需要定期备份:正在开发的项目、合同附件、博客素材和一些不适合上传云盘的工作文件。最开始的办法很朴素——隔一段时间把整个文件夹复制到移动硬盘。
这个方法用久后,问题越来越明显:
- 文件夹很大,即使只改了几个文件也要重新复制很久;
- 中途断开移动硬盘,不知道哪些文件已经完成;
- 同名目录越来越多,很难确认哪份是最新版本;
- 手动选择文件时可能漏掉新增的子目录;
- 如果使用镜像命令,源目录误删也可能同步删除备份。
我真正需要的不是双向同步,也不是实时监控,而是一个明确的单向流程:比较源目录和备份目录,列出即将新增或更新的文件,确认后执行,并保留可核对的记录。
2. 需求拆解:同步和备份不是一回事
在开始写代码前,我先划清“同步”和“备份”的边界。同步追求两边状态一致,备份更关心旧数据能否恢复。我的工具以备份为目标,因此默认不会删除目标端多出的文件。
| 诉求(人话) | 拆解后(可验收) |
|---|---|
| 只复制变化内容 | 比较相对路径、大小和修改时间 |
| 执行前知道会发生什么 | 生成新增、更新、跳过和冲突清单 |
| 原有备份不要被直接覆盖 | 更新前将旧文件移动到版本目录 |
| 中断后能继续 | 每个文件独立完成并记录结果 |
| 复制完确认内容正确 | 可选执行 SHA-256 校验 |
| 不让误删传到备份 | 默认永不删除目标端文件 |
第一版不做双向合并。双向同步需要解决两边同时修改、删除传播和冲突选择,规则复杂得多,也更容易让“备份工具”变成“误操作放大器”。
心得:工具越接近数据安全,默认行为越应该保守。
3. 技术选型:标准库就能完成增量备份
这个工具主要处理本地文件系统,Python 标准库已经覆盖所需能力:
| 能力 | 方案 |
|---|---|
| 遍历目录 | pathlib.Path.rglob() |
| 复制文件和元数据 | shutil.copy2() |
| 内容校验 | hashlib.sha256() |
| 原子替换 | os.replace() |
| 备份清单 | JSON Lines |
| 桌面界面 | tkinter |
我没有引入数据库。每次执行生成一份只追加的 JSON Lines 日志,一行对应一个文件结果。即使程序意外退出,已写入的记录仍然可以逐行读取,不会因为最后一个 JSON 数组没有闭合而整份损坏。
复制任务默认串行执行。备份目标经常是移动硬盘或机械硬盘,并行读取和写入不一定更快,还会让进度和错误恢复变复杂。稳定比跑满磁盘更重要。
4. 核心实现一:生成可检查的同步计划
同步计划以源目录中的相对路径为核心。无论源目录放在哪里,同一个文件都能映射到目标目录中的确定位置。
from dataclasses import dataclass
from pathlib import Path
@dataclass
class PlanItem:
action: str
source: Path
target: Path
relative: Path
size: int
def build_target(source_file, source_root, target_root):
relative = source_file.relative_to(source_root)
return relative, target_root / relative
比较时先使用文件大小和纳秒级修改时间。目标不存在就是新增;大小不同就是更新;两者相同则先视为未变化。
def decide_action(source, target):
if not target.exists():
return "add"
source_stat = source.stat()
target_stat = target.stat()
if source_stat.st_size != target_stat.st_size:
return "update"
if source_stat.st_mtime_ns != target_stat.st_mtime_ns:
return "verify"
return "skip"
verify 项会进一步计算哈希。如果内容相同,只修正清单状态,不重复复制;如果哈希不同,才进入更新列表。
扫描完成后,计划会汇总文件数量、预计复制字节数和目标端已有但源端不存在的文件。最后这一类只标记为“目标独有”,不会自动删除。
心得:先把文件变化变成一份静态计划,执行过程才有依据,也更容易复核。
5. 核心实现二:安全复制与旧版本保留
直接把源文件复制到最终目标有一个问题:写到一半断电,目标路径下会留下看似正常、实际不完整的文件。我的做法是先写同目录临时文件,成功后再替换。
import os
import shutil
import uuid
def copy_safely(source, target):
target.parent.mkdir(parents=True, exist_ok=True)
temp = target.with_name(f".{target.name}.{uuid.uuid4().hex}.tmp")
try:
shutil.copy2(source, temp)
os.replace(temp, target)
finally:
if temp.exists():
temp.unlink()
如果目标文件已经存在,程序不会立刻覆盖。它会先将旧版本移动到目标根目录下的版本区:
.backup_versions/2026-11-06_103500/文档/合同.docx
版本目录保留原始相对路径,恢复时不会丢失文件位置。只有旧版本移动成功后,新文件才开始写入。
可选的严格模式会在复制后分别计算源文件和目标文件的 SHA-256。校验失败时,新文件被标记为不可用,旧版本仍然保留。对大文件来说校验会增加一次完整读取,所以界面会明确展示时间成本。
6. UI 定制:任何写入都先展示影响范围
界面顶部只需要选择源目录和目标目录。点击“扫描差异”后,中间表格展示完整计划:
操作 | 相对路径 | 文件大小 | 原因 | 目标状态
新增使用绿色,更新使用橙色,冲突使用红色,跳过项默认折叠。用户可以按操作类型筛选,也能打开源文件和目标文件所在目录进行人工确认。
开始备份前,界面再次显示四个数字:新增数量、更新数量、预计写入体积和将保留的旧版本数量。只要源目录或目标目录在扫描后发生变化,原计划就会失效,必须重新扫描。
执行过程中,每完成一个文件就写一条日志。停止按钮只阻止后续文件开始,不会强行中断当前复制。结束页面会列出成功、失败和未执行项,并提供重新执行失败项的入口。
我没有加入“保持两边完全一致”的按钮。如果以后真的需要镜像模式,它也必须作为独立功能出现,并对删除列表进行单独确认。
7. 三个备份工具不能回避的坑
坑一:修改时间不是绝对可靠的内容证明。 不同文件系统的时间精度不同,复制工具也可能保留旧时间。大小和时间适合快速筛选,但对可疑项目仍要用哈希确认,严格模式则对所有实际复制文件做校验。
坑二:符号链接可能把扫描带到源目录之外。 一个链接可能指向上级目录、网络路径,甚至形成循环。工具默认只记录符号链接本身,不跟随目录链接;如果无法按当前平台安全复制,就明确跳过并报告。
坑三:能复制成功不等于能恢复。 备份目录中有文件,只能说明写入发生过。定期从备份中抽取文件、校验哈希并尝试打开,才能验证恢复链路。工具因此提供“只校验不复制”模式,用清单重新检查现有备份。
还有一个必须明确的事实:如果源目录和备份目录位于同一块物理硬盘,硬盘损坏时两份数据可能同时丢失。工具会提示这种情况,但不会假装同盘副本等同于真正备份。
8. 结尾:没有验证过的备份,只是一份希望
完成这个工具后,我不再定期复制整个大目录,也不会因为害怕镜像删除而一直拖着不备份。每次操作都从一份差异计划开始,哪些文件会新增、哪些会更新、旧版本保存在哪里,都能提前看到。
一个让我放心的本地备份流程包含五件事:
- 明确数据只从源目录流向目标目录;
- 写入之前展示完整计划;
- 更新文件之前保留旧版本;
- 中断后能识别已完成和未完成项目;
- 定期校验并实际尝试恢复。
如果你也想做类似工具,可以先实现单向新增复制和预览,不要一开始就加入删除、双向合并与实时监控。备份工具最可贵的能力不是自动化程度,而是每一步都可解释、可停止、可恢复。
当备份结果能够被验证,它才不再是一份心理安慰,而是真正可用的数据保障。
- 点赞
- 收藏
- 关注作者
评论(0)