早期用过群晖的NAS,有个很好的功能是自带SD读卡器,可以直接插卡,然后按一下按键,就能自动把SD卡上的文件复制到NAS的硬盘上。群晖上有个USB Copy的套件,也可以实现自动插卡后就复制卡上的文件。
前一段自己组了个NAS,用了飞牛系统,飞牛的备份策略还是比较弱,虽然能备份外置USB读卡器或者外置USB硬盘的文件,但没有自动插卡后执行备份的功能,必须手工操作,于是就打算结合AI编程,自己写个脚本实现这个功能。
飞牛还有很多NAS系统的底层实际上都是Linux系统,所以基本上方法是类似的。
其实核心是利用了Udev,它可以管理挂载的设备,相关说明:https://www.reactivated.net/writing_udev_rules.html
通过它,实现插入USB设备后,能自动执行脚本。
实现起来也很简单,就3步
- 编写一个脚本,复制SD卡内的文件到制定目录
- 把这个脚本配置为一个Systemd的Service
- 编写Udev的规则,当插入卡时,执行Service。
为什么要把脚本变成Service而不是直接执行脚本,是因为有个坑是Udev调用脚本有个默认超时时间,而如果复制文件的时间较长,超时了,脚本就会自动中断,这个也是我一开始测试的时候遇到的坑,AI编程工具没有意识到这个问题,而是遇到自动退出问题后反复的增加了各种监控和测试脚本。看来AI也不是万能的。
文件复制脚本
文件复制脚本其实内容可以很简单,插上SD卡以后,看一下SD卡自动挂载的路径就可以了,飞牛的话,是会固定挂载在 /vol00/MassStorageClass
目录下,每次都一样,当然,如果你的NAS同时插了好几个USB设备比如U盘、读卡器、USB硬盘之类的,那么可能路径会不一样,自己用SSH登录飞牛系统df命令看一下就可以了。
而目标路径,一般飞牛的话第一个储存空间是/vol1/1000/
,第二个就是/vol2/1000/
,以此类推,看你想把SD卡的文件复制到哪个就选具体的路径就可以了。
脚本里可以用cp命令直接复制 比如 cp /vol00/MassStorageClass/* /vol1/1000/backup/
,也可以用rsync命令 rsync -av /vol00/MassStorageClass/ /vol1/1000/backup/
,rsync命令还可以加 --exclude-from
参数,后面跟一个文本文件,里面写上你希望排除不复制的文件,比如 .* 表示不复制 .开头的隐藏文件等等。
当然,还可以加各种功能比如统计复制时间,统计复制了多少个文件,复制完成以后删除卡上的文件等功能,建议再结合 Server酱或者pushover、gotify之类的工具,在复制完成后发一个推送消息到手机,这样就能及时知道复制完了。以上功能都可以找一个AI编程工具,说清楚需求自动生成即可。
比如可以用类似如下的提示词(等待5秒是怕刚插入SD卡时还没完成挂载就运行复制命令,会报错):
写一个Linux下的BASH脚本,脚本能够使用rsync命令将/vol00/MassStorageClass/下的文件均复制到 /vol1/1000/backup/,需要在复制前检查源目录是否存在,不存在的话等待5秒重试。需要检查目标目录空间是否足够。统计复制了多少个文件及耗时,复制成功后将源目录下的文件均删除。
以上提示词可以根据需要修改。
Systemd的Service
这一步比较常规,就是在 /etc/systemd/system/
目录下创建一个Service文件,比如usbcopy.service 内容类似如下:
[Unit]
Description=功能描述
[Service]
ExecStart=/usr/bin/bash /root/usbcopy.sh (替换为你脚本的路径)
Restart=on-failure
[Install]
WantedBy=multi-user.target
然后执行 systemctl start usbcopy 试一下能否成功运行,可以的话就OK了。
编写Udev规则
这一步也比较简单,就是在 /etc/udev/rules.d/
目录下创建一个规则文件,比如usbcopy.rule ,写入类似如下内容:
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd[a-z][0-9]*", ENV{ID_BUS}=="usb", ENV{ID_FS_TYPE}!="", RUN+="/bin/systemctl start usbcopy.service"
这里我是匹配了所有设备
KERNEL=="sd[a-z][0-9]*"
当然你可以根据需要直接写你机器上SD卡对应的设备,比如sda 就是 KERNEL=="sda"
这个通过df命令也可以直接看到。
其实意思就是当机器上增加了sda设备后,执行 /bin/systemctl start usbcopy.service
也就是刚才增加的Systemd服务。
添加后,刷新一下规则
sudo udevadm control --reload-rules
sudo udevadm trigger
然后插个SD卡测试一下就可以了。
注意事项
如果脚本加入了删除文件的功能,记得多测试几次后再正式使用,避免复制没成功又把有用的文件删除了。
文件复制的示例代码
以下示例代码仅供参考,实现从/vol00/MassStorageClass
到/vol2/1000/photos/sdbackup
的文件复制,在/home/szqp/autocopy/exclude-list.txt
中写入了排除不复制的文件列表。统计复制文件数量和时间,完成后删除源目录下文件,并通过Server酱发送提醒信息。
因为基本是AI生成的,虽然在我的NAS上能用,但不代表你的也可以,要自行修改相关配置,不能直接运行:
#!/bin/bash
# USB自动复制脚本
# 功能:从固定USB挂载点复制文件到指定目录
# 日志文件
LOG_FILE="/var/log/usb_autocopy.log"
SOURCE_DIR="/vol00/MassStorageClass"
TARGET_DIR="/vol2/1000/photos/sdbackup"
# 信息发送函数
sc_send() {
local text=$1
local desp=$2
local key="你Server酱的key"
postdata="text=$text&desp=$desp"
opts=(
"--header" "Content-type: application/x-www-form-urlencoded"
"--data" "$postdata"
)
# 判断 key 是否以 "sctp" 开头,选择不同的 URL
if [[ "$key" =~ ^sctp([0-9]+)t ]]; then
# 使用正则表达式提取数字部分
num=${BASH_REMATCH[1]}
url="https://${num}.push.ft07.com/send/${key}.send"
else
url="https://sctapi.ftqq.com/${key}.send"
fi
# 使用动态生成的 url 发送请求
result=$(curl -X POST -s -o /dev/null -w "%{http_code}" "$url" "${opts[@]}")
echo "$result"
}
# 记录日志函数
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
# 检查USB挂载点是否存在
check_usb_mount() {
# 等待挂载点出现,最多等待10秒
for i in {1..10}; do
if [ -d "$SOURCE_DIR" ] && [ "$(ls -A "$SOURCE_DIR" 2>/dev/null)" ]; then
log_message "发现USB挂载点: $SOURCE_DIR (第${i}次检查)"
return 0
fi
if [ $i -eq 1 ]; then
log_message "等待USB挂载点出现: $SOURCE_DIR"
fi
sleep 1
done
log_message "USB挂载点不存在或为空: $SOURCE_DIR"
return 1
}
# 检查USB挂载点
log_message "检查USB挂载点..."
if ! check_usb_mount; then
log_message "错误: USB挂载点不可用或为空"
exit 1
fi
# 使用固定的挂载点
MOUNT_POINT="$SOURCE_DIR"
log_message "使用USB挂载点: $MOUNT_POINT"
# 验证挂载点的可访问性
if [ ! -r "$MOUNT_POINT" ]; then
log_message "错误: 挂载点 $MOUNT_POINT 不可读"
exit 1
fi
# 创建目标目录(如果不存在)
if [ ! -d "$TARGET_DIR" ]; then
mkdir -p "$TARGET_DIR"
if [ $? -ne 0 ]; then
log_message "错误: 无法创建目标目录 $TARGET_DIR"
exit 1
fi
log_message "创建目标目录: $TARGET_DIR"
fi
# 生成唯一的子目录名(基于时间戳)
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
COPY_DIR="$TARGET_DIR/usb_backup_${TIMESTAMP}"
# 创建复制目录
mkdir -p "$COPY_DIR"
if [ $? -ne 0 ]; then
log_message "错误: 无法创建复制目录 $COPY_DIR"
exit 1
fi
log_message "开始复制文件到: $COPY_DIR"
# 检查源目录是否为空
if [ -z "$(ls -A "$MOUNT_POINT" 2>/dev/null)" ]; then
log_message "警告: USB设备为空,没有文件需要复制"
COPY_RESULT=0
SOURCE_FILE_COUNT=0
else
# 计算源文件数量和大小
SOURCE_FILE_COUNT=$(find "$MOUNT_POINT" -type f 2>/dev/null | wc -l)
SOURCE_SIZE=$(du -sb "$MOUNT_POINT" 2>/dev/null | cut -f1)
log_message "检测到 $SOURCE_FILE_COUNT 个文件,总大小: $(echo $SOURCE_SIZE | awk '{printf "%.2f MB", $1/1024/1024}')"
# 检查目标目录可用空间
AVAILABLE_SPACE=$(df "$TARGET_DIR" | tail -1 | awk '{print $4}')
AVAILABLE_BYTES=$((AVAILABLE_SPACE * 1024))
if [ "$SOURCE_SIZE" -gt "$AVAILABLE_BYTES" ]; then
ERROR_MSG="磁盘空间不足。需要: $(echo $SOURCE_SIZE | awk '{printf "%.2f MB", $1/1024/1024}'),可用: $(echo $AVAILABLE_BYTES | awk '{printf "%.2f MB", $1/1024/1024}')"
log_message "错误: $ERROR_MSG"
sc_send "USB文件复制失败" "$ERROR_MSG"
rm -rf "$COPY_DIR"
exit 1
fi
# 记录复制开始时间
COPY_START_TIME=$(date +%s)
# 使用rsync进行文件复制
if command -v rsync >/dev/null 2>&1; then
log_message "使用rsync进行文件复制"
rsync -av --exclude-from='/home/szqp/autocopy/exclude-list.txt' "$MOUNT_POINT/" "$COPY_DIR/" 2>&1
COPY_RESULT=$?
COPY_METHOD="rsync"
else
log_message "使用cp进行文件复制"
cp -r "$MOUNT_POINT/"* "$COPY_DIR/" 2>&1
COPY_RESULT=$?
COPY_METHOD="cp"
fi
# 计算复制耗时
COPY_END_TIME=$(date +%s)
COPY_DURATION=$((COPY_END_TIME - COPY_START_TIME))
fi
if [ $COPY_RESULT -eq 0 ]; then
log_message "文件复制成功完成"
# 发送成功通知
if [ $SOURCE_FILE_COUNT -gt 0 ]; then
# 格式化复制时间
if [ $COPY_DURATION -ge 60 ]; then
DURATION_TEXT="${COPY_DURATION}秒 ($(($COPY_DURATION / 60))分$(($COPY_DURATION % 60))秒)"
else
DURATION_TEXT="${COPY_DURATION}秒"
fi
SUCCESS_MSG="成功复制 $SOURCE_FILE_COUNT 个文件,耗时: $DURATION_TEXT。总大小: $(echo $SOURCE_SIZE | awk '{printf "%.2f MB", $1/1024/1024}')。目标目录: $COPY_DIR"
log_message "复制了 $SOURCE_FILE_COUNT 个文件,耗时 $DURATION_TEXT"
# 删除USB设备上的源文件
log_message "开始删除USB设备上的源文件..."
if rm -rf "$MOUNT_POINT/"* 2>/dev/null; then
log_message "成功删除USB设备上的源文件"
sc_send "USB文件复制成功,清理完成" "$SUCCESS_MSG 。文件清理完成"
else
ERROR_MSG="删除USB设备上的源文件失败"
log_message "错误: $ERROR_MSG"
sc_send "USB文件复制成功,清理失败" "$SUCCESS_MSG 。错误:$ERROR_MSG"
fi
else
sc_send "USB设备为空" "USB设备中没有文件需要复制"
fi
else
ERROR_MSG="文件复制失败,错误代码: $COPY_RESULT"
log_message "$ERROR_MSG"
sc_send "USB文件复制失败" "$ERROR_MSG。复制方法: $COPY_METHOD。源目录: $MOUNT_POINT。目标目录: $COPY_DIR"
fi
log_message "脚本执行完成"
exit 0
© 2025, QP. 版权所有.