Wysyłanie pliku i powiązanych danych do usługi RESTful WebService najlepiej jako JSON

757

To prawdopodobnie będzie głupie pytanie, ale mam jedną z tych nocy. W aplikacji tworzę RESTful API i chcemy, aby klient wysyłał dane jako JSON. Część tej aplikacji wymaga od klienta przesłania pliku (zwykle obrazu) oraz informacji o obrazie.

Trudno mi wyśledzić, jak to się dzieje w jednym żądaniu. Czy możliwe jest Base64 danych pliku w ciągu JSON? Czy będę musiał wykonać 2 posty na serwerze? Czy nie powinienem do tego używać JSON?

Na marginesie, używamy Grails na backendie i do tych usług mają dostęp natywni klienci mobilni (iPhone, Android itp.), Jeśli coś z tego robi różnicę.

Gregg
źródło
1
Więc jaki jest najlepszy sposób, aby to zrobić?
James111,
3
Wyślij metadane w ciągu zapytania URL zamiast JSON.
jrc

Odpowiedzi:

632

Zadałem tutaj podobne pytanie:

Jak przesłać plik z metadanymi przy użyciu usługi sieci Web REST?

Zasadniczo masz trzy możliwości:

  1. Base64 koduje plik kosztem zwiększenia rozmiaru danych o około 33% i dodaje koszty przetwarzania zarówno na serwerze, jak i kliencie do kodowania / dekodowania.
  2. Najpierw wyślij plik w multipart/form-dataPOST i zwróć identyfikator klientowi. Następnie klient wysyła metadane z identyfikatorem, a serwer ponownie kojarzy plik i metadane.
  3. Najpierw wyślij metadane i zwróć identyfikator klientowi. Następnie klient wysyła plik o identyfikatorze, a serwer ponownie kojarzy plik i metadane.
Daniel T.
źródło
29
Jeśli wybiorę opcję 1, czy po prostu dołączę zawartość Base64 do ciągu JSON? {file: '234JKFDS # $ @ # $ MFDDMS ....', name: 'somename' ...} Czy jest coś jeszcze?
Gregg,
15
Gregg, dokładnie tak jak powiedziałeś, po prostu umieściłbyś go jako właściwość, a wartością byłby ciąg zakodowany w base64. Jest to prawdopodobnie najłatwiejsza metoda, ale może nie być praktyczna w zależności od rozmiaru pliku. Na przykład w przypadku naszej aplikacji musimy wysłać obrazy iPhone'a o wielkości 2-3 MB każdy. Wzrost o 33% jest niedopuszczalny. Jeśli wysyłasz tylko małe obrazy o wielkości 20 KB, ten narzut może być bardziej do zaakceptowania.
Daniel T.
19
Powinienem również wspomnieć, że kodowanie / dekodowanie base64 również zajmie trochę czasu. To może być najłatwiejsza rzecz, ale z pewnością nie jest najlepsza.
Daniel T.
8
json z base64? hmm .. Zastanawiam się nad przyklejeniem formy wieloczęściowej / formy
Wszechobecny
12
Dlaczego odmawia się używania danych wieloczęściowych / formularzy w jednym żądaniu?
1nstinct
107

Możesz wysłać plik i dane w jednym żądaniu, używając typu zawartości wieloczęściowej / formularza :

W wielu aplikacjach użytkownik może otrzymać formularz. Użytkownik wypełni formularz, w tym informacje wpisane, wygenerowane na podstawie danych wprowadzonych przez użytkownika lub zawarte w plikach wybranych przez użytkownika. Po wypełnieniu formularza dane z formularza są wysyłane od użytkownika do aplikacji odbierającej.

Definicja MultiPart / Form-Data pochodzi od jednej z tych aplikacji ...

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

„multipart / form-data” zawiera serię części. Oczekuje się, że każda część będzie zawierała nagłówek dyspozycji-zawartości [RFC 2183], w którym typ dyspozycji to „dane formularza” i gdzie dyspozycja zawiera (dodatkowy) parametr „nazwa”, gdzie wartość tego parametru jest oryginalna nazwa pola w formularzu. Na przykład część może zawierać nagłówek:

Dyspozycja treści: dane formularza; name = „user”

z wartością odpowiadającą wpisowi w polu „użytkownik”.

Możesz zawrzeć informacje o pliku lub informacje o polu w każdej sekcji między granicami. Z powodzeniem wdrożyłem usługę RESTful, która wymagała od użytkownika przesłania zarówno danych, jak i formularza, a dane wieloczęściowe / dane działały idealnie. Usługa została zbudowana przy użyciu Java / Spring, a klient korzystał z C #, więc niestety nie mam żadnych przykładów Grails, które mogłyby dać ci informacje na temat konfiguracji usługi. W tym przypadku nie musisz używać JSON, ponieważ każda sekcja „dane formularza” zapewnia miejsce do określenia nazwy parametru i jego wartości.

