Senden einer Datei und zugehöriger Daten an einen RESTful WebService, vorzugsweise als JSON

757

Dies wird wahrscheinlich eine dumme Frage sein, aber ich habe einen dieser Nächte. In einer Anwendung entwickle ich eine RESTful-API und wir möchten, dass der Client Daten als JSON sendet. Ein Teil dieser Anwendung erfordert, dass der Client eine Datei (normalerweise ein Bild) sowie Informationen zum Bild hochlädt.

Es fällt mir schwer, herauszufinden, wie dies in einer einzelnen Anfrage geschieht. Ist es möglich, die Dateidaten in eine JSON-Zeichenfolge zu Base64 umzuwandeln? Muss ich 2 Posts auf dem Server ausführen? Sollte ich dafür nicht JSON verwenden?

Nebenbei bemerkt, wir verwenden Grails im Backend und auf diese Dienste wird von nativen mobilen Clients (iPhone, Android usw.) zugegriffen, wenn dies einen Unterschied macht.

Gregg
quelle
1
Also, was ist der beste Weg, dies zu tun?
James111
3
Senden Sie die Metadaten in der URL-Abfragezeichenfolge anstelle von JSON.
jrc

Antworten:

632

Ich habe hier eine ähnliche Frage gestellt:

Wie lade ich eine Datei mit Metadaten über einen REST-Webdienst hoch?

Sie haben grundsätzlich drei Möglichkeiten:

  1. Base64 codiert die Datei auf Kosten einer Erhöhung der Datengröße um etwa 33% und erhöht den Verarbeitungsaufwand sowohl auf dem Server als auch auf dem Client für die Codierung / Decodierung.
  2. Senden Sie die Datei zuerst in einem multipart/form-dataPOST und geben Sie eine ID an den Client zurück. Der Client sendet dann die Metadaten mit der ID, und der Server ordnet die Datei und die Metadaten erneut zu.
  3. Senden Sie zuerst die Metadaten und geben Sie eine ID an den Client zurück. Der Client sendet dann die Datei mit der ID, und der Server ordnet die Datei und die Metadaten erneut zu.
Daniel T.
quelle
29
Wenn ich Option 1 gewählt habe, füge ich nur den Base64-Inhalt in die JSON-Zeichenfolge ein? {file: '234JKFDS # $ @ # $ MFDDMS ....', name: 'somename' ...} Oder steckt noch etwas dahinter?
Gregg
15
Gregg, genau wie Sie gesagt haben, würden Sie es einfach als Eigenschaft einfügen, und der Wert wäre die Base64-codierte Zeichenfolge. Dies ist wahrscheinlich die einfachste Methode, die jedoch je nach Dateigröße möglicherweise nicht praktikabel ist. Für unsere Anwendung müssen wir beispielsweise iPhone-Bilder mit jeweils 2-3 MB senden. Eine Steigerung von 33% ist nicht akzeptabel. Wenn Sie nur kleine 20-KB-Bilder senden, ist dieser Overhead möglicherweise akzeptabler.
Daniel T.
19
Ich sollte auch erwähnen, dass die Base64-Codierung / -Decodierung auch einige Verarbeitungszeit benötigt. Es ist vielleicht die einfachste Sache, aber sicherlich nicht die beste.
Daniel T.
8
json mit base64? hmm .. Ich denke darüber nach, mich an Multipart / Form zu halten
Allgegenwärtig
12
Warum ist es verweigert, mehrteilige / Formulardaten in einer Anfrage zu verwenden?
1.
107

Sie können die Datei und die Daten in einer Anfrage mit dem Inhaltstyp Multipart / Formulardaten senden :

In vielen Anwendungen kann einem Benutzer ein Formular angezeigt werden. Der Benutzer füllt das Formular aus, einschließlich Informationen, die eingegeben, durch Benutzereingaben generiert oder aus vom Benutzer ausgewählten Dateien enthalten sind. Wenn das Formular ausgefüllt wird, werden die Daten aus dem Formular vom Benutzer an die empfangende Anwendung gesendet.

Die Definition von MultiPart / Form-Data leitet sich aus einer dieser Anwendungen ab ...

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

"Multipart / Formulardaten" enthält eine Reihe von Teilen. Es wird erwartet, dass jeder Teil einen Inhaltsdisposition-Header [RFC 2183] enthält, in dem der Dispositionstyp "Formulardaten" ist und in dem die Disposition einen (zusätzlichen) Parameter "Name" enthält, wobei der Wert dieses Parameters das Original ist Feldname im Formular. Ein Teil kann beispielsweise einen Header enthalten:

Inhaltsdisposition: Formulardaten; name = "Benutzer"

mit dem Wert, der dem Eintrag des Feldes "Benutzer" entspricht.

