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

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\Classe;
use App\Models\Devoir;
use App\Models\RenduDevoir;
use App\Services\Stockage\StockageTravauxEleves;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -64,6 +65,16 @@ class DevoirController extends Controller
return redirect()->route('mes-classes.devoirs.show', [$classe, $devoir]);
}
public function telechargerRendu(Classe $classe, Devoir $devoir, RenduDevoir $rendu): RedirectResponse
{
$this->verifierProprietaire($classe);
$this->verifierAppartenance($classe, $devoir);
abort_if($rendu->devoir_id !== $devoir->id, 404);
abort_if($rendu->fichier_chemin === null, 404);
return redirect()->away(StockageTravauxEleves::urlTemporaire($rendu->fichier_chemin));
}
public function destroy(Classe $classe, Devoir $devoir): RedirectResponse
{
$this->verifierProprietaire($classe);

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\Devoir;
use App\Models\Eleve;
use App\Models\RenduDevoir;
use App\Services\Stockage\StockageTravauxEleves;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -23,6 +24,7 @@ class DevoirEleveController extends Controller
return view('devoirs.eleve', [
'devoir' => $devoir,
'rendu' => RenduDevoir::where('devoir_id', $devoir->id)->where('eleve_id', $eleve->id)->first(),
'depotFichierDisponible' => StockageTravauxEleves::disponible(),
]);
}
@ -31,12 +33,39 @@ class DevoirEleveController extends Controller
$eleve = $this->verifierAppartenance($devoir);
abort_if($devoir->echeanceDepassee(), 409, 'L’échéance est dépassée.');
$donnees = $request->validate(['reponse' => ['required', 'string', 'max:10000']]);
$donnees = $request->validate([
'reponse' => ['nullable', 'required_without:fichier', 'string', 'max:10000'],
'fichier' => [
'nullable', 'required_without:reponse', 'file',
'max:'.(int) (StockageTravauxEleves::TAILLE_MAX_OCTETS / 1024),
'mimes:'.implode(',', StockageTravauxEleves::EXTENSIONS_ACCEPTEES),
],
]);
RenduDevoir::updateOrCreate(
['devoir_id' => $devoir->id, 'eleve_id' => $eleve->id],
['reponse' => $donnees['reponse'], 'rendu_a' => now()]
);
if ($request->hasFile('fichier') && ! StockageTravauxEleves::disponible()) {
abort(503, 'Le dépôt de fichier n’est pas disponible pour le moment.');
}
$ancienRendu = RenduDevoir::where('devoir_id', $devoir->id)->where('eleve_id', $eleve->id)->first();
$valeurs = ['reponse' => $donnees['reponse'] ?? null, 'rendu_a' => now()];
// Pas de nouveau fichier : celui déjà rendu (s'il existe) reste en
// place, updateOrCreate ne touche pas aux colonnes absentes de $valeurs.
if ($request->hasFile('fichier')) {
$fichier = StockageTravauxEleves::stocker($request->file('fichier'), (string) $devoir->id, (string) $eleve->id);
$valeurs['fichier_chemin'] = $fichier['chemin'];
$valeurs['fichier_nom_original'] = $fichier['nom_original'];
$valeurs['fichier_taille_octets'] = $fichier['taille_octets'];
}
RenduDevoir::updateOrCreate(['devoir_id' => $devoir->id, 'eleve_id' => $eleve->id], $valeurs);
// Un nouveau fichier remplace l'ancien : celui-ci n'a plus de raison
// d'exister sur Object Storage.
if ($request->hasFile('fichier') && $ancienRendu?->fichier_chemin) {
StockageTravauxEleves::supprimer($ancienRendu->fichier_chemin);
}
return redirect()->route('devoirs.eleve.show', $devoir)->with('rendu_envoye', true);
}