Определение Etag изменено в Amazon S3

Я использовал Amazon S3 немного для резервного копирования в течение некоторого времени. Обычно после загрузки файла я проверяю совпадения суммы MD5, чтобы убедиться, что я сделал хорошую резервную копию. S3 имеет заголовок "etag", который использовал эту сумму.

Однако, когда я недавно загрузил большой файл, Etag больше не является суммой md5. Он имеет дополнительные цифры и дефис "696df35ad1161afbeb6ea667e5dd5dab-2861". Я не могу найти документацию об этом изменении. Я проверил использование консоли управления S3 и с помощью Cyberduck.

Я не могу найти документацию об этом изменении. Любые указатели?

Ответ 1

Если какой-либо файл загружается с помощью multipart, вы всегда будете получать такой тип ETag. Но если вы загрузите весь файл в виде одного файла, вы получите ETag, как и раньше.

Bucket Explorer, обеспечивающий вам нормальный ETag до загрузки 5Gb в многопользовательском режиме. Но более того, он не обеспечивает.

https://forums.aws.amazon.com/thread.jspa?messageID=203510#203510

Ответ 2

Amazon S3 вычисляет Etag с помощью другого алгоритма (а не MD5 Sum, как обычно) при загрузке файла с использованием multipart.

Этот алгоритм подробно описан здесь: http://permalink.gmane.org/gmane.comp.file-systems.s3.s3tools/583

"Вычислить хэш MD5 для каждой загруженной части файла, объединить хеши в одну двоичную строку и вычислить MD5 хэша этого результата."

Я просто разработал инструмент в bash для его вычисления, s3md5: https://github.com/Teachnova/s3md5

Например, чтобы вычислить Etag файла foo.bin, который был загружен с использованием multipart с размером блока 15 МБ, затем

# s3md5 15 foo.bin

Теперь вы можете проверить целостность очень большого файла (более 5 ГБ), потому что вы можете вычислить Etag локального файла и сравнить его с S3 Etag.

Ответ 3

Также в 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()

Ответ 4

Вот пример в 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

Здесь функция powershell для вычисления ETag Amazon для файла:

$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
}

Ответ 6

Если вы используете многостраничные закачки, "etag" не является суммой данных MD5 (см. Каков алгоритм вычисления Amazon-S3 Etag для файла размером более 5 ГБ?). Этот случай можно идентифицировать с помощью этага, содержащего тире, "-".

Теперь интересный вопрос заключается в том, как получить фактическую сумму MD5 данных без загрузки? Один простой способ - просто "скопировать" объект на себя, это не требует загрузки:

s3cmd cp s3://bucket/key s3://bucket/key

Это заставит S3 пересчитать сумму MD5 и сохранить ее как "etag" только что скопированного объекта. Команда "копировать" запускается непосредственно на 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

Ответ 7

Копирование на s3 с помощью aws s3 cp может использовать многостраничные закачки, а полученный etag не будет md5, как писали другие.

Чтобы загрузить файлы без multipart, используйте команду нижнего уровня put-object.

aws s3api put-object --bucket bucketname --key remote/file --body local/file

Ответ 8

Эта страница поддержки AWS. Как обеспечить целостность данных объектов, загруженных или загруженных из Amazon S3? - описывает более надежный способ проверки целостности ваших резервных копий s3.

Сначала определите md5sum в кодировке base64 файла, который вы хотите загрузить:

$ 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 для объекта put:

Чтобы данные не были повреждены при прохождении через сеть, используйте заголовок Content-MD5. Когда вы используете этот заголовок, Amazon S3 проверяет объект по предоставленному значению MD5 и, если они не совпадают, возвращает ошибку. Кроме того, вы можете вычислить MD5 при помещении объекта в Amazon S3 и сравнить возвращенный ETag с вычисленным значением MD5.

Использование этого флага заставляет S3 проверить, что файловый хеш-сервер соответствует указанному значению. Если совпадение хэшей s3 вернет ETag:

{
    "ETag": "\"599393a2c526c680119d84155d90f1e5\""
}

Значение ETag обычно будет шестнадцатеричной суммой md5 (см. Этот вопрос для некоторых сценариев, где это может быть не так).

Если хеш не соответствует указанному вами, вы получите ошибку.

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

Чтобы пойти на один шаг за пределы вопроса OP. Скорее всего, эти измененные ETags затрудняют вашу жизнь, пытаясь сравнить их с клиентской стороной.

Если вы публикуете свои артефакты на S3 с помощью команд awscli (cp, sync и т.д.), пороговое значение по умолчанию, при котором, как представляется, используется многопользовательская загрузка, составляет 10 МБ. Недавние релизы awscli позволяют настроить этот порог, поэтому вы можете отключить мультипартимент и получить простой в использовании MD5 ETag:

aws configure set default.s3.multipart_threshold 64MB

Полная документация здесь: http://docs.aws.amazon.com/cli/latest/topic/s3-config.html

Следствием этого может быть снижение производительности загрузки (я, честно говоря, не заметил). Но результат состоит в том, что все файлы, размер которых меньше вашего настроенного порога, теперь будут иметь нормальные хэширования MD5, что значительно упростит их дельта-клиентскую сторону.

Для этого требуется несколько недавняя установка awscli. Моя предыдущая версия (1.2.9) не поддерживала этот параметр, поэтому мне пришлось обновить до 1.10.x.

Я смог установить свой порог до 1024 МБ успешно.

Ответ 10

Основываясь на ответах здесь, я написал реализацию Python, которая корректно вычисляет как многочастные, так и одночастные файлы ETags.

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))

По умолчанию chunk_size имеет значение 8 МБ, используемое официальным инструментом aws cli, и он делает многостраничную загрузку для 2+ кусков. Он должен работать как на Python 2, так и на 3.

Ответ 11

Конечно, многопользовательская загрузка файлов может быть распространенной проблемой. В моем случае я обслуживал статические файлы через S3, и файл etag of.js выглядел отличным от локального файла, даже когда контент был тем же.

Оказывается, даже несмотря на то, что контент был тем же, это потому, что окончание строк было другим. Я исправил концы строк в моем репозитории git, загрузил измененные файлы на S3, и теперь он отлично работает.

Ответ 12

Вот версия С#

    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;
    }

Ответ 13

Улучшение ответов @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

Ответ 14

Я построил ответ на r03 и имею для этого отдельную утилиту Go: https://github.com/lambfrier/calc_s3_etag

Пример использования:

$ 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

Ответ 15

Пример python отлично работает, но при работе с Bamboo они устанавливают размер детали на 5 МБ, что НЕ СТАНДАРТ!! (s3cmd - 15 МБ) Также настроено на использование 1024 для вычисления байтов.

Пересмотрен для работы в бамбуковых артефактах s3 repos.

import hashlib
import binascii


# 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 = 5 * 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()