Téléchargement d'un fichier à partir des contrôleurs Spring

387

J'ai une exigence où je dois télécharger un PDF à partir du site Web. Le PDF doit être généré dans le code, ce que je pensais être une combinaison de freemarker et d'un cadre de génération de PDF comme iText. Une meilleure façon?

Cependant, mon principal problème est de savoir comment autoriser l'utilisateur à télécharger un fichier via un contrôleur Spring?

MilindaD
la source
2
Il convient de mentionner que Spring Framework a beaucoup changé depuis 2011, vous pouvez donc le faire également de manière réactive - voici un exemple
Krzysztof Skrzynecki

Réponses:

397
@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
public void getFile(
    @PathVariable("file_name") String fileName, 
    HttpServletResponse response) {
    try {
      // get your file as InputStream
      InputStream is = ...;
      // copy it to response's OutputStream
      org.apache.commons.io.IOUtils.copy(is, response.getOutputStream());
      response.flushBuffer();
    } catch (IOException ex) {
      log.info("Error writing file to output stream. Filename was '{}'", fileName, ex);
      throw new RuntimeException("IOError writing file to output stream");
    }

}

D'une manière générale, quand vous l'avez response.getOutputStream(), vous pouvez y écrire n'importe quoi. Vous pouvez passer ce flux de sortie comme un endroit pour mettre le PDF généré sur votre générateur. De plus, si vous savez quel type de fichier vous envoyez, vous pouvez définir

response.setContentType("application/pdf");
Infeligo
la source
4
C'est à peu près ce que j'allais dire, mais vous devriez probablement également définir l'en-tête du type de réponse sur quelque chose de approprié pour le fichier.
GaryF
2
Oui, je viens de modifier le post. J'ai généré différents types de fichiers, je l'ai donc laissé au navigateur pour déterminer le type de contenu du fichier par son extension.
Infeligo
J'ai oublié le flushBuffer, grâce à votre post, j'ai vu pourquoi le mien ne fonctionnait pas :-)
Jan Vladimir Mostert
35
Une raison particulière d'utiliser Apache au IOUtilslieu de Spring FileCopyUtils?
Powerlord
3
Voici une meilleure solution: stackoverflow.com/questions/16652760/…
Dmytro Plekhotkin
290

J'ai pu diffuser cette ligne en utilisant le support intégré de Spring avec son ResourceHttpMessageConverter. Cela définira la longueur et le type de contenu s'il peut déterminer le type MIME

@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(@PathVariable("file_name") String fileName) {
    return new FileSystemResource(myService.getFileFor(fileName)); 
}
Scott Carlson
la source
10
Cela marche. Mais le fichier (fichier .csv) est affiché dans le navigateur et n'est pas téléchargé - comment puis-je forcer le téléchargement du navigateur?
chzbrgla
41
Vous pouvez ajouter produit = MediaType.APPLICATION_OCTET_STREAM_VALUE à @RequestMapping pour forcer le téléchargement
David Kago
8
Vous devez également ajouter <bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" /> à la liste messageConverters (<mvc: annotation-driven> <mvc: message-converters>)
Sllouyssgort
4
Existe-t-il un moyen de définir l'en- Content-Dispositiontête de cette façon?
Ralph
8
Je n'en avais pas besoin, mais je pense que vous pourriez ajouter HttpResponse comme paramètre à la méthode, puis "response.setHeader (" Content-Disposition "," attachment; filename = somefile.pdf ");"
Scott Carlson
82

Vous devriez pouvoir écrire le fichier directement sur la réponse. Quelque chose comme

response.setContentType("application/pdf");      
response.setHeader("Content-Disposition", "attachment; filename=\"somefile.pdf\""); 

puis écrivez le fichier sous forme de flux binaire response.getOutputStream(). N'oubliez pas de faire response.flush()à la fin et cela devrait le faire.

homard1234
la source
8
n'est-ce pas la manière «printanière» de définir le type de contenu comme ceci? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Black
4
@Francis et si votre application télécharge différents types de fichiers? La réponse de Lobster1234 vous permet de définir dynamiquement la disposition du contenu.
Rose
2
c'est vrai @Rose, mais je pense qu'il serait préférable de définir différents points de terminaison par format
Black
3
Je suppose que non, car ce n'est pas évolutif. Nous soutenons actuellement une douzaine de types de ressources. Nous pourrions prendre en charge plus de types de fichiers en fonction de ce que les utilisateurs veulent télécharger dans ce cas, nous pourrions nous retrouver avec autant de points de terminaison faisant essentiellement la même chose. À mon humble avis, il ne doit y avoir qu'un seul point de fin de téléchargement et il gère une multitude de types de fichiers. @Francis
Rose
3
c'est absolument "évolutif", mais nous pouvons convenir d'être en désaccord sur le fait que ce soit la meilleure pratique
Black
74

Avec Spring 3.0, vous pouvez utiliser l' HttpEntityobjet de retour. Si vous l'utilisez, votre contrôleur n'a pas besoin d'un HttpServletResponseobjet et il est donc plus facile à tester. Sauf ceci, cette réponse est relative égale à celle d'Infeligo .

