Bagaimana cara mengunggah file dengan metadata menggunakan layanan web REST?

249

Saya memiliki layanan web REST yang saat ini memaparkan URL ini:

http: // server / data / media

di mana pengguna dapat POSTmengikuti JSON:

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

untuk membuat metadata Media baru.

Sekarang saya membutuhkan kemampuan untuk mengunggah file pada saat yang sama dengan metadata media. Apa cara terbaik untuk melakukan ini? Saya bisa memperkenalkan properti baru yang disebut filedan base64 menyandikan file, tetapi saya bertanya-tanya apakah ada cara yang lebih baik.

Ada juga yang menggunakan multipart/form-dataseperti apa bentuk HTML akan mengirim, tapi saya menggunakan layanan web REST dan saya ingin tetap menggunakan JSON jika memungkinkan.

Daniel T.
sumber
35
Tetap menggunakan JSON saja tidak benar-benar harus memiliki layanan web yang tenang. REST pada dasarnya adalah segala sesuatu yang mengikuti prinsip-prinsip utama metode HTTP dan beberapa aturan lain (yang bisa dibilang tidak terstandarisasi).
Erik Kaplun

Jawaban:

192

Saya setuju dengan Greg bahwa pendekatan dua fase adalah solusi yang masuk akal, namun saya akan melakukannya sebaliknya. Saya akan lakukan:

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

Untuk membuat entri metadata dan mengembalikan respons seperti:

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

Klien kemudian dapat menggunakan ContentUrl ini dan melakukan PUT dengan data file.

Yang menyenangkan tentang pendekatan ini adalah ketika server Anda mulai terbebani dengan volume data yang sangat besar, url yang Anda kembalikan hanya dapat menunjuk ke beberapa server lain dengan lebih banyak ruang / kapasitas. Atau Anda bisa menerapkan semacam pendekatan round robin jika bandwidth merupakan masalah.

Darrel Miller
sumber
8
Satu keuntungan untuk mengirim konten pertama adalah bahwa pada saat metadata ada, konten sudah ada. Pada akhirnya jawaban yang benar tergantung pada organisasi data dalam sistem.
Greg Hewgill
Terima kasih, saya menandai ini sebagai jawaban yang benar karena inilah yang ingin saya lakukan. Sayangnya, karena aturan bisnis yang aneh, kami harus mengizinkan pengunggahan terjadi dalam urutan apa pun (metadata lebih dulu atau arsipkan lebih dulu). Saya bertanya-tanya apakah ada cara untuk menggabungkan keduanya untuk menghemat sakit kepala berurusan dengan kedua situasi.
Daniel T.
@ Danielel Jika Anda POST file data pertama, maka Anda dapat mengambil URL kembali di Lokasi dan menambahkannya ke atribut ContentUrl di metadata. Dengan begitu, ketika server menerima metadata, jika ContentUrl ada maka ia sudah tahu di mana file tersebut berada. Jika tidak ada ContentUrl, maka ia tahu bahwa ia harus membuatnya.
Darrel Miller
jika Anda melakukan POST terlebih dahulu, apakah Anda akan memposting ke URL yang sama? (/ server / data / media) atau akankah Anda membuat titik masuk lain untuk unggahan file-pertama?
Matt Brailsford
1
@ Jauh Bagaimana jika metadata menyertakan jumlah "suka" dari suatu gambar? Apakah Anda akan memperlakukannya sebagai satu sumber saja? Atau yang lebih jelas, apakah Anda menyarankan bahwa jika saya ingin mengedit deskripsi suatu gambar, saya perlu mengunggah ulang gambar tersebut? Ada banyak kasus di mana bentuk multi-bagian adalah solusi yang tepat. Tidak selalu demikian.
Darrel Miller
103

Hanya karena Anda tidak membungkus seluruh badan permintaan di JSON, tidak berarti tidak RESTful digunakan multipart/form-datauntuk memposting JSON dan file dalam satu permintaan:

curl -F "metadata=<metadata.json" -F "[email protected]" http://example.com/add-file

di sisi server (menggunakan Python untuk pseudocode):

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

untuk mengunggah banyak file, dimungkinkan untuk menggunakan "kolom formulir" yang terpisah untuk masing-masing:

curl -F "metadata=<metadata.json" -F "[email protected]" -F "[email protected]" http://example.com/add-file

... dalam hal ini kode server akan memiliki request.args['file1'][0]danrequest.args['file2'][0]

atau menggunakan kembali yang sama untuk banyak orang:

curl -F "metadata=<metadata.json" -F "[email protected]" -F "[email protected]" http://example.com/add-file

... dalam hal request.args['files']ini hanya akan menjadi daftar panjang 2.

atau melewati beberapa file melalui satu bidang:

curl -F "metadata=<metadata.json" -F "[email protected],some-other-file.tar.gz" http://example.com/add-file

