The backup script missed DATA_DIR when the line carried an export prefix, quotes, padding or a CR, and silently fell back to ./data — dumping first and only then discovering there was nothing to mirror. It now strips all that, prints the paths it's using, and stops before writing anything if the files folder isn't there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80 lines
3.3 KiB
Bash
Executable file
80 lines
3.3 KiB
Bash
Executable file
#!/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")/.."
|
||
|
||
# A setting from the environment, else from .env, else the fallback. Tolerates
|
||
# "export NAME=", quotes, spaces and Windows line endings.
|
||
setting() { # name, fallback
|
||
local from_env="${!1:-}"
|
||
if [ -n "$from_env" ]; then echo "$from_env"; return; fi
|
||
if [ -f .env ]; then
|
||
local line
|
||
line="$(sed -e 's/\r$//' .env \
|
||
| grep -E "^[[:space:]]*(export[[:space:]]+)?$1[[:space:]]*=" \
|
||
| tail -1 | cut -d= -f2- \
|
||
| sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'$/\1/")"
|
||
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)"
|
||
|
||
echo "$(date '+%F %T') data: $DATA_DIR → backups: $BACKUP_DIR"
|
||
|
||
# Checked before anything is written: on an external disk that isn't mounted,
|
||
# the folder is simply missing, and a backup of nothing is worse than none.
|
||
if [ ! -d "$DATA_DIR/uploads" ]; then
|
||
echo "$(date '+%F %T') FAILED: no $DATA_DIR/uploads — is the disk mounted, and is DATA_DIR set in .env?" >&2
|
||
exit 1
|
||
fi
|
||
|
||
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.
|
||
rsync -a --delete "$DATA_DIR/uploads/" "$BACKUP_DIR/uploads/"
|
||
echo "$(date '+%F %T') files → $BACKUP_DIR/uploads ($(du -sh "$BACKUP_DIR/uploads" | cut -f1))"
|