2012-08-29 18 views
30

I file caricati su Amazon S3 di dimensioni inferiori a 5 GB dispongono di un ETag che è semplicemente l'hash MD5 del file, che consente di verificare facilmente se i file locali sono uguali a quelli inseriti in S3.Qual è l'algoritmo per calcolare l'Etag Amazon-S3 per un file più grande di 5 GB?

Ma se il file è più grande di 5 GB, Amazon calcola l'ETag in modo diverso.

Ad esempio, ho effettuato un caricamento multipart di un file di byte 5970150664 in 380 parti. Ora S3 mostra di avere un ETag di 6bcf86bed8807b8e78f0fc6e0a53079d-380. Il mio file locale ha un hash MD5 di 702242d3703818ddefe6bf7da2bed757. Penso che il numero dopo il trattino sia il numero di parti nel caricamento multipart.

Sospetto anche che il nuovo ETag (prima del trattino) sia ancora un hash MD5, ma con alcuni metadati inclusi lungo il percorso dal caricamento multipart in qualche modo.

Qualcuno sa come calcolare l'Etag utilizzando lo stesso algoritmo di Amazon S3?

+6

Giusto per chiarire, il problema non è che l'algoritmo ETag in qualche modo cambia se il file è superiore a 5 GB. L'algoritmo ETag è diverso per i caricamenti non multipart e per i caricamenti multipart. Incontrerai lo stesso problema cercando di calcolare l'ETag di un file da 6 MB se è stato caricato utilizzando una parte da 5 MB e una da 1 MB. MD5 viene utilizzato per caricamenti non multipart, che hanno un limite di 5 GB. L'algoritmo nella mia risposta è utilizzato per i caricamenti multipart, che hanno un limite di 5 GB per parte. –

+0

È anche diverso se la crittografia lato server è abilitata. Penso che etag dovrebbe probabilmente essere considerato come un dettaglio di implementazione, e non essere affidato al lato client. – wim

risposta

52

Appena verificato uno. Un saluto affettuoso ad Amazon per renderlo abbastanza semplice da essere indovinato.

Supponiamo che tu abbia caricato un file da 14 MB e che la dimensione della parte sia 5 MB. Calcola 3 checksum MD5 corrispondenti a ciascuna parte, ovvero il checksum del primo 5 MB, il secondo 5 MB e gli ultimi 4 MB. Quindi prendi il checksum della loro concatenazione. Poiché i checksum MD5 sono rappresentazioni esadecimali di dati binari, assicurati di prendere l'MD5 della concatenazione binaria decodificata, non della concatenazione codificata ASCII o UTF-8. Quando ciò è fatto, aggiungi un trattino e il numero di parti per ottenere l'ETag.

Qui ci sono i comandi per farlo su Mac OS X dalla console:

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

A questo punto tutti i checksum sono in checksums.txt.Per concatenare loro e decodificare l'esagono e ottenere il checksum MD5 del lotto, basta usare

$ xxd -r -p checksums.txt | md5 

E ora aggiungere "-3" per ottenere l'ETag, dato che c'erano 3 parti.

Vale la pena notare che il md5 su Mac OS X scrive solo il checksum, ma md5sum su Linux restituisce anche il nome file. Dovrai spogliarlo, ma sono sicuro che ci sono alcune opzioni per emettere solo i checksum. Non devi preoccuparti della causa dello spazio bianco, xxd lo ignorerai.

Nota: Se hai caricato con aws-cli via aws s3 cp allora è più probabile avere un chunksize 8MB. Secondo lo docs, questo è il valore predefinito.

Aggiornamento: mi è stato detto circa l'implementazione di questo a https://github.com/Teachnova/s3md5, che non funziona su OS X. Ecco un Gist che ho scritto con un working script for OS X.

+0

interessante scoperta, sperando che Amazon non la cambierà dal momento che è una funzionalità non documentata – sanyi

+0

Buon punto: secondo la specifica HTTP, l'ETag è completamente a sua discrezione, l'unica garanzia è che non possono restituire lo stesso ETag per una risorsa modificata. Suppongo che non ci sia molto vantaggio cambiare l'algoritmo però –

+1

Esiste un modo per calcolare la "dimensione della parte" fuori dall'etag? – DavidG

-1

No,

Fino ad ora non c'è soluzione per abbinare normale ETag di file e file di ETag Multipart e MD5 del file locale.

7

Non sono sicuro se può aiutare:

momento stiamo facendo un brutto (ma finora utile) per incidere correzione quei ETags sbagliate in file caricati più parti, che consiste nell'applicare una modifica il file nel secchio; che innesca un ricalcolo MD5 da Amazon che modifica l'ETag alle corrispondenze con la firma md5 effettiva.

