printhive/tools/backup.sh
dlawler489 a5bb4dc33d Add a nightly backup script
A dated pg_dump plus an rsync mirror of the model files, with the stack
running. The dump is read back with pg_restore -l inside the container
before it replaces the day's file, so a bad run leaves older backups
alone. Older dumps are pruned after KEEP_DAYS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:19:21 +10:00

70 lines
2.7 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Nightly backup: a dated dump of the database, and a mirror of the model files.
#
# Run it from anywhere — it finds the folder holding docker-compose.yml itself.
# Printhive keeps running throughout; pg_dump takes a consistent snapshot.
#
# ./tools/backup.sh
#
# Settings (in .env beside docker-compose.yml, or the environment):
# BACKUP_DIR where backups go (default: ./backups)
# DATA_DIR where the data lives (default: ./data)
# KEEP_DAYS dumps to keep, in days (default: 14)
set -euo pipefail
cd "$(dirname "$0")/.."
setting() { # name, fallback
local from_env="${!1:-}"
if [ -n "$from_env" ]; then echo "$from_env"; return; fi
if [ -f .env ]; then
local line
line="$(grep -E "^$1=" .env | tail -1 | cut -d= -f2-)"
if [ -n "$line" ]; then echo "$line"; return; fi
fi
echo "$2"
}
BACKUP_DIR="$(setting BACKUP_DIR ./backups)"
DATA_DIR="$(setting DATA_DIR ./data)"
KEEP_DAYS="$(setting KEEP_DAYS 14)"
stamp="$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR/db"
# --- the database -----------------------------------------------------------
# Written to a .part file first, so an interrupted run never leaves behind
# something that looks like a good backup.
dump="$BACKUP_DIR/db/printhive-$stamp.dump"
# Dumped inside the container and read back there — a dump that can't be listed
# is no backup at all, and pg_restore can only list a real file, not a pipe.
if ! docker compose exec -T db sh -c \
'pg_dump -U printhive -Fc printhive > /tmp/printhive.dump && pg_restore -l /tmp/printhive.dump > /dev/null'; then
echo "$(date '+%F %T') FAILED: couldn’t dump the database — keeping the old backups" >&2
docker compose exec -T db rm -f /tmp/printhive.dump || true
exit 1
fi
docker compose exec -T db cat /tmp/printhive.dump > "$dump.part"
docker compose exec -T db rm -f /tmp/printhive.dump || true
if [ ! -s "$dump.part" ]; then
echo "$(date '+%F %T') FAILED: the dump came out empty — keeping the old backups" >&2
rm -f "$dump.part"
exit 1
fi
mv "$dump.part" "$dump"
echo "$(date '+%F %T') database → $dump ($(du -h "$dump" | cut -f1))"
# Older dumps go, so the folder doesn't grow for ever.
find "$BACKUP_DIR/db" -name 'printhive-*.dump' -type f -mtime "+$KEEP_DAYS" -delete
# --- the model files --------------------------------------------------------
# A mirror, not a history: it matches what's there now, deletions included.
# Thumbnails, previews and extracted archives are left out — they rebuild.
if [ ! -d "$DATA_DIR/uploads" ]; then
echo "$(date '+%F %T') FAILED: no $DATA_DIR/uploads — is the disk mounted?" >&2
exit 1
fi
rsync -a --delete "$DATA_DIR/uploads/" "$BACKUP_DIR/uploads/"
echo "$(date '+%F %T') files → $BACKUP_DIR/uploads ($(du -sh "$BACKUP_DIR/uploads" | cut -f1))"