【问题标题】:What is the algorithm to compute the Amazon-S3 Etag for a file larger than 5GB?为大于 5GB 的文件计算 Amazon-S3 Etag 的算法是什么?
【发布时间】:2012-08-24 14:21:12
【问题描述】:

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

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

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

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

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

【问题讨论】:

  • 澄清一下,问题不在于如果文件超过 5GB,ETag 算法会发生某种变化。 ETag 算法对于非分段上传和分段上传是不同的。如果使用一个 5MB 部分和一个 1MB 部分上传,您在尝试计算 6MB 文件的 ETag 时会遇到同样的问题。 MD5 用于非分段上传,上限为 5GB。我的答案中的算法用于分段上传,每部分的上限为 5GB。
  • 启用服务器端加密也会有所不同。我认为 etag 可能应该被视为实现细节,而不是依赖于客户端。
  • @wim 知道启用 SSE 时如何计算 ETag?
  • 没有。而且我不认为这甚至是可能的 - 能够从 etag 本身推断出关于内容的任何东西将与加密的目标背道而驰,如果已知的有效负载可以预测的话复制相同的 etag 那么这将是信息泄漏。

标签: amazon-s3 s3cmd


【解决方案1】:

假设您将一个 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 个部分。

备注

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

代码链接

【讨论】:

  • 有趣的发现,希望亚马逊不会改变它,因为它是无证的功能
  • 好点。根据 HTTP 规范,ETag 完全由他们自行决定,唯一的保证是他们不能为更改的资源返回相同的 ETag。我猜想改变算法并没有太大的优势。
  • 有没有办法从 etag 中计算出“零件尺寸”?
  • “计算”不,“猜测”也许。如果 ETag 以“-4”结尾,则您知道有四个部分,但最后一部分的大小可以小至 1 个字节,直至该部分大小。因此,将文件大小除以部分数量可以给您一个估计值,但是当部分数量很少时,例如-2,更难猜。如果您有多个使用相同部分大小上传的文件,您还可以查找相邻的部分计数,例如-4 和 -5 并缩小零件尺寸的范围,例如-2 时为 1.9MB,-3 时为 2.1MB 表示部分大小为 2MB 正负 100KB。
  • 我认为依赖 AWS 的内部实现是不明智的,只要他们不将其哈希算法作为合同公开,特别是如果它影响应用程序的正确性(通常是这种情况)当您验证数据的完整性时。
【解决方案2】:

根据此处的答案,我编写了一个 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 下工作。

【讨论】:

  • 似乎我的块大小是 16MB 使用官方 aws cli 工具,也许他们更新了它?
  • 这对我来说适用于 8MB 块大小的 ~20GB 文件。我使用 aws cli 2.1.15 上传到带有深度存档存储类的 s3。
  • 很好!! 200GB 已确认,谢谢!!如果可以的话,请加倍支持。
【解决方案3】:

bash implementation

python implementation

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

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

【讨论】:

  • 这并没有真正解释算法是如何工作的,等等。(没有-1 btw)
  • 我在逐步列表中添加了实际算法。我写了 python 实现,整天都在翻阅关于如何做到这一点的帖子,其中大多数都充满了不正确或过时的信息。
  • 这似乎不起作用。使用 8 (MB) 的默认块大小,我得到了一个与亚马逊告诉我正确的不同的 etag。
  • @Cory 我不能代表 bash 脚本,但 python 实现存在文件大小小于 8MB 块大小的问题。但是有一个拉取请求正在纠正这个问题。
  • 花了我很多时间,但它的 python 版本对我有用,块大小为 16 (MB),我认为这可能是新的默认块大小
【解决方案4】:

不确定是否有帮助:

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

在我们的例子中:

文件:bucket/Foo.mpg.gpg

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

我们不知道算法,但由于我们可以“修复”ETag,我们也无需担心。

