Skip to content

Commit 41ca7ab

Browse files
committed
fix(allDownload): ajout d'un rate limit et possibilité d'annuler le téléchargement #72
1 parent 8f36e2a commit 41ca7ab

File tree

2 files changed

+325
-143
lines changed

2 files changed

+325
-143
lines changed

src/components/features/MenuCompenents/DownloadModal.tsx

Lines changed: 105 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { createModal } from "@codegouvfr/react-dsfr/Modal";
22
import { useDalleStore } from "../../../hooks/store/useDalleStore";
33
import "./styles/DownloadModal.css";
44
import { Select } from "@codegouvfr/react-dsfr/Select";
5-
import { useState, useEffect } from "react";
5+
import { useState, useEffect, useRef } from "react";
66
import { RadioButtons } from "@codegouvfr/react-dsfr/RadioButtons";
77
import Button from "@codegouvfr/react-dsfr/Button";
88
import { formatBytes } from "../../../utils/formatters";
9-
import { downloadZip, getFileSizes } from "../../../utils/download";
9+
import { downloadZip, getFileSizes, DownloadPhase } from "../../../utils/download";
1010
import { Dalle } from "../../../assets/@types/types";
1111

1212
/** Instance du modal de téléchargement, partageable pour l'ouvrir/fermer depuis l'extérieur. */
@@ -26,6 +26,14 @@ type DownloadMethod = "all" | "file" | "";
2626
// Helpers
2727
// ---------------------------------------------------------------------------
2828

