Rails+S3+CloudFrontで画像アップロード

## 概要
フロントからpostした画像をrailsからs3にアップロードして、それをCloudFront経由で表示するということをやったので、それを書きました。

## フロントからrails
フロント(nuxt)からrailsへaxiosでpost
下記のようなコードでrails側にpostします。

```javascript

public async createQuestion() {
  const formData = new FormData()
  formData.append("image", this.image)

  const headers = {
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  }

  try {
    await this.$axios.$post('/api/questions', formData, headers)
  } catch (e) {
    this.hasSubmitError = true
  }
}
```

`image`は簡易に書くと下記のように画像ファイルを選択したら`setImage()`が実行され、そこから画像ファイルを代入します。

```javascript
<template lang="pug">
  input(
    type="file" 
    id="image-uploader" 
    accept="image/png,image/jpeg" 
    @change="setImage"
  )
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'

@Component
export default class ImageUploader extends Vue {
  public image: File | null = null

  public setImage(e: { target: { files: File[] } }): void {
    this.image = e.target.files[0]
  }
}
</script>
```

## railsからs3
S3にアップロードするために諸々設定をします。

`aws-sdk-s3`をinstall
下記をGemfileに追加して`bundle install`

```ruby:Gemfile
gem 'aws-sdk-s3', require: false
```

`.env`にS3が操作可能なIAMユーザーのアクセスキーとシークレットキー、バケット名を設定します。

```:.env
AWS_ACCESS_KEY_ID="アクセスキー"
AWS_SECRET_ACCESS_KEY="シークレットキー"
S3_BUCKET="バケット名"
```

`config/initializers/aws.rb`を作成して、AWSの設定をします。
regionは各々の値を設定してください。

```ruby:config/initializers/aws.rb
require 'aws-sdk-s3'

Aws.config.update({
  region: 'ap-northeast-1',
  credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']),
})
```

今回は`questions_controller`でアップロード処理を書いています。
フロントからのpostリクエストはこちらにルーティングされます。

画像データをS3にアップロードした後にQuestionsに画像名を保存します。

```ruby:controllers/api/questions_controller.rb
require 'aws-sdk-s3'

class Api::QuestionsController < ApplicationController
  def create
    image_name = "question_image_#{Question.last.id + 1}"
    s3_resource = Aws::S3::Resource.new
    object = s3_resource.bucket(ENV['S3_BUCKET']).object(image_name)
    object.put(body: question_param[:image])
    Question.new(image: image_name)

    if @question.save
      render json: { status: 'Ok', question: @question }
    else
      render json: { error: @question.errors.messages }, status: :internal_server_error
    end
  rescue StandardError => e
    render json: { error: ['画像のアップロードでエラーが発生しました。'] }, status: :internal_server_error
  end

  private

  def question_param
    params.permit(:image)
  end
end
```

今回のQuestionモデルは下記のコマンドで作成しました。

```bash
rails g model Question image:string
```

## CloudFrontからS3

下記のサイトを参考にCloudFrontからS3のファイルにアクセスできるようにしました。
[CloudFront 経由で S3 のファイルにアクセスする](https://qiita.com/NaokiIshimura/items/46994e67b712831c3016)

## railsからフロント

下記のようにrails側でQuestionの値を返すAPIを作りました。

```ruby
def show
  @question = Question.find(params[:id])
  render json: { status: 'Ok', question: @question }
rescue ActiveRecord::RecordNotFound
  render status: :not_found, json: { body: 'question is not found' }
end
```

フロントで上記のAPIを呼んで画像を表示する画面を作りました。

```javascript
<template lang="pug">
  .image(v-if="question.image")
    img(:src="`${'CloudFrontのドメイン'}/${question.image}`", alt="image")
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'

@Component
export default class QuestionsShow extends Vue {
  public question = {
    image: ''
  }

  mounted() {
    const response = await this.$axios.$post(`/api/questions/${this.$route.params.id}`)
    this.question = response.question
  }
}
</script>
```

無事表示されました！
![スクリーンショット 2021-02-13 1.34.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1057421/55ba3474-8609-51dd-7459-30800d0eeefe.png)

## 参考

[CloudFront 経由で S3 のファイルにアクセスする](https://qiita.com/NaokiIshimura/items/46994e67b712831c3016)
[Rails での S3 への画像の直接アップロード](https://devcenter.heroku.com/ja/articles/direct-to-s3-image-uploads-in-rails)
[AWS SDK for Ruby を使用したオブジェクトのアップロード](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/UploadObjSingleOpRuby.html)