Каков алгоритм вычисления Amazon-S3 Etag для файла размером более 5 ГБ?

Файлы, загруженные в Amazon S3 размером менее 5 ГБ, имеют ETag, который представляет собой просто MD5-хеш файла, что позволяет легко проверить, совпадают ли ваши локальные файлы с тем, что вы положили на S3.

Но если ваш файл больше 5 ГБ, то Amazon вычисляет ETag по-другому.

Например, я выполнил многократную загрузку файла размером 5 970 150 664 байта в 380 частях. Теперь S3 показывает, что ETag имеет значение 6bcf86bed8807b8e78f0fc6e0a53079d-380. Мой локальный файл имеет хэш md5 702242d3703818ddefe6bf7da2bed757. Я думаю, что число после тире - это количество частей в многочастной загрузке.

Я также подозреваю, что новый ETag (до тире) по-прежнему является хешем MD5, но с некоторыми метаданными, включенными по пути из многоэтапной загрузки.

Кто-нибудь знает, как вычислить ETag, используя тот же алгоритм, что и Amazon S3?

Ответ 1

Просто проверено. Шляпы уходят в Амазонку, чтобы сделать ее достаточно простой, чтобы ее можно было догадаться.

Скажем, вы загрузили файл размером 14 МБ, а размер вашей части - 5 МБ. Вычислите 3 контрольные суммы MD5, соответствующие каждой части, то есть контрольную сумму первых 5 МБ, вторую 5 МБ и последние 4 МБ. Затем возьмите контрольную сумму их конкатенации. Поскольку контрольные суммы 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 части.

Стоит отметить, что md5 в Mac OS X просто выписывает контрольную сумму, но md5sum в Linux также выводит имя файла. Вам нужно снять это, но я уверен, что есть возможность выводить контрольные суммы. Вам не нужно беспокоиться о причине пробела xxd будет игнорировать его.

Примечание. Если вы загрузили aws-cli через aws s3 cp, то у вас, скорее всего, будет 8-мегабайтный чанкиз, Согласно docs, это по умолчанию.

Обновление. Мне рассказали о реализации этого в https://github.com/Teachnova/s3md5, который не работает OS X. Здесь Gist я написал с рабочим script для OS X.

Ответ 2

Тот же алгоритм, версия 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 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();
}

Ответ 3

реализация Bash

реализация Python

Алгоритм буквально (скопирован из readme в реализации Python):

  1. md5 куски
  2. объединить строки md5
  3. преобразовать глобус в двоичный файл
  4. MD5 бинарник сорванного фрагмента MD5S
  5. добавьте "-Number_of_chunks" в конец строки двоичного файла md5

Ответ 4

Не уверен, что это может помочь:

В настоящее время мы делаем уродливое (но до сих пор полезное) взломать, чтобы исправить эти неправильные ETags в многофайловых загруженных файлах, которые состоят в применении изменения к файлу в ведре; который запускает пересчет md5 из Amazon, который изменяет ETag на совпадения с фактической сигнатурой md5.

В нашем случае:

Файл: bucket/Foo.mpg.gpg

  • Получено ETag: "3f92dffef0a11d175e60fb8b958b4e6e-2"
  • Сделайте что-нибудь с файлом (переименуйте его, добавьте метаданные, например, поддельный заголовок).
  • Получено Etag: "c1d903ca1bb6dc68778ef21e74cc15b0"

Мы не знаем алгоритм, но поскольку мы можем "исправить" ETag, нам также не нужно беспокоиться об этом.

Ответ 5

Основываясь на ответах здесь, я написал реализацию 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))

Размер chunk_size по умолчанию составляет 8 МБ, используемый официальным инструментом aws cli, и он выполняет многоэтапную загрузку фрагментов 2+. Он должен работать под Python 2 и 3.

Ответ 6

В приведенном выше ответе кто-то спросил, есть ли способ получить 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) таким образом, чтобы вы могли получить к нему доступ. Он все равно будет терпеть неудачу, если кто-то загрузит файл без метаданных.

Ответ 7

Согласно документации AWS, ETag не является хешем MD5 ни для загрузки из нескольких частей, ни для зашифрованного объекта: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html

У объектов, созданных с помощью объекта PUT, объекта POST или операции копирования, или через Консоль управления AWS, и зашифрованных SSE-S3 или открытым текстом, есть ETag, которые являются дайджестом MD5 своих данных объекта.

У объектов, созданных с помощью объекта PUT, объекта POST или операции копирования, или через Консоль управления AWS, и зашифрованных SSE-C или SSE-KMS, есть ETag, которые не являются дайджестом MD5 своих данных объекта.

Если объект создается с помощью операции Multipart Upload или Part Copy, ETag не является дайджестом MD5, независимо от метода шифрования.

Ответ 8

Вот алгоритм в 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}"

Благодаря Кратчайший Hex2Bin в Ruby и Multipart Uploads to S3...

Ответ 9

И вот 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 - и даже догадываться о 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;
        }
    }
}

Ответ 10

Короткий ответ: вы берете 128-битный двоичный дайджест md5 каждой части, объединяете их в документ и хешируете этот документ. Алгоритм, представленный в этом ответе, является точным.

Примечание: составная форма ETAG с дефисом изменится на форму без дефиса, если вы "дотронетесь" до BLOB-объекта (даже без изменения содержимого). То есть, если вы копируете или делаете на месте копию вашего готового загруженного объекта, состоящего из нескольких частей (он же PUT-COPY), S3 повторно вычислит ETAG с простой версией алгоритма. то есть объект назначения будет иметь тег без дефиса.

Вы, наверное, уже рассмотрели это, но если ваши файлы меньше 5 ГБ, и вы уже знаете их MD5, и распараллеливание загрузки дает мало или ничего не дает (например, вы передаете загрузку из медленной сети или загрузку с медленного диска), тогда вы также можете рассмотреть возможность использования простого PUT вместо составного PUT и передать ваш известный Content-MD5 в заголовки ваших запросов - amazon не сможет выполнить загрузку, если они не совпадают. Имейте в виду, что вы платите за каждую UploadPart.

Кроме того, в некоторых клиентах передача известного MD5 для ввода операции PUT спасет клиента от повторного вычисления MD5 во время передачи. В boto3 (python) вы, например, используете параметр ContentMD5 метода client.put_object(). Если вы пропустите параметр, и вы уже знаете MD5, то клиент будет тратить циклы, вычисляя его снова перед передачей.

Ответ 11

У меня есть решение для 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 Bridging. Вы должны быть в порядке с этой настройкой.

Пример использования

Чтобы протестировать эту настройку, вы можете вызывать следующий метод внутри объекта, отвечающего за обработку соединений 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. Пожалуйста, прокомментируйте, если у вас возникнут какие-либо проблемы. Я работаю над быстрым решением, я обновлю этот ответ, как только закончу. Спасибо

Ответ 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}';
};

Ответ 13

Нет,

До сих пор не существует решения для соответствия нормальному файлу ETag и Multipart файлов ETag и MD5 локального файла.