copyFast.sh 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. #!/bin/bash
  2. set -euo pipefail
  3. # copyfast — 快速拷贝最新文件到绑定的U盘
  4. #
  5. # 模式:
  6. # (无参数) 正常模式: 找到绑定的U盘, 拷贝最新匹配文件并弹出
  7. # --count N 指定拷贝最新的 N 个文件 (默认1)
  8. # --no-unmount 拷贝后不弹出U盘
  9. # --bind 绑定当前插入的U盘 (自动选最大分区, 可绑定多个)
  10. # --unbind 解除已绑定的U盘
  11. # --install-udev 安装 udev 规则, 插入U盘时自动运行
  12. # --uninstall-udev 移除 udev 规则
  13. # --config 打印当前配置
  14. # --help 帮助信息
  15. # 真实用户检测: sudo → $SUDO_USER, 否则 → $USER, --user 可覆盖
  16. # 真实 HOME 根据最终 REAL_USER 推导
  17. _resolve_home() {
  18. getent passwd "$1" 2>/dev/null | cut -d: -f6
  19. }
  20. if [[ -n "${SUDO_USER:-}" ]]; then
  21. REAL_USER="$SUDO_USER"
  22. REAL_HOME="$(_resolve_home "$SUDO_USER")"
  23. else
  24. REAL_USER="$USER"
  25. REAL_HOME="$HOME"
  26. fi
  27. CONFIG_DIR="${XDG_CONFIG_HOME:-$REAL_HOME/.config}/copyfast"
  28. CONFIG_FILE="$CONFIG_DIR/config"
  29. SCRIPT_PATH="$(realpath "$0")"
  30. # ============================================================
  31. # 默认配置
  32. # ============================================================
  33. SOURCE_DIR="$REAL_HOME/Downloads"
  34. EXTENSIONS="" # 逗号分隔, 空=所有文件
  35. USB_UUIDS="" # 绑定的U盘UUID列表 (空格分隔)
  36. AUTO_UNMOUNT="true" # 拷贝后自动弹出 (默认开启)
  37. FILE_COUNT="1" # 拷贝最新文件的数量
  38. # ============================================================
  39. # 加载配置
  40. # ============================================================
  41. load_config() {
  42. if [[ -f "$CONFIG_FILE" ]]; then
  43. source "$CONFIG_FILE"
  44. fi
  45. # 迁移旧格式: USB_UUID → USB_UUIDS
  46. if [[ -z "$USB_UUIDS" ]] && [[ -n "${USB_UUID:-}" ]]; then
  47. USB_UUIDS="$USB_UUID"
  48. save_config
  49. fi
  50. }
  51. # ============================================================
  52. # 保存配置
  53. # ============================================================
  54. save_config() {
  55. mkdir -p "$CONFIG_DIR"
  56. cat > "$CONFIG_FILE" <<EOF
  57. # copyfast 配置文件
  58. # 由 copyfast --bind / --unbind 写入, 也可手动编辑
  59. SOURCE_DIR="$SOURCE_DIR"
  60. EXTENSIONS="$EXTENSIONS"
  61. USB_UUIDS="$USB_UUIDS"
  62. AUTO_UNMOUNT="$AUTO_UNMOUNT"
  63. FILE_COUNT="$FILE_COUNT"
  64. EOF
  65. }
  66. # ============================================================
  67. # 将USB_UUIDS字符串转为数组
  68. # ============================================================
  69. get_bound_uuids() {
  70. local -a result=()
  71. if [[ -n "$USB_UUIDS" ]]; then
  72. for uuid in $USB_UUIDS; do
  73. [[ -n "$uuid" ]] && result+=("$uuid")
  74. done
  75. fi
  76. echo "${result[@]:-}"
  77. }
  78. # ============================================================
  79. # 列出当前插入的USB设备 — 每磁盘只返回最大分区
  80. # 输出格式: DISK_NAME|UUID|LABEL|MOUNTPOINT|SIZE
  81. # ============================================================
  82. list_usb_disks() {
  83. # 获取USB磁盘名称
  84. local usb_disks
  85. usb_disks=$(lsblk -o NAME,TRAN -d -l -n 2>/dev/null | awk '$2=="usb"{print $1}')
  86. [[ -z "$usb_disks" ]] && return
  87. for disk in $usb_disks; do
  88. # 获取此磁盘下的分区
  89. local parts best_uuid best_label best_mp best_size_bytes best_size_str
  90. local part_size_bytes part_size_str part_uuid part_label part_mp
  91. best_size_bytes=0
  92. local parts_list
  93. parts_list=$(lsblk -o NAME -l -n "/dev/$disk" 2>/dev/null | grep -v "^$disk\$")
  94. while IFS= read -r part; do
  95. local dev="/dev/$part"
  96. part_uuid=$(lsblk -o UUID -n "$dev" | tr -d '[:space:]')
  97. [[ -z "$part_uuid" ]] && continue
  98. part_size_str=$(lsblk -o SIZE -n "$dev" | tr -d '[:space:]')
  99. part_label=$(lsblk -o LABEL -n "$dev" | tr -d '[:space:]')
  100. part_mp=$(lsblk -o MOUNTPOINT -n "$dev" | tr -d '[:space:]')
  101. # 解析 SIZE (用 awk 处理浮点数, 如 "57.3G")
  102. part_size_bytes=$(awk -v s="$part_size_str" '
  103. BEGIN {
  104. u = substr(s, length(s))
  105. v = substr(s, 1, length(s)-1) + 0
  106. if (u == "T") printf "%d", v * 1099511627776
  107. else if (u == "G") printf "%d", v * 1073741824
  108. else if (u == "M") printf "%d", v * 1048576
  109. else if (u == "K") printf "%d", v * 1024
  110. else printf "%d", v
  111. }')
  112. if [[ "$part_size_bytes" -gt "$best_size_bytes" ]]; then
  113. best_size_bytes=$part_size_bytes
  114. best_uuid=$part_uuid
  115. best_label=$part_label
  116. best_mp=$part_mp
  117. best_size_str=$part_size_str
  118. fi
  119. done <<< "$parts_list"
  120. if [[ -n "${best_uuid:-}" ]]; then
  121. echo "$disk|$best_uuid|${best_label:-}|${best_mp:-}|${best_size_str:-}"
  122. fi
  123. done
  124. }
  125. # ============================================================
  126. # 通过UUID查找设备 (不管是否挂载)
  127. # 返回 "MOUNTPOINT|KNAME" 或 "|KNAME" (未挂载时挂载点为空)
  128. # 通过 /dev/disk/by-uuid/ 和 lsblk 单列输出, 避免空列解析问题
  129. # ============================================================
  130. find_device_by_uuid() {
  131. local uuid="$1"
  132. local dev_path
  133. # 优先使用 udev 直接传递的设备路径 (避免 /dev/disk/by-uuid 未就绪)
  134. if [[ -n "${UDEV_DEVICE:-}" ]] && [[ -e "$UDEV_DEVICE" ]]; then
  135. dev_path="$UDEV_DEVICE"
  136. else
  137. dev_path="/dev/disk/by-uuid/$uuid"
  138. [[ -e "$dev_path" ]] || { return 1; }
  139. fi
  140. local mp k
  141. mp=$(lsblk -o MOUNTPOINT -n "$dev_path" | head -1 | tr -d '[:space:]')
  142. k=$(basename "$(readlink -f "$dev_path")")
  143. echo "$mp|$k"
  144. }
  145. # ============================================================
  146. # 挂载设备, 返回挂载点
  147. # 多级保底: root直接用mount, 普通用户用udisksctl
  148. # ============================================================
  149. mount_device() {
  150. local uuid="$1"
  151. local dev_path
  152. if [[ -n "${UDEV_DEVICE:-}" ]] && [[ -e "$UDEV_DEVICE" ]]; then
  153. dev_path="$UDEV_DEVICE"
  154. else
  155. dev_path="/dev/disk/by-uuid/$uuid"
  156. [[ -e "$dev_path" ]] || return 1
  157. fi
  158. # 已挂载则直接返回挂载点
  159. local mp
  160. mp=$(lsblk -o MOUNTPOINT -n "$dev_path" 2>/dev/null | head -1 | tr -d '[:space:]')
  161. if [[ -n "$mp" ]]; then
  162. echo "$mp"
  163. return 0
  164. fi
  165. # 尝试1: udisksctl (需要D-Bus会话, 适用于桌面环境)
  166. if udisksctl mount -b "$dev_path" 2>/dev/null; then
  167. return 0
  168. fi
  169. # 尝试2: root 直接 mount (适用于udev触发场景)
  170. if [[ "$(id -u)" == "0" ]]; then
  171. local mnt="/run/media/copyfast/$uuid"
  172. mkdir -p "$mnt"
  173. if mount "$dev_path" "$mnt" 2>/dev/null; then
  174. echo "$mnt"
  175. return 0
  176. fi
  177. fi
  178. # 尝试3: sudo mount
  179. local mnt="/run/media/copyfast/$uuid"
  180. mkdir -p "$mnt" 2>/dev/null || true
  181. if sudo mount "$dev_path" "$mnt" 2>/dev/null; then
  182. echo "$mnt"
  183. return 0
  184. fi
  185. return 1
  186. }
  187. # ============================================================
  188. # 获取挂载点 (设备可能已挂载或需挂载)
  189. # ============================================================
  190. get_mountpoint() {
  191. local uuid="$1"
  192. local dev_info
  193. dev_info=$(find_device_by_uuid "$uuid")
  194. if [[ -z "$dev_info" ]]; then
  195. return 1
  196. fi
  197. local mp="${dev_info%%|*}"
  198. if [[ -z "$mp" ]]; then
  199. if [[ "${UDEV_MODE:-false}" == "true" ]]; then
  200. # udev模式: 不挂载, 等待桌面环境自动挂载 (最长10秒)
  201. local i
  202. for i in {1..10}; do
  203. sleep 1
  204. dev_info=$(find_device_by_uuid "$uuid")
  205. mp="${dev_info%%|*}"
  206. [[ -n "$mp" ]] && break
  207. done
  208. else
  209. # 正常模式: 主动挂载
  210. mount_device "$uuid" > /dev/null 2>&1 || return 1
  211. sleep 1
  212. dev_info=$(find_device_by_uuid "$uuid")
  213. mp="${dev_info%%|*}"
  214. fi
  215. fi
  216. if [[ -n "$mp" ]]; then
  217. echo "$mp"
  218. return 0
  219. fi
  220. return 1
  221. }
  222. # ============================================================
  223. # 获取源目录中最新的 N 个文件 (每行一个路径)
  224. # $1: 源目录, $2: 逗号分隔的扩展名 (空=所有文件), $3: 数量 (默认1)
  225. # ============================================================
  226. get_latest_files() {
  227. local src="$1"
  228. local exts="$2"
  229. local count="${3:-1}"
  230. if [[ ! -d "$src" ]]; then
  231. echo ""
  232. return 1
  233. fi
  234. if [[ -z "$exts" ]]; then
  235. find "$src" -maxdepth 1 -type f -printf '%T@ %p\n' 2>/dev/null \
  236. | sort -rn | head -n "$count" | cut -d' ' -f2-
  237. return
  238. fi
  239. local IFS=','
  240. local args=()
  241. local ext_count=0
  242. for ext in $exts; do
  243. ext=$(echo "$ext" | tr -d '[:space:]')
  244. [[ -z "$ext" ]] && continue
  245. if [[ $ext_count -gt 0 ]]; then
  246. args+=(-o)
  247. fi
  248. args+=(-iname "*.$ext")
  249. ext_count=$((ext_count+1))
  250. done
  251. if [[ $ext_count -eq 0 ]]; then
  252. echo ""
  253. return 1
  254. fi
  255. if [[ $ext_count -gt 1 ]]; then
  256. find "$src" -maxdepth 1 -type f \( "${args[@]}" \) -printf '%T@ %p\n' 2>/dev/null \
  257. | sort -rn | head -n "$count" | cut -d' ' -f2-
  258. else
  259. find "$src" -maxdepth 1 -type f "${args[@]}" -printf '%T@ %p\n' 2>/dev/null \
  260. | sort -rn | head -n "$count" | cut -d' ' -f2-
  261. fi
  262. }
  263. # ============================================================
  264. # 日志 (udev触发时写入logger, 否则写入stderr)
  265. # ============================================================
  266. log() {
  267. if [[ "${UDEV_MODE:-false}" == "true" ]]; then
  268. logger -t "copyfast" "$@"
  269. else
  270. echo "$@" >&2
  271. fi
  272. }
  273. # ============================================================
  274. # 桌面通知 (通过 real user 的 D-Bus)
  275. # ============================================================
  276. _notify() {
  277. local uid
  278. uid=$(id -u "$REAL_USER" 2>/dev/null) || { log "通知: 无法获取用户UID"; return 0; }
  279. local bus="/run/user/$uid/bus"
  280. if [[ ! -S "$bus" ]]; then
  281. log "通知: D-Bus 会话总线不可用 ($bus)"
  282. return 0
  283. fi
  284. export DBUS_SESSION_BUS_ADDRESS="unix:path=$bus"
  285. # 尝试多种方式获取 DISPLAY
  286. if [[ -z "${DISPLAY:-}" ]]; then
  287. # 从 loginctl 获取用户会话的 display
  288. local display
  289. display=$(loginctl show-user "$REAL_USER" -p Display 2>/dev/null | cut -d= -f2)
  290. # 兜底: 常见的 display 值
  291. for d in "${display:-}" ":0" ":1"; do
  292. [[ -z "$d" ]] && continue
  293. export DISPLAY="$d"
  294. break
  295. done
  296. fi
  297. if notify-send -i drive-removable-media "copyfast" "$*" -t 5000 2>/dev/null; then
  298. log "通知: 桌面通知已发送"
  299. else
  300. log "通知: notify-send 执行失败"
  301. fi
  302. return 0
  303. }
  304. # ============================================================
  305. # 正常模式: 查找USB并拷贝最新文件
  306. # ============================================================
  307. do_copy() {
  308. if [[ -z "$USB_UUIDS" ]]; then
  309. log "错误: 未绑定U盘。请先插入U盘并运行: $(basename "$0") --bind"
  310. exit 1
  311. fi
  312. if [[ ! "$FILE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
  313. log "错误: 文件数量不是有效的正整数: $FILE_COUNT"
  314. exit 1
  315. fi
  316. # 遍历已绑定UUID, 找到第一个可用的
  317. log "正在查找已绑定的U盘..."
  318. local uuids uuid mountpoint was_mounted
  319. uuids=($(get_bound_uuids))
  320. for uuid in "${uuids[@]}"; do
  321. # 检查设备是否存在
  322. local dev_info
  323. dev_info=$(find_device_by_uuid "$uuid")
  324. [[ -z "$dev_info" ]] && continue
  325. local current_mp="${dev_info%%|*}"
  326. was_mounted="$current_mp"
  327. mountpoint=$(get_mountpoint "$uuid" 2>/dev/null) || {
  328. log "警告: 无法挂载 UUID=$uuid, 尝试下一个..."
  329. continue
  330. }
  331. break
  332. done
  333. if [[ -z "${mountpoint:-}" ]]; then
  334. log "错误: 未找到任何已绑定的U盘, 请确认U盘已插入"
  335. exit 1
  336. fi
  337. log "找到已绑定U盘: UUID=$uuid, 挂载点=$mountpoint"
  338. if [[ ! -d "$SOURCE_DIR" ]]; then
  339. log "错误: 源目录不存在: $SOURCE_DIR"
  340. exit 1
  341. fi
  342. log "正在 $SOURCE_DIR 中查找最新 $FILE_COUNT 个文件..."
  343. local files
  344. files=$(get_latest_files "$SOURCE_DIR" "$EXTENSIONS" "$FILE_COUNT")
  345. if [[ -z "$files" ]]; then
  346. if [[ -n "$EXTENSIONS" ]]; then
  347. log "错误: 未在 $SOURCE_DIR 中找到匹配扩展名 ($EXTENSIONS) 的文件"
  348. else
  349. log "错误: $SOURCE_DIR 中没有文件"
  350. fi
  351. exit 1
  352. fi
  353. local copied=0
  354. while IFS= read -r f; do
  355. [[ -z "$f" ]] && continue
  356. log "拷贝 ($((copied+1))/$FILE_COUNT): $(basename "$f") -> $mountpoint"
  357. cp "$f" "$mountpoint/"
  358. copied=$((copied+1))
  359. done <<< "$files"
  360. log "拷贝完成: 共 $copied 个文件 -> $mountpoint"
  361. # udev模式下发送桌面通知
  362. if [[ "${UDEV_MODE:-false}" == "true" ]]; then
  363. _notify "拷贝完成: 共 $copied 个文件"
  364. fi
  365. if [[ "$AUTO_UNMOUNT" == "true" ]]; then
  366. log "正在弹出U盘..."
  367. # udev模式下设置 D-Bus 环境以便 udisksctl 工作
  368. if [[ "${UDEV_MODE:-false}" == "true" ]]; then
  369. local uid
  370. uid=$(id -u "$REAL_USER" 2>/dev/null)
  371. if [[ -n "${uid:-}" ]] && [[ -S "/run/user/$uid/bus" ]]; then
  372. export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus"
  373. fi
  374. fi
  375. local dev_to_unmount="${UDEV_DEVICE:-/dev/disk/by-uuid/$uuid}"
  376. if udisksctl unmount -b "$dev_to_unmount" 2>/dev/null; then
  377. log "已弹出U盘 (unmount)"
  378. elif udisksctl power-off -b "$dev_to_unmount" 2>/dev/null; then
  379. log "已弹出U盘 (power-off)"
  380. elif umount "$mountpoint" 2>/dev/null; then
  381. log "已弹出U盘 (umount)"
  382. elif sudo umount "$mountpoint" 2>/dev/null; then
  383. log "已弹出U盘 (sudo umount)"
  384. else
  385. log "警告: 弹出 $mountpoint 失败 (可能有其他程序正在使用)"
  386. fi
  387. fi
  388. }
  389. # ============================================================
  390. # 绑定模式: 自动选择每个U盘的最大分区, 加入绑定列表
  391. # ============================================================
  392. do_bind() {
  393. local devices
  394. devices=$(list_usb_disks)
  395. if [[ -z "$devices" ]]; then
  396. echo "错误: 未检测到插入的USB设备, 请先插入U盘"
  397. exit 1
  398. fi
  399. local disk uuid label mp size
  400. # 收集当前已绑定的UUID
  401. local -A bound_map
  402. if [[ -n "$USB_UUIDS" ]]; then
  403. for bid in $USB_UUIDS; do
  404. bound_map["$bid"]=1
  405. done
  406. fi
  407. # 过滤掉已绑定的, 收集可绑定的
  408. local candidates=()
  409. while IFS='|' read -r disk uuid label mp size; do
  410. if [[ -n "${bound_map[$uuid]:-}" ]]; then
  411. echo "跳过已绑定: $uuid (标签=${label:-(无)}, 挂载点=${mp:-(未挂载)})"
  412. continue
  413. fi
  414. candidates+=("$disk|$uuid|$label|$mp|$size")
  415. done <<< "$devices"
  416. if [[ ${#candidates[@]} -eq 0 ]]; then
  417. echo "所有已插入的U盘都已绑定。"
  418. echo "当前绑定: $USB_UUIDS"
  419. echo "如需解绑, 请运行: $(basename "$0") --unbind"
  420. exit 0
  421. fi
  422. local chosen_uuid chosen_label chosen_disk
  423. local count=${#candidates[@]}
  424. if [[ "$count" -eq 1 ]]; then
  425. IFS='|' read -r chosen_disk chosen_uuid chosen_label _ _ <<< "${candidates[0]}"
  426. echo "检测到1个新U盘, 自动选择最大分区:"
  427. echo " 磁盘: $chosen_disk"
  428. echo " 标签: ${chosen_label:-(无)}"
  429. echo " UUID: $chosen_uuid"
  430. echo ""
  431. else
  432. echo "检测到 $count 个新U盘, 请选择要绑定的设备:"
  433. echo ""
  434. local i=1
  435. local lines=()
  436. for cand in "${candidates[@]}"; do
  437. IFS='|' read -r disk uuid label mp size <<< "$cand"
  438. echo " [$i] 磁盘=$disk 标签=${label:-(无)} UUID=$uuid 挂载点=${mp:-(未挂载)}"
  439. lines+=("$cand")
  440. ((i++))
  441. done
  442. echo ""
  443. read -rp "请选择要绑定的编号 [1-$count]: " choice
  444. if [[ ! "$choice" =~ ^[0-9]+$ ]] || [[ "$choice" -lt 1 ]] || [[ "$choice" -gt "$count" ]]; then
  445. echo "错误: 无效选择"
  446. exit 1
  447. fi
  448. IFS='|' read -r chosen_disk chosen_uuid chosen_label _ _ <<< "${lines[$((choice-1))]}"
  449. fi
  450. # 添加到绑定列表
  451. if [[ -z "$USB_UUIDS" ]]; then
  452. USB_UUIDS="$chosen_uuid"
  453. else
  454. USB_UUIDS="$USB_UUIDS $chosen_uuid"
  455. fi
  456. save_config
  457. echo "已绑定U盘:"
  458. echo " 磁盘: $chosen_disk"
  459. echo " 标签: ${chosen_label:-(无)}"
  460. echo " UUID: $chosen_uuid"
  461. echo ""
  462. echo "当前所有绑定: $USB_UUIDS"
  463. echo "配置已保存到 $CONFIG_FILE"
  464. echo ""
  465. echo "后续可直接运行 $(basename "$0") 或安装 udev 规则以实现自动运行:"
  466. echo " $(basename "$0") --install-udev"
  467. }
  468. # ============================================================
  469. # 解绑模式: 交互选择已绑定的U盘并移除
  470. # ============================================================
  471. do_unbind() {
  472. if [[ -z "$USB_UUIDS" ]]; then
  473. echo "当前没有绑定的U盘。"
  474. exit 0
  475. fi
  476. local uuids
  477. uuids=($USB_UUIDS)
  478. echo "当前绑定列表:"
  479. local i=1
  480. for uuid in "${uuids[@]}"; do
  481. # 查询标签信息
  482. local info
  483. info=$(lsblk -o UUID,LABEL,MOUNTPOINT -l -n 2>/dev/null | awk -v u="$uuid" '$1==u {print "标签=" $2 " 挂载点=" $3}')
  484. echo " [$i] UUID=$uuid ${info:-未插入}"
  485. ((i++))
  486. done
  487. echo ""
  488. if [[ ${#uuids[@]} -eq 1 ]]; then
  489. read -rp "确认解除此绑定? [y/N]: " confirm
  490. if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
  491. echo "已取消"
  492. exit 0
  493. fi
  494. USB_UUIDS=""
  495. else
  496. read -rp "请选择要解除的编号 [1-${#uuids[@]}] (留空取消): " choice
  497. if [[ -z "$choice" ]]; then
  498. echo "已取消"
  499. exit 0
  500. fi
  501. if [[ ! "$choice" =~ ^[1-9][0-9]*$ ]] || [[ "$choice" -gt "${#uuids[@]}" ]]; then
  502. echo "错误: 无效选择"
  503. exit 1
  504. fi
  505. unset 'uuids[$((choice-1))]'
  506. USB_UUIDS="${uuids[*]}"
  507. fi
  508. save_config
  509. echo "已解除绑定。"
  510. if [[ -n "$USB_UUIDS" ]]; then
  511. echo "当前绑定: $USB_UUIDS"
  512. else
  513. echo "当前没有任何绑定。"
  514. fi
  515. }
  516. # ============================================================
  517. # 安装 udev 规则 — 为每个已绑定UUID生成一条规则
  518. # ============================================================
  519. do_install_udev() {
  520. if [[ -z "$USB_UUIDS" ]]; then
  521. echo "错误: 尚未绑定U盘, 请先运行: $(basename "$0") --bind"
  522. exit 1
  523. fi
  524. local udev_rule="/etc/udev/rules.d/99-copyfast.rules"
  525. if [[ -f "$udev_rule" ]]; then
  526. echo "udev 规则已存在于 $udev_rule"
  527. read -rp "是否覆盖? [y/N]: " confirm
  528. if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
  529. echo "已取消"
  530. exit 0
  531. fi
  532. sudo rm "$udev_rule"
  533. fi
  534. echo "正在生成 udev 规则..."
  535. local uuids
  536. uuids=($USB_UUIDS)
  537. {
  538. echo "# copyfast udev rules — 插入已绑定U盘时自动运行"
  539. echo "# 安装时间: $(date)"
  540. for uuid in "${uuids[@]}"; do
  541. 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\""
  542. done
  543. } | sudo tee "$udev_rule" > /dev/null
  544. echo "正在重新加载 udev 规则..."
  545. sudo udevadm control --reload-rules
  546. sudo udevadm trigger
  547. echo ""
  548. echo "udev 规则安装完成: $udev_rule"
  549. echo "已为 ${#uuids[@]} 个U盘生成规则。"
  550. echo ""
  551. echo "规则内容:"
  552. cat "$udev_rule"
  553. echo ""
  554. echo "现在插入任意已绑定的U盘, 脚本将自动运行。"
  555. }
  556. # ============================================================
  557. # 卸载 udev 规则
  558. # ============================================================
  559. do_uninstall_udev() {
  560. local udev_rule="/etc/udev/rules.d/99-copyfast.rules"
  561. if [[ ! -f "$udev_rule" ]]; then
  562. echo "udev 规则不存在: $udev_rule"
  563. exit 0
  564. fi
  565. echo "正在移除 udev 规则..."
  566. sudo rm "$udev_rule"
  567. echo "正在重新加载 udev 规则..."
  568. sudo udevadm control --reload-rules
  569. echo "已移除 udev 规则。"
  570. }
  571. # ============================================================
  572. # 打印配置
  573. # ============================================================
  574. do_config() {
  575. echo "当前配置:"
  576. echo " 配置文件: $CONFIG_FILE"
  577. echo " 源目录: $SOURCE_DIR"
  578. echo " 文件后缀: ${EXTENSIONS:-(所有文件)}"
  579. echo " 拷贝数量: 最新的 $FILE_COUNT 个文件"
  580. echo " 拷贝后弹出: $AUTO_UNMOUNT"
  581. echo ""
  582. if [[ -z "$USB_UUIDS" ]]; then
  583. echo " 已绑定U盘: (无)"
  584. else
  585. echo " 已绑定U盘 ($(echo "$USB_UUIDS" | wc -w) 个):"
  586. local uuids
  587. uuids=($USB_UUIDS)
  588. for uuid in "${uuids[@]}"; do
  589. local info
  590. info=$(lsblk -o UUID,LABEL,MOUNTPOINT -l -n 2>/dev/null \
  591. | awk -v u="$uuid" '$1==u {printf "标签=%s, 挂载点=%s", ($2?$2:"无"), ($3?$3:"未挂载")}')
  592. echo " UUID=$uuid ${info:-未插入}"
  593. done
  594. fi
  595. }
  596. # ============================================================
  597. # 帮助
  598. # ============================================================
  599. do_help() {
  600. cat <<HELP
  601. copyfast — 从指定目录快速拷贝最新文件到绑定的U盘
  602. 用法: $(basename "$0") [选项]
  603. 正常使用:
  604. 直接运行即可将 SOURCE_DIR 中最新的 FILE_COUNT 个匹配文件
  605. 拷贝到已绑定的U盘 (第一个检测到的), 拷贝完成后自动弹出U盘。
  606. 选项:
  607. -n, --count N 拷贝最新的 N 个文件 (默认: 1)
  608. 例: $(basename "$0") --count 3
  609. -N, --no-unmount 拷贝后不弹出U盘
  610. 例: $(basename "$0") --no-unmount
  611. 绑定与管理:
  612. -b, --bind 绑定当前插入的U盘到配置文件
  613. 每个U盘只选最大分区, 可重复执行绑定多个U盘
  614. 例: $(basename "$0") --bind
  615. -u, --unbind 解除已绑定的U盘, 交互选择要移除的绑定
  616. 例: $(basename "$0") --unbind
  617. -i, --install-udev 安装 udev 规则 (需 sudo 权限)
  618. 为所有已绑定U盘生成规则, 插入任意一个即自动运行
  619. -U, --uninstall-udev 移除 udev 规则 (需 sudo 权限)
  620. -c, --config 打印当前所有配置项及其值
  621. -h, --help 显示此帮助信息
  622. 配置文件: $CONFIG_FILE
  623. 配置项说明:
  624. SOURCE_DIR 要拷贝哪个目录下的最新文件
  625. 默认: ~/Downloads
  626. EXTENSIONS 只拷贝这些后缀的文件, 逗号分隔, 大小写不敏感
  627. 留空表示所有文件
  628. 例: "ppt,pptx,pdf" 或 "zip,tar.gz,7z"
  629. FILE_COUNT 每次拷贝最新的几个文件, 必须是正整数 (默认1)
  630. USB_UUIDS 绑定的U盘UUID列表 (空格分隔), 由 --bind/--unbind 维护
  631. 请不要手动修改此项
  632. AUTO_UNMOUNT 拷贝后是否自动弹出U盘
  633. true=弹出 (默认), false=不弹出
  634. 命令行 --no-unmount 可临时覆盖为不弹出
  635. 使用示例:
  636. # 首次配置
  637. $(basename "$0") --bind # 插入U盘后绑定 (自动选最大分区)
  638. $(basename "$0") --count 3 # 拷贝最新3个文件到U盘
  639. $(basename "$0") --no-unmount # 拷贝但不弹出
  640. # 管理多个U盘
  641. $(basename "$0") --bind # 插入另一个U盘, 再次绑定
  642. $(basename "$0") --config # 查看所有绑定
  643. $(basename "$0") --unbind # 解除某个绑定
  644. # 修改默认行为: 编辑配置文件
  645. # 将 EXTENSIONS 设为 "ppt,pptx" 则之后只拷贝这两种文件
  646. # 将 FILE_COUNT 设为 "5" 则之后每次拷贝5个文件
  647. # 安装自动运行后, 插入任意已绑定U盘自动拷贝并弹出
  648. $(basename "$0") --install-udev
  649. 技术说明:
  650. - USB设备通过文件系统UUID识别, 不受挂载点路径变化影响
  651. - 每个U盘只选最大分区, 避免绑定到启动分区等小分区
  652. - 支持绑定多个不同U盘, 运行时使用最先检测到的那个
  653. - udev模式下轮询等待挂载 (最长10秒), 日志写入 syslog
  654. - 文件匹配使用 find -iname, 扩展名匹配不区分大小写
  655. HELP
  656. }
  657. # ============================================================
  658. # 入口与参数解析
  659. # ============================================================
  660. # 先加载配置文件, 后续命令行参数可覆盖配置值
  661. load_config
  662. UDEV_MODE=false
  663. UDEV_DEVICE=""
  664. # 使用 while+shift 解析参数, 支持 --count N 这种带值参数
  665. while [[ $# -gt 0 ]]; do
  666. case "$1" in
  667. --udev-triggered)
  668. UDEV_MODE=true
  669. shift
  670. ;;
  671. --user)
  672. if [[ -z "${2:-}" ]] || [[ "${2:0:1}" == "-" ]]; then
  673. echo "错误: --user 需要指定用户名"
  674. exit 1
  675. fi
  676. REAL_USER="$2"
  677. REAL_HOME="$(_resolve_home "$2")"
  678. shift 2
  679. ;;
  680. --device)
  681. if [[ -z "${2:-}" ]] || [[ "${2:0:1}" == "-" ]]; then
  682. echo "错误: --device 需要指定设备路径"
  683. exit 1
  684. fi
  685. UDEV_DEVICE="$2"
  686. shift 2
  687. ;;
  688. --count|-n)
  689. if [[ -z "${2:-}" ]] || [[ "${2:0:1}" == "-" ]]; then
  690. echo "错误: --count 需要指定一个正整数"
  691. exit 1
  692. fi
  693. FILE_COUNT="$2"
  694. shift 2
  695. ;;
  696. --no-unmount|-N)
  697. AUTO_UNMOUNT="false"
  698. shift
  699. ;;
  700. --bind|-b)
  701. do_bind
  702. exit 0
  703. ;;
  704. --unbind|-u)
  705. do_unbind
  706. exit 0
  707. ;;
  708. --install-udev|-i)
  709. do_install_udev
  710. exit 0
  711. ;;
  712. --uninstall-udev|-U)
  713. do_uninstall_udev
  714. exit 0
  715. ;;
  716. --config|-c)
  717. do_config
  718. exit 0
  719. ;;
  720. --help|-h)
  721. do_help
  722. exit 0
  723. ;;
  724. *)
  725. echo "错误: 未知选项 '$1'"
  726. echo "运行 $(basename "$0") --help 查看用法"
  727. exit 1
  728. ;;
  729. esac
  730. done
  731. # 进入正常拷贝模式
  732. # 注意: udev 模式下由 systemd-run 管理生命周期, 无需后台化
  733. do_copy