AmazonS3 putObject使用InputStream长度的示例

94

我正在使用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(从这里获取)时:
// 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 - 我不能将文件写入磁盘或创建临时文件,因为AppEngine不支持FileOutputStream。


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

73

由于原始问题从未得到解答,而我也遇到了同样的问题,解决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值,他们需要的是Base64编码的原始MD5字节数组,而不是十六进制字符串。当我使用这种方式后,它对我非常有效。


我们有一个赢家!感谢您在回答MD5问题时付出的额外努力。那正是我想要的部分... - Geek Stocks
这里的“content”是什么意思?我不明白。我也遇到了同样的警告。请帮忙一下,好吗? - Shaonline
@Shaonline的内容是inputStream。 - sirvon
有没有办法将十六进制转换回MD5字节数组?这是我们在数据库中存储的内容。 - Joel
请注意,meta.setContentLength(IOUtils.toByteArray(stream).length)会消耗InputStream。当AWS API尝试读取它时,它的长度为零,因此失败。您需要从ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)创建一个新的输入流。 - Bernie Lenz
请使用com.amazonaws.util.Md5Utils.md5AsBase64(byte[])代替。 - pwojnowski

46

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

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


27
你的决定是读取流两次!并将整个文件保存在内存中。这可能会导致OOM,正如S3所警告的那样! - Pavel Vyazankin
4
使用输入流的意义在于可以流式传输数据,而不是一次性将所有数据加载到内存中。 - Jordan Davidson
对于 AmazonServiceException,没有必要打印这么多 sout。getMessage 方法会打印除 getErrorType 之外的所有内容。 - saurabheights

37

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

PutObjectRequest(String bucketName, String key, File file)

PutObjectRequest(String bucketName, String key, InputStream input, ObjectMetadata metadata)
方法需要输入流的最小元数据内容长度。如果没有这个长度信息,它将在内存中缓冲以获取该信息,这可能会导致OOM。或者,您可以自己进行内存缓冲以获得长度,但是然后您需要获取第二个输入流。

虽然不是此提问者所要求的(他环境的限制),但对于其他人,例如我,我发现将输入流写入临时文件并将临时文件放置更容易且更安全(如果您有访问权限)。无需内存缓冲,也无需创建第二个输入流。

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) 中的第二个参数是 File 类型还是 OutputStream 类型? - Shaonline
2
虽然这个操作需要大量的IO,但我仍然支持这种方式。因为这可能是避免在处理大文件对象时出现OOM的最佳方法。不过,任何人也可以读取特定数量的字节并创建分段文件,然后将其分别上传到S3。 - linehrr

8

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

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

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


2
谢谢,但我正在使用Google应用引擎(更新的问题) - 无法将文件写入磁盘,如果我能够这样做,我就可以使用接受文件的putObject重载方法 :( - JohnIdol
@srikanta 刚刚采纳了你的建议。不需要指定临时文件的长度,只需将临时文件直接传递即可。 - Siya Sosibo
1
请注意,如果您像我一样想指定服务器端加密,则临时文件方法不是选项,因为这是在ObjectMetadata中完成的。不幸的是,没有PutObjectRequest(String bucketName,String key,File file,ObjectMetadata metadata)选项。 - Kevin Pauli
@kevin pauli 你可以使用request.setMetadata(); - dbaq
找不到其他方法,只能创建临时文件来存储需要发送到S3的内容。有点遗憾,希望能提供内存数据... - Camille

6

我正在做与此类似的事情,但是在我的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属性文件作为凭据。
希望这可以帮到您。

4

-2

对我来说,只需将文件对象传递给putobject方法即可。如果您正在获取流,请在将其传递到S3之前尝试将其写入临时文件。

amazonS3.putObject(bucketName, id,fileObject);

我正在使用 Aws SDK v1.11.414

https://dev59.com/PIbca4cB1Zd3GeqPYaXQ#35904801上的答案对我很有帮助


3
如果你有一个数据流,你应该直接使用它,把数据写入临时文件再读取是低效的做法,并会带来额外的麻烦(比如删除文件、占用磁盘空间)。 - devstructor
这将不允许您传递元数据,例如加密,这是在存储在AWS时的常见做法。 - user1412523

-17

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


4
我猜这只会隐藏日志警告,但不能解决错误本身。抱歉说话有点严厉,毕竟这是你的第一个回答,但这并没有解决这个问题。 - romualdr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接