#!/bin/sh set -eu # Run this on the Unraid host before Borg starts. # It refreshes the latest database dumps in a stable directory so Borg can # version the dump artifacts instead of raw live database files. DUMP_ROOT="${DUMP_ROOT:-/mnt/user/appdata/borg-ui/dumps}" LATEST_DIR="$DUMP_ROOT/latest" TMP_DIR="$DUMP_ROOT/.tmp" SHARED_PG_ADMIN_USER="${SHARED_PG_ADMIN_USER:-mailarchiver}" SHARED_PG_PASSWORD_FILE="${SHARED_PG_PASSWORD_FILE:-/mnt/user/appdata/secrets/postgres_password.txt}" log() { printf '%s %s\n' "[borg-dumps]" "$*" } warn() { printf '%s %s\n' "[borg-dumps][warn]" "$*" >&2 } need_cmd() { if ! command -v "$1" >/dev/null 2>&1; then warn "Required command missing: $1" exit 1 fi } need_container() { docker inspect "$1" >/dev/null 2>&1 } ensure_dirs() { mkdir -p "$LATEST_DIR" "$TMP_DIR" } atomic_write() { target="$1" tmp="$2" mkdir -p "$(dirname "$target")" mv "$tmp" "$target" } dump_pg_db() { container="$1" password="$2" user="$3" db="$4" output="$5" tmp="$TMP_DIR/$(basename "$output").tmp" log "Dumping PostgreSQL database '$db' from $container" docker exec -e "PGPASSWORD=$password" "$container" \ pg_dump -U "$user" -d "$db" -Fc > "$tmp" atomic_write "$output" "$tmp" } dump_pg_globals() { container="$1" password="$2" user="$3" output="$4" tmp="$TMP_DIR/$(basename "$output").tmp" log "Dumping PostgreSQL globals from $container" docker exec -e "PGPASSWORD=$password" "$container" \ pg_dumpall -U "$user" --globals-only > "$tmp" atomic_write "$output" "$tmp" } dump_optional_pg_db() { container="$1" password="$2" user="$3" db="$4" output="$5" if docker exec -e "PGPASSWORD=$password" "$container" \ psql -U "$user" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname = '$db'" \ | grep -q 1; then dump_pg_db "$container" "$password" "$user" "$db" "$output" else warn "Skipping missing PostgreSQL database '$db' in $container" fi } dump_mysql_container() { container="$1" output="$2" if ! need_container "$container"; then warn "Skipping missing container: $container" return 0 fi info="$(docker exec "$container" sh -lc 'printf "%s|%s|%s" "${MARIADB_DATABASE:-${MYSQL_DATABASE:-}}" "${MARIADB_USER:-${MYSQL_USER:-root}}" "${MARIADB_PASSWORD:-${MYSQL_PASSWORD:-}}"' || true)" db="$(printf '%s' "$info" | cut -d'|' -f1)" user="$(printf '%s' "$info" | cut -d'|' -f2)" password="$(printf '%s' "$info" | cut -d'|' -f3)" if [ -z "$db" ] || [ -z "$password" ]; then warn "Skipping MySQL/MariaDB dump for $container because DB credentials were not discoverable" return 0 fi tmp="$TMP_DIR/$(basename "$output").tmp" log "Dumping MariaDB/MySQL database '$db' from $container" docker exec "$container" sh -lc "mysqldump --single-transaction --quick -u\"$user\" -p\"$password\" \"$db\"" > "$tmp" atomic_write "$output" "$tmp" } dump_mongo_container() { container="$1" output="$2" if ! need_container "$container"; then warn "Skipping missing container: $container" return 0 fi if ! docker exec "$container" sh -lc 'command -v mongodump >/dev/null 2>&1'; then warn "Skipping Mongo dump for $container because mongodump is not available in the container image" return 0 fi tmp="$TMP_DIR/$(basename "$output").tmp" log "Dumping MongoDB archive from $container" docker exec "$container" sh -lc 'mongodump --archive --gzip --username "$MONGO_INITDB_ROOT_USERNAME" --password "$(cat /run/secrets/mongo_password)" --authenticationDatabase admin' > "$tmp" atomic_write "$output" "$tmp" } main() { need_cmd docker ensure_dirs # Shared PostgreSQL 17 if need_container "postgresql17"; then # Use the cluster admin/superuser for all shared-cluster dumps. The # application roles exist, but they can have different passwords from the # bootstrap postgres secret used by the shared container. shared_pg_password="$(cat "$SHARED_PG_PASSWORD_FILE")" dump_pg_globals "postgresql17" "$shared_pg_password" "$SHARED_PG_ADMIN_USER" "$LATEST_DIR/postgresql17-globals.sql" dump_pg_db "postgresql17" "$shared_pg_password" "$SHARED_PG_ADMIN_USER" "mailarchiver" "$LATEST_DIR/postgresql17-mailarchiver.dump" dump_pg_db "postgresql17" "$shared_pg_password" "$SHARED_PG_ADMIN_USER" "paperless" "$LATEST_DIR/postgresql17-paperless.dump" dump_optional_pg_db "postgresql17" "$shared_pg_password" "$SHARED_PG_ADMIN_USER" "semaphore" "$LATEST_DIR/postgresql17-semaphore.dump" dump_optional_pg_db "postgresql17" "$shared_pg_password" "$SHARED_PG_ADMIN_USER" "authelia" "$LATEST_DIR/postgresql17-authelia.dump" else warn "Skipping shared PostgreSQL dumps because container 'postgresql17' is missing" fi # Dedicated PostgreSQL databases if need_container "mealie-postgres"; then mealie_password="$(cat /mnt/user/appdata/secrets/mealie_postgres_password.txt)" dump_pg_db "mealie-postgres" "$mealie_password" "mealie" "mealie" "$LATEST_DIR/mealie.dump" else warn "Skipping missing container: mealie-postgres" fi if need_container "immich_postgres"; then immich_password="$(cat /mnt/user/appdata/secrets/immich_postgres_password.txt)" dump_pg_db "immich_postgres" "$immich_password" "immich" "immich" "$LATEST_DIR/immich.dump" else warn "Skipping missing container: immich_postgres" fi # MariaDB / MySQL dump_mysql_container "firefly-db" "$LATEST_DIR/firefly.sql" # MongoDB dump_mongo_container "komodo-mongo" "$LATEST_DIR/komodo-mongo.archive.gz" log "Finished refreshing dump set in $LATEST_DIR" } main "$@"