#!/usr/bin/env sh set -eu APP_VERSION="1.42.0" ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) MODE_FILE="$ROOT_DIR/.vendoo-install-mode" ENV_FILE="$ROOT_DIR/.env" ENV_EXAMPLE="$ROOT_DIR/.env.example" info() { printf '\033[36m[INFO]\033[0m %s\n' "$*"; } ok() { printf '\033[32m[OK]\033[0m %s\n' "$*"; } warn() { printf '\033[33m[WARN]\033[0m %s\n' "$*"; } err() { printf '\033[31m[ERROR]\033[0m %s\n' "$*" >&2; } has_command() { command -v "$1" >/dev/null 2>&1; } get_mode() { if [ -f "$MODE_FILE" ]; then tr -d '\r\n' < "$MODE_FILE"; else printf 'docker'; fi } set_mode() { printf '%s\n' "$1" > "$MODE_FILE" chmod 600 "$MODE_FILE" 2>/dev/null || true } compose_files() { case "$(get_mode)" in nas) printf '%s:%s' "$ROOT_DIR/compose.yaml" "$ROOT_DIR/compose.bind-mounts.example.yaml" ;; vps) printf '%s' "$ROOT_DIR/compose.vps.yaml" ;; *) printf '%s' "$ROOT_DIR/compose.yaml" ;; esac } compose() { COMPOSE_FILE=$(compose_files) docker compose "$@" } validate_env_value() { case "$2" in *'\n'*|*'\r'*) err "Ungültiger mehrzeiliger Wert für $1."; return 1 ;; esac } set_env() { key=$1 value=$2 validate_env_value "$key" "$value" tmp="$ENV_FILE.tmp.$$" if grep -q "^${key}=" "$ENV_FILE" 2>/dev/null; then awk -v k="$key" -v v="$value" 'BEGIN{FS="="} $1==k{print k"="v; next} {print}' "$ENV_FILE" > "$tmp" else cat "$ENV_FILE" > "$tmp" printf '\n%s=%s\n' "$key" "$value" >> "$tmp" fi chmod 600 "$tmp" 2>/dev/null || true mv "$tmp" "$ENV_FILE" } get_env() { key=$1 sed -n "s/^${key}=//p" "$ENV_FILE" 2>/dev/null | tail -n 1 } generate_token() { if has_command openssl; then openssl rand -hex 32 elif [ -r /dev/urandom ]; then od -An -N32 -tx1 /dev/urandom | tr -d ' \n' else date +%s | sha256sum | awk '{print $1}' fi } check_docker() { if ! has_command docker; then err "Docker ist auf diesem Zielsystem nicht installiert." printf '%s\n' "Installiere Docker Engine und das Compose-Plugin nach der offiziellen Docker-Dokumentation und starte setup.sh danach erneut." return 1 fi if ! docker compose version >/dev/null 2>&1; then err "Das Docker-Compose-Plugin fehlt." return 1 fi if ! docker info >/dev/null 2>&1; then err "Docker ist installiert, aber der Dienst ist nicht erreichbar oder dein Benutzer hat keine Berechtigung." return 1 fi ok "Docker und Docker Compose sind verfügbar." } ensure_env() { if [ ! -f "$ENV_FILE" ]; then cp "$ENV_EXAMPLE" "$ENV_FILE" chmod 600 "$ENV_FILE" 2>/dev/null || true ok ".env wurde automatisch aus der sicheren Vorlage erstellt." fi token=$(get_env VENDOO_SETUP_TOKEN) if [ -z "$token" ]; then token=$(generate_token) set_env VENDOO_SETUP_TOKEN "$token" ok "Ein zufälliger Erst-Admin-Setup-Code wurde erzeugt." fi set_env VENDOO_REQUIRE_SETUP_TOKEN true } read_default() { prompt=$1 default=$2 printf '%s [%s]: ' "$prompt" "$default" >&2 read -r value if [ -z "$value" ]; then printf '%s' "$default"; else printf '%s' "$value"; fi } read_yes_no() { prompt=$1 default=$2 while :; do printf '%s [%s]: ' "$prompt" "$default" read -r answer [ -n "$answer" ] || answer=$default case "$answer" in j|J|ja|JA|y|Y|yes|YES) return 0 ;; n|N|nein|NEIN|no|NO) return 1 ;; *) warn "Bitte ja oder nein eingeben." ;; esac done } validate_port() { case "$1" in ''|*[!0-9]*) return 1 ;; esac [ "$1" -ge 1024 ] && [ "$1" -le 65535 ] } validate_domain() { case "$1" in http://*|https://*|*/*|*:*|'') return 1 ;; *.*) return 0 ;; *) return 1 ;; esac } configure_wizard() { ensure_env printf '\nVendoo %s – geführte Installation\n\n' "$APP_VERSION" printf ' 1 Docker auf Linux/macOS/Docker Desktop (nur lokal erreichbar)\n' printf ' 2 NAS / Portainer / privates LAN (sichtbare Runtime-Ordner)\n' printf ' 3 VPS / öffentlicher Server mit automatischem HTTPS\n' printf ' 0 Abbrechen\n\n' printf 'Installationsziel: ' read -r choice case "$choice" in 1) mode=docker ;; 2) mode=nas ;; 3) mode=vps ;; 0) return 1 ;; *) err "Ungültige Auswahl."; return 1 ;; esac set_mode "$mode" current_port=$(get_env VENDOO_HOST_PORT) [ -n "$current_port" ] || current_port=8124 port=$(read_default "Port" "$current_port") validate_port "$port" || { err "Port muss zwischen 1024 und 65535 liegen."; return 1; } set_env PORT "$port" set_env VENDOO_HOST_PORT "$port" set_env VENDOO_COOKIE_SAMESITE lax set_env VENDOO_ALLOW_ANY_EXTENSION_ORIGIN false case "$mode" in docker) set_env VENDOO_PUBLISH_ADDRESS 127.0.0.1 set_env VENDOO_BIND_HOST 0.0.0.0 set_env VENDOO_TRUST_PROXY false set_env VENDOO_COOKIE_SECURE auto set_env PUBLIC_BASE_URL "" ;; nas) mkdir -p "$ROOT_DIR/runtime/db" "$ROOT_DIR/runtime/uploads" "$ROOT_DIR/runtime/backups" "$ROOT_DIR/runtime/operations" "$ROOT_DIR/runtime/logs" "$ROOT_DIR/runtime/config" "$ROOT_DIR/runtime/modules" chmod -R u+rwX "$ROOT_DIR/runtime" 2>/dev/null || true set_env VENDOO_PUBLISH_ADDRESS 0.0.0.0 set_env VENDOO_BIND_HOST 0.0.0.0 set_env VENDOO_TRUST_PROXY false set_env VENDOO_COOKIE_SECURE auto printf 'LAN-Basisadresse für QR-Uploads, z. B. http://192.168.1.20:%s (optional): ' "$port" read -r public_url case "$public_url" in '') set_env PUBLIC_BASE_URL "" ;; http://*|https://*) set_env PUBLIC_BASE_URL "${public_url%/}" ;; *) err "Adresse muss mit http:// oder https:// beginnen."; return 1 ;; esac warn "NAS-Modus veröffentlicht Port $port im privaten LAN. Nicht direkt aus dem Internet freigeben." ;; vps) while :; do printf 'Öffentliche Domain ohne https://, z. B. vendoo.example.de: ' read -r domain validate_domain "$domain" && break warn "Bitte einen vollständigen Domainnamen ohne Protokoll oder Pfad eingeben." done set_env VENDOO_DOMAIN "$domain" set_env PUBLIC_BASE_URL "https://$domain" set_env VENDOO_PUBLISH_ADDRESS 127.0.0.1 set_env VENDOO_BIND_HOST 0.0.0.0 set_env VENDOO_TRUST_PROXY true set_env VENDOO_COOKIE_SECURE true set_env VENDOO_ALLOWED_HOSTS "$domain" info "Caddy übernimmt HTTPS automatisch. DNS muss auf diesen Server zeigen; Ports 80 und 443 müssen erreichbar sein." ;; esac if read_yes_no "Lokales/externes FLUX aktivieren?" "nein"; then set_env LOCAL_IMAGE_ENABLED true default_flux=http://host.docker.internal:8188 flux_url=$(read_default "FLUX/ComfyUI-Adresse" "$default_flux") case "$flux_url" in http://*|https://*) set_env LOCAL_IMAGE_URL "${flux_url%/}" ;; *) err "FLUX-Adresse muss mit http:// oder https:// beginnen."; return 1 ;; esac else set_env LOCAL_IMAGE_ENABLED false fi token=$(get_env VENDOO_SETUP_TOKEN) printf '\nERST-ADMIN-SETUP-CODE:\n%s\n\n' "$token" warn "Code bis zur ersten Anmeldung sicher aufbewahren. AI- und Marktplatz-Schlüssel werden später in Vendoo → Einstellungen gepflegt." ok "Konfiguration gespeichert; keine Datei musste manuell bearbeitet werden." } wait_ready() { case "$(get_mode)" in vps) url="https://$(get_env VENDOO_DOMAIN)/readyz" ;; *) url="http://127.0.0.1:$(get_env VENDOO_HOST_PORT)/readyz" ;; esac if ! has_command curl; then warn "curl fehlt; Readiness später im Browser prüfen: $url"; return 0; fi i=0 while [ "$i" -lt 30 ]; do if curl -fsS --max-time 5 "$url" >/dev/null 2>&1; then ok "Vendoo ist bereit: $url"; return 0; fi i=$((i+1)); sleep 2 done warn "Readiness war noch nicht erfolgreich. Prüfe: compose logs --tail=200" return 0 } install_or_update() { check_docker configure_wizard || return 0 info "Prüfe Compose-Konfiguration …" compose config >/dev/null ok "Compose-Konfiguration ist gültig." info "Baue und starte Vendoo …" compose build --pull compose up -d --remove-orphans compose ps wait_ready } start_app() { check_docker; compose up -d; compose ps; wait_ready; } stop_app() { check_docker; compose down; } update_app() { check_docker ensure_env info "Erstelle vor dem Update automatisch ein Backup …" backup_app compose config >/dev/null compose build --pull compose up -d --remove-orphans compose ps wait_ready } status_app() { check_docker compose ps printf '\nReadiness:\n' case "$(get_mode)" in vps) url="https://$(get_env VENDOO_DOMAIN)/readyz" ;; *) url="http://127.0.0.1:$(get_env VENDOO_HOST_PORT)/readyz" ;; esac if has_command curl; then curl -fsS "$url" || true; printf '\n'; else warn "curl fehlt; Readiness bitte im Browser prüfen: $url"; fi } backup_app() { check_docker COMPOSE_FILE=$(compose_files) "$ROOT_DIR/scripts/docker-backup.sh" } restore_app() { printf 'Pfad zum Backup-TAR.GZ: ' read -r backup [ -f "$backup" ] || { err "Backup nicht gefunden."; return 1; } check_docker COMPOSE_FILE=$(compose_files) "$ROOT_DIR/scripts/docker-restore.sh" "$backup" --confirm wait_ready } show_setup_code() { ensure_env printf '\nErst-Admin-Setup-Code:\n%s\n\n' "$(get_env VENDOO_SETUP_TOKEN)" } menu() { while :; do printf '\nVendoo %s – Zero-Edit Deployment Assistant\n\n' "$APP_VERSION" printf ' 1 Installieren / Konfiguration ändern\n' printf ' 2 Starten\n' printf ' 3 Stoppen\n' printf ' 4 Status und Readiness\n' printf ' 5 Aktualisieren (mit Sicherheitsbackup)\n' printf ' 6 Backup erstellen\n' printf ' 7 Backup wiederherstellen\n' printf ' 8 Erst-Admin-Code anzeigen\n' printf ' 0 Beenden\n\n' printf 'Auswahl: ' read -r choice case "$choice" in 1) install_or_update ;; 2) start_app ;; 3) stop_app ;; 4) status_app ;; 5) update_app ;; 6) backup_app ;; 7) restore_app ;; 8) show_setup_code ;; 0) exit 0 ;; *) warn "Ungültige Auswahl." ;; esac done } case "${1:-menu}" in install|configure) install_or_update ;; start) start_app ;; stop) stop_app ;; status) status_app ;; update) update_app ;; backup) backup_app ;; restore) restore_app ;; setup-code) show_setup_code ;; menu) menu ;; *) err "Unbekannter Befehl: $1"; exit 2 ;; esac