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
评论 (0)