Compare commits

..
18 Commits
Author SHA1 Message Date
mathieu 81108d7879 Publier la version 0.4.1 2026-08-21 23:06:58 +02:00
mathieuandClaude Opus 5 4a97b21ca3 Aligne l'empreinte du manifeste sur l'archive réellement publiée
L'asset de la release v0.4.0 a été remplacé sur Gitea ; le sha256 du
manifeste désignait encore l'archive précédente, d'où l'échec de
`yunohost app upgrade` (et de son retour arrière, qui retélécharge la
même source).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 23:01:16 +02:00
mathieuandClaude Opus 5 a805721f04 Injecte la version et l'horodatage de build dans l'assembly
La page « À propos » de l'application affiche la version publiée : encore
faut-il que le binaire la connaisse. Le publish reçoit donc, à côté de
`-p:Version` déjà présent, `-p:MaBibliDateBuild`.

⚠️ Cet horodatage est le témoin de l'injection : sans lui, l'application se
déclare « version de développement » plutôt que d'afficher le « 1.0.0 » que le
SDK pose par défaut, lequel se lirait comme une vraie version alors qu'il ne
désigne rien.

`SOURCE_DATE_EPOCH` est respecté, comme pour l'archive : sinon deux
compilations du même commit donneraient deux sha256 différents pour la seule
raison que l'heure a changé, et la reproductibilité visée par les options de
`tar` ne vaudrait plus rien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:50:34 +02:00
mathieu 2daacd847f Publier la version 0.4.0 2026-08-21 11:48:48 +02:00
mathieu c00abb4006 Publier la version 0.3.3 2026-08-20 22:22:28 +02:00
mathieu 2858bd0755 Publier la version 0.3.2 2026-08-20 21:31:15 +02:00
mathieu 2d98f5c987 Publier la version 0.3.1 2026-08-20 19:51:27 +02:00
mathieuandClaude Opus 5 712741a883 Renvoie la documentation vers le README du dépôt du code
publier.sh enchaîne la publication de bout en bout : version déduite de la
précédente, archive, manifeste, commit et push du paquet, tag et push du code,
avec vérification côté distant. Seul le dépôt de l'archive dans la release
Gitea reste manuel — l'URL est rappelée en fin de sortie.

PUBLICATION.md et A_FAIRE.md sont supprimés : leur contenu vit désormais dans
le README de mabibli, point d'entrée unique du projet. doc/DESCRIPTION.md et
doc/ADMIN.md restent ici, YunoHost les lisant lui-même.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:49:38 +02:00
mathieu 0c2e94431b Publier la version 0.3.0 2026-08-20 18:18:01 +02:00
LIMONIER MathieuandCopilot 3d4ccd32ed Ajouter le logo de l'application (256x256, fond transparent)
Meme pictogramme de livre ouvert que MaBibli.Client (favicon, icones PWA) : deux pages
en V dans le bleu d'accent de l'app, tranche centrale blanche. Convention du catalogue
d'applications YunoHost : logo.png a la racine du depot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 13:03:21 +02:00
mathieuandClaude Opus 5 3fcb447bb8 Rappeler l'option -u à la mise à jour, l'app n'étant pas au catalogue
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:52:33 +02:00
mathieu 6f58527206 Publier la version 0.2.0 2026-08-19 22:48:50 +02:00
mathieuandClaude Opus 5 54a22b1579 Écrire la marche à suivre : mise en production et montée de version
PUBLICATION.md donne les gestes pas à pas, avec le contrôle attendu à
chaque étape : première mise en production, montée de version (0.1.0 →
0.1.1), retour arrière si la mise à jour échoue, désinstallation.

Deux points qui se paient cher s'ils sont oubliés y sont explicites :
`--version` est obligatoire pour une nouvelle version applicative (sans
lui le script reproduit celle du manifeste), et la sauvegarde de sécurité
pré-upgrade ne contient PAS le répertoire de données.

README et A_FAIRE renvoient au fichier plutôt que de tripler la
procédure. Dernière URL fictive éliminée : gitea.exemple.org subsistait
dans l'exemple CI de publier-release.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:05:56 +02:00
mathieuandClaude Opus 5 7ab423783f Rendre le data_dir visible au service : ProtectHome=tmpfs + BindPaths
Le data_dir de YunoHost vit sous /home/yunohost.app/mabibli, or
ProtectHome=yes rend /home vide dans le namespace du service : le démarrage
mourait sur « SQLite Error 14: unable to open database file ».

ReadWritePaths ne perce pas ProtectHome — vérifié sur une unité de test :
`yes` + ReadWritePaths échoue, `tmpfs` + BindPaths réussit. La forme retenue
garde les répertoires personnels du serveur invisibles au service.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:26:48 +02:00
mathieuandClaude Opus 5 4e9bd8be71 Guillemeter la chaîne de connexion dans l'unité systemd
`Environment=` découpe sur les espaces : sans guillemets, la ligne
définissait `ConnectionStrings__MaBibli=Data` et une variable parasite
`Source=…`. L'application recevait la chaîne de connexion `Data` et mourait
au démarrage en ArgumentException, faisant échouer l'installation à l'étape
« Démarrage de mabibli » (constaté sur le serveur, reproduit isolément sur
une unité de test).

Version de paquet à ~ynh2 : l'archive est inchangée, seule la conf bouge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:21:38 +02:00
mathieuandClaude Opus 5 33bab173bc Pointer réellement le manifeste vers la release Gitea
Le commit précédent portait le bon message mais pas les modifications :
manifest.toml, README.md, conf/systemd.service et la valeur par défaut de
--base-url référençaient encore gitea.example.org, ce qui faisait échouer
l'installation au Prefetching asset main.

