Was ist der Algorithmus zum Berechnen des Amazon-S3 Etag für eine Datei größer als 5 GB?

73

Auf Amazon S3 hochgeladene Dateien, die kleiner als 5 GB sind, haben ein ETag, das einfach der MD5-Hash der Datei ist. Auf diese Weise können Sie leicht überprüfen, ob Ihre lokalen Dateien mit denen in S3 übereinstimmen.

Wenn Ihre Datei jedoch größer als 5 GB ist, berechnet Amazon das ETag anders.

Zum Beispiel habe ich einen mehrteiligen Upload einer 5.970.150.664-Byte-Datei in 380 Teilen durchgeführt. Jetzt zeigt S3, dass es einen ETag von hat 6bcf86bed8807b8e78f0fc6e0a53079d-380. Meine lokale Datei hat einen MD5-Hash von 702242d3703818ddefe6bf7da2bed757. Ich denke, die Zahl nach dem Bindestrich ist die Anzahl der Teile im mehrteiligen Upload.

Ich vermute auch, dass das neue ETag (vor dem Bindestrich) immer noch ein MD5-Hash ist, aber mit einigen Metadaten, die auf dem Weg vom mehrteiligen Upload irgendwie enthalten sind.

Weiß jemand, wie man das ETag mit demselben Algorithmus wie Amazon S3 berechnet?

broc.seib
quelle
14
Zur Verdeutlichung besteht das Problem nicht darin, dass sich der ETag-Algorithmus irgendwie ändert, wenn die Datei größer als 5 GB ist. Der ETag-Algorithmus unterscheidet sich für nicht mehrteilige Uploads und für mehrteilige Uploads. Beim Versuch, das ETag einer 6-MB-Datei zu berechnen, würde das gleiche Problem auftreten, wenn es mit einem 5-MB-Teil und einem 1-MB-Teil hochgeladen würde. MD5 wird für nicht mehrteilige Uploads verwendet, die auf 5 GB begrenzt sind. Der Algorithmus in meiner Antwort wird für mehrteilige Uploads verwendet, die auf 5 GB pro Teil begrenzt sind.
Emerson Farrugia
Dies ist auch anders, wenn die serverseitige Verschlüsselung aktiviert ist. Ich denke, etag sollte wahrscheinlich als Implementierungsdetail betrachtet werden und nicht auf der Client-Seite.
wim
@wim Hast du eine Idee, wie man den ETag berechnet, wenn SSE aktiviert ist?
Avihoo Mamka
1
Nein. Und ich erwarte nicht, dass dies überhaupt möglich sein wird - die Möglichkeit, aus dem etag selbst etwas über den Inhalt abzuleiten, würde in erster Linie dem Ziel der Verschlüsselung zuwiderlaufen, und wenn bekannte Nutzdaten das gleiche etag vorhersehbar reproduzieren würden Dies wäre ein Informationsleck.
wim

Antworten:

87

Gerade verifiziert. Hut ab vor Amazon, weil es so einfach ist, dass man es erraten kann.

Angenommen, Sie haben eine 14-MB-Datei hochgeladen und Ihre Teilegröße beträgt 5 MB. Berechnen Sie 3 MD5-Prüfsummen für jedes Teil, dh die Prüfsumme der ersten 5 MB, der zweiten 5 MB und der letzten 4 MB. Nehmen Sie dann die Prüfsumme ihrer Verkettung. Da MD5-Prüfsummen hexadezimale Darstellungen von Binärdaten sind, stellen Sie sicher, dass Sie das MD5 der decodierten binären Verkettung und nicht der ASCII- oder UTF-8-codierten Verkettung verwenden. Wenn dies erledigt ist, fügen Sie einen Bindestrich und die Anzahl der Teile hinzu, um das ETag zu erhalten.

Hier sind die Befehle, um dies unter Mac OS X über die Konsole zu tun:

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

Zu diesem Zeitpunkt sind alle Prüfsummen in checksums.txt. Um sie zu verketten und das Hex zu dekodieren und die MD5-Prüfsumme des Loses zu erhalten, verwenden Sie einfach

$ xxd -r -p checksums.txt | md5

Und jetzt "-3" anhängen, um den ETag zu erhalten, da es 3 Teile gab.

