#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=wp13-common.sh source "${SCRIPT_DIR}/wp13-common.sh" usage() { cat <<'EOF' Usage: restore.sh --backup-dir PATH --project-name NAME --confirm-project NAME [options] Restore a WP13 backup into a new, empty Compose project. Same-project restore is intentionally blocked; use a new disaster-recovery project and cut over only after validation. Options: --backup-dir PATH Verified WP13 backup directory. --compose-file PATH Compose file. --env-file PATH Optional isolated port/environment file. --project-name NAME New destination project. --confirm-project NAME Must exactly match --project-name. --start Start the restored project after volume extraction. --timeout SECONDS Health timeout (default: 600). --help Show this help. EOF } ROOT="$(wp13_default_root)" BACKUP_DIR="" COMPOSE_FILE="" ENV_FILE="" PROJECT_NAME="" CONFIRM_PROJECT="" START_PROJECT=0 TIMEOUT_SECONDS=600 while [[ $# -gt 0 ]]; do case "$1" in --backup-dir) BACKUP_DIR="$2"; shift 2 ;; --compose-file) COMPOSE_FILE="$2"; shift 2 ;; --env-file) ENV_FILE="$2"; shift 2 ;; --project-name) PROJECT_NAME="$2"; shift 2 ;; --confirm-project) CONFIRM_PROJECT="$2"; shift 2 ;; --start) START_PROJECT=1; shift ;; --timeout) TIMEOUT_SECONDS="$2"; shift 2 ;; --help|-h) usage; exit 0 ;; *) wp13_die "unknown argument: $1" ;; esac done [[ -n "${PROJECT_NAME}" ]] || wp13_die "--project-name is required" [[ -n "${CONFIRM_PROJECT}" && "${CONFIRM_PROJECT}" == "${PROJECT_NAME}" ]] \ || wp13_die "project confirmation does not exactly match --project-name" [[ -n "${BACKUP_DIR}" && -d "${BACKUP_DIR}" ]] \ || wp13_die "backup directory not found: ${BACKUP_DIR:-}" BACKUP_DIR="$(cd "${BACKUP_DIR}" && pwd)" COMPOSE_FILE="${COMPOSE_FILE:-${ROOT}/deploy/docker/docker-compose.yml}" [[ -f "${COMPOSE_FILE}" ]] || wp13_die "compose file not found: ${COMPOSE_FILE}" wp13_verify_checksums "${BACKUP_DIR}" MANIFEST="${BACKUP_DIR}/backup-manifest.txt" [[ -f "${MANIFEST}" ]] || wp13_die "backup manifest not found" SOURCE_PROJECT="$(awk -F= '$1=="source_project" {print $2; exit}' "${MANIFEST}")" [[ -n "${SOURCE_PROJECT}" ]] || wp13_die "backup source project is missing" [[ "${PROJECT_NAME}" != "${SOURCE_PROJECT}" ]] \ || wp13_die "same-project restore is blocked: ${PROJECT_NAME}" wp13_require_command docker wp13_assert_no_project_containers "${PROJECT_NAME}" if docker volume ls \ --filter "label=com.docker.compose.project=${PROJECT_NAME}" \ --format '{{.Name}}' | grep -q .; then wp13_die "destination project already has labelled volumes: ${PROJECT_NAME}" fi while IFS= read -r line; do logical_name="$(printf '%s' "${line}" | awk -F'[=|]' '{print $2}')" archive_relative="$(printf '%s' "${line}" | awk -F'[=|]' '{print $4}')" [[ -n "${logical_name}" && -n "${archive_relative}" ]] \ || wp13_die "invalid volume manifest entry: ${line}" archive_path="${BACKUP_DIR}/${archive_relative}" [[ -f "${archive_path}" ]] || wp13_die "volume archive not found: ${archive_relative}" destination_name="${PROJECT_NAME}_${logical_name}" wp13_log "restoring volume ${logical_name} as ${destination_name}" docker volume create \ --label "com.docker.compose.project=${PROJECT_NAME}" \ --label "com.docker.compose.volume=${logical_name}" \ "${destination_name}" >/dev/null docker run --rm \ -v "${destination_name}:/volume" \ -v "${BACKUP_DIR}:/backup:ro" \ postgres:16-alpine \ sh -c "cd /volume && tar -xzf '/backup/${archive_relative}'" done < <(grep '^volume=' "${MANIFEST}") wp13_log "volume restore completed for ${PROJECT_NAME}" if [[ "${START_PROJECT}" -eq 1 ]]; then wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \ up -d --no-build wp13_wait_for_project "${PROJECT_NAME}" "${TIMEOUT_SECONDS}" current="$( wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \ exec -T backend alembic -c alembic.ini current \ | awk 'NF {print $1; exit}' )" head="$( wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \ exec -T backend alembic -c alembic.ini heads \ | awk 'NF {print $1; exit}' )" [[ -n "${current}" && "${current}" == "${head}" ]] \ || wp13_die "restored database migration mismatch: current=${current:-unknown}, head=${head:-unknown}" wp13_log "restored project is healthy at migration ${current}" fi