feat: génération GoAccess à la demande + README complet
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
25b68df5ec
commit
16c1af4143
2 changed files with 290 additions and 36 deletions
187
README.md
187
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 <fichiers>
|
||||
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=<login>` — 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=<votre-login>`. 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,24 +145,43 @@ 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 :
|
||||
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 <STATS_LOG_FILE> --log-format=COMBINED -o <STATS_FILE> [-o <STATS_JSON>]
|
||||
```
|
||||
|
||||
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
|
||||
|
|
@ -112,12 +189,58 @@ Si `goaccess.html` est absent, la page `/stats/` affiche un bouton **Générer e
|
|||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
|
|
|||
131
scripts/deploy-app.sh
Executable file
131
scripts/deploy-app.sh
Executable file
|
|
@ -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=<valeur-aleatoire>"
|
||||
echo " ALPID_CLIENT_ID=<client-keycloak>"
|
||||
echo " ALPID_CLIENT_SECRET=<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
|
||||
Loading…
Reference in a new issue