From 16c1af414392cc581457a720be084ee6a21a9d32 Mon Sep 17 00:00:00 2001 From: Alpinux Date: Sun, 3 May 2026 20:39:25 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20g=C3=A9n=C3=A9ration=20GoAccess=20?= =?UTF-8?q?=C3=A0=20la=20demande=20+=20README=20complet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stats.html : bouton "Générer et ouvrir" avec polling async ; supprime le window.open automatique (bloqué par les navigateurs) - app.py : routes POST /stats/generate et GET /stats/status ; exécution GoAccess en thread daemon, verrou anti-doublon - .env.example : documente STATS_LOG_FILE et STATS_GENERATE_CMD - README.md : flux de publication local→git→serveur, variables d'env, procédure première installation Co-Authored-By: Claude Sonnet 4.6 --- README.md | 195 ++++++++++++++++++++++++++++++++++-------- scripts/deploy-app.sh | 131 ++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+), 36 deletions(-) create mode 100755 scripts/deploy-app.sh diff --git a/README.md b/README.md index 9889e77..d977403 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,56 @@ # static.alpinux.org CDN pour les fichiers binaires et assets statiques de l'association : logos, favicons, images Open Graph. +Tableau de bord d'administration accessible sur `https://static.alpinux.org` (authentification AlpID). + +--- + +## Flux de publication + +Il y a **deux cibles** distinctes sur le serveur, gérées par des scripts séparés : + +| Quoi | Destination serveur | Script | Passe par git ? | +|------|--------------------|---------|----| +| Assets CDN (logo/, wiki/, stats/, error/…) | `/var/www/clients/client1/web17/web` | `push-assets.sh` | Non — rsync direct | +| App Flask (`app/`) | `/opt/static-cdn/` + redémarrage service | `deploy-app.sh` | Non — rsync direct | + +Git sert à **versionner et sauvegarder** le code sur Gitea. Il n'est pas dans la boucle de déploiement : les scripts rsynchent depuis le dépôt local, pas depuis Gitea. + +### Modifier les assets CDN (logo, wiki, stats…) + +```bash +# 1. Modifier les fichiers dans static/ +# 2. Versionner (optionnel mais recommandé) +git add +git commit -m "..." +git push # sauvegarde sur gitea.alpinux.org + +# 3. Pousser sur le serveur +./scripts/push-assets.sh # aperçu + confirmation +./scripts/push-assets.sh -y # sans confirmation +``` + +> `push-assets.sh` exclut automatiquement `app/`, `scripts/`, `.git/`, `.env`, `README.md`. +> Il envoie : `logo/`, `wiki/`, `stats/`, `error/`, `favicon.ico`, `robots.txt`, `standard_index.html`. + +### Modifier l'app Flask (`app/`) + +```bash +# 1. Modifier les fichiers dans static/app/ +# 2. Versionner +git add app/ +git commit -m "..." +git push # sauvegarde sur gitea.alpinux.org + +# 3. Déployer sur le serveur +./scripts/deploy-app.sh # rsync + pip install + restart service +./scripts/deploy-app.sh -n # dry-run +``` + +> `deploy-app.sh` rsync `app/` vers `/opt/static-cdn/`, met à jour le venv Python, +> et redémarre le service systemd `static-cdn`. + +--- ## Contenu hébergé @@ -13,48 +63,50 @@ CDN pour les fichiers binaires et assets statiques de l'association : logos, fav | `logo/favicon.ico` | Favicon ICO multi-taille | | `wiki/` | Images pour wiki.alpinux.org | -Ces fichiers sont synchronisés sur le serveur via les scripts `push-assets.sh` / `pull-assets.sh`. Ils ne sont pas versionnés dans ce dépôt. +--- -## Configuration +## Configuration locale (`.env`) ```bash cp .env.example .env ``` -Editer `.env` : - | Variable | Obligatoire | Description | |----------|-------------|-------------| | `STATIC_HOST` | oui | Alias SSH ou nom d'hôte (`alpinux.org`) | | `STATIC_PATH` | oui | Chemin absolu du web root sur le serveur | | `LOCAL_ASSETS_DIR` | oui | Chemin absolu du dépôt local (`static/`) | -| `STATIC_USER` | non | Login SSH — laisser vide si `~/.ssh/config` définit l'utilisateur pour cet hôte | +| `STATIC_USER` | non | Login SSH — laisser vide si `~/.ssh/config` définit l'utilisateur | -### Avec `~/.ssh/config` (recommandé) +Si l'hôte est déclaré dans `~/.ssh/config` (recommandé), laisser `STATIC_USER` vide. +Sinon, renseigner `STATIC_USER=` — les scripts construisent alors `USER@HOST:PATH`. -Si l'hôte est déclaré dans `~/.ssh/config` (ex : `Host alpinux.org` avec `User`, `IdentityFile`, etc.), laisser `STATIC_USER` vide. Les scripts utilisent l'alias directement. - -### Sans `~/.ssh/config` - -Renseigner `STATIC_USER=`. Les scripts construisent alors `USER@HOST:PATH`. +--- ## Scripts ```bash -./scripts/pull-assets.sh # aperçu des changements + confirmation -./scripts/pull-assets.sh -y # récupère sans confirmation +# Synchroniser les assets CDN +./scripts/pull-assets.sh # serveur → local (aperçu + confirmation) +./scripts/pull-assets.sh -y # sans confirmation ./scripts/pull-assets.sh -n # dry-run -./scripts/push-assets.sh # aperçu des changements + confirmation -./scripts/push-assets.sh -y # pousse sans confirmation +./scripts/push-assets.sh # local → serveur (aperçu + confirmation) +./scripts/push-assets.sh -y # sans confirmation ./scripts/push-assets.sh -n # dry-run + +# Déployer l'app Flask +./scripts/deploy-app.sh # déploie et redémarre le service +./scripts/deploy-app.sh -n # dry-run ``` -`push-assets.sh` n'envoie que les fichiers nouveaux ou modifiés ; il ne supprime jamais rien sur le serveur. +--- ## Tableau de bord Flask (`app/`) -Application Flask déployée sur le serveur via `scripts/deploy-app.sh`. Accessible sur `static.alpinux.org` (proxy Apache → `127.0.0.1:5003`). +Application Flask déployée sur le serveur via `deploy-app.sh`. +Accessible sur `https://static.alpinux.org` (proxy Apache → `127.0.0.1:5003`). +Authentification SSO via AlpID (Keycloak). ### Fonctionnalités @@ -67,7 +119,9 @@ Application Flask déployée sur le serveur via `scripts/deploy-app.sh`. Accessi ### Upload de fichiers -Dans le navigateur (`/browse/`), une zone de dépôt est affichée en bas de chaque dossier. Elle accepte plusieurs fichiers à la fois (glisser-déposer ou sélection). Les fichiers sont écrits dans le dossier affiché. Les chemins protégés (`.git`, `app`, `scripts`, etc.) sont refusés avec 403. +Dans le navigateur (`/browse/`), une zone de dépôt est affichée en bas de chaque dossier. +Elle accepte plusieurs fichiers à la fois (glisser-déposer ou sélection). +Les chemins protégés (`.git`, `app`, `scripts`, etc.) sont refusés avec 403. Pour limiter la taille des uploads, ajouter dans `app/app.py` : @@ -75,7 +129,11 @@ Pour limiter la taille des uploads, ajouter dans `app/app.py` : app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50 Mo ``` -### Variables d'environnement +### Variables d'environnement de l'app + +Le fichier de référence est `app/.env.example`. Sur le serveur : `/opt/static-cdn/.env`. + +**Auth & Flask** | Variable | Obligatoire | Description | |----------|-------------|-------------| @@ -87,37 +145,102 @@ app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50 Mo | `ADMIN_EMAILS` | non | Fallback si le claim `groups` est absent du token | | `ASSETS_ROOT` | oui | Racine du CDN (`/var/www/clients/client1/web17/web` en prod) | -### Variables d'environnement spécifiques aux stats +**Stats GoAccess** | Variable | Description | |----------|-------------| -| `STATS_FILE` | Chemin du rapport HTML GoAccess (servi dans l'iframe et l'onglet) | -| `STATS_JSON` | Chemin du JSON GoAccess (badges "Vues" dans le navigateur) | +| `STATS_FILE` | Rapport HTML GoAccess servi dans `/stats/` (défaut : `/opt/static-cdn/goaccess.html`) | +| `STATS_JSON` | Rapport JSON GoAccess pour les badges "Vues" (défaut : `/opt/static-cdn/goaccess.json`) | | `STATS_LOG_FILE` | Log Apache à analyser pour la génération à la demande | -| `STATS_GENERATE_CMD` | Commande complète de génération (remplace la commande par défaut) | +| `STATS_GENERATE_CMD` | Commande GoAccess complète — remplace la commande par défaut si renseigné | -### Génération du rapport à la demande +### Génération du rapport GoAccess à la demande Si `goaccess.html` est absent, la page `/stats/` affiche un bouton **Générer et ouvrir** : + 1. Lance GoAccess en arrière-plan (thread daemon) 2. Interroge `/stats/status` toutes les 2 s 3. Ouvre le rapport dans un nouvel onglet dès qu'il est prêt -**Prérequis serveur :** -- `abonnelc` doit appartenir au groupe `client1` pour lire les logs ISPConfig : - ```bash - sudo usermod -a -G client1 abonnelc - sudo systemctl restart static-cdn - ``` +Si le rapport existe, un bouton **↗ Ouvrir dans un nouvel onglet** l'affiche directement. + +La commande par défaut (si `STATS_GENERATE_CMD` est vide) : + +``` +goaccess --log-format=COMBINED -o [-o ] +``` + +Valeurs utilisées sur le serveur : + +``` +STATS_LOG_FILE=/var/log/ispconfig/httpd/static.alpinux.org/access.log +STATS_GENERATE_CMD=goaccess /var/log/ispconfig/httpd/static.alpinux.org/access.log \ + --config-file=/var/log/ispconfig/httpd/static.alpinux.org/goaccess.conf \ + -o /opt/static-cdn/goaccess.html -o /opt/static-cdn/goaccess.json +``` + +**Prérequis serveur :** l'utilisateur `abonnelc` doit appartenir au groupe `client1` +pour lire les logs ISPConfig (opération à faire une seule fois) : + +```bash +sudo usermod -a -G client1 abonnelc +sudo systemctl restart static-cdn +``` --- -## Déploiement serveur (ISPConfig) +## Première installation (nouveau serveur) -Le sous-domaine est créé via **ISPConfig** (`https://owni.alpinux.org:8080`) : +### 1. Créer le site dans ISPConfig -1. *Sites → Ajouter un site web* — domaine `static.alpinux.org` -2. Activer **Let's Encrypt SSL** dans l'onglet SSL -3. Pointer le DocumentRoot vers le répertoire contenant les assets +`https://owni.alpinux.org:8080` → *Sites → Ajouter un site web* -ISPConfig gère le VirtualHost et le certificat. Voir `../infra/static/` pour la configuration Apache de référence. +- Domaine : `static.alpinux.org` +- Activer **Let's Encrypt SSL** +- DocumentRoot : `/var/www/clients/client1/web17/web` + +### 2. Configurer le proxy Apache + +Dans ISPConfig → onglet **Directives Apache SSL** du site : + +```apache +# CDN public → Apache sert directement depuis DocumentRoot +ProxyPass /logo/ ! +ProxyPass /wiki/ ! +ProxyPass /error/ ! +ProxyPass /favicon.ico ! +ProxyPass /robots.txt ! + +# Tableau de bord → Flask +RequestHeader set X-Forwarded-Proto "https" +ProxyPreserveHost On +ProxyPass / http://127.0.0.1:5003/ +ProxyPassReverse / http://127.0.0.1:5003/ +``` + +### 3. Déployer l'app Flask + +```bash +# Depuis la machine locale +./scripts/deploy-app.sh + +# Sur le serveur — créer /opt/static-cdn/.env (voir app/.env.example) +ssh alpinux.org +nano /opt/static-cdn/.env + +# Activer le service +sudo systemctl enable --now static-cdn +``` + +### 4. Pousser les assets CDN + +```bash +./scripts/push-assets.sh -y +``` + +### 5. Droits sur les logs (pour la génération GoAccess) + +```bash +sudo usermod -a -G client1 abonnelc +sudo systemctl restart static-cdn +``` diff --git a/scripts/deploy-app.sh b/scripts/deploy-app.sh new file mode 100755 index 0000000..099608d --- /dev/null +++ b/scripts/deploy-app.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# deploy-app.sh — déploie l'app Flask (static/app/) sur static.alpinux.org +# +# Usage : +# ./deploy-app.sh # déploie et redémarre le service +# ./deploy-app.sh -n # dry-run (aucune modification) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="$SCRIPT_DIR/../app" +ENV_FILE="$SCRIPT_DIR/../.env" + +RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m' +BOLD='\033[1m'; RESET='\033[0m' + +if [ ! -f "$ENV_FILE" ]; then + echo -e "${RED}Erreur : .env introuvable. Copier .env.example et remplir.${RESET}" + exit 1 +fi +# shellcheck source=/dev/null +source "$ENV_FILE" + +REMOTE_HOST="${STATIC_HOST:?Variable STATIC_HOST manquante dans .env}" +REMOTE_DEST="/opt/static-cdn" +SERVICE_FILE="$SCRIPT_DIR/../../infra/services/static-cdn.service" + +DRY_RUN=false +for arg in "$@"; do + case "$arg" in + -n|--dry-run) DRY_RUN=true ;; + -h|--help) sed -n '2,5p' "$0" | sed 's/^# //'; exit 0 ;; + esac +done + +echo -e "${BOLD}Déploiement de l'app Flask → $REMOTE_HOST:$REMOTE_DEST${RESET}" +echo "" + +# ── 1. Rsync de l'app ──────────────────────────────────────────────── +echo -e "${CYAN}[1/4] Synchronisation des fichiers…${RESET}" +RSYNC_OPTS=(-rlcz --delete --human-readable + --exclude='.env' --exclude='__pycache__/' --exclude='*.pyc' + --exclude='venv/' --exclude='.env.example') + +if $DRY_RUN; then + rsync --dry-run --itemize-changes "${RSYNC_OPTS[@]}" "$APP_DIR/" "$REMOTE_HOST:$REMOTE_DEST/" + echo -e "${CYAN}Mode dry-run — aucune modification effectuée.${RESET}" + exit 0 +fi + +ssh "$REMOTE_HOST" "sudo mkdir -p $REMOTE_DEST && sudo chown abonnelc:abonnelc $REMOTE_DEST" +rsync "${RSYNC_OPTS[@]}" "$APP_DIR/" "$REMOTE_HOST:$REMOTE_DEST/" +echo -e " ${GREEN}✓ Fichiers copiés${RESET}" + +# ── 2. Environnement Python ────────────────────────────────────────── +echo -e "${CYAN}[2/4] Mise à jour de l'environnement Python…${RESET}" +ssh "$REMOTE_HOST" bash <<'ENDSSH' + set -e + cd /opt/static-cdn + [ ! -d venv ] && python3 -m venv venv + venv/bin/pip install --quiet --upgrade pip + venv/bin/pip install --quiet -r requirements.txt +ENDSSH +echo -e " ${GREEN}✓ Dépendances installées${RESET}" + +# ── 3. Fichier .env distant ────────────────────────────────────────── +echo -e "${CYAN}[3/4] Vérification du fichier .env distant…${RESET}" +ENV_EXISTS=$(ssh "$REMOTE_HOST" "test -f $REMOTE_DEST/.env && echo yes || echo no") +if [ "$ENV_EXISTS" = "no" ]; then + echo -e " ${RED}ATTENTION : /opt/static-cdn/.env absent sur le serveur.${RESET}" + echo " Créer le fichier avec les variables requises :" + echo " ssh $REMOTE_HOST" + echo " nano /opt/static-cdn/.env" + echo " Variables minimales :" + echo " SECRET_KEY=" + echo " ALPID_CLIENT_ID=" + echo " ALPID_CLIENT_SECRET=" + echo " ALPID_DISCOVERY_URL=https://alpid.alpinux.org/realms/master/.well-known/openid-configuration" + echo " ADMIN_EMAILS=cedric.alpinux@acemail.fr" + echo " ASSETS_ROOT=/var/www/clients/client1/web17/web" + echo " STATS_FILE=/opt/static-cdn/goaccess.html" + echo " STATS_JSON=/opt/static-cdn/goaccess.json" + echo " STATS_LOG_FILE=/var/log/ispconfig/httpd/static.alpinux.org/access.log" + echo " STATS_GENERATE_CMD=goaccess \$STATS_LOG_FILE --config-file=/var/log/ispconfig/httpd/static.alpinux.org/goaccess.conf -o \$STATS_FILE -o \$STATS_JSON" + echo "" +else + echo -e " ${GREEN}✓ .env présent${RESET}" +fi + +# ── 4. Service systemd ─────────────────────────────────────────────── +echo -e "${CYAN}[4/4] Déploiement du service systemd…${RESET}" +scp "$SERVICE_FILE" "$REMOTE_HOST:/tmp/static-cdn.service" +ssh "$REMOTE_HOST" bash <<'ENDSSH' + set -e + sudo mkdir -p /var/log/static-cdn + sudo chown abonnelc:abonnelc /var/log/static-cdn + sudo cp /tmp/static-cdn.service /etc/systemd/system/static-cdn.service + sudo systemctl daemon-reload + sudo systemctl enable static-cdn + if systemctl is-active --quiet static-cdn; then + sudo systemctl restart static-cdn + echo "Service redémarré." + else + sudo systemctl start static-cdn 2>/dev/null || true + echo "Service démarré." + fi +ENDSSH +echo -e " ${GREEN}✓ Service systemd actif${RESET}" + +echo "" +echo -e "${GREEN}✓ Déploiement terminé.${RESET}" +echo "" +echo -e "${BOLD}Étape manuelle restante — ISPConfig :${RESET}" +echo " https://owni.alpinux.org:8080 → Sites → static.alpinux.org → Directives Apache" +echo "" +echo " Coller dans le champ 'Directives Apache SSL' :" +echo "" +cat << 'EOF' + # CDN public → Apache sert directement depuis DocumentRoot + ProxyPass /logo/ ! + ProxyPass /wiki/ ! + ProxyPass /error/ ! + ProxyPass /favicon.ico ! + ProxyPass /robots.txt ! + + # Tableau de bord → Flask (inclut /stats/ avec auth SSO) + RequestHeader set X-Forwarded-Proto "https" + ProxyPreserveHost On + ProxyPass / http://127.0.0.1:5003/ + ProxyPassReverse / http://127.0.0.1:5003/ +EOF