restore.sh 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. # shellcheck source=wp13-common.sh
  5. source "${SCRIPT_DIR}/wp13-common.sh"
  6. usage() {
  7. cat <<'EOF'
  8. Usage: restore.sh --backup-dir PATH --project-name NAME --confirm-project NAME [options]
  9. Restore a WP13 backup into a new, empty Compose project. Same-project restore
  10. is intentionally blocked; use a new disaster-recovery project and cut over
  11. only after validation.
  12. Options:
  13. --backup-dir PATH Verified WP13 backup directory.
  14. --compose-file PATH Compose file.
  15. --env-file PATH Optional isolated port/environment file.
  16. --project-name NAME New destination project.
  17. --confirm-project NAME Must exactly match --project-name.
  18. --start Start the restored project after volume extraction.
  19. --timeout SECONDS Health timeout (default: 600).
  20. --help Show this help.
  21. EOF
  22. }
  23. ROOT="$(wp13_default_root)"
  24. BACKUP_DIR=""
  25. COMPOSE_FILE=""
  26. ENV_FILE=""
  27. PROJECT_NAME=""
  28. CONFIRM_PROJECT=""
  29. START_PROJECT=0
  30. TIMEOUT_SECONDS=600
  31. while [[ $# -gt 0 ]]; do
  32. case "$1" in
  33. --backup-dir) BACKUP_DIR="$2"; shift 2 ;;
  34. --compose-file) COMPOSE_FILE="$2"; shift 2 ;;
  35. --env-file) ENV_FILE="$2"; shift 2 ;;
  36. --project-name) PROJECT_NAME="$2"; shift 2 ;;
  37. --confirm-project) CONFIRM_PROJECT="$2"; shift 2 ;;
  38. --start) START_PROJECT=1; shift ;;
  39. --timeout) TIMEOUT_SECONDS="$2"; shift 2 ;;
  40. --help|-h) usage; exit 0 ;;
  41. *) wp13_die "unknown argument: $1" ;;
  42. esac
  43. done
  44. [[ -n "${PROJECT_NAME}" ]] || wp13_die "--project-name is required"
  45. [[ -n "${CONFIRM_PROJECT}" && "${CONFIRM_PROJECT}" == "${PROJECT_NAME}" ]] \
  46. || wp13_die "project confirmation does not exactly match --project-name"
  47. [[ -n "${BACKUP_DIR}" && -d "${BACKUP_DIR}" ]] \
  48. || wp13_die "backup directory not found: ${BACKUP_DIR:-<empty>}"
  49. BACKUP_DIR="$(cd "${BACKUP_DIR}" && pwd)"
  50. COMPOSE_FILE="${COMPOSE_FILE:-${ROOT}/deploy/docker/docker-compose.yml}"
  51. [[ -f "${COMPOSE_FILE}" ]] || wp13_die "compose file not found: ${COMPOSE_FILE}"
  52. wp13_verify_checksums "${BACKUP_DIR}"
  53. MANIFEST="${BACKUP_DIR}/backup-manifest.txt"
  54. [[ -f "${MANIFEST}" ]] || wp13_die "backup manifest not found"
  55. SOURCE_PROJECT="$(awk -F= '$1=="source_project" {print $2; exit}' "${MANIFEST}")"
  56. [[ -n "${SOURCE_PROJECT}" ]] || wp13_die "backup source project is missing"
  57. [[ "${PROJECT_NAME}" != "${SOURCE_PROJECT}" ]] \
  58. || wp13_die "same-project restore is blocked: ${PROJECT_NAME}"
  59. wp13_require_command docker
  60. wp13_assert_no_project_containers "${PROJECT_NAME}"
  61. if docker volume ls \
  62. --filter "label=com.docker.compose.project=${PROJECT_NAME}" \
  63. --format '{{.Name}}' | grep -q .; then
  64. wp13_die "destination project already has labelled volumes: ${PROJECT_NAME}"
  65. fi
  66. while IFS= read -r line; do
  67. logical_name="$(printf '%s' "${line}" | awk -F'[=|]' '{print $2}')"
  68. archive_relative="$(printf '%s' "${line}" | awk -F'[=|]' '{print $4}')"
  69. [[ -n "${logical_name}" && -n "${archive_relative}" ]] \
  70. || wp13_die "invalid volume manifest entry: ${line}"
  71. archive_path="${BACKUP_DIR}/${archive_relative}"
  72. [[ -f "${archive_path}" ]] || wp13_die "volume archive not found: ${archive_relative}"
  73. destination_name="${PROJECT_NAME}_${logical_name}"
  74. wp13_log "restoring volume ${logical_name} as ${destination_name}"
  75. docker volume create \
  76. --label "com.docker.compose.project=${PROJECT_NAME}" \
  77. --label "com.docker.compose.volume=${logical_name}" \
  78. "${destination_name}" >/dev/null
  79. docker run --rm \
  80. -v "${destination_name}:/volume" \
  81. -v "${BACKUP_DIR}:/backup:ro" \
  82. postgres:16-alpine \
  83. sh -c "cd /volume && tar -xzf '/backup/${archive_relative}'"
  84. done < <(grep '^volume=' "${MANIFEST}")
  85. wp13_log "volume restore completed for ${PROJECT_NAME}"
  86. if [[ "${START_PROJECT}" -eq 1 ]]; then
  87. wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \
  88. up -d --no-build
  89. wp13_wait_for_project "${PROJECT_NAME}" "${TIMEOUT_SECONDS}"
  90. current="$(
  91. wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \
  92. exec -T backend alembic -c alembic.ini current \
  93. | awk 'NF {print $1; exit}'
  94. )"
  95. head="$(
  96. wp13_compose "${COMPOSE_FILE}" "${PROJECT_NAME}" "${ENV_FILE}" \
  97. exec -T backend alembic -c alembic.ini heads \
  98. | awk 'NF {print $1; exit}'
  99. )"
  100. [[ -n "${current}" && "${current}" == "${head}" ]] \
  101. || wp13_die "restored database migration mismatch: current=${current:-unknown}, head=${head:-unknown}"
  102. wp13_log "restored project is healthy at migration ${current}"
  103. fi