Définition Etag modifiée dans Amazon S3

J'ai un peu utilisé Amazon S3 pour les sauvegardes depuis un certain temps. Habituellement, après avoir téléchargé un fichier, je vérifie les correspondances de somme MD5 pour m'assurer que j'ai fait une bonne sauvegarde. S3 A l'en-tête" etag " qui donnait cette somme.

Cependant, lorsque j'ai téléchargé un fichier volumineux récemment l'Etag ne semble plus être une somme md5. Il a des chiffres supplémentaires et un trait d'Union "696df35ad1161afbeb6ea667e5dd5dab-2861" . Je ne trouve aucune documentation sur ce changement. J'ai vérifié en utilisant la console de gestion S3 et avec Cyberduck.

Je ne trouve aucune documentation sur ce changement. Les pointeurs?

42
demandé sur jjh 2011-07-06 06:49:38

14 réponses

Si un fichier est téléchargé avec multipart, vous obtiendrez toujours ce type D'ETag. Mais si vous téléchargez un fichier entier en tant que Fichier unique, vous obtiendrez ETag comme avant.

Bucket Explorer vous fournissant ETag normal jusqu'à 5 Go de téléchargement en fonctionnement multipart. Mais plus alors il ne fournit pas.

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

31
répondu Tej Kiran 2012-09-22 21:56:32

Amazon S3 calcule Etag avec un algorithme différent (pas MD5 Sum, comme d'habitude) lorsque vous téléchargez un fichier en utilisant multipart.

Cet algorithme est détaillé ici : http://permalink.gmane.org/gmane.comp.file-systems.s3.s3tools/583

" Calculez le hachage MD5 pour chaque partie téléchargée du fichier, concaténer les hachages en une seule chaîne binaire et calculer le Hachage MD5 de ce résultat."

Je viens de développer un outil dans bash pour le calculer, s3md5 : https://github.com/Teachnova/s3md5

Par exemple, Pour calculer Etag d'un fichier foo.bin qui a été téléchargé en utilisant multipart avec une taille de bloc de 15 Mo, puis

# s3md5 15 foo.bin

Maintenant, vous pouvez vérifier l'intégrité d'un très gros fichier (plus grand que 5 Go) Car vous pouvez calculer L'Etag du fichier local et le comparer avec S3 Etag.

24
répondu Antonio Espinosa 2013-10-10 19:25:37

Aussi en 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()
18
répondu Spedge 2015-09-09 17:10:22

Voici un exemple dans 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
}

Ceci est juste un exemple, vous devriez Gérer les erreurs et les choses

5
répondu r03 2015-06-27 08:38:33

Voici une fonction powershell pour calculer L'ETag Amazon pour un fichier:

$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
répondu seanyboy 2015-07-18 09:37:34

Si vous utilisez des téléchargements en plusieurs parties, le "etag" n'est pas la somme MD5 des données (voir Quel est l'algorithme pour calculer L'Etag Amazon-S3 pour un fichier de plus de 5 Go?). On peut identifier ce cas par l'etag contenant un tiret "-".

Maintenant, la question intéressante est de savoir comment obtenir la somme MD5 réelle des données, sans télécharger ? Un moyen simple est de simplement "copier" l'objet sur lui-même, cela ne nécessite aucun téléchargement:

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

