繁体   English   中英

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

[英]What is the algorithm to compute the Amazon-S3 Etag for a file larger than 5GB?

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

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

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

我还怀疑新的 ETag(破折号之前)仍然是 MD5 hash,但在分段上传的过程中以某种方式包含了一些元数据。

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

假设您将一个 14MB 的文件上传到一个没有服务器端加密的存储桶,并且您的部分大小为 5MB。 计算每个部分对应的3个MD5校验和,即前5MB、后5MB、后4MB的校验和。 然后取它们连接的校验和。 MD5 校验和通常以二进制数据的十六进制表示形式打印,因此请确保采用解码二进制连接的 MD5,而不是 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 要连接它们并解码十六进制并获取该批次的 MD5 校验和,只需使用

$ xxd -r -p checksums.txt | md5

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

笔记

  • 如果您通过aws s3 cp使用aws-cli上传,那么您很可能拥有 8MB 的块大小。 根据docs ,这是默认设置。
  • 如果存储桶打开了服务器端加密 (SSE),则 ETag 将不是 MD5 校验和(请参阅API 文档)。 但是,如果您只是想验证上传的部分是否与您发送的Content-MD5匹配,您可以使用Content-MD5标头, S3 将为您进行比较
  • macOS 上的md5只写出校验和,但 Linux/brew 上的md5sum也会输出文件名。 你需要去掉它,但我确信有一些选项可以只输出校验和。 您无需担心空格,因为xxd会忽略它。

代码链接

基于这里的答案,我编写了一个 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(hashlib.md5().hexdigest())

    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 下运行。

bash 实现

蟒蛇实现

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

  1. md5 块
  2. 将 md5 字符串连接在一起
  3. 将 glob 转换为二进制
  4. md5 全局块 md5s 的二进制文件
  5. 将“-Number_of_chunks”附加到二进制文件的 md5 字符串的末尾

不确定它是否可以帮助:

我们目前正在做一个丑陋(但到目前为止有用)的 hack 来修复分段上传文件中那些错误的 ETag ,其中包括对存储桶中的文件应用更改; 这会触发 Amazon 的 md5 重新计算,将 ETag 更改为与实际的 md5 签名匹配。

在我们的例子中:

文件:bucket/Foo.mpg.gpg

  1. ETag 获得:“3f92dffef0a11d175e60fb8b958b4e6e-2”
  2. 对文件做一些事情重命名它,添加一个元数据,比如一个假标题,等等)
  3. 获得的 Etag:“c1d903ca1bb6dc68778ef21e74cc15b0”

我们不知道算法,但既然我们可以“修复”ETag,我们也不必担心它。

同算法,java版本:(BaseEncoding、Hasher、Hashing等来自番石榴库

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

在上面的回答中,有人问是否有办法为大于 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) 的方法。 如果有人上传没有元数据的文件,它仍然会失败。

根据 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 摘要,无论加密方法如何。

这是这个疯狂的 AWS 挑战难题中的另一部分。

FWIW,这个答案假设您已经知道如何计算“MD5 部分的 MD5”,并且可以从此处提供的所有其他答案中重建您的 AWS 多部分 ETag。

这个答案解决的是必须“猜测”或以其他方式“确定”原始上传部分大小的烦恼。

我们使用几种不同的工具上传到 S3,它们似乎都有不同的上传部分大小,所以“猜测”真的不是一种选择。 此外,我们有很多文件在历史上上传时部分大小似乎不同。 此外,使用内部服务器副本强制创建 MD5 类型 ETag 的老技巧也不再有效,因为 AWS 已将其内部服务器副本更改为也使用多部分(只是具有相当大的部分大小)。

所以...你怎么能算出物体的零件尺寸?

好吧,如果您首先发出一个 head_object 请求并检测到 ETag 是一个多部分类型的 ETag(最后包含一个“-<partcount>”),那么您可以发出另一个 head_object 请求,但附加一个 part_number 属性1(第一部分)。 这个后续的 head_object 请求将返回第一部分的 content_length。 中提琴...现在您知道所使用的部分大小,您可以使用该大小重新创建本地 ETag,该 ETag 应与上传对象时创建的原始上传 S3 ETag 相匹配。

此外,如果您想要准确(也许某些分段上传要使用可变的部分大小),那么您可以继续调用指定每个 part_number 的 head_object 请求,并根据返回的部分 content_length 计算每个部分的 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 ...

这是计算 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 进行验证 - 如果您不知道,甚至可以猜测块大小!

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

简短的回答是您获取每个部分的 128 位二进制 md5 摘要,将它们连接成一个文档,然后散列该文档。 此答案中提供的算法是准确的。

