155 lines
6.1 KiB
Bash
Executable File
155 lines
6.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# africa-alert-backup.sh
|
|
# ----------------------
|
|
# Hot online backup of the Africa Alert PWA SQLite database.
|
|
#
|
|
# Uses the SQLite `.backup` command (NOT plain `cp`) so the snapshot is taken
|
|
# safely while the server is running and the database is in WAL mode. Plain
|
|
# `cp` of a WAL-mode DB can produce a corrupted or torn snapshot because the
|
|
# -wal and -shm sidecars are still being written to.
|
|
#
|
|
# This script is meant to be run from cron on the production host
|
|
# (Ubuntu 22.04). It is NOT the recommended path on Windows. On a Windows
|
|
# dev box, run `node scripts/backup/africa-alert-backup.js` instead — that
|
|
# script uses better-sqlite3's online backup API and does not need the
|
|
# sqlite3 CLI. If you MUST run this .sh on Windows and the sqlite3 CLI is
|
|
# missing, the script will automatically delegate to the Node fallback so
|
|
# Windows `bash` invocations still produce a usable backup.
|
|
#
|
|
# Required tooling on the host (Linux production):
|
|
# - bash 4+ (Ubuntu 22.04 ships 5.1)
|
|
# - sqlite3 CLI (apt: `sudo apt-get install -y sqlite3`)
|
|
# - write access to $BACKUP_DIR and $LOG_DIR
|
|
#
|
|
# Environment variables (all optional):
|
|
# DB_PATH Absolute path to the live SQLite database file.
|
|
# Default: /opt/africa-alert/server/data/school.db
|
|
# BACKUP_DIR Directory to write the timestamped .db snapshot into.
|
|
# Default: /backups
|
|
# LOG_DIR Directory to write the human-readable log into.
|
|
# Default: <project root>/logs (i.e. the directory that
|
|
# contains scripts/, two levels up from this script)
|
|
# RETAIN_DAILY Number of most-recent daily snapshots to keep.
|
|
# Default: 30
|
|
#
|
|
# Exit codes:
|
|
# 0 backup succeeded
|
|
# 1 configuration error (missing tool, unreadable DB, etc.)
|
|
# 2 sqlite3 .backup itself failed
|
|
# 3 retention sweep failed (the snapshot itself is still on disk)
|
|
|
|
set -eu
|
|
|
|
# --- Configuration -----------------------------------------------------------
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# scripts/backup/<this>.sh -> project root is two levels up.
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
|
|
DB_PATH="${DB_PATH:-/opt/africa-alert/server/data/school.db}"
|
|
BACKUP_DIR="${BACKUP_DIR:-/backups}"
|
|
LOG_DIR="${LOG_DIR:-${PROJECT_ROOT}/logs}"
|
|
RETAIN_DAILY="${RETAIN_DAILY:-30}"
|
|
|
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
|
BACKUP_FILE="${BACKUP_DIR}/school-${TIMESTAMP}.db"
|
|
LOG_FILE="${LOG_DIR}/backup.log"
|
|
|
|
# --- Pre-flight --------------------------------------------------------------
|
|
|
|
log() {
|
|
# Tee to stdout (so cron can mail it) and to the log file.
|
|
local msg="[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [backup] $*"
|
|
echo "${msg}" | tee -a "${LOG_FILE}" >&2
|
|
}
|
|
|
|
mkdir -p "${LOG_DIR}"
|
|
touch "${LOG_FILE}"
|
|
|
|
if ! command -v sqlite3 >/dev/null 2>&1; then
|
|
# Linux production hosts always have sqlite3 (apt: `sudo apt-get install
|
|
# -y sqlite3`). The absence of sqlite3 here means either a misconfigured
|
|
# production host, or a Windows dev box where the CLI is not installed
|
|
# by default. In either case, fall through to the Node fallback which
|
|
# uses the same primitive via better-sqlite3's online backup API.
|
|
log "WARN: sqlite3 CLI not found in PATH; delegating to Node fallback (scripts/backup/africa-alert-backup.js)"
|
|
if ! command -v node >/dev/null 2>&1; then
|
|
log "ERROR: neither sqlite3 nor node is available. Install one (apt: sudo apt-get install -y sqlite3) before retrying."
|
|
exit 1
|
|
fi
|
|
exec node "${SCRIPT_DIR}/africa-alert-backup.js"
|
|
fi
|
|
|
|
if [ ! -f "${DB_PATH}" ]; then
|
|
log "ERROR: DB_PATH does not exist or is not a regular file: ${DB_PATH}"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -r "${DB_PATH}" ]; then
|
|
log "ERROR: DB_PATH is not readable by $(id -un): ${DB_PATH}"
|
|
exit 1
|
|
fi
|
|
|
|
# --- Backup ------------------------------------------------------------------
|
|
# Idempotency note: mkdir -p on BACKUP_DIR is what makes this script safe to
|
|
# run before the operator has manually created the destination.
|
|
|
|
mkdir -p "${BACKUP_DIR}"
|
|
|
|
log "starting backup of ${DB_PATH} -> ${BACKUP_FILE}"
|
|
|
|
# `.backup` is a SQLite dot-command; we invoke it via the CLI's stdin
|
|
# mechanism. The trailing path is the destination file. SQLite creates the
|
|
# destination atomically and flushes the WAL before copying.
|
|
if ! sqlite3 "${DB_PATH}" ".backup '${BACKUP_FILE}'"; then
|
|
log "ERROR: sqlite3 .backup failed for ${DB_PATH}"
|
|
exit 2
|
|
fi
|
|
|
|
if [ ! -s "${BACKUP_FILE}" ]; then
|
|
log "ERROR: backup file was not created or is empty: ${BACKUP_FILE}"
|
|
exit 2
|
|
fi
|
|
|
|
# Copy the -wal and -shm sidecars if they exist, so the snapshot is fully
|
|
# self-contained for restore. The sidecars may be absent if the live DB
|
|
# was idle (WAL checkpointed) — that is fine.
|
|
for ext in wal shm; do
|
|
if [ -f "${DB_PATH}-${ext}" ]; then
|
|
cp "${DB_PATH}-${ext}" "${BACKUP_FILE}-${ext}"
|
|
log "copied sidecar ${DB_PATH}-${ext} -> ${BACKUP_FILE}-${ext}"
|
|
fi
|
|
done
|
|
|
|
# Compute a checksum for tamper detection / off-host transfer verification.
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
( cd "$(dirname "${BACKUP_FILE}")" && sha256sum "$(basename "${BACKUP_FILE}")" ) >> "${LOG_FILE}"
|
|
log "wrote sha256 checksum next to backup file"
|
|
fi
|
|
|
|
# --- Retention sweep ---------------------------------------------------------
|
|
# Keep the most recent $RETAIN_DAILY daily snapshots. Old ones beyond that
|
|
# are deleted. (Weekly retention is handled separately by docs/BACKUP.md
|
|
# using a different selector that mirrors to /backups/weekly/.)
|
|
|
|
DELETED=0
|
|
if [ -d "${BACKUP_DIR}" ]; then
|
|
# List backups newest-first, skip the first RETAIN_DAILY, delete the rest.
|
|
# `2>/dev/null || true` keeps set -e from blowing up if there are fewer
|
|
# than RETAIN_DAILY+1 files.
|
|
TO_DELETE="$(ls -1t "${BACKUP_DIR}"/school-*.db 2>/dev/null \
|
|
| tail -n +"$((RETAIN_DAILY + 1))" || true)"
|
|
if [ -n "${TO_DELETE}" ]; then
|
|
while IFS= read -r f; do
|
|
[ -z "${f}" ] && continue
|
|
rm -f -- "${f}" "${f}-wal" "${f}-shm" "${f}.sha256" 2>/dev/null || true
|
|
DELETED=$((DELETED + 1))
|
|
log "retention: deleted old backup ${f}"
|
|
done <<< "${TO_DELETE}"
|
|
fi
|
|
fi
|
|
|
|
log "done. file=${BACKUP_FILE} size=$(stat -c %s "${BACKUP_FILE}" 2>/dev/null || echo unknown) bytes retention_deleted=${DELETED}"
|
|
exit 0
|