amazon s3 - aws - ¿Cuál es el algoritmo para calcular el Amazon-S3 Etag para un archivo de más de 5 GB?
aws getobject (10)
Aquí está el algoritmo en 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}"
Gracias a Shortest Hex2Bin en Ruby y Multipart Uploads a S3 ...
Los archivos cargados en Amazon S3 de menos de 5GB tienen un ETag que simplemente es el hash MD5 del archivo, lo que hace que sea fácil verificar si sus archivos locales son los mismos que los que puso en S3.
Pero si su archivo es más grande que 5GB, entonces Amazon calcula el ETag de manera diferente.
Por ejemplo, hice una carga multiparte de un archivo de bytes 5970150664 en 380 partes. Ahora S3 muestra que tiene un ETag de 6bcf86bed8807b8e78f0fc6e0a53079d-380. Mi archivo local tiene un hash md5 de 702242d3703818ddefe6bf7da2bed757. Creo que el número después del guión es el número de partes en la carga multiparte.
También sospecho que el nuevo ETag (antes del guión) sigue siendo un hash MD5, pero con algunos metadatos incluidos en el camino desde la carga multiparte de alguna manera.
¿Alguien sabe cómo calcular el Etag usando el mismo algoritmo que Amazon S3?
Basado en las respuestas aquí, escribí una implementación de Python que calcula correctamente ETags de archivos de varias partes y de una sola parte.
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))
El valor por defecto de chunk_size es de 8 MB utilizado por la herramienta oficial aws cli
, y carga varias partes para más de 2 fragmentos. Debería funcionar tanto en Python 2 como en 3.
En la respuesta anterior, alguien preguntó si había una forma de obtener el md5 para archivos de más de 5G.
Una respuesta que podría dar para obtener el valor de MD5 (para archivos de más de 5G) sería agregarlo manualmente a los metadatos, o usar un programa para hacer tus cargas que agregarán la información.
Por ejemplo, utilicé s3cmd para cargar un archivo y se agregaron los siguientes metadatos.
$ 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"
}
}
No es una solución directa que utiliza ETag, pero es una forma de completar los metadatos que desea (MD5) de una manera que pueda acceder a ellos. Todavía fallará si alguien sube el archivo sin metadatos.
Mismo algoritmo, versión java: (BaseEncoding, Hasher, Hashing, etc. proviene de la biblioteca guava
/**
* 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();
}
No estoy seguro de si puede ayudar:
Actualmente estamos haciendo un hack feo (pero hasta ahora útil) para corregir esos ETags incorrectos en archivos cargados en varias partes, que consiste en aplicar un cambio al archivo en el contenedor; que desencadena un recálculo md5 de Amazon que cambia el ETag a coincidencias con la firma md5 real.
En nuestro caso:
Archivo: cubo / Foo.mpg.gpg
- ETag obtenido: "3f92dffef0a11d175e60fb8b958b4e6e-2"
- Haz algo con el archivo ( renómbralo , agrega un metadato como un encabezado falso, entre otros)
- Etag obtuvo: "c1d903ca1bb6dc68778ef21e74cc15b0"
No conocemos el algoritmo, pero dado que podemos "corregir" el ETag, no tenemos que preocuparnos por eso tampoco.
No,
Hasta ahora no hay una solución que coincida con el ETag de archivo normal y el ETag de archivo multiparte y el archivo local MD5.
Según la documentación de AWS, ETag no es un hash MD5 para una carga de varias partes ni para un objeto cifrado: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html
Los objetos creados por el objeto PUT, el objeto POST o la operación Copiar, o a través de la consola de administración de AWS, y están encriptados por SSE-S3 o texto sin formato, tienen ETags que son un resumen MD5 de sus datos de objeto.
Los objetos creados por el objeto PUT, el objeto POST o la operación Copiar, o a través de la consola de administración de AWS, y están cifrados por SSE-C o SSE-KMS, tienen ETags que no son un resumen MD5 de sus datos de objeto.
Si se crea un objeto mediante la operación Multipart Upload o Part Copy, el ETag no es un resumen MD5, independientemente del método de encriptación.
Solo verificado uno. Felicitaciones a Amazon por hacerlo lo suficientemente simple para ser adivinable.
Supongamos que ha subido un archivo de 14 MB y su tamaño de pieza es de 5 MB. Calcule 3 sumas de control MD5 correspondientes a cada parte, es decir, la suma de comprobación de los primeros 5 MB, los segundos 5 MB y los últimos 4 MB. Luego tome la suma de comprobación de su concatenación. Como las sumas de comprobación MD5 son representaciones hexadecimales de datos binarios, solo asegúrese de tomar el MD5 de la concatenación binaria decodificada, no de la concatenación codificada ASCII o UTF-8. Cuando haya terminado, agregue un guión y la cantidad de partes para obtener el ETag.
Estos son los comandos para hacerlo en Mac OS X desde la consola:
$ 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)
En este punto, todas las sumas de comprobación están en checksums.txt
. Para concatenarlos y decodificar el hexágono y obtener la suma de control MD5 del lote, solo use
$ xxd -r -p checksums.txt | md5
Y ahora agregue "-3" para obtener el ETag, ya que había 3 partes.
Vale la pena señalar que md5
en Mac OS X simplemente escribe la suma de comprobación, pero md5sum
en Linux también genera el nombre del archivo. Necesitarás quitar eso, pero estoy seguro de que hay alguna opción para solo generar las sumas de comprobación. No necesita preocuparse por los espacios en blanco porque xxd
lo ignorará.
Nota : Si cargó con aws-cli través de aws s3 cp
, es muy probable que tenga un tamaño de 8 MB. Según los docs , ese es el valor predeterminado.
Actualización : me dijeron acerca de una implementación de esto en https://github.com/Teachnova/s3md5 , que no funciona en OS X. Aquí hay un Gist que escribí con un script de trabajo para OS X.
Y aquí hay una versión de PHP para calcular el 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);
Y aquí hay una versión mejorada que puede verificar contra un ETag esperado, e incluso adivinar el tamaño grande si no lo sabes.
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;
}
}
}
El algoritmo literalmente es (copiado del archivo Léame en la implementación de Python):
- md5 los pedazos
- glob las cadenas md5 juntas
- convertir el glob en binario
- md5 el binario del trozo globed md5s
- añada "-Number_of_chunks" al final de la cadena md5 del binario