【问题标题】:AmazonS3 putObject with InputStream length exampleAmazonS3 putObject 与 InputStream 长度示例
【发布时间】:2012-01-11 05:17:06
【问题描述】:

我正在使用 Java 将文件上传到 S3 - 这是我目前得到的:

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(取自 here)时:

// 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 无效。

我做错了什么?

任何帮助表示赞赏!

P.S.我在 Google App Engine 上 - 我无法将文件写入磁盘或 create a temp file,因为 AppEngine 不支持 FileOutputStream。

【问题讨论】:

  • IOUtils.toByteArray 将整个文件读取到您的内存中,因此根据文件的大小,它可能不是适当的解决方案。更好的解决方案是向文件提供者询问文件大小,然后将其流式传输到 S3,这样您就不必下载内存中的所有文件,因为您已经拥有有关大小的信息

标签: java google-app-engine amazon-s3 md5 inputstream


【解决方案1】:

因为最初的问题从未得到解答,而且我不得不遇到同样的问题,所以 MD5 问题的解决方案是 S3 不想要我们通常认为的 Hex 编码的 MD5 字符串。

相反,我不得不这样做。

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

他们想要的 MD5 值基本上是 Base64 编码的原始 MD5 字节数组,而不是十六进制字符串。当我切换到这个时,它开始对我很好。

【讨论】:

  • 我们赢了!感谢您在回答 MD5 问题时付出的额外努力。这就是我正在挖掘的部分......
  • 这种情况下的内容是什么?我没明白。我有同样的警告。请帮忙。?
  • @Shaonline 内容是输入流
  • 有什么方法可以从十六进制转换回 MD5 字节数组?这就是我们存储在数据库中的内容。
  • 请注意 meta.setContentLength(IOUtils.toByteArray(stream).length);消耗 InputStream。当 AWS API 尝试读取它时,它的长度为零,因此会失败。您需要从 ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); 创建一个新的输入流
【解决方案2】:

如果您要做的只是解决来自亚马逊的内容长度错误,那么您只需将输入流中的字节读取到 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();
    }
}

您需要使用这种精确的方法读取输入流两次,因此如果您要上传一个非常大的文件,您可能需要先将其读取一次到数组中,然后再从那里读取。

【讨论】:

  • 所以你的决定是读取流两次!并且您将整个文件保存在内存中。正如 S3 警告的那样,这可能会导致 OOM!
  • 能够使用输入流的关键在于您可以将数据流式传输,而不是一次将其全部加载到内存中。
  • 对于AmazonServiceException,没有必要打印那么多sout。 getMessage 方法打印除 getErrorType 之外的所有内容。
【解决方案3】:

对于上传,S3 SDK 有两个 putObject 方法:

PutObjectRequest(String bucketName, String key, File file)

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

inputstream+ObjectMetadata 方法需要输入流的 Content Length 的最小元数据。如果你不这样做,那么它将在内存中缓冲以获取该信息,这可能会导致 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?
  • 虽然这是 IO 密集型的,但我还是投了这个票。因为这可能是在更大的文件对象上避免 OOM 的最佳方法。但是,任何人也可以读取某些 n*bytes 并创建部分文件并单独上传到 s3。
【解决方案4】:

在写入 S3 时,您需要指定 S3 对象的长度以确保没有内存不足错误。

使用IOUtils.toByteArray(stream)也容易出现OOM错误,因为这是由ByteArrayOutputStream支持的

因此,最好的选择是首先将输入流写入本地磁盘上的临时文件,然后通过指定临时文件的长度使用该文件写入 S3。

【讨论】:

  • 谢谢,但我在谷歌应用引擎上(更新的问题) - 无法将文件写入磁盘,如果可以的话,我可以使用 putObject 重载,它需要一个 File :(
  • @srikanta 刚刚听取了您的建议。无需指定临时文件的长度。只需按原样传递临时文件。
  • 仅供参考,如果您像我一样想要指定在 ObjectMetadata 中完成的服务器端加密,则临时文件方法不是一个选项。不幸的是没有 PutObjectRequest(String bucketName, String key, File file, ObjectMetadata metadata)
  • @kevin pauli 你可以做到request.setMetadata();
【解决方案5】:

我实际上在做同样的事情,但在我的 AWS S3 存储上:-

接收上传文件的servlet代码:-

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 属性文件获取凭据。

希望这会有所帮助。

【讨论】:

    【解决方案6】:

    我创建了一个在后台使用分段上传的库,以避免缓冲内存中的所有内容并且也不写入磁盘:https://github.com/alexmojaki/s3-stream-upload

    【讨论】:

      【解决方案7】:

      只是将文件对象传递给 putobject 方法对我有用。如果您正在获取流,请尝试将其写入临时文件,然后再将其传递给 S3。

      amazonS3.putObject(bucketName, id,fileObject);
      

      我正在使用 Aws SDK v1.11.414

      https://stackoverflow.com/a/35904801/2373449 的回答帮助了我

      【讨论】:

      • 如果你有一个流,你想使用那个流。将流写入(临时)文件只是为了获取其数据效率低下,并且会给您带来额外的麻烦(删除文件,磁盘使用)
      • 这将不允许您传递元数据,例如加密,这是在 AWS 中存储时的常见做法
      【解决方案8】:

      添加 log4j-1.2.12.jar 文件已经解决了我的问题

      【讨论】:

      • -1 :我想这只会隐藏日志警告,但不能解决错误本身。抱歉这么苛刻,毕竟这是你的第一个答案,但这并不能解决这个问题。
      猜你喜欢
      • 2020-10-09
      • 1970-01-01
      • 2016-07-12
      • 2016-05-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-29
      • 2012-01-29
      相关资源
      最近更新 更多