Télécharger plusieurs fichiers par une seule commande

Bonjour à toutes et à tous,

Toujours dans mon application, je cherche à récupérer plusieurs fichiers en une seule commande. Jai vu des solutions sur la toile disant qu'il suffit de faire un *.zip de tous les fichiers à envoyer et de récupérer ce fichier.

Ce qui m'ennuie dans cette solution est :

  • l'ajout d'un fichier *.zip peut faire déborder la mémoire LittleFS
  • il faut créer / supprimer ce fichier à chaque appel.

Je me demandais, si, à l'aide d'une fonction récursive, je ne pourrais pas y arriver. Voilà l'allure que ça aurait :

function chgtMultiple(n) {
  fetch('/quelFichier?val='+n);
  Si (**réponse** et n < 7)
    chgtMultiple(n+1);
}

Côté serveur :

server.on("/quelFichier", HTTP_GET, [](AsyncWebServerRequest *request){
  if (request-> hasParam("val")) {
    noFch = request-> getParam("val")->value.toInt;
    Sélection du fichier voulu : fchSelect;
    request->send(LittleFS, fchSelect, String(), true);
  }
});

Le problème est que je ne sais pas s'il est possible d'avoir une réponse dans ma fonction chgtMultiple.

Une idée ?

Cordialement.

Pierre.

si le nom des fichiers est séquentiel genre toto1, toto2, ... toto7 vous pourriez simplement faire une boucle for, pas besoin de récursion

async function fetchTotoFiles() {
  for (let i = 1; i <= 7; i++) {
    const fileName = `toto$ {i}`; // on fabrique le nom toto1, toto2, toto3...
    try {
      const response = await fetch(fileName);
      if (!response.ok) {
        console.error(`Erreur lors de la récupération de $ {fileName}: $ {response.status}`);
        continue;
      }
      const data = await response.text(); // ou .json() si le contenu est JSON
      console.log(`Contenu de $ {fileName}: `, data);
    } catch (error) {
      console.error(`Erreur réseau pour $ {fileName}: `, error);
    }
  }
}

si vous ne connaissez pas vraiment le nom des fichiers côté client, alors oui, vous pouvez passer un indice en paramètre et vous laissez le serveur trouver le bon fichier. Mais ça peut être quand même une boucle for plutôt qu'un appel récursif.

Voilà ce que j'ai écrit :

        async function chgtHistos() {
          for (let i = 0; i < 7; i++) {
            try {
              const response = await fetch('/quelFichier?val='+i);
              if (!response.ok) {
                console.error(`Erreur lors de la récupération de $ {fileName}: $ {response.status}`);
                continue;
              }
              const data = await response.text(); // ou .json() si le contenu est JSON
              console.log(`Contenu de $ {fileName}: `, data);
            } catch (error) {
              console.error(`Erreur réseau pour $ {fileName}: `, error);
            }
          }
        }

côté serveur :

  server.on("/quelFichier", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request->hasParam("val")) {
      File root = LittleFS.open("/");
      File file = root.openNextFile();
      noFch = request->getParam("val")->value().toInt(); 
      bool vu = false;
      while(file) {
        if (strncmp(file.path(), nomJour[noFch], 4) == 0) {
          Serial.println(file.path());
          vu = true;
          request->send(LittleFS, file.path(), String(), true); // Envoie le fichier en téléchargement
        }
        if (vu == true)
          break;
        else
          file = root.openNextFile();
      }
    }
  }); 
}

Quand j’appuie sur le bouton qui déclenche la fonction chgtHistos(), je voie apparaître le nom du premier sélectionné dans le moniteur série et le contenu de ce fichier dans la console du navigateur (Firefox). Et puis c'est tout !

J'aurais voulu avoir ce fichier en téléchargement puis les autres ...

Cordialement.

Pierre.

cf ce post pour préparer un contenu en téléchargement

Il me semble que la bibliothèque ESPAsyncWebServer propose un beginResponse_P() pour passer un flux, faudrait creuser de ce côté (ou alors lires les octets du fichiers et les envoyés comme dans ma génération dynamique)

Ton soucis je pense, c'est que si tu utilise request->send, c'est que tu envois la réponse.
Je ne retrouve pas la librairie que tu utilises, pour regarder les sources et confirmer mon hypothése

Edit, j'ai bien trouvé les sources ESPAsyncWebServer avec la fonction que donne @J-M-L , mais je ne trouve pas la définition de la fonction send que tu utilises.

c'est dans la doc d'origine

Respond with content coming from a File

//Send index.htm with default content type
request->send(SPIFFS, "/index.htm"); 

//Send index.htm as text 
request->send(SPIFFS, "/index.htm", "text/plain");

