web-dev-qa-db-fra.com

Comment télécharger un fichier en utilisant un fichier Java REST service et flux de données

J'ai 3 machines:

  1. serveur où se trouve le fichier
  2. serveur sur lequel le service REST est en cours d'exécution (Jersey) 
  3. client (navigateur) avec accès au 2e serveur mais pas d'accès au 1er serveur

Comment puis-je directement (sans enregistrer le fichier sur le 2ème serveur) télécharger le fichier du 1er serveur sur la machine du client? 
À partir du 2e serveur, je peux obtenir un ByteArrayOutputStream pour obtenir le fichier du 1er serveur, puis-je transmettre ce flux au client en utilisant le service REST?

Cela fonctionnera de cette façon?

En gros, je veux permettre au client de télécharger un fichier du 1er serveur à l’aide du service REST sur le 2e serveur (car il n’ya pas d’accès direct du client au 1er serveur) en utilisant uniquement des flux de données données touchant le système de fichiers du 2e serveur).

Ce que j'essaie maintenant avec la bibliothèque EasyStream:

       final FTDClient client = FTDClient.getInstance();

        try {
            final InputStreamFromOutputStream<String> isOs = new InputStreamFromOutputStream<String>() {
                @Override
                public String produce(final OutputStream dataSink) throws Exception {
                    return client.downloadFile2(location, Integer.valueOf(spaceId), URLDecoder.decode(filePath, "UTF-8"), dataSink);
                }
            };
            try {
                String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);

                StreamingOutput output = new StreamingOutput() {
                    @Override
                    public void write(OutputStream outputStream) throws IOException, WebApplicationException {
                        int length;
                        byte[] buffer = new byte[1024];
                        while ((length = isOs.read(buffer)) != -1){
                            outputStream.write(buffer, 0, length);
                        }
                        outputStream.flush();

                    }
                };
                return Response.ok(output, MediaType.APPLICATION_OCTET_STREAM)
                        .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"" )
                        .build();

UPDATE2

Donc, mon code maintenant avec le MessageBodyWriter personnalisé est simple:

ByteArrayOutputStream baos = new ByteArrayOutputStream (2048); Client.downloadFile (emplacement, spaceId, filePath, baos); Return Response.ok (baos) .build ();

Mais j'obtiens la même erreur de tas lorsque j'essaie avec des fichiers volumineux.

UPDATE3 Enfin réussi à le faire fonctionner! StreamingOutput a fait l'affaire.

Merci @peeskillet! Merci beaucoup !

25
Alex

"Comment puis-je directement (sans enregistrer le fichier sur le 2ème serveur) télécharger le fichier du 1er serveur sur la machine du client?"

Il suffit d’utiliser l’API Client et d’obtenir la InputStream à partir de la réponse.

Client client = ClientBuilder.newClient();
String url = "...";
final InputStream responseStream = client.target(url).request().get(InputStream.class);

Il existe deux types d’obtention de la variable InputStream. Vous pouvez aussi utiliser 

Response response = client.target(url).request().get();
InputStream is = (InputStream)response.getEntity();

Lequel est le plus efficace? Je ne suis pas sûr, mais les InputStream retournés sont des classes différentes, vous pouvez donc vous renseigner à ce sujet si vous le souhaitez.

À partir du 2e serveur, je peux obtenir un ByteArrayOutputStream pour obtenir le fichier du 1er serveur. Puis-je transmettre ce flux au client en utilisant le service REST?

Donc, la plupart des réponses que vous verrez dans le lien fourni par @GradyGCooper semblent favoriser l'utilisation de StreamingOutput. Un exemple d'implémentation pourrait être quelque chose comme