注意:如果您“触摸”blob(即使不修改内容),带连字符的多部分 ETAG 表单将更改为不带连字符的表单。 也就是说,如果您对已完成的分段上传对象(又名 PUT-COPY)进行复制或就地复制,S3 将使用算法的简单版本重新计算 ETAG。 即目标对象将有一个没有连字符的 etag。

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

此外,在某些客户端中,为 PUT 操作的输入传递已知的 MD5 将避免客户端在传输期间重新计算 MD5。 例如,在 boto3 (python) 中,您将使用client.put_object()方法的ContentMD5参数。 如果您省略该参数,并且您已经知道 MD5,那么客户端将在传输之前浪费周期再次计算它。

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

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

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

扩展Timothy Gonzalez 的回答

使用分段上传时,相同的文件将具有不同的 etag。

使用 WinSCP 很容易测试它,因为它使用分段上传。

当我通过 WinSCP 将同一文件的多个相同副本上传到 S3 时,每个副本都有不同的 etag。 当我下载它们并计算 md5 时,它们仍然相同。

因此,根据我测试的不同 etags 并不意味着文件不同。

我认为没有其他方法可以在不先下载 S3 文件的情况下获取任何 hash。

这适用于分段上传。 对于 not-multipart,应该仍然可以在本地计算 etag。

我有一个适用于 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 扩展.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即可计算AWSS3MD5Hash。 如果您遇到任何问题,请发表评论。 我正在研究唯一的快速解决方案,我会在完成后立即更新此答案。 谢谢

关于块大小,我注意到它似乎取决于零件的数量。 作为 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)

零件必须倒圆角

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

使用该部分大小使用前面描述的方法为我提供了正确的 ETag 哈希。 感谢@TheStoryCoder 提供 php 版本。

感谢@hans 提出使用 head-object 来查看每个部分的实际尺寸的想法。

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

在 Node.js (TypeScript) 中实现的工作算法。

/**
 * Generate an S3 ETAG for multipart uploads in Node.js 
 * An implementation of this algorithm: https://stackoverflow.com/a/19896823/492325
 * Author: Richard Willis <willis.rh@gmail.com>
 */
import fs from 'node:fs';
import crypto, { BinaryLike } from 'node:crypto';

const defaultPartSizeInBytes = 5 * 1024 * 1024; // 5MB

function md5(contents: string | BinaryLike): string {
  return crypto.createHash('md5').update(contents).digest('hex');
}

export function getS3Etag(
  filePath: string,
  partSizeInBytes = defaultPartSizeInBytes
): string {
  const { size: fileSizeInBytes } = fs.statSync(filePath);
  let parts = Math.floor(fileSizeInBytes / partSizeInBytes);
  if (fileSizeInBytes % partSizeInBytes > 0) {
    parts += 1;
  }
  const fileDescriptor = fs.openSync(filePath, 'r');
  let totalMd5 = '';

  for (let part = 0; part < parts; part++) {
    const skipBytes = partSizeInBytes * part;
    const totalBytesLeft = fileSizeInBytes - skipBytes;
    const bytesToRead = Math.min(totalBytesLeft, partSizeInBytes);
    const buffer = Buffer.alloc(bytesToRead);
    fs.readSync(fileDescriptor, buffer, 0, bytesToRead, skipBytes);
    totalMd5 += md5(buffer);
  }

  const combinedHash = md5(Buffer.from(totalMd5, 'hex'));
  const etag = `${combinedHash}-${parts}`;
  return etag;
}

我喜欢 Emerson 上面的主要答案——尤其是xxd部分——但我懒得使用dd所以我选择了split ,猜测块大小为 8M,因为我使用aws s3 cp上传:

$ split -b 8M large.iso XXX
$ md5sum XXX* > checksums.txt
$ sed -i 's/ .*$//' checksums.txt 
$ xxd -r -p checksums.txt | md5sum
99a090df013d375783f0f0be89288529  -
$ wc -l checksums.txt 
80 checksums.txt
$ 

很明显,我的 S3 etag 的两个部分都与我文件的计算 etag 相匹配。

更新:

这一直很好地工作:

$ ll large.iso
-rw-rw-r-- 1 user   user   669134848 Apr 12  2021 large.iso
$ 
$ etag large.iso
99a090df013d375783f0f0be89288529-80
$ 
$ type etag
etag is a function
etag () 
{ 
    split -b 8M --filter=md5sum $1 | cut -d' ' -f1 | pee "xxd -r -p | md5sum | cut -d' ' -f1" "wc -l" | paste -d'-' - -
}
$ 

所有其他答案均采用标准和常规零件尺寸。 但这种假设可能不正确。 在控制台和各种 SDK 中有不同的默认值。 而低级 API 确实允许多种多样。

