为大于5GB的文件计算Amazon-S3 Etag的算法是什么?

时间:2012-08-29 21:47:00

标签: amazon-s3 s3cmd

上传到Amazon S3的小于5GB的文件有一个ETag,它只是文件的MD5哈希值,这样可以很容易地检查您的本地文件是否与您在S3上的文件相同。

但是如果你的文件大于5GB,那么亚马逊会以不同的方式计算ETag。

例如,我在380个零件中进行了5,970,150,664字节文件的分段上传。现在S3显示它的ETag为6bcf86bed8807b8e78f0fc6e0a53079d-380。我的本地文件的md5散列为702242d3703818ddefe6bf7da2bed757。我认为破折号后面的数字是分段上传中的零件数量。

我还怀疑新ETag(破折号之前)仍然是MD5哈希值,但是在某种程度上从分段上传中包含了一些元数据。

有没有人知道如何使用与Amazon S3相同的算法计算ETag?

17 个答案:

答案 0 :(得分:66)

刚刚验证了一个。向亚马逊致敬,让它足够简单,可以猜到。

假设您上传了一个14MB的文件,您的零件尺寸为5MB。计算对应于每个部分的3个MD5校验和,即前5MB,第二个5MB和最后4MB的校验和。然后取其连接的校验和。由于MD5校验和是二进制数据的十六进制表示,因此请确保采用解码二进制连接的MD5,而不是ASCII或UTF-8编码级联。完成后,添加连字符和部件数量以获得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。要连接它们并解码十六进制并获得该批次的MD5校验和,只需使用

$ xxd -r -p checksums.txt | md5

现在附加“-3”来获得ETag,因为有3个部分。

值得注意的是,Mac OS X上的md5只写出了校验和,但Linux上的md5sum也输出了文件名。你需要删除它,但我确信有一些选项只能输出校验和。您无需担心空白原因xxd会忽略它。

注意:如果您通过aws s3 cp上传了aws-cli,那么您很可能拥有8MB的大块空间。根据{{​​3}},这是默认值。

更新:我被告知在docs处执行此操作,这在OS X上无效。这是我用https://github.com/Teachnova/s3md5编写的要点。< / p>

答案 1 :(得分:9)

