website-scripts/restore-db.sh

240 lines
7.3 KiB
Bash
Executable File

#!/bin/bash
# ============================================
# 数据库恢复脚本
#
# 用法:
# 恢复本地数据库: bash scripts/restore-db.sh local <服务名> <备份文件名>
# 恢复云端数据库: bash scripts/restore-db.sh cloud <服务名> <备份文件名>
#
# 服务名: issue / assets
#
# 示例:
# bash scripts/restore-db.sh local issue issue-2026-04-29_1753.db
# bash scripts/restore-db.sh cloud assets assets-2026-04-29_1753.db
# ============================================
set -e
# ---- 参数检查 ----
if [ $# -lt 3 ]; then
echo "用法: $0 <local|cloud> <issue|assets> <备份文件名>"
echo ""
echo "示例:"
echo " $0 local issue issue-2026-04-29_1753.db"
echo " $0 cloud assets assets-2026-04-29_1753.db"
echo ""
echo "可用备份文件:"
echo " 本地:"
ls -1 /Users/niuniu/programs/docker/db-backups/ 2>/dev/null || echo " (无)"
echo " 云端:"
ssh txjp "ls -1 /root/docker/db-backups/" 2>/dev/null || echo " (无)"
exit 1
fi
TARGET=$1
SERVICE=$2
BACKUP_FILE=$3
# ---- 路径定义 ----
LOCAL_BACKUP_DIR="/Users/niuniu/programs/docker/db-backups"
CLOUD_BACKUP_DIR="/root/docker/db-backups"
case $SERVICE in
issue)
PROJECT_DIR="issue-ai"
CONTAINER="issue-ai"
DB_PATH_IN_CONTAINER="/app/data/issue.db"
LOCAL_DB_PATH="/Users/niuniu/programs/docker/issue-ai/data/issue.db"
;;
assets)
PROJECT_DIR="assets-ai"
CONTAINER="assets-ai"
DB_PATH_IN_CONTAINER="/app/data/assets.db"
LOCAL_DB_PATH="/Users/niuniu/programs/docker/assets-ai/data/assets.db"
;;
*)
echo "错误: 服务名必须是 issue 或 assets"
exit 1
;;
esac
# ============================================
# 本地恢复
# ============================================
restore_local() {
local BACKUP_PATH="${LOCAL_BACKUP_DIR}/${BACKUP_FILE}"
echo "========================================"
echo " 本地数据库恢复"
echo "========================================"
echo " 服务: ${SERVICE} (${CONTAINER})"
echo " 备份文件: ${BACKUP_PATH}"
echo " 目标数据库: ${LOCAL_DB_PATH}"
echo "========================================"
echo ""
# 1. 检查备份文件
if [ ! -f "$BACKUP_PATH" ]; then
echo "[错误] 备份文件不存在: ${BACKUP_PATH}"
exit 1
fi
# 2. 验证备份文件是有效的 SQLite 数据库
if ! sqlite3 "$BACKUP_PATH" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1; then
echo "[错误] 备份文件不是有效的 SQLite 数据库"
exit 1
fi
# 3. 确认操作
echo "[警告] 这将覆盖本地 ${CONTAINER} 数据库!"
echo " 当前数据库: ${LOCAL_DB_PATH}"
echo " 将替换为: ${BACKUP_PATH}"
echo ""
read -p "确认恢复? (输入 yes 继续): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "已取消"
exit 0
fi
# 4. 备份当前数据库(安全起见)
local SAFE_BACKUP="${LOCAL_DB_PATH}.before-restore-$(date +%Y%m%d_%H%M%S)"
if [ -f "$LOCAL_DB_PATH" ]; then
cp "$LOCAL_DB_PATH" "$SAFE_BACKUP"
echo "[备份] 当前数据库已备份至: ${SAFE_BACKUP}"
fi
# 5. WAL checkpoint on backup (确保数据完整)
sqlite3 "$BACKUP_PATH" "PRAGMA wal_checkpoint(TRUNCATE)" 2>/dev/null || true
# 6. 停止本地容器(如果在运行)
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
echo "[操作] 停止容器 ${CONTAINER}..."
docker stop "$CONTAINER"
fi
# 7. 替换数据库
cp "$BACKUP_PATH" "$LOCAL_DB_PATH"
rm -f "${LOCAL_DB_PATH}-shm" "${LOCAL_DB_PATH}-wal"
echo "[完成] 数据库已替换"
# 8. 启动容器
echo "[操作] 启动容器 ${CONTAINER}..."
docker start "$CONTAINER"
# 9. 验证
sleep 2
echo ""
echo "[验证] 恢复后数据:"
if [ "$SERVICE" = "issue" ]; then
echo " 工单数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM tickets")"
echo " 配件名称: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM tickets WHERE parts_name IS NOT NULL AND parts_name != ''")"
else
echo " 设备数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM assets")"
fi
echo ""
echo "恢复完成!"
}
# ============================================
# 云端恢复
# ============================================
restore_cloud() {
local BACKUP_PATH="${CLOUD_BACKUP_DIR}/${BACKUP_FILE}"
echo "========================================"
echo " 云端数据库恢复 (txjp)"
echo "========================================"
echo " 服务: ${SERVICE} (${CONTAINER})"
echo " 备份文件: ${BACKUP_PATH}"
echo "========================================"
echo ""
# 1. 检查备份文件
if ! ssh txjp "test -f ${BACKUP_PATH}"; then
echo "[错误] 云端备份文件不存在: ${BACKUP_PATH}"
exit 1
fi
# 2. 验证备份文件
if ! ssh txjp "sqlite3 ${BACKUP_PATH} 'SELECT count(*) FROM sqlite_master' > /dev/null 2>&1"; then
echo "[错误] 备份文件不是有效的 SQLite 数据库"
exit 1
fi
# 3. 显示备份和当前状态
echo "[信息] 备份文件信息:"
ssh txjp "ls -lh ${BACKUP_PATH}"
echo ""
echo "[信息] 当前云端数据库状态:"
if [ "$SERVICE" = "issue" ]; then
ssh txjp "docker exec ${CONTAINER} node -e \"
const D=require('better-sqlite3');
const db=new D('/app/data/issue.db',{readonly:true});
console.log(' 工单数:', db.prepare('SELECT COUNT(*) as c FROM tickets').get().c);
db.close();
\""
else
ssh txjp "docker exec ${CONTAINER} node -e \"
const D=require('better-sqlite3');
const db=new D('/app/data/assets.db',{readonly:true});
console.log(' 设备数:', db.prepare('SELECT COUNT(*) as c FROM assets').get().c);
db.close();
\""
fi
# 4. 确认
echo ""
echo "[警告] 这将覆盖云端 ${CONTAINER} 数据库!"
read -p "确认恢复? (输入 yes 继续): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "已取消"
exit 0
fi
# 5. 云端安全备份当前数据库
local SAFE_NAME="before-restore-$(date +%Y%m%d_%H%M%S).db"
echo "[操作] 备份云端当前数据库..."
ssh txjp "docker cp ${CONTAINER}:${DB_PATH_IN_CONTAINER} /root/docker/db-backups/${SAFE_NAME}"
echo "[备份] 当前数据库已保存至云端: /root/docker/db-backups/${SAFE_NAME}"
# 6. 复制备份文件到容器
echo "[操作] 复制备份文件到容器..."
ssh txjp "docker cp ${BACKUP_PATH} ${CONTAINER}:${DB_PATH_IN_CONTAINER}"
# 7. 重启容器
echo "[操作] 重启容器 ${CONTAINER}..."
ssh txjp "cd /root/docker/${PROJECT_DIR} && docker compose restart ${CONTAINER}"
# 8. 验证
sleep 3
echo ""
echo "[验证] 恢复后数据:"
if [ "$SERVICE" = "issue" ]; then
ssh txjp "docker exec ${CONTAINER} node -e \"
const D=require('better-sqlite3');
const db=new D('/app/data/issue.db',{readonly:true});
console.log(' 工单数:', db.prepare('SELECT COUNT(*) as c FROM tickets').get().c);
db.close();
\""
else
ssh txjp "docker exec ${CONTAINER} node -e \"
const D=require('better-sqlite3');
const db=new D('/app/data/assets.db',{readonly:true});
console.log(' 设备数:', db.prepare('SELECT COUNT(*) as c FROM assets').get().c);
db.close();
\""
fi
echo ""
echo "恢复完成!"
}
# ---- 执行 ----
case $TARGET in
local) restore_local ;;
cloud) restore_cloud ;;
*)
echo "错误: 目标必须是 local 或 cloud"
exit 1
;;
esac