让NAS自动复制存储卡上的文件

早期用过群晖的NAS,有个很好的功能是自带SD读卡器,可以直接插卡,然后按一下按键,就能自动把SD卡上的文件复制到NAS的硬盘上。群晖上有个USB Copy的套件,也可以实现自动插卡后就复制卡上的文件。

前一段自己组了个NAS,用了飞牛系统,飞牛的备份策略还是比较弱,虽然能备份外置USB读卡器或者外置USB硬盘的文件,但没有自动插卡后执行备份的功能,必须手工操作,于是就打算结合AI编程,自己写个脚本实现这个功能。

飞牛还有很多NAS系统的底层实际上都是Linux系统,所以基本上方法是类似的。

其实核心是利用了Udev,它可以管理挂载的设备,相关说明:https://www.reactivated.net/writing_udev_rules.html

通过它,实现插入USB设备后,能自动执行脚本。

实现起来也很简单,就3步

  1. 编写一个脚本,复制SD卡内的文件到制定目录
  2. 把这个脚本配置为一个Systemd的Service
  3. 编写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. 版权所有.

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