OpenCode 实践课:4 分钟用 Python 写出一个命令行目录同步备份工具

项目介绍

你是否经常需要把工作目录备份到 U 盘或 NAS?每次手动 cp -r 不仅麻烦,还会重复复制没有变化的文件。今天我们用 OpenCode 4 分钟写一个命令行目录同步工具——只复制有变化的文件,增量备份,高效省时。

技术栈:Python 3 标准库(osshutilargparsehashlib),零依赖。

最终效果

$ python sync.py ~/projects /mnt/backup/projects
[扫描] 找到 127 个文件...
[跳过] 112 个文件未变化
[复制] 15 个文件已更新
[摘要] 同步完成,耗时 2.3 秒

准备工作

  • Python 3.6+
  • OpenCode 已安装(pip install opencode 或访问 opencode.ai 获取)
  • 一个需要同步的源目录

实践过程

第一步:搭建命令行骨架

首先,我们让 OpenCode 生成一个接受两个目录参数的命令行脚本。

> 我对 OpenCode 说
>
> "帮我写一个 Python 命令行脚本 sync.py,用 argparse 接收两个位置参数:source 和 dest,分别表示源目录和目标目录。两个参数都是必填的,然后打印接收到的路径。"

OpenCode 生成如下代码:

import argparse
import os
import sys

def main():
    parser = argparse.ArgumentParser(description="目录同步备份工具")
    parser.add_argument("source", help="源目录路径")
    parser.add_argument("dest", help="目标目录路径")
    args = parser.parse_args()

    if not os.path.isdir(args.source):
        print(f"错误: 源目录不存在 - {args.source}")
        sys.exit(1)

    print(f"源目录: {args.source}")
    print(f"目标目录: {args.dest}")

if __name__ == "__main__":
    main()

关键要点:OpenCode 自动考虑了参数校验和目录存在性检查,这是经常被初学者忽略但至关重要的细节。你不需要自己记住 os.path.isdir() 这个 API,OpenCode 替你做了。

第二步:添加遍历逻辑

光有骨架不够,我们需要遍历源目录下的所有文件。继续让 OpenCode 干活。

> 我对 OpenCode 说
>
> "加上遍历 source 目录的功能,用 os.walk 递归获取所有文件的相对路径,存到一个列表里,打印文件数量。"

OpenCode 在 main() 中追加:

    files = []
    for root, dirs, filenames in os.walk(args.source):
        for f in filenames:
            rel_path = os.path.relpath(os.path.join(root, f), args.source)
            files.append(rel_path)

    print(f"[扫描] 找到 {len(files)} 个文件...")

关键要点:你不需要记住 os.walk 的三元组返回值、也不需要写相对路径转换的样板代码。OpenCode 帮你一行行写出来,你只需要确认逻辑正确即可。

第三步:增量对比——只复制变化的文件

核心逻辑来了:对比源文件和目标文件的 MD5 哈希值,只复制内容不同的文件。

> 我对 OpenCode 说
>
> "遍历文件列表,对每个文件计算 MD5 校验:如果目标文件不存在或哈希值不同,就用 shutil.copy2 复制过去(保留元数据)。统计跳过和复制的数量,最后打印摘要。进度用 print 实时输出,用 \r 做行内刷新。"

OpenCode 生成的完整同步代码:

import hashlib
import shutil
import time

def md5(path):
    h = hashlib.md5()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

start = time.time()
copied, skipped = 0, 0

for idx, rel in enumerate(files, 1):
    src_path = os.path.join(args.source, rel)
    dst_path = os.path.join(args.dest, rel)
    print(f"\r[{idx}/{len(files)}] {rel}", end="")

    if os.path.exists(dst_path):
        if md5(src_path) == md5(dst_path):
            skipped += 1
            continue
    os.makedirs(os.path.dirname(dst_path), exist_ok=True)
    shutil.copy2(src_path, dst_path)
    copied += 1

elapsed = time.time() - start
print(f"\n[跳过] {skipped} 个文件未变化")
print(f"[复制] {copied} 个文件已更新")
print(f"[摘要] 同步完成,耗时 {elapsed:.1f} 秒")

关键要点

OpenCode 用 hashlib.md5 做文件校验,比比较修改时间更可靠

shutil.copy2 保留了文件的时间戳和权限等元数据

进度显示用 \r 实现了原地刷新效果,体验流畅

os.makedirs(dst_path, exist_ok=True) 自动创建不存在的子目录

你完全不需要去查 shutil.copy2shutil.copy 的区别,OpenCode 直接给了正确答案。

第四步:收尾打磨

