AmazonS3 putObject с примером длины InputStream

83

Я загружаю файл на S3 с помощью Java - вот что у меня получилось:

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

Файл загружается, но появляется ПРЕДУПРЕЖДЕНИЕ, когда я не устанавливаю длину содержимого:

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.

Это файл я отправляю и streamпеременная является InputStream, из которого я могу получить массив байтов , как это: IOUtils.toByteArray(stream).

Поэтому, когда я пытаюсь установить длину содержимого и MD5 (взятые отсюда ) следующим образом:

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

Это приводит к возврату следующей ошибки из S3:

Вы указали недопустимый Content-MD5.

Что я делаю неправильно?

Любая помощь приветствуется!

PS Я использую Google App Engine - я не могу записать файл на диск или создать временный файл, потому что AppEngine не поддерживает FileOutputStream.

JohnIdol
источник
IOUtils.toByteArray считывает весь файл в вашу память, поэтому в зависимости от размера ваших файлов это не может быть адекватным решением. Лучшим решением было бы запросить у поставщика файлов размер файла, а затем передать его на S3, таким образом вам не нужно загружать все файлы в память, поскольку у вас уже есть информация о размере
Хамди,

Ответы:

69

Поскольку на исходный вопрос так и не ответили, и мне пришлось столкнуться с той же проблемой, решение проблемы MD5 состоит в том, что S3 не хочет строку MD5 в шестнадцатеричном коде, о которой мы обычно думаем.

Вместо этого мне пришлось сделать это.

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

По сути, то, что они хотят для значения MD5, - это необработанный массив байтов MD5 в кодировке Base64, а не строка Hex. Когда я перешел на это, у меня все стало отлично работать.

MarcG
источник
И у нас есть победа! Спасибо за дополнительные усилия при ответе на проблему MD5. Это та часть, которую я искал ...
Geek Stocks
Что в этом случае контент? я не понял. У меня такое же предупреждение. Небольшая помощь, пожалуйста.
Shaonline
@Shaonline content is the inputStream
sirvon
Есть ли способ преобразовать из Hex обратно в байтовый массив MD5? Это то, что мы храним в нашей БД.
Joel
Обратите внимание, что meta.setContentLength (IOUtils.toByteArray (stream) .length); потребляет InputStream. Когда AWS API пытается его прочитать, он имеет нулевую длину и поэтому терпит неудачу. Вам необходимо создать новый входной поток из ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes);
Берни Ленц
43

Если все, что вы пытаетесь сделать, это решить ошибку длины контента от Amazon, вы можете просто прочитать байты из входного потока в Long и добавить их в метаданные.

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

Вам нужно будет дважды прочитать входной поток, используя этот точный метод, поэтому, если вы загружаете очень большой файл, вам может потребоваться прочитать его один раз в массив, а затем прочитать его оттуда.

тарка
источник
24
Итак, ваше решение - прочитать стрим два раза! И вы сохраняете в памяти весь файл. Это может вызвать OOM, как предупреждает S3!
Павел Вязанкин
3
Смысл использования входного потока заключается в том, что вы можете передавать данные, а не загружать их все сразу в память.
Джордан Дэвидсон,
Для AmazonServiceException нет необходимости печатать столько sout. getMessage печатает все, кроме getErrorType.
saurabheights
33

Для загрузки в S3 SDK есть два метода putObject:

PutObjectRequest(String bucketName, String key, File file)

и

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

Для метода inputstream + ObjectMetadata требуются минимальные метаданные Content Length вашего inputstream. Если вы этого не сделаете, он будет буферизовать в памяти для получения этой информации, это может вызвать OOM. В качестве альтернативы вы можете выполнить собственную буферизацию в памяти, чтобы получить длину, но тогда вам нужно получить второй входной поток.

Не спрашивает OP (ограничения его среды), но для кого-то другого, например, меня. Я считаю, что проще и безопаснее (если у вас есть доступ к временному файлу) записать входной поток во временный файл и поместить временный файл. Нет буфера в памяти и нет необходимости создавать второй поток ввода.

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();
    }
}
Питер Дитц
источник
Второй аргумент в copyInputStreamToFile (inputStream, scratchFile) - это Type File или OutputStream?
Shaonline
1
хотя это интенсивный ввод-вывод, но я все же голосую за это. поскольку это может быть лучшим способом избежать OOM для файлового объекта большего размера. Тем не менее, любой может также прочитать определенные n * байтов и создать файлы частей и загрузить их в s3 отдельно.
linehrr
7

При записи в S3 вам необходимо указать длину объекта S3, чтобы быть уверенным в отсутствии ошибок нехватки памяти.

С помощью IOUtils.toByteArray(stream) также подвержено ошибкам OOM, поскольку это поддерживается ByteArrayOutputStream

Итак, лучший вариант - сначала записать входной поток во временный файл на локальном диске, а затем использовать этот файл для записи в S3, указав длину временного файла.

шриканта
источник
1
Спасибо, но я использую движок приложений google (обновленный вопрос) - не могу записать файл на диск, если бы я мог это сделать, я мог бы использовать перегрузку putObject, которая принимает файл :(
JohnIdol
@srikanta Просто прислушался к твоему совету. Нет необходимости указывать длину временного файла. Просто передайте временный файл как есть.
Siya Sosibo 05
К вашему сведению, использование временного файла НЕ подходит, если, как и я, вы хотите указать шифрование на стороне сервера, что выполняется в ObjectMetadata. К сожалению, нет PutObjectRequest (String bucketName, String key, File file, ObjectMetadata metadata)
Кевин Паули,
@kevin pauli Вы можете сделатьrequest.setMetadata();
dbaq
6

На самом деле я делаю то же самое, но на своем хранилище AWS S3: -

Код для сервлета, который получает загруженный файл: -

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

Код, который загружает эти данные как объект 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;
    }
}

Примечание. - Я использую файл свойств aws для учетных данных.

Надеюсь это поможет.

полоса
источник
3

Я создал библиотеку, которая использует многостраничные загрузки в фоновом режиме, чтобы избежать буферизации всего в памяти, а также не записывает на диск: https://github.com/alexmojaki/s3-stream-upload

Алекс Холл
источник
-1

У меня сработала простая передача файлового объекта методу putobject. Если вы получаете поток, попробуйте записать его во временный файл, прежде чем передавать его на S3.

amazonS3.putObject(bucketName, id,fileObject);

Я использую Aws SDK v1.11.414

Ответ на https://stackoverflow.com/a/35904801/2373449 мне помог

Викрам
источник
Если у вас есть поток, вы хотите использовать этот поток. Запись потока в (временный) файл только для получения его данных неэффективна и вызывает дополнительную головную боль (удаление файла, использование диска)
devstructor 02 июл.2020,
это не позволит вам передавать метаданные, такие как шифрование, что является обычной практикой при хранении в AWS
user1412523
-15

добавление файла log4j-1.2.12.jar решило проблему для меня

Раджеш
источник
2
-1: Думаю, это просто скроет предупреждение журнала, но не решит саму ошибку. Извините за такую ​​резкость, в конце концов, это ваш первый ответ, но это не решает этот вопрос.
romualdr