//Download index.htm 
request->send(SPIFFS, "/index.htm", String(), true);

il utilise la troisième version.

Oui, mais je parles en vrai dans les sources, je ne vois qu'une déclaration pour send, donc j'avais un doute sur le fait que ce soit bien cette librairie qu'il utilise.

J'ai refait un essai ce matin. Il faut noter que l'ordre des fichiers à télécharger est donné par :

const char *nomJour[] = {"/Sun", "/Mon", "/Tue", "/Wed", "/Thu", "/Fri", "/Sat"};

Aujourd'hui, seuls les fichiers de Sat, Sun et Mon sont disponibles. La procédure cherche bien à trouver les 7 fichiers. Elle trouve les trois disponibles et donne une erreur pour les quatre autres. Mais apparemment, il y a un délai d'environ 36 secondes lorsque qu'un fichier n'est pas disponibles. C'est pour cà qu'hier, n'ayant pas attendu ce temps, je pensais que la boucle ne se déroulait pas.

Bon, on est peut être en bonne voie.

Donc deux problèmes potentiels :

  • Les fichiers sont bien reçus dans la console mais ne sont pas téléchargés.
  • les délais de 36 secondes lorsqu'un fichier n’existe pas.
XHRGET
http://192.168.0.1/quelFichier?val=0
[HTTP/1.1 200 OK 420ms]

Contenu de $ {fileName}:  
26/01/2025 09:32:30,2.3,212.4,0.2
…
192.168.0.1:54:23
XHRGET
http://192.168.0.1/quelFichier?val=1
[HTTP/1.1 200 OK 439ms]

Contenu de $ {fileName}:  27/01/2025 09:40:30,2.4,218.0,0.2
27/01/2025 09:41:00,-4.9,216.6,0.7
...
192.168.0.1:54:23
XHRGET
http://192.168.0.1/quelFichier?val=2
NS_ERROR_NET_RESET

Erreur réseau pour $ {fileName}:  TypeError: NetworkError when attempting to fetch resource. 192.168.0.1:56:23
XHRGET
http://192.168.0.1/quelFichier?val=3
NS_ERROR_NET_RESET

Erreur réseau pour $ {fileName}:  TypeError: NetworkError when attempting to fetch resource. 192.168.0.1:56:23
XHRGET
http://192.168.0.1/quelFichier?val=4
NS_ERROR_NET_RESET

Erreur réseau pour $ {fileName}:  TypeError: NetworkError when attempting to fetch resource. 192.168.0.1:56:23
XHRGET
http://192.168.0.1/quelFichier?val=5
NS_ERROR_NET_RESET

Erreur réseau pour $ {fileName}:  TypeError: NetworkError when attempting to fetch resource. 192.168.0.1:56:23
XHRGET
http://192.168.0.1/quelFichier?val=6
[HTTP/1.1 200 OK 96ms]

Contenu de $ {fileName}:  
25/01/2025 17:35:312.4,214.8,1.2

25/01/…

NOTA : la procédure :

  server.on("/download-data", HTTP_GET, [](AsyncWebServerRequest *request){ // Envoi du fichier d'historique pour téléchargement
    request->send(LittleFS, fchHisto, String(), true);
  });

me télécharge bien une fichier lorsque je n'en demande qu'un.

Cordialement.

Pierre.

Désolé, je pense que je me suis perdu :frowning:
si on prend le plus facile l'instruction ne peut renvoyer qu'un seul fichier à chaque requête HTTP sur /download-data

Si j'ai bien compris ton code server, je crois que tu ne fais rien si aucun fichier n'est trouvé ?
Quand je dis tu ne fais rien, c'est à dire que tu ne renvois rien au client par exemple en faisant un request->send(404);

En grattouillant sur la toile (via perplexity), j'ai trouvé le code qui se cache derrière cet appel :

        <a href="download-data"><button class="button button-data">Télécharger les données</button></a>

J'ai alors utilisé ce code dans ma fonction de téléchargement multiple et ... ça fonctionne !

        async function chgtHistos() {
          for (let i = 0; i < 7; i++) {
            fetch('/quelFichier?val='+i)
              .then(response => response.blob())
              .then(blob => {
                  const url = window.URL.createObjectURL(blob);
                  const a = document.createElement('a');
                  a.style.display = 'none';
                  a.href = url;
                  a.download = 'fichier_telecharge.txt';
                  document.body.appendChild(a);
                  a.click();
                  window.URL.revokeObjectURL(url);
              })
              .catch(error => console.error('Erreur lors du téléchargement:', error));
          }
        }

A un bémol près, le nom du fichier téléchargé est en dur : 'fichier_telecharge.txt';.

J'ai vérifié dans mes téléchargements, le contenu de chaque fichier téléchargé est bien l'image de ce qui dans mon ESP32.