Dobrą rzeczą w korzystaniu z danych wieloczęściowych / formularzy jest to, że używasz nagłówków zdefiniowanych w HTTP, więc trzymasz się filozofii REST polegającej na używaniu istniejących narzędzi HTTP do tworzenia usługi.

McStretch
źródło
1
Dzięki, ale moje pytanie koncentrowało się na chęci użycia JSON do żądania i czy to było możliwe. Wiem już, że mogę wysłać to tak, jak sugerujesz.
Gregg,
15
Tak, to w zasadzie moja odpowiedź na „Czy nie powinienem do tego używać JSON?” Czy istnieje konkretny powód, dla którego chcesz, aby klient korzystał z JSON?
McStretch,
3
Najprawdopodobniej wymaganie biznesowe lub zachowanie spójności. Oczywiście idealną rzeczą do zrobienia jest zaakceptowanie zarówno (danych formularza, jak i odpowiedzi JSON) na podstawie nagłówka HTTP Content-Type.
Daniel T.
2
Wybór JSON skutkuje znacznie bardziej eleganckim kodem zarówno po stronie klienta, jak i serwera, co prowadzi do mniejszej liczby potencjalnych błędów. Dane formularzy są takie wczoraj.
superarts.org
5
Przepraszam za to, co powiedziałem, jeśli zaszkodziło to poczuciu programisty .Net. Chociaż angielski nie jest moim językiem ojczystym, nie jest to dla mnie uzasadniona wymówka, by powiedzieć coś niegrzecznego na temat samej technologii. Korzystanie z danych formularza jest niesamowite, a jeśli będziesz go nadal używać, będziesz jeszcze bardziej niesamowity!
superarts.org,
53

Wiem, że ten wątek jest dość stary, brakuje mi jednak jednej opcji. Jeśli masz metadane (w dowolnym formacie), które chcesz wysłać wraz z danymi do przesłania, możesz złożyć pojedyncze multipart/relatedżądanie.

Typ nośnika Multipart / Related jest przeznaczony do obiektów złożonych składających się z kilku powiązanych ze sobą części ciała.

Możesz sprawdzić specyfikację RFC 2387, aby uzyskać bardziej szczegółowe informacje.

Zasadniczo każda część takiego żądania może zawierać treść różnego typu, a wszystkie części są w jakiś sposób powiązane (np. Obraz i jego metadane). Części są identyfikowane za pomocą ciągu granicznego, a po ostatnim ciągu granicznym występują dwa myślniki.

Przykład:

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
źródło
Zdecydowanie najbardziej podobało mi się twoje rozwiązanie. Niestety wydaje się, że nie ma możliwości tworzenia żądań mutlipart / powiązanych w przeglądarce.
Petr Baudis
czy masz jakieś doświadczenie w zachęcaniu klientów (szczególnie JS) do komunikowania się w ten sposób z interfejsem API
pvgoddijn
niestety obecnie nie ma czytnika dla tego rodzaju danych na php (7.2.1) i musiałbyś zbudować swój własny parser
dewd
Szkoda, że ​​serwery i klienci nie mają na to dobrego wsparcia.
Nader Ghanbari
14

Wiem, że to pytanie jest stare, ale w ostatnich dniach szukałem w całej sieci rozwiązania tego samego pytania. Mam serwisy internetowe REST grails i klienta iPhone, które wysyłają zdjęcia, tytuł i opis.

Nie wiem, czy moje podejście jest najlepsze, ale jest takie łatwe i proste.

Robię zdjęcie za pomocą UIImagePickerController i przesyłam do serwera NSData za pomocą tagów nagłówka żądania wysłania danych obrazu.

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

Po stronie serwera otrzymuję zdjęcie za pomocą kodu:

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

}

Nie wiem, czy mam problemy w przyszłości, ale teraz działa dobrze w środowisku produkcyjnym.

Rscorreia
źródło
1
Podoba mi się ta opcja używania nagłówków http. Działa to szczególnie dobrze, gdy istnieje pewna symetria między metadanymi a standardowymi nagłówkami http, ale oczywiście możesz wymyślić własne.
EJ Campbell
14

Oto moje podejście API (korzystam z przykładu) - jak widać, nie używam żadnego file_id(przesłanego identyfikatora pliku na serwer) w API:

  1. Utwórz photoobiekt na serwerze:

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. Prześlij plik (uwaga, że filejest w formie pojedynczej, ponieważ jest tylko jeden na zdjęcie):

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

