Come faccio a caricare un file con metadati utilizzando un servizio web REST?


249

Ho un servizio web REST che attualmente espone questo URL:

http: // server / data / media

dove gli utenti possono POSTil seguente JSON:

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

al fine di creare nuovi metadati multimediali.

Ora ho bisogno della possibilità di caricare un file contemporaneamente ai metadati multimediali. Qual è il modo migliore per farlo? Potrei introdurre una nuova proprietà chiamata filee base64 codificare il file, ma mi chiedevo se ci fosse un modo migliore.

Si sta anche usando multipart/form-datacome ciò che un modulo HTML invierebbe, ma sto usando un servizio web REST e voglio attenermi all'utilizzo di JSON, se possibile.


35
Attenersi all'utilizzo solo di JSON non è realmente necessario per disporre di un servizio Web RESTful. REST è praticamente tutto ciò che segue i principi fondamentali dei metodi HTTP e alcune altre regole (probabilmente non standardizzate).
Erik Kaplun,

Risposte:


192

Concordo con Greg sul fatto che un approccio in due fasi sia una soluzione ragionevole, tuttavia lo farei al contrario. Farei:

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

Per creare la voce metadati e restituire una risposta come:

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

Il client può quindi utilizzare questo ContentUrl ed eseguire un PUT con i dati del file.

La cosa bella di questo approccio è quando il tuo server inizia a essere appesantito da immensi volumi di dati, l'URL che ritorni può puntare a qualche altro server con più spazio / capacità. Oppure potresti implementare una sorta di approccio round robin se la larghezza di banda è un problema.


8
Uno dei vantaggi di inviare prima il contenuto è che quando i metadati esistono, il contenuto è già presente. In definitiva, la risposta giusta dipende dall'organizzazione dei dati nel sistema.
Greg Hewgill,

Grazie, l'ho contrassegnata come la risposta corretta perché è quello che volevo fare. Sfortunatamente, a causa di una strana regola aziendale, dobbiamo consentire il caricamento in qualsiasi ordine (prima i metadati o prima i file). Mi chiedevo se ci fosse un modo per combinare i due al fine di evitare il mal di testa di affrontare entrambe le situazioni.
Daniel T.,

@Daniel Se prima pubblichi il file di dati, puoi prendere l'URL restituito in Posizione e aggiungerlo all'attributo ContentUrl nei metadati. In questo modo, quando il server riceve i metadati, se esiste un ContentUrl, allora sa già dove si trova il file. Se non c'è ContentUrl, allora sa che dovrebbe crearne uno.
Darrel Miller,

se dovessi fare prima il POST, pubblicheresti nello stesso URL? (/ server / data / media) o creeresti un altro punto di ingresso per i caricamenti del primo file?
Matt Brailsford,

1
@Faraway E se i metadati includessero il numero di "Mi piace" di un'immagine? Lo tratteresti come una singola risorsa allora? O più ovviamente, stai suggerendo che se volessi modificare la descrizione di un'immagine, avrei bisogno di ricaricare l'immagine? Esistono molti casi in cui i moduli multiparte sono la soluzione giusta. Non è sempre il caso.
Darrel Miller,

103

Solo perché non stai racchiudendo l'intero corpo della richiesta in JSON, non significa che non sia RESTful utilizzare multipart/form-dataper pubblicare sia il file JSON che i file in una singola richiesta:

curl -F "metadata=<metadata.json" -F "file=@my-file.tar.gz" http://example.com/add-file

sul lato server (usando Python per lo pseudocodice):

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

per caricare più file, è possibile utilizzare "campi modulo" separati per ciascuno:

curl -F "metadata=<metadata.json" -F "file1=@some-file.tar.gz" -F "file2=@some-other-file.tar.gz" http://example.com/add-file

... nel qual caso il codice del server avrà request.args['file1'][0]erequest.args['file2'][0]

o riutilizzare lo stesso per molti:

curl -F "metadata=<metadata.json" -F "files=@some-file.tar.gz" -F "files=@some-other-file.tar.gz" http://example.com/add-file

... nel qual caso request.args['files']sarà semplicemente un elenco di lunghezza 2.

o passa più file attraverso un singolo campo:

