#!/usr/bin/env bash # PostgreSQL 백업/복구 공통 함수. bin/backup-db.sh, bin/restore-db.sh 에서만 source 한다. . "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh" # 배포 단위 호스트 잠금. 파일을 지우거나 교체하면 잠금이 갈라지므로 유지한다. db_lock() { umask 077 command -v flock >/dev/null || die "flock (util-linux)이 필요하다." exec {DB_LOCK_FD}>>"${DEPLOY_DIR}/.db-operations.lock" flock -n "${DB_LOCK_FD}" || die "다른 백업/복구가 실행 중이다. 나중에 다시 실행한다." } # 잠금을 보유한 호출자만 사용한다. 복구 전 안전 백업은 재잠금하지 않는다. db_write_archive() ( # ; subshell confines the cleanup trap local output="$1" temp_file umask 077 mkdir -p "$(dirname -- "${output}")" [ ! -e "${output}" ] && [ ! -L "${output}" ] || die "이미 존재하는 파일을 덮어쓰지 않는다: ${output}" temp_file="$(mktemp "${output}.part.XXXXXX")" trap 'rm -f -- "${temp_file}"' EXIT log "PostgreSQL 백업 시작: ${PG_CONTAINER} / ${POSTGRESQL_DB}" engine exec "${PG_CONTAINER}" sh -ceu ' export PGPASSWORD="$POSTGRES_PASSWORD" exec pg_dump -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc ' > "${temp_file}" db_validate_archive "${temp_file}" ln -- "${temp_file}" "${output}" || die "백업 파일 게시에 실패했다: ${output}" log "백업 완료: ${output}" ) db_init() { resolve_engine "${ENGINE_ARG:-docker}" load_env PROJECT="${COMPOSE_PROJECT_NAME:-pi-continuum}" if [ -n "${AUTO_BACKUP_EXPECT_PROJECT:-}" ]; then [ "${PROJECT}" = "${AUTO_BACKUP_EXPECT_PROJECT}" ] && \ [ "${POSTGRESQL_DB:-}" = "${AUTO_BACKUP_EXPECT_DB}" ] \ || die "자동 백업 설치 이후 프로젝트/DB 설정이 바뀌었다. 재설치가 필요하다." fi [ -n "${POSTGRESQL_DB:-}" ] || die "POSTGRESQL_DB 가 비어 있다." [ -n "${POSTGRESQL_ID:-}" ] || die "POSTGRESQL_ID 가 비어 있다." DB_ARCHIVE_STEM="$(printf '%s' "${PROJECT}_${POSTGRESQL_DB}" | LC_ALL=C tr -c 'A-Za-z0-9_.-' '_')" case "${POSTGRESQL_DB}" in postgres|template0|template1) die "시스템 DB 는 대상이 될 수 없다: ${POSTGRESQL_DB}" ;; esac db_find_container engine exec "${PG_CONTAINER}" sh -ceu ' [ "$POSTGRES_DB" = "$1" ] && [ "$POSTGRES_USER" = "$2" ] ' sh "${POSTGRESQL_DB}" "${POSTGRESQL_ID}" \ || die ".env 의 DB 이름/사용자가 실행 중인 PostgreSQL 컨테이너 설정과 다르다." } db_service_name() { # <컨테이너명> <서비스명> — run/Compose 양쪽 이름 local name="$1" service="$2" suffix [ "${name}" = "${PROJECT}-${service}" ] && return 0 case "${name}" in "${PROJECT}-${service}-"*) suffix="${name#"${PROJECT}-${service}-"}" ;; "${PROJECT}_${service}_"*) suffix="${name#"${PROJECT}_${service}_"}" ;; *) return 1 ;; esac case "${suffix}" in ''|*[!0-9]*) return 1 ;; *) return 0 ;; esac } db_running_containers() { engine ps --format '{{.Names}}' } db_find_container() { local name PG_CONTAINER="" while IFS= read -r name; do if db_service_name "${name}" postgres; then [ -z "${PG_CONTAINER}" ] || die "실행 중인 ${PROJECT} PostgreSQL 컨테이너가 여러 개다. 대상이 모호해 중단한다." PG_CONTAINER="${name}" fi done < <(db_running_containers) [ -n "${PG_CONTAINER}" ] || die "실행 중인 ${PROJECT} PostgreSQL 컨테이너를 찾지 못했다." } db_check_app_stopped() { local name service running=() while IFS= read -r name; do for service in nginx web worker-default worker-high worker-log scheduler processing bootstrap; do if db_service_name "${name}" "${service}"; then running+=("${name}") break fi done done < <(db_running_containers) if [ "${#running[@]}" -gt 0 ]; then warn "복구 전에 앱 컨테이너를 정상 종료해야 한다: ${running[*]}" die "실행 중인 앱이 있어 복구하지 않았다. 작업 완료를 기다린 뒤 위 컨테이너를 stop 하고 다시 실행한다." fi } db_validate_archive() { # [ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ] || die "읽을 수 있는 비어 있지 않은 백업 파일이 아니다: $1" engine exec -i "${PG_CONTAINER}" pg_restore --list < "$1" >/dev/null \ || die "pg_restore 가 백업 파일 목록을 읽지 못했다: $1" }