#!/usr/bin/env bash # 备份:pg_dump(自定义格式,整库,含 secret 表密文与任务引擎 schema)+ 映射清单 JSON + Agent 会话归档 # → $DATA_DIR/backups//。SECRET_MASTER_KEY 不在备份里,必须另行保管(没有它 secret 表无法解密)。 # # 容器内:docker compose exec app umt backup # 环境变量:DATABASE_URL(必需)、DATA_DIR(缺省 /data)、BACKUP_KEEP(保留最近 N 份,缺省不轮转)、 # UMT_BIN(缺省 /app/umt-server,开发机可指向 dist/umt-server) 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}" KEEP="${BACKUP_KEEP:-0}" ROOT="$DATA_DIR/backups" ts="$(date -u +%Y%m%dT%H%M%SZ)" dest="$ROOT/$ts" work="$ROOT/.$ts.partial" mkdir -p "$work" trap 'rm -rf "$work"' EXIT echo "backup: pg_dump → $dest/db.dump" pg_dump --format=custom --compress=6 --no-owner --no-privileges --file "$work/db.dump" "$DATABASE_URL" echo "backup: 导出映射清单 → $dest/mappings.json" "$BIN" --role export-mappings --out "$work/mappings.json" >&2 sessions="${AGENT_SESSION_DIR:-$DATA_DIR/agent-sessions}" if [ -d "$sessions" ] && [ -n "$(ls -A "$sessions" 2>/dev/null)" ]; then echo "backup: 归档 Agent 会话 → $dest/agent-sessions.tar.gz" tar -C "$sessions" -czf "$work/agent-sessions.tar.gz" . fi { echo "created_at=$ts" echo "pg_dump=$(pg_dump --version)" echo "server_version=$(psql "$DATABASE_URL" -Atqc 'show server_version' 2>/dev/null || echo unknown)" echo "master_key=NOT_INCLUDED(SECRET_MASTER_KEY 需另行保管)" } >"$work/manifest.txt" (cd "$work" && sha256sum db.dump mappings.json $([ -f agent-sessions.tar.gz ] && echo agent-sessions.tar.gz) >SHA256SUMS) mv "$work" "$dest" trap - EXIT echo "backup: 完成 $dest ($(du -sh "$dest" | cut -f1))" if [ "$KEEP" -gt 0 ] 2>/dev/null; then # 只轮转符合时间戳命名的目录,按名字(即时间)倒序保留最近 KEEP 份 mapfile -t all < <(find "$ROOT" -mindepth 1 -maxdepth 1 -type d -name '[0-9]*T[0-9]*Z' -printf '%f\n' | sort -r) for old in "${all[@]:$KEEP}"; do echo "backup: 轮转删除 $ROOT/$old" rm -rf "${ROOT:?}/$old" done fi