相同的算法,java版本: (BaseEncoding,Hasher,Hashing等来自guava library

/**
 * 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();
}

答案 2 :(得分:8)

bash implementation

python implementation

算法字面上是(从python实现中的自述文件复制而来):

  1. md5 the chunks
  2. glob the md5 strings together
  3. 将glob转换为二进制
  4. md5 globbed chunk md5s的二进制文件
  5. 将“-Number_of_chunks”附加到二进制文件的md5字符串的末尾

答案 3 :(得分:7)

不确定它是否有帮助:

我们目前正在进行一项丑陋(但迄今为止有用)的黑客攻击修复多部分上传文件中的错误的ETags ,其中包括对文件应用更改在桶里;触发从Amazon重新计算的md5,将ETag更改为与实际的md5签名匹配。

在我们的案例中:

文件:bucket / Foo.mpg.gpg

  1. ETag获得:“3f92dffef0a11d175e60fb8b958b4e6e-2”
  2. 使用该文件某些重命名,添加虚假标题之类的元数据等)
  3. Etag获得:“c1d903ca1bb6dc68778ef21e74cc15b0”
  4. 我们不知道算法,但由于我们可以“修复”ETag,我们也不需要担心它。

答案 4 :(得分:7)

基于这里的答案,我编写了一个Python实现,可以正确计算多部分和单部分文件ETag。

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

官方aws cli工具使用的默认chunk_size是8 MB,它为2个以上的块进行分段上传。它应该在Python 2和3下工作。

答案 5 :(得分:5)

在上面的回答中,有人问是否有办法让md5获得大于5G的文件。

我可以给出获得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)的方法。如果有人上传没有元数据的文件,它仍然会失败。

答案 6 :(得分:4)

根据AWS文档,ETag不是多部分上传的MD5哈希,也不是加密对象:http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html

  

由PUT对象,POST对象或复制操作或通过AWS管理控制台创建并由SSE-S3或纯文本加密的对象具有ETag,这些ETag是其对象数据的MD5摘要。

     

由PUT对象,POST对象或复制操作或通过AWS管理控制台创建并由SSE-C或SSE-KMS加密的对象具有不是其对象数据的MD5摘要的ETag。 / p>      

如果通过分段上传或部分复制操作创建对象,则无论加密方法如何,ETag都不是MD5摘要。

答案 7 :(得分:2)

以下是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}"

感谢Shortest Hex2Bin in RubyMultipart Uploads to S3 ...

答案 8 :(得分:1)

这是计算ETag的PHP版本:

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 - 如果你不知道它,甚至猜测chunksize!

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;
        }
    }
}

答案 9 :(得分:1)

这是这个疯狂的AWS挑战难题中的又一块。

FWIW,此答案假设您已经弄清楚了如何计算“ MD5部件的MD5”,并且可以从此处已经提供的所有其他答案重建AWS Multi-part ETag。

此答案解决的烦恼是必须“猜测”或以其他方式“划分”原始上传的零件大小。

我们使用了几种不同的工具来上载到S3,并且它们似乎都具有不同的上载部分大小,因此“猜测”确实不是一种选择。另外,当零件尺寸似乎不同时,我们会上传许多文件。另外,使用旧的内部服务器副本强制创建MD5型ETag的技巧也不再起作用,因为AWS已将其内部服务器副本更改为也使用多部分(只是具有相当大的部分大小)。 / p>

所以... 如何找出物体的零件尺寸?

好吧,如果您首先发出一个head_object请求,并且检测到ETag是多部分类型的ETag(末尾包含一个'-'),那么您可以发出另一个head_object请求,但需要另外一个part_number属性为1(第一部分)。然后,该后续head_object请求将返回第一部分的content_length。 Viola ...现在您知道使用的零件尺寸,可以使用该尺寸重新创建您的本地ETag,该ETag应该与上载对象时创建的原始上载S3 ETag相匹配。

此外,如果您想精确(也许某些多部分上传内容使用可变的零件尺寸),则可以继续使用指定的每个零件编号调用head_object请求,并从返回的零件content_length计算每个零件的MD5。 / p>

希望有帮助...

答案 10 :(得分:0)

this answer中提出的算法是准确的。即,您获取每个部分的128位二进制md5摘要,将它们串联到一个文档中,然后对该文档进行哈希处理。

关于该算法,还有其他值得注意的地方:如果您复制或对已完成的多部分上传对象(也称为PUT-COPY)进行就地复制,则S3将重新计算ETAG并使用简单版本的算法。即目标对象将具有不带连字符的etag。

您可能已经考虑过这一点,但是如果您的文件小于5GB,并且已经知道它们的MD5,并且上传并行化几乎没有好处(例如,您正在从速度较慢的网络流式传输上传文件,或者从慢速磁盘),那么您也可以考虑使用简单的PUT而不是分段放置,并在请求标头中传递已知的Content-MD5-如果亚马逊不匹配,则上传将失败。请记住,您需要为每个UploadPart付费。

此外,在某些客户端中,将已知的MD5传递给PUT操作的输入将使客户端免于在传输过程中重新计算MD5。例如,在boto3(python)中,您将使用client.put_object()方法的ContentMD5参数。如果您省略该参数,而您已经知道MD5,那么客户端将浪费时间在传输之前再次对其进行计算。

答案 11 :(得分:0)

我有一个适用于iOS和macOS的解决方案,无需使用dd和xxd之类的外部帮助程序。我刚刚找到它,因此我将其报告为现状,并计划在以后的阶段进行改进。目前,它依赖于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的末尾没有“-”,则只需将0传递给calculateAWSS3MD5Hash。如果您遇到任何问题,请发表评论。我正在研究一种快速解决方案,我将在完成后立即更新此答案。谢谢

答案 12 :(得分:0)

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}`;
};

答案 13 :(得分:0)

Rust版本:

use crypto::digest::Digest;
use crypto::md5::Md5;
use std::fs::File;
use std::io::prelude::*;
use std::iter::repeat;

fn calculate_etag_from_read(f: &mut dyn Read, chunk_size: usize) -> Result<String> {
    let mut md5 = Md5::new();
    let mut concat_md5 = Md5::new();
    let mut input_buffer = vec![0u8; chunk_size];
    let mut chunk_count = 0;
    let mut current_md5: Vec<u8> = repeat(0).take((md5.output_bits() + 7) / 8).collect();

    let md5_result = loop {
        let amount_read = f.read(&mut input_buffer)?;
        if amount_read > 0 {
            md5.reset();
            md5.input(&input_buffer[0..amount_read]);
            chunk_count += 1;
            md5.result(&mut current_md5);
            concat_md5.input(&current_md5);
        } else {
            if chunk_count > 1 {
                break format!("{}-{}", concat_md5.result_str(), chunk_count);
            } else {
                break md5.result_str();
            }
        }
    };
    Ok(md5_result)
}

fn calculate_etag(file: &String, chunk_size: usize) -> Result<String> {
    let mut f = File::open(file)?;
    calculate_etag_from_read(&mut f, chunk_size)
}

查看带有简单实现的仓库:https://github.com/bn3t/calculate-etag/tree/master

答案 14 :(得分:0)

关于块大小,我注意到它似乎取决于零件数量。 作为AWS文档,最大零件数为10000。

因此,从默认的8MB开始,知道文件大小,块大小和部分可以按以下方式计算:

chunk_size=8*1024*1024
flsz=os.path.getsize(fl)

while flsz/chunk_size>10000:
  chunk_size*=2

parts=math.ceil(flsz/chunk_size)

零件必须四舍五入

答案 15 :(得分:0)

我刚刚看到,AWS S3控制台的“上传”使用了不寻常的部分(块)大小为17,179,870-至少对于较大的文件而言。

使用该零件尺寸,可以使用前面所述的方法为我提供正确的ETag哈希。感谢@TheStoryCoder的php版本。

感谢@hans,他的想法是使用头部对象查看每个零件的实际尺寸。

我使用AWS S3控制台(于2020年11月28日)上传了大约50个文件,大小从190MB到2.3GB不等,所有文件的部分大小都为17,179,870。

答案 16 :(得分:-2)

没有

到目前为止还没有匹配正常文件ETag和本地文件的Multipart文件ETag和MD5的解决方案。