ฉันจะอัพโหลดไฟล์ที่มีเมทาดาทาได้อย่างไรโดยใช้บริการเว็บ REST


250

ฉันมีบริการเว็บ REST ที่เปิดเผย URL นี้ในปัจจุบัน:

http: // เซิร์ฟเวอร์ / ข้อมูล / สื่อ

ที่ผู้ใช้สามารถPOSTJSON ต่อไปนี้:

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

เพื่อสร้างข้อมูลเมตาสื่อใหม่

ตอนนี้ฉันต้องการความสามารถในการอัปโหลดไฟล์ในเวลาเดียวกันกับข้อมูลเมตาของสื่อ อะไรคือวิธีที่ดีที่สุดในการทำสิ่งนี้? ฉันสามารถแนะนำคุณสมบัติใหม่ที่เรียกว่าfileและ base64 เข้ารหัสไฟล์ แต่ฉันสงสัยว่าถ้ามีวิธีที่ดีกว่า

นอกจากนี้ยังมีการใช้multipart/form-dataสิ่งที่ฟอร์ม HTML จะส่ง แต่ฉันใช้บริการเว็บ REST และฉันต้องการใช้ JSON ถ้าเป็นไปได้


36
การใช้ JSON อย่างเดียวไม่จำเป็นต้องใช้เว็บเซอร์วิส RESTful REST นั้นเป็นเพียงสิ่งใดก็ตามที่เป็นไปตามหลักการสำคัญของวิธีการ HTTP และกฎอื่น ๆ (ที่ไม่ได้มาตรฐาน)
Erik Kaplun

คำตอบ:


192

ฉันเห็นด้วยกับ Greg ว่าวิธีการสองเฟสนั้นเป็นวิธีแก้ปัญหาที่สมเหตุสมผล แต่ฉันจะทำในลักษณะอื่น ฉันจะทำ:

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

หากต้องการสร้างรายการเมทาดาทาและส่งคืนการตอบกลับเช่น:

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

จากนั้นไคลเอ็นต์สามารถใช้ ContentUrl นี้และทำ PUT กับข้อมูลไฟล์

สิ่งที่ดีเกี่ยวกับวิธีนี้คือเมื่อเซิร์ฟเวอร์ของคุณเริ่มรับภาระข้อมูลจำนวนมหาศาล URL ที่คุณส่งคืนสามารถชี้ไปที่เซิร์ฟเวอร์อื่นที่มีพื้นที่ / ความจุมากขึ้น หรือคุณสามารถใช้วิธีการปัดเศษแบบกลมบางอย่างถ้าแบนด์วิดท์เป็นปัญหา


8
ข้อดีอย่างหนึ่งของการส่งเนื้อหาก่อนคือเมื่อถึงเวลาที่ข้อมูลเมตามีอยู่เนื้อหานั้นก็มีอยู่แล้ว ท้ายที่สุดคำตอบที่ถูกต้องขึ้นอยู่กับองค์กรของข้อมูลในระบบ
Greg Hewgill

ขอบคุณฉันทำเครื่องหมายว่านี่เป็นคำตอบที่ถูกต้องเพราะนี่คือสิ่งที่ฉันต้องการจะทำ น่าเสียดายเนื่องจากกฎธุรกิจแปลก ๆ เราต้องอนุญาตให้อัปโหลดเกิดขึ้นในลำดับใด ๆ (ข้อมูลเมตาแรกหรือไฟล์แรก) ฉันสงสัยว่าจะมีวิธีการรวมสองอย่างนี้เข้าด้วยกันเพื่อช่วยให้ปวดหัวในการรับมือกับทั้งสองสถานการณ์หรือไม่
Daniel T.

@Daniel หากคุณโพสต์ไฟล์ข้อมูลก่อนคุณสามารถนำ URL ที่ส่งคืนมาในตำแหน่งและเพิ่มลงในแอตทริบิวต์ ContentUrl ในข้อมูลเมตา ด้วยวิธีนี้เมื่อเซิร์ฟเวอร์ได้รับข้อมูลเมตาหากมี ContentUrl อยู่แล้วจะรู้ว่าไฟล์อยู่ที่ไหน หากไม่มี ContentUrl ก็จะรู้ว่าควรสร้างใหม่
Darrel Miller

หากคุณต้องทำ POST ก่อนคุณจะโพสต์ไปที่ URL เดียวกันได้หรือไม่? (/ เซิร์ฟเวอร์ / ข้อมูล / สื่อ) หรือคุณจะสร้างจุดเริ่มต้นอื่นสำหรับการอัพโหลดไฟล์ครั้งแรก?
Matt Brailsford

