chore: updated restore.sh
d9c047a9
1 file(s) · +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 |
|