-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrestore.sh
More file actions
executable file
·109 lines (96 loc) · 4.75 KB
/
Copy pathrestore.sh
File metadata and controls
executable file
·109 lines (96 loc) · 4.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/bin/env bash
# Restore from a backup set. Destructive: it drops and recreates the project
# database, and the control database too when the set includes a control dump.
#
# ./scripts/restore.sh /var/backups/baselyra/db-20260823T020000Z.dump \
# /var/backups/baselyra/storage-20260823T020000Z.tar.gz
#
# The control dump is found beside the project dump by name (control-<stamp>),
# or given as a third argument. It holds the Studio accounts, so restoring
# without it leaves an instance nobody can sign in to.
set -euo pipefail
cd "$(dirname "$0")/.."
# docker compose accepts unquoted values containing spaces and angle brackets,
# which `. ./.env` would treat as redirection. Pull out only what we need.
envget() { sed -n "s/^$1=//p" .env | head -1 | sed 's/^"//; s/"$//'; }
POSTGRES_USER="${POSTGRES_USER:-$(envget POSTGRES_USER)}"
POSTGRES_DB="${POSTGRES_DB:-$(envget POSTGRES_DB)}"
: "${POSTGRES_USER:=baselyra}"
: "${POSTGRES_DB:=baselyra}"
# Same default as CONTROL_DATABASE_URL derives in src/config.ts.
CONTROL_DB="${CONTROL_POSTGRES_DB:-baselyra_control}"
DUMP="${1:?usage: restore.sh <db.dump> [storage.tar.gz] [control.dump]}"
STORAGE="${2:-}"
DB="$POSTGRES_DB"
USER="$POSTGRES_USER"
[ -s "$DUMP" ] || { echo "no such dump: $DUMP" >&2; exit 1; }
# Derive the control dump only from a name we actually recognise. The sed is a
# no-op on any other name, which would silently resolve CONTROL_DUMP to the
# project dump and restore application data over the Studio accounts.
if [ -n "${3:-}" ]; then
CONTROL_DUMP="$3"
elif [ "$(basename "$DUMP")" != "${DUMP##*/db-}" ]; then
CONTROL_DUMP="$(dirname "$DUMP")/control-${DUMP##*/db-}"
else
CONTROL_DUMP=""
fi
[ "$CONTROL_DUMP" != "$DUMP" ] || {
echo "the control dump cannot be the project dump — pass it as the third argument" >&2
exit 1
}
# Backups taken before the control/project split have no control dump, and they
# are still legitimate restores: the Studio accounts are inside the project dump
# and the upgrade in migrate.js moves them across on the next boot. Restoring
# only the project database is correct there, and dropping the live control
# database would throw away accounts the dump predates.
RESTORE_CONTROL=yes
if [ -z "$CONTROL_DUMP" ] || [ ! -s "$CONTROL_DUMP" ]; then
if docker compose exec -T db pg_restore --list < "$DUMP" 2>/dev/null | grep -q "baselyra platform_users"; then
RESTORE_CONTROL=no
echo "[restore] pre-split backup: it carries the Studio accounts itself."
echo "[restore] '$CONTROL_DB' will be left untouched; migrate moves the accounts across on the next boot."
else
echo "no control dump beside $DUMP, and the project dump has no Studio accounts either." >&2
echo "Pass the control dump as the third argument, or that instance would have nobody who can sign in." >&2
exit 1
fi
fi
if [ "$RESTORE_CONTROL" = yes ]; then
# Verify before destroying: a truncated archive passes `-s` and fails halfway
# through pg_restore, by which point the live control database is gone.
docker compose exec -T db pg_restore --list < "$CONTROL_DUMP" >/dev/null 2>&1 || {
echo "$CONTROL_DUMP is not a readable pg_dump archive — refusing to drop anything" >&2
exit 1
}
TARGETS="$DB $CONTROL_DB"
PROMPT="This REPLACES the databases '$DB' and '$CONTROL_DB' and the storage volume."
else
TARGETS="$DB"
PROMPT="This REPLACES the database '$DB' and the storage volume. '$CONTROL_DB' is left alone."
fi
docker compose exec -T db pg_restore --list < "$DUMP" >/dev/null 2>&1 || {
echo "$DUMP is not a readable pg_dump archive — refusing to drop anything" >&2
exit 1
}
read -rp "$PROMPT Type the database name to confirm: " ok
[ "$ok" = "$DB" ] || { echo "aborted"; exit 1; }
echo "[restore] stopping the app so nothing writes mid-restore"
docker compose stop app
# CREATE DATABASE cannot run inside a transaction, so these go through psql
# one statement at a time rather than as a script.
for target in $TARGETS; do
docker compose exec -T db psql -U "$USER" -d postgres -v ON_ERROR_STOP=1 \
-c "select pg_terminate_backend(pid) from pg_stat_activity where datname = '$target' and pid <> pg_backend_pid()" \
-c "drop database if exists \"$target\"" -c "create database \"$target\" owner \"$USER\""
done
docker compose exec -T db pg_restore -U "$USER" -d "$DB" --no-owner --clean --if-exists < "$DUMP"
if [ "$RESTORE_CONTROL" = yes ]; then
docker compose exec -T db pg_restore -U "$USER" -d "$CONTROL_DB" --no-owner --clean --if-exists < "$CONTROL_DUMP"
fi
if [ -n "$STORAGE" ]; then
[ -s "$STORAGE" ] || { echo "no such storage archive: $STORAGE" >&2; exit 1; }
docker run --rm -i -v baselyra_storage-data:/data alpine \
sh -c 'rm -rf /data/* && tar -xzf - -C /data' < "$STORAGE"
fi
docker compose start app
echo "[restore] done — the app is starting"