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?
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
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.
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()
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
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
}
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
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
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"
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.
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.
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.
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;
}
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
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()