feat(ops): W4-B single-container packaging, DATA_DIR, backup/restore
- DATA_DIR (/data: agent-sessions, backups, exports, tmp); entrypoint chowns it to PUID/PGID then drops privileges via gosu; /app binary stays root read-only - compile with --allow-read --allow-write so mounted library roots are writable - preflight: DATA_DIR/session dir must be writable (fatal); enabled library roots and downloader path mappings checked read/write (warn + audit) - --role export-mappings (mapping manifest JSON, --compare row counts) and --role read-only-on (automation.readOnlyReconcile=true + audit) - scripts/backup.sh (pg_dump custom + mappings + agent sessions, SHA256SUMS, BACKUP_KEEP rotation) and scripts/restore.sh (checksum, refuses with live connections, psql single-transaction replay compatible with PG16, read-only reconcile on, row-count verification) - compose: app + postgres only, data volume, env example with key generation - docs: runbook deploy.md, backup-restore.md with verification record Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8c6a922cfb
commit
34e8751b45
18 files changed
+918
-43
No files matched your search
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# 恢复:pg_restore 覆盖当前库 → 打开只读对账(automation.readOnlyReconcile=true)→ 映射清单行数与备份比对。
|
||||
# 会删除并重建备份里的全部对象。先停掉 app 再做:
|
||||
# docker compose stop app
|
||||
# docker compose run --rm --no-deps app restore /data/backups/<时间戳> --yes
|
||||
# docker compose up -d app
|
||||
# 选项:--yes 跳过确认;--force 即使库里还有其它连接也继续(不建议)。
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$(id -u)" = "0" ] && command -v gosu >/dev/null 2>&1; then
|
||||
exec gosu "${PUID:-10001}:${PGID:-10001}" "$0" "$@"
|
||||
fi
|
||||
|
||||
: "${DATABASE_URL:?需要 DATABASE_URL}"
|
||||
DATA_DIR="${DATA_DIR:-/data}"
|
||||
BIN="${UMT_BIN:-/app/umt-server}"
|
||||
|
||||
src="" yes=0 force=0
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--yes) yes=1 ;;
|
||||
--force) force=1 ;;
|
||||
-*) echo "restore: 未知选项 $a" >&2; exit 2 ;;
|
||||
*) src="$a" ;;
|
||||
esac
|
||||
done
|
||||
if [ -z "$src" ]; then
|
||||
echo "用法: restore <备份目录> [--yes] [--force] 可用备份:" >&2
|
||||
ls -1 "$DATA_DIR/backups" 2>/dev/null >&2 || true
|
||||
exit 2
|
||||
fi
|
||||
case "$src" in /*) ;; *) [ -d "$src" ] || src="$DATA_DIR/backups/$src" ;; esac
|
||||
for f in db.dump mappings.json; do
|
||||
[ -f "$src/$f" ] || { echo "restore: $src 缺少 $f" >&2; exit 2; }
|
||||
done
|
||||
if [ -f "$src/SHA256SUMS" ]; then
|
||||
(cd "$src" && sha256sum --quiet -c SHA256SUMS) || { echo "restore: 校验和不符,备份可能损坏" >&2; exit 2; }
|
||||
fi
|
||||
|
||||
others="$(psql "$DATABASE_URL" -Atqc "select count(*) from pg_stat_activity where datname = current_database() and pid <> pg_backend_pid()")"
|
||||
if [ "$others" != "0" ] && [ "$force" -ne 1 ]; then
|
||||
echo "restore: 数据库还有 $others 个其它连接(app 是否仍在运行?)。先 docker compose stop app,或加 --force" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [ "$yes" -ne 1 ]; then
|
||||
read -r -p "将用 $src 覆盖当前数据库,输入 yes 继续: " ans
|
||||
[ "$ans" = "yes" ] || { echo "已取消"; exit 1; }
|
||||
fi
|
||||
|
||||
echo "restore: pg_restore ← $src/db.dump"
|
||||
# 经 psql 回放而不是 pg_restore 直连:镜像里是 17 版客户端,它会输出 16 版服务端不认识的
|
||||
# SET transaction_timeout;过滤掉这一行即可兼容 16。单事务 + ON_ERROR_STOP:失败整体回滚。
|
||||
pg_restore --clean --if-exists --no-owner --no-privileges --file - "$src/db.dump" \
|
||||
| sed '/^SET transaction_timeout = /d' \
|
||||
| psql -X -q -v ON_ERROR_STOP=1 --single-transaction -d "$DATABASE_URL" >/dev/null
|
||||
|
||||
if [ -f "$src/agent-sessions.tar.gz" ]; then
|
||||
sessions="${AGENT_SESSION_DIR:-$DATA_DIR/agent-sessions}"
|
||||
mkdir -p "$sessions"
|
||||
echo "restore: 还原 Agent 会话 → $sessions"
|
||||
tar -C "$sessions" -xzf "$src/agent-sessions.tar.gz"
|
||||
fi
|
||||
|
||||
"$BIN" --role read-only-on --reason "restore $(basename "$src")" >&2
|
||||
|
||||
check="$DATA_DIR/tmp/restore-check-$(date -u +%Y%m%dT%H%M%SZ).json"
|
||||
set +e
|
||||
"$BIN" --role export-mappings --out "$check" --compare "$src/mappings.json" >&2
|
||||
cmp=$?
|
||||
set -e
|
||||
|
||||
cat <<MSG
|
||||
|
||||
==================================================================
|
||||
恢复完成:$(basename "$src")
|
||||
映射清单行数比对:$([ "$cmp" -eq 0 ] && echo "一致" || echo "不一致(见上方日志,退出码 $cmp)")
|
||||
只读对账模式已打开(automation.readOnlyReconcile = true):
|
||||
自动下载/整理/调度与 Agent 自动执行均被拒绝。
|
||||
恢复后先对账再关闭只读:
|
||||
1. 核对下载器任务与真实文件(控制台 → 下载 / 整理记录,库根"测试")
|
||||
2. 确认 SECRET_MASTER_KEY 与备份时一致(站点/下载器凭证能解密)
|
||||
3. 确认无误后在 设置 → 自动化 关闭"只读对账",
|
||||
或 PATCH /api/v1/settings {"automation":{"readOnlyReconcile":false}}
|
||||
==================================================================
|
||||
MSG
|
||||
exit "$cmp"
|
||||
Reference in new issue
Block a user