Ejemplo de longitud de putObject de AmazonS3 con InputStream

82

Estoy cargando un archivo en S3 usando Java; esto es lo que obtuve hasta ahora:

AmazonS3 s3 = new AmazonS3Client(new BasicAWSCredentials("XX","YY"));

List<Bucket> buckets = s3.listBuckets();

s3.putObject(new PutObjectRequest(buckets.get(0).getName(), fileName, stream, new ObjectMetadata()));

El archivo se está cargando pero aparece una ADVERTENCIA cuando no estoy configurando la longitud del contenido:

com.amazonaws.services.s3.AmazonS3Client putObject: No content length specified for stream > data.  Stream contents will be buffered in memory and could result in out of memory errors.

Este es un archivo Estoy subiendo y la streamvariable es una InputStream, de la que puede obtener la matriz de bytes de esta manera: IOUtils.toByteArray(stream).

Entonces, cuando trato de establecer la longitud del contenido y MD5 (tomado de aquí ) de esta manera:

// get MD5 base64 hash
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.reset();
messageDigest.update(IOUtils.toByteArray(stream));
byte[] resultByte = messageDigest.digest();
String hashtext = new String(Hex.encodeHex(resultByte));

ObjectMetadata meta = new ObjectMetadata();
meta.setContentLength(IOUtils.toByteArray(stream).length);
meta.setContentMD5(hashtext);

Provoca que el siguiente error vuelva desde S3:

El Content-MD5 que especificó no es válido.

¿Qué estoy haciendo mal?

¡Cualquier ayuda apreciada!

PD : Estoy en Google App Engine: no puedo escribir el archivo en el disco o crear un archivo temporal porque AppEngine no es compatible con FileOutputStream.

JohnIdol
fuente

Respuestas:

69

Debido a que la pregunta original nunca fue respondida, y tuve que encontrarme con el mismo problema, la solución para el problema MD5 es que S3 no quiere la cadena MD5 codificada en Hex en la que normalmente pensamos.

En cambio, tuve que hacer esto.

// content is a passed in InputStream
byte[] resultByte = DigestUtils.md5(content);
String streamMD5 = new String(Base64.encodeBase64(resultByte));
metaData.setContentMD5(streamMD5);

Básicamente, lo que quieren para el valor MD5 es la matriz de bytes MD5 sin procesar codificada en Base64, no la cadena Hex. Cuando me cambié a esto, comenzó a funcionar muy bien para mí.

MarcG
fuente
¡Y tenemos un winnahhhh! Gracias por el esfuerzo adicional para responder al problema MD5. Esa es la parte que estaba buscando ...
Geek Stocks
¿Qué es el contenido en este caso? no lo entendí. Tengo la misma advertencia. Un poco de ayuda, por favor.
Shaonline
El contenido de @Shaonline es inputStream
sirvon
¿Alguna forma de convertir de Hex a la matriz de bytes MD5? Eso es lo que almacenamos en nuestra base de datos.
Joel
Tenga en cuenta que meta.setContentLength (IOUtils.toByteArray (stream) .length); consume el InputStream. Cuando la API de AWS intenta leerlo, su longitud es cero y, por lo tanto, falla. Necesita crear un nuevo flujo de entrada desde ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes);
Bernie Lenz
43

Si todo lo que está tratando de hacer es resolver el error de longitud del contenido de Amazon, entonces puede leer los bytes del flujo de entrada a Long y agregarlo a los metadatos.

/*
 * Obtain the Content length of the Input stream for S3 header
 */
try {
    InputStream is = event.getFile().getInputstream();
    contentBytes = IOUtils.toByteArray(is);
} catch (IOException e) {
    System.err.printf("Failed while reading bytes from %s", e.getMessage());
} 

Long contentLength = Long.valueOf(contentBytes.length);

ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(contentLength);

/*
 * Reobtain the tmp uploaded file as input stream
 */
InputStream inputStream = event.getFile().getInputstream();

/*
 * Put the object in S3
 */
