#!/usr/bin/env bash
|
# ─────────────────────────────────────────────────────────────────────────────
|
# JNPF PostgreSQL 定时备份
|
# - docker exec pg_dump 逐库导出(custom 格式 -Fc,已压缩、可 pg_restore 选择性还原)
|
# - 备份产物落 volume 外的宿主目录(BACKUP_DIR),容器/卷损坏也不丢
|
# - 每份导出用 pg_restore -l 校验可读性
|
# - 保留最近 RETENTION_DAYS 天,更早的自动删除
|
#
|
# 用法:
|
# ./backup.sh # 备份全部三库
|
# DATABASES="jnpf_init" ./backup.sh
|
#
|
# 参数(环境变量):
|
# CONTAINER PG 容器名 默认 cx-pg-customer1(2026-08-08 客户A改名,交付环境用时显式传 CONTAINER=cx-postgres)
|
# PGUSER 超级用户 默认 postgres
|
# DATABASES 待备份库(空格分隔) 默认 "jnpf_init jnpf_flow jnpf_xxljob nacos_config"
|
# BACKUP_DIR 宿主备份目录 默认 /data/pg-backup(务必在 volume 外)
|
# RETENTION_DAYS 保留天数 默认 14
|
#
|
# ── cron 定时(现场 crontab -e,每天 02:00)─────────────────────────────────
|
# 0 2 * * * /opt/jnpf/offline/migration/backup.sh >> /var/log/jnpf-pg-backup.log 2>&1
|
# 注意:宿主文件存储(.env JNPF_DATA_DIR 下 jnpf-resources/,上传附件所在)不在 PG 里,
|
# 需另行纳入文件级备份(rsync/tar 均可)
|
# 还原示例:
|
# gunzip -c jnpf_init_YYYYMMDD_HHMMSS.dump.gz > /tmp/x.dump # 若已 gzip
|
# docker exec -i cx-pg-customer1 pg_restore -U postgres -d jnpf_init --clean --if-exists < /tmp/x.dump
|
# (-Fc 自身已压缩,本脚本不再额外 gzip;下方 .dump 可直接 pg_restore)
|
# ─────────────────────────────────────────────────────────────────────────────
|
set -euo pipefail
|
|
CONTAINER="${CONTAINER:-cx-pg-customer1}"
|
PGUSER="${PGUSER:-postgres}"
|
# nacos_config 一并备份(配置中心内容只存 PG,codex review 2026-07-17 补)
|
DATABASES="${DATABASES:-jnpf_init jnpf_flow jnpf_xxljob nacos_config}"
|
# Windows/Git Bash 护栏:默认 /data/pg-backup 会映射进 Git 安装目录(静默备份到错误位置),
|
# 故 MSYS 环境必须显式传 BACKUP_DIR(如 /d/pg-backup)
|
case "$(uname -s)" in
|
MINGW*|MSYS*) [ -n "${BACKUP_DIR:-}" ] || {
|
printf '\033[1;31m[backup][err]\033[0m %s\n' \
|
"Windows/Git Bash 下必须显式指定 BACKUP_DIR,如:BACKUP_DIR=/d/pg-backup $0" >&2
|
exit 1
|
} ;;
|
esac
|
BACKUP_DIR="${BACKUP_DIR:-/data/pg-backup}"
|
RETENTION_DAYS="${RETENTION_DAYS:-14}"
|
TS="$(date +%Y%m%d_%H%M%S)"
|
|
log() { printf '\033[1;34m[backup]\033[0m %s\n' "$*"; }
|
warn() { printf '\033[1;33m[backup][warn]\033[0m %s\n' "$*"; }
|
die() { printf '\033[1;31m[backup][err]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
docker inspect "$CONTAINER" >/dev/null 2>&1 || die "容器 $CONTAINER 不在"
|
mkdir -p "$BACKUP_DIR"
|
|
fail=0
|
for db in $DATABASES; do
|
out="$BACKUP_DIR/${db}_${TS}.dump"
|
log "pg_dump ${db} → $(basename "$out")"
|
# -Fc custom 格式;失败则删残缺文件并计入失败
|
if docker exec "$CONTAINER" pg_dump -U "$PGUSER" -Fc -d "$db" > "$out" 2>/dev/null; then
|
# 校验:pg_restore -l 能列出目录即视为可读
|
if docker exec -i "$CONTAINER" pg_restore -l < "$out" >/dev/null 2>&1; then
|
log " ✓ ${db} $(du -h "$out" | cut -f1) 校验通过"
|
else
|
warn " ✗ ${db} 导出文件 pg_restore -l 校验失败"; fail=$((fail+1))
|
fi
|
else
|
warn " ✗ ${db} pg_dump 失败"; rm -f "$out"; fail=$((fail+1))
|
fi
|
done
|
|
log "清理 ${RETENTION_DAYS} 天前的旧备份 …"
|
find "$BACKUP_DIR" -maxdepth 1 -name '*.dump' -type f -mtime "+${RETENTION_DAYS}" -print -delete | while read -r f; do
|
log " 删除 $(basename "$f")"
|
done
|
|
log "当前备份目录占用:$(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1)"
|
[ "$fail" = 0 ] || die "有 ${fail} 个库备份失败,检查日志"
|
log "备份完成 ✓(${BACKUP_DIR})"
|