#!/usr/bin/env bash set -euo pipefail project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P) source_dir=$project_dir pi_user= pi_host= remote_dir= apply=false bind_host= release="$(date -u +%Y%m%dT%H%M%SZ)-$(python3 -c 'import secrets; print(secrets.token_hex(4))')" usage() { cat <<'EOF' Versioniertes Pi-Update per SSH/rsync mit Autostart und automatischem Rückfall. Aufruf: bash tools/upload_to_pi.sh --user BENUTZER --host HOST \ --remote-dir /home/BENUTZER/halloween [--source ORDNER] \ [--web-host 127.0.0.1|0.0.0.0] [--apply] Ohne --apply nur Vorschau, keine Verbindung. Mit --apply neue Version unter releases/ vorbereiten, Daten sichern, Dienst umschalten und WebUI prüfen. Autostart wird immer eingerichtet (sudo auf dem Pi erforderlich). --web-host wählt SSH-Tunnel oder direkten Heimnetzzugriff; ohne Angabe bleibt die gespeicherte Wahl erhalten, erstmalig 127.0.0.1. --install-service wird als bisheriges Argument weiterhin akzeptiert. Daten, Einstellungen und Modelle unter data/ bleiben erhalten. EOF } die() { printf 'Fehler: %s\n' "$*" >&2 exit 2 } while (($#)); do case $1 in --user|--host|--remote-dir|--source|--web-host) (($# >= 2)) || die "Wert für $1 fehlt" case $1 in --user) pi_user=$2 ;; --host) pi_host=$2 ;; --remote-dir) remote_dir=$2 ;; --source) source_dir=$2 ;; --web-host) bind_host=$2 ;; esac shift 2 ;; --apply) apply=true; shift ;; --install-service) shift ;; # Compatibility: service installation is now automatic. -h|--help) usage; exit 0 ;; *) die "Unbekanntes Argument: $1" ;; esac done [[ $pi_user =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]] || die 'Gültigen --user angeben' [[ $pi_host =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || die 'Gültigen --host angeben (Name oder IPv4-Adresse)' [[ $remote_dir =~ ^/[a-zA-Z0-9_./-]+$ && $remote_dir != / ]] || die 'Sicheren absoluten --remote-dir angeben' [[ /$remote_dir/ != *'/../'* && /$remote_dir/ != *'/./'* ]] || die 'Zielpfad darf keine . oder .. Segmente enthalten' [[ -d $source_dir ]] || die "Quellordner fehlt: $source_dir" source_dir=$(cd -- "$source_dir" && pwd -P) [[ -z $bind_host || $bind_host == 127.0.0.1 || $bind_host == 0.0.0.0 ]] || die 'Ungültiger --web-host' for required in run.py requirements.txt tools/install_pi_service.sh tools/activate_pi_release.sh; do [[ -f "$source_dir/$required" ]] || die "Quelle unvollständig: $required fehlt" done command -v ssh >/dev/null || die 'ssh fehlt lokal' command -v rsync >/dev/null || die 'rsync fehlt lokal' target="$pi_user@$pi_host" excludes=( --exclude=releases/ --exclude=backups/ --exclude=current --exclude=service-host --exclude=.update.lock --exclude=.git/ --exclude=.venv/ --exclude=__pycache__/ --exclude=data/ --exclude=preview/ --exclude=references/ --exclude=.env --exclude='*.pem' --exclude='*.key' --exclude='*.p12' --exclude='*.pfx' ) ssh_cmd=(ssh -o StrictHostKeyChecking=ask "$target" "mkdir -p -- '$remote_dir/releases' && mkdir -- '$remote_dir/releases/$release'") rsync_cmd=( rsync -a --delay-updates --human-readable --info=stats2 "${excludes[@]}" -e 'ssh -o StrictHostKeyChecking=ask' -- "$source_dir/" "$target:$remote_dir/releases/$release/" ) service_cmd=(ssh -tt -o StrictHostKeyChecking=ask "$target" "bash '$remote_dir/releases/$release/tools/activate_pi_release.sh' '$remote_dir' '$release' '$bind_host'") printf 'Quelle: %s\nZiel: %s:%s/\n' "$source_dir" "$target" "$remote_dir" printf 'Ausgelassen: .git, .venv, data, preview, references, __pycache__, .env, Schlüsseldateien\n' if ! $apply; then printf '\nVorschau, noch keine Verbindung und keine Übertragung:\n' printf ' %q' "${ssh_cmd[@]}"; printf '\n' printf ' %q' "${rsync_cmd[@]}"; printf '\n' printf ' %q' "${service_cmd[@]}"; printf '\n' printf '\nMit --apply ausführen.\n' exit 0 fi "${ssh_cmd[@]}" "${rsync_cmd[@]}" printf 'Übertragung abgeschlossen.\n' "${service_cmd[@]}"