Posting File dan Data Terkait ke RESTful WebService lebih disukai sebagai JSON

757

Ini mungkin akan menjadi pertanyaan bodoh tapi saya mengalami salah satu malam itu. Dalam aplikasi saya sedang mengembangkan RESTful API dan kami ingin klien mengirim data sebagai JSON. Bagian dari aplikasi ini mengharuskan klien untuk mengunggah file (biasanya gambar) serta informasi tentang gambar.

Saya mengalami kesulitan melacak bagaimana ini terjadi dalam satu permintaan. Apakah mungkin untuk Base64 data file menjadi string JSON? Apakah saya perlu melakukan 2 posting ke server? Haruskah saya tidak menggunakan JSON untuk ini?

Sebagai catatan, kami menggunakan Grails di backend dan layanan ini diakses oleh klien seluler asli (iPhone, Android, dll), jika ada yang membuat perbedaan.

Gregg
sumber
1
Jadi, apa cara terbaik untuk melakukan ini?
James111
3
Kirim metadata dalam string kueri URL, alih-alih JSON.
jrc

Jawaban:

632

Saya mengajukan pertanyaan serupa di sini:

Bagaimana cara mengunggah file dengan metadata menggunakan layanan web REST?

Anda pada dasarnya memiliki tiga pilihan:

  1. Base64 menyandikan file, dengan mengorbankan peningkatan ukuran data sekitar 33%, dan menambahkan pemrosesan overhead di server dan klien untuk encoding / decoding.
  2. Kirim file terlebih dahulu dalam multipart/form-dataPOST, dan kembalikan ID ke klien. Klien kemudian mengirimkan metadata dengan ID, dan server menghubungkan kembali file dan metadata.
  3. Kirim metadata terlebih dahulu, dan kembalikan ID ke klien. Klien kemudian mengirim file dengan ID, dan server menghubungkan kembali file dan metadata.
Daniel T.
sumber
29
Jika saya memilih opsi 1, apakah saya hanya memasukkan konten Base64 di dalam string JSON? {file: '234JKFDS # $ @ # $ MFDDMS ....', name: 'somename' ...} Atau ada yang lebih dari itu?
Gregg
15
Gregg, persis seperti yang Anda katakan, Anda hanya akan memasukkannya sebagai properti, dan nilainya akan menjadi string yang disandikan base64. Ini mungkin metode yang paling mudah digunakan, tetapi mungkin tidak praktis tergantung pada ukuran file. Misalnya, untuk aplikasi kita, kita perlu mengirim gambar iPhone yang masing-masing 2-3 MB. Peningkatan 33% tidak dapat diterima. Jika Anda hanya mengirim gambar kecil 20KB, overhead itu mungkin lebih dapat diterima.
Daniel T.
19
Saya juga harus menyebutkan bahwa encoding / decoding base64 juga akan memakan waktu pemrosesan. Ini mungkin hal termudah untuk dilakukan, tetapi tentu saja bukan yang terbaik.
Daniel T.
8
json dengan base64? hmm .. aku berpikir tentang menempel ke multipart / form
Omnipresent
12
Mengapa ditolak untuk menggunakan multipart / formulir-data dalam satu permintaan?
1nstinct
107

Anda dapat mengirim file dan data ke dalam satu permintaan menggunakan tipe konten multipart / form-data :

Dalam banyak aplikasi, dimungkinkan bagi pengguna untuk disajikan dengan formulir. Pengguna akan mengisi formulir, termasuk informasi yang diketik, dihasilkan oleh input pengguna, atau termasuk dari file yang telah dipilih pengguna. Ketika formulir diisi, data dari formulir dikirim dari pengguna ke aplikasi penerima.

Definisi MultiPart / Formulir-Data berasal dari salah satu aplikasi ...

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

"multipart / formulir-data" berisi serangkaian bagian. Setiap bagian diharapkan mengandung header konten-disposisi [RFC 2183] di mana tipe disposisi adalah "form-data", dan di mana disposisi berisi parameter (nama) "nama", di mana nilai parameter itu adalah asli nama bidang dalam formulir. Misalnya, bagian mungkin berisi tajuk:

Content-Disposition: form-data; name = "user"

dengan nilai yang sesuai dengan entri bidang "pengguna".

