MaBibli 1.0.0

Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts,
scan de code-barres, consultation hors-ligne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mathieu Limonier
2026-08-22 22:36:16 +02:00
co-authored by Claude Opus 5
commit 6a6d745af4
207 changed files with 35543 additions and 0 deletions
+363
View File
@@ -0,0 +1,363 @@
# Installer MaBibli
Deux voies. **Seule la première apporte une authentification** — voir l'avertissement de
la section « Ailleurs que sur YunoHost ».
| | |
|---|---|
| [Le paquet YunoHost](#le-paquet-yunohost) · [Contraintes](#contraintes-permanentes-du-paquet--ce-ne-sont-pas-des-tâches) | ce qu'il contient, ce qui ne se négocie pas |
| [Première mise en production](#première-mise-en-production) | pas à pas, sur un serveur neuf |
| [Désinstaller](#désinstaller) | avec ou sans les données |
| [Ailleurs que sur YunoHost](#installer-ailleurs-que-sur-yunohost-synology-nas-vps) | Synology, NAS, VPS — sans authentification |
Pour **monter de version**, voir [publier-une-version.md](publier-une-version.md).
Pour comprendre le code, voir [architecture.md](architecture.md).
## Le paquet YunoHost
MaBibli se déploie via un **second dépôt**, [`mabibli_ynh`](https://forge.example.org/mabibli_ynh),
qui **ne contient aucun code C#** : il porte le manifeste, les configurations nginx et
systemd, et les scripts d'installation. Toute la documentation, elle, vit ici — ce
fichier est le point d'entrée unique du projet.
### Contenu
| Fichier | Rôle |
|---|---|
| `manifest.toml` | Identité, version, URL de l'archive et son `sha256`, ressources (utilisateur système, répertoires, port, permissions) |
| `conf/systemd.service` | Unité du service — écoute sur `127.0.0.1`, base dans le répertoire de données, durcissement |
| `conf/nginx.conf` | Reverse proxy et intégration SSOwat |
| `scripts/_common.sh` | Variables partagées et sauvegarde/restauration cohérente de la base SQLite |
| `scripts/install` `remove` `upgrade` `backup` `restore` | Cycle de vie de l'application |
| `build/publier.sh` | **Seul script de publication** : version, **une archive par architecture**, manifeste, commit et push du paquet, tag et push du code. `--archive-seule` compile et archive sans rien publier, pour reprendre après un échec |
| `doc/DESCRIPTION.md` `doc/ADMIN.md` | Textes affichés par YunoHost lui-même (catalogue et interface d'administration) — ils doivent rester dans ce dépôt |
### Points de conception
**Le service n'écoute que sur `127.0.0.1`.** L'application déduit l'identité de l'en-tête
`YNH_USER` injecté par SSOwat ; cet en-tête n'est digne de confiance que si nginx est le seul
chemin d'accès. Un service exposé sur le réseau permettrait de forger `YNH_USER` et de
contourner le portail. La contrainte est écrite dans `mabibli_ynh/conf/systemd.service` (`ASPNETCORE_URLS`)
et le port n'est pas ouvert au pare-feu.
Le packaging s'inspire de [radarr_ynh](https://github.com/YunoHost-Apps/radarr_ynh),
autre application .NET packagée sans Docker : c'est de là que vient le déploiement
self-contained, qui évite d'installer `dotnet-runtime` côté système.
**Aucune compilation sur le serveur.** Le publish est *self-contained* : il embarque son propre
runtime .NET, donc aucun paquet `dotnet-runtime` n'est nécessaire. `ynh_setup_source` vérifie le
`sha256` de l'archive avant de la déployer.
**Les données survivent aux mises à jour.** La base SQLite vit dans le répertoire de données, pas
à côté du binaire ; `upgrade` ne remplace que le répertoire d'installation.
**La sauvegarde passe par l'API de sauvegarde en ligne de SQLite.** La base est en mode WAL :
copier le seul fichier `.db` d'une base active peut ne rien sauvegarder du tout. Voir
`mabibli_ynh/doc/ADMIN.md`.
## Contraintes permanentes du paquet — ce ne sont pas des tâches
### Le dépôt du code doit rester public
`ynh_setup_source` télécharge **sans jeton**. Sur un dépôt privé, Gitea répond
**404 et non 403** à un anonyme : le symptôme est identique à « la release n'existe
pas », ce qui envoie chercher au mauvais endroit. Ne pas mettre de jeton dans l'URL
du manifeste — il serait lisible sur le serveur.
### L'application exige un domaine entier, pas un sous-chemin
MaBibli s'installe sur `mabibli.mondomaine.tld`, **pas** sur `mondomaine.tld/mabibli`.
Deux éléments sont figés **à la compilation** du client Blazor WebAssembly : la balise
`<base href="/">` de `index.html`, et les empreintes d'intégrité de
`service-worker-assets.js`. Les réécrire sur le serveur casserait le service worker,
donc le mode hors-ligne — et rien n'est recompilé sur le serveur, c'est tout l'intérêt
du self-contained. Le paquet déclare donc l'application en `full_domain`.
Lever cette contrainte demanderait une archive **par chemin d'installation**, ou une
compilation sur le serveur. Les deux annulent le bénéfice du self-contained.
### `install_dir` finit par appartenir à l'application, pas à root
Le manifeste demande `owner = "root:rwx"` pour que le service ne puisse pas réécrire
ses binaires, mais le helper `_ynh_apply_default_permissions` repasse derrière avec un
`chown -R mabibli:mabibli`. L'intention tient quand même, portée par
`ProtectSystem=strict` dans l'unité systemd : tout est en lecture seule sauf
`ReadWritePaths=`, qui ne liste que le `data_dir`.
⚠️ Ne pas retirer `ProtectSystem=strict` en croyant que la propriété des fichiers
protège encore.
---
## Mettre en production
La **première mise en production**, pas à pas. Pour la **montée de version**, voir
« Monter de version » dans [publier-une-version.md](publier-une-version.md).
`publier.sh` enchaîne les étapes courantes ; ce qui suit les détaille une à une, pour les
comprendre et pour les cas qu'il ne couvre pas.
```
machine de dev Gitea serveur YunoHost
────────────── ───── ────────────────
publier.sh
→ archive .tar.gz ────────► release du dépôt `mabibli`
→ manifest.toml ────────► dépôt `mabibli_ynh` ──────► yunohost app install
(url + sha256) (vérifie le sha256)
```
### Première mise en production
#### 1. Préalables, une seule fois
- Le dépôt du code (`mabibli`) est **public** sur Gitea. `ynh_setup_source` télécharge
sans jeton ; sur un dépôt privé Gitea répond 404 — indiscernable d'une release
absente.
- Le domaine dédié existe côté YunoHost. MaBibli s'installe sur un **domaine entier**
(`mabibli.mondomaine.tld`), jamais sur un sous-chemin :
```bash
sudo yunohost domain add mabibli.mondomaine.tld
```
- Le SDK .NET est installé sur la machine de développement (`dotnet --version`).
#### 2. Produire l'archive, le manifeste et le tag
Depuis `mabibli_ynh`, avec le dépôt du code à côté (`../mabibli`) :
```bash
./build/publier.sh --version 1.0.0
```
⚠️ **C'est le seul cas où `--version` s'impose.** Le script propose normalement
d'incrémenter la version précédente, qu'il lit dans les tags ou dans `manifest.toml` ;
à la toute première publication il n'y en a aucune, et il refuse plutôt que de deviner.
Ensuite, `./build/publier.sh` sans option suffit.
Le script compile en `Release` self-contained, **vérifie le publish** (binaire présent,
`wwwroot/` embarqué, toutes les ressources d'`index.html` réellement sur disque, aucun
placeholder d'empreinte non substitué), produit l'archive de façon reproductible, en
calcule le `sha256`, réécrit `version`, `amd64.url` et `amd64.sha256` dans
`manifest.toml`, **commite et pousse le paquet**, puis **tague et pousse le code**.
À contrôler dans sa sortie : la ligne `binaire, wwwroot et ressources d'index.html : OK`,
le `sha256` affiché — c'est celui que le manifeste porte désormais — et les deux `✓` de
la vérification côté distant.
Il ne reste alors que l'étape 3. Les étapes 4 et suivantes décrivent ce que le script
vient de faire, pour le cas où l'on reprend à la main après un échec en cours de route
(`--archive-seule` s'arrête après l'archive, sans toucher au manifeste ni à git).
#### 3. Déposer la release
Le tag `v1.0.0` est déjà poussé par l'étape 2 (`git tag v1.0.0 && git push origin main --tags`
si l'on reprend à la main).
**Dans l'interface Gitea** : créer la release `v1.0.0` sur le dépôt `mabibli`, et y
téléverser **les deux archives** :
```
build/dist/mabibli-1.0.0-linux-x64.tar.gz
build/dist/mabibli-1.0.0-linux-arm64.tar.gz
```
⚠️ **Les deux, sans exception.** Le manifeste déclare `architectures = ["amd64", "arm64"]`
et YunoHost choisit la ligne correspondant à la machine : une archive manquante n'échoue
pas ici, elle échoue chez celui qui installe, dans `ynh_setup_source`.
Vérification, à faire **sans être authentifié** (autre navigateur, ou `curl` comme
ci-dessous) :
```bash
curl -fsSLI "https://forge.example.org/mabibli/releases/download/v1.0.0/mabibli-1.0.0-linux-x64.tar.gz" | head -1
curl -fsSLI "https://forge.example.org/mabibli/releases/download/v1.0.0/mabibli-1.0.0-linux-arm64.tar.gz" | head -1
```
Attendu : `HTTP/2 200`. Un 404 signifie soit que la release n'est pas déposée, soit que
le dépôt est privé — les deux se ressemblent, commencer par vérifier la visibilité.
#### 4. Le paquet, sur Gitea
⚠️ **L'étape la plus facile à oublier quand elle est manuelle** `yunohost app install <url>` lit le manifeste
**depuis Gitea**, jamais la copie locale : un manifeste corrigé mais non poussé n'existe
pas pour le serveur. L'étape 2 s'en charge et vérifie l'arrivée par `git ls-remote` ; en
reprise à la main :
```bash
cd ../mabibli_ynh && git add -A && git commit -m "Publier la version 1.0.0" && git push
```
Le contrôle qui tranche, quel que soit le chemin suivi — c'est le manifeste **tel que le
serveur le lira** :
```bash
curl -fsSL "https://forge.example.org/mabibli_ynh/raw/branch/main/manifest.toml" | grep amd64
```
#### 5. Installer
```bash
sudo yunohost app install https://forge.example.org/mabibli_ynh --debug
```
YunoHost demande le domaine (celui créé à l'étape 1) et le groupe autorisé (`all_users`).
**Ce qu'il faut voir passer :** `Prefetching asset main` sur la bonne URL, puis
l'installation des fichiers, nginx, systemd, et enfin `The service mabibli has correctly
executed the action start` — le script attend la ligne `Application started` du journal,
ce qui fait échouer franchement l'installation si les migrations EF Core ne passent pas.
#### 6. Vérifier
```bash
sudo ss -tlnp | grep -i mabibli
```
Doit montrer **`127.0.0.1:<port>` uniquement**. C'est une frontière de sécurité : sur
`0.0.0.0`, n'importe qui sur le réseau pourrait forger l'en-tête `YNH_USER` et se faire
passer pour un membre du foyer.
```bash
sudo ls -l /home/yunohost.app/mabibli
```
`mabibli.db` doit exister : les migrations se sont appliquées seules au premier
démarrage. (Pas de `-wal` ni `-shm` au repos, c'est normal — SQLite fait un checkpoint à
la fermeture de la dernière connexion.)
Enfin, dans un navigateur sur `https://mabibli.mondomaine.tld` : le portail authentifie,
et l'application affiche **ton** nom d'utilisateur YunoHost — pas « anonyme ». C'est le
seul contrôle qui éprouve réellement l'intégration SSO.
---
### Désinstaller
```bash
sudo yunohost app remove mabibli
```
Retire le service, la conf nginx, la permission SSO, l'utilisateur système, le port et
`/var/www/mabibli`. **`/home/yunohost.app/mabibli` survit**, donc la bibliothèque aussi :
une désinstallation faite trop vite ne doit pas être irréversible.
⚠️ Corollaire à connaître en phase d'essai : réinstaller après un `remove` sans purge
**retrouve l'ancienne base**. Ce n'est pas une installation vierge, même si tout le
reste est neuf.
Pour tout effacer, données comprises :
```bash
sudo yunohost app remove mabibli --purge
```
Contrôle qu'il ne reste rien :
```bash
systemctl status mabibli; sudo ls -d /var/www/mabibli /home/yunohost.app/mabibli 2>&1; getent passwd mabibli
```
Les trois doivent être négatifs. Le domaine, lui, reste déclaré dans YunoHost.
---
## Installer ailleurs que sur YunoHost (Synology, NAS, VPS)
Le déploiement de référence reste le paquet `mabibli_ynh` : c'est lui qui est éprouvé, et
c'est le seul qui apporte une **authentification**. Ce qui suit sert aux hébergements qui
n'ont pas de portail SSO. `Dockerfile`, `.dockerignore` et `compose.yaml`, à la racine de
ce dépôt, existent pour ça.
L'application n'est qu'**un processus et un fichier SQLite** : `MaBibli.Api` sert lui-même
le client Blazor. Il n'y a donc rien à orchestrer.
### ⚠️ Hors YunoHost, il n'y a AUCUNE authentification
C'est le point à comprendre avant tout le reste, et il ne se contourne pas par la
configuration. `FournisseurUtilisateurSsowat` lit l'en-tête `YNH_USER` que le portail
injecte ; sans portail, il n'y a pas d'identité, et l'application **ne s'en invente pas**.
Conséquence exacte, vérifiée en exécution :
| | Sans identité |
|---|---|
| Catalogue, auteurs, séries, revues, prêts (**communs** au foyer) | fonctionnent |
| Statuts de lecture, listes d'envies (**personnels**) | refusés — `400 « Impossible d'ajouter une envie sans savoir à qui elle appartient. »` |
Deux façons de rendre une identité, **aucune des deux n'authentifie qui que ce soit** :
- `Identite__UtilisateurSimule` en variable d'environnement. C'est le mécanisme prévu pour
le développement, mais c'est de la configuration ordinaire : elle vaut aussi en
production. Mono-utilisateur.
- l'en-tête `YNH_USER` injecté par le proxy inversé (DSM : *Portail des applications >
Proxy inversé > En-tête personnalisé*). Même niveau de sécurité, mais si un proxy
authentifiant est ajouté un jour (Authelia, authentik), il n'y a plus qu'à lui faire
recopier son `Remote-User` vers `YNH_USER` : l'application n'a rien à changer.
⚠️ Dans les deux cas, **quiconque atteint le port EST cet utilisateur**. Le port ne doit
donc être publié que sur la boucle locale (`127.0.0.1:8080:8080`), le proxy inversé restant
le seul chemin d'accès. C'est la transposition exacte de la contrainte d'écoute que le
paquet YunoHost pose comme dure.
### Trois prérequis qui ne se négocient pas
- **Une machine x86_64 ou arm64.** Contrairement au paquet YunoHost, cette voie ne
télécharge aucune archive : le `Dockerfile` compile depuis les sources, pour
l'architecture de l'hôte, sur des images .NET qui existent pour les deux. ⚠️ **Rien n'a
été essayé sur un NAS ARM réel** — ni Container Manager, ni le proxy inversé de DSM.
- **HTTPS**, sans quoi le scan du code-barres ne s'ouvrira **jamais** : la caméra exige un
contexte sécurisé. Sur Synology, cela veut dire proxy inversé + certificat Let's Encrypt
sur un nom DDNS (`mabibli.xxx.synology.me`).
- **Un nom d'hôte entier, pas un sous-chemin** — `mabibli.xxx.synology.me`, jamais
`nas.xxx.synology.me/mabibli`. La raison est la même que pour le `full_domain` de
YunoHost, et elle est développée plus bas : `<base href="/">` et les empreintes du
service worker sont figés à la compilation.
### La marche à suivre
```bash
git clone <ce dépôt> mabibli && cd mabibli
# éditer compose.yaml : nom d'utilisateur, chemin du volume
docker compose up -d --build
```
Sur Synology, le même `compose.yaml` se colle dans *Container Manager > Projet*, en
remplaçant `./donnees` par un chemin réel (`/volume1/docker/mabibli`).
Les migrations EF Core s'appliquent seules au premier démarrage : la base se crée dans le
volume, et c'est **le volume seul** qui la fait survivre au remplacement de l'image.
### Ce qu'il faut savoir avant de s'y mettre
- **La sauvegarde ne se fait pas en copiant le `.db`.** SQLite tourne en mode WAL : copier
le seul fichier d'une base active peut ne rien sauvegarder du tout. Il faut arrêter le
conteneur, ou passer par `sqlite3 … ".backup"` — c'est ce que fait le paquet YunoHost, et
la raison est écrite dans `mabibli_ynh/doc/ADMIN.md`.
- **Il n'y a pas de mécanisme de mise à jour.** Ni `yunohost app upgrade`, ni release, ni
vérification de `sha256` : à chaque version, `git pull` puis `docker compose up -d
--build`. Les données ne bougent pas, elles sont dans le volume.
- **L'image est construite depuis les sources, pas depuis une release.** Elle ne porte donc
aucun numéro de version, et « À propos » affiche honnêtement « version de développement ».
Pour un vrai numéro, passer `-p:Version=X.Y.Z -p:MaBibliDateBuild=…` au `dotnet publish`
du `Dockerfile`.
### Vérifié en exécution le 2026-08-21
Image construite et démarrée localement, base neuve dans un volume monté :
| Cas | Résultat |
|---|---|
| Migrations au premier démarrage | appliquées, `mabibli.db` créé dans le volume |
| `GET /` et `_framework/blazor.webassembly.js` | **200**, `<base href="/">` intact |
| `GET /api/moi` avec `Identite__UtilisateurSimule` | `{"identifiant":"prenom","simule":true}` |
| `GET /api/moi` sans rien | `{"identifiant":null,"affichage":"inconnu"}` |
| `GET /api/moi` avec en-tête `YNH_USER: camille` | `{"identifiant":"camille","simule":false}` |
| `POST /api/souhaits` sans identité | **400**, message lisible |
⚠️ **Ce qui n'a PAS été vérifié** : l'installation sur un Synology réel, le proxy inversé de
DSM et le certificat. Ce sont des gestes de DSM, pas du code — mais personne ne les a
éprouvés ici.
---