Sie können Datei- oder Feldinformationen in jeden Abschnitt zwischen den Grenzen einfügen. Ich habe erfolgreich einen RESTful-Service implementiert, bei dem der Benutzer sowohl Daten als auch ein Formular senden musste, und Multipart- / Formulardaten funktionierten einwandfrei. Der Dienst wurde mit Java / Spring erstellt, und der Client verwendete C #. Daher habe ich leider keine Grails-Beispiele für die Einrichtung des Dienstes. In diesem Fall müssen Sie JSON nicht verwenden, da Sie in jedem Abschnitt "Formulardaten" den Namen des Parameters und seinen Wert angeben können.

Das Gute an der Verwendung von mehrteiligen / Formulardaten ist, dass Sie HTTP-definierte Header verwenden. Sie halten sich also an die REST-Philosophie, vorhandene HTTP-Tools zum Erstellen Ihres Dienstes zu verwenden.

McStretch
quelle
1
Vielen Dank, aber meine Frage konzentrierte sich darauf, JSON für die Anfrage verwenden zu wollen und ob dies möglich war. Ich weiß bereits, dass ich es so senden kann, wie Sie es vorschlagen.
Gregg
15
Ja, das ist im Wesentlichen meine Antwort für "Sollte ich JSON nicht dafür verwenden?" Gibt es einen bestimmten Grund, warum der Client JSON verwenden soll?
McStretch
3
Höchstwahrscheinlich eine Geschäftsanforderung oder die Einhaltung der Konsistenz. Am besten akzeptieren Sie natürlich beide (Formulardaten und JSON-Antwort) basierend auf dem HTTP-Header vom Inhaltstyp.
Daniel T.
2
Die Auswahl von JSON führt sowohl auf der Client- als auch auf der Serverseite zu einem wesentlich eleganteren Code, was zu weniger potenziellen Fehlern führt. Formulardaten sind so gestern.
Superarts.org
5
Ich entschuldige mich für das, was ich gesagt habe, wenn es das Gefühl eines .NET-Entwicklers verletzt hat. Obwohl Englisch nicht meine Muttersprache ist, ist es für mich keine gültige Entschuldigung, etwas Unhöfliches über die Technologie selbst zu sagen. Die Verwendung von Formulardaten ist fantastisch und wenn Sie sie weiterhin verwenden, werden Sie auch noch fantastischer!
Superarts.org
53

Ich weiß, dass dieser Thread ziemlich alt ist, aber mir fehlt hier eine Option. Wenn Sie Metadaten (in einem beliebigen Format) haben, die Sie zusammen mit den hochzuladenden Daten senden möchten, können Sie eine einzelne multipart/relatedAnfrage stellen.

Der mehrteilige / verwandte Medientyp ist für zusammengesetzte Objekte vorgesehen, die aus mehreren miteinander verbundenen Körperteilen bestehen.

Weitere Informationen finden Sie in der RFC 2387- Spezifikation.

Grundsätzlich kann jeder Teil einer solchen Anfrage Inhalte mit unterschiedlichem Typ haben und alle Teile sind irgendwie miteinander verbunden (z. B. ein Bild und seine Metadaten). Die Teile sind durch eine Grenzzeichenfolge gekennzeichnet, und auf die endgültige Grenzzeichenfolge folgen zwei Bindestriche.

Beispiel:

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
quelle
Ihre Lösung hat mir bei weitem am besten gefallen. Leider scheint es keine Möglichkeit zu geben, mehrteilige / verwandte Anfragen in einem Browser zu erstellen.
Petr Baudis
Haben Sie Erfahrung darin, Kunden (insbesondere JS) dazu zu bringen, auf diese Weise mit der API zu kommunizieren
pvgoddijn
Leider gibt es derzeit keinen Reader für diese Art von Daten auf PHP (7.2.1) und Sie müssten Ihren eigenen Parser
erstellen
Es ist traurig, dass Server und Clients dafür keine gute Unterstützung haben.
Nader Ghanbari
14

Ich weiß, dass diese Frage alt ist, aber in den letzten Tagen hatte ich das gesamte Web durchsucht, um dieselbe Frage zu lösen. Ich habe Grails REST Webservices und iPhone Client, die Bilder, Titel und Beschreibung senden.

Ich weiß nicht, ob mein Ansatz der beste ist, aber er ist so einfach und unkompliziert.

Ich mache ein Bild mit dem UIImagePickerController und sende die NSData mit den Header-Tags der Anfrage an den Server, um die Daten des Bildes zu senden.

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

Auf der Serverseite erhalte ich das Foto mit dem folgenden Code:

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

}

Ich weiß nicht, ob ich in Zukunft Probleme habe, aber jetzt funktioniert es gut in der Produktionsumgebung.

Rscorreia
quelle
1
Ich mag diese Option der Verwendung von http-Headern. Dies funktioniert besonders gut, wenn zwischen den Metadaten und den Standard-http-Headern eine gewisse Symmetrie besteht, Sie aber natürlich Ihre eigenen erfinden können.
EJ Campbell
14

