ゴール
- Webブラウザを使ってアップロードしたファイルをCloudFront経由でダウンロードする。
- ダウンロードしたファイルをブラウザで保存する時のダイアログには、S3のKeyとは別の任意のファイル名を指定できる。
上の「iOS の画像 (1).jpg」を content-disposition
で指定する。
バージョン情報
- JDK: amazon-corretto-11.0.3.7.1-windows-x64
- Spring boot : 2.2.4.RELEASE
- AWS SDK for Java: 2.14.28
はまりポイント
PutObjectRequest.Builder#contentDisposition(string)) の引数はURLエンコードを行う必要がある。
PutObjectRequest.builder().contentDisposition( "attachment; filename=\"iOS の画像(1).jpg\"")
と書くと、下のエラーになる。
software.amazon.awssdk.services.s3.model.S3Exception: The request signature we calculated does not match the signature you provided. Check your key and signing method. (Service: S3, Status Code: 403, Request ID: XXXXXXXXXXXXXXXX, Extended Request ID: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
)
今回は、下の通りにした。 +
を %20
に置換する理由はファイル名の半角スペースがあるとURLエンコードで %20
に変換されるので半角スペースに戻すため。
PutObjectRequest.builder().contentDisposition(
"attachment; filename=\""
+ URLEncoder.encode(fileName, "UTF-8").replace("+", "%20")
+ "\"")
設定した content-disposition の値は AWSコンソールのメタデータで確認できる。
コントローラーの実装
@RestController
public class SummernoteApiController {
private WyswygService wyswygService;
public SummernoteApiController(@Autowired WyswygService wyswygService) {
this.wyswygService = wyswygService;
}
@PostMapping("/api/attachfile")
public String attachfile(@RequestParam("upload_file") MultipartFile uploadFile) {
if (uploadFile.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "添付するファイルを指定してください");
}
try {
String publishedUrl = wyswygService.uploadToS3(uploadFile);
return publishedUrl;
} catch (IOException | S3Exception e) {
e.printStackTrace();
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
}
}
@Component
public class WyswygService {
/** AWSのリージョン */
@Value("${aws.region}")
private String regsion;
/** 画像などをアップロードするS3バケット */
@Value("${aws.s3.assetsBucket}")
private String assetsBucket;
/** S3バケットに保存する時のパスのPrefix */
@Value("${aws.s3.assetsPrefix}")
private String assetsPrefix;
/** CloudFrontのホスト名 */
@Value("${aws.s3.cloudFrontHost}")
private String cloudFrontHost;
/** https://github.com/huxi/sulky/tree/master/sulky-ulid */
private ULID ulid = new ULID();
/**
* ファイルをAmazonS3にアップロードする
*
* @param uploadFile アップロードするファイル
* @return CloudForntからアクセスできるパス
* @throws IOException
*/
public String uploadToS3(MultipartFile uploadFile) throws IOException {
String contentType =
uploadFile.getContentType() != null
? uploadFile.getContentType()
: "application/octet-stream ";
String fileName =
uploadFile.getOriginalFilename() != null
? uploadFile.getOriginalFilename()
: "attached_file.dat";
String s3key= this.genrateS3KeyPrefix() + fileName.substring(fileName.lastIndexOf("."));
String cloudFrontUrl = String.format("https://%s%s", this.cloudFrontHost, key);
PutObjectRequest putObject =
PutObjectRequest.builder()
.bucket(this.assetsBucket)
.key(s3key.startsWith("/") ? s3key.substring(1) : s3key) // 先頭に「/」があると重複するので削除する
.contentType(contentType)
.contentDisposition(
"attachment; filename=\""
+ URLEncoder.encode(fileName, "UTF-8").replace("+", "%20")
+ "\"")
.build();
s3Client.putObject(putObject, RequestBody.fromInputStream(uploadFile.getInputStream(), uploadFile.getSize()));
return cloudFrontUrl ;
}
/**
* S3のキー(ファイルパス)のプレフィックスを生成する
* ulidを使って時間でソートできる一意な文字列で保存する。
*/
private String genrateS3KeyPrefix() {
String month = DateTimeFormatter.ofPattern("yyyy-MM").format(LocalDate.now());
return String.format(
"%s/%s/%s", this.assetsPrefix, month, ulid.nextValue().increment().toString());
}
}