Compare commits

..
11 Commits
Author SHA1 Message Date
mathieu 090f3d88de Publier la version 1.2.0 2026-09-11 00:04:57 +02:00
mathieu e0b47e3432 Publier la version 1.1.5 2026-09-10 19:01:22 +02:00
mathieu b95c45530e Publier la version 1.1.4 2026-09-10 17:59:57 +02:00
mathieu d2307bdec6 Publier la version 1.1.3 2026-09-10 15:31:24 +02:00
mathieu 8033e5d074 Publier la version 1.1.2 2026-09-10 02:08:19 +02:00
mathieu 34339b2341 Merge branch 'main' of git.akbar.nohost.me:mathieu/mabibli_ynh into release-1.1.0 2026-09-09 22:24:23 +02:00
mathieu 31b16b0b36 Publier la version 1.1.1 2026-09-09 21:51:22 +02:00
mathieu 04ac5fb028 Merge pull request 'V1.1.0' (#1) from release-1.1.0 into main
Reviewed-on: #1
2026-09-09 02:51:35 +02:00
mathieu e0fa5743ea V1.1.0 2026-09-09 02:06:42 +02:00
Mathieu Limonier f24c58fc46 Publier la version 1.0.1 2026-08-23 17:38:53 +02:00
Mathieu LimonierandClaude Opus 5 1373532345 Paquet YunoHost de MaBibli 1.0.0
Installe l'archive self-contained publiée en release du dépôt du code,
amd64 ou arm64 selon la machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:36:27 +02:00
9 changed files with 546 additions and 514 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# Paquet YunoHost pour MaBibli # Paquet YunoHost pour MaBibli
Paquet d'installation de [MaBibli](https://git.akbar.nohost.me/mathieu/mabibli), application de Paquet d'installation de [MaBibli](https://forge.example.org/mabibli), application de
gestion de bibliothèque personnelle (catalogue, prêts, scan ISBN). 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 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 ## 📖 Toute la documentation est dans le dépôt du code
Elle a été regroupée dans **[le README de `mabibli`](https://git.akbar.nohost.me/mathieu/mabibli#readme)**, Elle a été regroupée dans **[le README de `mabibli`](https://forge.example.org/mabibli#readme)**,
qui est le point d'entrée unique du projet : contenu et points de conception du paquet, 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, publication d'une version, mise en production pas à pas, montée de version, retour arrière,
désinstallation, et contraintes permanentes. désinstallation, et contraintes permanentes.
-291
View File
@@ -1,291 +0,0 @@
#!/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
+408 -51
View File
@@ -2,37 +2,86 @@
# #
# Publie une version de MaBibli de bout en bout, en une commande. # Publie une version de MaBibli de bout en bout, en une commande.
# #
# Enchaîne ce qui était jusqu'ici tapé à la main, toujours dans le même ordre : # 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)
# #
# 1. produire l'archive et mettre à jour manifest.toml (publier-release.sh) # La seule chose qui reste manuelle est le dépôt des archives dans la release Gitea :
# 2. committer et pousser mabibli_ynh (le serveur lit Gitea, pas le local) # elles sont rappelées en fin de sortie, une par architecture.
# 3. taguer et pousser mabibli (le tag porte le code de la release)
# #
# La version se choisit ici, à partir de celle déjà publiée : rien à aller chercher. # Le mode `--archive-seule` compile et archive sans toucher ni
# La seule chose qui reste manuelle est le dépôt de l'archive dans la release Gitea — # au manifeste ni à git
# l'URL est rappelée en fin de sortie.
# #
set -euo pipefail set -euo pipefail
#=================================================
# VALEURS PAR DÉFAUT
#=================================================
racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
manifeste="$racine_paquet/manifest.toml" manifeste="$racine_paquet/manifest.toml"
source_dir="${MABIBLI_SOURCE_DIR:-$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)}" # ⚠️ Chaque réglage a UNE seule façon de se donner : son option en ligne de commande.
base_url="${MABIBLI_BASE_URL:-https://git.akbar.nohost.me/mathieu/mabibli/releases/download}" # ⚠️ LA SEULE LIGNE À CHANGER pour héberger ce projet ailleurs.
url_releases="${MABIBLI_URL_RELEASES:-https://git.akbar.nohost.me/mathieu/mabibli/releases}" # 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"
version="" version=""
increment="" increment=""
sans_confirmation=0 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() { usage() {
cat <<'FIN' cat <<'FIN'
Usage : publier.sh [options] Usage : publier.sh [options]
--version X.Y.Z Version à publier (sinon : demandée, à partir de la précédente) --version X.Y.Z Version à publier (sinon : demandée, à partir de la précédente)
--patch Incrémenter le dernier chiffre (0.3.0 -> 0.3.1) --patch Incrémenter le dernier chiffre (1.0.0 -> 1.0.1)
--minor Incrémenter le chiffre du milieu (0.3.0 -> 0.4.0) --minor Incrémenter le chiffre du milieu (1.0.0 -> 1.1.0)
--major Incrémenter le premier chiffre (0.3.0 -> 1.0.0) --major Incrémenter le premier chiffre (1.0.0 -> 2.0.0)
--oui Ne rien demander (pour une CI) --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)
-h, --help Cette aide -h, --help Cette aide
FIN FIN
} }
@@ -42,6 +91,11 @@ while [ $# -gt 0 ]; do
--version) version="$2"; shift 2 ;; --version) version="$2"; shift 2 ;;
--patch|--minor|--major) increment="${1#--}"; shift ;; --patch|--minor|--major) increment="${1#--}"; shift ;;
--oui|-y) sans_confirmation=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 ;; -h|--help) usage; exit 0 ;;
*) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;; *) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;;
esac esac
@@ -49,13 +103,74 @@ done
echoerr() { echo "$@" >&2; } echoerr() { echo "$@" >&2; }
if [ -z "$source_dir" ] || [ ! -d "$source_dir/.git" ]; then # ⚠️ LE SEUL ENDROIT où `--archive-seule` assouplit quoi que ce soit.
echoerr "Dépôt du code introuvable : '${source_dir:-<vide>}' (MABIBLI_SOURCE_DIR)." #
# 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."
exit 1 exit 1
fi fi
#================================================= #=================================================
# LA VERSION PRÉCÉDENTE, DEUX SOURCES QUI DOIVENT S'ACCORDER # LA VERSION
#================================================= #=================================================
# Le tag fait foi : c'est lui qui désigne le code publié. Le manifeste ne porte que la # Le tag fait foi : c'est lui qui désigne le code publié. Le manifeste ne porte que la
@@ -65,16 +180,7 @@ 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)" version_manifeste="$(sed -n 's/^version *= *"\([^"~]*\).*"/\1/p' "$manifeste" | head -1)"
precedente="$(printf '%s\n%s\n' "$version_tag" "$version_manifeste" \ precedente="$(printf '%s\n%s\n' "$version_tag" "$version_manifeste" \
| grep -E '^[0-9]+(\.[0-9]+)*$' | sort -V | tail -1)" | grep -E '^[0-9]+(\.[0-9]+)*$' | sort -V | tail -1 || true)"
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> suivante() { # suivante <version> <patch|minor|major>
IFS=. read -r maj min cor <<<"$1" IFS=. read -r maj min cor <<<"$1"
@@ -85,6 +191,19 @@ suivante() { # suivante <version> <patch|minor|major>
esac 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 if [ -z "$version" ] && [ -n "$increment" ]; then
version="$(suivante "$precedente" "$increment")" version="$(suivante "$precedente" "$increment")"
fi fi
@@ -115,30 +234,43 @@ if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
exit 1 exit 1
fi 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 # 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 # 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. # pas dans la release, alors même que l'archive, elle, le contiendrait.
if [ -n "$(git -C "$source_dir" status --porcelain)" ]; then 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 git -C "$source_dir" status --short >&2
exit 1 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."
fi fi
branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)"
echo echo
echo "=== À publier ===" echo "=== À publier ==="
echo " version : $precedente -> $version" echo " version : ${precedente:-—} -> $version"
branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)"
echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))" echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))"
echo " paquet : $racine_paquet" echo " paquet : $racine_paquet"
[ "$archive_seule" -eq 1 ] &&
echo " mode : --archive-seule (ni manifeste, ni commit, ni tag)"
if [ "$sans_confirmation" -ne 1 ]; then if [ "$sans_confirmation" -ne 1 ]; then
read -rp "Continuer ? [O/n] " reponse read -rp "Continuer ? [O/n] " reponse
@@ -149,16 +281,242 @@ if [ "$sans_confirmation" -ne 1 ]; then
fi fi
#================================================= #=================================================
# 1. ARCHIVE + MANIFESTE # COMPILATION
#================================================= #=================================================
"$racine_paquet/build/publier-release.sh" --version "$version" --source-dir "$source_dir" --base-url "$base_url" # 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
archive_nom="mabibli-$version-linux-x64.tar.gz" travail="$(mktemp -d)"
archive="${MABIBLI_OUTPUT_DIR:-$racine_paquet/build/dist}/$archive_nom" # 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"
#================================================= #=================================================
# 2. LE PAQUET (le serveur lit Gitea, jamais la copie locale) # 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)
#================================================= #=================================================
echo echo
@@ -172,13 +530,13 @@ fi
git -C "$racine_paquet" push git -C "$racine_paquet" push
#================================================= #=================================================
# 3. LE CODE : tag et push # LE CODE : tag et push
#================================================= #=================================================
echo echo
echo "=== Code : tag v$version et push ===" echo "=== Code : tag v$version et push ==="
git -C "$source_dir" tag "v$version" git -C "$source_dir" tag "v$version"
git -C "$source_dir" push origin "$branche" --tags git -C "$source_dir" push origin "$branche" "refs/tags/v$version"
# Un push qui échoue à demi ne se voit pas dans la sortie de git : on redemande au distant. # 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> verifier_pousse() { # verifier_pousse <dépôt> <ref>
@@ -199,25 +557,24 @@ verifier_pousse "$racine_paquet" "refs/heads/$(git -C "$racine_paquet" rev-parse
# CE QUI RESTE À FAIRE À LA MAIN # CE QUI RESTE À FAIRE À LA MAIN
#================================================= #=================================================
url_archive="$base_url/v$version/$archive_nom"
cat <<FIN cat <<FIN
=== Publié : $version === === Publié : $version ===
Il ne reste qu'une chose, dans Gitea : créer la release v$version et y téléverser Il ne reste qu'une chose, dans Gitea : créer la release v$version et y téléverser
l'archive. 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.
Releases : $url_releases Releases : $url_releases
Archive : $archive $(for r in "${runtimes[@]}"; do a="$(arch_ynh "$r")"; echo " $a : ${archives[$a]}"; done)
Ensuite, vérifier que le manifeste pointe sur un fichier réellement téléchargeable Ensuite, vérifier que le manifeste pointe sur des fichiers réellement téléchargeables
(sans être authentifié) : (sans être authentifié) :
curl -fsSLI "$url_archive" | head -1 $(for r in "${runtimes[@]}"; do a="$(arch_ynh "$r")"; echo " curl -fsSLI \"${urls[$a]}\" | head -1"; done)
Puis, sur le serveur : Puis, sur le serveur :
sudo yunohost app upgrade mabibli -u https://git.akbar.nohost.me/mathieu/mabibli_ynh sudo yunohost app upgrade mabibli -u ${depot_code}_ynh
FIN FIN
+1 -1
View File
@@ -1,6 +1,6 @@
[Unit] [Unit]
Description=MaBibli — gestion de bibliothèque personnelle Description=MaBibli — gestion de bibliothèque personnelle
Documentation=https://git.akbar.nohost.me/mathieu/mabibli Documentation=https://forge.example.org/mabibli
After=network.target After=network.target
[Service] [Service]
+47 -61
View File
@@ -1,82 +1,68 @@
## 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 ## Où vivent les données
| Quoi | Où | | Quoi | Où |
|---|---| |---|---|
| Binaires et client web | `/var/www/mabibli` (appartient à `root`, l'application ne peut pas s'y écrire) | | Binaires et client web | `/var/www/mabibli` |
| Base SQLite | `/home/yunohost.app/mabibli/mabibli.db` | | Base SQLite | `/home/yunohost.app/mabibli/mabibli.db` |
| Journaux | `journalctl -u mabibli` | | Journaux | `journalctl -u mabibli` |
La base est **délibérément séparée des binaires** : une mise à jour remplace intégralement La base est séparée des binaires : une mise à jour remplace `/var/www/mabibli` sans toucher
`/var/www/mabibli` sans jamais toucher aux données. Le schéma est migré automatiquement au aux données, et le schéma est migré automatiquement au démarrage. Rien à lancer à la main.
démarrage, il n'y a aucune commande à lancer après une mise à jour.
## Sauvegarde ## Qui a accès
La base tourne en mode **WAL**. C'est important pour qui voudrait bricoler une sauvegarde à la > ## ⚠️ MaBibli est une application **privée**
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 — > Elle n'est **pas** faite pour être publique. L'installation la règle sur `all_users` —
et ne contenait **aucune table** — pendant que le fichier `-wal` en portait 205 Ko. > 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.
Le script de sauvegarde du paquet ne copie donc pas les fichiers tels quels : il demande à MaBibli n'a pas de connexion propre : l'identité vient du portail YunoHost. **Réglez les accès
SQLite un instantané cohérent (`.backup`, l'API de sauvegarde en ligne), déposé à côté de la dans les permissions YunoHost** (`mabibli.main`), pas dans l'application.
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 La collection est **commune** à toutes les personnes autorisées. Les statuts de lecture sont
cohérence du fichier produit sans bloquer les lectures, et couper l'application à chaque personnels, les prêts sont communs.
sauvegarde nocturne coûterait une indisponibilité pour rien.
Pour une sauvegarde manuelle, la bonne commande est donc : ⚠️ **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.**
```bash ```bash
sqlite3 /home/yunohost.app/mabibli/mabibli.db ".backup '/quelque/part/mabibli.db'" sqlite3 /home/yunohost.app/mabibli/mabibli.db ".backup '/quelque/part/mabibli.db'"
``` ```
et surtout pas un `cp` du seul fichier `.db`. `yunohost backup create` fait déjà ce qu'il faut, sans arrêter le service.
## Le scan du code-barres exige HTTPS ## Ce dont le serveur a besoin
L'accès à la caméra n'est autorisé par les navigateurs que dans un contexte sécurisé. En - **Accès sortant HTTPS** vers `catalogue.bnf.fr` et `openlibrary.org`, pour pré-remplir les
production, le certificat Let's Encrypt de YunoHost suffit. En revanche, joindre le serveur par fiches depuis un ISBN. Sans lui l'application marche, mais toute saisie devient manuelle.
son IP locale (`http://192.168.x.x`) fera **toujours** échouer le scan : ce n'est pas un - **HTTPS** pour le scan du code-barres : les navigateurs n'ouvrent la caméra qu'en contexte
contexte sécurisé. La saisie manuelle de l'ISBN reste disponible dans tous les cas. 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.
## Accès sortant nécessaire ---
Le serveur doit pouvoir joindre `catalogue.bnf.fr` et `openlibrary.org` en HTTPS pour Le détail — conception du paquet, publication, mise à jour pas à pas, retour arrière —
pré-remplir les fiches à partir d'un ISBN. Sans accès sortant, l'application fonctionne, mais est dans le [README du dépôt du code](https://forge.example.org/mabibli#readme).
toute saisie devient manuelle.
-82
View File
@@ -1,82 +0,0 @@
## 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,5 +1,10 @@
MaBibli est une application de gestion de bibliothèque personnelle, pensée pour un foyer. 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é). - **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. - **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. - **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
@@ -1,9 +0,0 @@
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.
+83 -17
View File
@@ -7,7 +7,7 @@ name = "MaBibli"
description.fr = "Gestion de bibliothèque personnelle : catalogue, prêts et scan ISBN" description.fr = "Gestion de bibliothèque personnelle : catalogue, prêts et scan ISBN"
description.en = "Personal library manager: catalogue, loans and ISBN scanning" description.en = "Personal library manager: catalogue, loans and ISBN scanning"
version = "0.4.1~ynh1" version = "1.2.0~ynh1"
maintainers = ["mathieu"] maintainers = ["mathieu"]
@@ -19,10 +19,13 @@ code = "https://git.akbar.nohost.me/mathieu/mabibli"
yunohost = ">= 12.1.38" yunohost = ">= 12.1.38"
helpers_version = "2.1" helpers_version = "2.1"
# x86_64 uniquement : CLAUDE.md acte un serveur PC/VPS, et la chaîne de publication # ⚠️ Une architecture déclarée ici DOIT avoir son archive dans la release : YunoHost
# ne produit qu'un binaire `linux-x64`. Déclarer d'autres architectures promettrait # choisit `<arch>.url` d'après la machine, et une URL manquante échoue chez celui qui
# des archives qui n'existent pas. # installe, dans `ynh_setup_source`. `build/publier.sh` compile les deux à chaque
architectures = ["amd64"] # 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"]
# Une seule instance : la bibliothèque est commune au foyer (CLAUDE.md, « Portée des # 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 # données »). Deux instances voudraient dire deux collections étanches, ce qui est
@@ -37,8 +40,33 @@ sso = true
# ~170 Mo de publish self-contained (runtime .NET embarqué), plus la base et la marge # ~170 Mo de publish self-contained (runtime .NET embarqué), plus la base et la marge
# de décompression de l'archive. # de décompression de l'archive.
disk = "500M" 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" ram.build = "50M"
ram.runtime = "200M"
# 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"
[install] [install]
@@ -58,23 +86,34 @@ ram.runtime = "200M"
# le cœur classe l'app en « full_domain » et refusera un changement d'URL vers # le cœur classe l'app en « full_domain » et refusera un changement d'URL vers
# un sous-chemin. # un sous-chemin.
[install.init_main_permission] # Pas de question `init_main_permission` : l'accès est fixé à `all_users` par la
type = "group" # ressource `permissions` ci-dessous, et l'installation ne demande donc rien.
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." # Vérifié dans le cœur YunoHost (`src/utils/resources.py`) :
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." #
# 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.
[resources] [resources]
[resources.sources.main] [resources.sources.main]
# Archive **déjà compilée** (publish self-contained linux-x64), produite par # Archives **déjà compilées** (publish self-contained), produites par
# `build/publier-release.sh` et déposée en release sur le Gitea de l'utilisateur. # `build/publier.sh` et déposées 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 # Le SDK .NET n'est jamais installé sur le serveur : c'est tout l'intérêt du
# self-contained (CLAUDE.md, « Ne jamais compiler sur le serveur »). # self-contained (CLAUDE.md, « Décisions techniques actées »).
# #
# Ces deux valeurs sont réécrites par `build/publier-release.sh`. # YunoHost retient la ligne correspondant à l'architecture de la machine.
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" # ⚠️ 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"
# L'archive dépose ses fichiers à la racine, sans dossier intermédiaire. # L'archive dépose ses fichiers à la racine, sans dossier intermédiaire.
in_subdir = false in_subdir = false
@@ -105,6 +144,33 @@ ram.runtime = "200M"
main.url = "/" main.url = "/"
main.auth_header = true 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] [resources.ports]
# Port interne uniquement. `exposed` vaut `false` par défaut : le pare-feu reste # 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. # fermé, et l'unité systemd n'écoute que sur 127.0.0.1.