29+
/** Libellés affichés pour chaque phase de téléchargement. */
30+
const PHASE_LABELS: Record<DownloadPhase, string> = {
31+
idle: "",
32+
preparing: "Préparation du téléchargement...",
33+
downloading: "Téléchargement des fichiers en cours...",
34+
compressing: "Compression de l'archive ZIP...",
35+
};
36+
2937
/**
3038
* Génère le contenu texte d'un fichier listant les URLs des produits.
3139
* Une URL par ligne.
@@ -57,10 +65,10 @@ const triggerFileDownload = (
5765
};
5866

5967
/**
60-
* Génère et télécharge un unique fichier `metadonnees.json` agrégeant les
61-
* métadonnées de tous les produits, indexées par nom de produit.
68+
* Génère et télécharge un unique `metadonnees.json` agrégeant les métadonnées
69+
* de tous les produits, indexées par nom de produit.
6270
*
63-
* Structure du fichier généré :
71+
* Structure :
6472
* ```json
6573
* {
6674
* "LHD_FXX_0656_6861": {
@@ -96,12 +104,14 @@ const downloadAggregatedMetadata = (produits: Dalle[]): void => {
96104
*
97105
* - **Téléchargement automatique (ZIP)** : un sous-dossier par produit
98106
* contenant le fichier de données et son `.json` de métadonnées.
107+
* Respecte la limite de 10 requêtes/seconde du serveur.
108+
* Affiche la phase en cours (préparation / téléchargement / compression).
109+
* Demande confirmation avant d'annuler si un téléchargement est en cours.
99110
*
100-
* - **Liens de téléchargement** : un fichier `Liens_de_telechargement.txt`
101-
* (une URL par ligne) + un `metadonnees.json` unique indexé par nom.
111+
* - **Liens de téléchargement** : `Liens_de_telechargement.txt` + `metadonnees.json`.
102112
*
103-
* Si `isMetadata` est vrai, l'utilisateur doit obligatoirement choisir une
104-
* option dans le <Select> avant de pouvoir soumettre le formulaire.
113+
* Si `isMetadata` est vrai, l'utilisateur doit choisir une option dans le
114+
* <Select> avant de pouvoir soumettre.
105115
*/
106116
const DownloadModal = () => {
107117
// ---------------------------------------------------------------------------
@@ -137,6 +147,15 @@ const DownloadModal = () => {
137147
/** Progression du téléchargement ZIP (0-100). */
138148
const [downloadProgress, setDownloadProgress] = useState<number>(0);
139149

150+
/** Phase courante du téléchargement (pour afficher un label précis). */
151+
const [downloadPhase, setDownloadPhase] = useState<DownloadPhase>("idle");
152+
153+
/**
154+
* Référence vers l'AbortController actif.
155+
* Permet d'annuler le téléchargement en cours depuis n'importe quel handler.
156+
*/
157+
const abortControllerRef = useRef<AbortController | null>(null);
158+
140159
// ---------------------------------------------------------------------------
141160
// Effets
142161
// ---------------------------------------------------------------------------
@@ -184,9 +203,7 @@ const DownloadModal = () => {
184203
/**
185204
* Gère la soumission du formulaire.
186205
*
187-
* - Affiche une erreur sur le <Select> si `isMetadata` est vrai et qu'aucune
188-
* option n'a été choisie (le bouton est aussi désactivé dans ce cas).
189-
* - "all" : ZIP structuré en sous-dossiers (données + métadonnées par produit).
206+
* - "all" : ZIP avec rate-limiting 10 req/s, phases affichées, annulable.
190207
* - "file" : fichier texte des liens + `metadonnees.json` indexé par nom.
191208
*/
192209
const handleSubmit = async (e: React.FormEvent) => {
@@ -203,24 +220,44 @@ const DownloadModal = () => {
203220
}
204221

205222
if (downloadMethod === "all") {
223+
const controller = new AbortController();
224+
abortControllerRef.current = controller;
225+
206226
setIsDownloadLoading(true);
207227
setDownloadProgress(0);
228+
setDownloadPhase("idle");
208229

209-
await downloadZip(
210-
selectedProduits.map((p) => ({
211-
url: p.url,
212-
name: p.name,
213-
...(isMetadata && p.metadata ? { metadata: p.metadata } : {}),
214-
})),
215-
setDownloadProgress,
216-
fileSizes
217-
);
230+
try {
231+
await downloadZip(
232+
selectedProduits.map((p) => ({
233+
url: p.url,
234+
name: p.name,
235+
...(isMetadata && p.metadata ? { metadata: p.metadata } : {}),
236+
})),
237+
setDownloadProgress,
238+
setDownloadPhase,
239+
fileSizes,
240+
controller.signal
241+
);
242+
} catch (err) {
243+
if ((err as Error).name === "AbortError") {
244+
// Téléchargement annulé volontairement — pas d'erreur à afficher
245+
console.info("Téléchargement annulé par l'utilisateur.");
246+
} else {
247+
console.error("Erreur pendant le téléchargement :", err);
248+
}
249+
} finally {
250+
setIsDownloadLoading(false);
251+
setDownloadProgress(0);
252+
setDownloadPhase("idle");
253+
abortControllerRef.current = null;
254+
}
218255

219-
setIsDownloadLoading(false);
220256
downloadModal.close();
221257
return;
222258
}
223259

260+
// Méthode "file"
224261
triggerFileDownload(
225262
buildUrlFileContent(selectedProduits),
226263
"Liens_de_telechargement.txt"
@@ -233,8 +270,20 @@ const DownloadModal = () => {
233270
downloadModal.close();
234271
};
235272

236-
/** Remet le formulaire à son état initial et ferme le modal. */
237-
const handleReset = () => {
273+
/**
274+
* Tente de fermer le modal.
275+
* Si un téléchargement est en cours, demande confirmation avant d'annuler.
276+
*/
277+
const handleClose = () => {
278+
if (isDownloadLoading) {
279+
const confirmed = window.confirm(
280+
"Un téléchargement est en cours. Voulez-vous vraiment l'annuler ?"
281+
);
282+
if (!confirmed) return;
283+
284+
abortControllerRef.current?.abort();
285+
}
286+
238287
setSelectValue("");
239288
setSelectError(false);
240289
setDownloadMethod("all");
@@ -254,11 +303,21 @@ const DownloadModal = () => {
254303
const isSubmitDisabled = isMetadata && !selectValue;
255304

256305
return (
257-
<downloadModal.Component title="Télécharger" iconId="fr-icon-download-fill">
306+
<downloadModal.Component
307+
title="Télécharger"
308+
iconId="fr-icon-download-fill"
309+
concealingBackdrop={false}
310+
buttons={[]}
311+
>
258312
{isDownloadLoading ? (
259-
/* --- Barre de progression --- */
313+
/* --- Barre de progression avec phase --- */
260314
<div className="download-progress-container">
261-
<p>Téléchargement et compression en cours...</p>
315+
<p>{PHASE_LABELS[downloadPhase]}</p>
316+
{downloadPhase === "downloading" && (
317+
<p className="progress-file-count">
318+
{Math.round((downloadProgress / 99) * produitCount)}/{produitCount} fichiers
319+
</p>
320+
)}
262321
<div className="progress-bar-wrapper">
263322
<div className="progress-bar">
264323
<div
@@ -272,6 +331,11 @@ const DownloadModal = () => {
272331
</span>
273332
</div>
274333
</div>
334+
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 16 }}>
335+
<Button priority="secondary" type="button" onClick={handleClose}>
336+
Annuler le téléchargement
337+
</Button>
338+
</div>
275339
</div>
276340
) : (
277341
/* --- Formulaire principal --- */
@@ -328,8 +392,9 @@ const DownloadModal = () => {
328392
options={[
329393
{
330394
label: "Téléchargement automatique",
331-
hintText:
332-
"Lancer le téléchargement automatiquement de l'ensemble des données",
395+
hintText: isMetadata
396+
? "Télécharger un ZIP avec un sous-dossier par produit (données + métadonnées)"
397+
: "Lancer le téléchargement automatique de l'ensemble des données",
333398
nativeInputProps: {
334399
value: "all",
335400
defaultChecked: true,
@@ -339,8 +404,9 @@ const DownloadModal = () => {
339404
},
340405
{
341406
label: "Liens de téléchargement",
342-
hintText:
343-
"Télécharger la liste des liens de téléchargement associés aux données",
407+
hintText: isMetadata
408+
? "Télécharger la liste des liens et un fichier metadonnees.json indexé par produit"
409+
: "Télécharger la liste des liens de téléchargement associés aux données",
344410
nativeInputProps: {
345411
value: "file",
346412
onChange: (e) =>
@@ -367,24 +433,17 @@ const DownloadModal = () => {
367433
className="download-modal-actions"
368434
style={{
369435
display: "flex",
370-
alignItems: "center",
371-
justifyContent: "space-between",
436+
justifyContent: "flex-end",
372437
gap: 12,
373438
}}
374439
>
375-
<div />
376-
<div style={{ display: "flex", gap: 12 }}>
377-
<Button
378-
priority="primary"
379-
type="submit"
380-
disabled={isSubmitDisabled}
381-
>
382-
Télécharger
383-
</Button>
384-
<Button priority="secondary" type="button" onClick={handleReset}>
385-
Annuler
386-
</Button>
387-
</div>
440+
<Button
441+
priority="primary"
442+
type="submit"
443+
disabled={isSubmitDisabled}
444+
>
445+
Télécharger
446+
</Button>
388447
</div>
389448
</form>
390449
)}

0 commit comments

Comments
 (0)