Pubblicazione di un file e di dati associati su un servizio Web RESTful preferibilmente come JSON


757

Questa sarà probabilmente una domanda stupida, ma sto vivendo una di quelle notti. In un'applicazione sto sviluppando l'API RESTful e vogliamo che il client invii i dati come JSON. Parte di questa applicazione richiede al client di caricare un file (di solito un'immagine) e informazioni sull'immagine.

Sto facendo fatica a rintracciare come ciò accade in una singola richiesta. È possibile Base64 i dati del file in una stringa JSON? Ho bisogno di eseguire 2 post sul server? Non dovrei usare JSON per questo?

Come nota a margine, stiamo usando Grails sul backend e questi servizi sono accessibili da client mobili nativi (iPhone, Android, ecc.), Se uno di questi fa la differenza.


1
Quindi, qual è il modo migliore per farlo?
James111,

3
Invia i metadati nella stringa della query URL, anziché JSON.
jrc

Risposte:


632

Ho fatto una domanda simile qui:

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

Fondamentalmente hai tre scelte:

  1. Base64 codifica il file, a spese di aumentare la dimensione dei dati di circa il 33% e aggiunge un overhead di elaborazione sia nel server che nel client per la codifica / decodifica.
  2. Invia prima il file in un multipart/form-dataPOST e restituisce un ID al client. Il client invia quindi i metadati con l'ID e il server associa nuovamente il file e i metadati.
  3. Invia prima i metadati e restituisce un ID al client. Il client invia quindi il file con l'ID e il server associa nuovamente il file e i metadati.

29
Se ho scelto l'opzione 1, includo semplicemente il contenuto Base64 all'interno della stringa JSON? {file: '234JKFDS # $ @ # $ MFDDMS ....', nome: 'somename' ...} O c'è dell'altro?
Gregg,

15
Gregg, esattamente come hai detto, lo includeresti semplicemente come proprietà e il valore sarebbe la stringa con codifica base64. Questo è probabilmente il metodo più semplice da seguire, ma potrebbe non essere pratico a seconda della dimensione del file. Ad esempio, per la nostra applicazione, è necessario inviare immagini per iPhone da 2-3 MB ciascuna. Un aumento del 33% non è accettabile. Se invii solo piccole immagini da 20 KB, quell'overhead potrebbe essere più accettabile.
Daniel T.

19
Vorrei anche ricordare che la codifica / decodifica base64 richiederà anche del tempo di elaborazione. Potrebbe essere la cosa più semplice da fare, ma sicuramente non è la migliore.
Daniel T.

8
json con base64? hmm .. sto pensando di attenermi a multipart / form
Onnipresente

12
Perché è vietato utilizzare dati multipart / form in una richiesta?
1stinto

107

È possibile inviare il file e i dati in un'unica richiesta utilizzando il tipo di contenuto multipart / form-data :

In molte applicazioni, è possibile che un utente venga presentato con un modulo. L'utente compilerà il modulo, comprese le informazioni digitate, generate dall'input dell'utente o incluse dai file che l'utente ha selezionato. Quando il modulo viene compilato, i dati dal modulo vengono inviati dall'utente all'applicazione ricevente.

La definizione di MultiPart / Form-Data deriva da una di quelle applicazioni ...

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

"multipart / form-data" contiene una serie di parti. Ogni parte dovrebbe contenere un'intestazione di disposizione del contenuto [RFC 2183] in cui il tipo di disposizione è "form-data" e in cui la disposizione contiene un parametro (aggiuntivo) di "nome", dove il valore di quel parametro è l'originale nome del campo nel modulo. Ad esempio, una parte potrebbe contenere un'intestazione:

Disposizione del contenuto: form-data; name = "utente"

con il valore corrispondente alla voce del campo "utente".

È possibile includere informazioni sui file o informazioni sui campi all'interno di ciascuna sezione tra i limiti. Ho implementato con successo un servizio RESTful che ha richiesto all'utente di inviare sia i dati che un modulo e i dati multipart / form hanno funzionato perfettamente. Il servizio è stato creato utilizzando Java / Spring e il client utilizzava C #, quindi sfortunatamente non ho esempi Grails da darti su come impostare il servizio. In questo caso non è necessario utilizzare JSON poiché ogni sezione "dati-modulo" fornisce un posto per specificare il nome del parametro e il suo valore.

La cosa buona dell'utilizzo di multipart / form-data è che stai usando le intestazioni definite HTTP, quindi ti attieni alla filosofia REST di usare gli strumenti HTTP esistenti per creare il tuo servizio.


1
Grazie, ma la mia domanda era focalizzata sul voler usare JSON per la richiesta e se fosse possibile. So già che potrei inviarlo come suggerisci tu.
Gregg,

15
Sì, questa è essenzialmente la mia risposta per "Non dovrei usare JSON per questo?" C'è un motivo specifico per cui vuoi che il client usi JSON?
McStretch,

3
Molto probabilmente un requisito aziendale o mantenere coerenza. Naturalmente, la cosa ideale da fare è accettare entrambi (i dati del modulo e la risposta JSON) in base all'intestazione HTTP Content-Type.
Daniel T.

2
Scegliendo JSON si ottiene un codice molto più elegante sia sul lato client che sul lato server, il che porta a meno potenziali bug. I dati del modulo sono così ieri.
superarts.org

5
Mi scuso per quello che ho detto se ha danneggiato alcuni sentimenti dello sviluppatore .Net. Sebbene l'inglese non sia la mia lingua madre, non è una scusa valida per me per dire qualcosa di scortese sulla tecnologia stessa. L'uso dei dati dei moduli è fantastico e se continui a usarli, lo sarai anche di più!
superarts.org il

53

So che questo thread è piuttosto vecchio, tuttavia, qui mi manca un'opzione. Se si dispone di metadati (in qualsiasi formato) che si desidera inviare insieme ai dati da caricare, è possibile effettuare una singola multipart/relatedrichiesta.

Il tipo di supporto Multipart / Related è destinato a oggetti composti costituiti da diverse parti del corpo correlate.

È possibile controllare le specifiche RFC 2387 per dettagli più approfonditi.

Fondamentalmente ogni parte di tale richiesta può avere contenuti di tipo diverso e tutte le parti sono in qualche modo correlate (ad esempio un'immagine e i suoi metadati). Le parti sono identificate da una stringa di limite e la stringa di limite finale è seguita da due trattini.

Esempio:

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

Mi è piaciuta di gran lunga la tua soluzione. Sfortunatamente, non sembra esserci alcun modo per creare richieste multipart / correlate in un browser.
Petr Baudis,

hai qualche esperienza nel convincere i clienti (specialmente quelli di JS) a comunicare con l'API in questo modo
pvgoddijn,

sfortunatamente, al momento non esiste un lettore per questo tipo di dati su php (7.2.1) e dovresti costruire il tuo parser
dewd

È triste che server e client non abbiano un buon supporto per questo.
Nader Ghanbari,

14

So che questa domanda è vecchia, ma negli ultimi giorni avevo cercato su tutto il web per risolvere questa stessa domanda. Ho Grabs servizi web REST e client iPhone che inviano immagini, titolo e descrizione.

Non so se il mio approccio sia il migliore, ma è così facile e semplice.

Scatto una foto utilizzando UIImagePickerController e invio al server NSData utilizzando i tag di intestazione della richiesta per inviare i dati della foto.

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

Sul lato server, ricevo la foto utilizzando il codice:

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

}

Non so se avrò problemi in futuro, ma ora funziona bene nell'ambiente di produzione.


1
Mi piace questa opzione di utilizzo delle intestazioni http. Funziona particolarmente bene quando c'è una certa simmetria tra i metadati e le intestazioni http standard, ma puoi ovviamente inventare il tuo.
EJ Campbell

14

Ecco il mio approccio API (io uso esempio) - come puoi vedere, io non uso alcun file_id(identificatore di file caricato sul server) nell'API:

  1. Crea photooggetto sul server:

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. Carica file (nota che fileè in forma singolare perché è solo uno per foto):

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

E poi per esempio:

  1. Leggi l'elenco delle foto

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. Leggi alcuni dettagli della foto

    GET: /projects/{project_id}/photos/{photo_id}
    response: { id: 666, name: 'some_schema.jpg', comment:'blah'} (photo object)
  3. Leggi il file di foto

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

Quindi la conclusione è che prima crei un oggetto (foto) tramite POST, quindi invii una seconda richiesta con il file (di nuovo POST).


3
Questo sembra il modo più "RESTFUL" per raggiungere questo obiettivo.
James Webster,

Operazione POST per le risorse appena create, deve restituire l'ID posizione, nei dettagli della versione semplice dell'oggetto
Ivan Proskuryakov,

@ivanproskuryakov perché "must"? Nell'esempio sopra (POST al punto 2) l'id del file è inutile. Secondo argomento (per POST al punto 2) utilizzo la forma singolare '/ file' (non '/ file') quindi l'ID non è necessario perché il percorso: / projects / 2 / photos / 3 / file fornisce informazioni COMPLETE al file di foto di identità.
Kamil Kiełczewski,

Dalla specifica del protocollo HTTP. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Creato "La risorsa appena creata può essere referenziata dagli URI restituiti nell'entità della risposta, con l'URI più specifico per la risorsa fornita da un campo di intestazione di posizione. " @ KamilKiełczewski (uno) e (due) potrebbero essere combinati in un'unica operazione POST POST: / projects / {project_id} / photos Restituirà l'intestazione della posizione, che potrebbe essere utilizzata per ottenere un'operazione singola foto (risorsa *) OTTIENI: per ottenere un singola foto con tutti i dettagli CGET: per ottenere tutta la raccolta delle foto
Ivan Proskuryakov,

1
Se i metadati e il caricamento sono operazioni separate, gli endpoint presentano questi problemi: Per il caricamento di file Operazione POST utilizzata: POST non è idempotente. PUT (idempotent) deve essere utilizzato poiché si modifica la risorsa senza crearne una nuova. REST funziona con oggetti chiamati risorse . POST: “../photos/“ PUT: “../photos/{photo_id}” OTTIENI: “../photos/“ OTTIENI: “../photos/{photo_id}” PS. Separare il caricamento in un endpoint separato può portare a comportamenti non previsti. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov

6

Oggetti FormData: carica file usando Ajax

XMLHttpRequest Level 2 aggiunge il supporto per la nuova interfaccia FormData. Gli oggetti FormData forniscono un modo per costruire facilmente un insieme di coppie chiave / valore che rappresentano i campi modulo e i loro valori, che possono quindi essere facilmente inviati utilizzando il metodo send () XMLHttpRequest.

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


6

Poiché l'unico esempio mancante è l' esempio ANDROID , lo aggiungerò. Questa tecnica utilizza un AsyncTask personalizzato che deve essere dichiarato all'interno della 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);
    }

}

Quindi, quando vuoi caricare il tuo file, chiama:

new UploadFile().execute();

Ciao, cos'è AndroidMultiPartEntity, per favore, spiega ... e se voglio caricare file pdf, word o xls cosa devo fare, ti prego di dare qualche consiglio ... Sono nuovo di questo.
amand pandya,

1
@amitpandya Ho cambiato il codice in un caricamento di file generico, quindi è più chiaro a chiunque lo
legga

2

Volevo inviare alcune stringhe al server back-end. Non ho usato json con multipart, ho usato parametri di richiesta.

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

Url sarebbe simile

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

Sto passando due parametri (uuid e tipo) insieme al caricamento del file. Spero che questo possa aiutare chi non ha i dati json complessi da inviare.


1

Potresti provare a usare la libreria https://square.github.io/okhttp/ . È possibile impostare il corpo della richiesta su più parti e quindi aggiungere separatamente gli oggetti file e json in questo modo:

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());

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
}

-5

Assicurati di avere la seguente importazione. Naturalmente altre importazioni standard

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
    }

1
Questo arrivajava.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String
Mariano Ruiz il
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.