| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- #!/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:-<empty>}"
- 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
|