try {

    s3client.putObject(new PutObjectRequest(bucketName, keyName, inputStream, metadata));

} catch (AmazonServiceException ase) {
    System.out.println("Error Message:    " + ase.getMessage());
    System.out.println("HTTP Status Code: " + ase.getStatusCode());
    System.out.println("AWS Error Code:   " + ase.getErrorCode());
    System.out.println("Error Type:       " + ase.getErrorType());
    System.out.println("Request ID:       " + ase.getRequestId());
} catch (AmazonClientException ace) {
    System.out.println("Error Message: " + ace.getMessage());
} finally {
    if (inputStream != null) {
        inputStream.close();
    }
}

Deberá leer el flujo de entrada dos veces usando este método exacto, por lo que si está cargando un archivo muy grande, es posible que deba leerlo una vez en una matriz y luego leerlo desde allí.

tarka
fuente
24
¡Así que tu decisión es leer la transmisión dos veces! Y guarda el archivo completo en la memoria. ¡Esto puede causar OOM como advierte S3!
Pavel Vyazankin
3
El punto de poder usar un flujo de entrada es que puede transmitir los datos, no cargarlos todos en la memoria a la vez.
Jordan Davidson
Para AmazonServiceException, no es necesario imprimir tantos sout. El método getMessage imprime todo excepto getErrorType.
saurabheights
33

Para cargar, el SDK de S3 tiene dos métodos putObject:

PutObjectRequest(String bucketName, String key, File file)

y

PutObjectRequest(String bucketName, String key, InputStream input, ObjectMetadata metadata)

El método inputstream + ObjectMetadata necesita un mínimo de metadatos de longitud de contenido de su inputstream. Si no lo hace, se almacenará en memoria intermedia para obtener esa información, esto podría causar OOM. Alternativamente, puede hacer su propio almacenamiento en búfer en memoria para obtener la longitud, pero luego necesita obtener un segundo flujo de entrada.

No preguntado por el OP (limitaciones de su entorno), sino por alguien más, como yo. Me resulta más fácil y seguro (si tiene acceso al archivo temporal) escribir el flujo de entrada en un archivo temporal y poner el archivo temporal. Sin búfer en memoria y sin necesidad de crear un segundo flujo de entrada.

AmazonS3 s3Service = new AmazonS3Client(awsCredentials);
File scratchFile = File.createTempFile("prefix", "suffix");
try {
    FileUtils.copyInputStreamToFile(inputStream, scratchFile);    
    PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, id, scratchFile);
    PutObjectResult putObjectResult = s3Service.putObject(putObjectRequest);

} finally {
    if(scratchFile.exists()) {
        scratchFile.delete();
    }
}
Peter Dietz
fuente
El segundo argumento en copyInputStreamToFile (inputStream, scratchFile) es Type File o OutputStream?
Shaonline
1
aunque esto es intensivo en IO, pero todavía voto por esto. ya que esta podría ser la mejor manera de evitar OOM en un objeto de archivo más grande. Sin embargo, cualquiera podría leer ciertos n * bytes y crear archivos de piezas y cargarlos en s3 por separado.
linehrr
7

Mientras escribe en S3, debe especificar la longitud del objeto S3 para asegurarse de que no haya errores de memoria insuficiente.

El uso IOUtils.toByteArray(stream)también es propenso a errores OOM porque está respaldado por ByteArrayOutputStream

Entonces, la mejor opción es escribir primero el flujo de entrada en un archivo temporal en el disco local y luego usar ese archivo para escribir en S3 especificando la longitud del archivo temporal.