【讨论】:

  • 它不适用于大于 5GB 的文件 :( 你有解决方法吗?
  • 似乎这已停止工作,至少对于我正在检查的文件而言。
  • 我也发现了这个技巧,试图理解为什么通过 Web 界面上传的文件的 Etag 突然没有按预期方式计算。而在 2019 年,这仍然有效并且成功了。知道为什么会发生这种情况吗?
  • 无论如何,依靠 Etag 来比较文件似乎不是一个好主意(除了计算时间很长),因为该算法没有记录在案,它有时会被破坏。实际上,S3 系统元数据似乎包含文件 MD5 (docs.aws.amazon.com/AmazonS3/latest/dev/…),它可能会回答原始问题。但我还没有测试检索这个元数据。
【解决方案5】:

同样的算法,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();
}

【讨论】:

  • 我该死的英雄!!!!!!!!!我花了很多时间试图让二进制编码正确......我不知道番石榴有这个功能。
  • 非常好,就像一个魅力。请注意:如果需要,您可以使用 apache-commons 中的 oneliner DigestUtils.md5Hex(raw) 而不是 Guava Hasher。
  • @Pom12 你能在打字稿中转换这个函数吗?
【解决方案6】:

这是这个疯狂的 AWS 挑战拼图中的另一部分。

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

这个答案解决的是不得不“猜测”或以其他方式“判断”原始上传部分大小的烦恼。

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

所以... 你怎么知道物体的零件尺寸?

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

此外,如果您想要准确(也许某些多部分上传使用可变部分大小),那么您可以继续调用 head_object 请求并指定每个部分编号,并根据返回的部分内容长度计算每个部分的 MD5。

希望对您有所帮助...

【讨论】:

  • 注意:我最近不得不更新我的代码以遵循我在最后一段中的建议。我们遇到了一个具有多个不同零件尺寸的物体!去图吧!
【解决方案7】:

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

【讨论】:

    【解决方案8】:

    根据 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 摘要。

    【讨论】:

      【解决方案9】:

      这是 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 ...

      【讨论】:

      • 不错!我确认这对我有用。小改动:最后一个“multi_part_hash”应该是“multipart_hash”。我还在主要部分周围添加了一个“ARGV.each do”循环,并在最后添加了一个打印以使其成为命令行脚本。
      【解决方案10】:

      这里是计算 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;
              }
          }
      }
      

      【讨论】:

        【解决方案11】:

        简短的回答是您获取每个部分的 128 位二进制 md5 摘要,将它们连接成一个文档,然后对该文档进行哈希处理。 this answer 中提出的算法是准确的。

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

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

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

        【讨论】:

          【解决方案12】:

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

          【讨论】:

          • 当文件大小正好是一个块的大小时,该算法的行为与 S3 的行为不完全相同。不过,这可能取决于该工具是如何完成上传的。
          • 感谢@bernardn 指出这一点——我的图书馆刚刚遇到问题,AWS 最近是否可能改变了这一点? github.com/pyramation/etag-hash/issues/1
          • 如果 AWS 最近发生了变化,我相信这个解决方案现在对于 1 个块是正确的,而之前可能是不正确的。但是,我正在尝试在更新库之前进行尽职调查,以确保它已正式更改。
          • @pyramation 我重新测试了我的工具here,我认为 AWS 实施没有任何变化,因为我的测试仍然成功。可能会发生变化的是通过 Web 界面或 aws-cli 上传文件的方式。
          • 我采用了一种不同的方法,似乎与原始 bash 实现相匹配:stackoverflow.com/a/70375683/492325
          【解决方案13】:

          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

          【讨论】:

            【解决方案14】:

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

            【讨论】:

              【解决方案15】:

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

              使用该零件大小可以使用前面描述的方法为我提供正确的 ETag 哈希值。感谢 @TheStoryCoder 提供 php 版本。

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

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

              【讨论】:

                【解决方案16】:

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

                零件必须圆整

                【讨论】:

                  【解决方案17】:

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

                  我已将其发布到 npm

                  npm install s3-etag
                  
                  import { generateETag } from 's3-etag';
                  
                  const etag = generateETag(absoluteFilePath, partSizeInBytes);
                  

                  在这里查看项目:https://github.com/badsyntax/s3-etag

                  【讨论】:

                    【解决方案18】:

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

                    $ 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'-' - -
                    }
                    $ 
                    

                    【讨论】:

                      【解决方案19】:

                      不,

                      目前还没有匹配普通文件ETag和Multipart文件ETag和本地文件MD5的解决方案。

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 2012-07-31
                        • 2019-03-17
                        • 2016-09-13
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多