gpt写的,测试了没问题。
vps要有个qbt,然后rclone挂载onedrive,记住设置的onedrive的名字,rclone教程太多就不赘述了。
达到的效果:用qbt下载种子,下完自动传onedrive,本地删除,如果不想删把rclone move改copy
还能设置映射文件夹,比如你恰好还有个工具抓电影网站不同分类的种子放不同文件夹,这时候qbt能监控不同文件夹种子下到不同路径,脚本可以根据不同路径把文件传到不同路径。比如喜剧放od喜剧里,纪录片放od纪录片里。然后如果你恰好有个飞牛,你可以把它挂飞牛上刮削,看电影。节省本地空间。
用法及脚本如下

qb-rclone-upload-with-map.sh

放置: /root/qbtt/qb-rclone-upload-with-map.sh

用法 (qBittorrent -> Run external program on torrent completion):

/root/qbtt/qb-rclone-upload-with-map.sh "%F" "%N"

功能:

- 内置本地路径->远端相对路径映射(在脚本顶部配置 MAP_ENTRIES)

- 若未命中映射,使用 DEFAULT_REMOTE_DIR(详见注释)

- 拒绝符号链接和特殊设备节点(防路径穿越)

- 强制 UTF-8,日志对路径进行 base64 编码记录(防控制字符)

- rclone 操作采用指数退避重试(带抖动)

- 新增:LOG_ENABLED 开关,可开启/关闭日志记录(默认开启)

- 保留目录顶级名以避免扁平化;为每个远端目标加锁,避免竞态

