💻

ReactとRailsでS3にクライアントサイドファイルアップロードする

2021/03/07に公開

はじめに

S3にファイルアップロードする機能を実装することはよくあると思います。
僕自身Railsをよく使うので、carrierwaveのようなGemを使って実装していたのですが、クライアントサイドから直接S3にファイルアップロードする機能を実装することがあったので、その流れを備忘録として書いていきたいと思います。

使用する技術

  • React
  • Rails
  • S3
  • aws-sdk(Gem)

大まかな流れ

  1. AWSの設定(バックエンド)
  2. ファイルアップロード用の署名付きURLを発行する(バックエンド)
  3. 署名付きURLを取得する(フロントエンド)
  4. ファイルをアップロードする(フロントエンド)

AWSの設定

まずはAWSの設定を行います。

.envファイルにAWSのS3バケット名、アクセスキーID、シークレットアクセスキーを記載します。

S3_BUCKET=xxxxxxx
AWS_ACCESS_KEY_ID=xxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxx

上記の情報は流出すると大変危険ですので、取り扱いには十分注意してください。(.envファイルをgitignoreするなど)

※本番環境では環境変数に上記を記載して利用することになるかと思います

AWSの設定ファイルを作成し、regionとcredentials、S3バケットを設定します。

Aws.config.update({
  region: xxxxxxx,
  credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']),
})

S3_BUCKET = Aws::S3::Resource.new.bucket(ENV['S3_BUCKET'])

必要に応じてS3バケットのバケットポリシーで特定のオリジン(http://localhost:3000など)からのCORSを許可するように設定してください。

バックエンドでファイルアップロード用の署名付きURLを発行する

以下のメソッドを実装します。
このメソッドが返すURLがフロントエンドからアップロードする時のエンドポイントになります。

def s3_direct_post
  resource = S3_BUCKET.presigned_post(key: "<アップロードするディレクトリのpath>/#{SecureRandom.uuid}/${filename}", success_action_status: '201', acl: 'public-read', content_length_range: 1..(10.megabytes))
  render json: { url: resource.url, fields: resource.fields }
end

S3ではkeyがユニークである必要があるので、SecureRandom.uuidを使用しています。
また${filename}は特別な記法で、このように記載するとアップロードしたファイルの名前がこの部分に入ります。(例えば、hoge.pdfをアップロードすればhoge.pdfが入る)

フロントエンドでファイルアップロードする

file fieldのonChangeイベントで署名付きURLの取得〜ファイルアップロードを行います。

// jsx
<input type="file" onChange={handleChange} />

// handler
const handleChange = (e) => {
  const res = await fetch(<バックエンドのs3_direct_postを叩くためのURL>)
  const S3DirectPost = await res.json()

  const file = e.target.files[0]
  const fields = S3DirectPost[:fields]
  const formData = new FormData()
  for (let key in fields) {
    formData.append(key, fields[key])
  }
  formData.append('file', file)

  const ret = await fetch(S3DirectPost[:url], {
    method: 'POST',
    headers: { Accept: 'multipart/form-data' },
    body: formData,
  })
  const resText = await ret.text()
  const resXML = await parseXML(resText)
  const key = await resXML.getElementsByTagName('Key')[0].childNodes[0].nodeValue)
}

const parseXML = (text) => new DOMParser().parseFromString(text, 'application/xml')

最終的に取得したkeyをDBに保存するなどして利用してください。
例えばバックエンドで以下のようにしてkeyからファイルのURLを生成できます。

def file_url
  Aws::S3::Object.new(ENV['S3_BUCKET'], key).public_url
end

おわりに

署名付きURLの発行やhandleChangeの処理を再利用できるように共通化しておけば、それ以降は割とサクッと実装できるような気がしてます。

参考

https://devcenter.heroku.com/articles/direct-to-s3-image-uploads-in-rails
https://qiita.com/ytanaka3/items/ad150811df54aa7434fb

Discussion