Publicando um Arquivo e Dados Associados em um Serviço Web RESTful, preferencialmente como JSON

757

Provavelmente será uma pergunta estúpida, mas estou tendo uma daquelas noites. Em um aplicativo, estou desenvolvendo a API RESTful e queremos que o cliente envie dados como JSON. Parte desse aplicativo requer que o cliente faça upload de um arquivo (geralmente uma imagem), além de informações sobre a imagem.

Estou com dificuldade para rastrear como isso acontece em uma única solicitação. É possível Base64 os dados do arquivo em uma string JSON? Vou precisar executar 2 postagens no servidor? Não devo usar o JSON para isso?

Como uma observação lateral, estamos usando o Grails no back-end e esses serviços são acessados ​​por clientes móveis nativos (iPhone, Android, etc.), se isso faz alguma diferença.

Gregg
fonte
1
Então, qual é a melhor maneira de fazer isso?
James111
3
Envie os metadados na string de consulta da URL, em vez de JSON.
CCI

Respostas:

632

Fiz uma pergunta semelhante aqui:

Como faço para enviar um arquivo com metadados usando um serviço da web REST?

Você basicamente tem três opções:

  1. A Base64 codifica o arquivo, à custa de aumentar o tamanho dos dados em cerca de 33%, e adiciona uma sobrecarga de processamento no servidor e no cliente para codificação / decodificação.
  2. Envie o arquivo primeiro em um multipart/form-dataPOST e retorne um ID para o cliente. O cliente envia os metadados com o ID e o servidor associa novamente o arquivo e os metadados.
  3. Envie os metadados primeiro e retorne um ID ao cliente. O cliente envia o arquivo com o ID e o servidor associa novamente o arquivo e os metadados.
Daniel T.
fonte
29
Se eu escolhi a opção 1, incluí apenas o conteúdo Base64 na cadeia JSON? {file: '234JKFDS # $ @ # $ MFDDMS ....', nome: 'somename' ...} Ou há algo mais?
Gregg
15
Gregg, exatamente como você disse, você a incluiria como uma propriedade, e o valor seria a string codificada em base64. Esse é provavelmente o método mais fácil, mas pode não ser prático, dependendo do tamanho do arquivo. Por exemplo, para nosso aplicativo, precisamos enviar imagens do iPhone com 2-3 MB cada. Um aumento de 33% não é aceitável. Se você estiver enviando apenas imagens pequenas de 20 KB, essa sobrecarga poderá ser mais aceitável.
Daniel T.
19
Devo também mencionar que a codificação / decodificação base64 também levará algum tempo de processamento. Pode ser a coisa mais fácil de fazer, mas certamente não é a melhor.
Daniel T.
8
json com base64? hmm .. Estou pensando em furar a multipart / form
Onipresente
12
Por que é negado usar dados de várias partes / formulário em uma solicitação?
1nstinct
107

Você pode enviar o arquivo e os dados em uma solicitação usando o tipo de conteúdo multipart / form-data :

Em muitos aplicativos, é possível que um usuário seja apresentado com um formulário. O usuário preencherá o formulário, incluindo as informações digitadas, geradas pela entrada do usuário ou incluídas nos arquivos que o usuário selecionou. Quando o formulário é preenchido, os dados do formulário são enviados do usuário para o aplicativo de recebimento.

A definição de MultiPart / Form-Data é derivada de um desses aplicativos ...

Em http://www.faqs.org/rfcs/rfc2388.html :

"multipart / form-data" contém uma série de partes. Espera-se que cada parte contenha um cabeçalho de disposição de conteúdo [RFC 2183], onde o tipo de disposição é "formulário-dados" e onde a disposição contém um parâmetro (adicional) de "nome", onde o valor desse parâmetro é o original nome do campo no formulário. Por exemplo, uma peça pode conter um cabeçalho:

Disposição de conteúdo: dados do formulário; name = "usuário"

com o valor correspondente à entrada do campo "usuário".

Você pode incluir informações de arquivo ou campo em cada seção entre limites. Eu implementei com sucesso um serviço RESTful que exigia que o usuário enviasse dados e um formulário, e os dados de várias partes / formulário funcionavam perfeitamente. O serviço foi construído usando Java / Spring e o cliente estava usando C #, então, infelizmente, não tenho exemplos do Grails para dar a você sobre como configurar o serviço. Você não precisa usar JSON nesse caso, pois cada seção "form-data" fornece um local para especificar o nome do parâmetro e seu valor.

