xavis 소스·DB 스키마·활용사례/F-Scan/프롬프트 라이브러리 등 기능 반영. @xavis.co.kr → @ncue.net, 관리자 토큰 ncue-admin, 런타임 data/ Git 추적 제외. Co-authored-by: Cursor <cursoragent@cursor.com>
226 lines
6.3 KiB
Bash
Executable File
226 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# PostgreSQL 논리 복원 (pg_restore). custom format(.dump) 전용.
|
|
# 사용 예:
|
|
# bash scripts/pg-restore.sh /home/xavis/workspace/backup/ai_platform/latest/ai_web_platform.dump
|
|
# bash scripts/pg-restore.sh --all --globals --confirm /home/xavis/workspace/backup/ai_platform/latest/
|
|
# bash scripts/pg-restore.sh --test --confirm /home/xavis/workspace/backup/ai_platform/latest/ai_web_platform.dump
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: bash scripts/pg-restore.sh [options] <path-to.dump | backup-dir>
|
|
|
|
Options:
|
|
--all 백업 디렉터리 안의 *.dump 전체 복원 (00_globals.sql 있으면 --globals 권장)
|
|
--test 복원 대상 DB를 {dbname}_restore_test 로 생성 (단일 dump 또는 --all)
|
|
--clean pg_restore --clean --if-exists (기존 객체 삭제 후 복원, --test 미사용 시 위험)
|
|
--confirm 확인 없이 실행 (cron·자동화용; --test 없이 --clean 시 필수)
|
|
--globals 같은 디렉터리의 00_globals.sql 을 먼저 적용 (슈퍼유저 env 필요)
|
|
-h, --help 도움말
|
|
|
|
환경 변수 (.env):
|
|
DB_* 연결 정보
|
|
PG_BACKUP_SUPERUSER --globals 시 사용 (기본 postgres)
|
|
PG_BACKUP_SUPERUSER_PASSWORD
|
|
EOF
|
|
}
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
# shellcheck source=scripts/lib/load-env.sh
|
|
source "$REPO_ROOT/scripts/lib/load-env.sh"
|
|
|
|
MODE="prod"
|
|
CLEAN=0
|
|
CONFIRM=0
|
|
GLOBALS=0
|
|
RESTORE_ALL=0
|
|
TARGET_PATH=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--test) MODE="test"; shift ;;
|
|
--clean) CLEAN=1; shift ;;
|
|
--confirm) CONFIRM=1; shift ;;
|
|
--globals) GLOBALS=1; shift ;;
|
|
--all) RESTORE_ALL=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
-*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
|
*)
|
|
if [[ -n "$TARGET_PATH" ]]; then
|
|
echo "Unexpected argument: $1" >&2
|
|
exit 1
|
|
fi
|
|
TARGET_PATH="$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$TARGET_PATH" ]]; then
|
|
usage >&2
|
|
exit 1
|
|
fi
|
|
|
|
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}"
|
|
|
|
export PGHOST="$DB_HOST"
|
|
export PGPORT="$DB_PORT"
|
|
export PGUSER="$DB_USERNAME"
|
|
export PGPASSWORD="$DB_PASSWORD"
|
|
|
|
if ! command -v pg_restore >/dev/null 2>&1; then
|
|
echo "pg-restore: pg_restore not found. Install postgresql-client." >&2
|
|
exit 1
|
|
fi
|
|
|
|
log_ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
|
|
|
resolve_superuser_creds() {
|
|
SUPERUSER="${PG_BACKUP_SUPERUSER:-postgres}"
|
|
if [[ -n "${PG_BACKUP_SUPERUSER_PASSWORD:-}" ]]; then
|
|
SU_USER="$SUPERUSER"
|
|
SU_PASS="$PG_BACKUP_SUPERUSER_PASSWORD"
|
|
else
|
|
SU_USER="$DB_USERNAME"
|
|
SU_PASS="$DB_PASSWORD"
|
|
fi
|
|
}
|
|
|
|
apply_globals() {
|
|
local globals_file="$1"
|
|
if [[ ! -f "$globals_file" ]]; then
|
|
echo "pg-restore: globals file not found: $globals_file" >&2
|
|
exit 1
|
|
fi
|
|
: "${PG_BACKUP_SUPERUSER_PASSWORD:?PG_BACKUP_SUPERUSER_PASSWORD required for --globals}"
|
|
resolve_superuser_creds
|
|
export PGUSER="$SU_USER"
|
|
export PGPASSWORD="$SU_PASS"
|
|
echo "[$(log_ts)] applying globals: $globals_file"
|
|
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -v ON_ERROR_STOP=1 -f "$globals_file"
|
|
export PGUSER="$DB_USERNAME"
|
|
export PGPASSWORD="$DB_PASSWORD"
|
|
}
|
|
|
|
ensure_database() {
|
|
local db_name="$1"
|
|
resolve_superuser_creds
|
|
export PGUSER="$SU_USER"
|
|
export PGPASSWORD="$SU_PASS"
|
|
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -v ON_ERROR_STOP=1 -tc \
|
|
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$db_name' AND pid <> pg_backend_pid();" \
|
|
>/dev/null 2>&1 || true
|
|
if [[ "$MODE" == "test" ]]; then
|
|
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -v ON_ERROR_STOP=1 -c "DROP DATABASE IF EXISTS \"$db_name\";"
|
|
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -v ON_ERROR_STOP=1 -c \
|
|
"CREATE DATABASE \"$db_name\" OWNER \"$DB_USERNAME\";"
|
|
fi
|
|
export PGUSER="$DB_USERNAME"
|
|
export PGPASSWORD="$DB_PASSWORD"
|
|
}
|
|
|
|
restore_one_dump() {
|
|
local dump_path="$1"
|
|
local source_db
|
|
source_db="$(basename "$dump_path" .dump)"
|
|
local target_db="$source_db"
|
|
if [[ "$MODE" == "test" ]]; then
|
|
target_db="${source_db}_restore_test"
|
|
ensure_database "$target_db"
|
|
fi
|
|
|
|
local restore_args=(
|
|
-h "$PGHOST"
|
|
-p "$PGPORT"
|
|
-U "$PGUSER"
|
|
-d "$target_db"
|
|
--no-owner
|
|
--role="$DB_USERNAME"
|
|
-v
|
|
)
|
|
if [[ "$CLEAN" -eq 1 ]]; then
|
|
restore_args+=(--clean --if-exists)
|
|
fi
|
|
|
|
echo "[$(log_ts)] pg_restore → $target_db ($dump_path)"
|
|
pg_restore "${restore_args[@]}" "$dump_path"
|
|
echo "[$(log_ts)] pg-restore done: $target_db"
|
|
}
|
|
|
|
confirm_restore() {
|
|
local message="$1"
|
|
if [[ "$MODE" == "prod" && "$CLEAN" -eq 1 && "$CONFIRM" -ne 1 ]]; then
|
|
echo "pg-restore: --clean on production DB requires --confirm" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$CONFIRM" -eq 1 ]]; then
|
|
return 0
|
|
fi
|
|
echo "$message"
|
|
read -r -p "Continue? [y/N] " ans
|
|
[[ "$ans" == "y" || "$ans" == "Y" ]] || exit 0
|
|
}
|
|
|
|
if [[ -d "$TARGET_PATH" ]]; then
|
|
BACKUP_DIR="${TARGET_PATH%/}"
|
|
if [[ "$RESTORE_ALL" -ne 1 ]]; then
|
|
echo "pg-restore: path is a directory. Use --all to restore every *.dump inside." >&2
|
|
exit 1
|
|
fi
|
|
confirm_restore "About to restore ALL dumps in: $BACKUP_DIR on $DB_HOST:$PGPORT"
|
|
if [[ "$GLOBALS" -eq 1 ]]; then
|
|
apply_globals "$BACKUP_DIR/00_globals.sql"
|
|
fi
|
|
shopt -s nullglob
|
|
dumps=( "$BACKUP_DIR"/*.dump )
|
|
shopt -u nullglob
|
|
if [[ ${#dumps[@]} -eq 0 ]]; then
|
|
echo "pg-restore: no .dump files in $BACKUP_DIR" >&2
|
|
exit 1
|
|
fi
|
|
for dump in "${dumps[@]}"; do
|
|
restore_one_dump "$dump"
|
|
done
|
|
exit 0
|
|
fi
|
|
|
|
if [[ ! -f "$TARGET_PATH" ]]; then
|
|
echo "pg-restore: path not found: $TARGET_PATH" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$RESTORE_ALL" -eq 1 ]]; then
|
|
echo "pg-restore: --all requires a backup directory path." >&2
|
|
exit 1
|
|
fi
|
|
|
|
TARGET_DB="$DB_DATABASE"
|
|
if [[ "$MODE" == "test" ]]; then
|
|
TARGET_DB="${DB_DATABASE}_restore_test"
|
|
fi
|
|
|
|
confirm_restore "About to restore into database: $TARGET_DB on $DB_HOST:$PGPORT
|
|
Dump: $TARGET_PATH"
|
|
|
|
if [[ "$GLOBALS" -eq 1 ]]; then
|
|
apply_globals "$(dirname "$TARGET_PATH")/00_globals.sql"
|
|
fi
|
|
|
|
if [[ "$MODE" == "test" ]]; then
|
|
ensure_database "$TARGET_DB"
|
|
fi
|
|
|
|
restore_one_dump "$TARGET_PATH"
|
|
|
|
if [[ "$MODE" == "test" ]]; then
|
|
echo "Verify with: psql -h $DB_HOST -U $DB_USERNAME -d $TARGET_DB -c '\\dt'"
|
|
echo "Drop test DB: psql ... -c 'DROP DATABASE \"$TARGET_DB\";'"
|
|
fi
|