final InputStream responseStream = client.target(url).request().get(InputStream.class);
System.out.println(responseStream.getClass());
StreamingOutput output = new StreamingOutput() {
    @Override
    public void write(OutputStream out) throws IOException, WebApplicationException {  
        int length;
        byte[] buffer = new byte[1024];
        while((length = responseStream.read(buffer)) != -1) {
            out.write(buffer, 0, length);
        }
        out.flush();
        responseStream.close();
    }   
};
return Response.ok(output).header(
        "Content-Disposition", "attachment, filename=\"...\"").build();

Mais si nous examinons le code source de StreamingOutputProvider , vous verrez dans la variable writeTo qu’il écrit simplement les données d’un flux à l’autre. Donc, avec notre implémentation ci-dessus, nous devons écrire deux fois. 

Comment pouvons-nous obtenir une seule écriture? Retour simple la InputStream en tant que Response

final InputStream responseStream = client.target(url).request().get(InputStream.class);
return Response.ok(responseStream).header(
        "Content-Disposition", "attachment, filename=\"...\"").build();

Si nous examinons le code source pour InputStreamProvider , il délègue simplement des délégués à ReadWriter.writeTo(in, out) , qui fait simplement ce que nous avons fait ci-dessus dans l’implémentation StreamingOutput

 public static void writeTo(InputStream in, OutputStream out) throws IOException {
    int read;
    final byte[] data = new byte[BUFFER_SIZE];
    while ((read = in.read(data)) != -1) {
        out.write(data, 0, read);
    }
}

(À part:} _

  • Les objets Client sont des ressources coûteuses. Vous voudrez peut-être réutiliser la même Client pour la demande. Vous pouvez extraire une WebTarget du client pour chaque demande.

    WebTarget target = client.target(url);
    InputStream is = target.request().get(InputStream.class);
    

    Je pense que la WebTarget peut même être partagée. Je ne trouve rien dans Documentation Jersey 2.x (uniquement parce que c’est un document plus volumineux et que je suis trop paresseux pour le parcourir maintenant :-), mais dans le Jersey 1. x documentation , il est indiqué que les Client et WebResource (ce qui équivaut à WebTarget dans 2.x) peuvent être partagés entre les threads. Donc, je suppose que Jersey 2.x serait le même. mais vous voudrez peut-être confirmer par vous-même.

  • Vous n'êtes pas obligé d'utiliser l'API Client. Un téléchargement peut être facilement réalisé avec les API de package Java.net. Mais comme vous utilisez déjà Jersey, utiliser ses API ne fait pas de mal

  • Ce qui précède suppose que Jersey 2.x. Pour Jersey 1.x, une simple recherche sur Google devrait vous donner un tas de résultats pour travailler avec l'API (ou la documentation que j'ai liée à ci-dessus)


METTRE À JOUR

Je suis un dufus. Alors que le PO et moi-même examinons les moyens de transformer une ByteArrayOutputStream en une InputStream, j’ai raté la solution la plus simple, qui consiste simplement à écrire une MessageBodyWriter pour la ByteArrayOutputStream

import Java.io.ByteArrayOutputStream;
import Java.io.IOException;
import Java.io.OutputStream;
import Java.lang.annotation.Annotation;
import Java.lang.reflect.Type;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;

@Provider
public class OutputStreamWriter implements MessageBodyWriter<ByteArrayOutputStream> {

    @Override
    public boolean isWriteable(Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        return ByteArrayOutputStream.class == type;
    }

    @Override
    public long getSize(ByteArrayOutputStream t, Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        return -1;
    }

    @Override
    public void writeTo(ByteArrayOutputStream t, Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {
        t.writeTo(entityStream);
    }
}

Ensuite, nous pouvons simplement renvoyer le ByteArrayOutputStream dans la réponse

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

D'OH!

MISE À JOUR 2

Voici les tests que j'ai utilisés (

Classe de ressources

@Path("test")
public class TestResource {

    final String path = "some_150_mb_file";

