#!/usr/bin/env bash # Online SQLite backup using the .backup API. # Safe to run while the bot is live — SQLite's backup API acquires only a short # shared lock per page, never blocking the application for the full duration. # # Usage: # backup.sh --config /etc/mnemosyne/mnemosyne.conf [--keep-days N] # # Environment overrides: # MNEMOSYNE_BACKUP_DIR — destination directory (default: /var/backups/mnemosyne) # MNEMOSYNE_DB_PATH — database path (overrides config) set -euo pipefail # ── Defaults ───────────────────────────────────────────────────────────────── CONFIG_FILE="" KEEP_DAYS=30 BACKUP_DIR="${MNEMOSYNE_BACKUP_DIR:-/var/backups/mnemosyne}" # ── Argument parsing ───────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --config) CONFIG_FILE="$2"; shift 2 ;; --keep-days) KEEP_DAYS="$2"; shift 2 ;; *) echo "Unknown argument: $1" >&2; exit 1 ;; esac done # ── Resolve DB path ────────────────────────────────────────────────────────── # Priority: env var > config file > error if [[ -n "${MNEMOSYNE_DB_PATH:-}" ]]; then DB_PATH="$MNEMOSYNE_DB_PATH" elif [[ -n "$CONFIG_FILE" ]]; then if [[ ! -f "$CONFIG_FILE" ]]; then echo "Config file not found: $CONFIG_FILE" >&2 exit 1 fi # Parse db.path from the INI config (section [db], key path) DB_PATH="$(awk -F'=' '/^\[db\]/{in_db=1; next} /^\[/{in_db=0} in_db && /^[[:space:]]*path[[:space:]]*=/{gsub(/[[:space:]#].*/, "", $2); print $2; exit}' "$CONFIG_FILE")" DB_PATH="${DB_PATH// /}" # strip spaces if [[ -z "$DB_PATH" ]]; then echo "Could not read db.path from $CONFIG_FILE" >&2 exit 1 fi else echo "Provide --config or set MNEMOSYNE_DB_PATH" >&2 exit 1 fi if [[ ! -f "$DB_PATH" ]]; then echo "Database not found: $DB_PATH" >&2 exit 1 fi # ── Ensure backup directory exists ─────────────────────────────────────────── mkdir -p "$BACKUP_DIR" # ── Run backup ─────────────────────────────────────────────────────────────── TIMESTAMP="$(date +%Y%m%d-%H%M%S)" BACKUP_FILE="$BACKUP_DIR/mnemosyne-${TIMESTAMP}.db" # sqlite3 .backup uses the SQLite Online Backup API — consistent snapshot, # safe under concurrent writes, no application downtime required. sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'" # Verify the backup is a valid SQLite database if ! sqlite3 "$BACKUP_FILE" "PRAGMA integrity_check;" | grep -q "^ok$"; then echo "Backup integrity check failed: $BACKUP_FILE" >&2 rm -f "$BACKUP_FILE" exit 1 fi BYTES="$(wc -c < "$BACKUP_FILE")" echo "Backup written: $BACKUP_FILE (${BYTES} bytes)" # ── Prune old backups ───────────────────────────────────────────────────────── if [[ "$KEEP_DAYS" -gt 0 ]]; then PRUNED="$(find "$BACKUP_DIR" -maxdepth 1 -name 'mnemosyne-*.db' \ -mtime "+${KEEP_DAYS}" -print -delete | wc -l)" [[ "$PRUNED" -gt 0 ]] && echo "Pruned $PRUNED backup(s) older than ${KEEP_DAYS} days" fi