Files
ai_platform/scripts/pg-backup.sh
dsyoon 073a8343dd feat: xavis ai_platform 기능 이전 및 ncue 환경 전환
xavis 소스·DB 스키마·활용사례/F-Scan/프롬프트 라이브러리 등 기능 반영.
@xavis.co.kr → @ncue.net, 관리자 토큰 ncue-admin, 런타임 data/ Git 추적 제외.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:27:48 +09:00

182 lines
5.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# PostgreSQL 논리 백업 (pg_dump -Fc). .env의 DB_* 및 PG_BACKUP_* 사용.
# PG_BACKUP_SCOPE=all 이면 서버의 연결 가능·비템플릿 DB 전체를 각각 .dump 로 저장.
# 운영 서버 cron 예: 0 2 * * * cd /home/xavis/workspace/ai_platform && bash scripts/pg-backup.sh >> /var/log/pg-backup.log 2>&1
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=scripts/lib/load-env.sh
source "$REPO_ROOT/scripts/lib/load-env.sh"
load_project_env "$REPO_ROOT/.env"
: "${DB_HOST:?DB_HOST is required in .env}"
: "${DB_DATABASE:?DB_DATABASE is required in .env}"
: "${DB_USERNAME:?DB_USERNAME is required in .env}"
: "${DB_PASSWORD:?DB_PASSWORD is required in .env}"
DB_PORT="${DB_PORT:-5432}"
PG_BACKUP_DIR="${PG_BACKUP_DIR:-/home/xavis/workspace/backup/ai_platform}"
PG_BACKUP_RETENTION_DAYS="${PG_BACKUP_RETENTION_DAYS:-30}"
PG_BACKUP_SCOPE="${PG_BACKUP_SCOPE:-all}"
PG_BACKUP_GLOBALS="${PG_BACKUP_GLOBALS:-1}"
export PGHOST="$DB_HOST"
export PGPORT="$DB_PORT"
if ! command -v pg_dump >/dev/null 2>&1; then
echo "pg-backup: pg_dump not found. Install postgresql-client (apt) or postgresql (brew)." >&2
exit 1
fi
if ! command -v psql >/dev/null 2>&1; then
echo "pg-backup: psql not found. Install postgresql-client." >&2
exit 1
fi
log_ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
set_backup_credentials() {
if [[ -n "${PG_BACKUP_SUPERUSER_PASSWORD:-}" ]]; then
export PGUSER="${PG_BACKUP_SUPERUSER:-postgres}"
export PGPASSWORD="$PG_BACKUP_SUPERUSER_PASSWORD"
else
export PGUSER="$DB_USERNAME"
export PGPASSWORD="$DB_PASSWORD"
fi
}
list_all_databases() {
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -v ON_ERROR_STOP=1 -tAc \
"SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate ORDER BY datname;"
}
dump_database() {
local db_name="$1"
local dump_file="$2"
pg_dump \
-h "$PGHOST" \
-p "$PGPORT" \
-U "$PGUSER" \
-d "$db_name" \
-Fc \
--no-password \
-f "$dump_file"
}
backup_globals() {
if [[ "$PG_BACKUP_GLOBALS" != "1" ]]; then
return 0
fi
if [[ -z "${PG_BACKUP_SUPERUSER_PASSWORD:-}" ]]; then
echo "[$(log_ts)] globals skipped: set PG_BACKUP_SUPERUSER_PASSWORD for role/global backup." >&2
return 0
fi
local prev_user="$PGUSER" prev_pass="$PGPASSWORD"
export PGUSER="${PG_BACKUP_SUPERUSER:-postgres}"
export PGPASSWORD="$PG_BACKUP_SUPERUSER_PASSWORD"
pg_dumpall \
-h "$PGHOST" \
-p "$PGPORT" \
-U "$PGUSER" \
--globals-only \
--no-password \
-f "$RUN_DIR/00_globals.sql"
export PGUSER="$prev_user"
export PGPASSWORD="$prev_pass"
echo "[$(log_ts)] globals saved: $RUN_DIR/00_globals.sql"
}
set_backup_credentials
# cron 일 1회 기준: YYYYMMDD 폴더에 당일 백업 저장
STAMP="$(date +%Y%m%d)"
RUN_DIR="$PG_BACKUP_DIR/$STAMP"
mkdir -p "$RUN_DIR"
echo "[$(log_ts)] pg-backup start → $RUN_DIR (scope=${PG_BACKUP_SCOPE}, retention ${PG_BACKUP_RETENTION_DAYS} days)"
if [[ -z "${PG_BACKUP_SUPERUSER_PASSWORD:-}" && "$PG_BACKUP_SCOPE" == "all" ]]; then
echo "[$(log_ts)] note: PG_BACKUP_SUPERUSER_PASSWORD not set; backing up only databases visible to ${DB_USERNAME}." >&2
fi
backup_globals
declare -a TARGET_DBS=()
if [[ "$PG_BACKUP_SCOPE" == "single" ]]; then
TARGET_DBS=("$DB_DATABASE")
else
while IFS= read -r db; do
[[ -n "$db" ]] && TARGET_DBS+=("$db")
done < <(list_all_databases)
if [[ ${#TARGET_DBS[@]} -eq 0 ]]; then
echo "pg-backup: no databases found to backup." >&2
exit 1
fi
fi
MANIFEST="$RUN_DIR/00_manifest.txt"
{
echo "# pg-backup manifest $(log_ts)"
echo "scope=${PG_BACKUP_SCOPE}"
echo "host=${PGHOST}:${PGPORT}"
echo "user=${PGUSER}"
} > "$MANIFEST"
dump_ok=0
dump_fail=0
for db_name in "${TARGET_DBS[@]}"; do
dump_file="$RUN_DIR/${db_name}.dump"
echo "[$(log_ts)] dumping database: $db_name"
if dump_database "$db_name" "$dump_file"; then
bytes="$(wc -c < "$dump_file" | tr -d ' ')"
echo "[$(log_ts)] dump saved: $dump_file (${bytes} bytes)"
echo "${db_name}.dump ${bytes}" >> "$MANIFEST"
dump_ok=$((dump_ok + 1))
else
echo "[$(log_ts)] dump failed: $db_name" >&2
dump_fail=$((dump_fail + 1))
fi
done
if [[ "$dump_ok" -eq 0 ]]; then
echo "pg-backup: all database dumps failed." >&2
exit 1
fi
if [[ "$dump_fail" -gt 0 ]]; then
echo "[$(log_ts)] warning: ${dump_fail} database dump(s) failed, ${dump_ok} succeeded." >&2
fi
ln -sfn "$RUN_DIR" "$PG_BACKUP_DIR/latest"
prune_old_backups() {
local cutoff removed=0 base day entry
if date -d "1 day ago" +%Y%m%d >/dev/null 2>&1; then
cutoff=$(date -d "${PG_BACKUP_RETENTION_DAYS} days ago" +%Y%m%d)
else
cutoff=$(date -v-"${PG_BACKUP_RETENTION_DAYS}"d +%Y%m%d)
fi
shopt -s nullglob
for entry in "$PG_BACKUP_DIR"/*; do
[[ -d "$entry" ]] || continue
base=$(basename "$entry")
[[ "$base" == "latest" ]] && continue
if [[ "$base" =~ ^([0-9]{8}) ]]; then
day="${BASH_REMATCH[1]}"
if [[ "$day" < "$cutoff" ]]; then
rm -rf "$entry"
removed=$((removed + 1))
echo "[$(log_ts)] retention: removed $entry (backup date $day < cutoff $cutoff)"
fi
fi
done
shopt -u nullglob
echo "[$(log_ts)] retention: pruned ${removed} dir(s); keeping backups from ${cutoff} onward (${PG_BACKUP_RETENTION_DAYS} days)"
}
if [[ "$PG_BACKUP_RETENTION_DAYS" =~ ^[0-9]+$ ]] && [[ "$PG_BACKUP_RETENTION_DAYS" -gt 0 ]]; then
prune_old_backups
fi
echo "[$(log_ts)] pg-backup done: ${dump_ok} database(s), latest → $PG_BACKUP_DIR/latest"