curl -F "metadata=<metadata.json" -F "files=@some-file.tar.gz,some-other-file.tar.gz" http://example.com/add-file

... nel qual caso request.args['files']sarà una stringa contenente tutti i file, che dovrai analizzare tu stesso - non sei sicuro di come farlo, ma sono sicuro che non è difficile, o meglio usa semplicemente gli approcci precedenti.

La differenza tra @e <è che @fa sì che il file venga allegato come caricamento del file, mentre <allega il contenuto del file come campo di testo.

PS Solo perché sto usando curlun modo per generare le POSTrichieste non significa che le stesse richieste HTTP esatte non possano essere inviate da un linguaggio di programmazione come Python o usando uno strumento sufficientemente capace.


4
Mi stavo chiedendo da solo questo approccio e perché non avevo ancora visto nessun altro che lo esponesse. Sono d'accordo, mi sembra perfettamente RESTful.
cane zuppa

1
SÌ! Questo è un approccio molto pratico e non è meno RESTful dell'utilizzo di "application / json" come tipo di contenuto per l'intera richiesta.
sickill

..ma questo è possibile solo se hai i dati in un file .json e caricali, il che non è il caso
itsjavi

5
@mjolnic il tuo commento è irrilevante: gli esempi cURL sono solo, beh, esempi ; la risposta afferma esplicitamente che è possibile utilizzare qualsiasi cosa per inviare la richiesta ... inoltre, cosa ti impedisce di scrivere curl -f 'metadata={"foo": "bar"}'?
Erik Kaplun,

3
Sto usando questo approccio perché la risposta accettata non funzionerebbe per l'applicazione che sto sviluppando (il file non può esistere prima dei dati e aggiunge complessità non necessaria per gestire il caso in cui i dati vengono caricati per primi e il file non viene mai caricato) .
Inserito il

33

Un modo per affrontare il problema è rendere il caricamento un processo in due fasi. Innanzitutto, caricare il file stesso utilizzando un POST, in cui il server restituisce un identificatore al client (un identificatore potrebbe essere lo SHA1 del contenuto del file). Quindi, una seconda richiesta associa i metadati ai dati del file:

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

Includendo la base di dati di file64 codificata nella stessa richiesta JSON aumenterà la dimensione dei dati trasferiti del 33%. Questo può essere o non essere importante a seconda della dimensione complessiva del file.

Un altro approccio potrebbe essere quello di utilizzare un POST dei dati di file non elaborati, ma includere eventuali metadati nell'intestazione della richiesta HTTP. Tuttavia, questo non rientra nelle operazioni REST di base e potrebbe essere più imbarazzante per alcune librerie client HTTP.


Puoi usare Ascii85 aumentando solo di 1/4.
Singagirl,

Qualche riferimento sul perché base64 aumenta le dimensioni così tanto?
jam01

1
@ jam01: Per coincidenza, ieri ho visto qualcosa che risponde bene alla domanda spaziale: qual è il sovraccarico di spazio della codifica Base64?
Greg Hewgill,

10

Mi rendo conto che questa è una domanda molto vecchia, ma spero che questo possa aiutare qualcun altro a trovare questo post cercando la stessa cosa. Ho avuto un problema simile, solo che i miei metadati erano un Guid e int. La soluzione è la stessa però. Puoi semplicemente rendere i metadati necessari parte dell'URL.

Metodo di accettazione POST nella 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;
}

Quindi in qualunque cosa tu stia registrando i percorsi, WebApiConfig.Register (HttpConfiguration config) per me in questo caso.

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

5

Se il tuo file e i suoi metadati creano una risorsa, è perfettamente corretto caricarli entrambi in una richiesta. La richiesta di esempio sarebbe:

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

3

Non capisco perché, nel corso di otto anni, nessuno abbia pubblicato la risposta semplice. Invece di codificare il file come base64, codifica il json come stringa. Quindi decodifica il json sul lato server.

In Javascript:

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

POST usando Content-Type: multipart / form-data

Sul lato server, recuperare normalmente il file e recuperare il json come stringa. Converti la stringa in un oggetto, che di solito è una riga di codice, indipendentemente dal linguaggio di programmazione che usi.

(Sì, funziona benissimo. Farlo in una delle mie app.)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.