Help us understand the problem. What is going on with this article?

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

はじめに

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

※下記に記載しているコードは実際に実装したコードを少し変えたもので、S3が絡む都合上動作確認はできていませんので、あくまで実装の流れを参考にするようにしてください。

使用する技術

フロントエンド

  • React

バックエンド

  • Rails

Gem

  • aws-sdk

ファイルアップロード先

  • S3

大まかな流れ

バックエンドの実装
1. AWSの設定
2. ファイルアップロード用の署名付きURLを発行する

フロントエンドの実装
1. 署名付きURLを取得する
2. ファイルをアップロードする

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 file = e.target.files[0]
  fetch(s3_direct_post_url) // バックエンドのs3_direct_postを叩くためのURL
    .then((res) => res.json())
    .then((json) => {
      const fields = json.fields
      const formData = new FormData()
      for (let key in fields) {
        formData.append(key, fields[key])
      }
      formData.append('file', file)

      fetch(json.url, {
        method: 'POST',
        headers: { Accept: 'multipart/form-data' },
        body: formData,
      })
        .then((res) => res.text())
        .then((text) => parseXML(text))
        .then((xml) => const key = xml.getElementsByTagName('Key')[0].childNodes[0].nodeValue)
        .catch(error => console.error('Error:', error))
    })
    .catch(error => console.error('Error:', error))
}

const parseXML = (text) => {
  const parser = new DOMParser()
  return parser.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

perches
東京を拠点とした初心者向けのプログラミングサークルです。年6回ほどハッカソンを主催しています。
https://perches.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away