#!/bin/bash
# bpm-restore-deploy — Ownership-erhaltender Host-Datei-Restore (Sprint 22.9).
#
# Warum als root: restic restore setzt die im Snapshot gespeicherten
# uid/gid/mode nur, wenn der Prozess privilegiert ist (CAP_CHOWN). Lief der
# Restore wie früher als www-data, kollabierte alles auf www-data:www-data —
# bei einem /var-Restore eine Katastrophe. Zusätzlich MUSS das Staging auf
# einem echten POSIX-FS liegen (NICHT CIFS: forceuid/forcegid überschreiben
# jede Ownership). Beides bündelt dieser Helper hinter EINEM sudoers-Eintrag
# statt breiter (root)-Rechte auf restic/rsync (Defense in Depth).
#
# Ablauf (pro restore_path ein Aufruf):
#   1. restic restore <snapshot> --target <staging_jobdir> --include <path>
#      als root → Staging mit korrekten uid/gid/mode (numerisch).
#   2. rsync -a --numeric-ids vom Staging auf den Ziel-Host (Ziel-SSH-User
#      ist root → Ownership landet 1:1). Pull-Modell: BPM initiiert.
#
# Aufrufe (alle als root via sudo, von www-data):
#   bpm-restore-deploy deploy <repo_path> <snapshot> <staging_jobdir> \
#        <include_path> <ssh_key> <known_hosts> <ssh_user> <ssh_port> \
#        <target_addr> <target_base> [<target_name>]
#   bpm-restore-deploy cleanup <staging_jobdir>
#
# <target_name> (optional, 11. Argument): Ist es gesetzt, wird das Element
# unter genau diesem Namen nach <target_base>/<target_name> geschrieben
# (Datei ODER Verzeichnis), statt die Original-Pfadstruktur nachzubauen —
# so überschreibt ein Einzeldatei-/Ordner-Restore die bestehende Datei NICHT.
# Muss EINE Pfadkomponente sein (kein '/', kein '..').
#
# Env (via sudoers env_keep durchgereicht, NICHT als argv → kein ps-Leak):
#   RESTIC_PASSWORD       (Pflicht für deploy)
#   RESTIC_PROGRESS_FPS   (optional)
#   RESTIC_BINARY         (optional, Default /usr/bin/restic)
#
# Constraints (Defense in Depth):
#   - <repo_path>        muss unter /mnt/ liegen, kein '..'
#   - <staging_jobdir>   muss unter /mnt/restore-staging/ liegen, kein '..'
#   - <include_path>     absolut (/...), kein '..'
#   - <target_base>      absolut (/...), kein '..'

set -euo pipefail

# Staging muss auf einem echten POSIX-Storage-Mount unter /mnt liegen (NICHT
# CIFS). Der konkrete Pfad kommt aus config('bpm.restore_staging_path') und ist
# install-abhängig (z.B. /mnt/iscsi-ugreen/restore-staging) — daher hier nur
# „unter /mnt/" + Job-Dir-Name „restore-*" als Defense-in-Depth.
STAGING_ROOT="/mnt"
RESTIC_BIN="${RESTIC_BINARY:-/usr/bin/restic}"
export HOME=/root          # restic-Cache unter /root/.cache (root-schreibbar)

die() { printf '[bpm-restore-deploy] FAIL: %s\n' "$*" >&2; exit 1; }

require_root() { [ "$(id -u)" -eq 0 ] || die "muss als root laufen"; }

no_dotdot() { [[ "$1" != *".."* ]] || die "Pfad enthält '..', abgelehnt: $1"; }

valid_under() {
    # $1 = Pfad, $2 = erlaubtes Präfix
    no_dotdot "$1"
    [[ "$1" == "$2"* ]] || die "Pfad muss unter $2 liegen, war: $1"
}

valid_abs() {
    no_dotdot "$1"
    [[ "$1" == /* ]] || die "Pfad muss absolut sein, war: $1"
}

valid_name() {
    # Genau EINE Pfadkomponente — kein Slash, kein '..', nicht leer/'.'.
    no_dotdot "$1"
    case "$1" in
        */*)      die "target_name darf keinen '/' enthalten: $1" ;;
        ""|"."|"..") die "ungültiger target_name: $1" ;;
    esac
}

