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


251

У меня есть веб-сервис REST, который в настоящее время предоставляет этот URL:

Http: // сервер / данные / СМИ

где пользователи могут POSTследующие JSON:

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

для того, чтобы создать новые метаданные медиа.

Теперь мне нужна возможность загружать файл одновременно с метаданными медиа. Какой лучший способ сделать это? Я мог бы ввести новое свойство с именем filebase64 и кодировать файл, но мне было интересно, есть ли лучший способ.

Там также используется 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 Если вы сначала разместите файл данных POST, то вы можете взять URL-адрес, возвращенный в Location, и добавить его в атрибут ContentUrl в метаданных. Таким образом, когда сервер получает метаданные, если ContentUrl существует, он уже знает, где находится файл. Если ContentUrl не существует, он знает, что должен его создать.
— Даррел Миллер

если бы вы сначала делали POST, вы бы отправили на тот же URL? (/ server / data / media) или вы бы создали другую точку входа для первой загрузки файла?
— Мэтт Брэйлсфорд

1
@Faraway Что, если метаданные включают количество лайков изображения? Вы бы тогда относились к нему как к единому ресурсу? Или, более очевидно, вы предлагаете, чтобы, если бы я захотел отредактировать описание изображения, мне нужно было бы повторно загрузить изображение? Во многих случаях правильное решение состоит из нескольких частей. Это просто не всегда так.
— Даррел Миллер

104

То, что вы не заключаете тело запроса в JSON, вовсе не означает, что использовать REST 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"
}

Включение файла base64, закодированного в сам запрос JSON, увеличит размер передаваемых данных на 33%. Это может или не может быть важным в зависимости от общего размера файла.

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


Вы можете использовать Ascii85, увеличиваясь всего на 1/4.
— Singagirl

Любая ссылка на то, почему base64 увеличивает размер так сильно?
— jam01

1
@ jam01: По совпадению, я только что видел вчера что-то, что хорошо отвечает на космический вопрос: каково пространство в кодировке Base64?
— Грег Хьюгилл

10

Я понимаю, что это очень старый вопрос, но, надеюсь, это поможет кому-то другому, когда я наткнулся на этот пост в поисках того же самого. У меня была похожая проблема, просто мои метаданные были Guid и int. Решение то же самое, хотя. Вы можете просто сделать необходимые метаданные частью URL.

Метод приема POST в вашем классе «Контроллер»:

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

POST это используя Content-Type: multipart / form-data

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

(Да, это прекрасно работает. Делаю это в одном из моих приложений.)

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.