RESTful网络服务中的文件下发和上传的方法

607 阅读5分钟

文件下发以及RESTful网络服务中的上传

通常情况下,我们使用标准的数据交换格式,如JSON或XML与REST网络服务。然而,许多REST服务至少有一些操作是很难只用JSON或XML来完成的。例如,上传产品图片、使用上传的CSV文件导入数据或生成可下载的PDF报告。

在这篇文章中,我们重点讨论这些操作,它们通常被归类为文件下传和上传。这有点飘忽,因为发送一个简单的JSON文件也可以被看作是一个(JSON)文件上传操作。

思考你想表达的操作

一个常见的错误是把注意力放在操作所需的特定文件格式上。相反,我们应该思考我们想要表达的操作。文件格式只是决定操作所使用的媒体类型

例如,假设我们想设计一个API,让用户上传一个头像到他们的用户账户。

在这里,出于各种原因,将头像图片与用户账户资源分开通常是个好主意。

  • 头像图片不太可能改变,所以它可能是一个很好的缓存候选者。另一方面,用户账户资源可能包含像最后一次登录日期这样经常变化的东西。
  • 并非所有访问用户账户的客户都会对头像感兴趣。因此,可以节省带宽。
  • 对于客户来说,单独加载图像通常是比较好的(想想使用标签的网络应用)。

用户账户资源可能通过以下方式访问。

/users/<user-id>

我们可以想出一个简单的代表头像图像的子资源。

/users/<user-id>/avatar

上传头像是一个简单的替换操作,可以通过PUT来表达。

PUT /users/<user-id>/avatar
Content-Type: image/jpeg

<image data>

如果用户想删除他的头像,我们可以使用一个简单的DELETE操作。

DELETE /users/<user-id>/avatar

当然,客户也需要一种方法来显示头像图片。所以,我们可以用GET提供一个下载操作。

GET /users/<user-id>/avatar

其中返回

HTTP/1.1 200 Ok
Content-Type: image/jpeg

<image data>

在这个简单的例子中,我们使用了一个新的子资源,具有普通的更新、删除、获取操作。唯一的区别是我们使用图像媒体类型而不是JSON或XML。

让我们来看看一个不同的例子。

假设我们提供一个API来管理产品数据。我们想用一个选项来扩展这个API,从上传的CSV文件中导入产品。我们不应该考虑文件上传,而应该考虑如何表达产品导入操作。

最简单的方法可能是向一个单独的资源发送一个POST请求。

POST /product-import
Content-Type: text/csv

<csv data>

另外,我们也可以将其视为产品的批量 操作。正如我们在另一篇关于REST的批量操作的文章中所了解到的,PATCH方法是表达对一个集合的批量操作的一种可能的方式。在这种情况下,CSV文件描述了对产品集合的预期变化。

比如说:

PATCH /products
Content-Type: text/csv

action,id,name,price
create,,Cool Gadget,3.99
create,,Nice cap,9.50
delete,42,,

这个例子创建了两个新产品并删除了id为42的产品。

处理文件上传可能需要相当多的时间。所以要考虑把它设计成一个异步的REST操作

混合文件和元数据

在某些情况下,我们可能需要给一个文件附加额外的元数据。例如,假设我们有一个API,用户可以上传假日照片。除了实际的图片数据,一张照片可能还包含描述、拍摄地点等等。

在这里,我(再次)建议使用两个独立的操作,原因类似于上一节中所说的头像图片。即使这里的情况有些不同(数据直接与图片相连),通常也是比较简单的方法。

在这种情况下,我们可以首先通过发送实际的图片来创建一个照片资源。

POST /photos
Content-Type: image/jpeg

<image data>

作为我们得到的响应。

HTTP/1.1 201 Created
Location: /photos/123

之后,我们可以在照片上附加额外的元数据。

PUT /photos/123/metadata
Content-Type: application/json

{
    "description": "Nice shot of a beach in hawaii",
    "location": "hawaii",
    "filename": "hawaii-beach.jpg"
}

当然,我们也可以反过来设计,在图片之前发送元数据。

在JSON或XML中嵌入Base64编码的文件

如果在单独的请求中分割文件内容和元数据是不可能的,我们可以使用Base64编码将文件嵌入到JSON/XML文档。通过Base64编码,我们可以将二进制格式转换为文本表示,可以集成到其他基于文本的格式,如JSON或XML。

一个请求的例子可能是这样的:

POST /photos
Content-Type: application/json

{
    "width": "1280",
    "height": "920",
    "filename": "funny-cat.jpg",
    "image": "TmljZSBleGFt...cGxlIHRleHQ="
}

将媒体类型与多部分请求混合在一起

另一种在单个请求/响应中传输图像数据和元数据的可能方法是多部分媒体类型

多部分媒体类型需要一个边界参数,作为不同主体部分之间的分隔符。下面的请求由两个主体部分组成。第一个部分包含图像,第二个部分包含元数据。

比如说

POST /photos
Content-Type: multipart/mixed; boundary=foobar

--foobar
Content-Type: image/jpeg

<image data>
--foobar
Content-Type: application/json

{
    "width": "1280",
    "height": "920",
    "filename": "funny-cat.jpg"
}
--foobar--

不幸的是,多部分请求/响应往往很难处理。例如,不是每个REST客户端都能构建这些请求,而且在单元测试中也很难验证响应。