Files
UHD_Media_Tool/scripts/backup.sh
T
rootandClaude Opus 5.5 34e8751b45 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]>
2026-09-27 21:27:55 +08:00

58 lines
2.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# 备份:pg_dump(自定义格式,整库,含 secret 表密文与任务引擎 schema)+ 映射清单 JSON + Agent 会话归档
# → $DATA_DIR/backups/<UTC 时间戳>/。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