使用aws的pre-signed url上传文件

概述:

官方文档介绍了如何直接使用pre-signed url上传小文件, 但没有介绍如何使用pre-signed url来multipart 上传大文件,本文将详述。另, 本文将介绍如何分别使用OKHttp和resttemplate来将inputstream直接作为request body体发送给pre-signed url,这种方式省去了将上传内容保存到本地临时文件或者读入内存中的步骤。也会介绍我遇到的一些坑。


直接上传

    服务端(生成pre-signed url):

        服务端我是用java SDK签发pre-signed url,然后通过接口返回给调用端。

amazonS3.generatePresignedUrl(new GeneratePresignedUrlRequest(BUCKET_NAME,informationObject.getObjectKey(),HttpMethod.PUT)

.withContentMd5(informationObject.getMd5DigestValue())//如果在签发pre-signed url时附带了content-md5,那么当调用pre-signed url上传文件时,需要将文件的content-md5放到header中。

.withExpiration(new Date(System.currentTimeMillis() +EXPIRE_INTERVAL))

)

  

客户端(需要上传文件到S3,但没有AWS credential):

没有什么特别之处,与multipart upload共用同一上传inputstream的方法即可。

Multipart上传

采用multipart上传文件时,总共需要四步,解下来分别介绍服务端与客户端:

1. Initiate multipart upload.

2. Generate presigned url for every part.

3. Upload part through invoking pre-signed url.

4. Complete the multipart upload.


服务端:

作为拥有credentials、可通过SDK直接请求AWS的服务端,我们需要对client端提供1,2,4步所需的接口(为减少服务器承受的压力,这里建议每个接口做成批量处理,即一个请求体中包含所有要上传的文件的所有part)。

Interfaces in server:

1. Initiate multipart uploadS.

amazonS3.initiateMultipartUpload(new InitiateMultipartUploadRequest(BUCKET_NAME,informationObject.getObjectKey()))

2. Generate presigned urls for every part of all fileS.

给每个part签发pre-signed url(如果要上传多个文件,建议放到同一次请求中签发url)。并且request parameter中需要携带uploadID和partNumber。

GeneratePresignedUrlRequest generatePresignedUrlRequest =new GeneratePresignedUrlRequest(

BUCKET_NAME,objectKey).withContentMd5(uploadPart.getMd5DigestValue())

.withExpiration(new Date(System.currentTimeMillis() +EXPIRE_INTERVAL))

.withMethod(HttpMethod.PUT);

generatePresignedUrlRequest.addRequestParameter("uploadId",uploadId);

generatePresignedUrlRequest.addRequestParameter("partNumber",Integer.toString(uploadPart.getPartNumber()));

amazonS3.generatePresignedUrl(generatePresignedUrlRequest);

3. Complete the multipart uploadS.

ListpartETags =new ArrayList<>();

int partNumberMarker =0;

boolean isTruncated =true;

while (isTruncated) {

  PartListing partListing =amazonS3.listParts(

  new ListPartsRequest(BUCKET_NAME, objectKey, uploadId).withPartNumberMarker(partNumberMarker));

  partETags.addAll(partListing.getParts().stream()

  .map(part ->new PartETag(part.getPartNumber(), part.getETag())).collect(Collectors.toList()));

  isTruncated =partListing.isTruncated();

  if (isTruncated) {

    partNumberMarker =partListing.getNextPartNumberMarker();

  }

}

CompleteMultipartUploadResult completeMultipartUploadResult =amazonS3

            .completeMultipartUpload(new CompleteMultipartUploadRequest(BUCKET_NAME,

objectKey, uploadId,partETags));


客户端:

1. 调用服务端提供的init接口

2.调用服务端提供的presign接口

3. 调用pre-signed url上传文件流

两种调用pre-signed url的方式:

1. OKHttp client(如果上传的是inputstream,请求的header中需要包含"content-type: application/octet-stream")

package com.osthus.ais.file.monitor.service.common;

import static com.osthus.ais.file.monitor.util.FileMonitorConfigurationConstants.OKHTTP_TIMEOUT;

import static com.osthus.ais.file.monitor.util.FileMonitorConfigurationConstants.OKHTTP_TIMEOUT_DEFAULT;

import java.io.IOException;

import java.io.InputStream;

import java.util.Map;

import java.util.Optional;

import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.StringUtils;

import org.springframework.http.ResponseEntity;

import de.osthus.ambeth.config.Property;

import de.osthus.ambeth.ioc.IStartingBean;

