用树莓派做一个自动备份器

起因

由于自己拍行车的 POV 视频,一天会产出一到三百 GB 的视频文件,我的存储卡数量有限,再买存储卡的花费太高,就需要每天把存储卡里面的视频拷贝到移动硬盘上,回家之后一股脑地拷贝到 NAS 里面。由于家里的电脑是台式机,所以如果要外出旅游,就要租电脑来拷贝文件、剪视频,顺便做点别的。

但是,租赁并非总是一帆风顺:3 月的时候,我想为接下来回去的旅行租一台电脑,备份拍摄的素材、剪视频。然后家里人知道我租电脑后,都勃然大怒,骂我乱花钱(只要我为自己花钱,都会被这么骂),我妈甚至扬言要砸了租来的电脑,因此我和他们起了激烈冲突。拿到电脑后几个小时,电脑突然连不上网了,能够判断是软件问题,不重装系统无法解决,而这种租赁的电脑一般都在 BIOS、Bitlock 方面动手脚,强行重装系统会很麻烦。于是第二天好不容易退了。至此,我被折腾地也不想租电脑了,如果想拷贝自己拍摄的视频,也只能找网吧了。

然而网吧的电脑是专门为游戏组装的,提供的 USB 接口数量要么不够,要么是 USB 2.0 的,能找到两个 USB 3.0 的接口的网吧,在我的旅途中只有四家。最主要的是,我每次拷贝文件都要拷贝一到三百 GB 的文件,这导致自己要经常在网吧花费一两小时。虽然我能够依靠打游戏度过这段时间,但是由于我是旅游的,要把大部分时间留给开车和逛景点,这导致我到网吧的时间基本上都是半夜了,还要守着电脑拷贝文件,实在是严重影响睡眠。

之前我就想着,有没有一种设备,把存储设备插进去,就能自动备份存储设备里的数据。这样的话,出远门就不用带电脑了,毕竟再小的笔记本电脑,还是占体积的。这种设备实际上是有的,比如说:

相比之下,在自己有移动硬盘的情况下,用树莓派这种单板电脑做一个这种设备,应该是最实惠的了。

需求

设备上有至少两个 USB 接口。当插入了两个 USB 存储设备(如 U 盘、移动硬盘、读卡器),则开始复制较小容量的设备里面的文件到较大容量的设备里面。

如果目标设备的空间不足,报错。

复制的文件放在根目录的一个目录下,目录名称为复制时的日期加上数字序号。如果数字序号超出位数,报错。

复制时最好能够显示进度,以文件体积为单位。如果可能,要显示当前复制的文件内容。

复制完成后,要弹出两个存储设备。如果弹出失败,报错。

能够离线运行,启动设备后,不需要键鼠操作即可备份。

硬件选择

由于树莓派的各种资料比较完备,作为初学者还是用它吧。4B 及之后的型号支持两个 USB 3.0 接口,适合做这种备份设备。然后,树莓派 5B 支持 PCI,可以通过扩展板插 M.2 硬盘,如果你想做出更紧凑的设备,可以考虑这种型号(当然你要考虑怎么把硬盘数据拷贝出来)。至于内存,如果你只是想备份文件,没什么要求。

我买的是二手的树莓派 5B 8GB 版本(因为还想学习硬件开发之类的其他东西),除了连接线、电源和外壳外,没有买其他外设。

指示灯控制

理论上,可以通过树莓派自带的指示灯来展示复制状态。

树莓派 5B 有一个指示灯,有两种分立而位置接近的颜色:红、绿(两者组合就是黄)。通过这两个文件可以控制指示灯的开关:

  • /sys/class/leds/PWR/brightness
  • /sys/class/leds/ACT/brightness

这两个文件要 root 权限才能编辑。文件内容有且仅有一个字符串,值是 01。经测试,值与灯光颜色关系如下:

↓ PWR \ ACT → 0 1
0 绿
1

如果你用 Python,可以编写如下脚本验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import time


def led(file, bright):
with open(file, 'w') as f:
f.write(str(bright))

def pwr(bright):
led('/sys/class/leds/PWR/brightness', bright)

def act(bright):
led('/sys/class/leds/ACT/brightness', bright)

def idle(elapsed_time):
pwr(0)
act(1)
time.sleep(elapsed_time)

def red(elapsed_time):
pwr(1)
act(1)
time.sleep(elapsed_time)

def yellow(elapsed_time):
pwr(1)
act(0)
time.sleep(elapsed_time)

def green(elapsed_time):
pwr(0)
act(0)
time.sleep(elapsed_time)

if __name__ == '__main__':
while True:
idle(3)
red(1)
idle(1)
yellow(1)
idle(1)
yellow(1)
idle(1)
green(1)
idle(1)
green(1)
idle(1)
green(1)

以上脚本需 root 权限执行,结果就是做如下循环:红灯闪烁 1 次,黄灯闪烁 2 次、绿灯闪烁 3 次。

编写复制脚本

实际上,与其说是“编写”,不如说是 AI 生成。

我此前用 Cursor 生成过 Windows 环境下执行复制指令的脚本,效果还可以。由于 Windows 和 Linux 相关的 API 不一样,因此还是要重写。

最开始的提示词如下:

现在有一台安装 Raspberry Pi OS 的树莓派 5B。

