@@ -2,11 +2,11 @@ import { createModal } from "@codegouvfr/react-dsfr/Modal";
22import { useDalleStore } from "../../../hooks/store/useDalleStore" ;
33import "./styles/DownloadModal.css" ;
44import { Select } from "@codegouvfr/react-dsfr/Select" ;
5- import { useState , useEffect } from "react" ;
5+ import { useState , useEffect , useRef } from "react" ;
66import { RadioButtons } from "@codegouvfr/react-dsfr/RadioButtons" ;
77import Button from "@codegouvfr/react-dsfr/Button" ;
88import { formatBytes } from "../../../utils/formatters" ;
9- import { downloadZip , getFileSizes } from "../../../utils/download" ;
9+ import { downloadZip , getFileSizes , DownloadPhase } from "../../../utils/download" ;
1010import { 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 */
106116const 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