    @GET
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Response doTest() throws Exception {
        InputStream is = new FileInputStream(path);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int len;
        byte[] buffer = new byte[4096];
        while ((len = is.read(buffer, 0, buffer.length)) != -1) {
            baos.write(buffer, 0, len);
        }
        System.out.println("Server size: " + baos.size());
        return Response.ok(baos).build();
    }
}

Test client

public class Main {
    public static void main(String[] args) throws Exception {
        Client client = ClientBuilder.newClient();
        String url = "http://localhost:8080/api/test";
        Response response = client.target(url).request().get();
        String location = "some_location";
        FileOutputStream out = new FileOutputStream(location);
        InputStream is = (InputStream)response.getEntity();
        int len = 0;
        byte[] buffer = new byte[4096];
        while((len = is.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        out.flush();
        out.close();
        is.close();
    }
}

MISE À JOUR 3

La solution finale pour ce cas d'utilisation particulier consistait donc à ce que l'OP passe simplement la méthode OutputStream à partir de la méthode StreamingOutput 's write. Semble l’API tierce, requiert une OutputStream en tant qu’argument.

StreamingOutput output = new StreamingOutput() {
    @Override
    public void write(OutputStream out) {
        thirdPartyApi.downloadFile(.., .., .., out);
    }
}
return Response.ok(output).build();

Pas tout à fait sûr, mais il semble que la lecture/écriture au sein de la méthode de ressource, en utilisant ByteArrayOutputStream`, ait réalisé quelque chose en mémoire.

Le fait que la méthode downloadFile accepte une OutputStream a pour but d'écrire le résultat directement dans la variable OutputStream fournie. Par exemple, un FileOutputStream, si vous l'écriviez dans un fichier, alors que le téléchargement arrivait, il serait directement transmis au fichier. 

Cela ne signifie pas que nous gardions une référence à OutputStream, comme vous essayiez de le faire avec baos, qui est le lieu où la réalisation de la mémoire entre en jeu.

Donc, avec la façon dont cela fonctionne, nous écrivons directement dans le flux de réponses fourni pour nous. La méthode write n'est en fait pas appelée jusqu'à la méthode writeTo (dans la MessageBodyWriter), où la OutputStream lui est transmise.

Vous pouvez obtenir une meilleure image en regardant la MessageBodyWriter que j'ai écrite. En gros, dans la méthode writeTo, remplacez la ByteArrayOutputStream par StreamingOutput, puis dans la méthode, appelez streamingOutput.write(entityStream). Vous pouvez voir le lien que j'ai fourni dans la partie précédente de la réponse, où je fais un lien vers StreamingOutputProvider. C'est exactement ce qui se passe

29
Paul Samsotha

Reportez ceci:

@RequestMapping(value="download", method=RequestMethod.GET)
public void getDownload(HttpServletResponse response) {

// Get your file stream from wherever.
InputStream myStream = someClass.returnFile();

// Set the content type and attachment header.
response.addHeader("Content-disposition", "attachment;filename=myfilename.txt");
response.setContentType("txt/plain");

// Copy the stream to the response's output stream.
IOUtils.copy(myStream, response.getOutputStream());
response.flushBuffer();
}

Détails sur: https://twilblog.github.io/Java/spring/rest/file/stream/2015/08/14/return-a-file-stream-from-spring-rest.html

1
AVINASH SHRIMALI

Voir exemple ici: Flux binaires d'entrée et de sortie utilisant JERSEY?

Le pseudo-code ressemblerait à ceci (il y a quelques autres options similaires dans l'article mentionné ci-dessus):

@Path("file/")
@GET
@Produces({"application/pdf"})
public StreamingOutput getFileContent() throws Exception {
     public void write(OutputStream output) throws IOException, WebApplicationException {
        try {
          //
          // 1. Get Stream to file from first server
          //
          while(<read stream from first server>) {
              output.write(<bytes read from first server>)
          }
        } catch (Exception e) {
            throw new WebApplicationException(e);
        } finally {
              // close input stream
        }
    }
}
0
Grady G Cooper