A coisa boa sobre o uso de dados de várias partes / formulário é que você está usando cabeçalhos definidos por HTTP; portanto, segue a filosofia REST de usar as ferramentas HTTP existentes para criar seu serviço.

McStretch
fonte
1
Obrigado, mas minha pergunta estava focada em querer usar o JSON para a solicitação e se isso fosse possível. Eu já sei que poderia enviá-lo da maneira que você sugere.
Gregg
15
Sim, essa é basicamente a minha resposta para "Eu não deveria estar usando JSON para isso?" Existe uma razão específica para você querer que o cliente use JSON?
McStretch
3
Provavelmente, um requisito comercial ou manutenção consistente. Obviamente, o ideal é aceitar ambos (dados do formulário e resposta JSON) com base no cabeçalho HTTP do tipo de conteúdo.
Daniel T.
2
A escolha do JSON resulta em um código muito mais elegante no lado do cliente e do servidor, o que leva a menos erros em potencial. Os dados do formulário são tão ontem.
Superarts.org 14/05
5
Peço desculpas pelo que disse se isso machucou o sentimento de algum desenvolvedor .Net. Embora o inglês não seja minha língua nativa, não é uma desculpa válida para eu dizer algo rude sobre a própria tecnologia. O uso de dados de formulário é incrível e, se você continuar usando, também será ainda mais incrível!
Superarts.org
53

Eu sei que este segmento é bastante antigo, no entanto, estou faltando aqui uma opção. Se você possui metadados (em qualquer formato) que deseja enviar juntamente com os dados a serem enviados, é possível fazer uma única multipart/relatedsolicitação.

O tipo de mídia Multipart / Related é destinado a objetos compostos que consistem em várias partes do corpo inter-relacionadas.

Você pode verificar a especificação RFC 2387 para obter detalhes mais detalhados.

Basicamente, cada parte dessa solicitação pode ter conteúdo de tipo diferente e todas as partes estão de alguma forma relacionadas (por exemplo, uma imagem e seus metadados). As partes são identificadas por uma cadeia de fronteira e a cadeia de fronteira final é seguida por dois hífens.

Exemplo:

POST /upload HTTP/1.1
Host: www.hostname.com
Content-Type: multipart/related; boundary=xyz
Content-Length: [actual-content-length]

--xyz
Content-Type: application/json; charset=UTF-8

{
    "name": "Sample image",
    "desc": "...",
    ...
}

--xyz
Content-Type: image/jpeg

[image data]
[image data]
[image data]
...
--foo_bar_baz--
pgiecek
fonte
Gostei muito da sua solução de longe. Infelizmente, parece não haver maneira de criar solicitações relacionadas a mutlipart em um navegador.
Petr Baudis
você tem alguma experiência em fazer com que os clientes (principalmente os JS) se comuniquem com a API dessa maneira
pvgoddijn
infelizmente, atualmente não há leitor para esse tipo de dados no php (7.2.1) e você teria que criar seu próprio analisador
dewd
É triste que servidores e clientes não tenham um bom suporte para isso.
Nader Ghanbari 15/10/19
14

Sei que essa pergunta é antiga, mas nos últimos dias eu havia pesquisado na Web inteira para solucionar essa mesma pergunta. Tenho serviços web REST grails e iPhone Client que enviam fotos, título e descrição.

Não sei se minha abordagem é a melhor, mas é tão fácil e simples.

Eu tiro uma foto usando o UIImagePickerController e envio ao servidor o NSData usando as tags de cabeçalho de solicitação para enviar os dados da imagem.

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"myServerAddress"]];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:UIImageJPEGRepresentation(picture, 0.5)];
[request setValue:@"image/jpeg" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"myPhotoTitle" forHTTPHeaderField:@"Photo-Title"];
[request setValue:@"myPhotoDescription" forHTTPHeaderField:@"Photo-Description"];

NSURLResponse *response;

NSError *error;

