关于json:如何使用REST Web服务上传包含元数据的文件?

How do I upload a file with metadata using a REST web service?

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

http://server/data/media(服务器/数据/媒体)

其中用户可以选择以下JSON:

1
2
3
4
5
{
   "Name":"Test",
   "Latitude": 12.59817,
   "Longitude": 52.12873
}

以便创建新的媒体元数据。

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

还有使用multipart/form-data的方法,就像HTML表单发送的那样,但是我使用的是REST Web服务,如果可能的话,我希望继续使用JSON。


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

1
2
3
4
5
6
7
POST http://server/data/media
body:
{
   "Name":"Test",
   "Latitude": 12.59817,
   "Longitude": 52.12873
}

要创建元数据条目并返回如下响应:

1
2
3
4
5
6
7
8
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中包装整个请求体,并不意味着使用multipart/form-data在单个请求中发布JSON和文件是不安全的:

1
curl -F"metadata=<metadata.json" -F"[email protected]" http://example.com/add-file

在服务器端(将python用于伪代码):

1
2
3
4
5
class AddFileResource(Resource):
    def render_POST(self, request):
        metadata = json.loads(request.args['metadata'][0])
        file_body = request.args['file'][0]
        ...

要上载多个文件,可以对每个文件使用单独的"表单域":

1
curl -F"metadata=<metadata.json" -F"[email protected]" -F"[email protected]" http://example.com/add-file

…在这种情况下,服务器代码将有request.args['file1'][0]request.args['file2'][0]

或者对许多人重复使用同一个:

1
curl -F"metadata=<metadata.json" -F"[email protected]" -F"[email protected]" http://example.com/add-file

…在这种情况下,request.args['files']只是长度为2的列表。

或者一次将多个文件传递到一个字段中:

1
curl -F"metadata=<metadata.json" -F"[email protected],some-other-file.tar.gz" http://example.com/add-file

…在这种情况下,request.args['files']将是一个包含所有文件的字符串,您必须自己分析这些文件-不确定如何进行,但我确信这并不困难,或者最好使用前面的方法。

@<的区别在于@使文件作为文件上传附加,而<将文件内容作为文本字段附加。

另外,仅仅因为我使用curl作为生成POST请求的一种方法,并不意味着相同的HTTP请求不能从编程语言(如python)或使用任何足够功能的工具发送。


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

1
2
3
4
5
6
{
   "Name":"Test",
   "Latitude": 12.59817,
   "Longitude": 52.12873,
   "ContentID":"7a788f56fa49ae0ba5ebde780efe4d6a89b5db47"
}

包括编码到JSON请求本身的文件data base64,将使传输数据的大小增加33%。根据文件的总体大小,这可能很重要,也可能不重要。

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


我意识到这是一个非常古老的问题,但希望这能帮助其他人,因为我在这篇文章中寻找相同的东西。我有一个类似的问题,我的元数据是一个guid和int,但是解决方案是相同的。您可以将所需的元数据作为URL的一部分。

在"控制器"类中发布接受方法:

1
2
3
4
5
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)。

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

如果您的文件及其元数据创建了一个资源,那么可以在一个请求中同时上载它们。样品要求如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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\
)%[email protected]
[
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--


我不明白为什么在过去的八年里,没有人发布过简单的答案。不是将文件编码为base64,而是将JSON编码为字符串。然后在服务器端对JSON进行解码。

在javascript中:

1
2
3
let formData = new FormData();
formData.append("file", myfile);
formData.append("myjson", JSON.stringify(myJsonObject));

使用内容类型发布:多部分/表单数据

在服务器端,正常地检索文件,并以字符串的形式检索JSON。将字符串转换为一个对象,该对象通常是一行代码,无论您使用什么编程语言。

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