将 dev/test/main 的 app 服务从占位容器改为 Dockerfile 多阶段构建,增加 healthcheck 与 .regulations 只读挂载;新增法规库备份/恢复脚本并同步 env-service 目录初始化与 gitignore。 Co-authored-by: Cursor <cursoragent@cursor.com>
258 lines
8.9 KiB
Bash
Executable File
258 lines
8.9 KiB
Bash
Executable File
#!/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 "============================================================"
|
||
|