Hier ist meine Ansatz-API (ich verwende ein Beispiel) - wie Sie sehen können, verwende ich keine file_id(auf den Server hochgeladene Dateikennung) in der API:

  1. Erstellen photoObjekt auf Server :

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. Datei hochladen (beachten Sie das file in Singularform, da es nur eine pro Foto gibt):

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

Und dann zum Beispiel:

  1. Fotoliste lesen

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. Lesen Sie einige Fotodetails

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

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

Die Schlussfolgerung ist also, dass Sie zuerst ein Objekt (Foto) per POST erstellen und dann eine zweite Anfrage mit der Datei senden (erneut POST).

Kamil Kiełczewski
quelle
3
Dies scheint der "ruhigere" Weg zu sein, um dies zu erreichen.
James Webster
POST-Operation für neu erstellte Ressourcen, muss Standort-ID in einfachen Versionsdetails des Objekts zurückgeben
Ivan Proskuryakov
@ivanproskuryakov warum "muss"? Im obigen Beispiel (POST in Punkt 2) ist die Datei-ID unbrauchbar. Zweites Argument (für POST in Punkt 2) Ich verwende die Singularform '/ file' (nicht '/ files'), daher wird keine ID benötigt, da der Pfad: / projects / 2 / photos / 3 / file der Identitätsfotodatei VOLLSTÄNDIGE Informationen gibt.
Kamil Kiełczewski
Aus der HTTP-Protokollspezifikation. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Erstellt "Auf die neu erstellte Ressource kann durch die in der Entität der Antwort zurückgegebenen URIs verwiesen werden, wobei der spezifischste URI für die Ressource von angegeben wird ein Standort-Header-Feld. " @ KamilKiełczewski (eins) und (zwei) können zu einer POST-Operation kombiniert werden. POST: / projects / {project_id} / photos Gibt den Standort-Header zurück, der für die GET-Operation für ein einzelnes Foto (Ressource *) verwendet werden kann Einzelfoto mit allen Details CGET: um alle Sammlung der Fotos zu bekommen
Ivan Proskuryakov
1
Wenn Metadaten und Upload separate Vorgänge sind, treten bei den Endpunkten folgende Probleme auf: Beim Hochladen von Dateien wird der POST-Vorgang verwendet - POST ist nicht idempotent. PUT (idempotent) muss verwendet werden, da Sie die Ressource ändern, ohne eine neue zu erstellen. REST arbeitet mit Objekten, die als Ressourcen bezeichnet werden . POST: "../photos/" PUT: "../photos/{photo_id}" GET: "../photos/" GET: "../photos/{photo_id}" PS. Das Trennen des Uploads in einen separaten Endpunkt kann zu unvorhergesehenem Verhalten führen. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov
6

FormData-Objekte: Dateien mit Ajax hochladen

XMLHttpRequest Level 2 bietet Unterstützung für die neue FormData-Schnittstelle. FormData-Objekte bieten eine Möglichkeit, auf einfache Weise eine Reihe von Schlüssel / Wert-Paaren zu erstellen, die Formularfelder und deren Werte darstellen. Diese können dann einfach mit der XMLHttpRequest-Methode send () gesendet werden.

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

Da das einzige fehlende Beispiel das ANDROID-Beispiel ist , werde ich es hinzufügen. Diese Technik verwendet eine benutzerdefinierte AsyncTask, die in Ihrer Aktivitätsklasse deklariert werden sollte.

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

}

Wenn Sie also Ihre Datei hochladen möchten, rufen Sie einfach an:

new UploadFile().execute();
lifeisfoo
quelle
Hallo, was ist AndroidMultiPartEntity, bitte erkläre ... und wenn ich eine PDF-, Word- oder XLS-Datei hochladen möchte, was ich tun muss, gib bitte eine Anleitung ... ich bin neu in diesem Bereich.
Amit Pandya
1
@amitpandya Ich habe den Code in einen generischen Datei-Upload geändert, damit jeder, der ihn liest, klarer wird
lifeisfoo
2

Ich wollte einige Zeichenfolgen an den Backend-Server senden. Ich habe json nicht mit mehrteiligen verwendet, ich habe Anforderungsparameter verwendet.

@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 würde so aussehen

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

Ich übergebe zwei Parameter (UUID und Typ) zusammen mit dem Hochladen der Datei. Hoffe, dies wird helfen, wer nicht die komplexen JSON-Daten zum Senden hat.

Aslam anwer
quelle
1

Sie können versuchen, die Bibliothek https://square.github.io/okhttp/ zu verwenden. Sie können den Anforderungshauptteil auf mehrteilig einstellen und dann die Datei- und JSON-Objekte wie folgt separat hinzufügen:

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

Bitte stellen Sie sicher, dass Sie folgenden Import haben. Natürlich andere Standardimporte

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