#!/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 " >&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 " exit 0