Amazon S3中的Etag定义已更改

时间:2011-07-06 02:49:39

标签: amazon-s3

我已经使用Amazon S3进行了一段时间的备份。通常,在我上传文件后,我会检查MD5总和匹配,以确保我做了一个很好的备份。 S3有“etag”标题,用于表示此总和。

然而,当我最近上传一个大文件时,Etag似乎不再是md5总和。它有额外的数字和连字符“696df35ad1161afbeb6ea667e5dd5dab-2861”。我找不到任何关于这种变化的文件。我已经使用S3管理控制台和Cyber​​duck进行了检查。

我找不到有关此更改的任何文档。有什么指针吗?

14 个答案:

答案 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命令(cpsync等)将工件发布到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)

我建立在r03的答案上,并在这里有一个独立的Go实用程序: Updated image

用法示例:

$ dd if=/dev/zero bs=1M count=10 of=10M_file
$ calc_s3_etag 10M_file
669fdad9e309b552f1e9cf7b489c1f73-2
$ calc_s3_etag -chunksize=15 10M_file
9fbaeee0ccc66f9a8e3d3641dca37281-1