website-scripts/restore-db.sh

258 lines
8.4 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
# ============================================
# 数据库恢复脚本
#
# 用法:
# 恢复本地数据库: bash scripts/restore-db.sh local <服务名> <备份文件名>
# 恢复云端数据库: bash scripts/restore-db.sh cloud <服务名> <备份文件名>
#
# 服务名: issue / assets / ldap
#
# 示例:
# 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
# bash scripts/restore-db.sh cloud ldap ldap-2026-05-15_1200.db
# ============================================
set -e
# ---- 参数检查 ----
if [ $# -lt 3 ]; then
echo "用法: $0 <local|cloud> <issue|assets|ldap> <备份文件名>"
echo ""
echo "示例:"
echo " $0 local issue issue-2026-04-29_1753.db"
echo " $0 cloud ldap ldap-2026-05-15_1200.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"
;;
ldap)
PROJECT_DIR="ldap-ai"
CONTAINER="lldap"
DB_PATH_IN_CONTAINER="/data/users.db"
LOCAL_DB_PATH="/Users/niuniu/programs/docker/ldap-ai/data/lldap/users.db"
;;
*)
echo "错误: 服务名必须是 issue、assets 或 ldap"
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. 验证备份文件(通过容器内 better-sqlite3避免依赖宿主机或容器内 sqlite3 CLI
if ! ssh txjp "docker cp ${BACKUP_PATH} ${CONTAINER}:/tmp/restore-check.db 2>/dev/null && \
docker exec ${CONTAINER} node -e \"const D=require('better-sqlite3');const db=new D('/tmp/restore-check.db',{readonly:true});console.log(db.prepare('SELECT count(*) as c FROM sqlite_master').get().c);db.close();\" > /dev/null 2>&1 && \
docker exec ${CONTAINER} rm /tmp/restore-check.db"; 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. 停止容器(确保数据库不被写入,安全替换)
echo "[操作] 停止容器 ${CONTAINER}..."
ssh txjp "cd /root/docker/${PROJECT_DIR} && docker compose stop ${CONTAINER}"
# 6. 备份当前数据库(容器已停止,安全复制)
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}"
# 7. 替换数据库
echo "[操作] 复制备份文件到容器..."
ssh txjp "docker cp ${BACKUP_PATH} ${CONTAINER}:${DB_PATH_IN_CONTAINER}"
# 8. 删除残留 WAL/SHM 文件,防止旧日志与新数据库不匹配导致损坏
echo "[操作] 清理残留 WAL/SHM 文件..."
ssh txjp "docker exec ${CONTAINER} rm -f ${DB_PATH_IN_CONTAINER}-shm ${DB_PATH_IN_CONTAINER}-wal" 2>/dev/null || true
# 9. 启动容器
echo "[操作] 启动容器 ${CONTAINER}..."
ssh txjp "cd /root/docker/${PROJECT_DIR} && docker compose start ${CONTAINER}"
# 10. 验证
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