329 lines
10 KiB
Bash
329 lines
10 KiB
Bash
#!/usr/bin/env sh
|
||
set -eu
|
||
|
||
APP_VERSION="1.41.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
|