AmazonS3 putObject z przykładem długości InputStream

83

Wgrywam plik do S3 za pomocą Javy - tak mam do tej pory:

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

Plik jest przesyłany, ale pojawia się OSTRZEŻENIE, gdy nie ustawiam długości treści:

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.

Ten plik jest mi przesłać, a streamzmienna jest InputStream, z którego można uzyskać tablicę bajtów takiego: IOUtils.toByteArray(stream).

Więc kiedy próbuję ustawić długość treści i MD5 (wzięte stąd ) w ten sposób:

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

Powoduje to powrót następującego błędu z S3:

Podany Content-MD5 był nieprawidłowy.

Co ja robię źle?

Każda pomoc doceniona!

PS Korzystam z Google App Engine - nie mogę zapisać pliku na dysk ani utworzyć pliku tymczasowego, ponieważ AppEngine nie obsługuje funkcji FileOutputStream.

JohnIdol
źródło
IOUtils.toByteArray odczytuje cały plik do pamięci, więc w zależności od rozmiaru plików może to nie być odpowiednie rozwiązanie. Lepszym rozwiązaniem byłoby zapytanie dostawcy pliku o rozmiar pliku, a następnie przesłanie go do S3, w ten sposób nie musisz pobierać wszystkich plików w pamięci, ponieważ masz już informacje o rozmiarze
Hamdi

Odpowiedzi:

69

Ponieważ na pierwotne pytanie nigdy nie udzielono odpowiedzi i musiałem napotkać ten sam problem, rozwiązaniem problemu MD5 jest to, że S3 nie chce łańcucha MD5 zakodowanego szesnastkowo, o którym normalnie myślimy.

Zamiast tego musiałem to zrobić.

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

Zasadniczo to, czego chcą dla wartości MD5, to tablica bajtów MD5 zakodowana algorytmem Base64, a nie ciąg szesnastkowy. Kiedy przeszedłem na to, zaczęło mi działać świetnie.

MarcG
źródło
I mamy winnahhhh! Dzięki za dodatkowy wysiłek w odpowiedzi na problem MD5. To jest ta część, której szukałem ...
Geek Stocks
Jaka jest treść w tym przypadku? nie rozumiem. Mam to samo ostrzeżenie. Mała pomoc, proszę.
Shaonline,
Treść @Shaonline to inputStream
Sirvon
Czy jest jakiś sposób, aby przekonwertować z Hex z powrotem na tablicę bajtów MD5? To właśnie przechowujemy w naszej bazie danych.
Joel
Zwróć uwagę, że meta.setContentLength (IOUtils.toByteArray (stream) .length); zużywa InputStream. Kiedy API AWS próbuje go odczytać, ma zerową długość i dlatego kończy się niepowodzeniem. Musisz utworzyć nowy strumień wejściowy z ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bajty);
Bernie Lenz
43

Jeśli wszystko, co próbujesz zrobić, to rozwiązać błąd długości treści z amazon, możesz po prostu odczytać bajty ze strumienia wejściowego do Long i dodać to do metadanych.

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

Będziesz musiał dwukrotnie odczytać strumień wejściowy, używając tej dokładnej metody, więc jeśli przesyłasz bardzo duży plik, być może będziesz musiał spojrzeć na wczytanie go raz do tablicy, a następnie odczytanie stamtąd.

tarka
źródło
24
Więc twoją decyzją jest dwukrotne przeczytanie strumienia! I zapisujesz cały plik w pamięci. Może to spowodować OOM, jak ostrzega S3!
Pavel Vyazankin
3
Istotą możliwości korzystania ze strumienia wejściowego jest to, że można przesyłać strumieniowo dane, a nie ładować ich wszystkich jednocześnie do pamięci.
Jordan Davidson,
W przypadku AmazonServiceException nie ma potrzeby drukowania tak wielu południowych stron. Metoda getMessage wyświetla wszystko oprócz getErrorType.
saurabheights
33

Do przesyłania S3 SDK ma dwie metody putObject:

PutObjectRequest(String bucketName, String key, File file)

i

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

Metoda inputstream + ObjectMetadata wymaga minimalnych metadanych długości zawartości strumienia wejściowego. Jeśli tego nie zrobisz, buforuje w pamięci, aby uzyskać te informacje, co może spowodować OOM. Alternatywnie możesz zrobić własne buforowanie w pamięci, aby uzyskać długość, ale wtedy musisz uzyskać drugi strumień wejściowy.