1
@Faraway จะเกิดอะไรขึ้นถ้าเมทาดาทามีจำนวน "ไลค์" ของรูปภาพอยู่ด้วย คุณจะถือว่าเป็นทรัพยากรเดียวหรือไม่ คุณแนะนำว่าถ้าฉันต้องการแก้ไขคำอธิบายของรูปภาพฉันจะต้องอัปโหลดรูปภาพอีกครั้งหรือไม่ มีหลายกรณีที่รูปแบบหลายส่วนเป็นทางออกที่ถูกต้อง มันไม่ได้เป็นเช่นนั้นเสมอไป
Darrel Miller

104

เพียงเพราะคุณไม่ได้ห่อเนื้อหาคำขอทั้งหมดไว้ใน JSON ไม่ได้หมายความว่าไม่เหมาะที่จะใช้multipart/form-dataโพสต์ทั้ง JSON และไฟล์ในคำขอเดียว:

curl -F "metadata=<metadata.json" -F "file=@my-file.tar.gz" http://example.com/add-file

ทางฝั่งเซิร์ฟเวอร์ (ใช้ Python สำหรับ pseudocode):

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

หากต้องการอัปโหลดหลายไฟล์เป็นไปได้ที่จะใช้ "เขตข้อมูลฟอร์ม" แยกสำหรับแต่ละไฟล์

curl -F "metadata=<metadata.json" -F "file1=@some-file.tar.gz" -F "file2=@some-other-file.tar.gz" http://example.com/add-file

... ซึ่งในกรณีนี้รหัสเซิร์ฟเวอร์จะมีrequest.args['file1'][0]และrequest.args['file2'][0]

หรือนำมาใช้ซ้ำสำหรับหลาย ๆ คน:

curl -F "metadata=<metadata.json" -F "files=@some-file.tar.gz" -F "files=@some-other-file.tar.gz" http://example.com/add-file

... ซึ่งในกรณีนี้request.args['files']จะเป็นรายการความยาว 2

หรือส่งหลายไฟล์ผ่านฟิลด์เดียว:

curl -F "metadata=<metadata.json" -F "files=@some-file.tar.gz,some-other-file.tar.gz" http://example.com/add-file

... ซึ่งในกรณีนี้request.args['files']จะเป็นสตริงที่มีไฟล์ทั้งหมดซึ่งคุณจะต้องแยกวิเคราะห์ตัวเอง - ไม่แน่ใจว่าจะทำอย่างไร แต่ฉันแน่ใจว่ามันไม่ยากหรือดีกว่าเพียงใช้วิธีการก่อนหน้านี้

ความแตกต่างระหว่าง@และ<คือ@สาเหตุที่ทำให้ไฟล์แนบในขณะอัปโหลดไฟล์ในขณะที่<แนบเนื้อหาของไฟล์เป็นฟิลด์ข้อความ

PSเพียงเพราะฉันใช้curlเป็นวิธีในการสร้างPOSTคำขอไม่ได้หมายความว่าคำขอ HTTP ที่เหมือนกันไม่สามารถส่งจากภาษาการเขียนโปรแกรมเช่น Python หรือใช้เครื่องมือที่มีความสามารถเพียงพอ


4
ฉันสงสัยเกี่ยวกับวิธีการนี้ด้วยตนเองและทำไมฉันไม่เห็นคนอื่นเอามาเลย ฉันเห็นด้วยดูเหมือนว่าสมบูรณ์แบบสำหรับฉัน
soupdog

1
ใช่! นี่เป็นวิธีการที่ใช้งานได้จริงและมันก็ไม่ได้สงบไปกว่าการใช้ "application / json" เป็นประเภทเนื้อหาสำหรับคำขอทั้งหมด
sickill

.. แต่เป็นไปได้ก็ต่อเมื่อคุณมีข้อมูลในไฟล์. json และอัปโหลดซึ่งไม่ใช่กรณี
itsjavi

5
@mjolnic ความคิดเห็นของคุณไม่เกี่ยวข้อง: ตัวอย่าง cURL เป็นเพียงแค่ดีตัวอย่าง ; คำตอบระบุไว้อย่างชัดเจนว่าคุณสามารถใช้อะไรก็ได้เพื่อส่งคำขอ ... นอกจากนี้สิ่งที่ป้องกันไม่ให้คุณเขียนเพียงcurl -f 'metadata={"foo": "bar"}'?
Erik Kaplun

