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

249

Eu tenho um serviço web REST que atualmente expõe este URL:

http: // servidor / dados / mídia

onde os usuários podem POSTo seguinte JSON:

{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

para criar um novo metadado de mídia.

Agora, preciso fazer o upload de um arquivo ao mesmo tempo que os metadados da mídia. Qual é a melhor maneira de fazer isso? Eu poderia introduzir uma nova propriedade chamada filee base64 codificar o arquivo, mas queria saber se havia uma maneira melhor.

Também multipart/form-dataestou usando como o que um formulário HTML enviaria, mas estou usando um serviço da Web REST e quero continuar usando JSON, se possível.

Daniel T.
fonte
35
A utilização de apenas JSON não é realmente necessária para ter um serviço da Web RESTful. O REST é basicamente qualquer coisa que segue os princípios principais dos métodos HTTP e algumas outras regras (possivelmente não padronizadas).
Erik Kaplun

Respostas:

192

Concordo com Greg que uma abordagem em duas fases é uma solução razoável, mas eu faria o contrário. Eu faria:

POST http://server/data/media
body:
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873
}

Para criar a entrada de metadados e retornar uma resposta como:

201 Created
Location: http://server/data/media/21323
{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873,
    "ContentUrl": "http://server/data/media/21323/content"
}

O cliente pode então usar este ContentUrl e fazer um PUT com os dados do arquivo.

O ponto positivo dessa abordagem é que, quando o servidor começa a ficar sobrecarregado com imensos volumes de dados, a URL retornada pode apenas apontar para outro servidor com mais espaço / capacidade. Ou você pode implementar algum tipo de abordagem round robin se a largura de banda for um problema.

Darrel Miller
fonte
8
Uma vantagem em enviar o conteúdo primeiro é que, quando os metadados existem, o conteúdo já está presente. Por fim, a resposta certa depende da organização dos dados no sistema.
Greg Hewgill 15/10/10
Obrigado, marquei esta como a resposta correta, porque era isso que eu queria fazer. Infelizmente, devido a uma regra comercial estranha, precisamos permitir que o upload ocorra em qualquer ordem (primeiro metadados ou primeiro arquivo). Eu queria saber se havia uma maneira de combinar os dois, a fim de evitar a dor de cabeça ao lidar com as duas situações.
Daniel T.
@ Daniel Se você postar o arquivo de dados primeiro, poderá pegar o URL retornado no local e adicioná-lo ao atributo ContentUrl nos metadados. Dessa forma, quando o servidor recebe os metadados, se um ContentUrl existe, ele já sabe onde está o arquivo. Se não houver ContentUrl, ele saberá que deve criar um.
Darrel Miller
se você fizesse o POST primeiro, publicaria no mesmo URL? (/ server / data / media) ou você criaria outro ponto de entrada para os uploads do primeiro arquivo?
precisa
1
@ Faraway E se os metadados incluíssem o número de "curtidas" de uma imagem? Você o trataria como um único recurso? Ou, mais obviamente, você está sugerindo que, se eu quisesse editar a descrição de uma imagem, precisaria fazer o upload novamente da imagem? Existem muitos casos em que formulários com várias partes são a solução certa. Nem sempre é esse o caso.
Darrel Miller
103

Só porque você não está agrupando todo o corpo da solicitação no JSON, não significa que não seja RESTful usar multipart/form-datapara postar o JSON e o (s) arquivo (s) em uma única solicitação:

curl -F "metadata=<metadata.json" -F "[email protected]" http://example.com/add-file

no lado do servidor (usando Python para pseudocódigo):

class AddFileResource(Resource):
    def render_POST(self, request):
        metadata = json.loads(request.args['metadata'][0])
        file_body = request.args['file'][0]
        ...

para carregar vários arquivos, é possível usar "campos de formulário" separados para cada um:

curl -F "metadata=<metadata.json" -F "[email protected]" -F "[email protected]" http://example.com/add-file

... nesse caso, o código do servidor terá request.args['file1'][0]erequest.args['file2'][0]

ou reutilize o mesmo para muitos:

