AWS S3へのアップロード方法：ダイレクトアップロードを使って効率的に静的コンテンツをアップロードしよう

## AWS S3へのアップロード方法
静的なコンテンツをAWS S3へアップロードするのはよくある手法です。
例えば画像や動画をS3へアップロードすることがあると思います。
その中でも、ダイレクトアップロードという方法があります。ダイレクトアップロード機能を使うとサーバーに依存せずにアップロードが出来ます。


## 従来の方法
S3へのアップロードはサーバー側で処理を行い、DBに関連付けを記録することが一般的でした。この方法ではシンプルにアップロードするファイルをPOSTデータとしてWebサーバーへ送信し、WebサーバーでS3へのアップロードを行い、アップロードされたファイルの関連付けをDBへ記録しています。
![従来のS3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/7ec82a1b-d993-4847-8b34-91439f25bc8f.jpeg)



## ダイレクトアップロード
最近ではフロントエンドとバックエンドの分離が進んで来ています。このためダイレクトアップロードという方法でアップロードという手法を用いてS3へアップロードする方法が主流になってきています。
この方法では事前にアップロードに必要な情報を取得しておき、アップロード自体はフロントエンドのJavaScriptのAjax処理に任せて、アップロードした後に関連付け情報のみをサーバー側で行うという構成になります。
S3へのアップロードをサーバー側に依存させずに、フロントで直接アップロードできるのがダイレクトアップロードという機能になります。
![S3ダイレクトアップロード.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/64eb859c-ed51-d9d6-3f57-ed72645ecec4.jpeg)


## 実装
今回はバックエンドにRailsAPIサーバーを建てます。
Railsで標準で使えるActiveStorageを使います。
以下のページを参考に導入してください。

https://railsguides.jp/active_storage_overview.html


Userモデルに1:Nで紐づく画像がアップロードできることを想定します。
```User.rb
class User < ActiveRecord::Base
  # Active Storage
  has_many_attached :images
end
```

ActiveStorageを使うとUserモデルと紐づく下記のテーブルが作成されます。
S3へアップロードした画像への関連付けはこのテーブルに保存されます。

- active_storage_variant_records
- active_storage_blobs
- active_storage_attachments

## 1. アップロードに必要な情報を返すAPI
フロントエンドから直接アップロードするためには、事前に**S3への署名付きURL**の発行が必要になります。
![Screenshot 2023-04-29 at 13.18.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/aeb717cf-f2e0-07f8-965e-a2ba84ed974f.png)

署名付きURLの発行には以下のパラメータが必要になります。これらはフロントエンド側の処理で計算を行いパラメータとして含める必要があります。
![Screenshot 2023-04-29 at 13.27.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/ecda2063-9cb7-20a8-4720-b3eb022ab766.png)

APIサーバー側はレスポンスとしてS3へアプリケーションするのに必要な署名付きURLを返却します。さらに**blob_signed_id**というアップロード後にDBと関連付けするための情報も必要になります。
![Screenshot 2023-04-29 at 13.28.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/beb087ab-c6f8-92b4-f4f6-32249f7f38d4.png)

もう少し詳しくレスポンスの内容を見てみましょう。
blob_signed_idは先述の通り、このあとアップロードが成功した後にDBと紐づけるために必要なIDになります。
direct_upload_urlはS3への署名付きURLになります。フロントエンドでこのURLに向けてAjax通信でファイルを送信する必要があります。
また、アップロードにはdirect_upload_headersで指定したパラメータをHeader情報に含める必要があります。
![Screenshot 2023-04-29 at 13.31.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/9a150139-9365-0e9c-7e1a-0d40b1435dc1.png)

このAPIから取得できた情報で実際にはフロントエンド側でアップロードの処理を行いますが、crulコマンドを使ってアップロードのテストを行う事ができます。

```
curl -X PUT -T "/Users/XX/Desktop/test.png" \
-H Content-Type:"image/png" \
-H Content-MD5:"n4UCchK/eHAXIsO5QVWVCA==" \
-H Content-Disposition:"inline; filename=\"test.png\"; filename*=UTF-8''test.png" \
"http://XXX/activestorage/blobs/uploaded/users/1/user_images/u6dssto3bvwq72bz3bqcztom0f36?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20221009%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20221009T122108Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bcontent-type%3Bhost&X-Amz-Signature=11caec4677467f3c3410c8e5dceb924330ea967eb97dafff040d3f4e2acb3a0d"
```

(実装例)
``` app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
  before_action :authenticate_api_v1_user!
  
  def direct_uploads
    # Userでスペースを区切る
    prefix = "activestorage/blobs/uploaded/users/#{current_user.id}/user_images/"
    key = File.join(prefix, ActiveStorage::Blob.generate_unique_secure_token)

    blob = ActiveStorage::Blob.create_before_direct_upload!(
      filename: params[:filename],
      byte_size: params[:byte_size],
      checksum: params[:checksum],
      content_type: params[:content_type],
      key:,
    )

    # アップロードとアタッチに必要な情報
    @blob_signed_id = blob.signed_id # アップロード後の紐付け(アタッチ)に使うid
    @direct_upload_url = blob.service_url_for_direct_upload # アップロードURL
    @direct_upload_headers = blob.service_headers_for_direct_upload # アップロードヘッダー
  end
end
  
```

```app/views/api/v1/users/direct_uploads.json.jbuilder
json.status :ok
json.data do
  json.blob_signed_id @blob_signed_id
  json.direct_upload_url @direct_upload_url
  json.direct_upload_headers @direct_upload_headers
end
```

![署名付きURL.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/a3f1d60f-f042-0312-d128-4587c8239d5a.jpeg)



## 2. アップロードしたファイル情報とDBを紐付ける（アタッチする）API
アタッチAPIと名称していますが、フロントエンドでアップロードしたS3のファイルとDBの関連付けを行うAPIになります。
![Screenshot 2023-04-29 at 13.35.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/d4c36875-1b84-bcb1-0f40-f64a36078e55.png)

さきほどのアップロードに必要な情報を返すAPIでレスポンスとして取得できたblob_signed_idをパラメータとして送信します。これによりユーザーとアップロードした画像との関連付けをDBへ記録することが出来ます。
![Screenshot 2023-04-29 at 13.37.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/fd8ef345-8d60-87a3-6c4c-176816c38c2d.png)



(実装例)
``` app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
  before_action :authenticate_api_v1_user!
  
  def attach_image
    blob = ActiveStorage::Blob.find_signed!(params[:blob_signed_id])
    current_user.images.attach([blob])
    @message = 'Attached the user image'
  end
end
```

```app/views/api/v1/users/attach_image.json.jbuilder
json.status :ok
json.message @message
```


![アタッチ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174291/95b4be72-4f31-16e3-4f57-855e8a8b40aa.jpeg)



## まとめ
フロントエンドはモダンなシステムであればReactやVue.jsなどの構成があると思います。
またWebシステムだけでなく、モバイルアプリケーションのような場合も考えらます。
バックエンドだけでアップロードをすることも可能ですが、APIサーバーのタイムアウトなどを考えると、このようにフロントエンド側でアップロードという時間がかかる処理を行うことはとても合理的です。