Files
GovAI/baidu-backup.sh
2026-06-15 23:48:37 +08:00

348 lines
14 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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<BAR_FILLED; b++)); do BAR="${BAR}"; done
for ((b=0; b<BAR_EMPTY; b++)); do BAR="${BAR}"; done
printf "\r %s %3d%% (%d/%d) " "$BAR" "$PCT" "$COUNTER" "$TOTAL_FILES"
done
echo ""
FILE_SIZE=$(stat -f%z "$ZIP_FILE" 2>/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<BLOCKS; i+=PARALLEL)); do
PIDS=()
for ((j=0; j<PARALLEL && i+j<BLOCKS; j++)); do
SEQ=$((i + j))
upload_chunk $SEQ "${CHUNK_FILES[$SEQ]}" &
PIDS+=($!)
done
# 等待本批完成
BATCH_FAIL=0
for PID in "${PIDS[@]}"; do
wait $PID || BATCH_FAIL=1
done
if [ $BATCH_FAIL -ne 0 ]; then
echo ""
echo " ❌ 上传失败:"
cat "${FAIL_FLAG}_"* 2>/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<BAR_FILLED; b++)); do BAR="${BAR}"; done
for ((b=0; b<BAR_EMPTY; b++)); do BAR="${BAR}"; done
printf "\r %s %3d%% | %d/%dMB | %dMB/s | 剩余%s " "$BAR" "$PCT" "$UPLOADED_MB" "$FILE_SIZE_MB" "$SPEED_MB" "$ETA_STR"
done
echo ""
echo ""
echo " ✅ 所有分片上传完成"
# 删除临时分片
rm -rf "$SPLIT_DIR"
# ============================================================
# 步骤6: 合并文件
# ============================================================
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "[6/6] 🔗 合并文件..."
MD5_LIST=$(cat "${MD5_DIR}/"*.md5 | awk '{printf "\"%s\",", $0}' | sed 's/,$//')
CREATE_RESP=$(curl -s "https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=${ACCESS_TOKEN}" \
-d "path=${REMOTE_PATH}&size=${FILE_SIZE}&isdir=0&uploadid=${UPLOAD_ID}&block_list=[${MD5_LIST}]")
CREATE_ERRNO=$(echo "$CREATE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('errno',99))" 2>/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 ""