Es ist erwähnenswert, dass md5unter Mac OS X nur die Prüfsumme geschrieben wird, md5sumunter Linux jedoch auch der Dateiname ausgegeben wird. Sie müssen das entfernen, aber ich bin sicher, dass es eine Option gibt, nur die Prüfsummen auszugeben. Sie müssen sich keine Gedanken über Leerzeichen machen xxd, da diese ignoriert werden.

Hinweis : Wenn Sie mit aws-cli über hochgeladen aws s3 cphaben, haben Sie höchstwahrscheinlich eine 8-MB-Blockgröße. Laut den Dokumenten ist dies die Standardeinstellung.

Update : Unter https://github.com/Teachnova/s3md5 wurde mir über eine Implementierung informiert , die unter OS X nicht funktioniert. Hier ist eine Zusammenfassung, die ich mit einem funktionierenden Skript für OS X geschrieben habe .

Emerson Farrugia
quelle
1
interessante Entdeckung, in der Hoffnung, dass Amazon es nicht ändern wird, da es undokumentierte Funktion ist
Sanyi
Guter Punkt. Gemäß der HTTP-Spezifikation liegt das ETag völlig in ihrem Ermessen. Die einzige Garantie besteht darin, dass sie nicht dasselbe ETag für eine geänderte Ressource zurückgeben können. Ich vermute, es ist nicht sehr vorteilhaft, den Algorithmus zu ändern.
Emerson Farrugia
1
Gibt es eine Möglichkeit, die "Teilegröße" aus dem Etag zu berechnen?
DavidG
1
"Berechnen" nein, "raten" vielleicht. Wenn das ETag mit "-4" endet, wissen Sie, dass es vier Teile gibt, aber dieser letzte Teil kann eine Größe von nur 1 Byte bis zur Teilegröße haben. Wenn Sie also die Dateigröße durch die Anzahl der Teile dividieren, erhalten Sie eine Schätzung. Wenn die Anzahl der Teile jedoch gering ist, z. B. -2, ist es schwieriger zu erraten. Wenn Sie mehrere Dateien haben, die mit derselben Teilegröße hochgeladen wurden, können Sie auch nach benachbarten Teilezahlen suchen, z. B. -4 und -5, und die Teilegröße eingrenzen, z. B. 1,9 MB bei -2 und 2,1 MB bei - 3 bedeutet, dass die Teilegröße 2 MB plus oder minus 100 KB beträgt.
Emerson Farrugia
4
Ich denke nicht, dass es ratsam wäre, sich auf die interne Implementierung von AWS zu verlassen, solange sie ihren Hashing-Algorithmus nicht als Vertrag offenlegen, insbesondere wenn dies die Anwendungskorrektheit beeinträchtigt, was normalerweise der Fall ist, wenn Sie die Integrität von Daten überprüfen .
Iman
13

Basierend auf den Antworten hier habe ich eine Python-Implementierung geschrieben, die sowohl mehrteilige als auch einteilige Datei-ETags korrekt berechnet.

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

Die Standardgröße von chunk_size ist 8 MB, die vom offiziellen aws cliTool verwendet wird, und es werden mehrteilige Uploads für 2+ Chunks durchgeführt. Es sollte sowohl unter Python 2 als auch unter Python 3 funktionieren.

Hyperknoten
quelle
Anscheinend war meine Blockgröße 16 MB mit dem offiziellen aws cli-Tool. Vielleicht haben sie es aktualisiert?
SerialEnabler
11

Bash-Implementierung

Python-Implementierung

Der Algorithmus ist buchstäblich (aus der Readme-Datei in der Python-Implementierung kopiert):

  1. md5 die Brocken
  2. Glob die MD5-Strings zusammen
  3. Konvertieren Sie den Glob in Binär
  4. md5 die binärdatei des globbed chunk md5s
  5. Fügen Sie "-Number_of_chunks" an das Ende der md5-Zeichenfolge der Binärdatei an
tlastowka
quelle
Dies erklärt nicht wirklich, wie der Algorithmus funktioniert usw. (übrigens nicht -1)
Willem Van Onsem
Ich habe den eigentlichen Algorithmus in einer Schritt-für-Schritt-Liste hinzugefügt. Ich habe die Python-Implementierung geschrieben, in der ich den ganzen Tag über Beiträge dazu gelesen habe, die meisten davon voller falscher oder veralteter Informationen.
Tlastowka
2
Dies scheint nicht zu funktionieren. Bei Verwendung der Standardblockgröße von 8 (MB) habe ich ein anderes Etikett erhalten als von Amazon angegeben.
Cory
@Cory Ich kann nicht für das Bash-Skript sprechen, aber die Python-Implementierung hatte ein Problem mit Dateigrößen, die kleiner als die Blockgröße von 8 MB sind. Es gibt jedoch eine Pull-Anforderung, die dieses Problem behebt.
v.tralala
Es hat ewig gedauert, aber die Python-Version hat bei mir funktioniert, mit einer Blockgröße von 16 (MB), von der ich
annehme
9

