#!/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