srikanta
fuente
1
Gracias, pero estoy en el motor de la aplicación de Google (pregunta actualizada): no puedo escribir el archivo en el disco, si pudiera hacerlo, podría usar la sobrecarga de putObject que toma un Archivo :(
JohnIdol
@srikanta Acabo de seguir tu consejo. No es necesario especificar la longitud del archivo temporal. Simplemente pase el archivo temporal como está.
Siya Sosibo
Para su información, el enfoque del archivo temporal NO es una opción si, como yo, desea especificar el cifrado del lado del servidor, que se realiza en ObjectMetadata. Desafortunadamente, no hay PutObjectRequest (String bucketName, String key, File file, ObjectMetadata metadata)
Kevin Pauli
@kevin pauli Puedes hacerlorequest.setMetadata();
dbaq
5

De hecho, estoy haciendo algo similar pero en mi almacenamiento AWS S3: -

Código para el servlet que está recibiendo el archivo cargado: -

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import com.src.code.s3.S3FileUploader;

public class FileUploadHandler extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        PrintWriter out = response.getWriter();

        try{
            List<FileItem> multipartfiledata = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request);

            //upload to S3
            S3FileUploader s3 = new S3FileUploader();
            String result = s3.fileUploader(multipartfiledata);

            out.print(result);
        } catch(Exception e){
            System.out.println(e.getMessage());
        }
    }
}

Código que está cargando estos datos como objeto de AWS: -

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.UUID;

import org.apache.commons.fileupload.FileItem;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.ClasspathPropertiesFileCredentialsProvider;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.S3Object;

public class S3FileUploader {


    private static String bucketName     = "***NAME OF YOUR BUCKET***";
    private static String keyName        = "Object-"+UUID.randomUUID();

    public String fileUploader(List<FileItem> fileData) throws IOException {
        AmazonS3 s3 = new AmazonS3Client(new ClasspathPropertiesFileCredentialsProvider());
        String result = "Upload unsuccessfull because ";
        try {

            S3Object s3Object = new S3Object();

            ObjectMetadata omd = new ObjectMetadata();
            omd.setContentType(fileData.get(0).getContentType());
            omd.setContentLength(fileData.get(0).getSize());
            omd.setHeader("filename", fileData.get(0).getName());

            ByteArrayInputStream bis = new ByteArrayInputStream(fileData.get(0).get());

            s3Object.setObjectContent(bis);
            s3.putObject(new PutObjectRequest(bucketName, keyName, bis, omd));
            s3Object.close();

            result = "Uploaded Successfully.";
        } catch (AmazonServiceException ase) {
           System.out.println("Caught an AmazonServiceException, which means your request made it to Amazon S3, but was "
                + "rejected with an error response for some reason.");

           System.out.println("Error Message:    " + ase.getMessage());
           System.out.println("HTTP Status Code: " + ase.getStatusCode());
           System.out.println("AWS Error Code:   " + ase.getErrorCode());
           System.out.println("Error Type:       " + ase.getErrorType());
           System.out.println("Request ID:       " + ase.getRequestId());

           result = result + ase.getMessage();
        } catch (AmazonClientException ace) {
           System.out.println("Caught an AmazonClientException, which means the client encountered an internal error while "
                + "trying to communicate with S3, such as not being able to access the network.");

           result = result + ace.getMessage();
         }catch (Exception e) {
             result = result + e.getMessage();
       }

        return result;
    }
}

Nota: - Estoy usando el archivo de propiedades de AWS para las credenciales.

Espero que esto ayude.

racha
fuente
-1

Simplemente pasar el objeto de archivo al método putobject funcionó para mí. Si obtiene una transmisión, intente escribirla en un archivo temporal antes de pasarla a S3.

amazonS3.putObject(bucketName, id,fileObject);

Estoy usando Aws SDK v1.11.414

La respuesta en https://stackoverflow.com/a/35904801/2373449 me ayudó

Vikram
fuente
Si tiene una transmisión, desea utilizar esa transmisión. Escribiendo flujo de archivo (temp) sólo para obtener sus datos es ineficiente y le da dolor de cabeza adicional (al borrar el archivo, uso de disco)
devstructor
esto no le permitirá pasar metadatos, como el cifrado, que es una práctica común cuando se almacena en AWS
user1412523
-14

agregar el archivo log4j-1.2.12.jar me ha resuelto el problema

Rajesh
fuente
2
-1: Supongo que esto solo ocultará la advertencia de registro pero no resolverá el error en sí. Lamento ser tan duro, después de todo, es tu primera respuesta, pero esto no resuelve esta pregunta.
romualdr