对象存储swift clinet 对象存储服务(OSS)省钱建议_上传


此文表达对阿里云 OSS 功能完整性的敬意, 同时表达对其文档的不谢.

在使用第三方对象储存时, 成本是相当高的, 比如阿里云的 OSS, 不光有储存费用, 还有请求费用, 流量费用等等. 其中光是标准储存费用一项, 价格就达到了 0.12元/GB/月, 算下来 1TB 一年的储存费用为 1474.56元(百度个人网盘可比这个便宜多了).


对象存储swift clinet 对象存储服务(OSS)省钱建议_服务端_02


对于用户上传的内容, 如何节约储存空间呢, 其中一个办法就是对于内容相同的文件, 在 OSS 中只储存一个副本. 如果打算这么做, 会遇到几个安全性问题:

  1. 同样内容的文件, 不同用户的文件名可能不一样, 甚至后缀都不一样, 需要区别对待
  2. 客户端上传的时候, 对一个资源(ObjectUrl) 写入的内容应该是确定性的

幸好在翻遍了阿里云的文档, 看完了 ali-oss 的源代码, 并提交工单(并没有鸟用)之后, 终于发现阿里云完全支持上述规则. 下面描述完整的方案设计和要注意的问题:

方案

  1. 对于用户上传的文件, 通过文件内容的 Md5 值进行标记, 同一 Md5 值的文件在 OSS 中只储存一个副本, 储存对象的 id (ObjectUrl) 为 <Prefix>/<Md5>, 其中 Prefix 为一个固定值, Md5 为文件的 md5 值.
  2. 使用客户端 + 服务端签名直传到 OSS.

实现

  1. 用户选择文件后, 客户端计算文件 md5, 计算完成后将 md5 和文件名(name)发送给服务端签名, 其中 md5 用来限制用户上传文件的内容, name 用来在服务端计算 Content-Type.
  2. 客户端收到签名 url 后直接上传到 OSS.
  3. 客户端上传成功后将服务端生成的访问 url 作为最终结果使用.

要注意的问题

  1. 服务端签名: 服务端签名时, 要限制用户上传文件内容的 md5, 此选项在 Node.js 的 sdk 文档中未体现. 并且其 key 为 Content-Md5, (注意区分大小写), 代码在 https://github.com/ali-sdk/ali-oss/blob/master/lib/common/signUtils.js#L58
  2. md5 的 digest 方式为 base64, 不是 hex
  3. 动态设置 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
}