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

у меня есть веб-служба REST, которая в настоящее время предоставляет этот URL:

http://server/data/media

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

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

для создания новых метаданных мультимедиа.

теперь мне нужна возможность загружать файл одновременно с метаданными СМИ. Как лучше поступить? Я мог бы ввести новое свойство под названием file и base64 кодировать файл, но мне было интересно, если был способ получше.

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

5 ответов


Я согласен с Грегом, что двухфазный подход является разумным решением, однако я бы сделал это наоборот. Я бы сделал:

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, который вы возвращаете, может просто указать на некоторые другой сервер с большим пространством / емкостью. Или вы можете реализовать какой-то круговой подход, если проблема с пропускной способностью.


только потому, что вы не обертываете все тело запроса в JSON, не означает, что это не RESTful использовать multipart/form-data чтобы опубликовать JSON и файл (или несколько файлов) в одном запросе:

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

на стороне сервера (используя Python в качестве программирования lingua franca здесь):

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'] будет строка, содержащая все файлы, которые вам придется разобрать самостоятельно - не уверен, как это сделать, но я уверен, что это не так сложно, или лучше просто использовать предыдущие подходы.

разницу между @ и < это @ заставляет файл прикрепляться как загрузка файла, тогда как < прикрепляет содержимое файла в виде текстового поля.

П. С. просто потому, что я использую curl как способ создания POST запросы не означают, что те же самые HTTP-запросы не могут быть отправлены с языка программирования, такого как Python, или с использованием любого достаточно способный инструмент.


один из способов решения проблемы-сделать загрузку в два этапа. Во-первых, вы должны загрузить сам файл с помощью POST, где сервер возвращает клиенту некоторый идентификатор (идентификатор может быть SHA1 содержимого файла). Затем второй запрос связывает метаданные с данными файла:

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

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

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


Я понимаю, что это очень старый вопрос, но, надеюсь, это поможет кто-то другой, как я набрел на этот пост ища то же самое. У меня была аналогичная проблема, просто мои метаданные были 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.Регистрация (конфигурация HttpConfiguration) для меня в этом деле.

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

Если ваш файл и его метаданные создают один ресурс, его прекрасно загружать их обоих в одном запросе. Запрос образца был бы :

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