Как загрузить файл с метаданными с помощью веб-службы 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 в виде строки. Преобразуйте строку в объект, который обычно представляет собой одну строку кода, независимо от того, какой язык программирования вы используете.

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

ccleve
источник