Nicht sicher, ob es helfen kann:

Wir machen derzeit einen hässlichen (aber bisher nützlichen) Hack, um diese falschen ETags in mehrteilig hochgeladenen Dateien zu beheben. Dies besteht darin, eine Änderung an der Datei im Bucket vorzunehmen . Dies löst eine MD5-Neuberechnung von Amazon aus, bei der das ETag so geändert wird, dass es mit der tatsächlichen MD5-Signatur übereinstimmt.

In unserem Fall:

Datei: Bucket / Foo.mpg.gpg

  1. ETag erhalten: 3f92dffef0a11d175e60fb8b958b4e6e-2
  2. Machen Sie etwas mit der Datei ( benennen Sie sie um , fügen Sie Metadaten wie einen gefälschten Header hinzu)
  3. Etag erhalten: c1d903ca1bb6dc68778ef21e74cc15b0

Wir kennen den Algorithmus nicht, aber da wir den ETag "reparieren" können, müssen wir uns auch keine Sorgen machen.

juanjocv
quelle
2
Es funktioniert jedoch nicht bei Dateien, die größer als
5 GB sind
Scheint so, als ob dies nicht mehr funktioniert, zumindest für die Datei, die ich überprüfe.
Phunehehe
Ich habe diesen Trick auch entdeckt, um zu verstehen, warum die Etags von Dateien, die über die Weboberfläche hochgeladen wurden, plötzlich nicht mehr wie erwartet berechnet wurden. Und im Jahr 2019 funktioniert dies immer noch und macht den Trick. Irgendeine Idee, warum dies geschieht und immer noch der Fall ist?
Dletozeun
Auf jeden Fall scheint es keine gute Idee zu sein, sich beim Vergleichen von Dateien auf Etag zu verlassen (darüber hinaus ist die Berechnung langwierig), da der Algorithmus nicht dokumentiert ist und von Zeit zu Zeit kaputt geht. Tatsächlich scheinen S3-Systemmetadaten die Datei MD5 ( docs.aws.amazon.com/AmazonS3/latest/dev/… ) zu enthalten, die möglicherweise die ursprüngliche Frage beantworten würde. Ich habe diese Metadaten jedoch noch nicht getestet, um sie abzurufen.
Dletozeun
9

