Files
gochat/backend/scripts/db_restore.sh
T
Rogeeandrogee fb83285617 HH-445: deploy production observability and runbooks (#96)
* HH-445: deploy production observability and runbooks

* fix(ops): share production database DSN

* fix(HH-445): enforce database TLS gate

* fix(HH-445): preserve production serve command

* fix(prod): require external database dependencies

* fix(prod): unify database host rejection gates

* test(prod): enforce exact database TLS runbook contract

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-22 19:39:57 +08:00

74 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Restore an encrypted GoChat backup into an empty database/storage target and
# print auditable RPO/RTO and business-integrity evidence.
set -euo pipefail
umask 077
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
"$script_dir/database_client_entrypoint.sh" --check
bundle=${1:?usage: db_restore.sh /path/to/gochat-*.tar.enc}
dsn=${GOCHAT_DATABASE_DSN:?GOCHAT_DATABASE_DSN is required}
storage=${GOCHAT_STORAGE_PATH:?GOCHAT_STORAGE_PATH is required}
connector=${GOCHAT_CONNECTOR_DB_PATH:?GOCHAT_CONNECTOR_DB_PATH is required}
passphrase_file=${GOCHAT_BACKUP_PASSPHRASE_FILE:?GOCHAT_BACKUP_PASSPHRASE_FILE is required}
started_at=$(date -u +%s)
[[ -f "$bundle" ]] || { echo "backup bundle does not exist: $bundle" >&2; exit 1; }
[[ -f "$bundle.sha256" ]] || { echo "backup checksum does not exist: $bundle.sha256" >&2; exit 1; }
(cd "$(dirname "$bundle")" && sha256sum -c "$(basename "$bundle").sha256")
server_major=$(($(psql "$dsn" -Atqc "SHOW server_version_num") / 10000))
client_major=$(pg_restore --version | awk '{print $NF}' | cut -d. -f1)
[[ "$client_major" == "$server_major" ]] || {
echo "pg_restore major $client_major must match PostgreSQL major $server_major" >&2
exit 1
}
table_count=$(psql "$dsn" -Atqc "SELECT count(*) FROM pg_tables WHERE schemaname = 'public'")
[[ "$table_count" == "0" ]] || { echo "target database is not empty" >&2; exit 1; }
install -d -m 0700 "$storage" "$(dirname "$connector")"
[[ -z "$(find "$storage" -mindepth 1 -maxdepth 1 -print -quit)" ]] || { echo "target storage is not empty" >&2; exit 1; }
[[ ! -e "$connector" ]] || { echo "target connector database already exists" >&2; exit 1; }
work_dir=$(mktemp -d "$(dirname "$bundle")/.restore.XXXXXX")
trap 'rm -rf "$work_dir"' EXIT
archive=$work_dir/snapshot.tar
openssl enc -d -aes-256-cbc -pbkdf2 -pass "file:$passphrase_file" -in "$bundle" -out "$archive"
while IFS= read -r path; do
case "$path" in
/*|../*|*/../*|*/..) echo "unsafe archive path: $path" >&2; exit 1 ;;
esac
done < <(tar -tf "$archive")
if tar -tvf "$archive" | awk '{type = substr($1, 1, 1); if (type != "-" && type != "d") found = 1} END {exit(found ? 0 : 1)}'; then
echo "backup archive contains unsupported entry types" >&2
exit 1
fi
tar -C "$work_dir" -xf "$archive"
rm "$archive"
(cd "$work_dir" && sha256sum -c SHA256SUMS)
pg_restore --list "$work_dir/postgres.dump" >/dev/null
pg_restore --exit-on-error --no-owner --no-privileges --dbname "$dsn" "$work_dir/postgres.dump"
while IFS= read -r path; do
case "$path" in
/*|../*|*/../*|*/..) echo "unsafe attachment path: $path" >&2; exit 1 ;;
esac
done < <(tar -tf "$work_dir/attachments.tar")
if tar -tvf "$work_dir/attachments.tar" | awk '{type = substr($1, 1, 1); if (type != "-" && type != "d") found = 1} END {exit(found ? 0 : 1)}'; then
echo "attachment archive contains unsupported entry types" >&2
exit 1
fi
tar -C "$storage" -xf "$work_dir/attachments.tar"
install -m 0600 "$work_dir/connector.db" "$connector"
migration_version=$(psql "$dsn" -Atqc "SELECT COALESCE(MAX(version), 0) FROM schema_migrations WHERE NOT dirty")
accounts=$(psql "$dsn" -Atqc "SELECT count(*) FROM accounts WHERE deleted_at IS NULL")
attachments=$(psql "$dsn" -Atqc "SELECT count(*) FROM attachments WHERE deleted_at IS NULL")
backup_epoch=$(awk -F= '$1 == "created_at_epoch" {print $2}' "$work_dir/manifest")
version=$(awk -F= '$1 == "version" {print substr($0, index($0, "=") + 1)}' "$work_dir/manifest")
finished_at=$(date -u +%s)
echo "restore=ok version=$version migration=$migration_version accounts=$accounts attachments=$attachments rpo_seconds=$((started_at-backup_epoch)) rto_seconds=$((finished_at-started_at))"