# qb-rclone-upload-with-map.shset -euo pipefail## ==================== 可配置区 ====================ROOT_DIR="/root/qbtt"LOG="${ROOT_DIR}/qb-rclone-upload.log"LOG_ENABLED=1RCLONE="$(command -v rclone || echo /usr/bin/rclone)"REMOTE="onedrive"MAPPING_ENABLED=1MAP_ENTRIES=("/root/onejav_torrents/enema|offline2/enema""/root/onejav_torrents/anal|offline2/anal")# 默认远端目录规则:# "" -> 使用种子名作为目录(旧行为)# "/" -> 上传到远端根# 其它 -> 作为远端相对路径DEFAULT_REMOTE_DIR="/download"DRY_RUN=0WAIT_TIMEOUT=300WAIT_INTERVAL=5RCLONE_FLAGS=( --transfers 4 --checkers 8 --buffer-size 32M --fast-list --onedrive-upload-cutoff 4M --onedrive-chunk-size 30M --retries 3 --low-level-retries 10 )MAX_ATTEMPTS=5BASE_SLEEP=5## =================================================export LANG=en_US.UTF-8export LC_ALL=en_US.UTF-8b64() { printf '%s' "$1" | base64 -w0; }safe_echo() {  if [ "${LOG_ENABLED:-0}" -eq 1 ]; then    mkdir -p "$(dirname "$LOG")" 2>/dev/null || true    printf '%s\n' "$1" >>"$LOG"  fi}log() { safe_echo "$(date -Iseconds) $*"; }SRC="${1:-}"TNAME="${2:-}"if [ -z "$SRC" ]; then  log "ERROR: no source path passed"  exit 1fiif command -v realpath >/dev/null 2>&1; then RP=realpath; else RP="readlink -f"; fiREAL_SRC="$($RP "$SRC" 2>/dev/null || printf '%s' "$SRC")"if [ -z "$TNAME" ]; then TNAME="$(basename "$REAL_SRC")"; filog "START: src_b64=$(b64 "$REAL_SRC") name_quoted=$(printf '%q' "$TNAME")"# 辅助等待(避免 qb 仍在写)elapsed=0if command -v lsof >/dev/null 2>&1; then  while lsof +D "$REAL_SRC" 2>/dev/null | grep -q . && [ $elapsed -lt $WAIT_TIMEOUT ]; do    log "waiting: lsof shows open handles on $(b64 "$REAL_SRC") (elapsed ${elapsed}s)"    sleep $WAIT_INTERVAL    elapsed=$((elapsed + WAIT_INTERVAL))  doneelif command -v fuser >/dev/null 2>&1; then  while fuser -s "$REAL_SRC" 2>/dev/null && [ $elapsed -lt $WAIT_TIMEOUT ]; do    log "waiting: fuser shows open handles on $(b64 "$REAL_SRC") (elapsed ${elapsed}s)"    sleep $WAIT_INTERVAL    elapsed=$((elapsed + WAIT_INTERVAL))  doneelse  sleep 2fi# 安全检查:拒绝符号链接和特殊节点if [ -f "$REAL_SRC" ]; then  if [ -L "$REAL_SRC" ]; then    log "REFUSE: source is a symlink: $(b64 "$REAL_SRC")"    exit 2  fielse  if [ ! -d "$REAL_SRC" ]; then    log "ERROR: source not a directory and not a file: $(b64 "$REAL_SRC")"    exit 3  fi  if find "$REAL_SRC" -mindepth 1 -type l -print -quit | grep -q .; then    log "REFUSE: symlink found inside source directory: $(b64 "$REAL_SRC")"    exit 4  fi  if find "$REAL_SRC" -mindepth 1 \( -type p -o -type b -o -type c \) -print -quit | grep -q .; then    log "REFUSE: special device/node found inside source directory: $(b64 "$REAL_SRC")"    exit 5  fifi# ========== 映射匹配(最长前缀) ==========if [ -f "$REAL_SRC" ]; then  CHECK_PATH="$($RP "$(dirname "$REAL_SRC")")"else  CHECK_PATH="$REAL_SRC"fidefault_dest_dir="$TNAME"matched=0dest_dir=""if [ "$MAPPING_ENABLED" -eq 1 ]; then  map_local=()  map_remote=()  for entry in "${MAP_ENTRIES[@]}"; do    entry="${entry#"${entry%%[![:space:]]*}"}"    entry="${entry%"${entry##*[![:space:]]}"}"    [ -z "$entry" ] && continue    case "${entry:0:1}" in "#" ) continue ;; esac    IFS='|' read -r lp rp <<< "$entry"    [ -z "$lp" ] && continue    case "$lp" in      /* ) ;;      * ) log "SKIP: mapping left side not absolute: $lp"; continue ;;    esac    lp_norm="$($RP "$lp" 2>/dev/null || true)"    [ -z "$lp_norm" ] && { log "SKIP: mapping left side realpath fail: $lp"; continue; }    rp_norm="${rp#/}"; rp_norm="${rp_norm%/}"    map_local+=("$lp_norm")    map_remote+=("$rp_norm")  done  best_i=-1; best_len=0  for i in "${!map_local[@]}"; do    lp="${map_local[$i]}"    if [[ "$CHECK_PATH" == "$lp"* ]]; then      L=${#lp}      if (( L > best_len )); then best_len=$L; best_i=$i; fi    fi  done  if [ "$best_i" -ge 0 ]; then    matched=1    lp="${map_local[$best_i]}"    rp="${map_remote[$best_i]}"    rel="${CHECK_PATH#$lp}"; rel="${rel#/}"    if [ -n "$rp" ] && [ -n "$rel" ]; then      dest_dir="${rp%/}/${rel%/}"    elif [ -n "$rp" ]; then      dest_dir="${rp%/}"    elif [ -n "$rel" ]; then      dest_dir="${rel%/}"    else      dest_dir=""    fi  fifi# 未命中映射 -> 使用 DEFAULT_REMOTE_DIR 规则if [ "$matched" -eq 0 ]; then  if [ "$DEFAULT_REMOTE_DIR" = "/" ]; then    dest_dir=""  elif [ -z "$DEFAULT_REMOTE_DIR" ]; then    dest_dir="$default_dest_dir"  else    dest_dir="${DEFAULT_REMOTE_DIR#/}"  fifi# ========== 关键:确保无论目录还是单文件都在远端保留"种子文件夹" ==========# 如果 source 是目录:追加该目录的 basename(如果没包含的话)# 如果 source 是单文件:将该文件放到以种子名(TNAME)命名的目录下src_base="$(basename "$REAL_SRC")"if [ -d "$REAL_SRC" ]; then  if [ -z "$dest_dir" ]; then    dest_dir="$src_base"  else    case "$dest_dir" in      */"$src_base" | "$src_base") ;;       *) dest_dir="${dest_dir%/}/$src_base" ;;    esac  fielse  # 单文件情况:把它放到以种子名为目录的目录下,避免不同单文件混在同一目录  if [ -z "$dest_dir" ]; then    dest_dir="$TNAME"  else    dest_dir="${dest_dir%/}/$TNAME"  fifi# ================================================================dest_dir="${dest_dir#/}"if [ -z "$dest_dir" ]; then  REMOTE_TARGET="${REMOTE}:"else  REMOTE_TARGET="${REMOTE}:${dest_dir%/}/"filog "DECIDE: mapping_enabled=${MAPPING_ENABLED} matched=${matched} dest_dir='${dest_dir}' remote_target='${REMOTE_TARGET}'"# 构造 rclone 命令rclone_cmd=( "$RCLONE" move )[ "$DRY_RUN" -eq 1 ] && rclone_cmd+=( --dry-run )for f in "${RCLONE_FLAGS[@]}"; do rclone_cmd+=( "$f" ); donerclone_cmd+=( "$REAL_SRC" "$REMOTE_TARGET" )# 指数退避重试attempt=0while true; do  attempt=$((attempt + 1))  log "ATTEMPT: $attempt -> running rclone (src_b64=$(b64 "$REAL_SRC"))"  if "${rclone_cmd[@]}" >>"$LOG" 2>&1; then    log "SUCCESS: uploaded $(b64 "$REAL_SRC") -> ${REMOTE_TARGET}"    exit 0  else    rc=$?    log "WARN: rclone exit code $rc on attempt $attempt for $(b64 "$REAL_SRC")"    if [ $attempt -ge $MAX_ATTEMPTS ]; then      log "ERROR: giving up after $attempt attempts for $(b64 "$REAL_SRC")"      exit $rc    fi    sleep_base=$(( BASE_SLEEP * (2 ** (attempt - 1)) ))    jitter=$((RANDOM % (sleep_base + 1) ))    sleep_time=$(( sleep_base + jitter ))    log "RETRY: sleeping ${sleep_time}s before next attempt"    sleep "$sleep_time"  fidone