#!/bin/bash # ============================================================ # 百度网盘备份脚本 # 用法: ./baidu-backup.sh [目标目录] # 示例: ./baidu-backup.sh /2026/0517 # ./baidu-backup.sh # 自动使用 /年份/月日 # ============================================================ # 不使用 set -e(与并行后台进程不兼容) # ---- 配置 ---- APP_KEY="z3gemBZfg7KYj6U3eHNfIzTs7uYS9OMh" SECRET_KEY="ptCKj2DfxL0KtGR1pM08c9KO2t2UC7SR" TOKEN_FILE="$HOME/.baidu_pan_token.json" BLOCK_SIZE=$((32 * 1024 * 1024)) # 32MB 大分片减少HTTP开销 PARALLEL=4 # 并行上传数 # ---- 自动识别项目 ---- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_NAME="$(basename "$SCRIPT_DIR")" PARENT_DIR="$(dirname "$SCRIPT_DIR")" ZIP_FILE="/tmp/${PROJECT_NAME}.zip" MD5_DIR="/tmp/baidu_md5" # ---- 目标目录 ---- if [ -n "$1" ]; then REMOTE_DIR="$1" else REMOTE_DIR="/$(date +%Y)/$(date +%m%d)" fi REMOTE_PATH="${REMOTE_DIR}/${PROJECT_NAME}.zip" echo "" echo "╔══════════════════════════════════════════╗" echo "║ 📦 百度网盘备份工具 ║" echo "╚══════════════════════════════════════════╝" echo "" echo " 项目: $PROJECT_NAME" echo " 目标: $REMOTE_PATH" echo "" # ============================================================ # 步骤1: 打包 # ============================================================ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "[1/6] 📦 打包项目文件..." rm -f "$ZIP_FILE" cd "$PARENT_DIR" # 先统计文件总数 TOTAL_FILES=$(find "$PROJECT_NAME" -not -path "${PROJECT_NAME}/.git/*" -type f | wc -l | tr -d ' ') echo " 📊 共 ${TOTAL_FILES} 个文件" # 打包并实时显示百分比 COUNTER=0 zip -r "$ZIP_FILE" "$PROJECT_NAME" -x "${PROJECT_NAME}/.git/*" 2>&1 | while IFS= read -r line; do COUNTER=$((COUNTER + 1)) PCT=$((COUNTER * 100 / TOTAL_FILES)) if [ $PCT -gt 100 ]; then PCT=100; fi # 进度条 BAR_FILLED=$((PCT * 30 / 100)) BAR_EMPTY=$((30 - BAR_FILLED)) BAR="" for ((b=0; b/dev/null || stat -c%s "$ZIP_FILE" 2>/dev/null) FILE_SIZE_MB=$((FILE_SIZE / 1024 / 1024)) echo " ✅ 打包完成: ${FILE_SIZE_MB}MB" # ============================================================ # 步骤2: 获取 Token(缓存 / 刷新 / 设备授权) # ============================================================ echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "[2/6] 🔑 获取百度网盘授权..." ACCESS_TOKEN="" # 尝试读取缓存 if [ -f "$TOKEN_FILE" ]; then CACHED=$(python3 -c " import json, time, sys with open('$TOKEN_FILE') as f: d = json.load(f) if time.time() < d.get('expires_at', 0): print('VALID|' + d['access_token']) else: print('EXPIRED|' + d.get('refresh_token', '')) " 2>/dev/null || echo "FAIL|") STATUS="${CACHED%%|*}" VALUE="${CACHED#*|}" if [ "$STATUS" = "VALID" ]; then ACCESS_TOKEN="$VALUE" echo " ✅ 使用缓存 Token(有效)" elif [ "$STATUS" = "EXPIRED" ] && [ -n "$VALUE" ]; then echo " ⏳ Token 已过期,尝试刷新..." RESP=$(curl -s -X POST "https://openapi.baidu.com/oauth/2.0/token" \ -d "grant_type=refresh_token&refresh_token=${VALUE}&client_id=${APP_KEY}&client_secret=${SECRET_KEY}") NEW_TOKEN=$(echo "$RESP" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('access_token',''))" 2>/dev/null) if [ -n "$NEW_TOKEN" ]; then ACCESS_TOKEN="$NEW_TOKEN" # 更新缓存 python3 -c " import json, time d = json.loads('$RESP'.replace(\"'\", '\"')) token = {'access_token': d['access_token'], 'refresh_token': d['refresh_token'], 'expires_at': int(time.time()) + d['expires_in']} with open('$TOKEN_FILE', 'w') as f: json.dump(token, f) " 2>/dev/null echo " ✅ Token 刷新成功" else echo " ⚠️ 刷新失败,需要重新授权" fi fi fi # 如果没有有效 Token,走设备授权流程 if [ -z "$ACCESS_TOKEN" ]; then echo " 🔐 需要设备授权..." DEVICE_RESP=$(curl -s -X POST "https://openapi.baidu.com/oauth/2.0/device/code" \ -d "response_type=device_code&client_id=${APP_KEY}&scope=basic,netdisk") DEVICE_CODE=$(echo "$DEVICE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin)['device_code'])") USER_CODE=$(echo "$DEVICE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin)['user_code'])") QRCODE_URL=$(echo "$DEVICE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin)['qrcode_url'])") echo "" echo " ┌─────────────────────────────────────┐" echo " │ 请打开: https://openapi.baidu.com/device" echo " │ 输入码: $USER_CODE" echo " │ 或扫码: $QRCODE_URL" echo " └─────────────────────────────────────┘" echo "" read -p " 授权完成后按回车继续..." _ TOKEN_RESP=$(curl -s -X POST "https://openapi.baidu.com/oauth/2.0/token" \ -d "grant_type=device_token&code=${DEVICE_CODE}&client_id=${APP_KEY}&client_secret=${SECRET_KEY}") ACCESS_TOKEN=$(echo "$TOKEN_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null) if [ -z "$ACCESS_TOKEN" ]; then echo " ❌ 授权失败: $TOKEN_RESP" rm -f "$ZIP_FILE" exit 1 fi # 保存缓存 echo "$TOKEN_RESP" | python3 -c " import sys, json, time d = json.load(sys.stdin) token = {'access_token': d['access_token'], 'refresh_token': d['refresh_token'], 'expires_at': int(time.time()) + d['expires_in']} with open('$TOKEN_FILE', 'w') as f: json.dump(token, f) " echo " ✅ 授权成功,Token 已缓存" fi # ============================================================ # 步骤3: 创建远程目录 # ============================================================ echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "[3/6] 📁 创建远程目录 ${REMOTE_DIR} ..." DIR_RESP=$(curl -s "https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=${ACCESS_TOKEN}" \ -d "path=${REMOTE_DIR}&size=0&isdir=1") DIR_ERRNO=$(echo "$DIR_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('errno',99))" 2>/dev/null) if [ "$DIR_ERRNO" = "0" ] || [ "$DIR_ERRNO" = "-8" ]; then echo " ✅ 目录已就绪" else echo " ⚠️ 目录创建返回: errno=$DIR_ERRNO(继续尝试上传)" fi # ============================================================ # 步骤4: 预创建文件 # ============================================================ echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" BLOCKS=$(( (FILE_SIZE + BLOCK_SIZE - 1) / BLOCK_SIZE )) echo "[4/6] 📋 预创建文件(${BLOCKS}个分片)..." BLOCK_LIST=$(python3 -c "import json; print(json.dumps(['0'*32]*$BLOCKS))") PRE_RESP=$(curl -s "https://pan.baidu.com/rest/2.0/xpan/file?method=precreate&access_token=${ACCESS_TOKEN}" \ -d "path=${REMOTE_PATH}&size=${FILE_SIZE}&isdir=0&autoinit=1&block_list=${BLOCK_LIST}") UPLOAD_ID=$(echo "$PRE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('uploadid',''))" 2>/dev/null) if [ -z "$UPLOAD_ID" ]; then echo " ❌ 预创建失败: $PRE_RESP" rm -f "$ZIP_FILE" exit 1 fi echo " ✅ 预创建成功" # ============================================================ # 步骤5: 分片上传(32MB大分片 + 并行上传) # ============================================================ echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "[5/6] 🚀 上传中(${PARALLEL}路并行, 每片$((BLOCK_SIZE/1024/1024))MB)..." echo "" # 预切割文件 SPLIT_DIR="/tmp/baidu_chunks" rm -rf "$SPLIT_DIR" mkdir -p "$SPLIT_DIR" split -b ${BLOCK_SIZE} -a 4 "$ZIP_FILE" "${SPLIT_DIR}/chunk_" CHUNK_FILES=($(ls "${SPLIT_DIR}/chunk_"* | sort)) BLOCKS=${#CHUNK_FILES[@]} echo " 📊 已切割为 ${BLOCKS} 个分片" echo "" # 创建md5结果目录 MD5_DIR="/tmp/baidu_md5" rm -rf "$MD5_DIR" mkdir -p "$MD5_DIR" START_TIME=$(date +%s) DONE_COUNT=0 FAIL_FLAG="/tmp/baidu_upload_fail" rm -f "$FAIL_FLAG" # 单片上传函数(带重试) upload_chunk() { local SEQ=$1 local CHUNK_FILE=$2 local RETRY=0 local MAX_RETRY=3 while [ $RETRY -lt $MAX_RETRY ]; do local RESP RESP=$(curl -s --connect-timeout 30 --max-time 300 \ "https://d.pcs.baidu.com/rest/2.0/pcs/superfile2?method=upload&access_token=${ACCESS_TOKEN}&type=tmpfile&path=${REMOTE_PATH}&uploadid=${UPLOAD_ID}&partseq=${SEQ}" \ -F "file=@${CHUNK_FILE}" 2>&1) local MD5 MD5=$(echo "$RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('md5',''))" 2>/dev/null) if [ -n "$MD5" ]; then echo "$MD5" > "${MD5_DIR}/$(printf '%04d' $SEQ).md5" return 0 fi RETRY=$((RETRY + 1)) if [ $RETRY -lt $MAX_RETRY ]; then sleep 2 fi done echo "chunk $SEQ failed: $RESP" > "${FAIL_FLAG}_${SEQ}" return 1 } # 并行上传 for ((i=0; i/dev/null rm -rf "$SPLIT_DIR" "$MD5_DIR" "$ZIP_FILE" "${FAIL_FLAG}_"* exit 1 fi # 计算进度 DONE_COUNT=$((i + ${#PIDS[@]})) if [ $DONE_COUNT -gt $BLOCKS ]; then DONE_COUNT=$BLOCKS; fi PCT=$((DONE_COUNT * 100 / BLOCKS)) UPLOADED_MB=$((DONE_COUNT * BLOCK_SIZE / 1024 / 1024)) if [ $UPLOADED_MB -gt $FILE_SIZE_MB ]; then UPLOADED_MB=$FILE_SIZE_MB; fi NOW=$(date +%s) ELAPSED=$((NOW - START_TIME)) if [ $ELAPSED -gt 0 ]; then SPEED_MB=$((UPLOADED_MB / ELAPSED)) REMAINING_MB=$((FILE_SIZE_MB - UPLOADED_MB)) if [ $SPEED_MB -gt 0 ]; then ETA=$((REMAINING_MB / SPEED_MB)) ETA_STR="${ETA}s" if [ $ETA -ge 60 ]; then ETA_STR="$((ETA/60))m$((ETA%60))s"; fi else ETA_STR="计算中" fi else SPEED_MB=0 ETA_STR="计算中" fi BAR_FILLED=$((PCT * 30 / 100)) BAR_EMPTY=$((30 - BAR_FILLED)) BAR="" for ((b=0; b/dev/null) FINAL_PATH=$(echo "$CREATE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('path',''))" 2>/dev/null) # 清理 rm -f "$ZIP_FILE" /tmp/chunk_part rm -rf "$MD5_DIR" "$FAIL_FLAG" # 计算耗时 END_TIME=$(date +%s) TOTAL=$((END_TIME - START_TIME)) T_MIN=$((TOTAL / 60)) T_SEC=$((TOTAL % 60)) # 输出结果 echo "" echo "╔══════════════════════════════════════════════╗" if [ "$CREATE_ERRNO" = "0" ]; then echo "║ 📦 百度网盘备份完成! ║" echo "╠══════════════════════════════════════════════╣" echo " 📁 路径: $FINAL_PATH" echo " 📊 大小: ${FILE_SIZE_MB} MB" echo " ⏱️ 耗时: ${T_MIN}分${T_SEC}秒" echo " ✅ 状态: 上传成功" else echo "║ ❌ 合并失败 ║" echo "╠══════════════════════════════════════════════╣" echo " errno: $CREATE_ERRNO" echo " 响应: $CREATE_RESP" fi echo "╚══════════════════════════════════════════════╝" echo ""