Comment faire pour que le nom soit celui du fichier téléchargé.

Cordialement.

Pierre.

il faudrait que ta ressource /quelFichier renvoi le contenu du fichier et son nom, dans un json par exemple ou un format avec un retour chariot séparant le nom du fichier des données.
Si c'est bien uniquement du texte que tu manipules.
Si c'est du binaire, il faut encoder/décoder les données

Si je comprends bien le contenu de cette fonction, il n'est pas besoin que je fasse ce qu suit :

    if (LittleFS.exists(fchHisto)) {
      request->send(LittleFS, fchHisto, String(), true);
    } else {
      request->send(404, "text/plain", "File not found");
    }

un simple :

      request->send(LittleFS, fchHisto, String(), true);

suffit ?

Cordialement.

Pierre.

J'ai bien un retour du contenu puisque je peux télécharger le fichier. Le nom doit bien apparaître dans un des paramètres de retour ?

Cordialement.

Pierre.

en théorie oui, je n'ai jamais essayé

ben oui, c'est ce que vous dites

                  a.download = 'fichier_telecharge.txt';

vous devriez pouvoir récupérer le nom du fichier à partir de l'en-tête Content-Disposition de la réponse HTTP si celui-ci est bien configuré par le serveur. Je ne suis pas sûr...

un truc comme cela

async function chgtHistos() {
  for (let i = 0; i < 7; i++) {
    fetch('/quelFichier?val=' + i)
      .then(response => {
        const contentDisposition = response.headers.get('Content-Disposition');
        const defaultFileName = `fichier_${i}.txt`; // Nom par défaut si le serveur n'en fournit pas
        let fileName = defaultFileName;

        if (contentDisposition && contentDisposition.includes('filename=')) {
          const match = contentDisposition.match(/filename="?([^"]+)"?/);
          if (match) {
            fileName = match[1];
          }
        }

        return response.blob().then(blob => ({ blob, fileName }));
      })
      .then(({ blob, fileName }) => {
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.style.display = 'none';
        a.href = url;
        a.download = fileName;
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
      })
      .catch(error => console.error('Erreur lors du téléchargement:', error));
  }
}

Oui surement, dans le code comme l'indique @J-M-L, ils mettent le nom du fichier dans le header content-disposition

@J-M-L merci pour le .h, comme je n'aime pas mettre du code dans le .h, je pense jamais à y regarder.

Bravo pour le truc. Ça fonctionne parfaitement. Je vous en remercie beaucoup.

Pour autant, afin de ne pas recopier bêtement du code, j’aimerais comprendre un peu plus ce que j'écris. Avez-vous un lien où je peux trouver ces notions de transfert et de code associés.

Cordialement.

Pierre.

bravo - c'est comme cela qu'il faut faire :slight_smile:

Dans votre fonction, à chaque itération de la boucle for, la méthode fetch('/quelFichier?val=' + i) envoie une requête GET au serveur, avec un paramètre val égal à l'indice i.

Le code sur le serveur utilise cet indice pour trouver le bon fichier et bâtir une réponse HTTP sous forme de demande de téléchargement de fichier.

L'en-tête Content-Disposition dans cette réponse HTTP détermine la manière dont le contenu doit être traité par le client.

➜ Si la valeur est inline, le contenu sera affiché directement dans le navigateur, à condition que le type de fichier soit pris en charge.

➜ Si la valeur est attachment, cela incite le navigateur à télécharger le fichier, avec une boîte de dialogue permettant de le sauvegarder localement.

Dans ce cas, le serveur peut spécifier un nom pour le fichier via le paramètre filename.

Par exemple,

"Content-Disposition: attachment; filename=\"document.pdf\"" 

invitera à télécharger le fichier sous le nom document.pdf.

Donc on utilise cela dans un premier bloc then pour qu'une fois la réponse du serveur reçue on récupère l'en-tête Content-Disposition de la réponse. (il y a un bout de code qui dit que si on ne peut pas l'extraire alors on prend un nom par défaut en fonction de l'indice).

Comme vous ne voulez pas avoir l'interaction utilisateur pour sauver le fichier, dans le deuxième bloc then, la réponse est transformée en un objet Blob, représentant le contenu du fichier et un lien temporaire est créé avec l'URL du Blob. Ce lien est automatiquement cliqué pour déclencher le téléchargement du fichier et puis l'URL temporaire est révoquée pour libérer les ressources.


vous pouvez en lire plus sur Content-Disposition - Expert Guide to HTTP headers

si c'est comprendre la partie serveur, vous pourriez regarder l'exemple que j'ai donné plus haut où je bâtis une réponse sous forme de fichier à télécharger