#!/bin/bash # backup-sqlite.sh — Safe hot backup of the XAMXAM SQLite database. # # Uses sqlite3's .backup command (WAL-safe) then gzip-compresses. # Prunes backups older than RETENTION_DAYS. # # Usage: # /usr/local/bin/backup-sqlite.sh # default: 30 days # RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh # 90 days # # Expected to be run from cron: # 0 * * * * /usr/local/bin/backup-sqlite.sh >> /var/log/xamxam-backup-$(date +%Y-%m-%d).log 2>&1 # 0 2 * * * RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/xamxam-backup-$(date +%Y-%m-%d).log 2>&1 set -euo pipefail DB_PATH="${DB_PATH:-/var/www/xamxam/storage/xamxam.db}" BACKUP_DIR="${BACKUP_DIR:-/var/backups/xamxam}" RETENTION_DAYS="${RETENTION_DAYS:-30}" TIMESTAMP=$(date +"%Y-%m-%dT%H-%M-%S") BACKUP_FILE="$BACKUP_DIR/db-$TIMESTAMP.db.gz" TMP_SNAPSHOT="/tmp/xamxam-snapshot-$$.db" mkdir -p "$BACKUP_DIR" 2>/dev/null || { echo "ERROR: Cannot create backup directory '$BACKUP_DIR'. Run: just deploy-backup-cron" >&2 exit 1 } # Safe hot backup using SQLite's online backup API sqlite3 "$DB_PATH" ".backup $TMP_SNAPSHOT" gzip -c "$TMP_SNAPSHOT" > "$BACKUP_FILE" rm -f "$TMP_SNAPSHOT" # Prune old backups. # Use filename timestamps (deterministic) rather than -mtime, which has # day-rounding ambiguity (-mtime +N means N+1 days). Files are named # db-YYYY-MM-DDTHH-MM-SS.db.gz, so we delete anything whose date is more # than RETENTION_DAYS in the past. RETENTION_SECONDS=$((RETENTION_DAYS * 86400)) NOW_EPOCH=$(date +%s) for f in "$BACKUP_DIR"/db-*.db.gz; do [ -e "$f" ] || continue base=$(basename "$f" .db.gz) # db-YYYY-MM-DDTHH-MM-SS stamp=${base#db-} # YYYY-MM-DDTHH-MM-SS # Reformat YYYY-MM-DDTHH-MM-SS → YYYY-MM-DD HH:MM:SS for date(1). named=$(echo "$stamp" | sed 's/T/ /; s/-/:/3g') || continue ts=$(date -d "$named" +%s 2>/dev/null) || continue if [ $((NOW_EPOCH - ts)) -gt $RETENTION_SECONDS ]; then rm -f "$f" fi done echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup written: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"