Private
Public Access
146 lines
5.1 KiB
Bash
Executable File
146 lines
5.1 KiB
Bash
Executable File
#!/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'
|
|
)
|
|
# One SSH authentication for the entire upload. The socket is private and only
|
|
# lives for this invocation; no persistent key or password is written.
|
|
control_dir=
|
|
control_path=/tmp/halloween-ssh-VORSCHAU/control
|
|
master_started=false
|
|
cleanup_connection() {
|
|
if $master_started; then
|
|
ssh -o BatchMode=yes -S "$control_path" -O exit "$target" >/dev/null 2>&1 || true
|
|
fi
|
|
if [[ -n $control_dir ]]; then
|
|
rm -f -- "$control_path"
|
|
rmdir -- "$control_dir" 2>/dev/null || true
|
|
fi
|
|
}
|
|
if $apply; then
|
|
control_dir=$(mktemp -d /tmp/halloween-ssh-XXXXXXXX)
|
|
chmod 700 "$control_dir"
|
|
control_path="$control_dir/control"
|
|
trap cleanup_connection EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
fi
|
|
master_cmd=(ssh -M -N -f -o ControlPersist=no -o BatchMode=no
|
|
-o StrictHostKeyChecking=ask -o "ControlPath=$control_path" "$target")
|
|
# If the shared connection is lost, refuse password fallback on later steps.
|
|
ssh_options=(-o StrictHostKeyChecking=ask -o ControlMaster=no
|
|
-o "ControlPath=$control_path" -o BatchMode=yes)
|
|
ssh_cmd=(ssh "${ssh_options[@]}" "$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 -o ControlMaster=no -o ControlPath=$control_path -o BatchMode=yes"
|
|
-- "$source_dir/" "$target:$remote_dir/releases/$release/"
|
|
)
|
|
service_cmd=(ssh -tt "${ssh_options[@]}" "$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' "${master_cmd[@]}"; printf '\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
|
|
|
|
printf 'SSH-Verbindung einmal anmelden; danach folgt nur noch die sudo-Abfrage auf dem Pi.\n'
|
|
"${master_cmd[@]}"
|
|
master_started=true
|
|
"${ssh_cmd[@]}"
|
|
"${rsync_cmd[@]}"
|
|
printf 'Übertragung abgeschlossen.\n'
|
|
"${service_cmd[@]}"
|