chore: updated restore.sh d9c047a9
Steve Simkins · 2026-06-17 12:25 1 file(s) · +48 −23
apps/backup/restore.sh +48 −23
13 13
#   --yes            Skip the interactive confirmation prompt.
14 14
#
15 15
# Env (same as backup.sh): R2_ENDPOINT, R2_BUCKET, AWS_ACCESS_KEY_ID,
16 -
# AWS_SECRET_ACCESS_KEY, and optional <APP>_VOLUME overrides.
17 -
# These are read from ./.env (or $ENV_FILE) if set; real env vars win.
16 +
# AWS_SECRET_ACCESS_KEY. Read from ./.env (or $ENV_FILE) if set; real env wins.
17 +
#
18 +
# Volume names are read ONLY from the root docker-compose.yml (the `name:`
19 +
# field of each external volume), not from .env. Override the compose file
20 +
# location with $COMPOSE_FILE. Missing volumes are created automatically.
18 21
#
19 22
# Run this on the HOST. It shells out to `docker run` to write into volumes;
20 23
# the backup container itself mounts those volumes read-only.
42 45
43 46
BUCKET="${R2_BUCKET:-andromeda-backups}"
44 47
45 -
# app | volume-internal filename | volume env var | default volume name
46 -
APPS="jotts:jotts.sqlite:JOTTS_VOLUME:jotts_jotts-data
47 -
sipp:sipp.sqlite:SIPP_VOLUME:sipp_sipp-data
48 -
cellar:cellar.sqlite:CELLAR_VOLUME:cellar_cellar-data
49 -
posts:posts.sqlite:POSTS_VOLUME:posts_posts-data
50 -
feeds:feeds.sqlite:FEEDS_VOLUME:feeds_feeds-data
51 -
library:library.sqlite:LIBRARY_VOLUME:library_library-data
52 -
bookmarks:bookmarks.sqlite:BOOKMARKS_VOLUME:bookmarks_bookmarks-data
53 -
parcels:parcels.db:PARCELS_VOLUME:parcels_parcels_data
54 -
easel:easel.sqlite:EASEL_VOLUME:easel_easel-data"
48 +
# Root docker-compose.yml is the single source of truth for volume names.
49 +
COMPOSE_FILE="${COMPOSE_FILE:-$SCRIPT_DIR/../../docker-compose.yml}"
50 +
51 +
# app | volume-internal filename | compose volume key | fallback volume name
52 +
# The compose key is looked up in COMPOSE_FILE; the fallback is used only if
53 +
# the key (or compose file) is missing.
54 +
APPS="jotts:jotts.sqlite:jotts_data:jotts_jotts-data
55 +
sipp:sipp.sqlite:sipp_data:sipp-rust_sipp-data
56 +
cellar:cellar.sqlite:cellar_data:cellar_cellar_data
57 +
posts:posts.sqlite:posts_data:posts_posts-data
58 +
feeds:feeds.sqlite:feeds_data:feeds_feeds_data
59 +
library:library.sqlite:library_data:library_library-data
60 +
bookmarks:bookmarks.sqlite:bookmarks_data:bookmarks_bookmarks-data
61 +
parcels:parcels.db:parcels_data:parcels_parcels_data
62 +
easel:easel.sqlite:easel_data:easel_easel-data"
55 63
56 64
RESTORE_IMAGE="debian:bookworm-slim"
57 65
66 74
}
67 75
68 76
# Look up a field for an app from the APPS registry.
69 -
# $1 app name, $2 field index (2=file, 3=env var, 4=default volume)
77 +
# $1 app name, $2 field index (2=file, 3=compose key, 4=fallback volume)
70 78
app_field() {
71 -
  echo "$APPS" | while IFS=: read -r name file env def; do
79 +
  echo "$APPS" | while IFS=: read -r name file key def; do
72 80
    [ "$name" = "$1" ] || continue
73 81
    case "$2" in
74 82
      2) echo "$file" ;;
75 -
      3) echo "$env" ;;
83 +
      3) echo "$key" ;;
76 84
      4) echo "$def" ;;
77 85
    esac
78 86
    break
83 91
  echo "$APPS" | cut -d: -f1 | grep -qx "$1"
84 92
}
85 93
86 -
# Resolve the Docker volume name for an app, honoring the <APP>_VOLUME override.
94 +
# Read the external `name:` for a volume key from the compose file's top-level
95 +
# `volumes:` block. Echoes nothing if the file or key is absent.
96 +
compose_volume_name() {
97 +
  key="$1"
98 +
  [ -f "$COMPOSE_FILE" ] || return 0
99 +
  awk -v key="$key" '
100 +
    /^volumes:/             { invol=1; next }
101 +
    invol && /^[^[:space:]]/ { invol=0 }
102 +
    invol && $0 ~ "^  "key":[[:space:]]*$" { found=1; next }
103 +
    found && /^[[:space:]]*name:[[:space:]]/ {
104 +
      sub(/^[[:space:]]*name:[[:space:]]*/, ""); print; exit
105 +
    }
106 +
    found && /^  [^[:space:]]/ { exit }   # next volume block, no name set
107 +
  ' "$COMPOSE_FILE"
108 +
}
109 +
110 +
# Resolve the Docker volume name for an app from the root compose file,
111 +
# falling back to the registry default only if compose has no entry.
87 112
resolve_volume() {
88 113
  app="$1"
89 -
  env_var=$(app_field "$app" 3)
114 +
  key=$(app_field "$app" 3)
90 115
  def=$(app_field "$app" 4)
91 -
  # Indirect env lookup that works in POSIX sh.
92 -
  val=$(eval "printf '%s' \"\${$env_var:-}\"")
93 -
  if [ -n "$val" ]; then
94 -
    echo "$val"
116 +
  name=$(compose_volume_name "$key")
117 +
  if [ -n "$name" ]; then
118 +
    echo "$name"
95 119
  else
96 120
    echo "$def"
97 121
  fi
133 157
  src="s3://$BUCKET/$app/$key"
134 158
135 159
  if ! docker volume inspect "$volume" >/dev/null 2>&1; then
136 -
    echo "WARN: docker volume '$volume' not found for $app, skipping"
137 -
    return 1
160 +
    echo "$(date -u) Creating docker volume '$volume' for $app ..."
161 +
    docker volume create "$volume" >/dev/null \
162 +
      || { echo "WARN: failed to create volume '$volume' for $app, skipping"; return 1; }
138 163
  fi
139 164
140 165
  if [ "$ASSUME_YES" != "1" ]; then