设备上有红灯和绿灯,由两个文件控制,文件内容有且只有一个字符串,写入文件后要关闭文件指针才能生效。

  • /sys/class/leds/PWR/brightness,以下简称 PWR

  • /sys/class/leds/ACT/brightness,以下简称 ACT

  • 当 PWR 和 ACT 均为 0,则亮绿灯;

  • 当 PWR 和 ACT 均为 1,则亮红灯;

  • 当 PWR 为 0,ACT 为 1,则不亮;

  • 当 PWR 为 1,ACT 为 0,则亮黄灯。

我希望写一个 Python 脚本,随系统启动而启动。为调试方便,程序需作为守护程序运行,并提供注册为服务的方法。

如果插入的 USB 设备有两个存储设备(如 U 盘、移动硬盘、存储卡),那么自动将总容量小的存储设备里面的所有文件都复制到总容量大的那个设备。

复制完成后,自动弹出两个设备。如果弹出失败,隔 5 秒重试;重试 3 次仍然失败,则报错。

复制时,复制到根目录下的一个文件夹里面,文件夹命名形如“20250503001”,其中“20250503”为复制时的日期,“001”表示是第几次复制。 目标文件夹的序号应该从已有的这类文件夹读取。

  1. 搜索根目录下形如 XXXXXXXXXXX 的文件,前八位为日期,后三位为序号
  2. 如果不存在日期为当前日期的文件夹,则直接创建当前日期,序号为 001 的文件夹
  3. 如果存在日期为当前日期的文件夹,则找到序号最大的文件夹,以其加一命名。如果前面说的已有文件夹的序号为 999,则不要复制,并报错。

当总容量大的设备的剩余空间不足以复制,则不复制,报错。

由于没有屏幕,故通过 LED 灯指示复制状态:

  • 程序启动后,设备上亮黄灯慢速闪烁(亮 2s,灭 2s)。
  • 复制时,设备上亮黄灯快速闪烁(亮 0.5s,灭 0.5s)。
  • 当复制成功时,设备上亮绿灯持续闪烁(亮 1s,灭 1s)。
  • 当复制失败或报错时,设备上亮红灯持续闪烁:
    • 目标存储设备空间不足:亮 1s,灭 1s,亮两次后灭 3s,继续;
    • 文件夹序号达到最大值:亮 1s,灭 1s,亮三次后灭 3s,继续;
    • 文件系统不支持:亮 1s,灭 1s,亮四次后灭 2s,继续;
    • 未能弹出全部设备:亮 0.5s,灭 0.5s,亮三次后灭 2s,继续;
    • 其他:亮 0.5s,灭 0.5s,亮五次后灭 2s,继续。
  • 如果拔出两个存储设备,设备上恢复到亮黄灯慢速闪烁(亮 2s,灭 2s)。

复制文件时,在控制台中显示进度条,进度条单位为文件大小,如 MB、GB。显示进度时也显示文件数量、当前复制的文件名。

如果要使用配置文件存取配置,请使用 yaml 格式,配置文件为根目录下的 config.yaml。

现阶段大家虽然把 AI 编程吹得神乎其神,但是,就算 AI 编程能够一次性解决 90% 的问题,剩下的 10% 就足够搞得焦头烂额了,反倒是占了 90% 的时间。

接下来调试的时候,我遇到的问题包括但不限于:

  1. 插入移动硬盘,未识别:原因是被系统识别为 ATA 设备(执行 sudo udevadm info --query=property --name=设备文件 后的信息里面,ID_BUS=ata),程序只检测 ID_BUSusb 的设备。
  2. 插入一块 U 盘时就开始复制了:原因是程序错误地将同一个 USB 设备的磁盘和分区分别识别为两个不同设备。
  3. 弹出设备时,报错。实际发现均成功弹出:原因是:
    • 同一设备被尝试弹出两次;
    • 第一次成功弹出后,第二次尝试时设备已不存在,导致报错。
  4. 尝试用 Ctrl+C 结束程序时,要按两次才能退出:递归调用问题,修改两次解决。
  5. 指示灯有问题:之后的问题都是这个了,进行了不下 5 次调试和修改才勉强完成。

我原本想让它在弹出设备并手动拔出全部设备后,恢复到亮黄灯慢速闪烁状态的。但是,试了很多次都不行。我自己查阅资料,调试,发现至少在树莓派上面,是不可行的。于是就放弃了。

经测试,在未精简的 Raspberry Pi OS 上,只要安装一个 Python 包:PyYAML,即可使用该脚本。就算是精简的,也可以用安装脚本安装。

同时,还可以安装为服务,启动树莓派之后,如果指示灯变为黄色慢速闪烁,则代表已准备好备份。当然,启动前就插好 USB 存储设备,也可以在启动后自动备份。

项目文件见:DingJunyao/autocopy

如果要离线复制,需要给树莓派添加时钟模块电池。

扩展

这个项目实际上非常简陋:由于设备限制,只以单独的 LED 指示灯展示复制状态。如果能够添加一些元件(如 LED、显示屏、蜂鸣器),可以做到复制的时候显示进度、复制完成发出声音这样的功能。

而且,考虑到一些设备会在存储卡里面写缓存文件,以及其他一些与项目无关的文件,可以设置过滤的文件类型和目录。

由于我刚开始学硬件开发,这些东西就等以后吧。