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

Posted

技术标签:

【中文标题】如何使用 REST Web 服务上传包含元数据的文件?【英文标题】:How do I upload a file with metadata using a REST web service? 【发布时间】:2011-04-25 17:05:10 【问题描述】:

我有一个 REST Web 服务,它当前公开了这个 URL:

http://server/data/media

用户可以在哪里POST以下JSON:


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

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

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

还可以使用multipart/form-data,就像发送 html 表单一样,但我使用的是 REST Web 服务,如果可能的话,我想坚持使用 JSON。

【问题讨论】:

坚持仅使用 JSON 并不是真正需要拥有 RESTful Web 服务的必要条件。 REST 基本上只是遵循 HTTP 方法的主要原则和其他一些(可以说是非标准化的)规则的任何东西。 【参考方案1】:

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


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

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

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

【讨论】:

你可以使用 Ascii85 增加 1/4。 关于为什么 base64 会增加这么多大小的任何参考? @jam01:巧合的是,我昨天刚看到一个很好地回答了空间问题的东西:What is the space overhead of Base64 encoding?【参考方案2】:

我同意 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 可以指向具有更多空间/容量的其他服务器。或者,如果带宽是一个问题,您可以实施某种循环方法。

【讨论】:

首先发送内容的一个好处是,当元数据存在时,内容已经存在。最终,正确答案取决于系统中数据的组织方式。 谢谢,我将此标记为正确答案,因为这是我想做的。不幸的是,由于一个奇怪的业务规则,我们必须允许以任何顺序进行上传(元数据优先或文件优先)。我想知道是否有一种方法可以将两者结合起来,以省去处理这两种情况的麻烦。 @Daniel 如果您首先发布数据文件,那么您可以获取 Location 中返回的 URL 并将其添加到元数据中的 ContentUrl 属性中。这样,当服务器收到元数据时,如果 ContentUrl 存在,那么它已经知道文件在哪里。如果没有 ContentUrl,那么它知道它应该创建一个。 如果您先进行 POST,您会发布到同一个 URL 吗? (/server/data/media) 还是为文件优先上传创建另一个入口点? @Faraway 如果元数据包含图像的“喜欢”数量怎么办?那你会把它当作一个单一的资源吗?或者更明显的是,您是否建议如果我想编辑图像的描述,我需要重新上传图像?在许多情况下,多部分表单是正确的解决方案。并非总是如此。【参考方案3】:

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

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

在服务器端

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'] 将是一个包含所有文件的字符串,您必须自己解析它——不知道该怎么做,但我相信这并不难,或者最好只使用以前的方法。

@&lt; 之间的区别在于@ 导致文件作为文件上传附加,而&lt; 将文件内容作为文本字段附加。

PS 仅仅因为我使用 curl 作为生成 POST 请求的一种方式,并不意味着无法从编程语言发送完全相同的 HTTP 请求,例如Python 或使用任何功能足够强大的工具。

【讨论】:

我自己一直想知道这种方法,以及为什么我还没有看到其他人提出它。我同意,对我来说似乎完全是 RESTful。 是的!这是一种非常实用的方法,与使用“application/json”作为整个请求的内容类型相比,它的 RESTful 风格丝毫不逊色。 ..但只有在 .json 文件中有数据并上传时才有可能,事实并非如此 @mjolnic 您的评论无关紧要:cURL 示例只是,examples;答案明确指出你可以使用任何东西来发送请求......另外,是什么阻止你写curl -f 'metadata="foo": "bar"' 我正在使用这种方法,因为接受的答案不适用于我正在开发的应用程序(该文件不能在数据之前存在,并且它增加了不必要的复杂性来处理数据的情况先上传,文件从不上传)。【参考方案4】:

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

“控制器”类中的 POST 接受方法:

public Task<HttpResponseMessage> PostFile(string name, float latitude, float longitude)

    //See http://***.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--

【讨论】:

【参考方案6】:

我不明白为什么在八年的时间里,没有人发布简单的答案。不是将文件编码为 base64,而是将 json 编码为字符串。然后只需在服务器端解码 json。

javascript 中:

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

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

在服务器端,正常检索文件,将json作为字符串检索。将字符串转换为对象,不管你使用什么编程语言,这通常是一行代码。

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

【讨论】:

我很惊讶没有人扩展 Mike 的答案,因为这正是应该使用 multipart 东西的方式:每个部分都有自己的 mime 类型和 DRF 的 multipart解析器,应该相应地调度。也许很难在客户端创建这种类型的信封。我真的应该调查一下……【参考方案7】:

为了建立 ccleve 的答案,如果您使用的是 superagent / express / multer,请在前端构建您的多部分请求,执行如下操作:

superagent
    .post(url)
    .accept('application/json')
    .field('myVeryRelevantJsonData', JSON.stringify( peep: 'Peep Peep!!!' ))
    .attach('myFile', file);

cfhttps://visionmedia.github.io/superagent/#multipart-requests.

在 express 方面,以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 元数据计算目标目录/文件名是不可靠的

【讨论】:

以上是关于如何使用 REST Web 服务上传包含元数据的文件?的主要内容,如果未能解决你的问题,请参考以下文章

我如何将在本地工作的 Web 服务(REST api,在 java 中)上传到主机服务器

google drive simple upload rest API的示例

如何使用 Autodesk Forge API 将上传的 AutoCAD 文件中的元数据提取到 Web 应用程序中?

公开 WCF 4.0 Rest 模板服务的元数据

在自托管 Web 服务器环境中通过 POST REST API 上传 C# 文件

如何在c#中使用Rest Web服务