我有一个REST网络服务,目前公开此网址:
用户可以POST
以下JSON:
{
"Name": "Test",
"Latitude": 12.59817,
"Longitude": 52.12873
}
以创建新的媒体元数据。
现在我需要能够在媒体元数据的同时上传文件。解决这个问题的最佳方法是什么?我可以引入一个名为file
的新属性和base64编码文件,但我想知道是否有更好的方法。
还有使用multipart/form-data
的HTML表单会发送的内容,但我正在使用REST Web服务,我想尽可能坚持使用JSON。
答案 0 :(得分:175)
我同意格雷格的看法,两阶段方法是一个合理的解决方案,但我会以相反的方式做到这一点。我愿意:
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可以指向具有更多空间/容量的其他服务器。或者,如果带宽有问题,您可以实施某种循环方法。
答案 1 :(得分:96)
仅仅因为你没有用JSON包装整个请求体,并不意味着使用multipart/form-data
在单个请求中发布JSON和文件(或多个文件)并不是RESTful:< / p>
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
请求的方式并不意味着无法从a发送完全相同的HTTP请求编程语言,如Python或使用任何足够强大的工具。
答案 2 :(得分:30)
解决问题的一种方法是使上传成为两阶段过程。首先,您将使用POST上传文件本身,其中服务器将一些标识符返回给客户端(标识符可能是文件内容的SHA1)。然后,第二个请求将元数据与文件数据相关联:
{
"Name": "Test",
"Latitude": 12.59817,
"Longitude": 52.12873,
"ContentID": "7a788f56fa49ae0ba5ebde780efe4d6a89b5db47"
}
将包含文件数据base64编码到JSON请求本身中会使传输数据的大小增加33%。根据文件的整体大小,这可能很重要,也可能不重要。
另一种方法可能是使用原始文件数据的POST,但在HTTP请求标头中包含任何元数据。但是,这有点落在基本的REST操作之外,对某些HTTP客户端库来说可能更尴尬。
答案 3 :(得分:10)
我意识到这是一个非常古老的问题,但希望这会帮助其他人,因为我在这篇文章中寻找同样的事情。我有一个类似的问题,只是我的元数据是Guid和int。解决方案是一样的。您只需在URL中创建所需的元数据即可。
“Controller”类中的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 { }
);
答案 4 :(得分:4)
如果您的文件及其元数据创建了一个资源,那么在一个请求中上传它们就完全没问题。样品申请将是:
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--
答案 5 :(得分:1)
我不明白为什么在过去的八年中,没有人发布简单的答案。而不是将文件编码为base64,而是将json编码为字符串。然后只需在服务器端解码json。
在Java语言中:
let formData = new FormData();
formData.append("file", myfile);
formData.append("myjson", JSON.stringify(myJsonObject));
使用Content-Type将其发布:multipart / form-data
在服务器端,正常检索文件,并以字符串形式检索json。将字符串转换为对象,无论使用哪种编程语言,通常都是一行代码。
(是的,它很好用。可以在我的一个应用中执行。)
答案 6 :(得分:0)
要基于ccleve的答案,如果您使用的是superagent / express / multer,则在前端侧构建您的多部分请求,例如:
superagent
.post(url)
.accept('application/json')
.field('myVeryRelevantJsonData', JSON.stringify({ peep: 'Peep Peep!!!' }))
.attach('myFile', file);
cf https://pypi.org/project/pystockfish/。
在快速方面,做为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元数据来计算目标目录/文件名是不可靠的