do_deploy() {
    { [ $# -eq 10 ] || [ $# -eq 11 ]; } || die "deploy erwartet 10 oder 11 Argumente, bekam $#"
    local repo="$1" snapshot="$2" staging="$3" include="$4" \
          ssh_key="$5" known_hosts="$6" ssh_user="$7" ssh_port="$8" \
          target_addr="$9" target_base="${10}" target_name="${11:-}"

    valid_under "$repo" "/mnt/"
    valid_under "$staging" "${STAGING_ROOT}/"
    case "$(basename "$staging")" in
        restore-*) ;;
        *) die "staging-Job-Dir muss 'restore-*' heißen, war: $(basename "$staging")" ;;
    esac
    valid_abs "$include"
    valid_abs "$target_base"
    # Optionalen Ziel-Namen früh prüfen (fail-fast vor dem restic restore).
    [ -z "$target_name" ] || valid_name "$target_name"
    [ -n "${RESTIC_PASSWORD:-}" ] || die "RESTIC_PASSWORD nicht gesetzt"
    [[ "$ssh_port" =~ ^[0-9]+$ ]] || die "ssh_port keine Zahl: $ssh_port"
    [ -f "$ssh_key" ] || die "SSH-Key nicht gefunden: $ssh_key"

    # Staging-Job-Dir anlegen (0755, damit der Pfad-Präfix für alle
    # traversierbar ist; restic füllt den Snapshot-Baum mit Original-Modes).
    install -d -o root -g root -m 0755 "$staging"

    # 1. restic restore als root → Ownership/Perms aus dem Snapshot.
    printf '[bpm-restore-deploy] restic restore %s --include %s -> %s\n' \
        "$snapshot" "$include" "$staging" >&2
    "$RESTIC_BIN" -r "$repo" restore "$snapshot" \
        --target "$staging" --include "$include" --json

    # 2. Quelle/Ziel bestimmen. cleanRel = führender Slash, kein Trailing.
    local clean_rel="/${include#/}"; clean_rel="${clean_rel%/}"
    [ -n "$clean_rel" ] || clean_rel="/"
    local src="${staging%/}${clean_rel}"

    [ -e "$src" ] || die "erwarteter Pfad im Staging fehlt: $src (Snapshot-Pfad-Mismatch?)"

    local src_arg dst_arg
    if [ -n "$target_name" ]; then
        # Restore unter frei gewähltem Namen → kein Überschreiben des
        # Originals. Ziel = <target_base>/<target_name> (Datei oder Dir).
        # (target_name wurde oben bereits validiert.)
        if [ -d "$src" ]; then
            src_arg="${src%/}/"
            dst_arg="${target_base%/}/${target_name}/"
        else
            src_arg="$src"
            dst_arg="${target_base%/}/${target_name}"
        fi
    else
        # Original-Pfadstruktur unter target_base nachbauen (Default).
        local target_path="${target_base%/}${clean_rel}"
        if [ -d "$src" ]; then
            src_arg="${src%/}/"
            dst_arg="${target_path%/}/"
        else
            src_arg="$src"
            dst_arg="$(dirname "$target_path")/"
        fi
    fi

    local ssh_cmd
    ssh_cmd="ssh -i ${ssh_key} -p ${ssh_port} -o StrictHostKeyChecking=accept-new -o BatchMode=yes -o ConnectTimeout=15 -o UserKnownHostsFile=${known_hosts} -o GlobalKnownHostsFile=/dev/null"

    printf '[bpm-restore-deploy] rsync %s -> %s@%s:%s\n' \
        "$src_arg" "$ssh_user" "$target_addr" "$dst_arg" >&2
    # -a (inkl. -o -g -p) + --numeric-ids: System-Dateien per Nummer
    # (root=0 überall), nicht per Namen → korrekte Ownership host-übergreifend.
    rsync -a --numeric-ids --info=progress2 --stats --mkpath \
        -e "$ssh_cmd" \
        "$src_arg" "${ssh_user}@${target_addr}:${dst_arg}"
}

do_cleanup() {
    [ $# -eq 1 ] || die "cleanup erwartet 1 Argument (staging_jobdir)"
    local staging="$1"
    valid_under "$staging" "${STAGING_ROOT}/"
    # rm -rf nur auf einem 'restore-*'-Job-Dir (Defense-in-Depth gegen
    # versehentliches Löschen eines Repo-/Mount-Verzeichnisses).
    case "$(basename "$staging")" in
        restore-*) ;;
        *) die "cleanup verweigert: kein 'restore-*'-Job-Dir: $staging" ;;
    esac
    [ -d "$staging" ] && rm -rf "$staging"
    printf 'cleaned=%s\n' "$staging"
}

require_root
[ $# -ge 1 ] || die "Aufruf: bpm-restore-deploy {deploy|cleanup} ..."
cmd="$1"; shift
case "$cmd" in
    deploy)  do_deploy "$@" ;;
    cleanup) do_cleanup "$@" ;;
    *)       die "unbekanntes Kommando: $cmd" ;;
esac
