Amazon S3にアップロードされた5 GB未満のファイルには、ファイルのMD5ハッシュであるETagがあります。これにより、ローカルファイルがS3に置いたものと同じかどうかを簡単に確認できます。
ただし、ファイルが5GBを超える場合、AmazonはETagを異なる方法で計算します。
たとえば、380個の5,970,150,664バイトのファイルをマルチパートでアップロードしました。現在、S3は6bcf86bed8807b8e78f0fc6e0a53079d-380
のETagを持つことを示しています。私のローカルファイルには702242d3703818ddefe6bf7da2bed757
のmd5ハッシュがあります。ダッシュの後の数字は、マルチパートアップロードのパーツの数だと思います。
また、新しいETag(ダッシュの前)はまだMD5ハッシュであると思われますが、マルチパートアップロードの途中で何らかの形でメタデータが含まれています。
Amazon S3と同じアルゴリズムを使用してETagを計算する方法を知っている人はいますか?
確認しただけです。推測できるほど単純にするために、Amazonに嫌気がさします。
14MBのファイルをアップロードし、パーツサイズが5MBであるとします。各部分に対応する3つのMD5チェックサム、つまり最初の5MB、2番目の5MB、最後の4MBのチェックサムを計算します。次に、それらの連結のチェックサムを取ります。 MD5チェックサムはバイナリデータの16進表現であるため、ASCIIまたはUTF-8でエンコードされた連結ではなく、デコードされたバイナリ連結のMD5を取得することを確認してください。 ETagを取得する部品の数。
Mac OS Xでコンソールから実行するコマンドは次のとおりです。
$ dd bs=1m count=5 skip=0 if=someFile | md5 >>checksums.txt
5+0 records in
5+0 records out
5242880 bytes transferred in 0.019611 secs (267345449 bytes/sec)
$ dd bs=1m count=5 skip=5 if=someFile | md5 >>checksums.txt
5+0 records in
5+0 records out
5242880 bytes transferred in 0.019182 secs (273323380 bytes/sec)
$ dd bs=1m count=5 skip=10 if=someFile | md5 >>checksums.txt
2+1 records in
2+1 records out
2599812 bytes transferred in 0.011112 secs (233964895 bytes/sec)
この時点で、すべてのチェックサムはchecksums.txt
にあります。それらを連結して16進数をデコードし、ロットのMD5チェックサムを取得するには、単に
$ xxd -r -p checksums.txt | md5
そして、3つの部分があるため、「-3」を追加してETagを取得します。
Mac OS Xのmd5
はチェックサムを書き込むだけですが、Linuxのmd5sum
もファイル名を出力することに注意してください。これを削除する必要がありますが、チェックサムのみを出力するオプションがあると確信しています。 xxd
が空白を無視するので、空白について心配する必要はありません。
注:aws s3 cp
経由で aws-cli を使用してアップロードした場合、ほとんどの場合8MBのチャンクサイズがあります。 docs によると、これがデフォルトです。
Update: https://github.com/Teachnova/s3md5 でこれの実装について聞いたOS Xでは動作しません。これは OS Xの動作スクリプト で書いた要点です。
同じアルゴリズム、Javaバージョン:(BaseEncoding、Hasher、Hashingなどは guavaライブラリ
/**
* Generate checksum for object came from multipart upload</p>
* </p>
* AWS S3 spec: Entity tag that identifies the newly created object's data. Objects with different object data will have different entity tags. The entity tag is an opaque string. The entity tag may or may not be an MD5 digest of the object data. If the entity tag is not an MD5 digest of the object data, it will contain one or more nonhexadecimal characters and/or will consist of less than 32 or more than 32 hexadecimal digits.</p>
* Algorithm follows AWS S3 implementation: https://github.com/Teachnova/s3md5</p>
*/
private static String calculateChecksumForMultipartUpload(List<String> md5s) {
StringBuilder stringBuilder = new StringBuilder();
for (String md5:md5s) {
stringBuilder.append(md5);
}
String hex = stringBuilder.toString();
byte raw[] = BaseEncoding.base16().decode(hex.toUpperCase());
Hasher hasher = Hashing.md5().newHasher();
hasher.putBytes(raw);
String digest = hasher.hash().toString();
return digest + "-" + md5s.size();
}
それが役立つかどうかわからない:
現在、ugい(しかし今のところは便利な)ハックを行っています-fixそれらwrong ETagsアップロードされたマルチパートファイルでは、バケット内のファイルに変更を適用することから成ります; Amazonからmd5の再計算がトリガーされ、ETagが実際のmd5署名と一致するように変更されます。
私たちの場合には:
ファイル:bucket/Foo.mpg.gpg
アルゴリズムはわかりませんが、ETagを「修正」できるので、心配する必要もありません。
ここでの回答に基づいて、マルチパートファイルとシングルパートファイルの両方の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のドキュメントによると、ETagはマルチパートアップロードや暗号化オブジェクトのMD5ハッシュではありません: http://docs.aws.Amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html
PUTオブジェクト、POSTオブジェクト、またはコピー操作、またはAWSマネジメントコンソールを介して作成され、SSE-S3またはプレーンテキストによって暗号化されたオブジェクトには、オブジェクトのMD5ダイジェストであるETagがありますデータ。
PUTオブジェクト、POSTオブジェクト、またはコピー操作、またはAWSマネジメントコンソールを介して作成され、SSE-CまたはSSE-KMSによって暗号化されたオブジェクトは、MD5ダイジェストではないETagを持っていますオブジェクトデータの。
オブジェクトがマルチパートアップロードまたはパートコピー操作で作成された場合、暗号化の方法に関係なく、ETagはMD5ダイジェストではありません。
上記の回答では、5Gより大きいファイルに対してmd5を取得する方法があるかと尋ねられました。
MD5値(5Gを超えるファイルの場合)を取得するための答えは、メタデータに手動で追加するか、プログラムを使用して情報を追加するアップロードを行うことです。
たとえば、s3cmdを使用してファイルをアップロードし、次のメタデータを追加しました。
$ aws s3api head-object --bucket xxxxxxx --key noarch/epel-release-6-8.noarch.rpm
{
"AcceptRanges": "bytes",
"ContentType": "binary/octet-stream",
"LastModified": "Sat, 19 Sep 2015 03:27:25 GMT",
"ContentLength": 14540,
"ETag": "\"2cd0ae668a585a14e07c2ea4f264d79b\"",
"Metadata": {
"s3cmd-attrs": "uid:502/gname:staff/uname:xxxxxx/gid:20/mode:33188/mtime:1352129496/atime:1441758431/md5:2cd0ae668a585a14e07c2ea4f264d79b/ctime:1441385182"
}
}
これはETagを使用した直接的なソリューションではありませんが、必要なメタデータ(MD5)にアクセスできる方法で入力する方法です。誰かがメタデータなしでファイルをアップロードすると、失敗します。
ここにRubyのアルゴリズムがあります...
require 'digest'
# PART_SIZE should match the chosen part size of the multipart upload
# Set here as 10MB
PART_SIZE = 1024*1024*10
class File
def each_part(part_size = PART_SIZE)
yield read(part_size) until eof?
end
end
file = File.new('<path_to_file>')
hashes = []
file.each_part do |part|
hashes << Digest::MD5.hexdigest(part)
end
multipart_hash = Digest::MD5.hexdigest([hashes.join].pack('H*'))
multipart_etag = "#{multipart_hash}-#{hashes.count}"
Rubyで最も短いHex2Bin および S3へのマルチパートアップロード... に感謝
簡単な答えは、各部分の128ビットバイナリmd5ダイジェストを取得し、それらをドキュメントに連結して、そのドキュメントをハッシュすることです。 この回答 に示されているアルゴリズムは正確です。
注:blobに「タッチ」すると(コンテンツを変更しなくても)、ハイフン付きのマルチパートETAGフォームはハイフンなしのフォームに変更されます。つまり、完成したマルチパートアップロードされたオブジェクト(別名PUT-COPY)のコピー、またはインプレースコピーを行うと、S3はアルゴリズムの単純なバージョンでETAGを再計算します。つまり、宛先オブジェクトにはハイフンのないetagがあります。
おそらくこれはすでに検討しているでしょうが、ファイルが5GB未満で、MD5を既に知っていて、アップロードの並列化のメリットがほとんどない場合(たとえば、遅いネットワークからアップロードをストリーミングしたり、遅いディスクからアップロードしたりする場合) )、マルチパートPUTの代わりに単純なPUTを使用することを検討し、リクエストヘッダーで既知のContent-MD5を渡すことができます-一致しない場合、Amazonはアップロードに失敗します。 UploadPartごとに課金されることに注意してください。
さらに、一部のクライアントでは、PUT操作の入力に既知のMD5を渡すことにより、クライアントが転送中にMD5を再計算するのを防ぎます。 boto3(python)では、たとえば client.put_object() メソッドのContentMD5
パラメーターを使用します。パラメーターを省略し、MD5を既に知っている場合、クライアントは転送前に再度計算してサイクルを浪費することになります。
そして、ここにPHP ETagの計算バージョンがあります:
function calculate_aws_etag($filename, $chunksize) {
/*
DESCRIPTION:
- calculate Amazon AWS ETag used on the S3 service
INPUT:
- $filename : path to file to check
- $chunksize : chunk size in Megabytes
OUTPUT:
- ETag (string)
*/
$chunkbytes = $chunksize*1024*1024;
if (filesize($filename) < $chunkbytes) {
return md5_file($filename);
} else {
$md5s = array();
$handle = fopen($filename, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, $chunkbytes);
$md5s[] = md5($buffer);
unset($buffer);
}
fclose($handle);
$concat = '';
foreach ($md5s as $indx => $md5) {
$concat .= hex2bin($md5);
}
return md5($concat) .'-'. count($md5s);
}
}
$etag = calculate_aws_etag('path/to/myfile.ext', 8);
そして、予想されるETagに対して検証できる拡張バージョンがあります-知らない場合はチャンクサイズを推測することもできます!
function calculate_etag($filename, $chunksize, $expected = false) {
/*
DESCRIPTION:
- calculate Amazon AWS ETag used on the S3 service
INPUT:
- $filename : path to file to check
- $chunksize : chunk size in Megabytes
- $expected : verify calculated etag against this specified etag and return true or false instead
- if you make chunksize negative (eg. -8 instead of 8) the function will guess the chunksize by checking all possible sizes given the number of parts mentioned in $expected
OUTPUT:
- ETag (string)
- or boolean true|false if $expected is set
*/
if ($chunksize < 0) {
$do_guess = true;
$chunksize = 0 - $chunksize;
} else {
$do_guess = false;
}
$chunkbytes = $chunksize*1024*1024;
$filesize = filesize($filename);
if ($filesize < $chunkbytes && (!$expected || !preg_match("/^\\w{32}-\\w+$/", $expected))) {
$return = md5_file($filename);
if ($expected) {
$expected = strtolower($expected);
return ($expected === $return ? true : false);
} else {
return $return;
}
} else {
$md5s = array();
$handle = fopen($filename, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, $chunkbytes);
$md5s[] = md5($buffer);
unset($buffer);
}
fclose($handle);
$concat = '';
foreach ($md5s as $indx => $md5) {
$concat .= hex2bin($md5);
}
$return = md5($concat) .'-'. count($md5s);
if ($expected) {
$expected = strtolower($expected);
$matches = ($expected === $return ? true : false);
if ($matches || $do_guess == false || strlen($expected) == 32) {
return $matches;
} else {
// Guess the chunk size
preg_match("/-(\\d+)$/", $expected, $match);
$parts = $match[1];
$min_chunk = ceil($filesize / $parts /1024/1024);
$max_chunk = floor($filesize / ($parts-1) /1024/1024);
$found_match = false;
for ($i = $min_chunk; $i <= $max_chunk; $i++) {
if (calculate_aws_etag($filename, $i) === $expected) {
$found_match = true;
break;
}
}
return $found_match;
}
} else {
return $return;
}
}
}
node.jsの実装-
const fs = require('fs');
const crypto = require('crypto');
const chunk = 1024 * 1024 * 5; // 5MB
const md5 = data => crypto.createHash('md5').update(data).digest('hex');
const getEtagOfFile = (filePath) => {
const stream = fs.readFileSync(filePath);
if (stream.length < chunk) {
return md5(stream);
}
const md5Chunks = [];
const chunksNumber = Math.ceil(stream.length / chunk);
for (let i = 0; i < chunksNumber; i++) {
const chunkStream = stream.slice(i * chunk, (i + 1) * chunk);
md5Chunks.Push(md5(chunkStream));
}
return `${md5(Buffer.from(md5Chunks.join(''), 'hex'))}-${chunksNumber}`;
};
Ddやxxdなどの外部ヘルパーを使用せずに、iOSとmacOSのソリューションを持っています。見つけたばかりなので、そのまま報告し、後の段階で改善する予定です。差し当たり、Objective-CとSwiftコードの両方に依存しています。まず、Objective-Cでこのヘルパークラスを作成します。
AWS3MD5Hash.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AWS3MD5Hash : NSObject
- (NSData *)dataFromFile:(FILE *)theFile startingOnByte:(UInt64)startByte length:(UInt64)length filePath:(NSString *)path singlePartSize:(NSUInteger)partSizeInMb;
- (NSData *)dataFromBigData:(NSData *)theData startingOnByte:(UInt64)startByte length:(UInt64)length;
- (NSData *)dataFromHexString:(NSString *)sourceString;
@end
NS_ASSUME_NONNULL_END
AWS3MD5Hash.m
#import "AWS3MD5Hash.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 256
@implementation AWS3MD5Hash
- (NSData *)dataFromFile:(FILE *)theFile startingOnByte:(UInt64)startByte length:(UInt64)length filePath:(NSString *)path singlePartSize:(NSUInteger)partSizeInMb {
char *buffer = malloc(length);
NSURL *fileURL = [NSURL fileURLWithPath:path];
NSNumber *fileSizeValue = nil;
NSError *fileSizeError = nil;
[fileURL getResourceValue:&fileSizeValue
forKey:NSURLFileSizeKey
error:&fileSizeError];
NSInteger __unused result = fseek(theFile,startByte,SEEK_SET);
if (result != 0) {
free(buffer);
return nil;
}
NSInteger result2 = fread(buffer, length, 1, theFile);
NSUInteger difference = fileSizeValue.integerValue - startByte;
NSData *toReturn;
if (result2 == 0) {
toReturn = [NSData dataWithBytes:buffer length:difference];
} else {
toReturn = [NSData dataWithBytes:buffer length:result2 * length];
}
free(buffer);
return toReturn;
}
- (NSData *)dataFromBigData:(NSData *)theData startingOnByte: (UInt64)startByte length:(UInt64)length {
NSUInteger fileSizeValue = theData.length;
NSData *subData;
if (startByte + length > fileSizeValue) {
subData = [theData subdataWithRange:NSMakeRange(startByte, fileSizeValue - startByte)];
} else {
subData = [theData subdataWithRange:NSMakeRange(startByte, length)];
}
return subData;
}
- (NSData *)dataFromHexString:(NSString *)string {
string = [string lowercaseString];
NSMutableData *data= [NSMutableData new];
unsigned char whole_byte;
char byte_chars[3] = {'\0','\0','\0'};
NSInteger i = 0;
NSInteger length = string.length;
while (i < length-1) {
char c = [string characterAtIndex:i++];
if (c < '0' || (c > '9' && c < 'a') || c > 'f')
continue;
byte_chars[0] = c;
byte_chars[1] = [string characterAtIndex:i++];
whole_byte = strtol(byte_chars, NULL, 16);
[data appendBytes:&whole_byte length:1];
}
return data;
}
@end
ここで、プレーンSwiftファイルを作成します。
AWS Extensions.Swift
import UIKit
import CommonCrypto
extension URL {
func calculateAWSS3MD5Hash(_ numberOfParts: UInt64) -> String? {
do {
var fileSize: UInt64!
var calculatedPartSize: UInt64!
let attr:NSDictionary? = try FileManager.default.attributesOfItem(atPath: self.path) as NSDictionary
if let _attr = attr {
fileSize = _attr.fileSize();
if numberOfParts != 0 {
let partSize = Double(fileSize / numberOfParts)
var partSizeInMegabytes = Double(partSize / (1024.0 * 1024.0))
partSizeInMegabytes = ceil(partSizeInMegabytes)
calculatedPartSize = UInt64(partSizeInMegabytes)
if calculatedPartSize % 2 != 0 {
calculatedPartSize += 1
}
if numberOfParts == 2 || numberOfParts == 3 { // Very important when there are 2 or 3 parts, in the majority of times
// the calculatedPartSize is already 8. In the remaining cases we force it.
calculatedPartSize = 8
}
if mainLogToggling {
print("The calculated part size is \(calculatedPartSize!) Megabytes")
}
}
}
if numberOfParts == 0 {
let string = self.memoryFriendlyMd5Hash()
return string
}
let hasher = AWS3MD5Hash.init()
let file = fopen(self.path, "r")
defer { let result = fclose(file)}
var index: UInt64 = 0
var bigString: String! = ""
var data: Data!
while autoreleasepool(invoking: {
if index == (numberOfParts-1) {
if mainLogToggling {
//print("Siamo all'ultima linea.")
}
}
data = hasher.data(from: file!, startingOnByte: index * calculatedPartSize * 1024 * 1024, length: calculatedPartSize * 1024 * 1024, filePath: self.path, singlePartSize: UInt(calculatedPartSize))
bigString = bigString + MD5.get(data: data) + "\n"
index += 1
if index == numberOfParts {
return false
}
return true
}) {}
let final = MD5.get(data :hasher.data(fromHexString: bigString)) + "-\(numberOfParts)"
return final
} catch {
}
return nil
}
func memoryFriendlyMd5Hash() -> String? {
let bufferSize = 1024 * 1024
do {
// Open file for reading:
let file = try FileHandle(forReadingFrom: self)
defer {
file.closeFile()
}
// Create and initialize MD5 context:
var context = CC_MD5_CTX()
CC_MD5_Init(&context)
// Read up to `bufferSize` bytes, until EOF is reached, and update MD5 context:
while autoreleasepool(invoking: {
let data = file.readData(ofLength: bufferSize)
if data.count > 0 {
data.withUnsafeBytes {
_ = CC_MD5_Update(&context, $0, numericCast(data.count))
}
return true // Continue
} else {
return false // End of file
}
}) { }
// Compute the MD5 digest:
var digest = Data(count: Int(CC_MD5_DIGEST_LENGTH))
digest.withUnsafeMutableBytes {
_ = CC_MD5_Final($0, &context)
}
let hexDigest = digest.map { String(format: "%02hhx", $0) }.joined()
return hexDigest
} catch {
print("Cannot open file:", error.localizedDescription)
return nil
}
}
struct MD5 {
static func get(data: Data) -> String {
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
let _ = data.withUnsafeBytes { bytes in
CC_MD5(bytes, CC_LONG(data.count), &digest)
}
var digestHex = ""
for index in 0..<Int(CC_MD5_DIGEST_LENGTH) {
digestHex += String(format: "%02x", digest[index])
}
return digestHex
}
// The following is a memory friendly version
static func get2(data: Data) -> String {
var currentIndex = 0
let bufferSize = 1024 * 1024
//var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
// Create and initialize MD5 context:
var context = CC_MD5_CTX()
CC_MD5_Init(&context)
while autoreleasepool(invoking: {
var subData: Data!
if (currentIndex + bufferSize) < data.count {
subData = data.subdata(in: Range.init(NSMakeRange(currentIndex, bufferSize))!)
currentIndex = currentIndex + bufferSize
} else {
subData = data.subdata(in: Range.init(NSMakeRange(currentIndex, data.count - currentIndex))!)
currentIndex = currentIndex + (data.count - currentIndex)
}
if subData.count > 0 {
subData.withUnsafeBytes {
_ = CC_MD5_Update(&context, $0, numericCast(subData.count))
}
return true
} else {
return false
}
}) { }
// Compute the MD5 digest:
var digest = Data(count: Int(CC_MD5_DIGEST_LENGTH))
digest.withUnsafeMutableBytes {
_ = CC_MD5_Final($0, &context)
}
var digestHex = ""
for index in 0..<Int(CC_MD5_DIGEST_LENGTH) {
digestHex += String(format: "%02x", digest[index])
}
return digestHex
}
}
追加します:
#import "AWS3MD5Hash.h"
objective-Cブリッジングヘッダーに。このセットアップで大丈夫です。
使用例
この設定をテストするには、AWS接続の処理を担当するオブジェクト内で次のメソッドを呼び出します。
func getMd5HashForFile() {
let credentialProvider = AWSCognitoCredentialsProvider(regionType: AWSRegionType.USEast2, identityPoolId: "<INSERT_POOL_ID>")
let configuration = AWSServiceConfiguration(region: AWSRegionType.APSoutheast2, credentialsProvider: credentialProvider)
configuration?.timeoutIntervalForRequest = 3.0
configuration?.timeoutIntervalForResource = 3.0
AWSServiceManager.default().defaultServiceConfiguration = configuration
AWSS3.register(with: configuration!, forKey: "defaultKey")
let s3 = AWSS3.s3(forKey: "defaultKey")
let headObjectRequest = AWSS3HeadObjectRequest()!
headObjectRequest.bucket = "<NAME_OF_YOUR_BUCKET>"
headObjectRequest.key = self.latestMapOnServer.key
let _: AWSTask? = s3.headObject(headObjectRequest).continueOnSuccessWith { (awstask) -> Any? in
let headObjectOutput: AWSS3HeadObjectOutput? = awstask.result
var ETag = headObjectOutput?.eTag!
// Here you should parse the returned Etag and extract the number of parts to provide to the helper function. Etags end with a "-" followed by the number of parts. If you don't see this format, then pass 0 as the number of parts.
ETag = ETag!.replacingOccurrences(of: "\"", with: "")
print("headObjectOutput.ETag \(ETag!)")
let mapOnDiskUrl = self.getMapsDirectory().appendingPathComponent(self.latestMapOnDisk!)
let hash = mapOnDiskUrl.calculateAWSS3MD5Hash(<Take the number of parts from the ETag returned by the server>)
if hash == ETag {
print("They are the same.")
}
print ("\(hash!)")
return nil
}
}
サーバーから返されたETagのETagの末尾に「-」がない場合は、calculateAWSS3MD5Hashに0を渡すだけです。問題が発生した場合はコメントしてください。私はSwiftのみのソリューションに取り組んでいます。終了したらすぐにこの回答を更新します。ありがとうございます。