S3にファイルをアプロードして メタデータに content-disposition を設定する

## ゴール

- Webブラウザを使ってアップロードしたファイルをCloudFront経由でダウンロードする。
- ダウンロードしたファイルをブラウザで保存する時のダイアログには、S3のKeyとは別の任意のファイル名を指定できる。

![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145742/a9d6998f-d0e2-14cb-6324-9888b33181b5.png)

 上の「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))](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/s3/model/PutObjectRequest.Builder.html#contentDisposition-java.lang.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` に変換されるので半角スペースに戻すため。

```java
PutObjectRequest.builder().contentDisposition( 
    "attachment; filename=\""
    + URLEncoder.encode(fileName, "UTF-8").replace("+", "%20") 
    + "\"")
```

設定した content-disposition の値は AWSコンソールのメタデータで確認できる。
![s3-metadata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145742/880a5fac-c48e-6648-1779-e20c75260b52.png)



## コントローラーの実装


```java:コントローラー
@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());
    }
  }
}

```

```java:サービスクラス(WyswygService.java)
@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());
  }
}
```