Як завантажити файл з метаданими за допомогою веб-сервісу REST?


250

У мене є веб-служба REST, яка наразі відкриває цю URL-адресу:

http: // сервер / дані / медіа

де користувачі можуть POSTнаступні JSON:

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

щоб створити нові метадані Медіа.

Тепер мені потрібна можливість завантажувати файл одночасно з медіаданими. Який найкращий спосіб зробити це? Я міг би представити нову властивість, що називається fileта base64, кодувати файл, але мені було цікаво, чи є кращий спосіб.

Також використовується multipart/form-dataтаке, як надсилається HTML-форма, але я використовую веб-службу REST, і я хочу притримуватися використання JSON, якщо це можливо.


36
Дотримуватися лише JSON не потрібно, щоб мати RESTful веб-сервіс. REST - це практично все, що дотримується основних принципів методів HTTP та деяких інших (можливо, нестандартних) правил.
Ерік Каплун

Відповіді:


192

Я погоджуюся з Грегом, що двофазний підхід є розумним рішенням, однак я би зробив це навпаки. Я б робив:

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
Однією з переваг першої передачі вмісту є те, що до моменту існування метаданих, вміст вже присутній. Зрештою, правильна відповідь залежить від організації даних у системі.
Грег Хьюгілл

Дякую, я позначив це правильною відповіддю, бо це те, що я хотів зробити. На жаль, через дивне правило бізнесу, ми маємо дозволити завантаження в будь-якому порядку (метадані спочатку або файл спочатку). Мені було цікаво, чи є спосіб поєднати це двоє, щоб врятувати головний біль від обох ситуацій.
Даніель Т.

@Daniel Якщо ви опублікуєте файл даних спочатку, то ви можете взяти URL-адресу, повернуту в Location і додати її до атрибута ContentUrl у метаданих. Таким чином, коли сервер отримує метадані, якщо ContentUrl існує, то він уже знає, де знаходиться файл. Якщо немає ContentUrl, то він знає, що його слід створити.
Даррел Міллер

якби ви спочатку робили POST, чи не опублікували б ви ту саму URL-адресу? (/ сервер / дані / медіа) чи ви створили б іншу точку входу для завантаження файлів спочатку?
Метт Брейлсфорд

1
@Faraway Що робити, якщо метадані включали кількість "лайків" зображення? Чи ставитесь ви до цього як до єдиного ресурсу? Або, очевидно, ви припускаєте, що якщо я хотів би відредагувати опис зображення, мені потрібно було б повторно завантажити зображення? Є багато випадків, коли багатоскладові форми є правильним рішенням. Це не завжди так.
Даррел Міллер

104

Тільки тому, що ви не загортаєте все тіло запиту в JSON, це не означає, що його НЕ РЕЗУЛЬТАТИ використовувати multipart/form-dataдля розміщення як JSON, так і файлів (файлів) в одному запиті:

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

на стороні сервера (використовуючи Python для псевдокоду):

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
Мені вже було цікаво про цей підхід, і чому я ще не бачив, щоб хтось його виклав. Я погоджуюсь, здається мені абсолютно ВІДПОВІДНИМ.
суп

1
ТАК! Це дуже практичний підхід, і він не менш RESTful, ніж використання "application / json" як типу вмісту для всього запиту.
хвороба

.. але це можливо лише в тому випадку, якщо у вас є дані у файлі .json та завантажте їх, а це не так
itsjavi

5
@mjolnic ваш коментар не має значення: приклади CURL - це просто, ну, приклади ; у відповіді прямо вказано, що ви можете використовувати що-небудь, щоб відправити запит ... також, що заважає вам просто писати curl -f 'metadata={"foo": "bar"}'?
Ерік Каплун

3
Я використовую цей підхід, тому що прийнята відповідь не працює для програми, яку я розробляю (файл не може існувати до даних, і це додає зайвої складності для обробки випадку, коли дані завантажуються спочатку, а файл ніколи не завантажується) .
BitsEvolved

33

Один із способів наблизитись до проблеми - зробити процес завантаження двофазним. По-перше, ви будете завантажувати сам файл за допомогою POST, де сервер повертає якийсь ідентифікатор назад клієнту (ідентифікатором може бути SHA1 вмісту файлу). Потім другий запит пов'язує метадані з файловими даними:

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

Включення файлової бази даних64, закодованої в запит JSON, збільшить розмір переданих даних на 33%. Це може бути або не бути важливим залежно від загального розміру файлу.

Іншим підходом може бути використання POST вихідних даних про файл, але включити будь-які метадані в заголовку запиту HTTP. Однак це трохи виходить за рамки основних операцій REST і може бути більш незручним для деяких клієнтських бібліотек HTTP.


Ви можете використовувати Ascii85 збільшуючи лише на 1/4.
Singagirl

Будь-яка посилання на те, чому base64 настільки збільшує розмір?
jam01

1
@ jam01: Випадково я вчора що-небудь побачив щось, що добре відповідає на простірне питання: Який простір над кодуванням Base64?
Грег Х'югілл

10

Я розумію, що це дуже старе питання, але, сподіваюся, це допоможе комусь іншому, коли я прийшов на цю посаду, шукаючи те саме. У мене була подібна проблема, лише те, що мої метадані були Guid and int. Однак рішення те саме. Ви можете просто зробити необхідні метадані частиною URL-адреси.

Метод прийняття пошти у вашому класі "Контролер":

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.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: багаточастинні / форми-дані

На стороні сервера завантажте файл у звичайному режимі та отримайте json як рядок. Перетворіть рядок в об'єкт, який зазвичай є одним рядком коду, незалежно від того, якою мовою програмування ви використовуєте.

(Так, це чудово. Це робиться в одному з моїх додатків.)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.