nonvioce 5 maanden geleden
bovenliggende
commit
3735c10ca5
2 gewijzigde bestanden met toevoegingen van 836 en 18 verwijderingen
  1. 1 1
      README.md
  2. 835 17
      copyFast.sh

+ 1 - 1
README.md

@@ -45,7 +45,7 @@
 | `decrypt.sh` | AES-256-CBC 解密文件。用法:`./decrypt.sh encrypted.txt output.txt password` |
 | `decrypt.sh` | AES-256-CBC 解密文件。用法:`./decrypt.sh encrypted.txt output.txt password` |
 | `orgnazieFolder.sh` | 按文件后缀名归类整理文件夹(默认整理 `~/Downloads`) |
 | `orgnazieFolder.sh` | 按文件后缀名归类整理文件夹(默认整理 `~/Downloads`) |
 | `removeEmptyFolders.sh` | 递归删除目标目录下的所有空文件夹 |
 | `removeEmptyFolders.sh` | 递归删除目标目录下的所有空文件夹 |
-| `copyFast.sh` | 将 `~/Downloads` 中最新的文件复制到 U 盘并自动弹出 |
+| `copyFast.sh` | **快速拷贝最新文件到 U 盘**。支持绑定多个 U 盘(通过 UUID)、按扩展名过滤、拷贝最新 N 个文件、自动弹出/保持挂载、插入 U 盘自动运行(udev 规则)、桌面通知等。配置文件位于 `~/.config/copyfast/config`,运行 `./copyFast.sh --help` 查看完整用法 |
 
 
 ### Python
 ### Python
 
 

+ 835 - 17
copyFast.sh

@@ -1,20 +1,838 @@
 #!/bin/bash
 #!/bin/bash
-#此脚本用于将downloads目录下最新的ppt拷贝到u盘,并弹出u盘
-
-# 获取U盘路径
-usb_path="/run/media/nonvioce/U盘"
-
-# 检查是否有U盘插入
-if [ -d "$usb_path" ]; then
-  # 获取Downloads文件夹中的所有文件
-  files=$(ls /home/nonvioce/Downloads)
-
-  # 找到最新的文件
-  latest_file=$(ls -t /home/nonvioce/Downloads | head -n1)
-  echo $latest_file
-  # 将最新的文件复制到U盘
-  cp /home/nonvioce/Downloads/$latest_file "$usb_path"
-  umount /run/media/nonvioce/U盘
+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
 else
-  echo "U盘未插入"
+    REAL_USER="$USER"
+    REAL_HOME="$HOME"
 fi
 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