Nie pytany przez OP (ograniczenia jego środowiska), ale dla kogoś innego, takiego jak ja. Uważam, że łatwiej i bezpieczniej (jeśli masz dostęp do pliku tymczasowego), aby zapisać strumień wejściowy do pliku tymczasowego i umieścić plik tymczasowy. Brak bufora w pamięci i brak konieczności tworzenia drugiego strumienia wejściowego.

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
źródło
Drugi argument funkcji copyInputStreamToFile (inputStream, scratchFile) to Type File lub OutputStream?
Shaonline
1
chociaż jest to intensywne IO, ale nadal głosuję za tym. ponieważ może to być najlepszy sposób na uniknięcie OOM w przypadku większego obiektu pliku. Jednak każdy mógł również odczytać określone n * bajtów i utworzyć pliki części i przesłać je do s3 oddzielnie.
linehrr
7

Pisząc do S3, musisz określić długość obiektu S3, aby mieć pewność, że nie ma błędów braku pamięci.

Używanie IOUtils.toByteArray(stream)jest również podatne na błędy OOM, ponieważ jest obsługiwane przez ByteArrayOutputStream

Dlatego najlepszą opcją jest najpierw zapisanie strumienia wejściowego do pliku tymczasowego na dysku lokalnym, a następnie użycie tego pliku do zapisu w S3, określając długość pliku tymczasowego.

srikanta
źródło
1
Dzięki, ale jestem na silniku aplikacji Google (zaktualizowane pytanie) - nie mogę zapisać pliku na dysk, gdybym mógł to zrobić, mógłbym użyć przeciążenia putObject, które pobiera plik :(
JohnIdol
@srikanta Właśnie skorzystałem z Twojej rady. Nie ma potrzeby określania długości pliku tymczasowego. Po prostu przekaż plik tymczasowy bez zmian.
Siya Sosibo
FYI podejście do pliku tymczasowego NIE jest opcją, jeśli, tak jak ja, chcesz określić szyfrowanie po stronie serwera, które jest wykonywane w ObjectMetadata. Niestety nie ma PutObjectRequest (String bucketName, String key, File file, ObjectMetadata metadata)
Kevin Pauli
@kevin pauli Możesz to zrobićrequest.setMetadata();
dbaq
6

faktycznie robię to samo, ale na moim magazynie AWS S3: -

Kod serwletu odbierającego wgrany plik: -

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

Kod, który przesyła te dane jako obiekt 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;
    }
}

Uwaga: - Używam pliku właściwości aws do poświadczeń.

Mam nadzieję że to pomoże.

smuga
źródło
3

Utworzyłem bibliotekę, która używa przesyłania wieloczęściowego w tle, aby uniknąć buforowania wszystkiego w pamięci, a także nie zapisuje na dysku: https://github.com/alexmojaki/s3-stream-upload

Alex Hall
źródło
-1

Po prostu przekazanie obiektu pliku do metody putobject działało dla mnie. Jeśli otrzymujesz strumień, spróbuj zapisać go w pliku tymczasowym przed przekazaniem go do S3.

amazonS3.putObject(bucketName, id,fileObject);

Używam Aws SDK w wersji 1.11.414

Pomogła mi odpowiedź na https://stackoverflow.com/a/35904801/2373449

Vikram
źródło
Jeśli masz strumień, chcesz go użyć. Zapisywanie strumienia do pliku (tymczasowego) tylko po to, aby uzyskać jego dane, jest nieefektywne i powoduje dodatkowy ból głowy (usuwanie pliku, użycie dysku)
devstructor
nie pozwoli ci to na przekazywanie metadanych, takich jak szyfrowanie, co jest powszechną praktyką podczas przechowywania w AWS
user1412523
-15

dodanie pliku log4j-1.2.12.jar rozwiązało ten problem

Rajesh
źródło
2
-1: Myślę, że to po prostu ukryje ostrzeżenie dziennika, ale nie rozwiąże samego błędu. Przepraszam, że jestem taki szorstki, w końcu to twoja pierwsza odpowiedź, ale to nie rozwiązuje tego pytania.
romualdr,