此文表达对阿里云 OSS 功能完整性的敬意, 同时表达对其文档的不谢.
在使用第三方对象储存时, 成本是相当高的, 比如阿里云的 OSS, 不光有储存费用, 还有请求费用, 流量费用等等. 其中光是标准储存费用一项, 价格就达到了 0.12元/GB/月, 算下来 1TB 一年的储存费用为 1474.56元(百度个人网盘可比这个便宜多了).
对于用户上传的内容, 如何节约储存空间呢, 其中一个办法就是对于内容相同的文件, 在 OSS 中只储存一个副本. 如果打算这么做, 会遇到几个安全性问题:
- 同样内容的文件, 不同用户的文件名可能不一样, 甚至后缀都不一样, 需要区别对待
- 客户端上传的时候, 对一个资源(ObjectUrl) 写入的内容应该是确定性的
幸好在翻遍了阿里云的文档, 看完了 ali-oss
的源代码, 并提交工单(并没有鸟用)之后, 终于发现阿里云完全支持上述规则. 下面描述完整的方案设计和要注意的问题:
方案
- 对于用户上传的文件, 通过文件内容的 Md5 值进行标记, 同一 Md5 值的文件在 OSS 中只储存一个副本, 储存对象的 id (ObjectUrl) 为
<Prefix>/<Md5>
, 其中Prefix
为一个固定值,Md5
为文件的md5
值. - 使用客户端 + 服务端签名直传到
OSS
.
实现
- 用户选择文件后, 客户端计算文件 md5, 计算完成后将 md5 和文件名(name)发送给服务端签名, 其中 md5 用来限制用户上传文件的内容, name 用来在服务端计算
Content-Type
. - 客户端收到签名 url 后直接上传到 OSS.
- 客户端上传成功后将服务端生成的访问 url 作为最终结果使用.
要注意的问题
- 服务端签名: 服务端签名时, 要限制用户上传文件内容的 md5, 此选项在 Node.js 的 sdk 文档中未体现. 并且其 key 为
Content-Md5
, (注意区分大小写), 代码在 https://github.com/ali-sdk/ali-oss/blob/master/lib/common/signUtils.js#L58 - md5 的 digest 方式为
base64
, 不是hex
- 动态设置
Content-Type
的方式为在GetObject
URL 参数中添加response-content-type
, 文档在 https://help.aliyun.com/document_detail/31980.html?spm=a2c4g.11186623.6.795.57af58d5qSbHu4#title-tze-yh1-amx
示例:
// 服务端签名
import AliOSS from 'ali-oss'
import mime from 'mime'
export function signatureUploadUrl(md5: string, filename: string) {
const sts = new AliOSS.STS(/* options */);
const { credentials } = await sts.assumeRole(/* arn */, void 0, /* expires */);
const store = new AliOSS(/* credentials & options */);
const uploadUrl = store.signatureUrl(`${STORE_PREFIX}/${md5}`, {
expires: /* expires */,
method: 'PUT',
'Content-Type': 'application/octet-stream',
'Content-Md5': md5, // 注意这里的大小写 */
response: {
'Content-Type': 'application/octet-stream',
},
} as AliOSS.SignatureUrlOptions)
const contentType = mime.lookup(filename)
// 访问 URL
// response-content-type 为 OSS 所用于设置 Content-Type
// filename 用于客户端期望下载文件时, 设置 response-content-disposition 供 OSS 使用
const publicUrl = `${CDN}/${STORE_PREFIX}/${md5}?response-content-type=${encodeURIComponent(contentType)}&filename=${encodeURIComponent(filename)}`
return { uploadUrl, publicUrl }
}
// 客户端上传
import md5File from 'browser-md5-file'
import Axios from 'axios'
function uploadFile(file: File) {
const hex =await new Promise((resolve, reject) => {
md5File(file, resolve, reject)
})
const md5 = hexToBase64(hex)
const { data } = await Axios.post('/api/Media.signatureUploadUrl', { md5, filename: file.name })
await Axios.put(data.uploadUrl, file, {
headers: {
'Content-Type': 'application/octet-stream',
'Content-MD5': md5,
},
})
return data.publicUrl
}