¿Cuál es la forma correcta de enviar un archivo desde el servicio web REST al cliente?

103

Acabo de comenzar a desarrollar servicios REST, pero me he encontrado con una situación difícil: enviar archivos desde mi servicio REST a mi cliente. Hasta ahora he aprendido a enviar tipos de datos simples (cadenas, números enteros, etc.), pero enviar un archivo es un asunto diferente, ya que hay tantos formatos de archivo que no sé ni por dónde debería empezar. Mi servicio REST está hecho en Java y estoy usando Jersey, estoy enviando todos los datos usando el formato JSON.

He leído acerca de la codificación base64, algunas personas dicen que es una buena técnica, otras dicen que no lo es por problemas de tamaño de archivo. ¿Cuál es la manera correcta? Así es como se ve una clase de recurso simple en mi proyecto:

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;
    }
}

Supongo que el código para enviar un archivo sería algo como:

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;
    }
}

¿Qué tipo de anotaciones debo utilizar? He visto a algunas personas recomendar su @GETuso @Produces({application/x-octet-stream}), ¿es esa la forma correcta? Los archivos que estoy enviando son específicos, por lo que el cliente no necesita navegar por los archivos. ¿Alguien puede guiarme sobre cómo se supone que debo enviar el archivo? ¿Debo codificarlo usando base64 para enviarlo como un objeto JSON? ¿O la codificación no es necesaria para enviarlo como un objeto JSON? Gracias por cualquier ayuda que pueda brindar.

Uriel
fuente
¿Tiene una java.io.Fileruta real (o ruta de archivo) en su servidor o los datos provienen de alguna otra fuente, como una base de datos, servicio web, llamada de método que devuelve un InputStream?
Philipp Reichart

Respuestas:

138

No recomiendo codificar datos binarios en base64 y envolverlos en JSON. Simplemente aumentará innecesariamente el tamaño de la respuesta y ralentizará las cosas.

Simplemente entregue los datos de su archivo usando GET y application/octect-streamusando uno de los métodos de fábrica de javax.ws.rs.core.Response(parte de la API JAX-RS, para que no esté bloqueado en 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 no tiene un Fileobjeto real , pero un InputStream, Response.ok(entity, mediaType)debería poder manejarlo también.

Philipp Reichart
fuente
gracias, esto funcionó muy bien, pero ¿y si quiero consumir una estructura de carpetas completa? Estaba pensando en algo como esto. Además, dado que recibiré varios archivos en el cliente, ¿cómo debo tratar la respuesta de la entidad HttpResponse?
Uriel
4
Echar un vistazo a ZipOutputStreamlo largo con el retorno de una StreamingOutputde getFile(). De esta manera, obtiene un formato de múltiples archivos conocido que la mayoría de los clientes deberían poder leer fácilmente. Utilice la compresión solo si tiene sentido para sus datos, es decir, no para archivos precomprimidos como JPEG. En el lado del cliente, hay ZipInputStreamque analizar la respuesta.
Philipp Reichart
1
Esto podría ayudar: stackoverflow.com/questions/10100936/…
Basil Dsouza
¿Hay alguna forma de agregar metadatos del archivo en la respuesta junto con los datos binarios del archivo?
Abhig
Siempre puede agregar más encabezados a la respuesta. Si eso no es suficiente, tendrá que codificarlo en el flujo de octetos, es decir, entregar un formato contenedor que tenga tanto metadatos como el archivo que desea.
Philipp Reichart
6

Si desea devolver un archivo para que se descargue, especialmente si desea integrarlo con algunas bibliotecas de javascript de carga / descarga de archivos, entonces el siguiente código debería hacer el trabajo:

@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();
}
juan 4d5
fuente
3

Cambie la dirección de la máquina de localhost a la dirección IP con la que desea que su cliente se conecte para llamar al servicio mencionado a continuación.

Cliente para llamar al servicio web 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();
    }
}

Servicio al cliente de respuesta:

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 necesario:

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
fuente
-2

Como está utilizando JSON, lo codificaría en Base64 antes de enviarlo a través del cable.

Si los archivos son grandes, intente buscar BSON o algún otro formato que sea mejor con transferencias binarias.

También puede comprimir los archivos, si se comprimen bien, antes de codificarlos en base64.

LarsK
fuente
Estaba planeando comprimirlos antes de enviarlos por el motivo del tamaño del archivo completo, pero si lo codifico en base64, ¿qué debería @Producescontener mi anotación?
Uriel
application / json según la especificación JSON, independientemente de lo que ponga en él. ( ietf.org/rfc/rfc4627.txt?number=4627 ) Tenga en cuenta que el archivo codificado en base64 aún debe estar dentro de las etiquetas JSON
LarsK
3
No hay ningún beneficio en codificar datos binarios en base64 y luego envolverlos en JSON. Simplemente aumentará innecesariamente el tamaño de la respuesta y ralentizará las cosas.
Philipp Reichart