#!/usr/bin/env bash # --------------------------------------------------------------------------- # scripts/backup.sh — local SQLite + media snapshot # # Produces two artifacts under data/backups/: # app-.db : SQLite .backup of the live database (safe while # the app is running; sqlite3 .backup coordinates # with WAL). # media-.tar.gz : tar archive of the media upload directory. # # Retention: keep the 14 newest of each artifact; prune the rest. # # The script is intended to be invoked by cron on the Debian VM. Example # crontab line (daily at 03:15 UTC): # 15 3 * * * cd /srv/chicken_babies_site && ./scripts/backup.sh \ # >> /var/log/chicken_babies/backup.log 2>&1 # # Exit codes: # 0 on success. Any failing step aborts via `set -e` with a non-zero # status so cron emails / log scrapers can flag it. # --------------------------------------------------------------------------- set -euo pipefail # UTC timestamp: sortable, compact, unambiguous. Matches the ISO-8601 # "basic" form with a literal Z suffix. TS="$(date -u +%Y%m%dT%H%M%SZ)" BACKUP_DIR="data/backups" DB_SRC="data/app.db" MEDIA_SRC="data/media" # Ensure the output directory exists. `mkdir -p` is idempotent and never # fails if the directory already exists, which is exactly what we want. mkdir -p "${BACKUP_DIR}" # --- SQLite snapshot ------------------------------------------------------ # Use sqlite3's .backup command rather than `cp` so we get a consistent # copy even if the database is actively being written to. sqlite3 serialises # with the WAL and produces a file that's safe to open elsewhere. DB_DEST="${BACKUP_DIR}/app-${TS}.db" sqlite3 "${DB_SRC}" ".backup ${DB_DEST}" # --- Media archive -------------------------------------------------------- # tar the whole media tree. gzip compression keeps the archive small for # typical image payloads; restore is just `tar -xzf`. MEDIA_DEST="${BACKUP_DIR}/media-${TS}.tar.gz" tar -czf "${MEDIA_DEST}" "${MEDIA_SRC}" # --- Retention prune ------------------------------------------------------- # Keep the 14 newest of each type; delete the rest. ls -t sorts newest- # first; tail -n +15 drops the first 14 lines (the survivors). We guard # each rm with `|| true` so a cold start with fewer than 14 artifacts # doesn't cause the script to exit non-zero via `set -e`. keep_newest_14() { local pattern="$1" # shellcheck disable=SC2012 # `ls -t` is the simplest way to sort by mtime ls -1t ${pattern} 2>/dev/null | tail -n +15 | xargs -r rm -f } keep_newest_14 "${BACKUP_DIR}/app-*.db" keep_newest_14 "${BACKUP_DIR}/media-*.tar.gz" echo "backup complete: ${DB_DEST}, ${MEDIA_DEST}"