ajoute le stockage S3 des devoirs maison (bloc B, dernier chantier)

Dépôt de fichier en plus du texte libre pour les devoirs maison, stocké
sur Hetzner Object Storage (jamais sur le disque du serveur), masqué
côté élève tant que le bucket n'est pas configuré. Purge RGPD étendue
pour supprimer aussi les fichiers d'Object Storage. Ansible lit les
identifiants depuis un fichier que seul Adam peut créer sur le serveur
(runbook docs/runbooks/stockage-s3.md) ; le bloc B du CLAUDE.md est
maintenant complet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-14 09:52:15 +02:00
parent 79d4a06fdf
commit 6ea7a1cc16
16 changed files with 1022 additions and 59 deletions

View file

@ -1150,3 +1150,28 @@ Deux fois dans ce chantier, mesurer plutôt que deviner a changé la décision :
**Suite**
Dernier point du bloc B : stockage S3 des travaux d'élèves (Hetzner Object Storage, avec versioning).
## 2026-10-14 — Stockage S3 des devoirs maison (bloc B, dernier chantier)
Dernier point du bloc B annoncé par le CLAUDE.md : dépôt de fichier pour les devoirs maison, en plus de la réponse en texte libre déjà existante, stocké sur Hetzner Object Storage (jamais sur le disque du serveur).
**Fait**
- Migration : `reponse` devient facultative sur `rendus_devoirs`, trois colonnes ajoutées (`fichier_chemin`, `fichier_nom_original`, `fichier_taille_octets`) — un rendu a désormais au moins l'un des deux, texte ou fichier.
- `App\Services\Stockage\StockageTravauxEleves` : stocke/supprime sur le disque `s3` (`league/flysystem-aws-s3-v3`), lien de téléchargement temporaire (5 min), jamais le fichier servi en clair par le portail. `disponible()` renvoie `false` tant que `AWS_BUCKET` n'est pas renseigné — le bouton de dépôt reste alors simplement masqué côté élève (CLAUDE.md : pas de fonction qui a l'air de marcher sans vraiment marcher).
- `DevoirEleveController::rendre()` : validation « l'un ou l'autre » (`required_without`), 10 Mo max, extensions limitées (pdf, doc(x), odt, txt, images, zip), 503 si un fichier est envoyé alors que le disque n'est pas configuré.
- Bug trouvé et corrigé avant même de tester (relecture de mon propre code) : la première version ne supprimait l'ancien fichier d'Object Storage que par comparaison de chemins, ce qui aurait effacé un fichier déjà déposé à chaque renvoi en texte seul. Corrigé : la suppression de l'ancien fichier n'a lieu que si un *nouveau* fichier est vraiment envoyé dans la requête.
- Téléchargement côté prof : `DevoirController::telechargerRendu`, redirige vers un lien signé temporaire, réservé au prof propriétaire de la classe.
- Purge RGPD (`classes:purger-archivees`) étendue : les fichiers d'Object Storage des devoirs de la classe sont supprimés avant la suppression en base (la cascade en base ne connaît pas Object Storage).
- Ansible (`roles/portail/tasks/03-code.yml`) : bloc conditionnel qui lit `/etc/syllabus/s3-paillasse.yaml` s'il existe et écrit les variables `AWS_*` dans `.env` — fichier à créer par Adam lui-même sur le serveur (jamais par Claude, CLAUDE.md « commandes et dépenses »), rien ne change tant qu'il n'existe pas. Runbook dédié : `docs/runbooks/stockage-s3.md`.
- 9 tests Pest (`Storage::fake('s3')`) : dépôt masqué/refusé sans bucket configuré, fichier seul sans texte, validation (ni l'un ni l'autre, taille, extension), remplacement supprime l'ancien mais un renvoi texte seul le conserve, téléchargement réservé au prof propriétaire, affichage côté prof, suppression lors de la purge RGPD. 125 tests au total, tous verts ; Pint et Larastan (niveau 6) sans erreur sur tout le projet.
**Comprendre**
Le dernier bug (suppression accidentelle d'un fichier conservé) est le genre d'erreur qu'un test aurait aussi bien pu laisser passer si je ne l'avais écrit qu'après coup en pensant au cas « remplacement » et en oubliant le cas « renvoi texte seul » : relire sa propre logique de décision (« dans quels cas est-ce qu'on supprime ? ») avant d'écrire le test, pas seulement après, aide à voir le cas manquant.
**Vérifié en réel** : déployé sur le serveur, migration appliquée, bloc Ansible S3 confirmé sans effet tant que `/etc/syllabus/s3-paillasse.yaml` n'existe pas (aucune variable `AWS_` dans `.env`), `StockageTravauxEleves::disponible()` renvoie bien `false` en production (vérifié par tinker, compte jetable supprimé ensuite). Le dépôt réel contre un vrai bucket Hetzner ne pourra être vérifié qu'une fois le bucket créé par Adam (hors de portée de Claude) — couvert pour l'instant par les tests contre un disque S3 factice.
**Suite**
Le bloc B du CLAUDE.md est maintenant complet (API publique, cache Redis, optimisation MariaDB, stockage S3). La suite reste au choix d'Adam : créer le bucket S3 (runbook ci-dessus), avancer sur le bloc C, ou organiser le pilote de la semaine 8 du bloc A.