A potem na przykład:

  1. Przeczytaj listę zdjęć

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. Przeczytaj szczegóły zdjęcia

    GET: /projects/{project_id}/photos/{photo_id}
    response: { id: 666, name: 'some_schema.jpg', comment:'blah'} (photo object)
  3. Przeczytaj plik ze zdjęciem

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

Wniosek jest taki, że najpierw tworzysz obiekt (zdjęcie) za pomocą POST, a następnie wysyłasz drugie żądanie z plikiem (ponownie POST).

Kamil Kiełczewski
źródło
3
Wydaje się, że jest to „RESTFULOWANY” sposób na osiągnięcie tego.
James Webster
Operacja POST dla nowo utworzonych zasobów musi zwrócić identyfikator lokalizacji, w prostych szczegółach wersji obiektu
Ivan Proskuryakov
@ivanproskuryakov dlaczego „musi”? W powyższym przykładzie (POST w punkcie 2) identyfikator pliku jest bezużyteczny. Drugi argument (dla POST w punkcie 2) używam liczby pojedynczej „/ file” (nie „/ files”), więc identyfikator nie jest potrzebny, ponieważ ścieżka: / projects / 2 / photos / 3 / file podaje PEŁNE informacje do pliku zdjęcia tożsamości.
Kamil Kiełczewski
Ze specyfikacji protokołu HTTP. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Utworzono „Do nowo utworzonego zasobu można odwoływać się do identyfikatorów URI zwróconych w encji odpowiedzi, z najbardziej szczegółowym identyfikatorem URI dla zasobu podanym przez pole nagłówka lokalizacji. ” @ KamilKiełczewski (jeden) i (dwa) mogą być połączone w jedną operację POST POST: / projects / {project_id} / photos Zwróci ci nagłówek lokalizacji, który może być użyty do operacji GET single photo (resource *) GET: aby uzyskać jedno zdjęcie ze wszystkimi szczegółami CGET: aby uzyskać całą kolekcję zdjęć
Ivan Proskuryakov
1
Jeśli metadane i przesyłanie są oddzielnymi operacjami, punkty końcowe mają następujące problemy: W przypadku przesyłania plików użyto operacji POST - POST nie jest idempotentny. Należy użyć PUT (idempotent), ponieważ zmieniasz zasób bez tworzenia nowego. REST działa z obiektami zwanymi zasobami . POST: „../photos/„ PUT: „../photos/{photo_id}” GET: „../photos/„ GET: „../photos/{photo_id}” PS. Rozdzielenie przesyłania na osobny punkt końcowy może prowadzić do nieprzewidzianego zachowania. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov
6

Obiekty FormData: przesyłanie plików za pomocą Ajax

XMLHttpRequest poziom 2 dodaje obsługę nowego interfejsu FormData. Obiekty FormData umożliwiają łatwe tworzenie zestawu par klucz / wartość reprezentujących pola formularza i ich wartości, które można następnie łatwo przesłać za pomocą metody 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

lakhan_Ideavate
źródło
6

Ponieważ jedynym brakującym przykładem jest ANDROID , dodam go. W tej technice użyto niestandardowego zadania AsyncTask, które powinno być zadeklarowane w klasie aktywności.

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

}

Jeśli chcesz przesłać plik, zadzwoń:

new UploadFile().execute();
lifeisfoo
źródło
Cześć, co to jest AndroidMultiPartEntity, proszę wyjaśnić ... a jeśli chcę przesłać plik pdf, słowo lub plik xls, co muszę zrobić, proszę podać kilka wskazówek ... jestem nowy w tym.
amit pandya
1
@amitpandya Zmieniłem kod na ogólny przesyłanie plików, więc jest bardziej zrozumiały dla każdego, kto go czyta
lifeisfoo
2

Chciałem wysłać kilka ciągów do serwera zaplecza. Nie używałem jsona z wieloma częściami, użyłem parametrów żądania.

@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 wyglądałby

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

Mijam dwa parametry (identyfikator UUID i typ) wraz z przesyłaniem pliku. Mam nadzieję, że pomoże to tym, którzy nie mają skomplikowanych danych Json do wysłania.

Aslam anwer
źródło
1

Możesz spróbować użyć biblioteki https://square.github.io/okhttp/ . Możesz ustawić treść żądania na wieloczęściową, a następnie osobno dodać plik i obiekty json:

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
źródło
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
źródło
-5

Upewnij się, że masz następujący import. Oczywiście inne standardowe przywozy

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
źródło
1
To dostajejava.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String
Mariano Ruiz