Как загрузить файл с метаданными с помощью веб-службы REST?
У меня есть веб-сервис REST, который в настоящее время предоставляет этот URL:
Http: // сервер / данные / СМИ
где пользователи могут 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, не означает, что его нельзя использовать 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, или с помощью достаточно мощного инструмента.
Один из способов решения этой проблемы - сделать загрузку двухфазной. Во-первых, вы должны загрузить сам файл, используя POST, где сервер возвращает некоторый идентификатор обратно клиенту (идентификатор может быть SHA1 содержимого файла). Затем второй запрос связывает метаданные с данными файла:
{
"Name": "Test",
"Latitude": 12.59817,
"Longitude": 52.12873,
"ContentID": "7a788f56fa49ae0ba5ebde780efe4d6a89b5db47"
}
Включение файла base64, закодированного в сам запрос JSON, увеличит размер передаваемых данных на 33%. Это может или не может быть важным в зависимости от общего размера файла.
Другим подходом может быть использование POST необработанных данных файла, но включение любых метаданных в заголовок HTTP-запроса. Однако это немного выходит за рамки базовых операций REST и может быть более неудобным для некоторых клиентских библиотек HTTP.
Я не понимаю, почему за восемь лет никто не опубликовал простой ответ. Вместо того, чтобы кодировать файл как 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 в виде строки. Преобразуйте строку в объект, который обычно представляет собой одну строку кода, независимо от того, какой язык программирования вы используете.
(Да, отлично работает. Делаю это в одном из моих приложений.)
Я понимаю, что это очень старый вопрос, но, надеюсь, это поможет кому-то другому, когда я наткнулся на этот пост в поисках того же самого. У меня была похожая проблема, просто мои метаданные были Guid и int. Решение то же самое, хотя. Вы можете просто сделать необходимые метаданные частью URL.
Метод приема POST в вашем классе "Контроллер":
public Task<HttpResponseMessage> PostFile(string name, float latitude, float longitude)
{
//See http://stackru.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 { }
);
Если ваш файл и его метаданные создают один ресурс, вполне нормально загрузить их оба в одном запросе. Пример запроса будет:
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--
Чтобы опираться на ответ ccleve, если вы используете superagent / express / multer, на стороне интерфейса создайте свой многостраничный запрос, сделав что-то вроде этого:
superagent
.post(url)
.accept('application/json')
.field('myVeryRelevantJsonData', JSON.stringify({ peep: 'Peep Peep!!!' }))
.attach('myFile', file);
cf https://visionmedia.github.io/superagent/#multipart-requests.
На экспресс-стороне все, что было передано как
field
окажется в req.body после выполнения:
app.use(express.json({ limit: '3MB' }));
Ваш маршрут будет включать что-то вроде этого:
const multerMemStorage = multer.memoryStorage();
const multerUploadToMem = multer({
storage: multerMemStorage,
// Also specify fileFilter, limits...
});
router.post('/myUploads',
multerUploadToMem.single('myFile'),
async (req, res, next) => {
// Find back myVeryRelevantJsonData :
logger.verbose(`Uploaded req.body=${JSON.stringify(req.body)}`);
// If your file is text:
const newFileText = req.file.buffer.toString();
logger.verbose(`Uploaded text=${newFileText}`);
return next();
},
...
Однако следует иметь в виду следующее примечание из документа multer, касающееся дискового хранилища:
Обратите внимание, что req.body может быть еще не полностью заполнен. Это зависит от порядка, в котором клиент передает поля и файлы на сервер.
Я предполагаю, что это означает, что было бы ненадежно, скажем, вычислить целевой каталог / имя файла на основе метаданных json, переданных вместе с файлом