#!/bin/sh # Installe le serveur Fanal (fanald) en une commande : # # curl -fsSL https://get.fanal.tv | sh # # Le serveur démarre VIERGE : aucune configuration, aucun secret, aucune clé à saisir ici. Il # s'annonce en mDNS sur le réseau local et c'est l'app Fanal qui l'appaire au foyer, lui donne son # nom et ses dossiers (voir docs/pairing.md). D'où une installation qui se réduit à poser le # binaire et à lancer le service. # # Variables d'environnement reconnues : # FANAL_VERSION version à installer (défaut : la dernière publiée) # FANAL_REPO dépôt GitHub des binaires (défaut : aroneus/fanal-releases) # FANAL_BASE_URL base de téléchargement alternative (miroir, ou test local) ; les fichiers # y sont attendus sous // # FANAL_PREFIX répertoire d'installation (défaut : /usr/local/bin) # # Options : # --no-service binaires seuls, ni service ni mise à jour automatique # --no-auto-update installe le service, mais pas la vérification quotidienne # --force réinstalle même si la version en place est déjà la bonne # --uninstall retire binaires, service et minuterie set -eu REPO="${FANAL_REPO:-aroneus/fanal-releases}" PREFIX="${FANAL_PREFIX:-/usr/local/bin}" VERSION="${FANAL_VERSION:-}" BASE_URL="${FANAL_BASE_URL:-}" WITH_SERVICE=1 WITH_AUTO_UPDATE=1 FORCE=0 UNINSTALL=0 for arg in "$@"; do case "$arg" in --no-service) WITH_SERVICE=0 ;; --no-auto-update) WITH_AUTO_UPDATE=0 ;; --force) FORCE=1 ;; --uninstall) UNINSTALL=1 ;; --version=*) VERSION="${arg#--version=}" ;; -h | --help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "option inconnue : $arg" >&2 exit 2 ;; esac done # Emplacement de la copie locale du script, celle que la minuterie exécute. libdir() { if [ "$OS" = "darwin" ]; then echo "$HOME/Library/Application Support/Fanal" else echo "/usr/local/lib/fanal" fi } has_systemd() { command -v systemctl >/dev/null 2>&1; } say() { printf '\033[1m→\033[0m %s\n' "$*"; } warn() { printf '\033[33m!\033[0m %s\n' "$*" >&2; } die() { printf '\033[31m✗\033[0m %s\n' "$*" >&2 exit 1 } # Élévation : on ne la demande que si le répertoire cible ne nous appartient pas déjà. SUDO="" need_root() { [ "$(id -u)" -eq 0 ] && return 0 command -v sudo >/dev/null 2>&1 || die "droits administrateur requis, et sudo est absent." SUDO="sudo" } # ─────────────────────────────── Plateforme ─────────────────────────────── detect_platform() { os="$(uname -s)" arch="$(uname -m)" case "$os" in Linux) OS="linux" ;; Darwin) OS="darwin" ;; *) die "système non pris en charge : $os (Linux et macOS seulement)." ;; esac case "$arch" in x86_64 | amd64) ARCH="amd64" ;; aarch64 | arm64) ARCH="arm64" ;; *) die "architecture non prise en charge : $arch (amd64 et arm64 seulement)." ;; esac } # ──────────────────────────────── Désinstallation ───────────────────────── do_uninstall() { if [ "$OS" = "linux" ] && [ -f /etc/systemd/system/fanald.service ]; then need_root $SUDO systemctl disable --now fanald-update.timer 2>/dev/null || true $SUDO systemctl disable --now fanald 2>/dev/null || true $SUDO rm -f /etc/systemd/system/fanald.service \ /etc/systemd/system/fanald-update.service /etc/systemd/system/fanald-update.timer $SUDO systemctl daemon-reload say "service et minuterie systemd retirés" fi if [ "$OS" = "darwin" ]; then for label in tv.fanal.fanald tv.fanal.update; do launchctl bootout "gui/$(id -u)/$label" 2>/dev/null || true rm -f "$HOME/Library/LaunchAgents/$label.plist" done say "agents launchd retirés" fi rm -rf "$(libdir)" 2>/dev/null || $SUDO rm -rf "$(libdir)" 2>/dev/null || true need_root $SUDO rm -f "$PREFIX/fanald" "$PREFIX/fanalctl" say "binaires retirés" warn "l'état local (serverId, jeton d'appairage) est conservé — supprime-le à la main si tu veux repartir de zéro :" [ "$OS" = "linux" ] && echo " sudo rm -rf /var/lib/fanal" || echo " rm -rf ~/Library/Application\\ Support/Fanal" exit 0 } # ──────────────────────────────── Téléchargement ────────────────────────── resolve_version() { [ -n "$VERSION" ] && return 0 [ -n "$BASE_URL" ] && die "avec FANAL_BASE_URL, précise aussi FANAL_VERSION." say "recherche de la dernière version…" VERSION="$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)" [ -n "$VERSION" ] || die "impossible de déterminer la dernière version. Force-la : FANAL_VERSION=v0.1.0" } # Version actuellement installée, ou vide s'il n'y a rien. installed_version() { [ -x "$PREFIX/fanald" ] || return 0 "$PREFIX/fanald" --version 2>/dev/null | awk '{print $2}' } # La minuterie appelle ce script chaque jour : l'immense majorité des exécutions ne doivent rien # télécharger ni redémarrer quoi que ce soit. up_to_date() { [ "$FORCE" -eq 1 ] && return 1 current="$(installed_version)" [ -n "$current" ] && [ "$current" = "$VERSION" ] } download() { if [ -n "$BASE_URL" ]; then base="$BASE_URL/$VERSION" else base="https://github.com/$REPO/releases/download/$VERSION" fi TMP="$(mktemp -d)" # Le répertoire temporaire disparaît quoi qu'il arrive, y compris sur interruption. trap 'rm -rf "$TMP"' EXIT INT TERM say "téléchargement de Fanal $VERSION ($OS/$ARCH)…" for bin in fanald fanalctl; do curl -fsSL -o "$TMP/$bin" "$base/$bin-$OS-$ARCH" || die "téléchargement de $bin-$OS-$ARCH impossible depuis $base" done # Le script est publié avec les binaires et porte lui aussi une empreinte : la mise à jour # quotidienne ne télécharge donc RIEN qui ne soit vérifié contre le SHA256SUMS de la version. curl -fsSL -o "$TMP/install.sh" "$base/install.sh" || die "script d'installation introuvable dans la version $VERSION." curl -fsSL -o "$TMP/SHA256SUMS" "$base/SHA256SUMS" || die "sommes de contrôle introuvables — installation interrompue." # Vérification obligatoire : on n'installe jamais un binaire dont l'empreinte ne correspond # pas à celle publiée. Un échec ici signifie un téléchargement corrompu ou altéré. say "vérification des empreintes…" verify fanald "fanald-$OS-$ARCH" verify fanalctl "fanalctl-$OS-$ARCH" verify install.sh install.sh } # Compare l'empreinte d'un fichier téléchargé à celle publiée. Un écart interrompt tout : on # n'installe jamais un fichier qui ne correspond pas à la version annoncée. verify() { file="$1" name="$2" expected="$(sed -n "s/^\([0-9a-f]\{64\}\)[[:space:]]*[*]\{0,1\}$name\$/\1/p" "$TMP/SHA256SUMS")" [ -n "$expected" ] || die "aucune empreinte publiée pour $name." if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "$TMP/$file" | cut -d' ' -f1)" else actual="$(shasum -a 256 "$TMP/$file" | cut -d' ' -f1)" fi [ "$actual" = "$expected" ] || die "empreinte incorrecte pour $name (attendu $expected, obtenu $actual)." } install_binaries() { # Un préfixe déjà accessible en écriture (installation utilisateur) n'exige aucune élévation. if [ -w "$PREFIX" ] || { [ ! -d "$PREFIX" ] && [ -w "$(dirname "$PREFIX")" ]; }; then SUDO="" else need_root fi $SUDO install -d -m 0755 "$PREFIX" for bin in fanald fanalctl; do $SUDO install -m 0755 "$TMP/$bin" "$PREFIX/$bin" done say "installé dans $PREFIX ($("$PREFIX/fanald" --version))" } # Dépose la copie vérifiée du script, celle que la minuterie exécutera. Elle est remplacée à # chaque mise à jour réussie : le script s'améliore donc avec les versions, la nouvelle mouture # prenant la main au passage suivant de la minuterie. install_updater_script() { dir="$(libdir)" $SUDO install -d -m 0755 "$dir" $SUDO install -m 0755 "$TMP/install.sh" "$dir/install.sh" } install_update_timer_systemd() { has_systemd || return 0 # sans systemd, il n'y a pas de minuterie à poser need_root $SUDO tee /etc/systemd/system/fanald-update.service >/dev/null </dev/null <<'UNIT' [Unit] Description=Vérifie chaque nuit les mises à jour de Fanal [Timer] OnCalendar=*-*-* 04:00:00 RandomizedDelaySec=2h Persistent=true [Install] WantedBy=timers.target UNIT $SUDO systemctl daemon-reload $SUDO systemctl enable --now fanald-update.timer say "mise à jour automatique activée (chaque nuit, vers 4 h)" } install_update_timer_launchd() { dir="$(libdir)" plist="$HOME/Library/LaunchAgents/tv.fanal.update.plist" cat >"$plist" < Labeltv.fanal.update ProgramArguments$dir/install.sh StartCalendarIntervalHour4 StandardErrorPath$dir/update.log PLIST launchctl bootout "gui/$(id -u)/tv.fanal.update" 2>/dev/null || true launchctl bootstrap "gui/$(id -u)" "$plist" say "mise à jour automatique activée (chaque nuit, vers 4 h)" } # La minuterie tourne sans surveillance : elle ne peut donc pas réclamer de mot de passe. Si # l'installation exige une élévation, la mise à jour ne peut pas être automatique — mieux vaut le # dire que d'installer une minuterie qui échouerait en silence chaque jour. install_auto_update() { if [ "$OS" = "linux" ]; then install_update_timer_systemd return 0 fi if [ -w "$PREFIX" ]; then install_update_timer_launchd else warn "mise à jour automatique impossible : $PREFIX exige une élévation de droits." warn "réinstalle dans un répertoire qui t'appartient pour en profiter :" echo " FANAL_PREFIX=\$HOME/.local/bin sh install.sh" fi } # ──────────────────────────────── Services ──────────────────────────────── install_systemd() { has_systemd || { warn "systemd absent : ni service ni mise à jour automatique. Lance le serveur à la main :" echo " $PREFIX/fanald" echo " # et relance ce script de temps en temps pour mettre à jour" return 0 } need_root # Utilisateur système dédié : le démon n'a aucune raison de tourner en root. id fanal >/dev/null 2>&1 || $SUDO useradd --system --home-dir /var/lib/fanal \ --shell /usr/sbin/nologin fanal 2>/dev/null || $SUDO adduser --system --home /var/lib/fanal --no-create-home fanal 2>/dev/null || true $SUDO install -d -o fanal -g fanal -m 0755 /var/lib/fanal $SUDO install -d -m 0755 /etc/fanal # Config locale minimale : elle ne contient QUE des chemins, jamais de secret. Le reste # (dossiers, clé TMDB, nom) descend du foyer après appairage. if [ ! -f /etc/fanal/config.yaml ]; then printf 'stateDir: /var/lib/fanal\ncachePath: /var/lib/fanal/cache.sqlite\n' | $SUDO tee /etc/fanal/config.yaml >/dev/null fi $SUDO tee /etc/systemd/system/fanald.service >/dev/null <"$support/config.yaml" plist="$HOME/Library/LaunchAgents/tv.fanal.fanald.plist" cat >"$plist" < Labeltv.fanal.fanald ProgramArguments $PREFIX/fanald --config$support/config.yaml RunAtLoad KeepAlive StandardErrorPath$support/fanald.log PLIST # bootout puis bootstrap : c'est aussi ce qui recharge le binaire après une mise à jour. launchctl bootout "gui/$(id -u)/tv.fanal.fanald" 2>/dev/null || true launchctl bootstrap "gui/$(id -u)" "$plist" say "agent launchd démarré (journal : $support/fanald.log)" } # ────────────────────────────────── Marche ──────────────────────────────── detect_platform [ "$UNINSTALL" -eq 1 ] && do_uninstall command -v curl >/dev/null 2>&1 || die "curl est requis." resolve_version if up_to_date; then say "Fanal $VERSION est déjà installé — rien à faire." exit 0 fi download install_binaries if [ "$WITH_SERVICE" -eq 1 ]; then install_updater_script case "$OS" in linux) install_systemd ;; darwin) install_launchd ;; esac [ "$WITH_AUTO_UPDATE" -eq 1 ] && install_auto_update fi cat <<'DONE' Fanal est installé. Prochaine étape : ouvre l'app Fanal sur ton iPhone, ton iPad ou ton Mac, en étant sur le même réseau. Le serveur s'y annonce tout seul — tu le nommes et tu choisis ses dossiers depuis l'app. Rien d'autre à configurer ici. DONE