Làm cách nào để tải lên tệp có siêu dữ liệu bằng dịch vụ web REST?


249

Tôi có một dịch vụ web REST hiện đang hiển thị URL này:

http: // máy chủ / dữ liệu / phương tiện

nơi người dùng có thể POSTJSON sau đây:

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

để tạo siêu dữ liệu truyền thông mới.

Bây giờ tôi cần khả năng tải lên một tệp cùng lúc với siêu dữ liệu truyền thông. Cách tốt nhất để làm điều này là gì? Tôi có thể giới thiệu một thuộc tính mới được gọi filevà base64 mã hóa tệp, nhưng tôi tự hỏi liệu có cách nào tốt hơn không.

Cũng có sử dụng multipart/form-datanhư những gì một biểu mẫu HTML sẽ gửi qua, nhưng tôi đang sử dụng dịch vụ web REST và tôi muốn sử dụng JSON nếu có thể.


35
Bám sát chỉ sử dụng JSON là không thực sự cần thiết để có một dịch vụ web RESTful. REST về cơ bản chỉ là bất cứ thứ gì tuân theo các nguyên tắc chính của các phương thức HTTP và một số quy tắc khác (được cho là không được chuẩn hóa).
Erik Kaplun

Câu trả lời:


192

Tôi đồng ý với Greg rằng cách tiếp cận hai giai đoạn là một giải pháp hợp lý, tuy nhiên tôi sẽ làm theo cách khác. Tôi sẽ làm:

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

Để tạo mục siêu dữ liệu và trả về phản hồi như:

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

Sau đó, khách hàng có thể sử dụng ContentUrl này và thực hiện PUT với dữ liệu tệp.

Điều hay ho của phương pháp này là khi máy chủ của bạn bắt đầu bị đè nặng với khối lượng dữ liệu khổng lồ, url mà bạn trả về có thể chỉ đến một số máy chủ khác có nhiều dung lượng / dung lượng hơn. Hoặc bạn có thể thực hiện một số cách tiếp cận vòng tròn nếu băng thông là một vấn đề.


8
Một lợi thế để gửi nội dung trước tiên là vào thời điểm siêu dữ liệu tồn tại, nội dung đã có sẵn. Cuối cùng, câu trả lời đúng phụ thuộc vào tổ chức dữ liệu trong hệ thống.
Greg Hewgill

Cảm ơn, tôi đã đánh dấu đây là câu trả lời đúng vì đây là điều tôi muốn làm. Thật không may, do một quy tắc kinh doanh kỳ lạ, chúng tôi phải cho phép tải lên xảy ra theo bất kỳ thứ tự nào (trước tiên là siêu dữ liệu hoặc tệp). Tôi đã tự hỏi nếu có một cách kết hợp cả hai để tiết kiệm sự đau đầu của việc xử lý cả hai tình huống.
Daniel T.

@Daniel Nếu bạn POST tệp dữ liệu trước, thì bạn có thể lấy URL được trả về trong Vị trí và thêm nó vào thuộc tính ContentUrl trong siêu dữ liệu. Theo cách đó, khi máy chủ nhận được siêu dữ liệu, nếu ContentUrl tồn tại thì nó đã biết tệp nằm ở đâu. Nếu không có ContentUrl, thì nó biết rằng nó nên tạo một cái.
Darrel Miller

nếu bạn định làm POST trước, bạn có đăng lên cùng một URL không? (/ máy chủ / dữ liệu / phương tiện) hoặc bạn sẽ tạo một điểm nhập khác để tải lên tệp đầu tiên?
Matt Brailsford

1
@Faraway Điều gì xảy ra nếu siêu dữ liệu bao gồm số lượt "thích" của một hình ảnh? Bạn sẽ coi nó như một tài nguyên duy nhất chứ? Hoặc rõ ràng hơn, bạn đang gợi ý rằng nếu tôi muốn chỉnh sửa mô tả của một hình ảnh, tôi sẽ cần phải tải lên lại hình ảnh? Có nhiều trường hợp các hình thức đa phần là giải pháp phù hợp. Nó không chỉ là luôn luôn như vậy.
Darrel Miller

103

Chỉ vì bạn không gói toàn bộ phần yêu cầu trong JSON, không có nghĩa là nó không RESTful để sử dụng multipart/form-datađể đăng cả JSON và (các) tệp trong một yêu cầu:

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

về phía máy chủ (sử dụng Python cho mã giả):

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

để tải lên nhiều tệp, có thể sử dụng "trường biểu mẫu" riêng biệt cho mỗi tệp:

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

... trong trường hợp đó, mã máy chủ sẽ có request.args['file1'][0]request.args['file2'][0]

hoặc sử dụng lại cùng một thứ cho nhiều người:

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

... trong trường hợp đó request.args['files']sẽ chỉ là một danh sách có độ dài 2.

hoặc chuyển nhiều tệp qua một trường:

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

...trong trường hợp request.args['files'] sẽ là một chuỗi chứa tất cả các tệp, bạn sẽ phải tự phân tích - không chắc chắn cách thực hiện, nhưng tôi chắc chắn rằng nó không khó, hoặc tốt hơn là chỉ sử dụng các phương pháp trước đó.

