diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100755 index 0000000..9d8aac2 --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ts() { date +"%Y-%m-%d %H:%M:%S"; } +log() { echo "[$(ts)] [INFO] $*"; } +warn(){ echo "[$(ts)] [WARN] $*" >&2; } +err() { echo "[$(ts)] [ERROR] $*" >&2; } +die() { err "$*"; exit 1; } + +usage() { + cat <<'EOF' +Usage: + ./scripts/restore.sh [options] + +Options: + --replace Overwrite existing database. + --compat Set compatibility level after restore (e.g. 170). + --recovery Set recovery model after restore: SIMPLE|FULL|BULK_LOGGED + Default: SIMPLE (recommended for local dev). + --no-tuning Skip post-restore tuning (compat/recovery changes). + +Examples: + ./scripts/restore.sh KundenDB.bak KundenDB + ./scripts/restore.sh KundenDB.bak KundenDB --replace + ./scripts/restore.sh KundenDB.bak KundenDB --compat 170 --recovery SIMPLE + ./scripts/restore.sh KundenDB.bak KundenDB --no-tuning + +Notes: +- Put the .bak into ./backup/ (host) so it appears in the container as: + /var/opt/mssql/backup/ +- Requires a running, healthy container named "mssql2025". +EOF +} + +dump_logs() { + warn "Container logs (last 250 lines):" + docker logs --tail=250 mssql2025 2>/dev/null || true +} + +on_error() { + local code=$? + err "Restore failed (exit code=$code)." + dump_logs + exit "$code" +} +trap on_error ERR + +cd "$(dirname "$0")/.." + +# -------- Preflight -------- +command -v docker >/dev/null 2>&1 || die "docker not found in PATH." +docker info >/dev/null 2>&1 || die "docker daemon not reachable." + +[[ -f .env ]] || die "Missing .env. Create it first: cp .env.example .env" +set -a +# shellcheck disable=SC1091 +source .env +set +a +[[ -n "${MSSQL_SA_PASSWORD:-}" ]] || die "MSSQL_SA_PASSWORD is empty in .env" + +# -------- Args -------- +[[ $# -ge 2 ]] || { usage; exit 1; } + +bak_file="$1" +target_db="$2" +shift 2 + +replace="0" +compat_level="" +recovery_model="SIMPLE" +do_tuning="1" + +while [[ $# -gt 0 ]]; do + case "$1" in + --replace) + replace="1"; shift ;; + --compat) + [[ $# -ge 2 ]] || die "--compat requires a value (e.g. 170)" + compat_level="$2" + shift 2 ;; + --recovery) + [[ $# -ge 2 ]] || die "--recovery requires SIMPLE|FULL|BULK_LOGGED" + recovery_model="$2" + shift 2 ;; + --no-tuning) + do_tuning="0"; shift ;; + -h|--help) + usage; exit 0 ;; + *) + die "Unknown argument: $1 (use --help)" ;; + esac +done + +# Validate recovery model +case "$recovery_model" in + SIMPLE|FULL|BULK_LOGGED) ;; + *) + die "Invalid --recovery value: '$recovery_model'. Use SIMPLE|FULL|BULK_LOGGED." ;; +esac + +# Validate compat level if provided (basic numeric check) +if [[ -n "$compat_level" ]]; then + [[ "$compat_level" =~ ^[0-9]+$ ]] || die "Invalid --compat value: '$compat_level' (must be numeric, e.g. 170)." +fi + +# Container must be running and healthy +if ! docker ps --format '{{.Names}}' | grep -qx 'mssql2025'; then + die "Container mssql2025 is not running. Start it first: ./scripts/start.sh" +fi +health="$(docker inspect -f '{{.State.Health.Status}}' mssql2025 2>/dev/null || true)" +[[ "$health" == "healthy" ]] || die "Container is not healthy (health=$health). Run ./scripts/start.sh" + +# Backup must exist on host +host_bak_path="./backup/$bak_file" +[[ -f "$host_bak_path" ]] || die "Backup file not found: $host_bak_path" + +container_bak_path="/var/opt/mssql/backup/$bak_file" + +log "Restore requested:" +log " backup : $host_bak_path (container: $container_bak_path)" +log " target : $target_db" +log " replace : $replace" +log " tuning : $do_tuning (recovery=$recovery_model${compat_level:+, compat=$compat_level})" + +# -------- Get logical file names from backup -------- +log "Reading FILELISTONLY..." +filelist=$( + docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \ + -S localhost -U sa -P "$MSSQL_SA_PASSWORD" \ + -W -s"|" -h -1 \ + -Q "SET NOCOUNT ON; RESTORE FILELISTONLY FROM DISK = N'$container_bak_path';" \ + < /dev/null \ + | tr -d '\r' +) + +[[ -n "$filelist" ]] || die "RESTORE FILELISTONLY returned no output. Is the .bak readable?" + +data_logical="$(echo "$filelist" | awk -F'|' '$3 ~ /^D$/ {print $1; exit}')" +log_logical="$(echo "$filelist" | awk -F'|' '$3 ~ /^L$/ {print $1; exit}')" + +[[ -n "$data_logical" ]] || die "Could not detect data logical name (Type=D) from FILELISTONLY." +[[ -n "$log_logical" ]] || die "Could not detect log logical name (Type=L) from FILELISTONLY." + +log "Detected logical names:" +log " DATA: $data_logical" +log " LOG : $log_logical" + +# Destination files in container +dest_mdf="/var/opt/mssql/data/${target_db}.mdf" +dest_ldf="/var/opt/mssql/data/${target_db}_log.ldf" + +# -------- Build RESTORE DATABASE command -------- +with_opts="MOVE N'$data_logical' TO N'$dest_mdf', + MOVE N'$log_logical' TO N'$dest_ldf', + STATS = 5" +if [[ "$replace" == "1" ]]; then + with_opts="$with_opts, + REPLACE" +fi + +restore_sql=$( + cat <