一、前言
需要把外部 U 盘 / USB 存储自动“入库”归档、备份并记录元信息?本篇给出极简、可落地的方案:在一台 Linux(可用 Raspberry Pi)上通过 udev 触发 + 挂载脚本 + rsync 备份 + SQLite 记录,实现 U 盘一插即存、自动去重与日志留存。脚本短小、步骤直接可复制粘贴。

二、适用场景
- 会议现场、实验室、公司要聚焦收集多人的 U 盘资料;
- 希望自动化拷贝 U 盘内容并记录来源、时间与大小;
- 需要一个本地化、无需人工干预的“U 盘入库”流程。
三、准备(目标主机示例:Ubuntu / Debian / Raspberry Pi OS)
sudo apt update
sudo apt install -y rsync sqlite3 udev
# 可选安装 ntfs-3g 支持 NTFS 文件系统写入
sudo apt install -y ntfs-3g
创建存储与工作目录:
sudo mkdir -p /srv/usb_archive /opt/usb_ingest
sudo chown $(whoami):$(whoami) /srv/usb_archive /opt/usb_ingest

四、设计思路(简要)
- udev 规则检测到 USB 存储设备插入,调用处理脚本并传入设备节点(如 /dev/sda1)。
- 脚本读取设备的唯一标识(LABEL / UUID / SERIAL),在 /srv/usb_archive/<serial>/<timestamp>/ 下挂载并用 rsync -a 复制文件。
- 复制完成后调用 Python 日志脚本写入 SQLite(记录设备 id、挂载点、文件大小、文件数、时间、来源 IP/备注等)。
- 卸载设备并清理临时目录。
五、实现步骤(全部可复制)
1)SQLite 日志脚本(用于记录每次入库)
保存为 /opt/usb_ingest/log_db.py:
#!/usr/bin/env python3
# /opt/usb_ingest/log_db.py
import sqlite3,sys,time,os
DB=”/opt/usb_ingest/usb_archive.db”
def init():
os.makedirs(os.path.dirname(DB), exist_ok=True)
conn=sqlite3.connect(DB)
c=conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS archives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
label TEXT,
mount_point TEXT,
path TEXT,
files INTEGER,
bytes INTEGER,
ts INTEGER,
note TEXT
)''')
conn.commit()
conn.close()
def log(device_id,label,mount_point,path,files,bytes,note=””):
conn=sqlite3.connect(DB)
c=conn.cursor()
c.execute('INSERT INTO archives (device_id,label,mount_point,path,files,bytes,ts,note) VALUES (?,?,?,?,?,?,?,?)',
(device_id,label,mount_point,path,files,bytes,int(time.time()),note))
conn.commit()
conn.close()
if __name__ == “__main__”:
init()
if len(sys.argv)>=7:
log(*sys.argv[1:7], note=(sys.argv[7] if len(sys.argv)>7 else “”))
else:
print(“usage: log_db.py device_id label mount_point path files bytes [note]”)
赋可执行权限:
chmod +x /opt/usb_ingest/log_db.py
2)主入库脚本(udev 调用它)
保存为
/opt/usb_ingest/ingest_usb.sh:
#!/bin/bash
# /opt/usb_ingest/ingest_usb.sh
set -e
DEVNODE=”$1″ # e.g. /dev/sda1
WORKDIR=”/opt/usb_ingest/work”
ARCHIVE_BASE=”/srv/usb_archive”
LOGSCRIPT=”/opt/usb_ingest/log_db.py”
mkdir -p “$WORKDIR”
# sleep 小延迟以确保设备就绪
sleep 1
# 获取识别信息(优先用 ID_SERIAL_SHORT,否则用 UUID 或 device name)
DEVNAME=$(basename “$DEVNODE”)
UDEV_INFO=$(udevadm info –query=property –name=”$DEVNODE” 2>/dev/null || true)
ID_SERIAL=$(echo “$UDEV_INFO” | awk -F= '/ID_SERIAL_SHORT=/{print $2; exit}')
ID_FS_LABEL=$(echo “$UDEV_INFO” | awk -F= '/ID_FS_LABEL=/{print $2; exit}')
ID_FS_UUID=$(echo “$UDEV_INFO” | awk -F= '/ID_FS_UUID=/{print $2; exit}')
DEVICE_ID=”${ID_SERIAL:-${ID_FS_UUID:-${DEVNAME}}}”
LABEL=”${ID_FS_LABEL:-unknown}”
TIMESTAMP=$(date +%F_%H%M%S)
TARGET_DIR=”$ARCHIVE_BASE/$DEVICE_ID/$TIMESTAMP”
mkdir -p “$TARGET_DIR”
# 挂载点临时目录
MOUNTPOINT=”$WORKDIR/$DEVNAME”
mkdir -p “$MOUNTPOINT”
# 尝试自动挂载(支持 vfat, exfat, ntfs, ext*)
mount “$DEVNODE” “$MOUNTPOINT” || {
# 若默认 mount 失败,尝试 ntfs-3g
mount -t auto “$DEVNODE” “$MOUNTPOINT” || {
echo “mount failed for $DEVNODE” >&2
exit 1
}
}
# 使用 rsync 复制,保留权限与时间;排除某些系统文件夹(可自定义)
rsync -a –exclude='System Volume Information' –exclude='$RECYCLE.BIN' “$MOUNTPOINT/” “$TARGET_DIR/”
# 统计文件数与大小(字节)
FILE_COUNT=$(find “$TARGET_DIR” -type f | wc -l)
BYTE_COUNT=$(du -sb “$TARGET_DIR” | cut -f1)
# 记录到 sqlite(调用 Python 脚本)
python3 “$LOGSCRIPT” “$DEVICE_ID” “$LABEL” “$MOUNTPOINT” “$TARGET_DIR” “$FILE_COUNT” “$BYTE_COUNT” “auto-ingest”
# 卸载并清理
sync
umount “$MOUNTPOINT” || { echo “umount failed for $MOUNTPOINT” >&2; }
rmdir “$MOUNTPOINT” || true
# 可选:给管理员发通知(例如写到 /var/log)
logger “USB ingest: $DEVICE_ID $LABEL -> $TARGET_DIR files=$FILE_COUNT bytes=$BYTE_COUNT”
exit 0
赋可执行权限并确认可运行:
chmod +x /opt/usb_ingest/ingest_usb.sh
3)udev 规则(检测块设备插入并触发脚本)
在
/etc/udev/rules.d/99-usb-ingest.rules 写入:
# /etc/udev/rules.d/99-usb-ingest.rules
# 当 USB 可移动块设备(非分区)插入时触发(使用 KERNEL==”sd?1″ 可以改为更宽松)
ACTION==”add”, KERNEL==”sd[b-z][0-9]”, SUBSYSTEM==”block”, ENV{ID_BUS}==”usb”, RUN+=”/opt/usb_ingest/udev_wrapper.sh %k”
说明:规则匹配 USB 块设备的分区(如 sdb1, sdc1)。调整 KERNEL 匹配以符合你系统。
由于 udev 直接调用脚本时环境有限,提议用一个轻 wrapper 去后台执行脚本。创建
/opt/usb_ingest/udev_wrapper.sh:
#!/bin/bash
# /opt/usb_ingest/udev_wrapper.sh
DEVNAME=”$1″
DEVNODE=”/dev/$DEVNAME”
# 把实际工作放到后台避免 udev 超时
/usr/bin/nohup /opt/usb_ingest/ingest_usb.sh “$DEVNODE” >/opt/usb_ingest/ingest_$DEVNAME.log 2>&1 &
exit 0
赋权限并重载 udev:
chmod +x /opt/usb_ingest/udev_wrapper.sh
sudo udevadm control –reload
sudo udevadm trigger
4)测试流程
- 插入 U 盘(含数据)。
- 检查日志文件 /opt/usb_ingest/ingest_sdb1.log 或 system log (sudo journalctl -f)。
- 成功时,备份目录示例:/srv/usb_archive/<device_id>/<YYYY-MM-DD_HHMMSS>/,并在 SQLite 中有一条记录:
sqlite3 /opt/usb_ingest/usb_archive.db “SELECT id,device_id,label,path,files,bytes,datetime(ts,'unixepoch','localtime') FROM archives ORDER BY ts DESC LIMIT 5;”
六、安全与注意事项
- 唯一标识:有些 U 盘没有 ID_SERIAL_SHORT,脚本会退回到 UUID 或设备名。若需要更强的唯一性,可结合 lsblk -o NAME,SERIAL 或读取 filesystem signature。
- 权限与安全:udev 脚本以 root 环境触发。ingest_usb.sh 可能会以 root 权限拷贝文件 —— 若你希望以普通用户身份保存,请在脚本中 su -c 或用 runuser 切换用户。
- 恶意 U 盘:自动挂载有风险(自动执行、损坏系统等)。若环境不可信,提议改为人工确认或在隔离环境中运行。
- 文件冲突/去重:当前方案按时间目录保存所有文件,若想去重可在拷贝后对文件计算 SHA256 并只保存新文件(可在 rsync 前后增加 dedupe 步骤)。
- 日志体积:长期大量 U 盘入库会占用大量磁盘,提议配合定期清理策略与备份策略。
七、故障排查(常见)
- udev 未触发:sudo udevadm monitor –environment –udev 插入设备看事件是否产生;检查规则文件名是否正确且重载了 udev。
- 脚本报错:查看 /opt/usb_ingest/ingest_*.log 与 sudo journalctl -u systemd-udevd -g udev。
- 挂载失败:手动 sudo mount /dev/sdb1 /mnt 检查文件系统类型或缺少 ntfs-3g 等驱动。
八、扩展想法(非必须)
- 在入库同时计算并存储每个文件的 SHA256 到数据库以便去重与索引。
- 把 SQLite 的记录同步到中央服务器(rsync / scp)或生成 nightly 报告邮件。
- 对拷贝的文件按类型分类(图片/文档/视频)并生成缩略图供快速预览(需额外依赖)。
九、总结
本文提供一个轻量、可复制的 U 盘自动入库方案:通过 udev 自动触发、挂载并用 rsync 入库,同时用 SQLite 记录元信息。脚本短小明了,几条命令即可在 Raspberry Pi 或任意 Linux 主机上实现“插即存”的入库流程。