3
ฉันใช้วิธีการนี้เนื่องจากคำตอบที่ยอมรับแล้วจะไม่ทำงานสำหรับแอปพลิเคชันที่ฉันกำลังพัฒนา (ไฟล์ไม่สามารถอยู่ก่อนหน้าข้อมูลได้และจะเพิ่มความซับซ้อนที่ไม่จำเป็นในการจัดการกรณีที่มีการอัปโหลดข้อมูลก่อนและไม่เคยอัพโหลดไฟล์) .
BitsEvolved

33

วิธีหนึ่งในการแก้ไขปัญหาคือการทำให้กระบวนการอัปโหลดสองเฟส ขั้นแรกคุณจะอัปโหลดไฟล์เองโดยใช้ POST โดยที่เซิร์ฟเวอร์จะส่งคืนตัวบ่งชี้บางตัวกลับไปยังไคลเอนต์ (ตัวระบุอาจเป็น SHA1 ของเนื้อหาไฟล์) จากนั้นคำขอที่สองจะเชื่อมโยงข้อมูลเมตากับข้อมูลไฟล์:

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

การรวมไฟล์ข้อมูล base64 ที่เข้ารหัสลงในคำขอ JSON นั้นจะเพิ่มขนาดของข้อมูลที่ถ่ายโอน 33% สิ่งนี้อาจมีหรือไม่มีความสำคัญขึ้นอยู่กับขนาดโดยรวมของไฟล์

อีกวิธีหนึ่งอาจใช้ POST ของข้อมูลไฟล์ raw แต่รวมข้อมูลเมตาใด ๆ ในส่วนหัวคำขอ HTTP อย่างไรก็ตามสิ่งนี้อยู่นอกการดำเนินการ REST ขั้นพื้นฐานเล็กน้อยและอาจไม่สะดวกสำหรับบางไลบรารีไคลเอ็นต์ HTTP


คุณสามารถใช้ Ascii85 เพิ่มขึ้นเพียง 1/4
Singagirl

การอ้างอิงใด ๆ ว่าเพราะเหตุใด base64 จึงเพิ่มขนาดที่มากขึ้น?
jam01

1
@ jam01: บังเอิญฉันเพิ่งเห็นบางสิ่งเมื่อวานนี้ซึ่งตอบคำถามพื้นที่ได้ดี: อะไรคือค่าใช้จ่ายด้านบนของการเข้ารหัส Base64 คืออะไร
เกร็ก

10

ฉันรู้ว่านี่เป็นคำถามที่เก่ามาก แต่หวังว่านี่จะช่วยให้คนอื่นเห็นเมื่อฉันเข้ามาที่โพสต์นี้เพื่อค้นหาสิ่งเดียวกัน ฉันมีปัญหาที่คล้ายกันเพียงว่าข้อมูลเมตาของฉันเป็น Guid และ int วิธีการแก้ปัญหาเหมือนกันว่า คุณสามารถสร้างเมทาดาทาที่ต้องการใน URL ได้

วิธีการยอมรับ POST ในคลาส "Controller" ของคุณ:

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

จากนั้นในสิ่งที่คุณกำลังลงทะเบียนเส้นทาง WebApiConfig.Register (HttpConfiguration config) สำหรับฉันในกรณีนี้

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

6

หากไฟล์และข้อมูลเมตาของคุณสร้างหนึ่งแหล่งข้อมูลก็เป็นเรื่องที่สมบูรณ์แบบที่จะอัปโหลดทั้งสองอย่างในคำขอเดียว คำขอตัวอย่างจะเป็น:

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

3

ฉันไม่เข้าใจว่าทำไมในช่วงแปดปีที่ผ่านมาไม่มีใครโพสต์คำตอบง่ายๆ แทนที่จะเข้ารหัสไฟล์เป็น base64 เข้ารหัส json เป็นสตริง จากนั้นเพียงแค่ถอดรหัส json ที่ฝั่งเซิร์ฟเวอร์

ใน Javascript:

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

โพสต์โดยใช้ Content-Type: multipart / form-data

ที่ด้านเซิร์ฟเวอร์ให้ดึงไฟล์ตามปกติและดึง json เป็นสตริง แปลงสตริงเป็นวัตถุซึ่งโดยทั่วไปจะเป็นหนึ่งบรรทัดของรหัสไม่ว่าคุณจะใช้ภาษาการเขียนโปรแกรมอะไร

(ใช่มันใช้งานได้ดีทำในหนึ่งในแอพของฉัน)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.