import de.osthus.ambeth.log.ILogger;

import de.osthus.ambeth.log.LogInstance;

import okhttp3.MediaType;

import okhttp3.OkHttpClient;

import okhttp3.Request;

import okhttp3.RequestBody;

import okhttp3.Response;

import okhttp3.ResponseBody;

import okhttp3.internal.Util;

import okio.BufferedSink;

import okio.Okio;

import okio.Source;

public class OKHttpService implements IStartingBean {

private static OkHttpClient client;

@LogInstance

    private ILogger log;

@Property(name =OKHTTP_TIMEOUT, defaultValue =OKHTTP_TIMEOUT_DEFAULT)

private long okHttpTimeOut;

public ResponseEntityput(String url,InputStream inputStream,long partLength,String contentType,

Map headers) {

log.debug(String.format("PUT request to %s", url));

MediaType mediaType =MediaType.parse(contentType);

RequestBody body =this.createRequestBody(mediaType, inputStream, partLength);

Request.Builder builder =new Request.Builder().url(url).method("PUT",body);

builder.addHeader("Content-Type", contentType);

if (headers !=null) {

headers.forEach(builder::addHeader);

}

Request request =builder.build();

try (Response response =client.newCall(request).execute()) {

ResponseBody responseBody =response.body();

String returnBody ="";

if (Optional.ofNullable(responseBody).isPresent()) {

String result =responseBody.string();

returnBody =StringUtils.isNotEmpty(result) ?result : returnBody;

}

return ResponseEntity.status(response.code()).body(returnBody);

}catch (IOException e) {

throw new RuntimeException(e);

}

}

private RequestBody createRequestBody(MediaType contentType,final InputStream inputStream,long length) {

return new RequestBody() {

@Override

          public MediaType contentType() {

return contentType;

}

@Override

          public long contentLength() {

return length;

}

@Override

          public void writeTo(BufferedSink bufferedSink)throws IOException {

Source source =null;

try {

source =Okio.source(inputStream);

bufferedSink.writeAll(source);

}finally {

Util.closeQuietly(source);

}

}

};

}

@Override

    public void afterStarted()throws Throwable {

client =new OkHttpClient().newBuilder()

.readTimeout(okHttpTimeOut,TimeUnit.MILLISECONDS)

.writeTimeout(okHttpTimeOut,TimeUnit.MILLISECONDS)

.connectTimeout(okHttpTimeOut,TimeUnit.MILLISECONDS)

.build();

}

}

2. Rest template

public void uploadInputStream(String url, InputStream inputStream, long contentLength, String md5DigestValue){

Map<String, String> headers = new HashMap<>();

headers.put(CONTENT_MD5, md5DigestValue);

headers.put(CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);

ResponseEntity<?> response = restInvoker.put(URI.create(url), new CommonInputStreamResource(inputStream, contentLength), ResponseEntity.class, headers);String result = (String)response.getBody();

加粗的代码URI.create(url)有一个坑,这里不可以将url以字符串的形式直接传进去。因为aws所签发的pre-signed url是经过encode的,里面会包含%。而restTemplate会再一次encode传入的url字符串,这就导致url变得无效。所以用加粗位置的代码,自己直接传入URI。如果我们对aws返回的per-signed url进行decode,然后传入,也是不行的。

因为restemplate会对url中的%进行encode,这就导致经过decode的pre-signed url无法成功(aws返回的默认是encode过的,应该被decode后使用);另外, restemplate不会对url中的+进行encode(实际上应该encode才会成功),这就导致手动decode后的pre-signed url也会报错(里面包含+)。详细分析见:https://blog.csdn.net/xs_challenge/article/details/109451263

public class CommonInputStreamResource extends InputStreamResource {

private long length;

public CommonInputStreamResource(InputStream inputStream) {

super(inputStream);

}

public CommonInputStreamResource(InputStream inputStream,long length) {

super(inputStream);

this.length = length;

}

@Override

    public String getFilename() {

return "temp";

}

@Override

    public long contentLength() {

long estimate =length;

return estimate ==0 ?1 :estimate;

}

}

4. 调用服务端的complete接口


当enable KMS时,generate和consume pre-signed url需要做哪些修改在下面这篇文章里说的很详细。

https://aws.amazon.com/blogs/developer/generating-amazon-s3-pre-signed-urls-with-sse-kms-part-2/

解决方案:应用服务OkHttpClient创建大量对外连接时内存溢出 

https://zhuanlan.zhihu.com/p/266363937

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容