如何使用 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 服务上传包含元数据的文件?的主要内容,如果未能解决你的问题,请参考以下文章