...dalam hal ini request.args['files'] akan menjadi string yang berisi semua file, yang Anda harus mengurai sendiri - tidak yakin bagaimana melakukannya, tapi saya yakin itu tidak sulit, atau lebih baik gunakan saja pendekatan sebelumnya.

Perbedaan antara @dan <adalah yang @menyebabkan file untuk dilampirkan sebagai unggahan file, sedangkan <melampirkan konten file sebagai bidang teks.

PS Hanya karena saya menggunakan curlsebagai cara untuk menghasilkan POSTpermintaan tidak berarti permintaan HTTP yang sama persis tidak dapat dikirim dari bahasa pemrograman seperti Python atau menggunakan alat yang cukup mampu.

Erik Kaplun
sumber
4
Saya sendiri bertanya-tanya tentang pendekatan ini, dan mengapa saya belum melihat orang lain melakukannya. Saya setuju, sepertinya TETAP bagi saya.
soupdog
1
IYA! Ini adalah pendekatan yang sangat praktis, dan tidak kalah tenang daripada menggunakan "application / json" sebagai tipe konten untuk seluruh permintaan.
sickill
..tapi itu hanya mungkin jika Anda memiliki data dalam file .json dan mengunggahnya, yang tidak terjadi
itsjavi
5
@mjolnic komentar Anda tidak relevan: contoh CURL hanya, yah, contoh ; jawabannya secara eksplisit menyatakan bahwa Anda dapat menggunakan apa saja untuk mengirim permintaan ... juga, apa yang mencegah Anda dari hanya menulis curl -f 'metadata={"foo": "bar"}'?
Erik Kaplun
3
Saya menggunakan pendekatan ini karena jawaban yang diterima tidak akan berfungsi untuk aplikasi yang saya kembangkan (file tidak dapat ada sebelum data dan menambah kompleksitas yang tidak perlu untuk menangani kasus di mana data diunggah terlebih dahulu dan file tidak pernah diunggah) .
BitsEvolved
33

Salah satu cara untuk mendekati masalah adalah membuat proses unggah menjadi dua fase. Pertama, Anda akan mengunggah file itu sendiri menggunakan POST, di mana server mengembalikan beberapa pengenal kembali ke klien (pengidentifikasi mungkin SHA1 dari konten file). Kemudian, permintaan kedua mengaitkan metadata dengan data file:

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

Termasuk data base64 yang disandikan ke dalam permintaan JSON itu sendiri akan meningkatkan ukuran data yang ditransfer sebesar 33%. Ini mungkin atau mungkin tidak penting tergantung pada ukuran keseluruhan file.

Pendekatan lain mungkin menggunakan POST dari data file mentah, tetapi sertakan metadata apa pun di header permintaan HTTP. Namun, ini sedikit di luar operasi REST dasar dan mungkin lebih canggung untuk beberapa perpustakaan klien HTTP.

Greg Hewgill
sumber
Anda dapat menggunakan Ascii85 meningkat hanya dengan 1/4.
Singagirl
Adakah referensi mengapa base64 meningkatkan ukuran sebanyak itu?
jam01
1
@ jam01: Secara kebetulan, saya baru saja melihat sesuatu kemarin yang menjawab pertanyaan ruang dengan baik: Berapa overhead ruang pengkodean Base64?
Greg Hewgill
10

Saya menyadari ini adalah pertanyaan yang sangat lama, tetapi mudah-mudahan ini akan membantu orang lain ketika saya menemukan posting ini mencari hal yang sama. Saya memiliki masalah serupa, hanya saja metadata saya adalah Guid dan int. Solusinya sama saja. Anda bisa menjadikan metadata yang dibutuhkan sebagai bagian dari URL.

Metode penerimaan POST di kelas "Controller" Anda:

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

Kemudian dalam apa pun yang Anda daftarkan rute, WebApiConfig.Register (konfigurasi HttpConfiguration) untuk saya dalam kasus ini.

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

Jika file Anda dan metadata-nya menciptakan satu sumber daya, tidak apa-apa untuk mengunggah keduanya dalam satu permintaan. Permintaan sampel akan:

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--
Mike Ezzati
sumber
3

Saya tidak mengerti mengapa, selama delapan tahun, tidak ada yang memposting jawaban yang mudah. Daripada mengkodekan file sebagai base64, mengkodekan json sebagai string. Kemudian, cukup decode json di sisi server.

Dalam Javascript:

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

POST menggunakan Content-Type: multipart / form-data

Di sisi server, ambil file secara normal, dan ambil json sebagai string. Konversikan string ke objek, yang biasanya satu baris kode apa pun bahasa pemrograman yang Anda gunakan.

(Ya, itu bekerja dengan baik. Melakukannya di salah satu aplikasi saya.)

ccleve
sumber