A_FAIRE.md est réécrit : la visibilité privée du dépôt du code devient le
point bloquant restant (Gitea répond 404, pas 403, à un anonyme).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:16:13 +02:00
mathieu ffc3db2dcc Manifeste pointe vers la bonne release 2026-08-18 19:14:14 +02:00
mathieu f942f41f34 premier essaie 2026-08-18 15:15:53 +02:00
9 changed files with 514 additions and 546 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# Paquet YunoHost pour MaBibli
Paquet d'installation de [MaBibli](https://forge.example.org/mabibli), application de
Paquet d'installation de [MaBibli](https://git.akbar.nohost.me/mathieu/mabibli), application de
gestion de bibliothèque personnelle (catalogue, prêts, scan ISBN).
Ce dépôt **ne contient aucun code C#**. Il porte uniquement de quoi installer sur YunoHost une
@@ -9,7 +9,7 @@ d'installation, et les scripts de publication.
## 📖 Toute la documentation est dans le dépôt du code
Elle a été regroupée dans **[le README de `mabibli`](https://forge.example.org/mabibli#readme)**,
Elle a été regroupée dans **[le README de `mabibli`](https://git.akbar.nohost.me/mathieu/mabibli#readme)**,
qui est le point d'entrée unique du projet : contenu et points de conception du paquet,
publication d'une version, mise en production pas à pas, montée de version, retour arrière,
désinstallation, et contraintes permanentes.
+291
View File
@@ -0,0 +1,291 @@
#!/bin/bash
#
# Produit l'archive de release de MaBibli et met à jour manifest.toml.
#
# Pourquoi ce script vit dans `mabibli_ynh` et non dans `mabibli` :
# sa sortie utile n'est pas l'archive, c'est le couple (url, sha256) que le
# manifeste doit porter. Le garder ici permet de compiler, d'archiver et de mettre à
# jour le manifeste en une commande, puis de committer le tout d'un bloc — le
# manifeste ne peut pas se désynchroniser de l'archive qu'il décrit.
#
# Réutilisable en CI (Gitea Actions) sans modification : aucune interaction, aucune
# variable propre à un environnement d'intégration, tout passe par des options ou des
# variables d'environnement, et le script échoue au premier problème.
#
# Exemple d'usage en CI :
# ./build/publier-release.sh \
# --source-dir "$GITHUB_WORKSPACE/mabibli" \
# --version 0.2.0 \
# --base-url "https://git.akbar.nohost.me/mathieu/mabibli/releases/download"
#
set -euo pipefail
#=================================================
# VALEURS PAR DÉFAUT
#=================================================
racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source_dir="${MABIBLI_SOURCE_DIR:-$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)}"
version="${MABIBLI_VERSION:-}"
base_url="${MABIBLI_BASE_URL:-https://git.akbar.nohost.me/mathieu/mabibli/releases/download}"
output_dir="${MABIBLI_OUTPUT_DIR:-$racine_paquet/build/dist}"
maj_manifeste=1
runtime="linux-x64"
projet="MaBibli.Api"
#=================================================
# OPTIONS
#=================================================
usage() {
cat <<'FIN'
Usage : publier-release.sh [options]
--source-dir CHEMIN Dépôt du code C# (défaut : ../mabibli)
--version X.Y.Z Version de la release (défaut : déduite de manifest.toml)
--base-url URL Racine des URL de release, sans le tag ni le fichier
--output-dir CHEMIN Où déposer l'archive (défaut : build/dist)
--no-manifest-update Ne pas réécrire manifest.toml
-h, --help Cette aide
Variables d'environnement équivalentes : MABIBLI_SOURCE_DIR, MABIBLI_VERSION,
MABIBLI_BASE_URL, MABIBLI_OUTPUT_DIR.
FIN
}
while [ $# -gt 0 ]; do
case "$1" in
--source-dir) source_dir="$2"; shift 2 ;;
--version) version="$2"; shift 2 ;;
--base-url) base_url="$2"; shift 2 ;;
--output-dir) output_dir="$2"; shift 2 ;;
--no-manifest-update) maj_manifeste=0; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;;
esac
done
manifeste="$racine_paquet/manifest.toml"
#=================================================
# CONTRÔLES PRÉALABLES
#=================================================
echoerr() { echo "$@" >&2; }
if [ -z "$source_dir" ] || [ ! -d "$source_dir" ]; then
echoerr "Dépôt du code introuvable : '${source_dir:-<vide>}'"
echoerr "Précisez-le avec --source-dir."
exit 1
fi
source_dir="$(cd "$source_dir" && pwd)"
if [ ! -d "$source_dir/$projet" ]; then
echoerr "'$source_dir' ne contient pas de projet $projet."
exit 1
fi
if ! command -v dotnet >/dev/null; then
echoerr "Le SDK .NET est introuvable. Il n'est nécessaire QUE sur cette machine :"
echoerr "le serveur YunoHost reçoit un publish self-contained et n'a pas de SDK."
exit 1
fi
# La version par défaut se lit dans le manifeste, en retirant le suffixe ~ynhN :
# le tag et le nom de fichier portent la version applicative, pas celle du paquet.
if [ -z "$version" ]; then
version="$(sed -n 's/^version *= *"\([^"~]*\).*"/\1/p' "$manifeste" | head -1)"
if [ -z "$version" ]; then
echoerr "Impossible de déduire la version depuis $manifeste ; utilisez --version."
exit 1
fi
echo "Version déduite de manifest.toml : $version"
fi
#=================================================
# COMPILATION
#=================================================
# Horodatage du build, injecté dans l'assembly à côté de la version (voir plus bas).
#
# ⚠️ Il sert de TÉMOIN : l'application ne montre sa version que si cet horodatage est présent.
# Sans lui, le numéro lu serait le « 1.0.0 » que le SDK pose par défaut, qui se lirait comme une
# vraie version alors qu'il ne désigne rien — et c'est précisément la valeur qu'on va chercher
# pour diagnostiquer un appareil dépareillé. Un binaire compilé à la main se déclare donc
# « version de développement », ce qui est vrai.
#
# `SOURCE_DATE_EPOCH` est respecté, comme pour l'archive : sans cela, deux compilations du même
# commit donneraient deux binaires — donc deux sha256 — pour la seule raison que l'heure a
# changé, et la reproductibilité visée plus bas ne vaudrait plus rien.
if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then
date_build="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y-%m-%dT%H:%M:%SZ)"
else
date_build="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
fi
travail="$(mktemp -d)"
# shellcheck disable=SC2064
trap "rm -rf '$travail'" EXIT
publish="$travail/publish"
echo
echo "=== Compilation self-contained $runtime ==="
echo " source : $source_dir"
echo " version: $version"
echo " build : $date_build"
# `--self-contained` embarque le runtime .NET dans le dossier publié : c'est ce qui
# permet au paquet YunoHost de ne dépendre d'aucun `dotnet-runtime` côté serveur.
#
# `MaBibli.Api` référence `MaBibli.Client` : le client Blazor WebAssembly est compilé
# au passage et atterrit dans `wwwroot/`. Un seul publish suffit donc pour les trois
# projets.
dotnet publish "$source_dir/$projet" \
--configuration Release \
--runtime "$runtime" \
--self-contained \
--output "$publish" \
-p:Version="$version" \
-p:MaBibliDateBuild="$date_build"
#=================================================
# CONTRÔLES SUR LE RÉSULTAT
#=================================================
echo
echo "=== Vérification du publish ==="
if [ ! -x "$publish/$projet" ]; then
echoerr "Binaire '$projet' absent ou non exécutable dans le publish."
exit 1
fi
# Sans `wwwroot`, le service démarrerait et répondrait — en ne servant aucune page.
# C'est le défaut qui est passé inaperçu pendant trois phases du projet, faute de
# regarder ailleurs que la racine. On le vérifie donc ici, à la source.
if [ ! -f "$publish/wwwroot/index.html" ]; then
echoerr "wwwroot/index.html absent : le client Blazor n'a pas été embarqué."
exit 1
fi
# `index.html` doit référencer un script réellement présent. Un placeholder non
# substitué (`blazor.webassembly#[.{fingerprint}].js`) donnerait une page blanche
# alors que `/` répondrait 200.
if grep -q '#\[\.{fingerprint}\]' "$publish/wwwroot/index.html"; then
echoerr "index.html contient des placeholders d'empreinte non substitués."
echoerr "Voir CLAUDE.md, « Empreintes WASM désactivées »."
exit 1
fi
for ressource in $(grep -oE '(src|href)="[^"]+"' "$publish/wwwroot/index.html" \
| sed -E 's/^(src|href)="//; s/"$//' \
| grep -vE '^(https?:|data:|#|/$|\.$)'); do
if [ ! -f "$publish/wwwroot/$ressource" ]; then
echoerr "index.html référence '$ressource', absent du publish."
exit 1
fi
done
echo " binaire, wwwroot et ressources d'index.html : OK"
#=================================================
# ARCHIVE
#=================================================
archive_nom="mabibli-$version-$runtime.tar.gz"
mkdir -p "$output_dir"
archive="$output_dir/$archive_nom"
echo
echo "=== Archive ==="
# Fichiers à la racine de l'archive, sans dossier intermédiaire : cohérent avec
# `in_subdir = false` dans manifest.toml.
#
# Options de reproductibilité : sans elles, deux compilations identiques donneraient
# deux sha256 différents (dates, ordre de parcours, uid/gid). En CI comme en local,
# on veut pouvoir vérifier qu'une archive correspond bien à un commit.
tar --create --gzip \
--file="$archive" \
--directory="$publish" \
--owner=0 --group=0 --numeric-owner \
--mtime="@${SOURCE_DATE_EPOCH:-0}" \
--sort=name \
.
somme="$(sha256sum "$archive" | cut -d' ' -f1)"
taille="$(du -h "$archive" | cut -f1)"
taille_octets="$(stat -c%s "$archive")"
echo " fichier : $archive"
echo " taille : $taille ($taille_octets octets)"
echo " sha256 : $somme"
#=================================================
# MISE À JOUR DU MANIFESTE
#=================================================
url="$base_url/v$version/$archive_nom"
if [ "$maj_manifeste" -eq 1 ]; then
echo
echo "=== Mise à jour de manifest.toml ==="
# Le suffixe ~ynhN est conservé s'il existe déjà pour cette version, sinon
# remis à ~ynh1 : un changement de version applicative repart toujours de 1.
version_actuelle="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$manifeste" | head -1)"
if [[ "$version_actuelle" == "$version~ynh"* ]]; then
version_paquet="$version_actuelle"
echo " version : $version_paquet (inchangée)"
else
version_paquet="$version~ynh1"
echo " version : $version_actuelle -> $version_paquet"
fi
python3 - "$manifeste" "$version_paquet" "$url" "$somme" <<'PY'
import re, sys
chemin, version, url, somme = sys.argv[1:5]
with open(chemin, encoding="utf-8") as f:
texte = f.read()
remplacements = [
(r'^version = ".*"$', f'version = "{version}"'),
(r'^ amd64\.url = ".*"$', f' amd64.url = "{url}"'),
(r'^ amd64\.sha256 = ".*"$', f' amd64.sha256 = "{somme}"'),
]
for motif, valeur in remplacements:
texte, n = re.subn(motif, valeur, texte, count=1, flags=re.MULTILINE)
if n != 1:
sys.exit(f"Motif introuvable dans le manifeste : {motif}")
with open(chemin, "w", encoding="utf-8") as f:
f.write(texte)
PY
echo " url : $url"
echo " sha256 : $somme"
fi
#=================================================
# SUITE À DONNER
#=================================================
cat <<FIN
=== Terminé ===
Archive prête : $archive
Il reste à faire, à la main (le dépôt Gitea et la release ne sont pas automatisés) :
1. Créer le tag v$version sur le dépôt du code, puis la release correspondante.
2. Y téléverser $archive_nom.
3. Vérifier que l'URL du manifeste répond :
curl -fsSLI "$url"
4. Committer manifest.toml dans mabibli_ynh.
FIN
+51 -408
View File
@@ -2,86 +2,37 @@
#
# Publie une version de MaBibli de bout en bout, en une commande.
#
# 1. choisir la version, à partir de celle déjà publiée
# 2. compiler self-contained POUR CHAQUE ARCHITECTURE, vérifier chaque publish,
# produire une archive par architecture
# 3. réécrire manifest.toml (version, url et sha256 de chaque architecture)
# 4. committer et pousser mabibli_ynh (le serveur lit Gitea, jamais le local)
# 5. taguer et pousser mabibli (le tag porte le code de la release)
# Enchaîne ce qui était jusqu'ici tapé à la main, toujours dans le même ordre :
#
# La seule chose qui reste manuelle est le dépôt des archives dans la release Gitea :
# elles sont rappelées en fin de sortie, une par architecture.
# 1. produire l'archive et mettre à jour manifest.toml (publier-release.sh)
# 2. committer et pousser mabibli_ynh (le serveur lit Gitea, pas le local)
# 3. taguer et pousser mabibli (le tag porte le code de la release)
#
# Le mode `--archive-seule` compile et archive sans toucher ni
# au manifeste ni à git
# La version se choisit ici, à partir de celle déjà publiée : rien à aller chercher.
# La seule chose qui reste manuelle est le dépôt de l'archive dans la release Gitea —
# l'URL est rappelée en fin de sortie.
#
set -euo pipefail
#=================================================
# VALEURS PAR DÉFAUT
#=================================================
racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
manifeste="$racine_paquet/manifest.toml"
# ⚠️ Chaque réglage a UNE seule façon de se donner : son option en ligne de commande.
# ⚠️ LA SEULE LIGNE À CHANGER pour héberger ce projet ailleurs.
# Tout ce qui suit en dérive : URL de release, URL d'archive, message de fin.
#
# Une copie publique du projet porte ici `https://forge.example.org/mabibli`, injoignable
# par construction (example.org est réservé à la documentation par la RFC 2606). Le
# contrôle préalable plus bas REFUSE alors de publier, et refuse AVANT de compiler :
# celui qui reprend le projet doit renseigner cette ligne avant sa première release,
# plutôt que de découvrir le problème à l'installation, dans ynh_setup_source.
depot_code="https://git.akbar.nohost.me/mathieu/mabibli"
source_dir="$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)"
base_url="$depot_code/releases/download"
url_releases="$depot_code/releases"
output_dir="$racine_paquet/build/dist"
source_dir="${MABIBLI_SOURCE_DIR:-$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)}"
base_url="${MABIBLI_BASE_URL:-https://git.akbar.nohost.me/mathieu/mabibli/releases/download}"
url_releases="${MABIBLI_URL_RELEASES:-https://git.akbar.nohost.me/mathieu/mabibli/releases}"
version=""
increment=""
sans_confirmation=0
archive_seule=0
projet="MaBibli.Api"
# ⚠️ Les DEUX architectures se compilent à chaque publication, et il n'y a pas d'option
# pour n'en faire qu'une. Le manifeste déclare `architectures = ["amd64", "arm64"]` : un
# serveur arm64 ira chercher `arm64.url`. Publier une seule archive laisserait le
# manifeste promettre une URL que personne n'a téléversée — et l'échec surviendrait chez
# celui qui installe, dans `ynh_setup_source`, sans rapport apparent avec la cause.
#
# ⚠️ Le RID désigne le SERVEUR, jamais la machine qui compile : le SDK télécharge le
# runtime pack de la cible, et un hôte arm64 produit un binaire x86-64 sans rien de plus.
runtimes=("linux-x64" "linux-arm64")
# Nom YunoHost de chaque RID .NET — c'est la clé du manifeste (`amd64.url`, `arm64.url`).
arch_ynh() {
case "$1" in
linux-x64) echo "amd64" ;;
linux-arm64) echo "arm64" ;;
*) echoerr "RID sans architecture YunoHost connue : $1"; exit 1 ;;
esac
}
usage() {
cat <<'FIN'
Usage : publier.sh [options]
--version X.Y.Z Version à publier (sinon : demandée, à partir de la précédente)
--patch Incrémenter le dernier chiffre (1.0.0 -> 1.0.1)
--minor Incrémenter le chiffre du milieu (1.0.0 -> 1.1.0)
--major Incrémenter le premier chiffre (1.0.0 -> 2.0.0)
--oui, -y Ne rien demander (pour une CI)
--archive-seule Compiler et archiver SEULEMENT : ni manifeste, ni commit, ni tag.
Pour REPRENDRE une publication à la main après un échec en cours
de route ; ne publie rien et ne modifie aucun dépôt. Les contrôles
restent les mêmes, mais avertissent au lieu de refuser.
--source-dir CHEMIN Dépôt du code C# (défaut : ../mabibli)
--depot-code URL Dépôt public du code, d'où dérivent les URL de release
--base-url URL Racine des URL de release, sans le tag ni le fichier
--output-dir CHEMIN Où déposer l'archive (défaut : build/dist)
--patch Incrémenter le dernier chiffre (0.3.0 -> 0.3.1)
--minor Incrémenter le chiffre du milieu (0.3.0 -> 0.4.0)
--major Incrémenter le premier chiffre (0.3.0 -> 1.0.0)
--oui Ne rien demander (pour une CI)
-h, --help Cette aide
FIN
}
@@ -91,11 +42,6 @@ while [ $# -gt 0 ]; do
--version) version="$2"; shift 2 ;;
--patch|--minor|--major) increment="${1#--}"; shift ;;
--oui|-y) sans_confirmation=1; shift ;;
--archive-seule) archive_seule=1; shift ;;
--source-dir) source_dir="$2"; shift 2 ;;
--depot-code) depot_code="$2"; base_url="$2/releases/download"; url_releases="$2/releases"; shift 2 ;;
--base-url) base_url="$2"; shift 2 ;;
--output-dir) output_dir="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;;
esac
@@ -103,74 +49,13 @@ done
echoerr() { echo "$@" >&2; }
# ⚠️ LE SEUL ENDROIT où `--archive-seule` assouplit quoi que ce soit.
#
# Les garde-fous ci-dessous protègent la *publication* : ils empêchent qu'une release
# contienne autre chose que le code que son tag désigne. En `--archive-seule`, rien n'est
# publié — ni manifeste, ni commit, ni tag — donc rien ne peut mentir, et un refus n'a
# plus lieu d'être. Il reste un avertissement, car ce qu'il signale est toujours vrai.
#
# Chaque garde-fou s'écrit donc UNE fois, sans savoir dans quel mode il tourne. C'est ce
# qui empêche d'assouplir par inadvertance un contrôle qui devait rester dur.
# ⚠️ Le CONSTAT et le CONSEIL sont séparés, et ce n'est pas de la cosmétique : le constat
# reste vrai dans les deux modes, le conseil non. « Choisissez un numéro libre » est le
# bon conseil pour une publication, et un mauvais conseil en reprise — où le tag visé est
# justement celui qu'on veut retrouver.
refuser() { # refuser <conseil> <constat> [<constat>...]
local conseil="$1"; shift
if [ "$archive_seule" -eq 1 ]; then
printf ' ⚠ %s\n' "$@"
echo " (poursuite : --archive-seule ne publie rien)"
else
printf '%s\n' "$@" >&2
[ -n "$conseil" ] && echoerr "$conseil"
exit 1
fi
}
#=================================================
# CONTRÔLES PRÉALABLES
#=================================================
if [ -z "$source_dir" ] || [ ! -d "$source_dir" ]; then
echoerr "Dépôt du code introuvable : '${source_dir:-<vide>}'"
echoerr "Précisez-le avec --source-dir."
exit 1
fi
source_dir="$(cd "$source_dir" && pwd)"
if [ ! -d "$source_dir/$projet" ]; then
echoerr "'$source_dir' ne contient pas de projet $projet."
exit 1
fi
# ⚠️ Exigé dans les DEUX modes. `--archive-seule` sert à reprendre une publication à la
# main, jamais à compiler un arbre quelconque : sans dépôt, il ne saurait ni proposer une
# version, ni dire si l'archive correspond au code d'un tag. L'exiger ici plutôt qu'à
# quatre endroits plus bas est ce qui rend le reste du script inconditionnel.
if [ ! -d "$source_dir/.git" ]; then
echoerr "'$source_dir' n'est pas un dépôt git."
echoerr "publier.sh compile ce qu'un commit désigne : sans dépôt, rien ne le désigne."
exit 1
fi
# ⚠️ Refuser AVANT de compiler, comme les autres garde-fous. Publier avec le placeholder
# écrirait dans manifest.toml une `amd64.url` que personne ne peut télécharger — et
# `ynh_setup_source` échouerait à l'installation, loin d'ici et sans rapport apparent.
if [ "$archive_seule" -eq 0 ] && [[ "$base_url" == *example.org* ]]; then
echoerr "Le dépôt public n'est pas configuré : $depot_code"
echoerr "Changez « depot_code » en tête de ce script, ou passez --depot-code URL."
exit 1
fi
if ! command -v dotnet >/dev/null; then
echoerr "Le SDK .NET est introuvable. Il n'est nécessaire QUE sur cette machine :"
echoerr "le serveur YunoHost reçoit un publish self-contained et n'a pas de SDK."
if [ -z "$source_dir" ] || [ ! -d "$source_dir/.git" ]; then
echoerr "Dépôt du code introuvable : '${source_dir:-<vide>}' (MABIBLI_SOURCE_DIR)."
exit 1
fi
#=================================================
# LA VERSION
# LA VERSION PRÉCÉDENTE, DEUX SOURCES QUI DOIVENT S'ACCORDER
#=================================================
# Le tag fait foi : c'est lui qui désigne le code publié. Le manifeste ne porte que la
@@ -180,7 +65,16 @@ version_tag="$(git -C "$source_dir" tag --list 'v[0-9]*' --sort=-v:refname | hea
version_manifeste="$(sed -n 's/^version *= *"\([^"~]*\).*"/\1/p' "$manifeste" | head -1)"
precedente="$(printf '%s\n%s\n' "$version_tag" "$version_manifeste" \
| grep -E '^[0-9]+(\.[0-9]+)*$' | sort -V | tail -1 || true)"
| grep -E '^[0-9]+(\.[0-9]+)*$' | sort -V | tail -1)"
if [ -z "$precedente" ]; then
echoerr "Aucune version précédente trouvée (ni tag, ni manifest.toml) ; utilisez --version."
exit 1
fi
echo "Dernière version publiée : $precedente"
[ "$version_tag" != "$version_manifeste" ] &&
echo " ⚠ tag v${version_tag:-—} et manifest.toml ${version_manifeste:-—} divergent"
suivante() { # suivante <version> <patch|minor|major>
IFS=. read -r maj min cor <<<"$1"
@@ -191,19 +85,6 @@ suivante() { # suivante <version> <patch|minor|major>
esac
}
# ⚠️ Le calcul de la version est le MÊME dans les deux modes, et c'est délibéré : une
# reprise après échec veut la version qu'on était en train de publier, c'est-à-dire ce
# que l'incrément propose. Faire retomber `--archive-seule` sur la version du manifeste
# reproduirait le numéro DÉJÀ publié tant qu'on ne passerait pas `--version`.
if [ -z "$precedente" ]; then
echoerr "Aucune version précédente trouvée (ni tag, ni manifest.toml) ; utilisez --version."
exit 1
fi
echo "Dernière version publiée : $precedente"
[ "$version_tag" != "$version_manifeste" ] &&
echo " ⚠ tag v${version_tag:-—} et manifest.toml ${version_manifeste:-—} divergent"
if [ -z "$version" ] && [ -n "$increment" ]; then
version="$(suivante "$precedente" "$increment")"
fi
@@ -234,43 +115,30 @@ if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
exit 1
fi
if git -C "$source_dir" rev-parse "v$version" >/dev/null 2>&1; then
echoerr "Le tag v$version existe déjà dans $source_dir."
exit 1
fi
#=================================================
# CE QUI DOIT ÊTRE VRAI AVANT DE COMPILER
#=================================================
# ⚠️ Recompiler une version dont le tag existe
# déjà produit une archive qui ne correspond PAS au code que ce tag désigne : la release
# se met alors à mentir, et l'écran « À propos » avec elle, sans que rien ne le signale.
if git -C "$source_dir" rev-parse "v$version" >/dev/null 2>&1; then
commit_tag="$(git -C "$source_dir" rev-list -n1 "v$version")"
commit_head="$(git -C "$source_dir" rev-parse HEAD)"
if [ "$commit_tag" != "$commit_head" ]; then
refuser "Choisissez un numéro libre (--patch)." \
"Le tag v$version existe et désigne ${commit_tag:0:7} ; HEAD est ${commit_head:0:7}." \
"L'archive ne serait donc PAS le code que ce tag désigne."
else
refuser "Choisissez un numéro libre (--patch)." \
"Le tag v$version existe déjà (${commit_tag:0:7}) : ce numéro est pris."
fi
fi
# Le tag va désigner le HEAD du dépôt du code : un travail non committé ne serait donc
# pas dans la release, alors même que l'archive, elle, le contiendrait.
if [ -n "$(git -C "$source_dir" status --porcelain)" ]; then
echoerr "Le dépôt du code a des modifications non committées :"
git -C "$source_dir" status --short >&2
refuser "" \
"Le dépôt du code a des modifications non committées (ci-dessus)." \
"Le tag désignera HEAD : ce travail ne serait pas dans la release."
exit 1
fi
branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)"
echo
echo "=== À publier ==="
echo " version : ${precedente:-—} -> $version"
branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)"
echo " version : $precedente -> $version"
echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))"
echo " paquet : $racine_paquet"
[ "$archive_seule" -eq 1 ] &&
echo " mode : --archive-seule (ni manifeste, ni commit, ni tag)"
if [ "$sans_confirmation" -ne 1 ]; then
read -rp "Continuer ? [O/n] " reponse
@@ -281,242 +149,16 @@ if [ "$sans_confirmation" -ne 1 ]; then
fi
#=================================================
# COMPILATION
# 1. ARCHIVE + MANIFESTE
#=================================================
# Horodatage du build, injecté dans l'assembly à côté de la version.
#
# ⚠️ Il sert de TÉMOIN : l'application ne montre sa version que si cet horodatage est présent.
# Sans lui, le numéro lu serait le « 1.0.0 » que le SDK pose par défaut, qui se lirait comme une
# vraie version alors qu'il ne désigne rien — et c'est précisément la valeur qu'on va chercher
# pour diagnostiquer un appareil dépareillé. Un binaire compilé à la main se déclare donc
# « version de développement », ce qui est vrai.
#
# `SOURCE_DATE_EPOCH` est respecté, comme pour l'archive : sans cela, deux compilations du même
# commit donneraient deux binaires — donc deux sha256 — pour la seule raison que l'heure a
# changé, et la reproductibilité visée plus bas ne vaudrait plus rien.
if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then
date_build="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y-%m-%dT%H:%M:%SZ)"
else
date_build="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
fi
"$racine_paquet/build/publier-release.sh" --version "$version" --source-dir "$source_dir" --base-url "$base_url"
travail="$(mktemp -d)"
# shellcheck disable=SC2064
trap "rm -rf '$travail'" EXIT
# Une passe complète par architecture : compilation, contrôles, archive. Les trois
# blocs sont RIGOUREUSEMENT les mêmes d'une architecture à l'autre — c'est ce qui
# garantit qu'une archive arm64 est vérifiée aussi durement qu'une amd64.
declare -A sommes=()
declare -A urls=()
declare -A archives=()
for runtime in "${runtimes[@]}"; do
arch="$(arch_ynh "$runtime")"
publish="$travail/publish-$runtime"
echo
echo "=== Compilation self-contained $runtime ($arch) ==="
echo " source : $source_dir"
echo " version: $version"
echo " build : $date_build"
# `--self-contained` embarque le runtime .NET dans le dossier publié : c'est ce qui
# permet au paquet YunoHost de ne dépendre d'aucun `dotnet-runtime` côté serveur.
#
# `MaBibli.Api` référence `MaBibli.Client` : le client Blazor WebAssembly est compilé
# au passage et atterrit dans `wwwroot/`. Un seul publish suffit donc pour les trois
# projets.
#
# ⚠️ Le client compilé est le MÊME quelle que soit l'architecture — c'est du WebAssembly,
# zbar compris. Seul l'hôte .NET change. On le recompile quand même par architecture
# plutôt que de recopier un `wwwroot/` d'une passe à l'autre : le publish est le seul
# geste dont on sait qu'il produit un ensemble cohérent, et les contrôles ci-dessous
# portent sur ce qu'on archive vraiment, pas sur ce qu'on croit y avoir mis.
dotnet publish "$source_dir/$projet" \
--configuration Release \
--runtime "$runtime" \
--self-contained \
--output "$publish" \
-p:Version="$version" \
-p:MaBibliDateBuild="$date_build"
archive_nom="mabibli-$version-linux-x64.tar.gz"
archive="${MABIBLI_OUTPUT_DIR:-$racine_paquet/build/dist}/$archive_nom"
#=================================================
# CONTRÔLES SUR LE RÉSULTAT
#=================================================
echo
echo "=== Vérification du publish ($arch) ==="
if [ ! -x "$publish/$projet" ]; then
echoerr "Binaire '$projet' absent ou non exécutable dans le publish $runtime."
exit 1
fi
# Sans `wwwroot`, le service démarrerait et répondrait — en ne servant aucune page.
# C'est le défaut qui est passé inaperçu pendant trois phases du projet, faute de
# regarder ailleurs que la racine. On le vérifie donc ici, à la source.
if [ ! -f "$publish/wwwroot/index.html" ]; then
echoerr "wwwroot/index.html absent : le client Blazor n'a pas été embarqué ($runtime)."
exit 1
fi
# `index.html` doit référencer un script réellement présent. Un placeholder non
# substitué (`blazor.webassembly#[.{fingerprint}].js`) donnerait une page blanche
# alors que `/` répondrait 200.
if grep -q '#\[\.{fingerprint}\]' "$publish/wwwroot/index.html"; then
echoerr "index.html contient des placeholders d'empreinte non substitués ($runtime)."
echoerr "Voir CLAUDE.md, « Les invariants à ne jamais casser »."
exit 1
fi
for ressource in $(grep -oE '(src|href)="[^"]+"' "$publish/wwwroot/index.html" \
| sed -E 's/^(src|href)="//; s/"$//' \
| grep -vE '^(https?:|data:|#|/$|\.$)'); do
if [ ! -f "$publish/wwwroot/$ressource" ]; then
echoerr "index.html référence '$ressource', absent du publish ($runtime)."
exit 1
fi
done
# ⚠️ Le contrôle qui n'existait pas tant qu'il n'y avait qu'une architecture : rien
# n'empêche un publish de rendre un binaire de l'architecture de la MACHINE plutôt que
# de la cible. Le symptôme serait un service qui refuse de démarrer sur le serveur, avec
# un « Exec format error » qui ne désigne pas la publication.
attendu_elf="$(case "$runtime" in linux-x64) echo "x86-64" ;; linux-arm64) echo "aarch64" ;; esac)"
if ! file -b "$publish/$projet" | grep -q "$attendu_elf"; then
echoerr "Le binaire produit n'est pas du $attendu_elf :"
echoerr " $(file -b "$publish/$projet")"
exit 1
fi
echo " binaire ($attendu_elf), wwwroot et ressources d'index.html : OK"
#=================================================
# ARCHIVE
#=================================================
archive_nom="mabibli-$version-$runtime.tar.gz"
mkdir -p "$output_dir"
archive="$output_dir/$archive_nom"
echo
echo "=== Archive ($arch) ==="
# Fichiers à la racine de l'archive, sans dossier intermédiaire : cohérent avec
# `in_subdir = false` dans manifest.toml.
#
# Options de reproductibilité : sans elles, deux compilations identiques donneraient
# deux sha256 différents (dates, ordre de parcours, uid/gid). En CI comme en local,
# on veut pouvoir vérifier qu'une archive correspond bien à un commit.
tar --create --gzip \
--file="$archive" \
--directory="$publish" \
--owner=0 --group=0 --numeric-owner \
--mtime="@${SOURCE_DATE_EPOCH:-0}" \
--sort=name \
.
somme="$(sha256sum "$archive" | cut -d' ' -f1)"
taille="$(du -h "$archive" | cut -f1)"
taille_octets="$(stat -c%s "$archive")"
echo " fichier : $archive"
echo " taille : $taille ($taille_octets octets)"
echo " sha256 : $somme"
sommes[$arch]="$somme"
archives[$arch]="$archive"
urls[$arch]="$base_url/v$version/$archive_nom"
# Le publish pèse ~150 Mo : le garder pendant la passe suivante doublerait l'occupation
# du disque pour rien, l'archive étant déjà produite.
rm -rf "$publish"
done
if [ "$archive_seule" -eq 1 ]; then
echo
echo "=== Terminé (--archive-seule) ==="
echo
for runtime in "${runtimes[@]}"; do
arch="$(arch_ynh "$runtime")"
echo " $arch : ${archives[$arch]}"
echo " sha256 ${sommes[$arch]}"
done
cat <<FIN
Rien n'a été publié : manifest.toml est inchangé, aucun commit, aucun tag.
Pour publier réellement cette version, relancez sans --archive-seule.
FIN
exit 0
fi
#=================================================
# MANIFESTE
#=================================================
echo
echo "=== Mise à jour de manifest.toml ==="
# Le suffixe ~ynhN est conservé s'il existe déjà pour cette version, sinon
# remis à ~ynh1 : un changement de version applicative repart toujours de 1.
version_actuelle="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$manifeste" | head -1)"
if [[ "$version_actuelle" == "$version~ynh"* ]]; then
version_paquet="$version_actuelle"
echo " version : $version_paquet (inchangée)"
else
version_paquet="$version~ynh1"
echo " version : $version_actuelle -> $version_paquet"
fi
# Les architectures sont passées à plat : arch, url, sha256, arch, url, sha256…
arguments=()
for runtime in "${runtimes[@]}"; do
arch="$(arch_ynh "$runtime")"
arguments+=("$arch" "${urls[$arch]}" "${sommes[$arch]}")
done
python3 - "$manifeste" "$version_paquet" "${arguments[@]}" <<'PY'
import re, sys
chemin, version = sys.argv[1:3]
reste = sys.argv[3:]
with open(chemin, encoding="utf-8") as f:
texte = f.read()
remplacements = [(r'^version = ".*"$', f'version = "{version}"')]
# ⚠️ Chaque motif DOIT correspondre exactement une fois. Une architecture déclarée au
# manifeste mais absente d'ici laisserait une URL périmée pointer sur la version
# précédente : le serveur téléchargerait une archive réelle, dont le sha256 ne
# correspondrait pas — ou pire, correspondrait, et installerait l'ancienne version.
for arch, url, somme in zip(reste[0::3], reste[1::3], reste[2::3]):
remplacements += [
(rf'^ {arch}\.url = ".*"$', f' {arch}.url = "{url}"'),
(rf'^ {arch}\.sha256 = ".*"$', f' {arch}.sha256 = "{somme}"'),
]
for motif, valeur in remplacements:
texte, n = re.subn(motif, valeur, texte, count=1, flags=re.MULTILINE)
if n != 1:
sys.exit(f"Motif introuvable dans le manifeste : {motif}")
with open(chemin, "w", encoding="utf-8") as f:
f.write(texte)
PY
for runtime in "${runtimes[@]}"; do
arch="$(arch_ynh "$runtime")"
echo " $arch : ${urls[$arch]}"
echo " sha256 ${sommes[$arch]}"
done
#=================================================
# LE PAQUET (le serveur lit Gitea, jamais la copie locale)
# 2. LE PAQUET (le serveur lit Gitea, jamais la copie locale)
#=================================================
echo
@@ -530,13 +172,13 @@ fi
git -C "$racine_paquet" push
#=================================================
# LE CODE : tag et push
# 3. LE CODE : tag et push
#=================================================
echo
echo "=== Code : tag v$version et push ==="
git -C "$source_dir" tag "v$version"
git -C "$source_dir" push origin "$branche" "refs/tags/v$version"
git -C "$source_dir" push origin "$branche" --tags
# Un push qui échoue à demi ne se voit pas dans la sortie de git : on redemande au distant.
verifier_pousse() { # verifier_pousse <dépôt> <ref>
@@ -557,24 +199,25 @@ verifier_pousse "$racine_paquet" "refs/heads/$(git -C "$racine_paquet" rev-parse
# CE QUI RESTE À FAIRE À LA MAIN
#=================================================
url_archive="$base_url/v$version/$archive_nom"
cat <<FIN
=== Publié : $version ===
Il ne reste qu'une chose, dans Gitea : créer la release v$version et y téléverser
LES DEUX archives. Une seule des deux téléversée, et le manifeste promet à l'autre
architecture une URL qui répondra 404 à l'installation.
l'archive.
Releases : $url_releases
$(for r in "${runtimes[@]}"; do a="$(arch_ynh "$r")"; echo " $a : ${archives[$a]}"; done)
Archive : $archive
Ensuite, vérifier que le manifeste pointe sur des fichiers réellement téléchargeables
Ensuite, vérifier que le manifeste pointe sur un fichier réellement téléchargeable
(sans être authentifié) :
$(for r in "${runtimes[@]}"; do a="$(arch_ynh "$r")"; echo " curl -fsSLI \"${urls[$a]}\" | head -1"; done)
curl -fsSLI "$url_archive" | head -1
Puis, sur le serveur :
sudo yunohost app upgrade mabibli -u ${depot_code}_ynh
sudo yunohost app upgrade mabibli -u https://git.akbar.nohost.me/mathieu/mabibli_ynh
FIN
+1 -1
View File
@@ -1,6 +1,6 @@
[Unit]
Description=MaBibli — gestion de bibliothèque personnelle
Documentation=https://forge.example.org/mabibli
Documentation=https://git.akbar.nohost.me/mathieu/mabibli
After=network.target
[Service]
+61 -47
View File
@@ -1,68 +1,82 @@
## Comment l'authentification fonctionne
MaBibli n'a **pas de connexion propre**. L'identité vient entièrement du portail YunoHost :
nginx authentifie le visiteur, puis SSOwat injecte dans la requête les en-têtes `YNH_USER`,
`YNH_USER_EMAIL` et `YNH_USER_FULLNAME`, que l'application se contente de lire.
Conséquences pratiques :
- **Qui peut voir la bibliothèque se règle dans les permissions YunoHost** (`mabibli.main`),
pas dans l'application.
- La collection est **commune** à toutes les personnes autorisées. Chaque livre garde une trace
de qui l'a saisi, mais personne n'est cloisonné : c'est une bibliothèque de foyer.
- Les **statuts de lecture sont personnels** (chacun sa progression), les **prêts sont communs**
(un livre absent l'est pour tout le monde).
- Se déconnecter du portail YunoHost ne déconnecte pas nécessairement des applications :
chacune garde sa propre session. C'est une limite connue de YunoHost, pas de MaBibli.
⚠️ **Ne pas exposer le port interne.** Le service écoute volontairement sur `127.0.0.1`
uniquement. L'application fait confiance à `YNH_USER` parce que SSOwat écrase cet en-tête à
chaque requête ; un service joignable directement permettrait à quiconque de forger
`YNH_USER` et de contourner le portail. Le port n'est pas ouvert au pare-feu, et
`ASPNETCORE_URLS` dans l'unité systemd ne doit jamais être élargi à `0.0.0.0` ou `*`.
## Domaine entier obligatoire
MaBibli s'installe **sur un domaine entier**, pas sous un sous-chemin (`/mabibli`).
Le client est une application Blazor WebAssembly : son chemin de base et les empreintes
d'intégrité de son service worker sont figés **à la compilation**. Comme le paquet installe une
archive déjà compilée — et ne compile jamais rien sur le serveur — il n'existe pas de moyen
propre de les réécrire à l'installation. YunoHost refusera donc un changement d'URL vers un
sous-chemin.
## Où vivent les données
| Quoi | Où |
|---|---|
| Binaires et client web | `/var/www/mabibli` |
| Binaires et client web | `/var/www/mabibli` (appartient à `root`, l'application ne peut pas s'y écrire) |
| Base SQLite | `/home/yunohost.app/mabibli/mabibli.db` |
| Journaux | `journalctl -u mabibli` |
La base est séparée des binaires : une mise à jour remplace `/var/www/mabibli` sans toucher
aux données, et le schéma est migré automatiquement au démarrage. Rien à lancer à la main.
La base est **délibérément séparée des binaires** : une mise à jour remplace intégralement
`/var/www/mabibli` sans jamais toucher aux données. Le schéma est migré automatiquement au
démarrage, il n'y a aucune commande à lancer après une mise à jour.
## Qui a accès
## Sauvegarde
> ## ⚠️ MaBibli est une application **privée**
>
> Elle n'est **pas** faite pour être publique. L'installation la règle sur `all_users` —
> les comptes du serveur — et **ne pose pas la question**, parce qu'il n'y a pas de bonne
> raison de l'ouvrir plus large.
>
> **N'ajoutez jamais le groupe `visitors` à la permission `mabibli.main`.**
>
> Deux raisons, et ce sont des propriétés du code, pas des préférences :
>
> 1. **L'application n'a aucune authentification à elle.** Elle lit l'en-tête `YNH_USER`
> que le portail YunoHost injecte à chaque requête, et lui fait confiance. Ouverte aux
> visiteurs, plus personne n'écrit cet en-tête : tout ce qui est **personnel** — statuts
> de lecture, liste d'envies — devient lisible et modifiable par n'importe qui.
> 2. **Toute personne autorisée peut tout modifier**, y compris **supprimer** des livres du
> catalogue commun. Il n'existe ni rôle, ni lecture seule, ni journal des suppressions.
> C'est assumé pour un foyer, dont les membres se connaissent.
>
> Restreindre plus (un groupe dédié) est parfaitement légitime, et se fait dans les
> permissions YunoHost (`mabibli.main`). C'est **l'élargissement** qui est un contresens.
La base tourne en mode **WAL**. C'est important pour qui voudrait bricoler une sauvegarde à la
main : à un instant donné, l'essentiel des données peut se trouver dans `mabibli.db-wal` et
**pas** dans `mabibli.db`. Mesuré sur une base fraîchement migrée, `mabibli.db` faisait 4 Ko —
et ne contenait **aucune table** — pendant que le fichier `-wal` en portait 205 Ko.
MaBibli n'a pas de connexion propre : l'identité vient du portail YunoHost. **Réglez les accès
dans les permissions YunoHost** (`mabibli.main`), pas dans l'application.
Le script de sauvegarde du paquet ne copie donc pas les fichiers tels quels : il demande à
SQLite un instantané cohérent (`.backup`, l'API de sauvegarde en ligne), déposé à côté de la
base sous le nom `mabibli-instantane.db`. C'est ce fichier que la restauration remet en place,
en écartant au passage les `-wal` / `-shm` de l'archive, qui décrivaient l'état d'une autre
copie de la base.
La collection est **commune** à toutes les personnes autorisées. Les statuts de lecture sont
personnels, les prêts sont communs.
Le service **n'est pas arrêté** pendant la sauvegarde : l'API de sauvegarde en ligne garantit la
cohérence du fichier produit sans bloquer les lectures, et couper l'application à chaque
sauvegarde nocturne coûterait une indisponibilité pour rien.
⚠️ **Ne jamais élargir l'écoute du service.** Il écoute sur `127.0.0.1` uniquement, et c'est
une frontière de sécurité : l'application fait confiance à l'en-tête `YNH_USER` parce que
SSOwat l'écrase à chaque requête. Un service joignable directement permettrait de le forger.
## Sauvegarde manuelle
La base est en mode WAL : **un `cp` du fichier `.db` peut ne rien sauvegarder du tout.**
Pour une sauvegarde manuelle, la bonne commande est donc :
```bash
sqlite3 /home/yunohost.app/mabibli/mabibli.db ".backup '/quelque/part/mabibli.db'"
```
`yunohost backup create` fait déjà ce qu'il faut, sans arrêter le service.
et surtout pas un `cp` du seul fichier `.db`.
## Ce dont le serveur a besoin
## Le scan du code-barres exige HTTPS
- **Accès sortant HTTPS** vers `catalogue.bnf.fr` et `openlibrary.org`, pour pré-remplir les
fiches depuis un ISBN. Sans lui l'application marche, mais toute saisie devient manuelle.
- **HTTPS** pour le scan du code-barres : les navigateurs n'ouvrent la caméra qu'en contexte
sécurisé. Le certificat Let's Encrypt de YunoHost suffit ; joindre le serveur par son IP
locale fera toujours échouer le scan. La saisie manuelle reste disponible.
- **Un domaine entier.** MaBibli ne s'installe pas sous un sous-chemin : le client Blazor fige
son chemin de base à la compilation, et rien n'est compilé sur le serveur.
L'accès à la caméra n'est autorisé par les navigateurs que dans un contexte sécurisé. En
production, le certificat Let's Encrypt de YunoHost suffit. En revanche, joindre le serveur par
son IP locale (`http://192.168.x.x`) fera **toujours** échouer le scan : ce n'est pas un
contexte sécurisé. La saisie manuelle de l'ISBN reste disponible dans tous les cas.
---
## Accès sortant nécessaire
Le détail — conception du paquet, publication, mise à jour pas à pas, retour arrière —
est dans le [README du dépôt du code](https://forge.example.org/mabibli#readme).
Le serveur doit pouvoir joindre `catalogue.bnf.fr` et `openlibrary.org` en HTTPS pour
pré-remplir les fiches à partir d'un ISBN. Sans accès sortant, l'application fonctionne, mais
toute saisie devient manuelle.
+82
View File
@@ -0,0 +1,82 @@
## Comment l'authentification fonctionne
MaBibli n'a **pas de connexion propre**. L'identité vient entièrement du portail YunoHost :
nginx authentifie le visiteur, puis SSOwat injecte dans la requête les en-têtes `YNH_USER`,
`YNH_USER_EMAIL` et `YNH_USER_FULLNAME`, que l'application se contente de lire.
Conséquences pratiques :
- **Qui peut voir la bibliothèque se règle dans les permissions YunoHost** (`mabibli.main`),
pas dans l'application.
- La collection est **commune** à toutes les personnes autorisées. Chaque livre garde une trace
de qui l'a saisi, mais personne n'est cloisonné : c'est une bibliothèque de foyer.
- Les **statuts de lecture sont personnels** (chacun sa progression), les **prêts sont communs**
(un livre absent l'est pour tout le monde).
- Se déconnecter du portail YunoHost ne déconnecte pas nécessairement des applications :
chacune garde sa propre session. C'est une limite connue de YunoHost, pas de MaBibli.
⚠️ **Ne pas exposer le port interne.** Le service écoute volontairement sur `127.0.0.1`
uniquement. L'application fait confiance à `YNH_USER` parce que SSOwat écrase cet en-tête à
chaque requête ; un service joignable directement permettrait à quiconque de forger
`YNH_USER` et de contourner le portail. Le port n'est pas ouvert au pare-feu, et
`ASPNETCORE_URLS` dans l'unité systemd ne doit jamais être élargi à `0.0.0.0` ou `*`.
## Domaine entier obligatoire
MaBibli s'installe **sur un domaine entier**, pas sous un sous-chemin (`/mabibli`).
Le client est une application Blazor WebAssembly : son chemin de base et les empreintes
d'intégrité de son service worker sont figés **à la compilation**. Comme le paquet installe une
archive déjà compilée — et ne compile jamais rien sur le serveur — il n'existe pas de moyen
propre de les réécrire à l'installation. YunoHost refusera donc un changement d'URL vers un
sous-chemin.
## Où vivent les données
| Quoi | Où |
|---|---|
| Binaires et client web | `/var/www/mabibli` (appartient à `root`, l'application ne peut pas s'y écrire) |
| Base SQLite | `/home/yunohost.app/mabibli/mabibli.db` |
| Journaux | `journalctl -u mabibli` |
La base est **délibérément séparée des binaires** : une mise à jour remplace intégralement
`/var/www/mabibli` sans jamais toucher aux données. Le schéma est migré automatiquement au
démarrage, il n'y a aucune commande à lancer après une mise à jour.
## Sauvegarde
La base tourne en mode **WAL**. C'est important pour qui voudrait bricoler une sauvegarde à la
main : à un instant donné, l'essentiel des données peut se trouver dans `mabibli.db-wal` et
**pas** dans `mabibli.db`. Mesuré sur une base fraîchement migrée, `mabibli.db` faisait 4 Ko —
et ne contenait **aucune table** — pendant que le fichier `-wal` en portait 205 Ko.
Le script de sauvegarde du paquet ne copie donc pas les fichiers tels quels : il demande à
SQLite un instantané cohérent (`.backup`, l'API de sauvegarde en ligne), déposé à côté de la
base sous le nom `mabibli-instantane.db`. C'est ce fichier que la restauration remet en place,
en écartant au passage les `-wal` / `-shm` de l'archive, qui décrivaient l'état d'une autre
copie de la base.
Le service **n'est pas arrêté** pendant la sauvegarde : l'API de sauvegarde en ligne garantit la
cohérence du fichier produit sans bloquer les lectures, et couper l'application à chaque
sauvegarde nocturne coûterait une indisponibilité pour rien.
Pour une sauvegarde manuelle, la bonne commande est donc :
```bash
sqlite3 /home/yunohost.app/mabibli/mabibli.db ".backup '/quelque/part/mabibli.db'"
```
et surtout pas un `cp` du seul fichier `.db`.
## Le scan du code-barres exige HTTPS
L'accès à la caméra n'est autorisé par les navigateurs que dans un contexte sécurisé. En
production, le certificat Let's Encrypt de YunoHost suffit. En revanche, joindre le serveur par
son IP locale (`http://192.168.x.x`) fera **toujours** échouer le scan : ce n'est pas un
contexte sécurisé. La saisie manuelle de l'ISBN reste disponible dans tous les cas.
## Accès sortant nécessaire
Le serveur doit pouvoir joindre `catalogue.bnf.fr` et `openlibrary.org` en HTTPS pour
pré-remplir les fiches à partir d'un ISBN. Sans accès sortant, l'application fonctionne, mais
toute saisie devient manuelle.
-5
View File
@@ -1,10 +1,5 @@
MaBibli est une application de gestion de bibliothèque personnelle, pensée pour un foyer.
⚠️ **Application privée.** L'accès est réservé aux comptes du serveur (`all_users`) et
l'installation ne pose pas la question. MaBibli n'a pas de connexion propre — elle fait
confiance au portail YunoHost — et toute personne autorisée peut modifier et supprimer
n'importe quel livre. **Ne l'ouvrez pas aux visiteurs.**
- **Catalogue** des livres physiques et des ebooks (fiches uniquement, aucun fichier n'est hébergé).
- **Prêts** : à qui le livre a été confié, depuis quand, et l'historique complet des prêts passés.
- **Scan ISBN** au code-barres depuis le téléphone, ou saisie manuelle, avec pré-remplissage automatique du titre, de l'auteur, de l'éditeur et de la couverture.
+9
View File
@@ -0,0 +1,9 @@
MaBibli est une application de gestion de bibliothèque personnelle, pensée pour un foyer.
- **Catalogue** des livres physiques et des ebooks (fiches uniquement, aucun fichier n'est hébergé).
- **Prêts** : à qui le livre a été confié, depuis quand, et l'historique complet des prêts passés.
- **Scan ISBN** au code-barres depuis le téléphone, ou saisie manuelle, avec pré-remplissage automatique du titre, de l'auteur, de l'éditeur et de la couverture.
- **Statuts de lecture** personnels : chaque membre du foyer suit sa propre progression sur une collection commune.
- **Consultation hors-ligne** : l'application est une PWA installable, et la bibliothèque reste consultable sans réseau.
Les métadonnées viennent du catalogue de la **BnF** puis d'**OpenLibrary**, deux sources libres et sans clé d'API. Aucune dépendance à Google Books.
+17 -83
View File
@@ -7,7 +7,7 @@ name = "MaBibli"
description.fr = "Gestion de bibliothèque personnelle : catalogue, prêts et scan ISBN"
description.en = "Personal library manager: catalogue, loans and ISBN scanning"
version = "1.2.0~ynh1"
version = "0.4.1~ynh1"
maintainers = ["mathieu"]
@@ -19,13 +19,10 @@ code = "https://git.akbar.nohost.me/mathieu/mabibli"
yunohost = ">= 12.1.38"
helpers_version = "2.1"
# ⚠️ Une architecture déclarée ici DOIT avoir son archive dans la release : YunoHost
# choisit `<arch>.url` d'après la machine, et une URL manquante échoue chez celui qui
# installe, dans `ynh_setup_source`. `build/publier.sh` compile les deux à chaque
# publication, précisément pour qu'aucune des deux ne puisse être oubliée.
#
# armhf (32 bits) n'est PAS déclarée : .NET ne la prend plus en charge.
architectures = ["amd64", "arm64"]
# x86_64 uniquement : CLAUDE.md acte un serveur PC/VPS, et la chaîne de publication
# ne produit qu'un binaire `linux-x64`. Déclarer d'autres architectures promettrait
# des archives qui n'existent pas.
architectures = ["amd64"]
# Une seule instance : la bibliothèque est commune au foyer (CLAUDE.md, « Portée des
# données »). Deux instances voudraient dire deux collections étanches, ce qui est
@@ -40,33 +37,8 @@ sso = true
# ~170 Mo de publish self-contained (runtime .NET embarqué), plus la base et la marge
# de décompression de l'archive.
disk = "500M"
# ⚠️ `ram.build` ne désigne AUCUNE compilation : rien n'est compilé sur le serveur, c'est
# tout l'intérêt du self-contained. Il couvre le téléchargement et la décompression de
# l'archive de 69 Mo. Ne pas aller chercher la valeur du côté d'un build .NET.
ram.build = "50M"
# Mesuré sur le serveur le 2026-08-22, service en marche :
# cat /sys/fs/cgroup/system.slice/mabibli.service/memory.peak -> 208 793 600 o
# soit 199,1 Mio de pic, contre 200M déclarés jusqu'ici : 0,4 % de marge, c'est-à-dire
# aucune. Porté à 256M.
#
# ⚠️ `systemctl show -p MemoryPeak` ne renvoie RIEN sur ce serveur — la commande réussit
# en silence et l'on croit lire un pic là où l'on ne lit rien. Passer par le cgroup.
#
# ⚠️ Le chiffre MAJORE le besoin : `memory.peak` compte le cache de fichiers, donc une
# part des 69 Mo de binaire mappé, récupérable sous pression — et le GC de .NET se serre
# sur une machine étroite. Mais YunoHost compare une déclaration statique à la RAM
# disponible, sans faire cette nuance. L'asymétrie tranche : un refus d'installation est
# visible et se contourne, un OOM survient plus tard, sous une opération lourde, sans que
# rien ne désigne la cause.
#
# ⚠️ Le pic n'est PAS le lookup ISBN, contrairement à l'intuition — une notice. C'est
# « Nouveautés » sur un auteur très réédité : jusqu'à dix pages de 100 notices SRU en
# parallèle, plus le catalogue entier chargé pour le rapprochement. Ce cas-là n'a pas
# encore été mesuré, et il croît avec la taille de la bibliothèque : à reprendre le jour
# où le fonds aura beaucoup grossi.
ram.runtime = "256M"
ram.runtime = "200M"
[install]
@@ -86,34 +58,23 @@ ram.runtime = "256M"
# le cœur classe l'app en « full_domain » et refusera un changement d'URL vers
# un sous-chemin.
# Pas de question `init_main_permission` : l'accès est fixé à `all_users` par la
# ressource `permissions` ci-dessous, et l'installation ne demande donc rien.
#
# Vérifié dans le cœur YunoHost (`src/utils/resources.py`) :
#
# init_allowed = infos["allowed"] or self.get_setting(f"init_{perm}_permission") or []
#
# `allowed` l'emporte sur la question ; garder la question l'aurait donc posée pour
# rien, en laissant croire que la réponse comptait.
#
# ⚠️ Ce code ne s'exécute qu'à la CRÉATION de la permission. Une montée de version ne
# réaligne rien : un accès élargi à la main le reste.
[install.init_main_permission]
type = "group"
default = "all_users"
help.fr = "Qui peut accéder à la bibliothèque. MaBibli n'a pas de connexion propre : l'identité vient du portail YunoHost, et la collection est commune à toutes les personnes autorisées."
help.en = "Who may access the library. MaBibli has no login of its own: identity comes from the YunoHost portal, and the collection is shared between all allowed users."
[resources]
[resources.sources.main]
# Archives **déjà compilées** (publish self-contained), produites par
# `build/publier.sh` et déposées en release sur le Gitea de l'utilisateur.
# Archive **déjà compilée** (publish self-contained linux-x64), produite par
# `build/publier-release.sh` et déposée en release sur le Gitea de l'utilisateur.
# Le SDK .NET n'est jamais installé sur le serveur : c'est tout l'intérêt du
# self-contained (CLAUDE.md, « Décisions techniques actées »).
# self-contained (CLAUDE.md, « Ne jamais compiler sur le serveur »).
#
# YunoHost retient la ligne correspondant à l'architecture de la machine.
#
# ⚠️ Ces quatre valeurs sont réécrites par `build/publier.sh`, jamais à la main.
amd64.url = "https://git.akbar.nohost.me/mathieu/mabibli/releases/download/v1.2.0/mabibli-1.2.0-linux-x64.tar.gz"
amd64.sha256 = "daac8651a51b252aa1d8e569b195a5b62fc5de4149589d52e21bf0ad7b699acd"
arm64.url = "https://git.akbar.nohost.me/mathieu/mabibli/releases/download/v1.2.0/mabibli-1.2.0-linux-arm64.tar.gz"
arm64.sha256 = "d3c3e31338745d8373664a50c1bb72f849d04f05c824a6c31801f5ecf8365db6"
# Ces deux valeurs sont réécrites par `build/publier-release.sh`.
amd64.url = "https://git.akbar.nohost.me/mathieu/mabibli/releases/download/v0.4.1/mabibli-0.4.1-linux-x64.tar.gz"
amd64.sha256 = "21f6a437dcd118ed863d24ce1137f711ca19e004b5e89b6d482dcfd07ab7a884"
# L'archive dépose ses fichiers à la racine, sans dossier intermédiaire.
in_subdir = false
@@ -144,33 +105,6 @@ ram.runtime = "256M"
main.url = "/"
main.auth_header = true
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ ⚠️ MaBibli EST UNE APPLICATION PRIVÉE. NE JAMAIS L'OUVRIR AUX `visitors`. ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
#
# Ce n'est pas une préférence de confidentialité, c'est une propriété du code, et
# elle vaut sur les DEUX moitiés de l'application :
#
# • L'application n'a AUCUNE authentification propre. Elle lit `YNH_USER`, un
# en-tête que SSOwat injecte, et fait confiance à ce qu'il contient. Ouverte aux
# visiteurs, il n'y a plus personne pour l'écraser : tout ce qui est PERSONNEL
# (statuts de lecture, liste d'envies) devient lisible et modifiable par le
# premier venu. C'est la même frontière que « le service n'écoute que sur
# 127.0.0.1 » — SSOwat n'est pas un confort, c'est la seule porte.
#
# • Il n'existe AUCUNE notion d'écriture réservée. Toute personne autorisée peut
# ajouter, modifier et SUPPRIMER n'importe quel livre du catalogue commun, ainsi
# que noter un prêt ou son retour. C'est délibéré — un foyer, pas un service
# public — et cela suppose que les personnes autorisées se connaissent.
#
# `all_users` est donc le plus large qui ait du sens : les comptes du serveur.
# Restreindre davantage (un groupe dédié) est légitime ; élargir ne l'est pas.
#
# ⚠️ Volontairement PAS `protected = true` : l'administrateur du serveur reste
# maître de ses permissions, et peut vouloir restreindre à un groupe. C'est
# l'élargissement qui est un contresens, pas le réglage lui-même.
main.allowed = "all_users"
[resources.ports]
# Port interne uniquement. `exposed` vaut `false` par défaut : le pare-feu reste
# fermé, et l'unité systemd n'écoute que sur 127.0.0.1.