しばらくの間、バックアップにAmazon S3を少し使用しました。通常、ファイルをアップロードした後、MD5の合計の一致をチェックして、適切なバックアップが作成されていることを確認します。 S3には、この合計を与えるために使用される「etag」ヘッダーがあります。
ただし、最近大きなファイルをアップロードしたとき、Etagはもはやmd5サムではないようです。余分な数字とハイフン "696df35ad1161afbeb6ea667e5dd5dab-2861"があります。この変更に関するドキュメントは見つかりません。 S3管理コンソールとCyberduckを使用して確認しました。
この変更に関するドキュメントが見つかりません。ポインタはありますか?
ファイルがマルチパートでアップロードされている場合、そのようなタイプのETagを常に取得します。ただし、ファイル全体を単一のファイルとしてアップロードすると、以前と同様にETagを取得できます。
バケットエクスプローラーは、マルチパート操作で5Gbアップロードまで通常のETagを提供します。しかし、それ以上は提供していません。
AWS:
マルチパートアップロードAPIを使用して作成されたオブジェクトのETagには、1つ以上の16進数以外の文字が含まれ、16桁未満または16桁以上の16進数で構成されます。
参照: https://forums.aws.Amazon.com/thread.jspa?messageID=203510#20351
マルチパートを使用してファイルをアップロードすると、Amazon S3は異なるアルゴリズム(通常はMD5 Sumではありません)でEtagを計算します。
このアルゴリズムの詳細は次のとおりです。 http://permalink.gmane.org/gmane.comp.file-systems.s3.s3tools/58
「ファイルのアップロードされた各部分のMD5ハッシュを計算し、ハッシュを単一のバイナリ文字列に連結し、その結果のMD5ハッシュを計算します。」
Bashで計算するツールs3md5を開発するだけです。 https://github.com/Teachnova/s3md5
たとえば、チャンクサイズが15 MBのマルチパートを使用してアップロードされたファイルfoo.binのEtagを計算するには、
# s3md5 15 foo.bin
ローカルファイルのEtagを計算し、それをS3 Etagと比較できるため、非常に大きなファイル(5GB以上)の整合性を確認できるようになりました。
また、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()
ファイルのAmazon ETagを計算するためのpowershell関数は次のとおりです。
$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
}
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
}
これは単なる例であり、エラーなどを処理する必要があります
aws s3 cp
を使用してs3にコピーすると、マルチパートアップロードを使用でき、他の人が書いたように、結果のetagはmd5になりません。
マルチパートなしでファイルをアップロードするには、下位レベルのput-object
コマンドを使用します。
aws s3api put-object --bucket bucketname --key remote/file --body local/file
マルチパートアップロードを使用する場合、「etag」はデータのMD5合計ではありません( 5GBを超えるファイルのAmazon-S3 Etagを計算するアルゴリズムは? を参照)。このケースは、ダッシュ「-」を含むetagで識別できます。
さて、興味深い質問は、データの実際のMD5合計を取得する方法ですダウンロードなしで? 1つの簡単な方法は、オブジェクトをそれ自体に「コピー」することです。これにはダウンロードは必要ありません。
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
ここでの回答に基づいて、マルチパートファイルとシングルパートファイルの両方のETagを正しく計算するPython実装を作成しました。
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は、公式のaws cli
ツール。2チャンク以上のマルチパートアップロードを行います。 Python 2と3.の両方で動作するはずです。
このAWSサポートページ- 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.
これはwhyでこのフラグを使用することについてあまり言及していませんが、この情報は putオブジェクトのAPIドキュメント :
ネットワークを通過するデータが破損しないようにするには、Content-MD5ヘッダーを使用します。このヘッダーを使用すると、Amazon S3は指定されたMD5値に対してオブジェクトをチェックし、それらが一致しない場合はエラーを返します。さらに、オブジェクトをAmazon S3に入れながらMD5を計算し、返されたETagを計算されたMD5値と比較できます。
このフラグを使用すると、S3はファイルハッシュサーバー側が指定された値と一致することを確認します。ハッシュがs3に一致する場合、ETagが返されます。
{
"ETag": "\"599393a2c526c680119d84155d90f1e5\""
}
ETag値は通常16進数のmd5sumです(これが当てはまらないシナリオについては この質問 を参照してください)。
ハッシュが指定したものと一致しない場合、エラーが発生します。
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=="
}
}
以下は、コンテンツmd5を使用してメタデータを追加し、S3によって返された値がローカルハッシュと一致することを確認するbashスクリプトです。
#!/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"
OPの問題をさらに一歩進めるには、チャンスがあります。これらのチャンク化されたETagにより、クライアント側で比較するのが難しくなります。
awscli
コマンド(cp
、sync
など)を使用してアーティファクトをS3に公開している場合、マルチパートアップロードが使用されると思われるデフォルトのしきい値は10MBです。最近のawscli
リリースでは、このしきい値を設定できるため、マルチパートを無効にして、使いやすい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まで正常に設定できました。
これが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;
}
@Spedgeと@Robの答えを改善したものが、ファイルのようなものを取り込んでos.path.getsize
でファイルサイズを取得できることに依存しないpython3 md5関数です。
# 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
もちろん、ファイルのマルチパートアップロードは一般的な問題です。私の場合、S3を介して静的ファイルを提供していましたが、コンテンツが同じであっても、.jsファイルのetagがローカルファイルと異なるようになりました。
内容が同じであっても、行末が異なるためであることがわかりました。 gitリポジトリの行末を修正し、変更されたファイルをS3にアップロードしましたが、現在は正常に動作します。
私は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