Anda dapat memasukkan informasi file atau informasi bidang dalam setiap bagian di antara batas. Saya telah berhasil mengimplementasikan layanan RESTful yang mengharuskan pengguna untuk mengirimkan data dan formulir, dan multipart / formulir-data bekerja dengan sempurna. Layanan ini dibangun menggunakan Java / Spring, dan klien menggunakan C #, jadi sayangnya saya tidak punya contoh Grails untuk memberi Anda tentang cara mengatur layanan. Anda tidak perlu menggunakan JSON dalam hal ini karena setiap bagian "formulir-data" memberi Anda tempat untuk menentukan nama parameter dan nilainya.

Hal yang baik tentang menggunakan multipart / form-data adalah Anda menggunakan header yang didefinisikan HTTP, jadi Anda tetap berpegang pada filosofi REST untuk menggunakan alat HTTP yang ada untuk membuat layanan Anda.

McStretch
sumber
1
Terima kasih, tapi pertanyaan saya terfokus pada keinginan untuk menggunakan JSON untuk permintaan dan jika itu mungkin. Saya sudah tahu bahwa saya bisa mengirimkannya seperti yang Anda sarankan.
Gregg
15
Ya itu pada dasarnya tanggapan saya untuk "Haruskah saya tidak menggunakan JSON untuk ini?" Apakah ada alasan khusus mengapa Anda ingin klien menggunakan JSON?
McStretch
3
Kemungkinan besar persyaratan bisnis atau tetap dengan konsistensi. Tentu saja, hal yang ideal untuk dilakukan adalah menerima keduanya (formulir data dan respons JSON) berdasarkan header HTTP Content-Type.
Daniel T.
2
Memilih JSON menghasilkan kode yang jauh lebih elegan di sisi klien dan server, yang mengarah pada bug yang kurang potensial. Formulir data sangat kemarin.
superarts.org
5
Saya minta maaf atas apa yang saya katakan jika itu menyakiti perasaan pengembang .Net. Meskipun bahasa Inggris bukan bahasa ibu saya, itu bukan alasan yang sah bagi saya untuk mengatakan sesuatu yang kasar tentang teknologi itu sendiri. Menggunakan data formulir itu luar biasa dan jika Anda terus menggunakannya Anda juga akan lebih hebat!
superarts.org
53

Saya tahu bahwa utas ini cukup lama, namun, saya kehilangan satu opsi di sini. Jika Anda memiliki metadata (dalam format apa pun) yang ingin Anda kirim bersama dengan data yang akan diunggah, Anda dapat membuat satu multipart/relatedpermintaan.

Jenis media Multipart / Related ditujukan untuk objek gabungan yang terdiri dari beberapa bagian tubuh yang saling terkait.

Anda dapat memeriksa spesifikasi RFC 2387 untuk detail lebih mendalam.

Pada dasarnya setiap bagian dari permintaan semacam itu dapat memiliki konten dengan tipe yang berbeda dan semua bagian saling terkait (misalnya gambar dan metadata). Bagian-bagian diidentifikasi oleh string batas, dan string batas akhir diikuti oleh dua tanda hubung.

Contoh:

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
sumber
Saya menyukai solusi Anda yang terbaik sejauh ini. Sayangnya, tampaknya tidak ada cara untuk membuat permintaan mutlipart / terkait di browser.
Petr Baudis
apakah Anda memiliki pengalaman dalam mendapatkan klien untuk (terutama yang JS) untuk berkomunikasi dengan api dengan cara ini
pvgoddijn
Sayangnya, saat ini tidak ada pembaca untuk jenis data ini di php (7.2.1) dan Anda harus membuat parser Anda sendiri
dewd
Sangat menyedihkan bahwa server dan klien tidak memiliki dukungan yang baik untuk ini.
Nader Ghanbari
14

Saya tahu pertanyaan ini sudah lama, tetapi pada hari-hari terakhir saya telah mencari seluruh web untuk menyelesaikan pertanyaan yang sama. Saya memiliki layanan web REST grails dan Klien iPhone yang mengirim gambar, judul, dan deskripsi.

Saya tidak tahu apakah pendekatan saya adalah yang terbaik, tetapi sangat mudah dan sederhana.

Saya mengambil gambar menggunakan UIImagePickerController dan mengirim ke server NSData menggunakan tag header permintaan untuk mengirim data gambar.

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

Di sisi server, saya menerima foto menggunakan kode:

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

}

Saya tidak tahu apakah saya memiliki masalah di masa depan, tetapi sekarang bekerja dengan baik di lingkungan produksi.

Rscorreia
sumber
1
Saya suka opsi ini menggunakan header http. Ini berfungsi baik terutama ketika ada beberapa simetri antara metadata dan header http standar, tetapi Anda jelas dapat menciptakannya sendiri.
EJ Campbell
14

