J'ai un peu utilisé Amazon S3 pour les sauvegardes pendant un certain temps. Habituellement, après avoir téléchargé un fichier, je vérifie les correspondances MD5 afin de garantir une bonne sauvegarde. S3 a l'en-tête "etag" qui donnait cette somme.
Cependant, lorsque j'ai récemment téléchargé un fichier volumineux, 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 pas de documentation sur ce changement. J'ai vérifié à l'aide de la console de gestion S3 et de Cyberduck.
Je ne trouve aucune documentation sur ce changement. Des pointeurs?
Si un fichier est chargé en plusieurs parties, 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 auparavant.
Bucket Explorer vous fournit un transfert ETag normal jusqu’à 5 Go en mode multipart. Mais plus que cela ne fournit pas.
https://forums.aws.Amazon.com/thread.jspa?messageID=203510#203510
Amazon S3 calcule Etag avec un algorithme différent (et non MD5 Sum, comme d'habitude) lorsque vous téléchargez un fichier à l'aide de plusieurs parties.
Cet algorithme est détaillé ici: http://permalink.gmane.org/gmane.comp.file-systems.s3.s3tools/583
"Calculer 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 en bash pour le calculer, s3md5: https://github.com/Teachnova/s3md5
Par exemple, pour calculer l’Etag d’un fichier foo.bin qui a été chargé à l’aide de plusieurs parties avec une taille de bloc de 15 Mo,
# s3md5 15 foo.bin
Vous pouvez maintenant vérifier l'intégrité d'un très gros fichier (supérieur à 5 Go), car vous pouvez calculer l'Etag du fichier local et le comparer à 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 permettant de calculer Amazon ETag 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
}
La copie sur s3 avec aws s3 cp
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 put-object
de niveau inférieur.
aws s3api put-object --bucket bucketname --key remote/file --body local/file
Si vous utilisez des téléchargements en plusieurs parties, "etag" n'est pas la somme MD5 des données (voir Quel est l'algorithme permettant de 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 réelle des données MD5, sans téléchargement? 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
S3 devra alors recalculer la somme MD5 et la stocker en tant que "etag" de l'objet qui vient d'être copié. La commande "copy" s’exécute directement sur S3, c’est-à-dire qu’aucune donnée objet n’est transférée vers/depuis S3, de sorte que la bande passante nécessaire est réduite! (Remarque: n'utilisez pas s3cmd mv, cela effacerait vos données.)
La commande REST sous-jacente est la suivante:
PUT /key HTTP/1.1
Host: bucket.s3.amazonaws.com
x-amz-copy-source: /buckey/key
x-amz-metadata-directive: COPY
Sur la base des réponses fournies ici, j’ai écrit une implémentation Python qui calcule correctement les ETags de fichiers en plusieurs parties et en une 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))
La taille de bloc par défaut est 8 Mo, elle est utilisée par l'outil aws cli
officiel. Elle permet le téléchargement en plusieurs parties pour plus de 2 morceaux. Cela devrait fonctionner sous Python 2 et 3.
Cette page de support AWS - Comment garantir l'intégrité des données des objets téléchargés ou téléchargés à partir d'Amazon S3? - décrit un moyen plus fiable de vérifier l'intégrité de vos sauvegardes s3.
Tout d’abord, déterminez la somme md5 encodée 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 de l'indicateur --content-md5
, l'aide pour cet indicateur indique:
--content-md5 (string) The base64-encoded 128-bit MD5 digest of the part data.
Cela ne dit pas grand chose sur why d'utiliser cet indicateur, mais nous pouvons trouver cette information dans la documentation de l'API pour l'objet put :
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 compare l'objet à la valeur MD5 fournie et, s'ils ne correspondent pas, renvoie une erreur. En outre, vous pouvez calculer le MD5 en plaçant un objet dans Amazon S3 et comparer le rapport ETag renvoyé à la valeur MD5 calculée.
En utilisant cet indicateur, S3 vérifie que le fichier hash serveride correspond à la valeur spécifiée. Si les hachages correspondent, s3 renverra l'ETag:
{
"ETag": "\"599393a2c526c680119d84155d90f1e5\""
}
La valeur ETag sera généralement la somme hexadécimale md5 (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 aux métadonnées du fichier en guise de 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 commande head-object
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 le contenu 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 un peu plus loin que la question du PO, il y a de fortes chances que ces ETags tronqués vous rendent la vie difficile en essayant de les comparer côté client.
Si vous publiez vos artefacts sur S3 à l'aide des commandes awscli
(cp
, sync
, etc.), le seuil par défaut auquel le téléchargement multipart semble être utilisé est de 10 Mo. Les récentes versions awscli
vous permettent de configurer ce seuil. Vous pouvez donc désactiver le 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 ceci pourrait être rétrogradé (je ne l’ai honnêtement pas remarqué). Mais le résultat est que tous les fichiers plus petits que votre seuil configuré auront désormais des ETags de hachage MD5 normaux, ce qui les rendra beaucoup plus faciles à supprimer du 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, je devais donc passer à 1.10.x.
J'ai été en mesure de définir mon seuil jusqu'à 1024 Mo avec succès.
Pour améliorer les réponses de @ Spedge et @ Rob, voici une fonction python3 md5 qui prend la forme d’un fichier et ne repose pas sur l’obtention de 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
Bien sûr, le téléchargement de fichiers en plusieurs parties peut être un problème courant. Dans mon cas, je servais des fichiers statiques via S3 et le fichier etag du fichier .js était différent du fichier local, même si le contenu était identique.
Il 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 lignes dans mon référentiel git, j'ai chargé les fichiers modifiés sur S3 et tout fonctionne correctement 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;
}
J'ai construit sur la réponse de r03 et ai un utilitaire autonome Go pour cela ici: https://github.com/lambfrier/calc_s3_etag
Exemple d'utilisation:
$ 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