如何使用REST Web服务上载带有元数据的文件?


249

我有一个REST Web服务,当前公开此URL:

http:// server / data / media

用户可以POST在其中使用以下JSON:

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

为了创建一个新的媒体元数据。

现在,我需要能够与媒体元数据同时上传文件。最好的方法是什么?我可以引入一个称为的新属性file并对文件进行base64编码,但是我想知道是否有更好的方法。

也有使用multipart/form-dataHTML表单发送的东西,但是我正在使用REST Web服务,并且我希望尽可能使用JSON。


35
拥有RESTful Web服务并不一定需要仅使用JSON。REST基本上就是遵循HTTP方法和其他一些(可能是非标准化的)规则的主要原理的事物。
埃里克·卡普伦

Answers:


192

我同意格雷格(Greg)的观点,两阶段方法是一个合理的解决方案,但是我会反过来做。我会做:

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
首先发送内容的一个优点是,到元数据存在时,内容已经存在。最终,正确答案取决于系统中数据的组织。
Greg Hewgill

谢谢,我将其标记为正确答案,因为这是我想要做的。遗憾的是,由于一条奇怪的业务规则,我们必须允许以任何顺序进行上传(首先是元数据,还是首先是文件)。我想知道是否有一种方法可以将两者结合起来,从而避免处理这两种情况的麻烦。
Daniel T.

@Daniel如果先发布数据文件,则可以获取Location中返回的URL,并将其添加到元数据的ContentUrl属性中。这样,当服务器接收元数据时,如果存在ContentUrl,则它已经知道文件在哪里。如果没有ContentUrl,则它知道应该创建一个。
Darrel Miller 2010年

如果您要先进行POST,您会张贴到相同的URL吗?(/ server / data / media)还是要为文件优先上传创建另一个入口点?
Matt Brailsford 2010年

1
@Faraway如果元数据包含图像的“赞”数量怎么办?那您将其视为单一资源吗?或更明显的是,您是否建议我要编辑图像的描述,是否需要重新上传图像?在许多情况下,多部分形式是正确的解决方案。并非总是如此。
Darrel Miller '18

103

仅仅因为您没有将整个请求主体包装在JSON中,并不意味着multipart/form-data在单个请求中同时发布JSON和文件不是RESTful的:

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请求的方式,并不意味着无法从诸如Python之类的编程语言或使用任何功能强大的工具发送完全相同的HTTP请求。


4
我本人一直在想这种方法,以及为什么我还没有看到其他人提出过这种方法。我同意,对我来说似乎完全是RESTful。
13年

1
是!这是一种非常实用的方法,并且与使用“ application / json”作为整个请求的内容类型一样,它的RESTful丝毫不逊色。
生病了

..但是只有当您将数据保存在.json文件中并上传时,这才是可能的
事实

5
@mjolnic您的评论是无关紧要的:cURL示例只是示例;答案明确指出,您可以使用任何东西来发送请求……而且,是什么使您无法编写curl -f 'metadata={"foo": "bar"}'
埃里克·卡普伦

3
我之所以使用这种方法,是因为接受的答案不适用于我正在开发的应用程序(文件不能在数据之前存在,并且会增加不必要的复杂性以处理先上传数据而从未上传文件的情况) 。
BitsEvolved

33

解决该问题的一种方法是使上传过程分为两个阶段。首先,您将使用POST上传文件本身,服务器将在其中将一些标识符返回给客户端(标识符可能是文件内容的SHA1)。然后,第二个请求将元数据与文件数据相关联:

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

包括编码为JSON请求本身的文件数据库base64将使传输的数据大小增加33%。这可能重要也可能不重要,具体取决于文件的整体大小。

另一种方法可能是使用原始文件数据的POST,但在HTTP请求标头中包含任何元数据。但是,这有点超出基本的REST操作,对于某些HTTP客户端库而言可能更尴尬。


您可以使用仅增加1/4的Ascii85。
Singagirl

关于为什么base64会增加大小的任何参考?
jam01年

1
@ jam01:巧合的是,昨天我刚刚看到一些东西可以很好地回答空间问题:Base64编码的空间开销是多少?
Greg Hewgill '19

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

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

5

如果您的文件及其元数据创建了一种资源,则可以在一个请求中将它们都上传完全正常。样品要求为:

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

使用Content-Type发布:multipart / form-data

在服务器端,正常检索文件,然后将json作为字符串检索。将字符串转换为对象,无论您使用哪种编程语言,通常都是一行代码。

(是的,它很好用。在我的一个应用程序中进行。)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.