#!/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 <