Beispiel für AmazonS3 putObject mit InputStream-Länge

82

Ich lade eine Datei mit Java auf S3 hoch - das habe ich bisher:

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

Die Datei wird hochgeladen, aber eine WARNUNG wird ausgelöst, wenn ich die Inhaltslänge nicht einstelle:

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.

Dies ist eine Datei, die ich hochlade, und die streamVariable ist eine InputStream, aus der ich das Byte-Array wie folgt abrufen kann : IOUtils.toByteArray(stream).

Wenn ich also versuche, die Inhaltslänge und MD5 (von hier übernommen ) wie folgt einzustellen :

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

Der folgende Fehler wird von S3 zurückgegeben:

Das von Ihnen angegebene Content-MD5 war ungültig.

Was mache ich falsch?

Jede Hilfe geschätzt!

PS Ich bin in Google App Engine - Ich kann die Datei nicht auf die Festplatte schreiben oder eine temporäre Datei erstellen, da AppEngine FileOutputStream nicht unterstützt.

JohnIdol
quelle

Antworten:

69

Da die ursprüngliche Frage nie beantwortet wurde und ich auf dasselbe Problem stoßen musste, besteht die Lösung für das MD5-Problem darin, dass S3 die hexadezimal codierte MD5-Zeichenfolge, an die wir normalerweise denken, nicht möchte.

Stattdessen musste ich das tun.

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

Im Wesentlichen möchten sie für den MD5-Wert das Base64-codierte rohe MD5-Byte-Array, nicht die Hex-Zeichenfolge. Als ich dazu wechselte, funktionierte es großartig für mich.

MarcG
quelle
Und wir haben einen Winnahhhh! Vielen Dank für den zusätzlichen Aufwand bei der Beantwortung des MD5-Problems. Das ist der Teil, nach dem ich gegraben habe ...
Geek Stocks
Was ist in diesem Fall Inhalt? Ich habe es nicht verstanden. Ich habe die gleiche Warnung. Ein bisschen Hilfe bitte.
Shaonline
@Shaonline Inhalt ist der inputStream
Sirvon
Gibt es eine Möglichkeit, von Hex zurück in das MD5-Byte-Array zu konvertieren? Das speichern wir in unserer DB.
Joel
Bitte beachten Sie, dass meta.setContentLength (IOUtils.toByteArray (stream) .length); verbraucht den InputStream. Wenn die AWS-API versucht, sie zu lesen, hat sie eine Länge von Null und schlägt daher fehl. Sie müssen einen neuen Eingabestream aus ByteArrayInputStream erstellen. ByteArrayInputStream = new ByteArrayInputStream (Bytes);
Bernie Lenz
43

Wenn Sie nur versuchen, den Fehler bei der Inhaltslänge von Amazon zu beheben, können Sie einfach die Bytes aus dem Eingabestream zu einem Long lesen und diese zu den Metadaten hinzufügen.