Sự khác biệt giữa @<@làm cho tệp được đính kèm dưới dạng tải lên tệp, trong khi <đính kèm nội dung của tệp dưới dạng trường văn bản.

PS Chỉ vì tôi đang sử dụng curlnhư một cách để tạo các POSTyêu cầu không có nghĩa là các yêu cầu HTTP chính xác giống nhau không thể được gửi từ một ngôn ngữ lập trình như Python hoặc sử dụng bất kỳ công cụ đủ khả năng nào.


4
Tôi đã tự hỏi về cách tiếp cận này bản thân mình, và tại sao tôi chưa thấy ai khác đưa ra. Tôi đồng ý, dường như hoàn toàn RESTful với tôi.
súpdog

1
ĐÚNG! Đây là cách tiếp cận rất thực tế và không kém RESTful hơn là sử dụng "application / json" làm kiểu nội dung cho toàn bộ yêu cầu.
ốm

..nhưng điều đó chỉ có thể nếu bạn có dữ liệu trong tệp .json và tải nó lên, đó không phải là trường hợp
itjavi

5
@mjolnic bình luận của bạn không liên quan: các ví dụ về cURL chỉ là, ví dụ , ví dụ ; câu trả lời nói rõ rằng bạn có thể sử dụng bất cứ điều gì để gửi yêu cầu ... còn nữa, điều gì ngăn cản bạn viết curl -f 'metadata={"foo": "bar"}'?
Erik Kaplun

3
Tôi đang sử dụng phương pháp này vì câu trả lời được chấp nhận sẽ không hoạt động cho ứng dụng tôi đang phát triển (tệp không thể tồn tại trước dữ liệu và nó làm tăng thêm sự phức tạp không cần thiết để xử lý trường hợp dữ liệu được tải lên trước và tệp không bao giờ tải lên) .
BitsEvolve 17/03/2016

33

Một cách để tiếp cận vấn đề là thực hiện quá trình tải lên hai giai đoạn. Trước tiên, bạn sẽ tự tải tệp lên bằng POST, trong đó máy chủ trả lại một số định danh cho khách hàng (một định danh có thể là SHA1 của nội dung tệp). Sau đó, yêu cầu thứ hai liên kết siêu dữ liệu với dữ liệu tệp:

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

Bao gồm dữ liệu tệp cơ sở64 được mã hóa vào chính yêu cầu JSON sẽ tăng kích thước của dữ liệu được truyền lên 33%. Điều này có thể hoặc không quan trọng tùy thuộc vào kích thước tổng thể của tệp.

Một cách tiếp cận khác có thể là sử dụng POST của dữ liệu tệp thô, nhưng bao gồm bất kỳ siêu dữ liệu nào trong tiêu đề yêu cầu HTTP. Tuy nhiên, điều này nằm ngoài các hoạt động REST cơ bản và có thể khó xử hơn đối với một số thư viện máy khách HTTP.


Bạn có thể sử dụng Ascii85 tăng chỉ bằng 1/4.
Singagirl

Bất kỳ tài liệu tham khảo về lý do tại sao Base64 tăng kích thước đó nhiều?
kẹt01

1
@ jam01: Thật trùng hợp, tôi vừa thấy một cái gì đó ngày hôm qua trả lời tốt câu hỏi không gian: Chi phí không gian của mã hóa Base64 là gì?
Greg Hewgill

10

Tôi nhận ra đây là một câu hỏi rất cũ, nhưng hy vọng điều này sẽ giúp được người khác khi tôi tìm thấy bài đăng này để tìm kiếm điều tương tự. Tôi đã có một vấn đề tương tự, chỉ là siêu dữ liệu của tôi là một Guid và int. Các giải pháp là như nhau mặc dù. Bạn chỉ có thể tạo phần siêu dữ liệu cần thiết của URL.

Phương thức chấp nhận POST trong lớp "Trình điều khiển" của bạn:

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

Sau đó, trong bất cứ điều gì bạn đang đăng ký tuyến đường, WebApiConfig.Register (cấu hình httpConfiguration) cho tôi trong trường hợp này.

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

5

Nếu tệp của bạn và siêu dữ liệu của nó tạo ra một tài nguyên, việc tải lên cả hai trong một yêu cầu là hoàn toàn tốt. Yêu cầu mẫu sẽ là:

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

Tôi không hiểu tại sao, trong suốt tám năm, không ai đăng câu trả lời dễ dàng. Thay vì mã hóa tệp dưới dạng base64, hãy mã hóa json dưới dạng chuỗi. Sau đó, chỉ cần giải mã json ở phía máy chủ.

Trong Javascript:

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

POST nó bằng Content-Type: Multipart / form-data

Về phía máy chủ, lấy tệp bình thường và lấy json dưới dạng chuỗi. Chuyển đổi chuỗi thành một đối tượng, thường là một dòng mã cho dù bạn sử dụng ngôn ngữ lập trình nào.

(Vâng, nó hoạt động rất tốt. Thực hiện nó trong một trong các ứng dụng của tôi.)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.