Cela provoquera S3 à recalculez la somme MD5 et stockez-la comme "etag" de l'objet juste copié. La commande" copy " s'exécute directement sur S3, c'est-à-dire qu'aucune donnée d'objet n'est transférée vers / depuis S3, donc cela nécessite peu de bande passante! (Remarque: n'utilisez pas s3cmd mv; cela supprimerait vos données.)

La commande REST sous-jacente est:

PUT /key HTTP/1.1
Host: bucket.s3.amazonaws.com
x-amz-copy-source: /buckey/key
x-amz-metadata-directive: COPY
3
répondu hrr 2017-05-23 11:47:05

Copier vers s3 avec {[1] } peut utiliser des téléchargements en plusieurs parties et l'etag résultant ne sera pas un md5, comme d'autres l'ont écrit.

Pour télécharger des fichiers sans multipart, utilisez la commande de niveau inférieur put-object.

aws s3api put-object --bucket bucketname --key remote/file --body local/file
2
répondu Synesso 2015-06-17 06:06:14

Cette page de support AWS - Comment puis-je assurer l'intégrité des données des objets téléchargés ou téléchargés depuis Amazon S3? - décrit un moyen plus fiable de vérifier l'intégrité de vos sauvegardes s3.

Déterminez tout d'abord le md5sum codé en base64 du fichier que vous souhaitez télécharger:

$ md5_sum_base64="$( openssl md5 -binary my-file | base64 )"

Ensuite, utilisez le s3api pour télécharger le fichier:

$ aws s3api put-object --bucket my-bucket --key my-file --body my-file --content-md5 "$md5_sum_base64"

Notez l'utilisation du drapeau --content-md5, l'aide pour ce drapeau indique:

--content-md5  (string)  The  base64-encoded  128-bit MD5 digest of the part data.

Cela ne dit pas grand-chose sur pourquoi utiliser cet indicateur, mais nous pouvons trouver cette information dans la documentation de l'API pour put object :

Pour vous assurer que les données ne sont pas corrompues sur le réseau, utilisez L'en-tête Content-MD5. Lorsque vous utilisez cet en-tête, Amazon S3 vérifie l'objet par rapport à la valeur MD5 fournie et, s'ils ne correspondent pas, renvoie une erreur. En outre, vous pouvez calculer le MD5 tout en plaçant un objet sur Amazon S3 et comparer L'ETag retourné au MD5 calculé valeur.

L'utilisation de cet indicateur amène S3 à vérifier que le hachage du fichier côté serveur correspond à la valeur spécifiée. Si les hachages correspondent à s3 retournera L'ETag:

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

La valeur ETag sera généralement le md5sum hexadécimal (voir cette question pour certains scénarios où cela peut ne pas être le cas).

Si le hachage ne correspond pas à celui que vous avez spécifié, vous obtenez une erreur.

A client error (InvalidDigest) occurred when calling the PutObject operation: The Content-MD5 you specified was invalid.

En plus de cela, vous pouvez également ajouter le fichier md5sum du fichier de métadonnées comme une vérification supplémentaire:

$ aws s3api put-object --bucket my-bucket --key my-file --body my-file --content-md5 "$md5_sum_base64" --metadata md5chksum="$md5_sum_base64"

Après le téléchargement, vous pouvez émettre la head-object commande pour vérifier les valeurs.

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

Voici un script bash qui utilise content md5 et ajoute des métadonnées, puis vérifie que les valeurs renvoyées par S3 correspondent aux hachages locaux:

#!/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"
2
répondu htaccess 2018-08-21 23:40:30

Pour aller au-delà de la question du PO.. les chances sont, ces etags chunked rendent votre vie difficile à essayer de les comparer côté client.

Si vous publiez vos artefacts dans S3 à l'aide des commandes awscli (cp, sync, etc), le seuil par défaut auquel le téléchargement multipart semble être utilisé est 10MB. Les versions récentes de awscli vous permettent de configurer ce seuil, de sorte que vous pouvez désactiver multipart et obtenir un ETag MD5 facile à utiliser:

aws configure set default.s3.multipart_threshold 64MB

Documentation complète ici: http://docs.aws.amazon.com/cli/latest/topic/s3-config.html

Une conséquence de cela pourrait {[16] } être rétrogradé les performances de téléchargement (honnêtement, je n'ai pas remarqué). Mais le résultat est que tous les fichiers plus petits que votre seuil configuré auront maintenant des ETags de hachage MD5 normaux, ce qui les rend beaucoup plus faciles à delta côté client.

Cela nécessite une installation awscli quelque peu récente. Ma version précédente (1.2.9) ne supportait pas cette option, j'ai donc dû passer à 1.10.X.

J'ai pu définir mon seuil jusqu'à 1024MB avec succès.

1
répondu jdolan 2017-03-13 13:05:49

Sur la base des réponses ici, j'ai écrit une implémentation Python qui calcule correctement les ETags de fichiers en plusieurs parties et en une seule partie.

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

Le chunk_size par défaut est de 8 Mo utilisé par l'outil officiel aws cli, et il télécharge en plusieurs parties pour plus de 2 morceaux. Cela devrait fonctionner sous Python 2 et 3.

1
répondu hyperknot 2017-05-06 10:22:25

Bien sûr, le téléchargement multipart des fichiers pourrait être un problème commun. Dans mon cas, je servais des fichiers statiques via S3 et l'etag de .le fichier js sortait pour être différent du fichier local même si le contenu était le même.

S'avère que même si le contenu était le même, c'était parce que les fins de ligne étaient différentes. J'ai corrigé les fins de ligne dans mon référentiel git, téléchargé les fichiers modifiés sur S3 et cela fonctionne bien maintenant.

0
répondu Gaurav Toshniwal 2014-02-19 07:35:57

Voici la version C #

    string etag = HashOf("file.txt",8);

Code Source

    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;
    }
0
répondu Pitipong Guntawong 2016-04-13 07:09:29

En améliorant la réponse de @Spedge et @Rob, voici une fonction python3 md5 qui prend un fichier et ne repose pas sur la capacité d'obtenir la taille du fichier avec 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
0
répondu Samuel Barbosa 2018-09-12 17:28:50

L'exemple python fonctionne très bien, mais lorsque vous travaillez avec du bambou, ils définissent la taille de la pièce à 5 Mo, ce qui N'est pas STANDARD!! (s3cmd est 15MB) également ajusté pour utiliser 1024 pour calculer les octets.

Révisé pour fonctionner pour bamboo artifact 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()
-1
répondu Rob 2015-09-09 16:29:46