curl -F "metadata=<metadata.json" -F "[email protected]" -F "[email protected]" http://example.com/add-file

... nesse caso request.args['files'] será simplesmente uma lista de comprimento 2.

ou passe vários arquivos por um único campo:

curl -F "metadata=<metadata.json" -F "[email protected],some-other-file.tar.gz" http://example.com/add-file

... nesse caso request.args['files'], será uma string contendo todos os arquivos, que você terá que analisar por conta própria - não sabe como fazê-lo, mas tenho certeza de que não é difícil, ou melhor, use as abordagens anteriores.

A diferença entre @e <é que @faz com que o arquivo seja anexado como um upload de arquivo, enquanto< anexa o conteúdo do arquivo como um campo de texto.

PS Só porque estou usando curlcomo uma forma de gerar POSTsolicitações não significa que as mesmas solicitações HTTP não puderam ser enviadas de uma linguagem de programação como Python ou usando qualquer ferramenta com capacidade suficiente.

Erik Kaplun
fonte
4
Eu mesmo estava me perguntando sobre essa abordagem e por que não tinha visto mais ninguém. Concordo, parece perfeitamente RESTful para mim.
soupdog
1
SIM! Essa é uma abordagem muito prática e não é menos RESTful do que usar "application / json" como um tipo de conteúdo para toda a solicitação.
sickill
..mas que só é possível se você tem os dados em um arquivo .json e enviá-lo, o que não é o caso
itsjavi
5
@mjolnic seu comentário é irrelevante: os exemplos cURL são apenas, bem, exemplos ; a resposta afirma explicitamente que você pode usar qualquer coisa para enviar a solicitação ... além disso, o que impede você de escrever curl -f 'metadata={"foo": "bar"}'?
precisa saber é o seguinte
3
Estou usando essa abordagem porque a resposta aceita não funcionaria para o aplicativo que estou desenvolvendo (o arquivo não pode existir antes dos dados e adiciona complexidade desnecessária para lidar com o caso em que os dados são carregados primeiro e o arquivo nunca carrega) .
BitsEvolved
33

Uma maneira de abordar o problema é fazer do upload um processo de duas fases. Primeiro, você faria o upload do próprio arquivo usando um POST, em que o servidor retornará algum identificador de volta ao cliente (um identificador pode ser o SHA1 do conteúdo do arquivo). Em seguida, uma segunda solicitação associa os metadados aos dados do arquivo:

{
    "Name": "Test",
    "Latitude": 12.59817,
    "Longitude": 52.12873,
    "ContentID": "7a788f56fa49ae0ba5ebde780efe4d6a89b5db47"
}

A inclusão da base de dados do arquivo64 codificada na própria solicitação JSON aumentará o tamanho dos dados transferidos em 33%. Isso pode ou não ser importante, dependendo do tamanho geral do arquivo.

Outra abordagem pode ser usar um POST dos dados brutos do arquivo, mas incluir quaisquer metadados no cabeçalho da solicitação HTTP. No entanto, isso fica um pouco fora das operações REST básicas e pode ser mais complicado para algumas bibliotecas de clientes HTTP.

Greg Hewgill
fonte
Você pode usar o Ascii85 aumentando apenas 1/4.
Singagirl 13/09/16
Alguma referência sobre por que base64 aumenta tanto o tamanho?
jam01
1
@ jam01: Por coincidência, acabei de ver algo ontem que responde bem à questão do espaço: Qual é a sobrecarga de espaço da codificação Base64?
Greg Hewgill 31/01/19
10

Sei que essa é uma pergunta muito antiga, mas espero que isso ajude outra pessoa quando cheguei neste post procurando a mesma coisa. Eu tive um problema semelhante, apenas que meus metadados eram Guid e int. A solução é a mesma. Você pode apenas tornar os metadados necessários parte do URL.

Método de aceitação POST na sua classe "Controller":

public Task<HttpResponseMessage> PostFile(string name, float latitude, float longitude)
{
    //See http://stackoverflow.com/a/10327789/431906 for how to accept a file
    return null;
}

Então, no que quer que você esteja registrando rotas, WebApiConfig.Register (HttpConfiguration config) para mim nesse caso.

