#!/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" </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 (用 awk 处理浮点数, 如 "57.3G") part_size_bytes=$(awk -v s="$part_size_str" ' BEGIN { u = substr(s, length(s)) v = substr(s, 1, length(s)-1) + 0 if (u == "T") printf "%d", v * 1099511627776 else if (u == "G") printf "%d", v * 1073741824 else if (u == "M") printf "%d", v * 1048576 else if (u == "K") printf "%d", v * 1024 else printf "%d", v }') 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 <