[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

No lado do servidor, recebo a foto usando o código:

InputStream is = request.inputStream

def receivedPhotoFile = (IOUtils.toByteArray(is))

def photo = new Photo()
photo.photoFile = receivedPhotoFile //photoFile is a transient attribute
photo.title = request.getHeader("Photo-Title")
photo.description = request.getHeader("Photo-Description")
photo.imageURL = "temp"    

if (photo.save()) {    

    File saveLocation = grailsAttributes.getApplicationContext().getResource(File.separator + "images").getFile()
    saveLocation.mkdirs()

    File tempFile = File.createTempFile("photo", ".jpg", saveLocation)

    photo.imageURL = saveLocation.getName() + "/" + tempFile.getName()

    tempFile.append(photo.photoFile);

} else {

    println("Error")

}

Não sei se tenho problemas no futuro, mas agora está funcionando bem no ambiente de produção.

Rscorreia
fonte
1
Eu gosto dessa opção de usar cabeçalhos http. Isso funciona especialmente bem quando há alguma simetria entre os metadados e os cabeçalhos http padrão, mas você obviamente pode inventar seus próprios.
EJ Campbell
14

Aqui está minha API de abordagem (eu uso o exemplo) - como você pode ver, você não usa nenhum file_id(identificador de arquivo carregado no servidor) na API:

  1. Crie um photoobjeto no servidor:

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. Carregar arquivo (observe que fileestá no formato singular porque é apenas um por foto):

    POST: /projects/{project_id}/photos/{photo_id}/file
    body: file to upload
    response: -

E então, por exemplo:

  1. Leia a lista de fotos

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. Leia alguns detalhes da foto

    GET: /projects/{project_id}/photos/{photo_id}
    response: { id: 666, name: 'some_schema.jpg', comment:'blah'} (photo object)
  3. Ler arquivo de foto

    GET: /projects/{project_id}/photos/{photo_id}/file
    response: file content

Portanto, a conclusão é que, primeiro você cria um objeto (foto) pelo POST e depois envia a segunda solicitação com o arquivo (novamente POST).

Kamil Kiełczewski
fonte
3
Esta parece ser a maneira mais 'RESTFUL' de conseguir isso.
James Webster
A operação POST para recursos recém-criados, deve retornar a identificação do local, nos detalhes da versão simples do objeto
Ivan Proskuryakov
@ivanproskuryakov por que "deve"? No exemplo acima (POST no ponto 2), o ID do arquivo é inútil. Segundo argumento (para POST no ponto 2) eu uso a forma singular '/ file' (não '/ files'), para que o ID não seja necessário porque path: / projects / 2 / photos / 3 / file fornece informações COMPLETAS ao arquivo de fotos de identidade.
Kamil Kiełczewski
Da especificação do protocolo HTTP. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Criados "O recurso recém-criado pode ser referenciado pelos URIs retornados na entidade da resposta, com o URI mais específico para o recurso fornecido por um campo de cabeçalho de localização ". @ KamilKiełczewski (um) e (dois) podem ser combinados em uma operação POST POST: / projects / {project_id} / photos Retornará o cabeçalho do local, que pode ser usado para a operação GET de foto única (recurso *) GET: para obter um única foto com todos os detalhes CGET: para obter toda a colecção das fotos
Ivan Proskuryakov
1
Se os metadados e o upload forem operações separadas, os pontos de extremidade terão os seguintes problemas: Para operação de upload de arquivo POST usada - o POST não é idempotente. PUT (idempotente) deve ser usado, pois você está alterando o recurso sem criar um novo. O REST trabalha com objetos chamados recursos . POST: “../photos/“ PUT: “../photos/{photo_id}” GET: “../photos/“ GET: “../photos/{photo_id}” PS. Separar o upload em um terminal separado pode levar a um comportamento imprevisível. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov
6

Objetos FormData: Carregar arquivos usando o Ajax

XMLHttpRequest Nível 2 adiciona suporte para a nova interface FormData. Os objetos FormData fornecem uma maneira de construir facilmente um conjunto de pares de chave / valor representando os campos do formulário e seus valores, que podem ser facilmente enviados usando o método XMLHttpRequest send ().

function AjaxFileUpload() {
    var file = document.getElementById("files");
    //var file = fileInput;
    var fd = new FormData();
    fd.append("imageFileData", file);
    var xhr = new XMLHttpRequest();
    xhr.open("POST", '/ws/fileUpload.do');
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
             alert('success');
        }
        else if (uploadResult == 'success')
             alert('error');
    };
    xhr.send(fd);
}

https://developer.mozilla.org/en-US/docs/Web/API/FormData

lakhan_Ideavate
fonte
6

Como o único exemplo que falta é o exemplo do ANDROID , vou adicioná-lo. Essa técnica usa um AsyncTask personalizado que deve ser declarado dentro da sua classe Activity.