/*
 * 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();
    }
}

Sie müssen den Eingabestream zweimal genau mit dieser Methode lesen. Wenn Sie also eine sehr große Datei hochladen, müssen Sie sie möglicherweise einmal in ein Array einlesen und dann von dort aus lesen.

Tarka
quelle
24
Sie entscheiden sich also, den Stream zweimal zu lesen! Und Sie speichern die gesamte Datei im Speicher. Dies kann OOM verursachen, da S3 warnt!
Pavel Vyazankin
3
Der Punkt, an dem Sie einen Eingabestream verwenden können, besteht darin, dass Sie die Daten streamen können, ohne alles auf einmal in den Speicher zu laden.
Jordan Davidson
Für AmazonServiceException müssen nicht so viele Sout gedruckt werden. Die Methode getMessage druckt alles außer getErrorType.
Saurabheights
33

Zum Hochladen verfügt das S3 SDK über zwei putObject-Methoden:

PutObjectRequest(String bucketName, String key, File file)

und

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

Die Methode inputstream + ObjectMetadata benötigt eine Mindestmetadate der Inhaltslänge Ihres Eingabestreams. Wenn Sie dies nicht tun, wird der Speicher im Speicher gepuffert, um diese Informationen abzurufen. Dies kann zu OOM führen. Alternativ können Sie Ihre eigene In-Memory-Pufferung durchführen, um die Länge zu ermitteln. Dann müssen Sie jedoch einen zweiten Eingabestream abrufen.

Nicht vom OP gefragt (Einschränkungen seiner Umgebung), sondern für jemand anderen, wie mich. Ich finde es einfacher und sicherer (wenn Sie Zugriff auf temporäre Dateien haben), den Eingabestream in eine temporäre Datei zu schreiben und die temporäre Datei abzulegen. Kein speicherinterner Puffer und keine Notwendigkeit, einen zweiten Eingabestream zu erstellen.

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
quelle
Das zweite Argument in copyInputStreamToFile (inputStream, ScratchFile) ist Type File oder OutputStream?
Shaonline
1
Das ist zwar IO-intensiv, aber ich stimme trotzdem dafür. da dies möglicherweise der beste Weg ist, um OOM bei größeren Dateiobjekten zu vermeiden. Jeder kann jedoch auch bestimmte n * Bytes lesen, Teiledateien erstellen und separat auf s3 hochladen.
Linehrr
7

Beim Schreiben in S3 müssen Sie die Länge des S3-Objekts angeben, um sicherzustellen, dass keine Speicherfehler vorliegen.

Die Verwendung IOUtils.toByteArray(stream)ist auch anfällig für OOM-Fehler, da dies von ByteArrayOutputStream unterstützt wird

Die beste Option besteht also darin, zuerst den Eingabestream in eine temporäre Datei auf der lokalen Festplatte zu schreiben und dann mit dieser Datei in S3 zu schreiben, indem die Länge der temporären Datei angegeben wird.

Srikanta
quelle
1
Vielen Dank, aber ich bin in der Google App Engine (aktualisierte Frage) - kann die Datei nicht auf die Festplatte schreiben. Wenn ich das könnte, könnte ich die putObject-Überladung verwenden, die eine Datei benötigt :(
JohnIdol
@srikanta Habe gerade deinen Rat befolgt. Die Länge der temporären Datei muss nicht angegeben werden. Übergeben Sie einfach die temporäre Datei wie sie ist.
Siya Sosibo
Zu Ihrer Information, der Ansatz für temporäre Dateien ist KEINE Option, wenn Sie wie ich die serverseitige Verschlüsselung angeben möchten, die in den ObjectMetadata durchgeführt wird. Leider gibt es keine PutObjectRequest (String BucketName, String Key, Datei Datei, ObjectMetadata Metadaten)
Kevin Pauli
@ Kevin Pauli Sie können tunrequest.setMetadata();
dbaq
5

Ich mache eigentlich etwas das Gleiche, aber auf meinem AWS S3-Speicher: -

Code für das Servlet, das die hochgeladene Datei empfängt: -

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());
        }
    }
}

Code, der diese Daten als AWS-Objekt hochlädt: -

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

Hinweis: - Ich verwende die aws-Eigenschaftendatei für Anmeldeinformationen.

Hoffe das hilft.

Streifen
quelle
3

Ich habe eine Bibliothek erstellt, die mehrteilige Uploads im Hintergrund verwendet, um zu vermeiden, dass alles im Speicher gepuffert wird, und auch nicht auf die Festplatte schreibt: https://github.com/alexmojaki/s3-stream-upload

Alex Hall
quelle
-1

Das Übergeben des Dateiobjekts an die putobject-Methode hat bei mir funktioniert. Wenn Sie einen Stream erhalten, versuchen Sie, ihn in eine temporäre Datei zu schreiben, bevor Sie ihn an S3 weitergeben.

amazonS3.putObject(bucketName, id,fileObject);

Ich verwende Aws SDK v1.11.414

Die Antwort unter https://stackoverflow.com/a/35904801/2373449 hat mir geholfen

Vikram
quelle
Wenn Sie einen Stream haben, möchten Sie diesen Stream verwenden. Das Schreiben eines Streams in eine (temporäre) Datei, nur um deren Daten
abzurufen,
Auf diese Weise können Sie keine Metadaten wie die Verschlüsselung übergeben, die beim Speichern in AWS
user1412523
-14

Das Hinzufügen der Datei log4j-1.2.12.jar hat das Problem für mich behoben

Rajesh
quelle
2
-1: Ich denke, dies wird nur die Protokollwarnung verbergen, aber den Fehler selbst nicht beheben. Tut mir leid, dass ich so hart bin, es ist schließlich Ihre erste Antwort, aber dies löst diese Frage nicht.
Romualdr