Gleicher Algorithmus, Java-Version: (BaseEncoding, Hasher, Hashing usw. stammen aus der Guavenbibliothek

/**
 * 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();
}
petertc
quelle
Mein verdammter Held !!!!!!!!! Ich verbringe viele VIELE Stunden damit, die Binärcodierung korrekt zu machen ... Ich wusste nicht, dass Guave diese Funktionalität hat.
nterry
Sehr schön, wirkt wie ein Zauber. Nur eine Anmerkung: Sie können bei Bedarf den Oneliner DigestUtils.md5Hex(raw)von apache-commonsanstelle von Guava Hasher verwenden.
Pom12
5

In einer obigen Antwort fragte jemand, ob es eine Möglichkeit gibt, den md5 für Dateien größer als 5G zu erhalten.

Eine Antwort, die ich geben könnte, um den MD5-Wert (für Dateien größer als 5 G) zu erhalten, wäre, ihn entweder manuell zu den Metadaten hinzuzufügen oder ein Programm zu verwenden, um Ihre Uploads durchzuführen, die die Informationen hinzufügen.

Zum Beispiel habe ich s3cmd verwendet, um eine Datei hochzuladen, und es wurden die folgenden Metadaten hinzugefügt.

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

Es ist keine direkte Lösung mit dem ETag, aber es ist eine Möglichkeit, die gewünschten Metadaten (MD5) so aufzufüllen, dass Sie darauf zugreifen können. Es schlägt immer noch fehl, wenn jemand die Datei ohne Metadaten hochlädt.

Cinderhaze
quelle
5

Gemäß der AWS-Dokumentation ist der ETag weder ein MD5-Hash für einen mehrteiligen Upload noch für ein verschlüsseltes Objekt: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html

Objekte, die vom PUT-Objekt-, POST-Objekt- oder Kopiervorgang oder über die AWS Management Console erstellt und mit SSE-S3 oder Klartext verschlüsselt wurden, verfügen über ETags, die einen MD5-Digest ihrer Objektdaten darstellen.

Objekte, die vom PUT-Objekt-, POST-Objekt- oder Kopiervorgang oder über die AWS Management Console erstellt und von SSE-C oder SSE-KMS verschlüsselt werden, verfügen über ETags, die keine MD5-Zusammenfassung ihrer Objektdaten darstellen.

Wenn ein Objekt entweder durch das Hochladen mehrerer Teile oder durch das Kopieren von Teilen erstellt wird, ist das ETag unabhängig von der Verschlüsselungsmethode kein MD5-Digest.

Timothy Gonzalez
quelle
3

Hier ist der Algorithmus in 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}"

Dank kürzester Hex2Bin in Ruby und mehrteiligen Uploads auf S3 ...

vince
quelle
Nett! Ich bestätige, dass dies für mich funktioniert. Kleinere Änderung: Das letzte "multi_part_hash" sollte "multipart_hash" sein. Ich habe auch eine "ARGV.each do" -Schleife um den Hauptteil und einen Ausdruck am Ende hinzugefügt, um daraus ein Befehlszeilenskript zu machen.
William Pietri
1

Und hier ist eine PHP-Version der Berechnung des 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);

Und hier ist eine erweiterte Version, die sich gegen einen erwarteten ETag verifizieren kann - und sogar die Chunksize erraten kann, wenn Sie es nicht wissen!

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;
        }
    }
}
TheStoryCoder
quelle
1

Die kurze Antwort lautet, dass Sie den 128-Bit-Binär-MD5-Digest jedes Teils nehmen, sie zu einem Dokument verketten und dieses Dokument hashen. Der in dieser Antwort dargestellte Algorithmus ist genau.

Hinweis: Das mehrteilige ETAG-Formular mit dem Bindestrich ändert sich in das Formular ohne Bindestrich, wenn Sie den Blob "berühren" (auch ohne den Inhalt zu ändern). Das heißt, wenn Sie Ihr abgeschlossenes mehrteilig hochgeladenes Objekt (auch bekannt als PUT-COPY) kopieren oder direkt kopieren, berechnet S3 die ETAG mit der einfachen Version des Algorithmus neu. dh das Zielobjekt hat ein Etag ohne Bindestrich.

Sie haben dies wahrscheinlich bereits in Betracht gezogen, aber wenn Ihre Dateien weniger als 5 GB groß sind und Sie ihre MD5 bereits kennen und die Upload-Parallelisierung wenig bis gar keinen Vorteil bietet (z. B. wenn Sie den Upload von einem langsamen Netzwerk streamen oder von einer langsamen Festplatte hochladen) ), dann können Sie auch die Verwendung eines einfachen PUT anstelle eines mehrteiligen PUT in Betracht ziehen und Ihr bekanntes Content-MD5 in Ihren Anforderungsheadern übergeben - amazon schlägt den Upload fehl, wenn sie nicht übereinstimmen. Beachten Sie, dass Ihnen für jedes UploadPart eine Gebühr berechnet wird.

Darüber hinaus wird bei einigen Clients durch Übergeben eines bekannten MD5 für die Eingabe einer PUT-Operation der Client daran gehindert, den MD5 während der Übertragung neu zu berechnen. In boto3 (Python) würden Sie beispielsweise den ContentMD5Parameter der Methode client.put_object () verwenden. Wenn Sie den Parameter weglassen und den MD5 bereits kennen, verschwendet der Client Zyklen, in denen er vor der Übertragung erneut berechnet wird.

init_js
quelle
1

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

Elad
quelle
1
Dieser Algorithmus verhält sich nicht genau so wie S3, wenn die Dateigröße genau der Größe eines Blocks entspricht. Dies kann jedoch davon abhängen, wie der Upload vom Tool durchgeführt wurde.
Bernard
0

Ich habe eine Lösung für iOS und MacOS ohne externe Helfer wie dd und xxd. Ich habe es gerade gefunden, also melde ich es so wie es ist und plane, es zu einem späteren Zeitpunkt zu verbessern. Im Moment basiert es sowohl auf Objective-C- als auch auf Swift-Code. Erstellen Sie zunächst diese Hilfsklasse in 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

Erstellen Sie nun eine einfache schnelle Datei:

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

}
}

Fügen Sie nun hinzu:

#import "AWS3MD5Hash.h"

zu Ihrem Objective-C Bridging-Header. Sie sollten mit diesem Setup einverstanden sein.

Anwendungsbeispiel

Um dieses Setup zu testen, können Sie die folgende Methode innerhalb des Objekts aufrufen, das für die Verarbeitung der AWS-Verbindungen zuständig ist:

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
    }



}

Wenn der vom Server zurückgegebene ETag am Ende des ETag kein "-" hat, übergeben Sie einfach 0 an berechneAWSS3MD5Hash. Bitte kommentieren Sie, wenn Sie auf Probleme stoßen. Ich arbeite an einer schnellen Lösung. Ich werde diese Antwort aktualisieren, sobald ich fertig bin. Vielen Dank

Alfonso Tesauro
quelle
0

Eine Version in 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)
}

Sehen Sie sich ein Repo mit einer einfachen Implementierung an: https://github.com/bn3t/calculate-etag/tree/master

bernardn
quelle
0

Hier ist noch ein Teil dieses verrückten AWS-Herausforderungspuzzles.

FWIW, diese Antwort setzt voraus, dass Sie bereits herausgefunden haben, wie der "MD5 von MD5-Teilen" berechnet wird, und dass Sie Ihr mehrteiliges AWS-ETag aus allen anderen hier bereits bereitgestellten Antworten neu erstellen können.

Was diese Antwort anspricht, ist der Ärger, die ursprüngliche Upload-Teilegröße "erraten" oder auf andere Weise "erraten" zu müssen.

Wir verwenden verschiedene Tools zum Hochladen in S3 und alle scheinen unterschiedliche Upload-Teilegrößen zu haben, daher war "Raten" wirklich keine Option. Außerdem haben wir viele Dateien, die historisch hochgeladen wurden, als die Teilegrößen unterschiedlich zu sein schienen. Der alte Trick, eine interne Serverkopie zu verwenden, um die Erstellung eines ETag vom Typ MD5 zu erzwingen, funktioniert ebenfalls nicht mehr, da AWS seine internen Serverkopien so geändert hat, dass sie auch mehrteilig sind (nur mit einer relativ großen Teilegröße).

Also ... Wie können Sie die Teilegröße des Objekts herausfinden?

Wenn Sie zuerst eine head_object-Anfrage stellen und feststellen, dass es sich bei dem ETag um ein mehrteiliges ETag handelt (das am Ende ein '- <partcount>' enthält), können Sie eine weitere head_object-Anfrage stellen, jedoch mit einem zusätzlichen part_number-Attribut von 1 (der erste Teil). Diese nachfolgende head_object-Anfrage gibt Ihnen dann die content_length des ersten Teils zurück. Viola ... Jetzt kennen Sie die verwendete Teilegröße und können diese Größe verwenden, um Ihr lokales ETag neu zu erstellen, das mit dem ursprünglich hochgeladenen S3-ETag übereinstimmen sollte, das beim Hochladen des Objekts erstellt wurde.

Wenn Sie genau sein möchten (möglicherweise sollten einige mehrteilige Uploads variable Teilegrößen verwenden), können Sie weiterhin head_object-Anforderungen mit jeder angegebenen Teilenummer aufrufen und die MD5 jedes Teils aus der zurückgegebenen Inhaltsinhaltslänge berechnen.

Hoffentlich hilft das...

Hans
quelle
0

In Bezug auf die Blockgröße habe ich festgestellt, dass dies anscheinend von der Anzahl der Teile abhängt. Die maximale Anzahl von Teilen beträgt 10000 als AWS-Dokumente.

Ab einem Standardwert von 8 MB und Kenntnis der Dateigröße, der Blockgröße und der Teile können Sie also wie folgt berechnen:

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

while flsz/chunk_size>10000:
  chunk_size*=2

parts=math.ceil(flsz/chunk_size)

Teile müssen abgerundet sein

Salva.
quelle
-3

Nein,

Bis jetzt gibt es keine Lösung, um die normale Datei ETag und die mehrteilige Datei ETag und MD5 der lokalen Datei abzugleichen.

Tej Kiran
quelle