private class UploadFile extends AsyncTask<Void, Integer, String> {
    @Override
    protected void onPreExecute() {
        // set a status bar or show a dialog to the user here
        super.onPreExecute();
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        // progress[0] is the current status (e.g. 10%)
        // here you can update the user interface with the current status
    }

    @Override
    protected String doInBackground(Void... params) {
        return uploadFile();
    }

    private String uploadFile() {

        String responseString = null;
        HttpClient httpClient = new DefaultHttpClient();
        HttpPost httpPost = new HttpPost("http://example.com/upload-file");

        try {
            AndroidMultiPartEntity ampEntity = new AndroidMultiPartEntity(
                new ProgressListener() {
                    @Override
                        public void transferred(long num) {
                            // this trigger the progressUpdate event
                            publishProgress((int) ((num / (float) totalSize) * 100));
                        }
            });

            File myFile = new File("/my/image/path/example.jpg");

            ampEntity.addPart("fileFieldName", new FileBody(myFile));

            totalSize = ampEntity.getContentLength();
            httpPost.setEntity(ampEntity);

            // Making server call
            HttpResponse httpResponse = httpClient.execute(httpPost);
            HttpEntity httpEntity = httpResponse.getEntity();

            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                responseString = EntityUtils.toString(httpEntity);
            } else {
                responseString = "Error, http status: "
                        + statusCode;
            }

        } catch (Exception e) {
            responseString = e.getMessage();
        }
        return responseString;
    }

    @Override
    protected void onPostExecute(String result) {
        // if you want update the user interface with upload result
        super.onPostExecute(result);
    }

}

Então, quando você quiser enviar seu arquivo, basta ligar para:

new UploadFile().execute();
lifeisfoo
fonte
Olá, o que é o AndroidMultiPartEntity, por favor, explique ... e se eu quiser fazer upload de arquivos pdf, word ou xls, o que devo fazer, por favor, forneça algumas orientações ... sou novo nisso.
Amit pandya
1
@amitpandya eu mudei o código para um upload de arquivo genérico por isso é mais claro para quem lê-lo
lifeisfoo
2

Eu queria enviar algumas strings para o servidor back-end. Eu não usei o json com multipart, usei parâmetros de solicitação.

@RequestMapping(value = "/upload", method = RequestMethod.POST)
public void uploadFile(HttpServletRequest request,
        HttpServletResponse response, @RequestParam("uuid") String uuid,
        @RequestParam("type") DocType type,
        @RequestParam("file") MultipartFile uploadfile)

O URL pareceria

http://localhost:8080/file/upload?uuid=46f073d0&type=PASSPORT

Estou passando dois parâmetros (uuid e tipo) junto com o upload do arquivo. Espero que isso ajude quem não possui os dados complexos do json para enviar.

Aslam anwer
fonte
1

Você pode tentar usar a https://square.github.io/okhttp/ library. Você pode definir o corpo da solicitação como multipart e adicionar os objetos file e json separadamente da seguinte maneira:

MultipartBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("uploadFile", uploadFile.getName(), okhttp3.RequestBody.create(uploadFile, MediaType.parse("image/png")))
                .addFormDataPart("file metadata", json)
                .build();

        Request request = new Request.Builder()
                .url("https://uploadurl.com/uploadFile")
                .post(requestBody)
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            logger.info(response.body().string());
OneXer
fonte
0
@RequestMapping(value = "/uploadImageJson", method = RequestMethod.POST)
    public @ResponseBody Object jsongStrImage(@RequestParam(value="image") MultipartFile image, @RequestParam String jsonStr) {
-- use  com.fasterxml.jackson.databind.ObjectMapper convert Json String to Object
}
sunleo
fonte
-5

Verifique se você tem a seguinte importação. Claro que outras importações padrão

import org.springframework.core.io.FileSystemResource


    void uploadzipFiles(String token) {

        RestBuilder rest = new RestBuilder(connectTimeout:10000, readTimeout:20000)

        def zipFile = new File("testdata.zip")
        def Id = "001G00000"
        MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>()
        form.add("id", id)
        form.add('file',new FileSystemResource(zipFile))
        def urld ='''http://URL''';
        def resp = rest.post(urld) {
            header('X-Auth-Token', clientSecret)
            contentType "multipart/form-data"
            body(form)
        }
        println "resp::"+resp
        println "resp::"+resp.text
        println "resp::"+resp.headers
        println "resp::"+resp.body
        println "resp::"+resp.status
    }
Mak Kul
fonte
1
Este getjava.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String
Mariano Ruiz