diff --git a/docker-compose.yml b/docker-compose.yml index 1c34ddd..43fa139 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: # ownership and permissions (ext4, xfs, APFS...), which exFAT, FAT and # most network shares don't provide. Keep it on local disk if the files # live somewhere like that. - - ${PGDATA_DIR:-./data/pgdata}:/var/lib/postgresql/data + - ${PGDATA_DIR:-/Volumes/Data/printhive/pgdata}:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U printhive -d printhive"] interval: 5s @@ -37,12 +37,14 @@ services: MAKERWORLD_TOKEN: ${MAKERWORLD_TOKEN:-} PRINTABLES_TOKEN: ${PRINTABLES_TOKEN:-} volumes: - # Folders on the host: ./data unless DATA_DIR says otherwise. + # Absolute on purpose: a relative path resolves against whatever folder + # runs compose, which isn't this one when a stack manager deploys it — + # and an empty folder makes a working install look wiped. # uploads and pgdata hold everything that matters; the other two rebuild. - - ${DATA_DIR:-./data}/uploads:/app/uploads - - ${DATA_DIR:-./data}/thumbnails:/app/thumbnails - - ${DATA_DIR:-./data}/glb:/app/glb - - ${DATA_DIR:-./data}/extracted:/app/extracted # cache of files unpacked from archives + - ${DATA_DIR:-/Volumes/Data/printhive}/uploads:/app/uploads + - ${DATA_DIR:-/Volumes/Data/printhive}/thumbnails:/app/thumbnails + - ${DATA_DIR:-/Volumes/Data/printhive}/glb:/app/glb + - ${DATA_DIR:-/Volumes/Data/printhive}/extracted:/app/extracted # cache of files unpacked from archives # No host port published — Traefik auto-discovers the app on the shared network. networks: - default # talk to the db diff --git a/tools/backup.sh b/tools/backup.sh index 76e7b53..20a9d10 100755 --- a/tools/backup.sh +++ b/tools/backup.sh @@ -8,7 +8,7 @@ # # 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) +# UPLOADS_DIR the model files (default: asked of the running container) # KEEP_DAYS dumps to keep, in days (default: 14) set -euo pipefail @@ -21,7 +21,7 @@ setting() { # name, fallback if [ -n "$from_env" ]; then echo "$from_env"; return; fi if [ -f .env ]; then local line - line="$(sed -e 's/\r$//' .env \ + line="$(tr -d '\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/")" @@ -31,16 +31,31 @@ setting() { # name, fallback } 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" +# Where the model files really are: ask the running container, rather than +# trusting a setting that may have drifted from how the stack was deployed. +UPLOADS_DIR="${UPLOADS_DIR:-}" +if [ -z "$UPLOADS_DIR" ]; then + UPLOADS_DIR="$(docker inspect printhive-app \ + --format '{{range .Mounts}}{{if eq .Destination "/app/uploads"}}{{.Source}}{{end}}{{end}}' 2>/dev/null || true)" +fi +if [ -z "$UPLOADS_DIR" ]; then + echo "$(date '+%F %T') FAILED: couldn’t find where /app/uploads comes from — is printhive-app running?" >&2 + exit 1 +fi -# 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 +echo "$(date '+%F %T') files: $UPLOADS_DIR → backups: $BACKUP_DIR" + +# A path the container can see but this machine can't (a stack manager deploying +# from inside a container does that) would back up nothing at all. +if [ ! -d "$UPLOADS_DIR" ]; then + echo "$(date '+%F %T') FAILED: $UPLOADS_DIR isn’t reachable from here — set UPLOADS_DIR to its path on this machine" >&2 + exit 1 +fi +if [ -z "$(ls -A "$UPLOADS_DIR" 2>/dev/null)" ]; then + echo "$(date '+%F %T') FAILED: $UPLOADS_DIR is empty — refusing to mirror nothing over a good backup" >&2 exit 1 fi @@ -49,6 +64,14 @@ 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. +# An empty library where backups already exist means something is wrong with +# the stack, not that you deleted everything: don't write that over good ones. +models="$(docker compose exec -T db psql -U printhive -d printhive -tAc 'select count(*) from models' 2>/dev/null | tr -d '[:space:]' || true)" +if [ "$models" = "0" ] && ls "$BACKUP_DIR"/db/printhive-*.dump >/dev/null 2>&1; then + echo "$(date '+%F %T') FAILED: the database has no models, but older backups do — leaving them alone" >&2 + exit 1 +fi + dump="$BACKUP_DIR/db/printhive-$stamp.dump" # Dumped inside the container and read back there — a dump that can't be listed @@ -76,5 +99,5 @@ find "$BACKUP_DIR/db" -name 'printhive-*.dump' -type f -mtime "+$KEEP_DAYS" -del # --- 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/" +rsync -a --delete "$UPLOADS_DIR/" "$BACKUP_DIR/uploads/" echo "$(date '+%F %T') files → $BACKUP_DIR/uploads ($(du -sh "$BACKUP_DIR/uploads" | cut -f1))"