最后让 OpenCode 加上 --dry-run 预览模式和 --delete 清理过期文件。

> 我对 OpenCode 说
>
> "加上 --dry-run 参数(只显示会复制什么,不实际复制)和 --delete 参数(删除目标目录中源目录不存在的文件)。"

OpenCode 在 argparse 部分新增两个可选参数,并在复制逻辑中加判断:

parser.add_argument("--dry-run", action="store_true", help="仅预览,不实际复制")
parser.add_argument("--delete", action="store_true", help="删除目标中多余的文件")

# ... 在复制处包裹条件
if not args.dry_run:
    os.makedirs(os.path.dirname(dst_path), exist_ok=True)
    shutil.copy2(src_path, dst_path)

# --delete 逻辑
if args.delete:
    dest_files = []
    for root, dirs, filenames in os.walk(args.dest):
        for f in filenames:
            dest_files.append(os.path.relpath(os.path.join(root, f), args.dest))
    for f in dest_files:
        if f not in set(files):
            print(f"[删除] {f}")
            if not args.dry_run:
                os.remove(os.path.join(args.dest, f))

完整代码见下方,最终 98 行。

完整代码

#!/usr/bin/env python3
"""sync.py - 目录增量同步备份工具"""
import argparse
import hashlib
import os
import shutil
import sys
import time

def md5(path):
    """计算文件 MD5 哈希值"""
    h = hashlib.md5()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

def collect_files(directory):
    """递归收集目录下所有文件的相对路径"""
    files = []
    for root, dirs, filenames in os.walk(directory):
        for f in filenames:
            rel_path = os.path.relpath(os.path.join(root, f), directory)
            files.append(rel_path)
    return files

def main():
    parser = argparse.ArgumentParser(description="目录增量同步备份工具")
    parser.add_argument("source", help="源目录路径")
    parser.add_argument("dest", help="目标目录路径")
    parser.add_argument("--dry-run", action="store_true", help="仅预览,不实际复制")
    parser.add_argument("--delete", action="store_true",
                        help="删除目标目录中源目录不存在的文件")
    args = parser.parse_args()

    if not os.path.isdir(args.source):
        print(f"错误: 源目录不存在 — {args.source}")
        sys.exit(1)

    files = collect_files(args.source)
    print(f"[扫描] 找到 {len(files)} 个文件...")

    start = time.time()
    copied, skipped, deleted = 0, 0, 0

    for idx, rel in enumerate(files, 1):
        src_path = os.path.join(args.source, rel)
        dst_path = os.path.join(args.dest, rel)

        if os.path.exists(dst_path) and md5(src_path) == md5(dst_path):
            skipped += 1
            continue

        print(f"\r[{idx}/{len(files)}] {rel}", end="")
        if not args.dry_run:
            os.makedirs(os.path.dirname(dst_path), exist_ok=True)
            shutil.copy2(src_path, dst_path)
        copied += 1

    if args.delete:
        dest_files = collect_files(args.dest)
        source_set = set(files)
        for rel in dest_files:
            if rel not in source_set:
                print(f"\r[删除] {rel}")
                if not args.dry_run:
                    os.remove(os.path.join(args.dest, rel))
                deleted += 1

    elapsed = time.time() - start
    print(f"\r{'─' * 40}")
    print(f"[跳过] {skipped} 个文件未变化")
    print(f"[复制] {copied} 个文件已更新")
    if args.delete:
        print(f"[清理] {deleted} 个文件已删除")
    print(f"[摘要] 同步完成,耗时 {elapsed:.1f} 秒")

    if args.dry_run:
        print("[提示] 预览模式,未实际修改文件")

if __name__ == "__main__":
    main()

小结

从零到生成 98 行可运行的 Python 同步工具,整个过程只用了 4 分钟。我做的事情仅仅是:

用自然语言描述每一步要什么功能

让 OpenCode 生成代码

看一眼确认逻辑正确

进入下一步

没有翻标准库文档查 shutil.copy2 的参数,没有手写 os.walk 的嵌套循环,没有纠结 MD5 分块读取的缓冲区大小——这些 OpenCode 全搞定了。

相比传统开发方式:查文档 → 写代码 → 调试 → 查文档的循环,OpenCode 把 "写代码" 这个环节变成了 "描述需求 → 确认结果"。效率提升至少 3 倍,而且代码质量有保障——OpenCode 生成的代码自带错误处理、进度提示、边界条件判断,这些细节如果自己手写很容易遗漏。

如果你还没试过用 AI 编程助手来写脚本,这个 4 分钟的小项目就是最好的起点。