社内で話したメディアダウンロード機能の実装について

## 背景
最近、社内でS3に保存したメディアをHTML上で手元にダウンロードする機能を実装してほしいとの要望があった。
実際に作成していく中で、私の知識不足もあり、いろいろ嵌っていったので、共有と備忘録も込めて、この記事を記述する。

## なんだ、簡単じゃん
調べてみると下記のようなコードで実装できるらしい！（やったね！）
```html
<a href="https://image-test.s3.amazonaws.com/test.png" download>ダウンロード</a>
```

うっきうきで実装してみると...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1770714/02d8e9ab-68b2-041d-a702-0f94825b0894.png)
違う、そうじゃない...
調べてみると、下記のような文言がありました。
>この属性は同一オリジンのURLに限り動作する

今回、ダウンロードしたい画像はS3に置いてあるため、当然のことながら同一ドメインではない。よくリファレンスを読んでみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1770714/d46bd86c-839f-6abd-6099-39993eb8b2dd.png)

どうやらcontent-dispositionを設定しなければならないみたい。
というわけで、content-dispositionを付けてみよう！

## 準備
今回行ったことは以下の三つである。
1. S3にメディアを保存するときに拡張子を指定する
2. content-dispositionを設定する
3. CORS対応を行う

画像はS3を保存しており、違うドメインのHTMLで画像をダウンロードさせるために上記の三つが必要となる。

## 1. S3にメディアを保存するときに拡張子を指定する
今回は簡単なスクリプトでrubyで記述する。
実際にはメディアをアップロードするだけでいいのだが、aws-cliでrubyから作ってみる
```rb: content_type.rb
require 'aws-sdk'
require 'mime/types'
require 'httpclient'

file_name = "test.jpeg"
region = "ap-northeast-1"
bucket_name = image-test

file = File.read(file_name)
File.write("test", file)

s3 = Aws::S3::Resource.new(region: region)
obj = s3.bucket(bucket_name).object(file_name)

obj.put(
  body: file,
  content_type: 'png'
)
```

## 2.content-dispositionを設定する
そもそもcontent-dispositionってなんぞや？となっていた。
調べてみると、下記の通り。
>コンテンツをwebページの一部として表示するかダウンロードするかを示します。
inline を指定すればwebページとして、attachment を指定すればダウンロードファイルとして、を表します。 また、filenameパラメータで、ファイルの初期名を指定できます。

なるほど、それでは、実際にメディアをgetする時に指定してみよう！
設定するaws-cliは以下の通り
```ruby: presigned_url.rb
require 'aws-sdk'

file_name = "test.jpeg"
region = "ap-northeast-1"
bucket_name = image-test

s3client = Aws::S3::Client.new(
    region: region,
    credentials: '~/.aws/config',
)

signer = Aws::S3::Presigner.new(Client: s3client)

puts signer.presigned_url(
    :get_object,
    bucket: bucket_name,
    key: file_name,
    expires_in: 3600,
    response_content_disposition: "attachment; filename=#{file_name}"
)
```

## 3.CORSを設定する
今回、AWSのS3に保存しているメディアについて、presigned_urlを発行し、ダウンロードページに渡すことを想定する。
そのため、使用されるメディアのCORS対応を行わなければ、HTMLでダウンロードしようとしたときに引っかかるため、この設定を行う。
AWS S3では、バケットのアクセス許可のタブから下記の場所にCORSの設定を施すことができる。
![スクリーンショット 2022-12-05 230322.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1770714/808f4be6-e2dd-bc03-28e7-f2b04b3291e7.jpeg)
exposeHeadersにContent-Dispositionをセットして準備完了！！

## 実行
まず、presigned_urlを発行する。
先ほどのコードを実行してS3にメディアをアップロードする。
```sh: 実行
ruby content_type.rb
```

今回は、拡張子pngで試してみよう。
次に、このメディアをダウンロードするためのpresigned_urlを発行する。
```sh: 実行
ruby presigned_url.rb 
```

あとはこのURLをHTMLのボタンに組み込む。
本当はAPIを作成して都度presigned_urlができるようにしたいが、今回は直に入力する。
```html
<a href="実行した結果のpresigned_url">ダウンロード</a>
```

すると...?

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1770714/93d640b9-7c4c-9b54-d142-fb1110ebcb16.png)

ボタンを押したらダウンロードできた！！
これで目的を達成！！


## 気を付ける嵌りポイント
### メディアのcontent-typeを指定しないと...?
binary/octet-streamというcontent-typeになる。
これが厄介で、binary/octet-streamのファイル名をtest.pngとかにしてダウンロードすると、凡そ全てのブラウザでダウンロードできる、iPhone君だけはきちんとbinary/octet-streamのファイルとして処理されてしまう...
やはり、content-typeはしっかりと指定しよう！！

## まとめ
content-dispositionという、メディアをダウンロードするかどうかをヘッダーに付与することで、メディアをダウンロードする機能を実行できます。
他にもやり方がたくさんあるかもしれないが、awsなど別ドメインを介してダウンロードする場合、参考になればと思います。

## 参考
https://developer.mozilla.org/ja/docs/Web/HTML/Element/a