SDK for Rubyを使用してS3で署名付きURLを発行し、ブラウザにてダウンロードを行う。

# 実装したもの
view上のダウンロードボタンを押すと、S3にアップロード済みのログデータの署名付きURLを発行してリダイレクトを行いダウンロードさせる機能を作成しました。
今後似たような機能を作成するときに困らないように自分の言葉で備忘録として残しておくための記事となります。

# 必要な知識、参考にした記事
### SDK for Ruby
RubyによってS3などのAWSサービスにアクセスし管理ができるようになるものです。SDKはソフトウェア開発キットと呼ばれるけどあんまりピンときてない。現状私はライブラリ、フレームワーク、APIを提供するパッケージというイメージを持っています。

なお、今回の記事についてはSDK for Rubyの導入については省略します。

https://aws.amazon.com/jp/sdk-for-ruby/

### AWS::S3::Client
SDK for Rubyに含まれる「クラス」でS3との低レベル（抽象度が低く詳細なことはできない）なAPI通信を行うことができます。
すでに用意されたメソッドによってS3に対してさまざまな操作を行うことができます。

<br>
例えば、

```Ruby
client.put_object(bucket: "bucket-01", key: "example_01.txt", body: "example")
```
clientインスタンスを用意して、`put_object`メソッドによって`example_01.txt`を指定したバケットにアップロードすることができます。
基本的にはAWS::S3::Clientを継承したインスタンスを立てて、そこに色々なオリジナルのメソッドを持たせて開発を進めていきました。


公式

https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html

参考にした記事↓（会社の先輩の記事でした）

https://qiita.com/Kta-M/items/b9f22277e983a05389fb

<br>

### Aws::S3::Presigner
先ほどのClientのようにAWS側で用意されたクラス。こちらは署名付きURLの管理に関わるもの。


公式

https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Presigner.html


# 実際の作業履歴
大まかな流れとしては、
1. Clientインスタンスを作成する
2. 署名付きURLを発行するインスタンスメソッドを作成する
3. ダウンロードアクションを追加してそこでClientに対して2のメソッドを実行させる
4. ブラウザ側で、コントローラーから受け取った署名付きURLにリダイレクトする
という流れでした

### 1 Clientインスタンスの初期化
SDKの導入等については割愛。

OriginalClientについては既に作成済みだったので、深くは関われませんでしたが自分なりにまとめてみます。

clientをインスタンスを生成するための独自のクラスを作成していました。
```ruby
class OriginalClient
  HOGE_BUCKET_NAME = "hoge-huga-#{Rails.env}".freeze
  # freezeによって予期せぬ変更を防ぐ

  # initialize:クラスのインスタンスが生成されたときに自動的に呼び出される特別なメソッドで、インスタンスの初期設定を行う。
  # initializeを呼び出す際にclient引数を省略した場合、自動的に新しいAws::S3::Clientインスタンスを生成する。
  def initialize(client: Aws::S3::Client.new)
    @client = client
    @presigner = Aws::S3::Presigner.new(client: @client) #clientとpresignerの初期化が完了
  end
end

```
クラスにバケット名を定数として定めています。
バケット名の中にRailsの現在の実行環境名（development, test, production）を動的に挿入することによって、開発環境、テスト環境、本番環境それぞれで異なるS3バケットを指し示すことができます。(S3バケットは3つ作成している)

ここまでで、OriginalClientを作成すると、S3バケットと通信を行うための`@client`と`@presigner`を初期設定が完了しました。

<br>

### 2 用意したClientインスタンスに署名付きURLを発行するメソッドを追加


元々用意されている`presigned_url`メソッドを使用して署名付きURLを発行するオリジナルのメソッドを作成する

```ruby
def generate_get_object_url(bucket, key, expires_in: 3600)
  @presigner.presigned_url(:get_object, bucket:, key:, expires_in:)
end
```
`presigned_url`はClass: Aws::S3::Presignerにて元々用意されているメソッド。引数に指定したバケットへの署名付きURLを作成する。
expires_inによって、URLの有効期限を指定できる。今回はデフォルトで１時間の有効期限としています。

ここまでで、OriginalClientのインスタンスを作成し、適切なバケット名を引数として`generate_get_object_url`を呼び出すことで署名付きURLを発行する準備が整いました。


### 3. コントローラーアクションによって、Clientインスタンスを生成しメソッドを呼び出す
### 4. ブラウザ側で署名付きURLを受け取ってリダイレクトする

ほぼ同時に実装することになりました


view側
```index.html.erb
<% logs.each do |log| %> # eachでlogを回していて、複数のダウンロードボタンを作成しています
    <button class='btn btn-default download-button' data-log-id="<%= log.id %>"> #data-log-idでjsに渡す
        <%= 'ダウンロード' %>
    </button>
<% end %>
```
その後のリダイレクト処理のために、詳細についてはJavaScriptに持たせました
```js
<script>
$(document).ready(function() {
  $('.download-button').click(function(e) {
    e.preventDefault();

    const LogId = $(this).data('log-id'); //buttonの部分から受け取る
    const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');

    fetch(`/hoge/~~~/aaaaaa/${LogId}/download`, { //この書き方でコントローラー側でparamsで受け取れるようになる
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
      },
    })
    .then(response => {
      if (!response.ok) {
        console.error({ response });
      }
      return response.json(); //コントローラーからjson形式の署名付きURLを受け取ったあとの処理
    })
    .then(data => {
      window.location.href = data.presigned_url; //コントローラーから返った署名付きURLにリダイレクトすることでダウンロードが実行される
    })
    .catch(error => {
      console.error({ error: error.message });
    });
  });
});
</script>
```



ルーティング

```routes.rb
resources :aaaaaa, only: [:index, ~~~], controller: 'hogege' do
    member do
      get 'download' #ダウンロードアクション自体ははgetメソッドで実装しました
    end
  end
```

downloadアクション（コントローラー）

```controller.rb
def download
    log_id = params[:id] # /hoge/~~~/aaaaaa/${LogId}/downloadの形のURLから${LogId}を受け取れる
    log = @aaaaaa.logs.find(log_id) # 既に作成済みのlogと紐づける
    file_name = log.file_name  # file_nameについてはlogの保存先を作成するインスタンスメソッドを別で組んでいます。
    presigned_url = OriginalClient.new.generate_get_object_url( #別途解説
      OriginalClient::HOGE_BUCKET_NAME, file_name
    )
    render json: { presigned_url: } #作成した署名付きURLをjson形式でブラウザに返す
  rescue StandardError
    flash[:alert] = 'ダウンロードエラーが発生しました'
    redirect_to '元々のページ'
  end

```

重要なのはこの部分
```controller.rb
presigned_url = OriginalClient.new.generate_get_object_url(
  OriginalClient::HOGE_BUCKET_NAME, file_name
)
```

一番最初に作成した`OriginalClient`クラスのインスタンスをnewして、自作したメソッドの`generate_get_object_url`を呼び出しています。
それの引数として、バケット名とkey(ファイル名)を入れています。expires_in: 3600についてはデフォルトなので引数は必要無しです。

このような流れで、HOGE_BUCKET_NAMEというS3の中にある、file_nameというkeyのファイルへの署名付きURLを発行してダウンロードすることができるようになりました。