151 lines
5.4 KiB
Bash
Executable File
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
|