我已经使用Amazon S3进行了一段时间的备份。通常,在我上传文件后,我会检查MD5总和匹配,以确保我做了一个很好的备份。 S3有“etag”标题,用于表示此总和。
然而,当我最近上传一个大文件时,Etag似乎不再是md5总和。它有额外的数字和连字符“696df35ad1161afbeb6ea667e5dd5dab-2861”。我找不到任何关于这种变化的文件。我已经使用S3管理控制台和Cyberduck进行了检查。
我找不到有关此更改的任何文档。有什么指针吗?
答案 0 :(得分:33)
如果使用multipart上传任何文件,那么您将始终获得此类ETag。但是如果你将整个文件作为单个文件上传,那么你将像以前一样获得ETag。
Bucket Explorer在多部分操作中为您提供正常的ETag直到5Gb上传。但更多的是它没有提供。
AWS:
使用分段上传api创建的对象的ETag将包含一个或多个非十六进制字符和/或将包含少于16个或超过16个十六进制数字。
参考:https://forums.aws.amazon.com/thread.jspa?messageID=203510#203510
答案 1 :(得分:28)
当您使用multipart上传文件时,Amazon S3使用不同的算法(通常不是MD5 Sum)计算Etag。
此算法详述如下:http://permalink.gmane.org/gmane.comp.file-systems.s3.s3tools/583
“计算文件每个上传部分的MD5哈希值, 将哈希值连接成一个二进制字符串并计算 该结果的MD5哈希。“
我只是在bash中开发一个工具来计算它,s3md5:https://github.com/Teachnova/s3md5
例如,要计算使用块大小为15 MB的多部分上载的文件 foo.bin 的Etag,那么
# s3md5 15 foo.bin
现在您可以检查一个非常大的文件(大于5GB)的完整性,因为您可以计算本地文件的Etag并将其与S3 Etag进行比较。
答案 2 :(得分:20)
也在python ...
# Max size in bytes before uploading in parts.
AWS_UPLOAD_MAX_SIZE = 20 * 1024 * 1024
# Size of parts when uploading in parts
AWS_UPLOAD_PART_SIZE = 6 * 1024 * 1024
#
# Function : md5sum
# Purpose : Get the md5 hash of a file stored in S3
# Returns : Returns the md5 hash that will match the ETag in S3
def md5sum(sourcePath):
filesize = os.path.getsize(sourcePath)
hash = hashlib.md5()
if filesize > AWS_UPLOAD_MAX_SIZE:
block_count = 0
md5string = ""
with open(sourcePath, "rb") as f:
for block in iter(lambda: f.read(AWS_UPLOAD_PART_SIZE), ""):
hash = hashlib.md5()
hash.update(block)
md5string = md5string + binascii.unhexlify(hash.hexdigest())
block_count += 1
hash = hashlib.md5()
hash.update(md5string)
return hash.hexdigest() + "-" + str(block_count)
else:
with open(sourcePath, "rb") as f:
for block in iter(lambda: f.read(AWS_UPLOAD_PART_SIZE), ""):
hash.update(block)
return hash.hexdigest()
答案 3 :(得分:6)
这是一个powershell函数,用于计算文件的Amazon ETag:
$blocksize = (1024*1024*5)
$startblocks = (1024*1024*16)
function AmazonEtagHashForFile($filename) {
$lines = 0
[byte[]] $binHash = @()
$md5 = [Security.Cryptography.HashAlgorithm]::Create("MD5")
$reader = [System.IO.File]::Open($filename,"OPEN","READ")
if ((Get-Item $filename).length -gt $startblocks) {
$buf = new-object byte[] $blocksize
while (($read_len = $reader.Read($buf,0,$buf.length)) -ne 0){
$lines += 1
$binHash += $md5.ComputeHash($buf,0,$read_len)
}
$binHash=$md5.ComputeHash( $binHash )
}
else {
$lines = 1
$binHash += $md5.ComputeHash($reader)
}
$reader.Close()
$hash = [System.BitConverter]::ToString( $binHash )
$hash = $hash.Replace("-","").ToLower()
if ($lines -gt 1) {
$hash = $hash + "-$lines"
}
return $hash
}
答案 4 :(得分:6)
这是Go中的一个例子:
func GetEtag(path string, partSizeMb int) string {
partSize := partSizeMb * 1024 * 1024
content, _ := ioutil.ReadFile(path)
size := len(content)
contentToHash := content
parts := 0
if size > partSize {
pos := 0
contentToHash = make([]byte, 0)
for size > pos {
endpos := pos + partSize
if endpos >= size {
endpos = size
}
hash := md5.Sum(content[pos:endpos])
contentToHash = append(contentToHash, hash[:]...)
pos += partSize
parts += 1
}
}
hash := md5.Sum(contentToHash)
etag := fmt.Sprintf("%x", hash)
if parts > 0 {
etag += fmt.Sprintf("-%d", parts)
}
return etag
}
这只是一个例子,你应该处理错误和内容
答案 5 :(得分:3)
如果您使用分段上传,则“etag”不是数据的MD5总和(请参阅What is the algorithm to compute the Amazon-S3 Etag for a file larger than 5GB?)。人们可以通过包含短划线“ - ”的etag来识别这种情况。
现在,有趣的问题是如何获得数据的实际MD5总和,而无需下载?一种简单的方法是将对象“复制”到自身,这不需要下载:
s3cmd cp s3://bucket/key s3://bucket/key
这将导致S3重新计算MD5总和并将其存储为刚刚复制的对象的“etag”。 “copy”命令直接在S3上运行,即没有对象数据传输到S3或从S3传输,因此这需要很少的带宽! (注意:不要使用s3cmd mv;这会删除你的数据。)
底层REST命令是:
PUT /key HTTP/1.1
Host: bucket.s3.amazonaws.com
x-amz-copy-source: /buckey/key
x-amz-metadata-directive: COPY
答案 6 :(得分:3)
使用aws s3 cp
复制到s3可以使用分段上传,并且生成的etag不会像其他人写的那样是md5。
要上传没有multipart的文件,请使用较低级别的put-object
命令。
aws s3api put-object --bucket bucketname --key remote/file --body local/file
答案 7 :(得分:2)
基于这里的答案,我编写了一个Python实现,可以正确计算多部分和单部分文件ETag。
def calculate_s3_etag(file_path, chunk_size=8 * 1024 * 1024):
md5s = []
with open(file_path, 'rb') as fp:
while True:
data = fp.read(chunk_size)
if not data:
break
md5s.append(hashlib.md5(data))
if len(md5s) == 1:
return '"{}"'.format(md5s[0].hexdigest())
digests = b''.join(m.digest() for m in md5s)
digests_md5 = hashlib.md5(digests)
return '"{}-{}"'.format(digests_md5.hexdigest(), len(md5s))
官方aws cli
工具使用的默认chunk_size是8 MB,它为2个以上的块进行分段上传。它应该在Python 2和3下工作。
答案 8 :(得分:2)
此AWS支持页面 - How do I ensure data integrity of objects uploaded to or downloaded from Amazon S3? - 描述了验证s3备份完整性的更可靠方法。
首先确定您要上传的文件的base64编码的md5sum:
$ md5_sum_base64="$( openssl md5 -binary my-file | base64 )"
然后使用s3api上传文件:
$ aws s3api put-object --bucket my-bucket --key my-file --body my-file --content-md5 "$md5_sum_base64"
请注意使用--content-md5
标志,此标志的帮助声明:
--content-md5 (string) The base64-encoded 128-bit MD5 digest of the part data.
这并没有多说为什么来使用此标记,但我们可以在API documentation for put object中找到此信息:
要确保数据没有损坏遍历网络,请使用Content-MD5标头。当您使用此标头时,Amazon S3会根据提供的MD5值检查对象,如果它们不匹配,则会返回错误。此外,您可以在将对象放入Amazon S3时计算MD5,并将返回的ETag与计算的MD5值进行比较。
使用此标志会导致S3验证文件哈希服务器端是否与指定值匹配。如果哈希匹配s3将返回ETag:
{
"ETag": "\"599393a2c526c680119d84155d90f1e5\""
}
ETag值通常是十六进制md5sum(对于某些可能不是这种情况的情况,请参阅this question。)
如果哈希值与您指定的哈希值不匹配,则会收到错误。
A client error (InvalidDigest) occurred when calling the PutObject operation: The Content-MD5 you specified was invalid.
除此之外,您还可以将文件md5sum添加到文件元数据中作为附加检查:
$ aws s3api put-object --bucket my-bucket --key my-file --body my-file --content-md5 "$md5_sum_base64" --metadata md5chksum="$md5_sum_base64"
上传后,您可以发出head-object
命令来检查值。
$ aws s3api head-object --bucket my-bucket --key my-file
{
"AcceptRanges": "bytes",
"ContentType": "binary/octet-stream",
"LastModified": "Thu, 31 Mar 2016 16:37:18 GMT",
"ContentLength": 605,
"ETag": "\"599393a2c526c680119d84155d90f1e5\"",
"Metadata": {
"md5chksum": "WZOTosUmxoARnYQVXZDx5Q=="
}
}
这是一个bash脚本,它使用内容md5并添加元数据,然后验证S3返回的值是否与本地哈希匹配:
#!/bin/bash
set -euf -o pipefail
# assumes you have aws cli, jq installed
# change these if required
tmp_dir="$HOME/tmp"
s3_dir="foo"
s3_bucket="stack-overflow-example"
aws_region="ap-southeast-2"
aws_profile="my-profile"
test_dir="$tmp_dir/s3-md5sum-test"
file_name="MailHog_linux_amd64"
test_file_url="https://github.com/mailhog/MailHog/releases/download/v1.0.0/MailHog_linux_amd64"
s3_key="$s3_dir/$file_name"
return_dir="$( pwd )"
cd "$tmp_dir" || exit
mkdir "$test_dir"
cd "$test_dir" || exit
wget "$test_file_url"
md5_sum_hex="$( md5sum $file_name | awk '{ print $1 }' )"
md5_sum_base64="$( openssl md5 -binary $file_name | base64 )"
echo "$file_name hex = $md5_sum_hex"
echo "$file_name base64 = $md5_sum_base64"
echo "Uploading $file_name to s3://$s3_bucket/$s3_dir/$file_name"
aws \
--profile "$aws_profile" \
--region "$aws_region" \
s3api put-object \
--bucket "$s3_bucket" \
--key "$s3_key" \
--body "$file_name" \
--metadata md5chksum="$md5_sum_base64" \
--content-md5 "$md5_sum_base64"
echo "Verifying sums match"
s3_md5_sum_hex=$( aws --profile "$aws_profile" --region "$aws_region" s3api head-object --bucket "$s3_bucket" --key "$s3_key" | jq -r '.ETag' | sed 's/"//'g )
s3_md5_sum_base64=$( aws --profile "$aws_profile" --region "$aws_region" s3api head-object --bucket "$s3_bucket" --key "$s3_key" | jq -r '.Metadata.md5chksum' )
if [ "$md5_sum_hex" == "$s3_md5_sum_hex" ] && [ "$md5_sum_base64" == "$s3_md5_sum_base64" ]; then
echo "checksums match"
else
echo "something is wrong checksums do not match:"
cat <<EOM | column -t -s ' '
$file_name file hex: $md5_sum_hex s3 hex: $s3_md5_sum_hex
$file_name file base64: $md5_sum_base64 s3 base64: $s3_md5_sum_base64
EOM
fi
echo "Cleaning up"
cd "$return_dir"
rm -rf "$test_dir"
aws \
--profile "$aws_profile" \
--region "$aws_region" \
s3api delete-object \
--bucket "$s3_bucket" \
--key "$s3_key"
答案 9 :(得分:1)
要比OP的问题更进一步......很可能,这些分块的ETag让你的生活很难在客户端进行比较。
如果使用awscli
命令(cp
,sync
等)将工件发布到S3,则似乎使用分段上传的默认阈值为10MB。最近的awscli
版本允许您配置此阈值,因此您可以禁用multipart并获得易于使用的MD5 ETag:
aws configure set default.s3.multipart_threshold 64MB
此处提供完整文档:http://docs.aws.amazon.com/cli/latest/topic/s3-config.html
这个的结果可能降级上传性能(老实说我没注意到)。但结果是所有小于配置阈值的文件现在都具有正常的MD5哈希ETag,这使得它们更容易实现增量客户端。
这需要稍微安装awscli
。我之前的版本(1.2.9)不支持此选项,因此我必须升级到1.10.x。
我能够成功将阈值设置为1024MB。
答案 10 :(得分:0)
当然,文件的多部分上传可能是常见问题。就我而言,我通过S3提供静态文件,即使内容相同,.js文件的etag也与本地文件不同。
事实证明,即使内容相同,也是因为行结尾不同。我修改了我的git存储库中的行结尾,将更改的文件上传到S3,现在工作正常。
答案 11 :(得分:0)
这是C#版
string etag = HashOf("file.txt",8);
源代码
private string HashOf(string filename,int chunkSizeInMb)
{
string returnMD5 = string.Empty;
int chunkSize = chunkSizeInMb * 1024 * 1024;
using (var crypto = new MD5CryptoServiceProvider())
{
int hashLength = crypto.HashSize/8;
using (var stream = File.OpenRead(filename))
{
if (stream.Length > chunkSize)
{
int chunkCount = (int)Math.Ceiling((double)stream.Length/(double)chunkSize);
byte[] hash = new byte[chunkCount*hashLength];
Stream hashStream = new MemoryStream(hash);
long nByteLeftToRead = stream.Length;
while (nByteLeftToRead > 0)
{
int nByteCurrentRead = (int)Math.Min(nByteLeftToRead, chunkSize);
byte[] buffer = new byte[nByteCurrentRead];
nByteLeftToRead -= stream.Read(buffer, 0, nByteCurrentRead);
byte[] tmpHash = crypto.ComputeHash(buffer);
hashStream.Write(tmpHash, 0, hashLength);
}
returnMD5 = BitConverter.ToString(crypto.ComputeHash(hash)).Replace("-", string.Empty).ToLower()+"-"+ chunkCount;
}
else {
returnMD5 = BitConverter.ToString(crypto.ComputeHash(stream)).Replace("-", string.Empty).ToLower();
}
stream.Close();
}
}
return returnMD5;
}
答案 12 :(得分:0)
改进@Spedge和@Rob的答案,这是一个python3 md5函数,该函数接受类似文件的内容,并且不依赖于能够使用os.path.getsize
获取文件大小。
# Function : md5sum
# Purpose : Get the md5 hash of a file stored in S3
# Returns : Returns the md5 hash that will match the ETag in S3
# https://github.com/boto/boto3/blob/0cc6042615fd44c6822bd5be5a4019d0901e5dd2/boto3/s3/transfer.py#L169
def md5sum(file_like,
multipart_threshold=8 * 1024 * 1024,
multipart_chunksize=8 * 1024 * 1024):
md5hash = hashlib.md5()
file_like.seek(0)
filesize = 0
block_count = 0
md5string = b''
for block in iter(lambda: file_like.read(multipart_chunksize), b''):
md5hash = hashlib.md5()
md5hash.update(block)
md5string += md5hash.digest()
filesize += len(block)
block_count += 1
if filesize > multipart_threshold:
md5hash = hashlib.md5()
md5hash.update(md5string)
md5hash = md5hash.hexdigest() + "-" + str(block_count)
else:
md5hash = md5hash.hexdigest()
file_like.seek(0)
return md5hash
答案 13 :(得分:0)