Files
Agent-System/scripts/backup_regulations.sh
T
WeiCHENandCursor 8abf190e75 chore(deploy): 三环境 Compose 构建真实 app 镜像并补齐法规库运维
将 dev/test/main 的 app 服务从占位容器改为 Dockerfile 多阶段构建,增加 healthcheck 与 .regulations 只读挂载;新增法规库备份/恢复脚本并同步 env-service 目录初始化与 gitignore。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 19:53:24 +08:00

258 lines
8.9 KiB
Bash
Executable File
Raw 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.
#!/usr/bin/env bash
# =============================================================================
# 法规知识库完整备份脚本 (backup_regulations.sh)
#
# 作用:
# 1. 检查共享基础设施容器 shuimu-kb-shared 与数据库状态;
# 2. 采用 pg_dump -Fc 高压缩二进制格式导出 regulations 数据库(含向量与索引);
# 3. 镜像备份项目根目录的 .regulations 物理文件(支持目录克隆/同步及 tar 归档);
# 4. 生成 manifest.json 记录基准元数据、统计指标及 SHA256 签名。
#
# 用法:
# ./scripts/backup_regulations.sh <目标备份目录> [选项]
#
# 选项:
# --archive, -a 额外将 .regulations 打包为 regulations_files.tar.gz
# --help, -h 显示帮助信息
# =============================================================================
set -eo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
CONTAINER_NAME="shuimu-kb-shared"
DB_NAME="regulations"
DB_USER="regkb"
show_help() {
cat <<EOF
用法:
$0 <目标备份目录> [选项]
参数:
目标备份目录 必须指定,备份文件将保存至该目录下
选项:
--archive, -a 额外将 .regulations 打包为 regulations_files.tar.gz 压缩包
--help, -h 显示本帮助信息
示例:
$0 /Volumes/Backups/regulations_20260911
$0 ./backups/reg_backup --archive
EOF
exit 0
}
if [[ $# -lt 1 ]]; then
show_help
fi
TARGET_DIR=""
ARCHIVE_MODE=0
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
show_help
;;
--archive|-a)
ARCHIVE_MODE=1
shift
;;
*)
if [[ -z "${TARGET_DIR}" ]]; then
TARGET_DIR="$1"
else
echo "[错误] 未知参数: $1"
show_help
fi
shift
;;
esac
done
if [[ -z "${TARGET_DIR}" ]]; then
echo "[错误] 请指定目标备份目录!"
exit 1
fi
# 解析绝对路径
mkdir -p "${TARGET_DIR}"
TARGET_DIR="$(cd "${TARGET_DIR}" && pwd)"
echo "============================================================"
echo ">>> 开始执行法规知识库完整备份"
echo ">>> 项目根路径: ${ROOT_DIR}"
echo ">>> 目标备份目录: ${TARGET_DIR}"
echo "============================================================"
# 1. 检查环境与 Docker 容器
if ! command -v docker >/dev/null 2>&1; then
echo "[错误] 未找到 docker 命令,请先安装或启动 Docker!"
exit 1
fi
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "[警告] 容器 ${CONTAINER_NAME} 未运行,尝试启动共享基础设施..."
if [[ -f "${ROOT_DIR}/scripts/env-service.sh" ]]; then
"${ROOT_DIR}/scripts/env-service.sh" shared start
else
docker compose -f "${ROOT_DIR}/docker-compose.shared.yml" up -d
fi
fi
# 等待数据库就绪
echo "[等待] 检查数据库连接状态..."
for i in {1..30}; do
if docker exec -i "${CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; then
echo "[就绪] 数据库连接正常。"
break
fi
sleep 1
if [ "$i" -eq 30 ]; then
echo "[错误] 数据库连接超时,备份中止!"
exit 1
fi
done
# 2. 检查 .regulations 物理目录
REG_SOURCE_DIR="${ROOT_DIR}/.regulations"
if [[ ! -d "${REG_SOURCE_DIR}" ]]; then
echo "[错误] 项目根目录下未找到 .regulations 目录 (${REG_SOURCE_DIR})!"
exit 1
fi
# 3. 统计数据库基准指标
echo "[统计] 采集数据库元数据与统计指标..."
DOC_COUNT=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c "SELECT count(*) FROM reg_documents;" 2>/dev/null || echo "0")
CHUNK_COUNT=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c "SELECT count(*) FROM reg_chunks;" 2>/dev/null || echo "0")
NODE_COUNT=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c "SELECT count(*) FROM reg_nodes;" 2>/dev/null || echo "0")
DB_SIZE=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c "SELECT pg_size_pretty(pg_database_size('${DB_NAME}'));" 2>/dev/null || echo "0 MB")
echo " • 法规文档数 (reg_documents): ${DOC_COUNT}"
echo " • 法规切片数 (reg_chunks): ${CHUNK_COUNT}"
echo " • 大纲节点数 (reg_nodes): ${NODE_COUNT}"
echo " • 数据库当前体积: ${DB_SIZE}"
# 4. 导出数据库
DUMP_FILENAME="regulations_db.dump"
TMP_CONTAINER_DUMP="/tmp/${DUMP_FILENAME}"
DEST_DUMP_PATH="${TARGET_DIR}/${DUMP_FILENAME}"
echo "[导出] 正在执行 PostgreSQL 数据库导出 (pg_dump -Fc 高压缩格式)..."
START_TIME=$(date +%s)
docker exec "${CONTAINER_NAME}" pg_dump -U "${DB_USER}" -d "${DB_NAME}" -Fc -Z 6 -f "${TMP_CONTAINER_DUMP}"
docker cp "${CONTAINER_NAME}:${TMP_CONTAINER_DUMP}" "${DEST_DUMP_PATH}"
docker exec "${CONTAINER_NAME}" rm -f "${TMP_CONTAINER_DUMP}"
DUMP_DURATION=$(( $(date +%s) - START_TIME ))
DUMP_SIZE_HUMAN=$(ls -lh "${DEST_DUMP_PATH}" | awk '{print $5}')
DUMP_SIZE_BYTES=$(stat -f "%z" "${DEST_DUMP_PATH}" 2>/dev/null || stat -c "%s" "${DEST_DUMP_PATH}" 2>/dev/null || echo "0")
echo "[完成] 数据库导出成功,耗时: ${DUMP_DURATION} 秒,体积: ${DUMP_SIZE_HUMAN}"
# 计算哈希
echo "[校验] 计算数据库 Dump 文件的 SHA256 校验和..."
if command -v shasum >/dev/null 2>&1; then
DUMP_SHA256=$(shasum -a 256 "${DEST_DUMP_PATH}" | awk '{print $1}')
elif command -v sha256sum >/dev/null 2>&1; then
DUMP_SHA256=$(sha256sum "${DEST_DUMP_PATH}" | awk '{print $1}')
else
DUMP_SHA256="unavailable"
fi
echo " • SHA256: ${DUMP_SHA256}"
# 5. 备份物理目录 .regulations
echo "[备份] 正在同步 .regulations 物理文档与解析工作区..."
DEST_REG_DIR="${TARGET_DIR}/.regulations"
mkdir -p "${DEST_REG_DIR}"
# 统计原文件数量与体积
TOTAL_FILES=$(find "${REG_SOURCE_DIR}" -type f | wc -l | tr -d ' ')
DIR_SIZE_HUMAN=$(du -sh "${REG_SOURCE_DIR}" | awk '{print $1}')
echo " • 源文件总量: ${TOTAL_FILES} 个"
echo " • 目录总大小: ${DIR_SIZE_HUMAN}"
# 优先使用 macOS APFS clonefile (快速且零物理块拷贝)
if cp -R -p -c "${REG_SOURCE_DIR}/." "${DEST_REG_DIR}/" 2>/dev/null; then
echo "[完成] 已通过 APFS Copy-on-Write (零额外空间) 完成物理目录克隆镜像。"
else
# 跨磁盘或非 APFS 降级为 rsync / cp
if command -v rsync >/dev/null 2>&1; then
rsync -a --delete "${REG_SOURCE_DIR}/" "${DEST_REG_DIR}/"
else
cp -R -p "${REG_SOURCE_DIR}/." "${DEST_REG_DIR}/"
fi
echo "[完成] 物理目录同步完成。"
fi
ARCHIVE_TAR_FILE=""
ARCHIVE_TAR_SHA256=""
if [[ "${ARCHIVE_MODE}" -eq 1 ]]; then
echo "[归档] 正在将 .regulations 物理目录打包为 tar.gz..."
ARCHIVE_TAR_FILE="regulations_files.tar.gz"
ARCHIVE_TAR_PATH="${TARGET_DIR}/${ARCHIVE_TAR_FILE}"
tar -czf "${ARCHIVE_TAR_PATH}" -C "${ROOT_DIR}" .regulations
if command -v shasum >/dev/null 2>&1; then
ARCHIVE_TAR_SHA256=$(shasum -a 256 "${ARCHIVE_TAR_PATH}" | awk '{print $1}')
fi
echo "[完成] 归档包已生成: ${ARCHIVE_TAR_PATH}"
fi
# 6. 生成元数据清单 manifest.json
MANIFEST_FILE="${TARGET_DIR}/manifest.json"
echo "[元数据] 写入备份清单 ${MANIFEST_FILE}..."
NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
HOSTNAME_STR=$(hostname 2>/dev/null || echo "unknown")
cat <<EOF > "${MANIFEST_FILE}"
{
"manifest_version": "1.0",
"created_at": "${NOW_ISO}",
"project": "Chat-Agent-System",
"source_hostname": "${HOSTNAME_STR}",
"database": {
"container_name": "${CONTAINER_NAME}",
"database_name": "${DB_NAME}",
"user": "${DB_USER}",
"dump_file": "${DUMP_FILENAME}",
"dump_sha256": "${DUMP_SHA256}",
"dump_size_bytes": ${DUMP_SIZE_BYTES},
"dump_size_human": "${DUMP_SIZE_HUMAN}",
"document_count": ${DOC_COUNT},
"chunk_count": ${CHUNK_COUNT},
"node_count": ${NODE_COUNT},
"database_size": "${DB_SIZE}"
},
"regulations_files": {
"relative_path": ".regulations",
"total_files": ${TOTAL_FILES},
"directory_size": "${DIR_SIZE_HUMAN}",
"subdirectories": [
"intake",
"library",
"quarantine",
"work"
],
"archive_file": "${ARCHIVE_TAR_FILE}",
"archive_sha256": "${ARCHIVE_TAR_SHA256}"
}
}
EOF
echo "============================================================"
echo ">>> [SUCCESS] 法规知识库完整备份成功!"
echo ">>> 备份清单: ${MANIFEST_FILE}"
echo ">>> 数据库 Dump: ${DEST_DUMP_PATH} (${DUMP_SIZE_HUMAN})"
echo ">>> 物理文件目录: ${DEST_REG_DIR} (${TOTAL_FILES} 文件)"
if [[ -n "${ARCHIVE_TAR_FILE}" ]]; then
echo ">>> 物理文件归档: ${TARGET_DIR}/${ARCHIVE_TAR_FILE}"
fi
echo "============================================================"
echo "恢复提示: 在任意目标机器上拉取本工程后,只需执行:"
echo " ./scripts/restore_regulations.sh \"${TARGET_DIR}\""
echo "============================================================"