#!/usr/bin/env bash set -euo pipefail MODE="dry-run" CUTOFF_DATE="2026-06-02" ARCHIVE_ROOT="/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602" if [[ "${1:-}" == "--execute" ]]; then MODE="execute" elif [[ "${1:-}" != "" && "${1:-}" != "--dry-run" ]]; then echo "Usage: $0 [--dry-run|--execute]" >&2 exit 2 fi if [[ "$(id -u)" -ne 0 ]]; then echo "Must run as root on the Unraid host." >&2 exit 1 fi today="$(date +%F)" if [[ "$MODE" == "execute" && "$today" < "$CUTOFF_DATE" ]]; then echo "Refusing: cutoff is $CUTOFF_DATE, today is $today." >&2 exit 1 fi declare -a CANDIDATES=( "/mnt/user/appdata/postgresql17|/mnt/user/appdata/postgresql18|postgresql17|shared PostgreSQL 17 rollback" "/mnt/user/appdata/mealie/postgres|/mnt/user/appdata/mealie/postgres18|mealie-postgres17|Mealie PostgreSQL 17 rollback" "/mnt/user/appdata/nextcloud/postgres|/mnt/user/appdata/nextcloud/postgres18|nextcloud-postgres17|Nextcloud PostgreSQL 17 rollback" "/mnt/user/appdata/immich_postgres|/mnt/user/appdata/immich_postgres_vectorchord|immich-postgres-pgvecto-rs|Immich pgvecto.rs rollback" ) require_container_healthy() { local name="$1" local state local health state="$(docker inspect "$name" --format '{{.State.Status}}' 2>/dev/null || true)" health="$(docker inspect "$name" --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' 2>/dev/null || true)" if [[ "$state" != "running" ]]; then echo "Container $name is not running (state=$state)." >&2 exit 1 fi if [[ "$health" != "healthy" && "$health" != "none" ]]; then echo "Container $name is not healthy (health=$health)." >&2 exit 1 fi } echo "Alt-volume archive check" echo "Mode: $MODE" echo "Date: $today" echo "Archive: $ARCHIVE_ROOT" echo require_container_healthy postgresql17 require_container_healthy mealie-postgres require_container_healthy nextcloud-postgres require_container_healthy immich_postgres if docker ps --filter health=unhealthy --format '{{.Names}}' | grep -q .; then echo "Refusing: unhealthy containers exist." >&2 docker ps --filter health=unhealthy --format ' {{.Names}} {{.Status}}' >&2 exit 1 fi if [[ -x /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh ]]; then /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness fi mapfile -t active_mounts < <(docker inspect $(docker ps -aq) --format '{{range .Mounts}}{{println .Source}}{{end}}' 2>/dev/null || true) if [[ "$MODE" == "execute" ]]; then mkdir -p "$ARCHIVE_ROOT" fi for entry in "${CANDIDATES[@]}"; do IFS='|' read -r old_path active_path archive_name label <<< "$entry" archive_path="$ARCHIVE_ROOT/$archive_name" if [[ ! -d "$active_path" ]]; then echo "Missing active path for $label: $active_path" >&2 exit 1 fi if printf '%s\n' "${active_mounts[@]}" | grep -Fxq "$old_path"; then echo "Refusing: old path is still mounted by a container: $old_path" >&2 exit 1 fi if [[ -d "$old_path" && -d "$archive_path" ]]; then echo "Refusing: both old path and archive path exist for $label." >&2 echo "Old: $old_path" >&2 echo "Archive: $archive_path" >&2 exit 1 fi if [[ -d "$archive_path" ]]; then size="$(du -sh "$archive_path" 2>/dev/null | awk '{print $1}')" echo "Archived: $archive_path ($label, $size)" echo continue fi if [[ ! -d "$old_path" ]]; then echo "Absent and not archived: $old_path ($label)" >&2 exit 1 fi size="$(du -sh "$old_path" 2>/dev/null | awk '{print $1}')" echo "Candidate: $old_path ($label, $size)" echo "Active: $active_path" echo "Archive: $archive_path" if [[ "$MODE" == "execute" ]]; then mv "$old_path" "$archive_path" printf '%s MOVE %s -> %s size=%s\n' "$(date -Is)" "$old_path" "$archive_path" "$size" >> "$ARCHIVE_ROOT/MANIFEST.txt" echo "Moved: $archive_path" else echo "Dry-run: would move $old_path to $archive_path" fi echo done echo "Alt-volume archive check completed."