Як завантажити файл з метаданими за допомогою веб-сервісу 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.