Quelle est la bonne façon d'envoyer un fichier du service Web REST au client?

103

Je viens de commencer à développer des services REST, mais je suis tombé sur une situation difficile: envoyer des fichiers de mon service REST à mon client. Jusqu'à présent, j'ai compris comment envoyer des types de données simples (chaînes, entiers, etc.), mais l'envoi d'un fichier est une autre affaire car il y a tellement de formats de fichiers que je ne sais même pas par où commencer. Mon service REST est fait sur Java et j'utilise Jersey, j'envoie toutes les données au format JSON.

J'ai lu sur l'encodage base64, certaines personnes disent que c'est une bonne technique, d'autres disent que ce n'est pas à cause de problèmes de taille de fichier. Quelle est la bonne manière? Voici à quoi ressemble une classe de ressources simple dans mon projet:

import java.sql.SQLException;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.UriInfo;

import com.mx.ipn.escom.testerRest.dao.TemaDao;
import com.mx.ipn.escom.testerRest.modelo.Tema;

@Path("/temas")
public class TemaResource {

    @GET
    @Produces({MediaType.APPLICATION_JSON})
    public List<Tema> getTemas() throws SQLException{

        TemaDao temaDao = new TemaDao();        
        List<Tema> temas=temaDao.getTemas();
        temaDao.terminarSesion();

        return temas;
    }
}

J'imagine que le code pour envoyer un fichier serait quelque chose comme:

import java.sql.SQLException;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/resourceFiles")
public class FileResource {

    @GET
    @Produces({application/x-octet-stream})
    public File getFiles() throws SQLException{ //I'm not really sure what kind of data type I should return

        // Code for encoding the file or just send it in a data stream, I really don't know what should be done here

        return file;
    }
}

Quel type d'annotations dois-je utiliser? J'ai vu certaines personnes recommander pour une @GETutilisation @Produces({application/x-octet-stream}), est-ce la bonne façon? Les fichiers que j'envoie sont spécifiques, le client n'a donc pas besoin de parcourir les fichiers. Quelqu'un peut-il me guider sur la manière dont je suis censé envoyer le fichier? Dois-je l'encoder en utilisant base64 pour l'envoyer en tant qu'objet JSON? ou l'encodage n'est pas nécessaire pour l'envoyer en tant qu'objet JSON? Merci pour toute aide que vous pourriez apporter.

Uriel
la source
Avez-vous un réel java.io.File(ou un chemin de fichier) sur votre serveur ou les données proviennent-elles d'une autre source, comme une base de données, un service Web, un appel de méthode renvoyant un InputStream?
Philipp Reichart

Réponses:

138

Je ne recommande pas d'encoder les données binaires en base64 et de les encapsuler dans JSON. Cela augmentera inutilement la taille de la réponse et ralentira les choses.

Diffusez simplement vos données de fichier en utilisant GET et en application/octect-streamutilisant l'une des méthodes d'usine de javax.ws.rs.core.Response(une partie de l'API JAX-RS, vous n'êtes donc pas enfermé dans Jersey):