config.Routes.MapHttpRoute(
    name: "FooController",
    routeTemplate: "api/{controller}/{name}/{latitude}/{longitude}",
    defaults: new { }
);
Greg Biles
fonte
5

Se o seu arquivo e seus metadados criarem um recurso, é perfeitamente bom fazer o upload dos dois em uma solicitação. A solicitação de amostra seria:

POST https://target.com/myresources/resourcename HTTP/1.1

Accept: application/json

Content-Type: multipart/form-data; 

boundary=-----------------------------28947758029299

Host: target.com

-------------------------------28947758029299

Content-Disposition: form-data; name="application/json"

{"markers": [
        {
            "point":new GLatLng(40.266044,-74.718479), 
            "homeTeam":"Lawrence Library",
            "awayTeam":"LUGip",
            "markerImage":"images/red.png",
            "information": "Linux users group meets second Wednesday of each month.",
            "fixture":"Wednesday 7pm",
            "capacity":"",
            "previousScore":""
        },
        {
            "point":new GLatLng(40.211600,-74.695702),
            "homeTeam":"Hamilton Library",
            "awayTeam":"LUGip HW SIG",
            "markerImage":"images/white.png",
            "information": "Linux users can meet the first Tuesday of the month to work out harward and configuration issues.",
            "fixture":"Tuesday 7pm",
            "capacity":"",
            "tv":""
        },
        {
            "point":new GLatLng(40.294535,-74.682012),
            "homeTeam":"Applebees",
            "awayTeam":"After LUPip Mtg Spot",
            "markerImage":"images/newcastle.png",
            "information": "Some of us go there after the main LUGip meeting, drink brews, and talk.",
            "fixture":"Wednesday whenever",
            "capacity":"2 to 4 pints",
            "tv":""
        },
] }

-------------------------------28947758029299

Content-Disposition: form-data; name="name"; filename="myfilename.pdf"

Content-Type: application/octet-stream

%PDF-1.4
%
2 0 obj
<</Length 57/Filter/FlateDecode>>stream
x+r
26S00SI2P0Qn
F
!i\
)%!Y0i@.k
[
endstream
endobj
4 0 obj
<</Type/Page/MediaBox[0 0 595 842]/Resources<</Font<</F1 1 0 R>>>>/Contents 2 0 R/Parent 3 0 R>>
endobj
1 0 obj
<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>
endobj
3 0 obj
<</Type/Pages/Count 1/Kids[4 0 R]>>
endobj
5 0 obj
<</Type/Catalog/Pages 3 0 R>>
endobj
6 0 obj
<</Producer(iTextSharp 5.5.11 2000-2017 iText Group NV \(AGPL-version\))/CreationDate(D:20170630120636+02'00')/ModDate(D:20170630120636+02'00')>>
endobj
xref
0 7
0000000000 65535 f 
0000000250 00000 n 
0000000015 00000 n 
0000000338 00000 n 
0000000138 00000 n 
0000000389 00000 n 
0000000434 00000 n 
trailer
<</Size 7/Root 5 0 R/Info 6 0 R/ID [<c7c34272c2e618698de73f4e1a65a1b5><c7c34272c2e618698de73f4e1a65a1b5>]>>
%iText-5.5.11
startxref
597
%%EOF

-------------------------------28947758029299--
Mike Ezzati
fonte
3

Não entendo por que, ao longo de oito anos, ninguém postou a resposta fácil. Em vez de codificar o arquivo como base64, codifique o json como uma sequência. Em seguida, decodifique o json no lado do servidor.

Em Javascript:

let formData = new FormData();
formData.append("file", myfile);
formData.append("myjson", JSON.stringify(myJsonObject));

POSTA-O usando o Tipo de Conteúdo: multipart / form-data

No lado do servidor, recupere o arquivo normalmente e recupere o json como uma sequência. Converta a string em um objeto, que geralmente é uma linha de código, independentemente da linguagem de programação usada.

(Sim, funciona muito bem. Fazê-lo em um dos meus aplicativos.)

ccleve
fonte