geocrop-platform./apps/nextgen/scripts/backup/restore.sh

151 lines
5.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# restore.sh
# ----------
# Restore the Africa Alert PWA SQLite database from a backup file.
#
# This is a DESTRUCTIVE operation: it overwrites the live database
# (and its WAL/SHM sidecars) with the contents of the backup file.
# The server MUST be stopped before running this, otherwise the running
# Node process will keep writing to the old (now-orphaned) inode and
# any new writes will be lost on restart.
#
# Usage:
# DB_PATH=/opt/africa-alert/server/data/school.db \
# ./restore.sh /backups/school-20260716-120000.db
#
# This script is meant to be run on the production Ubuntu 22.04 host.
# It is NOT a Windows script. On a Windows dev box, only
# `bash -n restore.sh` (syntax check) is meaningful.
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
DB_PATH="${DB_PATH:-/opt/africa-alert/server/data/school.db}"
LOG_DIR="${LOG_DIR:-${PROJECT_ROOT}/logs}"
LOG_FILE="${LOG_DIR}/backup.log"
# --- Pre-flight --------------------------------------------------------------
if [ $# -lt 1 ]; then
echo "usage: $0 <backup-file>" >&2
echo " e.g. $0 /backups/school-20260716-120000.db" >&2
exit 64
fi
BACKUP_FILE="$1"
log() {
local msg="[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [restore] $*"
echo "${msg}" | tee -a "${LOG_FILE}" >&2
}
if [ ! -f "${BACKUP_FILE}" ]; then
log "ERROR: backup file not found: ${BACKUP_FILE}"
exit 1
fi
if [ ! -s "${BACKUP_FILE}" ]; then
log "ERROR: backup file is empty: ${BACKUP_FILE}"
exit 1
fi
# Make sure the destination's parent directory exists; in production this
# is /opt/africa-alert/server/data which the installer already created,
# but a custom DB_PATH may need a parent.
DB_PARENT="$(dirname "${DB_PATH}")"
if [ ! -d "${DB_PARENT}" ]; then
log "ERROR: DB_PATH parent directory does not exist: ${DB_PARENT}"
exit 1
fi
# --- Confirmation prompt -----------------------------------------------------
# Non-interactive host (e.g. piped from cron or CI) must explicitly
# opt in with RESTORE_CONFIRM=yes. Otherwise we require a human
# y/N at a TTY to prevent accidental overwrites.
if [ -t 0 ]; then
echo "============================================================"
echo " AFRICA ALERT — DATABASE RESTORE (DESTRUCTIVE)"
echo "============================================================"
echo " Backup file : ${BACKUP_FILE} ($(stat -c %s "${BACKUP_FILE}") bytes)"
echo " Live DB : ${DB_PATH}"
echo " Sidecars : ${DB_PATH}-wal ${DB_PATH}-shm (will be overwritten too)"
echo "============================================================"
printf "Type 'yes' to overwrite the live database: "
read -r CONFIRM
else
if [ "${RESTORE_CONFIRM:-}" != "yes" ]; then
log "ERROR: refusing to restore non-interactively without RESTORE_CONFIRM=yes"
exit 1
fi
CONFIRM="yes"
fi
if [ "${CONFIRM}" != "yes" ]; then
log "aborted by operator (confirmation not 'yes')"
exit 1
fi
# --- Stop the server if we can ----------------------------------------------
# Best-effort: if systemctl is present, stop the service so the running
# Node process releases its file handles. If the operator already stopped
# it manually, this is a no-op.
if command -v systemctl >/dev/null 2>&1; then
if systemctl list-unit-files 2>/dev/null | grep -q '^africa-alert\.service'; then
log "stopping africa-alert service via systemctl"
systemctl stop africa-alert.service || true
fi
fi
# --- Snapshot the pre-restore DB to a safe name ------------------------------
# If something goes wrong mid-restore, we want a "before" copy on disk so
# the operator can attempt a manual recovery. This is best-effort; if it
# fails we log and continue (the operator can re-run the backup cron to
# make a fresh safety net).
if [ -f "${DB_PATH}" ]; then
SAFETY="${DB_PATH}.pre-restore-$(date +%Y%m%d-%H%M%S)"
if cp -p "${DB_PATH}" "${SAFETY}" 2>/dev/null; then
log "safety snapshot: ${SAFETY}"
else
log "WARN: could not write safety snapshot; continuing anyway"
fi
fi
# --- Overwrite ---------------------------------------------------------------
# IMPORTANT: SQLite WAL mode keeps -wal and -shm sidecars alongside the
# main .db file. If the backup was taken while the DB was busy, the
# snapshot includes those sidecars (the backup script copies them too).
# If those sidecars exist next to the backup, copy them over the live
# sidecars. If not, the live sidecars must be REMOVED, otherwise SQLite
# on next open will replay stale WAL frames from before the restore
# and silently corrupt the freshly-restored database.
log "restoring ${BACKUP_FILE} -> ${DB_PATH}"
cp -f "${BACKUP_FILE}" "${DB_PATH}"
for ext in wal shm; do
if [ -f "${BACKUP_FILE}-${ext}" ]; then
cp -f "${BACKUP_FILE}-${ext}" "${DB_PATH}-${ext}"
log "restored sidecar ${DB_PATH}-${ext}"
else
# Backup has no sidecar (idle-time snapshot) — drop any stale one
# on the live DB so SQLite does not replay pre-restore frames.
if [ -f "${DB_PATH}-${ext}" ]; then
rm -f "${DB_PATH}-${ext}"
log "removed stale live sidecar ${DB_PATH}-${ext} (no matching backup sidecar)"
fi
fi
done
# --- Restart the server if we stopped it ------------------------------------
if command -v systemctl >/dev/null 2>&1; then
if systemctl list-unit-files 2>/dev/null | grep -q '^africa-alert\.service'; then
log "starting africa-alert service via systemctl"
systemctl start africa-alert.service || true
fi
fi
log "restore complete. next step: run ./verify-restore.sh <backup-file>"
exit 0