Nel nostro caso:

File: Benna/Foo.mpg.gpg

  1. ETag ottenuto: "3f92dffef0a11d175e60fb8b958b4e6e-2"
  2. Fare qualcosa con il file (rinominarlo , aggiungere un meta-dati come un'intestazione falsa, tra gli altri)
  3. Etag ottenuto: "c1d903ca1bb6dc68778ef21e74cc15b0"

Non conosciamo l'algoritmo, ma dato che possiamo "aggiustare" l'ETag, non dobbiamo nemmeno preoccuparcene.

+1

Impressionante trovare! Grazie! – d33pika

+1

Non funziona su file più grandi di 5 GB :(Hai una soluzione per questo? – d33pika

+0

Sembra che questo abbia smesso di funzionare, almeno per il file che sto controllando. – phunehehe

7

stesso algoritmo, la versione Java: (BaseEncoding, Hasher, hashing, ecc deriva dal 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(); 
} 
+0

Il mio fottuto eroe !!!!!!!!! Trascorro molte MOLTE ore cercando di ottenere la codifica binaria corretta ... Non sapevo che guava avesse questa funzionalità. – nterry

3

In una risposta di cui sopra, qualcuno ha chiesto se ci fosse un modo per ottenere il MD5 per i file più grandi di 5G.

Una risposta che potrei dare per ottenere il valore MD5 (per i file più grandi di 5G) sarebbe quella di aggiungerlo manualmente ai metadati, o utilizzare un programma per fare i tuoi upload che aggiungeranno le informazioni

Ad esempio, ho usato s3cmd per caricare un file e aggiunto i seguenti metadati.

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

Non è una soluzione diretta utilizzando l'ETag, ma è un modo per popolare i metadati che si desidera (MD5) in modo da potervi accedere. Non funzionerà ancora se qualcuno carica il file senza metadati.

4

bash implementation

python implementation

L'algoritmo è letteralmente (copiato dal readme nella realizzazione python):

  1. md5 i pezzi
  2. glob le stringhe MD5 insieme
  3. convert the glob to binary
  4. md5 il binario del pezzo globbed md5s
  5. aggiungere "-Number_of_chunks" alla fine della stringa md5 del binario
+0

Questo non spiega in realtà come funziona l'algoritmo, ecc.(non -1 btw) –

+0

Ho aggiunto l'algoritmo attuale in una lista passo passo. Ho scritto l'implementazione python attraverso i post su come farlo tutto il giorno, la maggior parte dei quali pieni di informazioni errate o obsolete. – tlastowka

+0

Questo non sembra funzionare. Usando la dimensione del blocco predefinito di 8 (MB) ho ottenuto un etag diverso da quello che amazon mi dice sia corretto. – Cory

1

Ed ecco una versione di PHP per calcolare l'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); 

E qui c'è una versione migliorata che può verificare contro un ETag previsto - e persino indovinare il chunksize se non lo conosci!

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

Ecco l'algoritmo 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}" 

Grazie a Shortest Hex2Bin in Ruby e Multipart Uploads to S3 ...

+0

Bello! Confermo che funziona per me Cambiamento minore: l'ultimo "multi_part_hash" dovrebbe essere "multipart_hash". Ho anche aggiunto un ciclo "ARGV.each do" attorno alla parte principale e una stampa alla fine per renderlo uno script da riga di comando. –

1

Secondo la documentazione AWS l'ETag non è un hash MD5 per un multi-parte caricare né per un oggetto crittografato: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html

Oggetti creati dal Obbligazione PUT ect, POST Object, o Copy operation, o tramite AWS Management Console, e sono crittografati da SSE-S3 o plaintext, dispongono di ETags che sono un digest MD5 dei loro dati oggetto.

Gli oggetti creati dall'oggetto PUT, dall'oggetto POST o dall'operazione Copia, o tramite la Console di gestione AWS e crittografati da SSE-C o SSE-KMS, dispongono di ETags che non sono un digest MD5 dei propri dati oggetto.

Se un oggetto viene creato dall'operazione Caricamento multipartuata o Copia parte, ETag non è un digest MD5, indipendentemente dal metodo di crittografia.

2

In base alle risposte qui, ho scritto un'implementazione Python che calcola correttamente sia gli ETags di file multiparte che di parti singole.

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

Il chunk_size di default è di 8 MB utilizzati dal aws cli strumento ufficiale, e lo fa caricare multipart per 2+ pezzi. Dovrebbe funzionare con Python 2 e 3.

Problemi correlati