Merge nightly backup script
All checks were successful
build-and-push / docker (push) Successful in 15s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dlawler489 2026-09-20 16:44:43 +10:00
commit 22645f139a
4 changed files with 104 additions and 0 deletions

View file

@ -10,5 +10,10 @@ DATA_DIR=/Volumes/Data/printhive
# or a network share. # or a network share.
PGDATA_DIR=/Volumes/Data/printhive/pgdata PGDATA_DIR=/Volumes/Data/printhive/pgdata
# Where tools/backup.sh writes (default ./backups) and how many days of
# database dumps it keeps (default 14). Best on a different disk from DATA_DIR.
BACKUP_DIR=/Volumes/Data/printhive-backups
KEEP_DAYS=14
# Thingiverse import — get a free app token at https://www.thingiverse.com/developers # Thingiverse import — get a free app token at https://www.thingiverse.com/developers
THINGIVERSE_TOKEN= THINGIVERSE_TOKEN=

1
.gitignore vendored
View file

@ -11,3 +11,4 @@ tools/venv/
__pycache__/ __pycache__/
extracted/ extracted/
models/ models/
backups/

View file

@ -77,6 +77,34 @@ files live on one of those.
A backup needs the database (or a `pg_dump`) and `uploads`; the other three rebuild themselves. Everything else in A backup needs the database (or a `pg_dump`) and `uploads`; the other three rebuild themselves. Everything else in
the container is replaced on each update, so nothing outside these folders is kept. the container is replaced on each update, so nothing outside these folders is kept.
### Backups
`tools/backup.sh` takes a dated dump of the database and mirrors the model files, leaving Printhive running:
```bash
./tools/backup.sh
```
It writes to `./backups` unless `BACKUP_DIR` says otherwise: `backups/db/printhive-YYYY-MM-DD.dump` (kept for
`KEEP_DAYS`, 14 by default) and `backups/uploads`, an rsync mirror of the files — matching what's there now,
deletions included, rather than a history. Thumbnails, previews and extracted archives are skipped, as they rebuild.
Each dump is read back with `pg_restore -l` before it replaces the day's file, so a failed run leaves the older
backups alone and exits non-zero.
Nightly, at 3:30am — `crontab -e`, with the path to your own copy:
```
30 3 * * * cd /path/to/printhive && ./tools/backup.sh >> backups/backup.log 2>&1
```
Docker has to be running for it to work, and the backups are best kept on a different disk from `DATA_DIR`.
To restore the database into a running stack:
```bash
docker compose exec -T db pg_restore -U printhive -d printhive --clean --if-exists < backups/db/printhive-2026-01-01.dump
```
Upgrading from a version that used Docker named volumes: stop the stack, run `tools/volumes-to-folders.sh` to copy the Upgrading from a version that used Docker named volumes: stop the stack, run `tools/volumes-to-folders.sh` to copy the
data across, then start it again. The script only copies — the old volumes stay until you remove them yourself. data across, then start it again. The script only copies — the old volumes stay until you remove them yourself.

70
tools/backup.sh Executable file
View file

@ -0,0 +1,70 @@
#!/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))"