Si la valeur de retour de votre framework pdf est un tableau d'octets (lisez la deuxième partie de ma réponse pour d'autres valeurs de retour) :

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    byte[] documentBody = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(documentBody.length);

    return new HttpEntity<byte[]>(documentBody, header);
}

Si le type de retour de votre PDF Framework ( documentBbody) n'est pas déjà un tableau d'octets (et également non ByteArrayInputStream), il serait sage de NE PAS en faire un tableau d'octets en premier. Au lieu de cela, il vaut mieux utiliser:

exemple avec FileSystemResource:

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    File document = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(document.length());

    return new HttpEntity<byte[]>(new FileSystemResource(document),
                                  header);
}
Ralph
la source
11
-1 cela chargera inutilement l'intégralité du fichier en mémoire peut facilement entraîner OutOfMemoryErrors.
Faisal Feroz
1
@FaisalFeroz: oui c'est vrai, mais le document fichier est quand même créé en mémoire (voir la question: "Le PDF doit être généré dans le code"). Quoi qu'il en soit - quelle est votre solution pour surmonter ce problème?
Ralph
1
Vous pouvez également utiliser ResponseEntity qui est un super de HttpEntity qui vous permet de spécifier le code d'état http de réponse. Exemple:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa
@Amr Mostafa: ResponseEntityest une sous-classe de HttpEntity(mais je comprends) d'autre part, 201 CREATED n'est pas ce que j'utiliserais lorsque je retournerais juste une vue des données. (voir w3.org/Protocols/rfc2616/rfc2616-sec10.html pour 201 CREATED )
Ralph
1
Y a-t-il une raison pour laquelle vous remplacez les espaces par un trait de soulignement dans le nom de fichier? Vous pouvez le mettre entre guillemets pour envoyer le nom réel.
Alexandru Severin
63

Si vous:

  • Je ne veux pas charger le fichier entier dans un byte[]avant de l'envoyer à la réponse;
  • Vous voulez / devez l'envoyer / le télécharger via InputStream;
  • Vous voulez avoir un contrôle total sur le type MIME et le nom du fichier envoyé;
  • Ayez d'autres @ControllerAdviceexceptions pour vous (ou non).

Le code ci-dessous est ce dont vous avez besoin:

@RequestMapping(value = "/stuff/{stuffId}", method = RequestMethod.GET)
public ResponseEntity<FileSystemResource> downloadStuff(@PathVariable int stuffId)
                                                                      throws IOException {
    String fullPath = stuffService.figureOutFileNameFor(stuffId);
    File file = new File(fullPath);
    long fileLength = file.length(); // this is ok, but see note below

    HttpHeaders respHeaders = new HttpHeaders();
    respHeaders.setContentType("application/pdf");
    respHeaders.setContentLength(fileLength);
    respHeaders.setContentDispositionFormData("attachment", "fileNameIwant.pdf");

    return new ResponseEntity<FileSystemResource>(
        new FileSystemResource(file), respHeaders, HttpStatus.OK
    );
}

À propos de la partie de la longueur du fichier : File#length()devrait être assez bonne dans le cas général, mais j'ai pensé que je ferais cette observation car elle peut être lente , auquel cas vous devriez l'avoir stockée précédemment (par exemple dans la base de données). Les cas peuvent être lents: si le fichier est volumineux, spécialement si le fichier est dans un système distant ou quelque chose de plus élaboré comme ça - une base de données, peut-être.



InputStreamResource

Si votre ressource n'est pas un fichier, par exemple si vous récupérez les données de la base de données, vous devez utiliser InputStreamResource. Exemple:

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
acdcjunior
la source
Vous déconseillez l'utilisation de la classe FileSystemResource?
Stéphane
En fait, je crois que c'est OK d'utiliser le FileSystemResourcelà - bas. Il est même conseillé si votre ressource est un fichier . Dans cet exemple, FileSystemResourcepeut être utilisé où InputStreamResourceest.
acdcjunior
À propos de la partie de calcul de la longueur du fichier: Si vous êtes inquiet, ne le soyez pas. File#length()devrait être suffisant dans le cas général. Je viens de le mentionner car il peut être lent , surtout si le fichier se trouve dans un système distant ou quelque chose de plus élaboré comme ça - une base de données, peut-être?. Mais ne vous inquiétez que si cela devient un problème (ou si vous avez des preuves tangibles, il le deviendra), pas avant. Le point principal est: vous faites un effort pour diffuser le fichier, si vous devez tout précharger avant, alors le streaming finit par ne faire aucune différence, hein?
acdcjunior
pourquoi le code ci-dessus ne fonctionne-t-il pas pour moi? Il télécharge un fichier de 0 octet. J'ai vérifié et vérifié que les convertisseurs ByteArray et ResourceMessage sont là. Suis-je en train de manquer quelque chose?
coding_idiot
Pourquoi vous inquiétez-vous des convertisseurs ByteArray et ResourceMessage?
acdcjunior
20