@GET
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response getFile() {
  File file = ... // Initialize this to the File path you want to serve.
  return Response.ok(file, MediaType.APPLICATION_OCTET_STREAM)
      .header("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"" ) //optional
      .build();
}

Si vous n'avez pas d' Fileobjet réel , mais un InputStream, Response.ok(entity, mediaType)devrait être capable de gérer cela également.

Philipp Reichart
la source
merci, cela a très bien fonctionné, mais que faire si je veux utiliser toute une structure de dossiers? Je pensais à quelque chose comme ça Aussi puisque je vais recevoir divers fichiers sur le client, comment dois-je traiter la réponse d'entité de HttpResponse?
Uriel
4
Jetez un oeil à ZipOutputStreamlong avec un retour StreamingOutputde getFile(). De cette façon, vous obtenez un format multi-fichiers bien connu que la plupart des clients devraient facilement pouvoir lire. N'utilisez la compression que si cela a du sens pour vos données, c'est-à-dire pas pour les fichiers précompressés comme les JPEG. Du côté client, il y a ZipInputStreampour analyser la réponse.
Philipp Reichart
1
Cela pourrait aider: stackoverflow.com/questions/10100936/…
Basil Dsouza
Existe-t-il un moyen d'ajouter des métadonnées du fichier dans la réponse avec les données binaires du fichier?
abhig
Vous pouvez toujours ajouter d'autres en-têtes à la réponse. Si cela ne suffit pas, vous devrez l'encoder dans le flux d'octets, c'est-à-dire servir un format de conteneur contenant à la fois des métadonnées et le fichier souhaité.
Philipp Reichart
6

Si vous souhaitez renvoyer un fichier à télécharger, spécialement si vous souhaitez l'intégrer à certaines bibliothèques javascript de téléchargement / téléchargement de fichiers, le code ci-dessous devrait faire le travail:

@GET
@Path("/{key}")
public Response download(@PathParam("key") String key,
                         @Context HttpServletResponse response) throws IOException {
    try {
        //Get your File or Object from wherever you want...
            //you can use the key parameter to indentify your file
            //otherwise it can be removed
        //let's say your file is called "object"
        response.setContentLength((int) object.getContentLength());
        response.setHeader("Content-Disposition", "attachment; filename="
                + object.getName());
        ServletOutputStream outStream = response.getOutputStream();
        byte[] bbuf = new byte[(int) object.getContentLength() + 1024];
        DataInputStream in = new DataInputStream(
                object.getDataInputStream());
        int length = 0;
        while ((in != null) && ((length = in.read(bbuf)) != -1)) {
            outStream.write(bbuf, 0, length);
        }
        in.close();
        outStream.flush();
    } catch (S3ServiceException e) {
        e.printStackTrace();
    } catch (ServiceException e) {
        e.printStackTrace();
    }
    return Response.ok().build();
}
Jean 4d5
la source
3

Changez l'adresse de la machine d'hôte local en adresse IP à laquelle vous souhaitez que votre client se connecte pour appeler le service mentionné ci-dessous.

Client pour appeler le webservice REST:

package in.india.client.downloadfiledemo;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response.Status;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.multipart.BodyPart;
import com.sun.jersey.multipart.MultiPart;

public class DownloadFileClient {

    private static final String BASE_URI = "http://localhost:8080/DownloadFileDemo/services/downloadfile";

    public DownloadFileClient() {

        try {
            Client client = Client.create();
            WebResource objWebResource = client.resource(BASE_URI);
            ClientResponse response = objWebResource.path("/")
                    .type(MediaType.TEXT_HTML).get(ClientResponse.class);

            System.out.println("response : " + response);
            if (response.getStatus() == Status.OK.getStatusCode()
                    && response.hasEntity()) {
                MultiPart objMultiPart = response.getEntity(MultiPart.class);
                java.util.List<BodyPart> listBodyPart = objMultiPart
                        .getBodyParts();
                BodyPart filenameBodyPart = listBodyPart.get(0);
                BodyPart fileLengthBodyPart = listBodyPart.get(1);
                BodyPart fileBodyPart = listBodyPart.get(2);

                String filename = filenameBodyPart.getEntityAs(String.class);
                String fileLength = fileLengthBodyPart
                        .getEntityAs(String.class);
                File streamedFile = fileBodyPart.getEntityAs(File.class);

                BufferedInputStream objBufferedInputStream = new BufferedInputStream(
                        new FileInputStream(streamedFile));

                byte[] bytes = new byte[objBufferedInputStream.available()];

                objBufferedInputStream.read(bytes);

                String outFileName = "D:/"
                        + filename;
                System.out.println("File name is : " + filename
                        + " and length is : " + fileLength);
                FileOutputStream objFileOutputStream = new FileOutputStream(
                        outFileName);
                objFileOutputStream.write(bytes);
                objFileOutputStream.close();
                objBufferedInputStream.close();
                File receivedFile = new File(outFileName);
                System.out.print("Is the file size is same? :\t");
                System.out.println(Long.parseLong(fileLength) == receivedFile
                        .length());
            }
        } catch (UniformInterfaceException e) {
            e.printStackTrace();
        } catch (ClientHandlerException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String... args) {
        new DownloadFileClient();
    }
}

Service au client de réponse:

package in.india.service.downloadfiledemo;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.sun.jersey.multipart.MultiPart;

@Path("downloadfile")
@Produces("multipart/mixed")
public class DownloadFileResource {

    @GET
    public Response getFile() {

        java.io.File objFile = new java.io.File(
                "D:/DanGilbert_2004-480p-en.mp4");
        MultiPart objMultiPart = new MultiPart();
        objMultiPart.type(new MediaType("multipart", "mixed"));
        objMultiPart
                .bodyPart(objFile.getName(), new MediaType("text", "plain"));
        objMultiPart.bodyPart("" + objFile.length(), new MediaType("text",
                "plain"));
        objMultiPart.bodyPart(objFile, new MediaType("multipart", "mixed"));

        return Response.ok(objMultiPart).build();

    }
}

JAR nécessaire:

jersey-bundle-1.14.jar
jersey-multipart-1.14.jar
mimepull.jar

WEB.XML:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    id="WebApp_ID" version="2.5">
    <display-name>DownloadFileDemo</display-name>
    <servlet>
        <display-name>JAX-RS REST Servlet</display-name>
        <servlet-name>JAX-RS REST Servlet</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <init-param>
             <param-name>com.sun.jersey.config.property.packages</param-name> 
             <param-value>in.india.service.downloadfiledemo</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>JAX-RS REST Servlet</servlet-name>
        <url-pattern>/services/*</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
Shankar Saran Singh
la source
-2

Puisque vous utilisez JSON, je l'encoderais en Base64 avant de l'envoyer sur le fil.

Si les fichiers sont volumineux, essayez de regarder BSON ou un autre format mieux adapté aux transferts binaires.

Vous pouvez également compresser les fichiers, s'ils sont bien compressés, avant de les encoder en base64.

LarsK
la source
J'avais l'intention de les compresser avant de les envoyer pour toute la raison de la taille du fichier, mais si je l'encode en base64, que devrait @Producescontenir mon annotation?
Uriel
application / json selon les spécifications JSON, indépendamment de ce que vous y mettez. ( ietf.org/rfc/rfc4627.txt?number=4627 ) Gardez à l'esprit que le fichier encodé en base64 doit toujours être à l'intérieur des balises JSON
LarsK
3
Il n'y a aucun avantage à encoder des données binaires en base64, puis à les encapsuler dans JSON. Cela augmentera inutilement la taille de la réponse et ralentira les choses.
Philipp Reichart