并发症:

  • S3 分段上传可以有任何大小的部分(对于非最后部分,在最小和最大范围内)。
  • 即使是非最后部分也可以有不同的尺寸。
  • 当您上传时,它们不必是连续的零件号。
  • 如果你做一个只有 1 部分的多部分上传,etag 是更复杂的版本,而不是简单的 MD5
  • etags 倾向于用双引号引起来。 我不知道为什么。 但那只是一件可能会绊倒你的事情。

所以我们需要找出有多少个零件,它们有多大。

  • 您无法从boto3 的Object.parts_count属性可靠地获取零件数。 不知道其他SDK是否也是如此。
  • get_object_attributes API文档声称它返回一个部件和尺寸列表。 但是当我测试时,这些字段丢失了。 即使对于未完成的多部分上传。
  • 即使您假设各部分大小相等(最后一部分除外),您也无法从内容长度和部分数量推断出各部分大小。 例如,如果一个 90MB 的文件有 3 个部分,那么是 30MBx3,还是 40MB+40MB+10MB?

假设你有一个本地文件,你想检查它是否与 S3 中的 object 的内容相匹配。 (并假设您已经检查了长度是否不同,因为这样检查速度更快。)

这是执行此操作的 python3 脚本。 (我选择 python 只是因为这是我熟悉的。)

我们使用head_object来获取电子标签。 通过电子标签,我们可以推断出它是单部分上传还是多部分上传,以及有多少部分。 我们使用head_object传入PartNumber ,为每个部分调用它,以获取每个部分的长度。 您可以使用多处理来加快速度。 (注意boto3client不应在进程之间传递。)

import boto3
from hashlib import md5

def content_matches(local_path, bucket, key) -> bool:
    client = boto3.client('s3')
    resp = client.head_object(Bucket=bucket, Key=key)
    remote_e_tag = resp['ETag']
    total_length = resp['ContentLength']
    if '-' not in remote_e_tag:
        # it was a single-part upload
        m = md5()
        
        # you could read from the file in chunks to avoid loading the whole thing into memory
        # the chunks would not have to match any SDK standard. It can be whatever you want.
        # (The MD5 library will act as if you hashed in one go)
        with open(file, 'rb') as f:
            local_etag = f'"md5(f.read()).hexdigest()"'
        return local_etag == remote_e_tag
    else:
        # multi-part upload
        # to find the number of parts, get it from the e-tag
        # e.g. 123-56 has 56 parts
        num_parts = int(remote_e_tag.strip('"').split('-')[-1])
        print(f"Assuming {num_parts=} from {remote_e_tag=}")
        
        md5s = []
    
        with open(local_path, 'rb') as f:
            sz_read = 0
            
            for part_num in range(1,num_parts+1):
                resp = client.head_object(Bucket=bucket, Key=key, PartNumber=part_num)
                
                sz_read += resp['ContentLength']
                local_data_part = f.read(resp['ContentLength'])
                assert len(local_data_part) == resp['ContentLength'] # sanity check
                md5s.append(md5(local_data_part))
        assert sz_read == total_length, "Sum of part sizes doesn't equal total file size"
        digests = b''.join(m.digest() for m in md5s)
        digests_md5 = md5(digests)
        local_etag = f'"{digests_md5.hexdigest()}-{len(md5s)}"'
        return remote_e_tag == local_etag

还有一个脚本来测试所有这些边缘情况:

import boto3
from pprint import pprint
from hashlib import md5
from main import content_matches

MB = 2 ** 20

bucket = 'mybucket'
key = 'test-multi-part-upload'
local_path = 'test-data'

# first upload the object
s3 = boto3.resource('s3')
obj = s3.Object(bucket, key)
mpu = obj.initiate_multipart_upload()

parts = []
part_sizes = [6 * MB, 5 * MB, 5] # deliberately non-standard and not consistent
upload_part_nums = [1,3,8] # test non-consecutive part numbers for upload
with open(local_path, 'wb') as fw:
    with open('/dev/random', 'rb') as fr:
        for (part_num, part_size) in zip(upload_part_nums, part_sizes):
            part = mpu.Part(part_num)
            data = fr.read(part_size)
            print(f"Uploading part {part_num}")
            resp = part.upload(Body=data)
            parts.append({
                'ETag': resp['ETag'],
                'PartNumber': part_num
            })
            fw.write(data)

resp = mpu.complete(MultipartUpload={
    'Parts': parts
})

obj.reload()

assert content_matches(local_path, bucket, key)

不,

目前还没有办法匹配普通文件ETag和本地文件的多部分文件ETag和MD5。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM