前言
我们的业务有上传服务,不管是我们自己的文件,还是用户生成的文件, 都会传到第三方的存储云服务中。 如果是国内用户,我们用的是七牛云服务, 如果是国外用户,我们用的是 aws 的 s3 存储服务。
如果是存放用户上传文件,一般我们建立的 bucket 就是私有桶。 正常情况下如果上传成功的话,一般文件都是完整的。 但是不排除网络丢包的情况, 那么我们怎么去保证我们上传的文件,一定是完整的呢。
最简单的方法,就是在上传到云服务之前,先把本地的文件进行 md5 计算,得到一个 md5 的 hash 值。 然后上传到云服务之后, 再去云服务请求这个文件的 md5 的值,两者对比,如果一致的话,那么文件就是完整的。
获取本地文件的 md5 值
这个就很简单了,各种语言都可以实现,就以 golang 为例,代码:
1 | func GetPackageFileHash(filePath) (string, error) { |
这样子就可以了。
获取七牛的文件的 md5 值
七牛也有接口可以获取已上传文件的 md5 值: 文件HASH值(qhash)
他这边有地区限制, 该功能目前支持华东、华南、华北、北美、东南亚区域的存储 bucket
简单的来说,就是在原来的资源的连接 url 最后再补上 ?qhash/md5
字串,这样子就会返回该文件的 md5 值了。
1 | http://dn-odum9helk.qbox.me/resource/gogopher.jpg?qhash/md5 |
不过要注意的是,该方式仅适用于公开桶, 不适用于私有桶, 私有桶的文件的 md5 值的请求,就跟我们请求私有桶的下载连接一样,也要对整个请求进行签名才行。 只不过这时候的资源文件的名称就会由 gogopher.jpg
变成 gogopher.jpg?qhash/md5
, 而本例就是用的私有桶,用的就是这种方式。
搞笑的是,关于私有桶怎么请求 md5 值,他的文档竟然都没有提到, 还是我提交工单给他们, 他们工程师才说的。
而且之前在处理这个事情的时候, 他们也有开放一个接口来获取资源的元信息: 资源元信息查询, 上面文档返回值有 md5
, 但是我用了他们的 golang 的 SDK 里面的方式, 发现响应返回的结构体, 只有一些必须返回的,比如 hash, fsize 等, 并没有 md5 值
1 | type Entry struct { |
也不知道是不是我使用方式不当,反正我请求下来, 是没有看到 md5 值。 所以就用了上面第一种方式来得到 md5 值。
获取 S3 的文件的 md5 值
关于怎么保证 S3 上传文件的完整性的话,他们是有官方文档的: 如何检查已上载到 Amazon S3 的对象的完整性?, 不过他们的处理方式是在上传的时候, 先计算文件的 md5 值,然后在上传的时候,将 Content-MD5
当做 header 头部带过去, 上传的时候, Amazon S3 根据提供的 Content-MD5 值检查对象。如果值不匹配,则会收到错误消息。
这个是在上传结算的时候, 由 s3 那边来判断是否完整。 其实是可以达到我们的需求的。 但是为了跟七牛的完整性校验保持一致。 还是采用了上传完成之后,再获取 md5 值进行匹配的方式来处理。
如果要获取资源的 md5 的话,S3 也有提供一个接口,可以获取这个资源的 etag: HeadObject, 正常情况下,这个 etag 就是这个文件的 md5 值。 但是在某些情况下, 他并不等于文件的 md5 值.
实体标签表示对象的特定版本。 ETag 仅反映对象内容的更改,而不反映其元数据。 ETag 可能是也可能不是对象数据的 MD5 摘要。它是否取决于对象是如何创建的以及它是如何加密的,如下所述:
通过 AWS 管理控制台或通过 PUT 对象、POST 对象或复制操作创建的对象:
- 由 SSE-S3 或明文加密的对象具有作为其数据的 MD5 摘要的 ETag。
- 由 SSE-C 或 SSE-KMS 加密的对象具有不是其对象数据的 MD5 摘要的 ETag。
无论加密方法如何,由分段上传或分段复制操作创建的对象都具有不是 MD5 摘要的 ETag。
简单的来说,正常情况下,ETag 应该是跟资源的 md5 一致。 但是不同的加密上传会导致有可能 ETag 会不一致。 尤其是分段上传肯定是不一致的。 而且我也试过了, 直接在 AWS 管理控制台上传的资源算出来的 ETag 也跟资源本身的 md5 也不一致。
不过对于我们的业务来说,不是分段上传, 加密对象也不是 SSE-C 或 SSE-KMS, 所以 ETag 和 md5 是一致 。 所以我们的场景是可以用这个方法来得到 ETag,然后校验的。
举个栗子
接下来写了完整的 demo 来说明一下, 我这边就直接贴代码了, 尽可能代码简介一点。 整个 demo 的文件有几个 (用 golang 写的)
1 | - main.go 入口文件 |
结构非常简单清晰,而且用 golang 来写可以让代码巨简洁,没有一行是多余的。 接下来开始介绍各个文件。
工具方法文件 utils.go
1 | package main |
封装了几个等下要用到的方法。 不再多说,注释写的也很清楚。
七牛上传文件 qiniu.go
1 | package main |
注释也写的很清楚, 就是上传和获取 md5,直接用他们的 sdk 方法就可以搞定了。 也不多说
s3 上传文件 aws.go 和 s3.go
因为 s3 是属于 aws 的其中一个服务, 而 aws 的 accessKey 和 secretKey 是适用于所有的 aws 产品的,不仅仅是 s3。 所以这边将权限初始化分开,会显得架构更加的清晰。
首先是 aws.go 就是权限初始化:
1 | package main |
他有很多个区域, 一般我们的bucket, 要么是在美东,要么是在美西, 不需要所有的 region 都列出来。
接下来是 s3.go:
1 | package main |
注释也很清楚, 不再多说。
最后执行的 main 文件
1 | package main |
做了一个 interface, 这样子就可以将通用的处理流程都抽出来。 显得代码更加的干净了。
跑起来看下效果
可以看到,刚开始是跑七牛上传, 然后接下来是跑 s3 上传的。 先看七牛的 log:
1 | start=== |
log 也写的很详细了, 反正就是先上传,然后再获取 md5, 最后匹配。
然后接下来看 s3 上传的 log:
1 | get file hash: e10adc3949ba59abbe56e057f20f883e |
一样测试结果肯定是匹配的。