| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838 |
- #!/bin/bash
- set -euo pipefail
- # copyfast — 快速拷贝最新文件到绑定的U盘
- #
- # 模式:
- # (无参数) 正常模式: 找到绑定的U盘, 拷贝最新匹配文件并弹出
- # --count N 指定拷贝最新的 N 个文件 (默认1)
- # --no-unmount 拷贝后不弹出U盘
- # --bind 绑定当前插入的U盘 (自动选最大分区, 可绑定多个)
- # --unbind 解除已绑定的U盘
- # --install-udev 安装 udev 规则, 插入U盘时自动运行
- # --uninstall-udev 移除 udev 规则
- # --config 打印当前配置
- # --help 帮助信息
- # 真实用户检测: sudo → $SUDO_USER, 否则 → $USER, --user 可覆盖
- # 真实 HOME 根据最终 REAL_USER 推导
- _resolve_home() {
- getent passwd "$1" 2>/dev/null | cut -d: -f6
- }
- if [[ -n "${SUDO_USER:-}" ]]; then
- REAL_USER="$SUDO_USER"
- REAL_HOME="$(_resolve_home "$SUDO_USER")"
- else
- REAL_USER="$USER"
- REAL_HOME="$HOME"
- fi
- CONFIG_DIR="${XDG_CONFIG_HOME:-$REAL_HOME/.config}/copyfast"
- CONFIG_FILE="$CONFIG_DIR/config"
- SCRIPT_PATH="$(realpath "$0")"
- # ============================================================
- # 默认配置
- # ============================================================
- SOURCE_DIR="$REAL_HOME/Downloads"
- EXTENSIONS="" # 逗号分隔, 空=所有文件
- USB_UUIDS="" # 绑定的U盘UUID列表 (空格分隔)
- AUTO_UNMOUNT="true" # 拷贝后自动弹出 (默认开启)
- FILE_COUNT="1" # 拷贝最新文件的数量
- # ============================================================
- # 加载配置
- # ============================================================
- load_config() {
- if [[ -f "$CONFIG_FILE" ]]; then
- source "$CONFIG_FILE"
- fi
- # 迁移旧格式: USB_UUID → USB_UUIDS
- if [[ -z "$USB_UUIDS" ]] && [[ -n "${USB_UUID:-}" ]]; then
- USB_UUIDS="$USB_UUID"
- save_config
- fi
- }
- # ============================================================
- # 保存配置
- # ============================================================
- save_config() {
- mkdir -p "$CONFIG_DIR"
- cat > "$CONFIG_FILE" <<EOF
- # copyfast 配置文件
- # 由 copyfast --bind / --unbind 写入, 也可手动编辑
- SOURCE_DIR="$SOURCE_DIR"
- EXTENSIONS="$EXTENSIONS"
- USB_UUIDS="$USB_UUIDS"
- AUTO_UNMOUNT="$AUTO_UNMOUNT"
- FILE_COUNT="$FILE_COUNT"
- EOF
- }
- # ============================================================
- # 将USB_UUIDS字符串转为数组
- # ============================================================
- get_bound_uuids() {
- local -a result=()
- if [[ -n "$USB_UUIDS" ]]; then
- for uuid in $USB_UUIDS; do
- [[ -n "$uuid" ]] && result+=("$uuid")
- done
- fi
- echo "${result[@]:-}"
- }
- # ============================================================
- # 列出当前插入的USB设备 — 每磁盘只返回最大分区
- # 输出格式: DISK_NAME|UUID|LABEL|MOUNTPOINT|SIZE
- # ============================================================
- list_usb_disks() {
- # 获取USB磁盘名称
- local usb_disks
- usb_disks=$(lsblk -o NAME,TRAN -d -l -n 2>/dev/null | awk '$2=="usb"{print $1}')
- [[ -z "$usb_disks" ]] && return
- for disk in $usb_disks; do
- # 获取此磁盘下的分区
- local parts best_uuid best_label best_mp best_size_bytes best_size_str
- local part_size_bytes part_size_str part_uuid part_label part_mp
- best_size_bytes=0
- local parts_list
- parts_list=$(lsblk -o NAME -l -n "/dev/$disk" 2>/dev/null | grep -v "^$disk\$")
- while IFS= read -r part; do
- local dev="/dev/$part"
- part_uuid=$(lsblk -o UUID -n "$dev" | tr -d '[:space:]')
- [[ -z "$part_uuid" ]] && continue
- part_size_str=$(lsblk -o SIZE -n "$dev" | tr -d '[:space:]')
- part_label=$(lsblk -o LABEL -n "$dev" | tr -d '[:space:]')
- part_mp=$(lsblk -o MOUNTPOINT -n "$dev" | tr -d '[:space:]')
- # 解析 SIZE
- local unit val
- unit="${part_size_str: -1}"
- val="${part_size_str%?}"
- case "$unit" in
- T) part_size_bytes=$((val * 1099511627776)) ;;
- G) part_size_bytes=$((val * 1073741824)) ;;
- M) part_size_bytes=$((val * 1048576)) ;;
- K) part_size_bytes=$((val * 1024)) ;;
- *) part_size_bytes=$val ;;
- esac
- if [[ "$part_size_bytes" -gt "$best_size_bytes" ]]; then
- best_size_bytes=$part_size_bytes
- best_uuid=$part_uuid
- best_label=$part_label
- best_mp=$part_mp
- best_size_str=$part_size_str
- fi
- done <<< "$parts_list"
- if [[ -n "${best_uuid:-}" ]]; then
- echo "$disk|$best_uuid|${best_label:-}|${best_mp:-}|${best_size_str:-}"
- fi
- done
- }
- # ============================================================
- # 通过UUID查找设备 (不管是否挂载)
- # 返回 "MOUNTPOINT|KNAME" 或 "|KNAME" (未挂载时挂载点为空)
- # 通过 /dev/disk/by-uuid/ 和 lsblk 单列输出, 避免空列解析问题
- # ============================================================
- find_device_by_uuid() {
- local uuid="$1"
- local dev_path
- # 优先使用 udev 直接传递的设备路径 (避免 /dev/disk/by-uuid 未就绪)
- if [[ -n "${UDEV_DEVICE:-}" ]] && [[ -e "$UDEV_DEVICE" ]]; then
- dev_path="$UDEV_DEVICE"
- else
- dev_path="/dev/disk/by-uuid/$uuid"
- [[ -e "$dev_path" ]] || { return 1; }
- fi
- local mp k
- mp=$(lsblk -o MOUNTPOINT -n "$dev_path" | head -1 | tr -d '[:space:]')
- k=$(basename "$(readlink -f "$dev_path")")
- echo "$mp|$k"
- }
- # ============================================================
- # 挂载设备, 返回挂载点
- # 多级保底: root直接用mount, 普通用户用udisksctl
- # ============================================================
- mount_device() {
- local uuid="$1"
- local dev_path
- if [[ -n "${UDEV_DEVICE:-}" ]] && [[ -e "$UDEV_DEVICE" ]]; then
- dev_path="$UDEV_DEVICE"
- else
- dev_path="/dev/disk/by-uuid/$uuid"
- [[ -e "$dev_path" ]] || return 1
- fi
- # 已挂载则直接返回挂载点
- local mp
- mp=$(lsblk -o MOUNTPOINT -n "$dev_path" 2>/dev/null | head -1 | tr -d '[:space:]')
- if [[ -n "$mp" ]]; then
- echo "$mp"
- return 0
- fi
- # 尝试1: udisksctl (需要D-Bus会话, 适用于桌面环境)
- if udisksctl mount -b "$dev_path" 2>/dev/null; then
- return 0
- fi
- # 尝试2: root 直接 mount (适用于udev触发场景)
- if [[ "$(id -u)" == "0" ]]; then
- local mnt="/run/media/copyfast/$uuid"
- mkdir -p "$mnt"
- if mount "$dev_path" "$mnt" 2>/dev/null; then
- echo "$mnt"
- return 0
- fi
- fi
- # 尝试3: sudo mount
- local mnt="/run/media/copyfast/$uuid"
- mkdir -p "$mnt" 2>/dev/null || true
- if sudo mount "$dev_path" "$mnt" 2>/dev/null; then
- echo "$mnt"
- return 0
- fi
- return 1
- }
- # ============================================================
- # 获取挂载点 (设备可能已挂载或需挂载)
- # ============================================================
- get_mountpoint() {
- local uuid="$1"
- local dev_info
- dev_info=$(find_device_by_uuid "$uuid")
- if [[ -z "$dev_info" ]]; then
- return 1
- fi
- local mp="${dev_info%%|*}"
- if [[ -z "$mp" ]]; then
- if [[ "${UDEV_MODE:-false}" == "true" ]]; then
- # udev模式: 不挂载, 等待桌面环境自动挂载 (最长10秒)
- local i
- for i in {1..10}; do
- sleep 1
- dev_info=$(find_device_by_uuid "$uuid")
- mp="${dev_info%%|*}"
- [[ -n "$mp" ]] && break
- done
- else
- # 正常模式: 主动挂载
- mount_device "$uuid" > /dev/null 2>&1 || return 1
- sleep 1
- dev_info=$(find_device_by_uuid "$uuid")
- mp="${dev_info%%|*}"
- fi
- fi
- if [[ -n "$mp" ]]; then
- echo "$mp"
- return 0
- fi
- return 1
- }
- # ============================================================
- # 获取源目录中最新的 N 个文件 (每行一个路径)
- # $1: 源目录, $2: 逗号分隔的扩展名 (空=所有文件), $3: 数量 (默认1)
- # ============================================================
- get_latest_files() {
- local src="$1"
- local exts="$2"
- local count="${3:-1}"
- if [[ ! -d "$src" ]]; then
- echo ""
- return 1
- fi
- if [[ -z "$exts" ]]; then
- find "$src" -maxdepth 1 -type f -printf '%T@ %p\n' 2>/dev/null \
- | sort -rn | head -n "$count" | cut -d' ' -f2-
- return
- fi
- local IFS=','
- local args=()
- local ext_count=0
- for ext in $exts; do
- ext=$(echo "$ext" | tr -d '[:space:]')
- [[ -z "$ext" ]] && continue
- if [[ $ext_count -gt 0 ]]; then
- args+=(-o)
- fi
- args+=(-iname "*.$ext")
- ext_count=$((ext_count+1))
- done
- if [[ $ext_count -eq 0 ]]; then
- echo ""
- return 1
- fi
- if [[ $ext_count -gt 1 ]]; then
- find "$src" -maxdepth 1 -type f \( "${args[@]}" \) -printf '%T@ %p\n' 2>/dev/null \
- | sort -rn | head -n "$count" | cut -d' ' -f2-
- else
- find "$src" -maxdepth 1 -type f "${args[@]}" -printf '%T@ %p\n' 2>/dev/null \
- | sort -rn | head -n "$count" | cut -d' ' -f2-
- fi
- }
- # ============================================================
- # 日志 (udev触发时写入logger, 否则写入stderr)
- # ============================================================
- log() {
- if [[ "${UDEV_MODE:-false}" == "true" ]]; then
- logger -t "copyfast" "$@"
- else
- echo "$@" >&2
- fi
- }
- # ============================================================
- # 桌面通知 (通过 real user 的 D-Bus)
- # ============================================================
- _notify() {
- local uid
- uid=$(id -u "$REAL_USER" 2>/dev/null) || { log "通知: 无法获取用户UID"; return 0; }
- local bus="/run/user/$uid/bus"
- if [[ ! -S "$bus" ]]; then
- log "通知: D-Bus 会话总线不可用 ($bus)"
- return 0
- fi
- export DBUS_SESSION_BUS_ADDRESS="unix:path=$bus"
- # 尝试多种方式获取 DISPLAY
- if [[ -z "${DISPLAY:-}" ]]; then
- # 从 loginctl 获取用户会话的 display
- local display
- display=$(loginctl show-user "$REAL_USER" -p Display 2>/dev/null | cut -d= -f2)
- # 兜底: 常见的 display 值
- for d in "${display:-}" ":0" ":1"; do
- [[ -z "$d" ]] && continue
- export DISPLAY="$d"
- break
- done
- fi
- if notify-send -i drive-removable-media "copyfast" "$*" -t 5000 2>/dev/null; then
- log "通知: 桌面通知已发送"
- else
- log "通知: notify-send 执行失败"
- fi
- return 0
- }
- # ============================================================
- # 正常模式: 查找USB并拷贝最新文件
- # ============================================================
- do_copy() {
- if [[ -z "$USB_UUIDS" ]]; then
- log "错误: 未绑定U盘。请先插入U盘并运行: $(basename "$0") --bind"
- exit 1
- fi
- if [[ ! "$FILE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
- log "错误: 文件数量不是有效的正整数: $FILE_COUNT"
- exit 1
- fi
- # 遍历已绑定UUID, 找到第一个可用的
- log "正在查找已绑定的U盘..."
- local uuids uuid mountpoint was_mounted
- uuids=($(get_bound_uuids))
- for uuid in "${uuids[@]}"; do
- # 检查设备是否存在
- local dev_info
- dev_info=$(find_device_by_uuid "$uuid")
- [[ -z "$dev_info" ]] && continue
- local current_mp="${dev_info%%|*}"
- was_mounted="$current_mp"
- mountpoint=$(get_mountpoint "$uuid" 2>/dev/null) || {
- log "警告: 无法挂载 UUID=$uuid, 尝试下一个..."
- continue
- }
- break
- done
- if [[ -z "${mountpoint:-}" ]]; then
- log "错误: 未找到任何已绑定的U盘, 请确认U盘已插入"
- exit 1
- fi
- log "找到已绑定U盘: UUID=$uuid, 挂载点=$mountpoint"
- if [[ ! -d "$SOURCE_DIR" ]]; then
- log "错误: 源目录不存在: $SOURCE_DIR"
- exit 1
- fi
- log "正在 $SOURCE_DIR 中查找最新 $FILE_COUNT 个文件..."
- local files
- files=$(get_latest_files "$SOURCE_DIR" "$EXTENSIONS" "$FILE_COUNT")
- if [[ -z "$files" ]]; then
- if [[ -n "$EXTENSIONS" ]]; then
- log "错误: 未在 $SOURCE_DIR 中找到匹配扩展名 ($EXTENSIONS) 的文件"
- else
- log "错误: $SOURCE_DIR 中没有文件"
- fi
- exit 1
- fi
- local copied=0
- while IFS= read -r f; do
- [[ -z "$f" ]] && continue
- log "拷贝 ($((copied+1))/$FILE_COUNT): $(basename "$f") -> $mountpoint"
- cp "$f" "$mountpoint/"
- copied=$((copied+1))
- done <<< "$files"
- log "拷贝完成: 共 $copied 个文件 -> $mountpoint"
- # udev模式下发送桌面通知
- if [[ "${UDEV_MODE:-false}" == "true" ]]; then
- _notify "拷贝完成: 共 $copied 个文件"
- fi
- if [[ "$AUTO_UNMOUNT" == "true" ]]; then
- log "正在弹出U盘..."
- # udev模式下设置 D-Bus 环境以便 udisksctl 工作
- if [[ "${UDEV_MODE:-false}" == "true" ]]; then
- local uid
- uid=$(id -u "$REAL_USER" 2>/dev/null)
- if [[ -n "${uid:-}" ]] && [[ -S "/run/user/$uid/bus" ]]; then
- export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus"
- fi
- fi
- local dev_to_unmount="${UDEV_DEVICE:-/dev/disk/by-uuid/$uuid}"
- if udisksctl unmount -b "$dev_to_unmount" 2>/dev/null; then
- log "已弹出U盘 (unmount)"
- elif udisksctl power-off -b "$dev_to_unmount" 2>/dev/null; then
- log "已弹出U盘 (power-off)"
- elif umount "$mountpoint" 2>/dev/null; then
- log "已弹出U盘 (umount)"
- elif sudo umount "$mountpoint" 2>/dev/null; then
- log "已弹出U盘 (sudo umount)"
- else
- log "警告: 弹出 $mountpoint 失败 (可能有其他程序正在使用)"
- fi
- fi
- }
- # ============================================================
- # 绑定模式: 自动选择每个U盘的最大分区, 加入绑定列表
- # ============================================================
- do_bind() {
- local devices
- devices=$(list_usb_disks)
- if [[ -z "$devices" ]]; then
- echo "错误: 未检测到插入的USB设备, 请先插入U盘"
- exit 1
- fi
- local disk uuid label mp size
- # 收集当前已绑定的UUID
- local -A bound_map
- if [[ -n "$USB_UUIDS" ]]; then
- for bid in $USB_UUIDS; do
- bound_map["$bid"]=1
- done
- fi
- # 过滤掉已绑定的, 收集可绑定的
- local candidates=()
- while IFS='|' read -r disk uuid label mp size; do
- if [[ -n "${bound_map[$uuid]:-}" ]]; then
- echo "跳过已绑定: $uuid (标签=${label:-(无)}, 挂载点=${mp:-(未挂载)})"
- continue
- fi
- candidates+=("$disk|$uuid|$label|$mp|$size")
- done <<< "$devices"
- if [[ ${#candidates[@]} -eq 0 ]]; then
- echo "所有已插入的U盘都已绑定。"
- echo "当前绑定: $USB_UUIDS"
- echo "如需解绑, 请运行: $(basename "$0") --unbind"
- exit 0
- fi
- local chosen_uuid chosen_label chosen_disk
- local count=${#candidates[@]}
- if [[ "$count" -eq 1 ]]; then
- IFS='|' read -r chosen_disk chosen_uuid chosen_label _ _ <<< "${candidates[0]}"
- echo "检测到1个新U盘, 自动选择最大分区:"
- echo " 磁盘: $chosen_disk"
- echo " 标签: ${chosen_label:-(无)}"
- echo " UUID: $chosen_uuid"
- echo ""
- else
- echo "检测到 $count 个新U盘, 请选择要绑定的设备:"
- echo ""
- local i=1
- local lines=()
- for cand in "${candidates[@]}"; do
- IFS='|' read -r disk uuid label mp size <<< "$cand"
- echo " [$i] 磁盘=$disk 标签=${label:-(无)} UUID=$uuid 挂载点=${mp:-(未挂载)}"
- lines+=("$cand")
- ((i++))
- done
- echo ""
- read -rp "请选择要绑定的编号 [1-$count]: " choice
- if [[ ! "$choice" =~ ^[0-9]+$ ]] || [[ "$choice" -lt 1 ]] || [[ "$choice" -gt "$count" ]]; then
- echo "错误: 无效选择"
- exit 1
- fi
- IFS='|' read -r chosen_disk chosen_uuid chosen_label _ _ <<< "${lines[$((choice-1))]}"
- fi
- # 添加到绑定列表
- if [[ -z "$USB_UUIDS" ]]; then
- USB_UUIDS="$chosen_uuid"
- else
- USB_UUIDS="$USB_UUIDS $chosen_uuid"
- fi
- save_config
- echo "已绑定U盘:"
- echo " 磁盘: $chosen_disk"
- echo " 标签: ${chosen_label:-(无)}"
- echo " UUID: $chosen_uuid"
- echo ""
- echo "当前所有绑定: $USB_UUIDS"
- echo "配置已保存到 $CONFIG_FILE"
- echo ""
- echo "后续可直接运行 $(basename "$0") 或安装 udev 规则以实现自动运行:"
- echo " $(basename "$0") --install-udev"
- }
- # ============================================================
- # 解绑模式: 交互选择已绑定的U盘并移除
- # ============================================================
- do_unbind() {
- if [[ -z "$USB_UUIDS" ]]; then
- echo "当前没有绑定的U盘。"
- exit 0
- fi
- local uuids
- uuids=($USB_UUIDS)
- echo "当前绑定列表:"
- local i=1
- for uuid in "${uuids[@]}"; do
- # 查询标签信息
- local info
- info=$(lsblk -o UUID,LABEL,MOUNTPOINT -l -n 2>/dev/null | awk -v u="$uuid" '$1==u {print "标签=" $2 " 挂载点=" $3}')
- echo " [$i] UUID=$uuid ${info:-未插入}"
- ((i++))
- done
- echo ""
- if [[ ${#uuids[@]} -eq 1 ]]; then
- read -rp "确认解除此绑定? [y/N]: " confirm
- if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
- echo "已取消"
- exit 0
- fi
- USB_UUIDS=""
- else
- read -rp "请选择要解除的编号 [1-${#uuids[@]}] (留空取消): " choice
- if [[ -z "$choice" ]]; then
- echo "已取消"
- exit 0
- fi
- if [[ ! "$choice" =~ ^[1-9][0-9]*$ ]] || [[ "$choice" -gt "${#uuids[@]}" ]]; then
- echo "错误: 无效选择"
- exit 1
- fi
- unset 'uuids[$((choice-1))]'
- USB_UUIDS="${uuids[*]}"
- fi
- save_config
- echo "已解除绑定。"
- if [[ -n "$USB_UUIDS" ]]; then
- echo "当前绑定: $USB_UUIDS"
- else
- echo "当前没有任何绑定。"
- fi
- }
- # ============================================================
- # 安装 udev 规则 — 为每个已绑定UUID生成一条规则
- # ============================================================
- do_install_udev() {
- if [[ -z "$USB_UUIDS" ]]; then
- echo "错误: 尚未绑定U盘, 请先运行: $(basename "$0") --bind"
- exit 1
- fi
- local udev_rule="/etc/udev/rules.d/99-copyfast.rules"
- if [[ -f "$udev_rule" ]]; then
- echo "udev 规则已存在于 $udev_rule"
- read -rp "是否覆盖? [y/N]: " confirm
- if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
- echo "已取消"
- exit 0
- fi
- sudo rm "$udev_rule"
- fi
- echo "正在生成 udev 规则..."
- local uuids
- uuids=($USB_UUIDS)
- {
- echo "# copyfast udev rules — 插入已绑定U盘时自动运行"
- echo "# 安装时间: $(date)"
- for uuid in "${uuids[@]}"; do
- echo "ACTION==\"add\", SUBSYSTEM==\"block\", ENV{ID_FS_UUID}==\"$uuid\", RUN+=\"/usr/bin/systemd-run --uid=$REAL_USER --gid=$(id -g "$REAL_USER") --collect -- $SCRIPT_PATH --udev-triggered --device /dev/\$kernel\""
- done
- } | sudo tee "$udev_rule" > /dev/null
- echo "正在重新加载 udev 规则..."
- sudo udevadm control --reload-rules
- sudo udevadm trigger
- echo ""
- echo "udev 规则安装完成: $udev_rule"
- echo "已为 ${#uuids[@]} 个U盘生成规则。"
- echo ""
- echo "规则内容:"
- cat "$udev_rule"
- echo ""
- echo "现在插入任意已绑定的U盘, 脚本将自动运行。"
- }
- # ============================================================
- # 卸载 udev 规则
- # ============================================================
- do_uninstall_udev() {
- local udev_rule="/etc/udev/rules.d/99-copyfast.rules"
- if [[ ! -f "$udev_rule" ]]; then
- echo "udev 规则不存在: $udev_rule"
- exit 0
- fi
- echo "正在移除 udev 规则..."
- sudo rm "$udev_rule"
- echo "正在重新加载 udev 规则..."
- sudo udevadm control --reload-rules
- echo "已移除 udev 规则。"
- }
- # ============================================================
- # 打印配置
- # ============================================================
- do_config() {
- echo "当前配置:"
- echo " 配置文件: $CONFIG_FILE"
- echo " 源目录: $SOURCE_DIR"
- echo " 文件后缀: ${EXTENSIONS:-(所有文件)}"
- echo " 拷贝数量: 最新的 $FILE_COUNT 个文件"
- echo " 拷贝后弹出: $AUTO_UNMOUNT"
- echo ""
- if [[ -z "$USB_UUIDS" ]]; then
- echo " 已绑定U盘: (无)"
- else
- echo " 已绑定U盘 ($(echo "$USB_UUIDS" | wc -w) 个):"
- local uuids
- uuids=($USB_UUIDS)
- for uuid in "${uuids[@]}"; do
- local info
- info=$(lsblk -o UUID,LABEL,MOUNTPOINT -l -n 2>/dev/null \
- | awk -v u="$uuid" '$1==u {printf "标签=%s, 挂载点=%s", ($2?$2:"无"), ($3?$3:"未挂载")}')
- echo " UUID=$uuid ${info:-未插入}"
- done
- fi
- }
- # ============================================================
- # 帮助
- # ============================================================
- do_help() {
- cat <<HELP
- copyfast — 从指定目录快速拷贝最新文件到绑定的U盘
- 用法: $(basename "$0") [选项]
- 正常使用:
- 直接运行即可将 SOURCE_DIR 中最新的 FILE_COUNT 个匹配文件
- 拷贝到已绑定的U盘 (第一个检测到的), 拷贝完成后自动弹出U盘。
- 选项:
- -n, --count N 拷贝最新的 N 个文件 (默认: 1)
- 例: $(basename "$0") --count 3
- -N, --no-unmount 拷贝后不弹出U盘
- 例: $(basename "$0") --no-unmount
- 绑定与管理:
- -b, --bind 绑定当前插入的U盘到配置文件
- 每个U盘只选最大分区, 可重复执行绑定多个U盘
- 例: $(basename "$0") --bind
- -u, --unbind 解除已绑定的U盘, 交互选择要移除的绑定
- 例: $(basename "$0") --unbind
- -i, --install-udev 安装 udev 规则 (需 sudo 权限)
- 为所有已绑定U盘生成规则, 插入任意一个即自动运行
- -U, --uninstall-udev 移除 udev 规则 (需 sudo 权限)
- -c, --config 打印当前所有配置项及其值
- -h, --help 显示此帮助信息
- 配置文件: $CONFIG_FILE
- 配置项说明:
- SOURCE_DIR 要拷贝哪个目录下的最新文件
- 默认: ~/Downloads
- EXTENSIONS 只拷贝这些后缀的文件, 逗号分隔, 大小写不敏感
- 留空表示所有文件
- 例: "ppt,pptx,pdf" 或 "zip,tar.gz,7z"
- FILE_COUNT 每次拷贝最新的几个文件, 必须是正整数 (默认1)
- USB_UUIDS 绑定的U盘UUID列表 (空格分隔), 由 --bind/--unbind 维护
- 请不要手动修改此项
- AUTO_UNMOUNT 拷贝后是否自动弹出U盘
- true=弹出 (默认), false=不弹出
- 命令行 --no-unmount 可临时覆盖为不弹出
- 使用示例:
- # 首次配置
- $(basename "$0") --bind # 插入U盘后绑定 (自动选最大分区)
- $(basename "$0") --count 3 # 拷贝最新3个文件到U盘
- $(basename "$0") --no-unmount # 拷贝但不弹出
- # 管理多个U盘
- $(basename "$0") --bind # 插入另一个U盘, 再次绑定
- $(basename "$0") --config # 查看所有绑定
- $(basename "$0") --unbind # 解除某个绑定
- # 修改默认行为: 编辑配置文件
- # 将 EXTENSIONS 设为 "ppt,pptx" 则之后只拷贝这两种文件
- # 将 FILE_COUNT 设为 "5" 则之后每次拷贝5个文件
- # 安装自动运行后, 插入任意已绑定U盘自动拷贝并弹出
- $(basename "$0") --install-udev
- 技术说明:
- - USB设备通过文件系统UUID识别, 不受挂载点路径变化影响
- - 每个U盘只选最大分区, 避免绑定到启动分区等小分区
- - 支持绑定多个不同U盘, 运行时使用最先检测到的那个
- - udev模式下轮询等待挂载 (最长10秒), 日志写入 syslog
- - 文件匹配使用 find -iname, 扩展名匹配不区分大小写
- HELP
- }
- # ============================================================
- # 入口与参数解析
- # ============================================================
- # 先加载配置文件, 后续命令行参数可覆盖配置值
- load_config
- UDEV_MODE=false
- UDEV_DEVICE=""
- # 使用 while+shift 解析参数, 支持 --count N 这种带值参数
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --udev-triggered)
- UDEV_MODE=true
- shift
- ;;
- --user)
- if [[ -z "${2:-}" ]] || [[ "${2:0:1}" == "-" ]]; then
- echo "错误: --user 需要指定用户名"
- exit 1
- fi
- REAL_USER="$2"
- REAL_HOME="$(_resolve_home "$2")"
- shift 2
- ;;
- --device)
- if [[ -z "${2:-}" ]] || [[ "${2:0:1}" == "-" ]]; then
- echo "错误: --device 需要指定设备路径"
- exit 1
- fi
- UDEV_DEVICE="$2"
- shift 2
- ;;
- --count|-n)
- if [[ -z "${2:-}" ]] || [[ "${2:0:1}" == "-" ]]; then
- echo "错误: --count 需要指定一个正整数"
- exit 1
- fi
- FILE_COUNT="$2"
- shift 2
- ;;
- --no-unmount|-N)
- AUTO_UNMOUNT="false"
- shift
- ;;
- --bind|-b)
- do_bind
- exit 0
- ;;
- --unbind|-u)
- do_unbind
- exit 0
- ;;
- --install-udev|-i)
- do_install_udev
- exit 0
- ;;
- --uninstall-udev|-U)
- do_uninstall_udev
- exit 0
- ;;
- --config|-c)
- do_config
- exit 0
- ;;
- --help|-h)
- do_help
- exit 0
- ;;
- *)
- echo "错误: 未知选项 '$1'"
- echo "运行 $(basename "$0") --help 查看用法"
- exit 1
- ;;
- esac
- done
- # 进入正常拷贝模式
- # 注意: udev 模式下由 systemd-run 管理生命周期, 无需后台化
- do_copy
|