Ini API pendekatan saya (saya menggunakan contoh) - seperti yang Anda lihat, Anda saya tidak menggunakan file_id(pengidentifikasi file yang diunggah ke server) di API:

  1. Buat photoobjek di server:

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. Unggah file (catatan yang fileberbentuk tunggal karena hanya satu per foto):

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

Dan kemudian misalnya:

  1. Baca daftar foto

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. Baca beberapa detail foto

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

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

Jadi kesimpulannya adalah, pertama Anda membuat objek (foto) dengan POST, dan kemudian Anda mengirim permintaan kedua dengan file (lagi POST).

Kamil Kiełczewski
sumber
3
Ini sepertinya cara yang lebih 'RESTFUL' untuk mencapai ini.
James Webster
Operasi POST untuk sumber daya yang baru dibuat, harus mengembalikan id lokasi, dalam detail versi sederhana objek
Ivan Proskuryakov
@ivanproskuryakov mengapa "harus"? Pada contoh di atas (POST di poin 2) id file tidak berguna. Argumen kedua (untuk POST di poin 2) saya menggunakan bentuk tunggal '/ file' (bukan '/ file') sehingga ID tidak diperlukan karena jalur: / proyek / 2 / foto / 3 / file memberikan informasi LENGKAP ke file foto identitas.
Kamil Kiełczewski
Dari spesifikasi protokol HTTP. w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Dibuat "Sumber daya yang baru dibuat dapat dirujuk oleh URI yang dikembalikan dalam entitas respons, dengan URI paling spesifik untuk sumber daya yang diberikan oleh bidang header Lokasi. " @ KamilKiełczewski (satu) dan (dua) dapat digabungkan menjadi satu operasi POST POST: / proyek / {project_id} / foto Akan mengembalikan Anda header lokasi, yang dapat digunakan untuk DAPATKAN satu foto (sumber daya *) operasi DAPATKAN: untuk mendapatkan foto tunggal dengan semua detail CGET: untuk mendapatkan semua koleksi foto
Ivan Proskuryakov
1
Jika metadata dan unggah adalah operasi terpisah, maka titik akhir memiliki masalah ini: Untuk unggah file, operasi POST digunakan - POST tidak idempoten. PUT (idempotent) harus digunakan karena Anda mengubah sumber tanpa membuat yang baru. REST bekerja dengan objek yang disebut sumber daya . POST: "../photos/" PUT: "../photos/{photo_id}" GET: "../photos/" GET: "../photos/{photo_id}" PS. Memisahkan unggahan menjadi titik akhir yang terpisah dapat menyebabkan perilaku yang tidak terduga. restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov
6

Objek FormData: Unggah File Menggunakan Ajax

XMLHttpRequest Level 2 menambahkan dukungan untuk antarmuka FormData baru. Objek FormData menyediakan cara untuk dengan mudah membangun satu set pasangan kunci / nilai yang mewakili bidang formulir dan nilainya, yang kemudian dapat dengan mudah dikirim menggunakan metode 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
sumber
6

Karena satu-satunya contoh yang hilang adalah contoh ANDROID , saya akan menambahkannya. Teknik ini menggunakan AsyncTask khusus yang harus dideklarasikan di dalam kelas Activity Anda.

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

}

Jadi, ketika Anda ingin mengunggah file, panggil saja:

new UploadFile().execute();
lifeisfoo
sumber
Hai, apa itu AndroidMultiPartEntity tolong jelaskan ... dan jika saya ingin mengunggah file pdf, word, atau xls apa yang harus saya lakukan, tolong beri panduan ... saya baru dalam hal ini.
amit pandya
1
@amitpandya Saya telah mengubah kode untuk mengunggah file generik sehingga lebih jelas bagi siapa pun yang membacanya
lifeisfoo
2

Saya ingin mengirim beberapa string ke server backend. Saya tidak menggunakan json dengan multipart, saya telah menggunakan params permintaan.

@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 akan terlihat seperti

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

Saya melewati dua params (uuid dan tipe) bersama dengan unggahan file. Semoga ini akan membantu yang tidak memiliki data json kompleks untuk dikirim.

Aslam anwer
sumber
1

Anda dapat mencoba menggunakan perpustakaan https://square.github.io/okhttp/ . Anda dapat mengatur badan permintaan untuk mengalikan dan kemudian menambahkan file dan objek json secara terpisah seperti:

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

Harap pastikan bahwa Anda telah mengikuti impor. Tentu impor standar lainnya

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