Ce code fonctionne très bien pour télécharger un fichier automatiquement à partir du contrôleur de printemps en cliquant sur un lien sur jsp.

@RequestMapping(value="/downloadLogFile")
public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
    try {
        String filePathToBeServed = //complete file name with path;
        File fileToDownload = new File(filePathToBeServed);
        InputStream inputStream = new FileInputStream(fileToDownload);
        response.setContentType("application/force-download");
        response.setHeader("Content-Disposition", "attachment; filename="+fileName+".txt"); 
        IOUtils.copy(inputStream, response.getOutputStream());
        response.flushBuffer();
        inputStream.close();
    } catch (Exception e){
        LOGGER.debug("Request could not be completed at this moment. Please try again.");
        e.printStackTrace();
    }

}
Sunil
la source
14

Le code ci-dessous a fonctionné pour moi pour générer et télécharger un fichier texte.

@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity<byte[]> getDownloadData() throws Exception {

    String regData = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
    byte[] output = regData.getBytes();

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("charset", "utf-8");
    responseHeaders.setContentType(MediaType.valueOf("text/html"));
    responseHeaders.setContentLength(output.length);
    responseHeaders.set("Content-disposition", "attachment; filename=filename.txt");

    return new ResponseEntity<byte[]>(output, responseHeaders, HttpStatus.OK);
}
Siva Kumar
la source
5

Ce à quoi je peux penser rapidement, c'est de générer le pdf et de le stocker dans webapp / Downloads / <RANDOM-FILENAME> .pdf à partir du code et d'envoyer un transfert vers ce fichier en utilisant HttpServletRequest

request.getRequestDispatcher("/downloads/<RANDOM-FILENAME>.pdf").forward(request, response);

ou si vous pouvez configurer votre résolveur de vue quelque chose comme,

  <bean id="pdfViewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass"
              value="org.springframework.web.servlet.view.JstlView" />
    <property name="order" value=”2″/>
    <property name="prefix" value="/downloads/" />
    <property name="suffix" value=".pdf" />
  </bean>

puis reviens

return "RANDOM-FILENAME";
kalyan
la source
1
Si j'ai besoin de deux résolveurs de vue, comment puis-je également retourner le nom du résolveur ou le choisir dans le contrôleur ??
azerafati
3

La solution suivante fonctionne pour moi

    @RequestMapping(value="/download")
    public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
        try {

            String fileName="archivo demo.pdf";
            String filePathToBeServed = "C:\\software\\Tomcat 7.0\\tmpFiles\\";
            File fileToDownload = new File(filePathToBeServed+fileName);

            InputStream inputStream = new FileInputStream(fileToDownload);
            response.setContentType("application/force-download");
            response.setHeader("Content-Disposition", "attachment; filename="+fileName); 
            IOUtils.copy(inputStream, response.getOutputStream());
            response.flushBuffer();
            inputStream.close();
        } catch (Exception exception){
            System.out.println(exception.getMessage());
        }

    }
Jorge Santos Neill
la source
2

quelque chose comme ci-dessous

@RequestMapping(value = "/download", method = RequestMethod.GET)
public void getFile(HttpServletResponse response) {
    try {
        DefaultResourceLoader loader = new DefaultResourceLoader();
        InputStream is = loader.getResource("classpath:META-INF/resources/Accepted.pdf").getInputStream();
        IOUtils.copy(is, response.getOutputStream());
        response.setHeader("Content-Disposition", "attachment; filename=Accepted.pdf");
        response.flushBuffer();
    } catch (IOException ex) {
        throw new RuntimeException("IOError writing file to output stream");
    }
}

Vous pouvez afficher le PDF ou télécharger des exemples ici

xxy
la source
1

Si cela aide quelqu'un. Vous pouvez faire ce que la réponse acceptée par Infeligo a suggéré mais il suffit de mettre ce bit supplémentaire dans le code pour un téléchargement forcé.

response.setContentType("application/force-download");
Sagar Nair
la source
0

Dans mon cas, je génère un fichier à la demande, il faut donc également générer une URL.

Pour moi, ça fonctionne comme ça:

@RequestMapping(value = "/files/{filename:.+}", method = RequestMethod.GET, produces = "text/csv")
@ResponseBody
public FileSystemResource getFile(@PathVariable String filename) {
    String path = dataProvider.getFullPath(filename);
    return new FileSystemResource(new File(path));
}

Très important est le type mime produceset aussi, ce nom du fichier fait partie du lien, vous devez donc l'utiliser @PathVariable.

Le code HTML ressemble à ça:

<a th:href="@{|/dbreport/files/${file_name}|}">Download</a>

${file_name}est généré par Thymeleaf dans le contrôleur et c'est-à-dire: result_20200225.csv, de sorte que le lien entier derrière l'URL est:example.com/aplication/dbreport/files/result_20200225.csv .

Après avoir cliqué sur le navigateur de lien me demande quoi faire avec le fichier - enregistrer ou ouvrir.

Tomasz Dzięcielewski
la source