ReactからRails経由でS3へファイルをアップロードする方法

やっはろー、へっぽこだよ。
Reactでファイルのアップロードをする機能を作ったら案外手こずったから記事にしてみました。
バックエンドはRailsでS3にファイルを格納してます。
## ReactからファイルデータをRailsへ飛ばす
まずファイルを添付するボタンを作ります。
色々ありますが、今回は[Material-UI](https://mui.com)を採用しました。
おしゃれかつ痒い所まで手が届く機能を備えてます。
まずはMaterial-UIをインストールします。
```
yarn add @mui/material @emotion/react @emotion/styled
```
次に以下のコードをコピペしてください。
```typescript:typescript
import React,{useMemo} from "react";
import Input from "@mui/material/Input";
export default function APP() {
  const [file, setFile] = React.useState<File | string>();
  const handleChangeFile = useMemo(
    () => (e: any) => {
      const file = e.target.files[0];
      setFile(file);
    },
    [file]
  );
  return (
    <label htmlFor="contained-button-file">
      <Input
        id="contained-button-file"
        type="file"
        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
          handleChangeFile(e);
        }}
      />
    </label>
  );
}
```
[コードサンドボックス：サンプル](https://codesandbox.io/s/focused-sid-we13fh?file=/src/App.tsx)
そうすると↑を見て貰えば分かる通り、ファイル添付ボタンが現れたと思います。
仕組みを説明すると、添付ファイルを添付すると
```typescript:typescript
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
  handleChangeFile(e);
}}
```
↑が発火し
```typescript:typescript
const handleChangeFile = useMemo(
  () => (e: any) => {
    const file = e.target.files[0];
    setFile(file);
  },
  [file]
);
```
↑が実行され、選択した一番目のファイルが`setFile`によって`file`に格納されます。
それで`file`をRailsに飛ばしておｋって具合でいけばいいのですが、このままだとRailsがファイルを認識しません。
`file`をファイル形式にコンバートしないといけないのです。
以下のコードを追加します。
```diff_typescript:typescript
import React,{useMemo} from "react";
import Input from "@mui/material/Input";
export default function APP() {
  const [file, setFile] = React.useState<File | string>();
  const handleChangeFile = useMemo(
    () => (e: any) => {
      const file = e.target.files[0];
      setFile(file);
    },
    [file]
  );
+  const createFormData = () => {
+    const formData = new FormData();
+    formData.append("user[file]", file!);
+    return formData;
+  };
  return (
    <label htmlFor="contained-button-file">
      <Input
        id="contained-button-file"
        type="file"
        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
          handleChangeFile(e);
        }}
      />
    </label>
  );
}
```
`formData.append`によってfile形式にコンバートされます。
`formData.append`の引数は
```typescript:typescript
formData.append("Railsのテーブル名[カラム名]", ファイルが格納された変数);
```
にします。
テーブル名はs抜きなので注意してください。
次にaxiosでRailsにデータを飛ばします。
```
yarn add axios
```
axiosをインストール。
以下のコードを追加してください。
```diff_typescript:typescript
import React, { useMemo } from "react";
import Input from "@mui/material/Input";
+ import Button from "@mui/material/Button";
+ import axios from "axios";
export default function APP() {
  const [file, setFile] = React.useState<File | string>();
  const handleChangeFile = useMemo(
    () => (e: any) => {
      const file = e.target.files[0];
      setFile(file);
    },
    [file]
  );
  const createFormData = () => {
    const formData = new FormData();
    formData.append("user[file]", file!);
    return formData;
  };
+   const postFile = async () => {
+     const data = createFormData();
+     await axios
+       .post(`http://localhost:3000/post_file/`, data)
+       .then((response) => {
+         //成功したときの処理
+       })
+       .catch(() => {
+         //失敗したときの処理
+       });
+   };
  return (
    <label htmlFor="contained-button-file">
      <Input
        id="contained-button-file"
        type="file"
        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
          handleChangeFile(e);
        }}
      />
+       <Button
+         size="medium"
+         onClick={() => {
+           postFile();
+         }}
+       >
+         Railsにファイルを送信
+       </Button>
    </label>
  );
}
```
[コードサンドボックス：サンプル](https://codesandbox.io/s/wizardly-sid-p39sxi?file=/src/App.tsx:0-1198)
コードを追加したら↑と同じ感じになっていると思います。
ボタンを押したら`postFile`が発火して、axiosが動くという具合になっています。
```typescript:typescript
const data = createFormData();
```
ここでコンバートされたファイルを`data`に格納して
```typescript:typescript
.post(`http://localhost:3000/post_file/`, data)
```
こういう感じでrailsにデータを送信します。
本番の場合はもちろん`http://localhost:3000`の部分が変わります。
ご自身の本番のURLを入れてください。
細かいaxiosの使い方は他記事を参考にしてください。
## Railsでデータを受け取る
デフォルトでRailsは別のポートからpostリクエストを受け付けていません。
なので、`rack-cors`を使って許可します。
```diff_ruby:gemfile
+ gem 'rack-cors'
```
gemfileに↑を追加して`bundle install`を実行してください。
次に`config/initializers`配下に`cors.rb`を作成します。
設定ファイルの中身は↓みたいな感じ。
```ruby:config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:3001'
    resource '*',
             headers: :any,
             expose: %w[access-token expiry token-type uid client],
             methods: %i[get post put patch delete options head],
             credentials: true
  end
end
```
`origins`部分はRailsが使用しているポート部分です。
デフォルトは3000ですが、へっぽこはReactを3000にしてるので、Railsは3001です。
本番の場合も同じで、originsに**ドメイン**を入れます。
次に`carrierwave`と`fog-aws`とをインストールします。
```diff_ruby:gemfile
  gem 'rack-cors'
+ gem 'carrierwave'
+ gem 'fog-aws'
```
`carrierwave`はアップロード、ダウンロードなどをよしなににやってくれる、便利なやつです。
`bundle exec rails g uploader File`を実行して`app/uploaders/file_uploader.rb`を作成。
```ruby:app/uploaders/file_uploader.rb
class FileUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  # include CarrierWave::MiniMagick
  # Choose what kind of storage to use for this uploader:
  # if Rails.env.production?
  #   storage :fog
  # else
  #   storage :file
  # end
  if Rails.env.development?
    storage :file
  else
    storage :fog
  end
  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
  # Provide a default URL as a default if there hasn't been a file uploaded:
  # def default_url(*args)
  #   # For Rails 3.1+ asset pipeline compatibility:
  #   # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
  #
  #   "/images/fallback/" + [version_name, "default.png"].compact.join('_')
  # end
  # Process files as they are uploaded:
  # process scale: [200, 300]
  #
  # def scale(width, height)
  #   # do something
  # end
  # Create different versions of your uploaded files:
  # version :thumb do
  #   process resize_to_fit: [50, 50]
  # end
  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  # def extension_whitelist
  #   %w(jpg jpeg gif png)
  # end
  # Override the filename of the uploaded files:
  # Avoid using model.id or version_name here, see uploader/store.rb for details.
  # def filename
  #   "something.jpg" if original_filename
  # end
end
```
↑みたいな感じにします。
```ruby:ruby
if Rails.env.development?
  storage :file
else
  storage :fog
end
```
`storage :file`だとアップロードしたファイルは`public/uploads`配下に保存されます。
`storage :fog`だとS3に保存されます、`fog-aws`のおかげで。
上記はローカルのときは`public/uploads`配下、本番の時はS3に保存されるように設定しています。
envはこんな感じで設定しています。
Railsのルートディレクトリに置くやつですね。
```.env
RAILS_ENV = "development"
```
本番だったらこれをProductionに変えるだけです。
S3に保存するために`config/initializers`配下に`carrierwave.rb`を作成します。
```ruby:config/initializers/carrierwave.rb
require 'carrierwave/storage/abstract'
require 'carrierwave/storage/file'
require 'carrierwave/storage/fog'
CarrierWave.configure do |config|
    config.storage :fog
    config.fog_provider = 'fog/aws'
    config.fog_directory  = 'バケット名' # 作成したバケット名を記述
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'], # 環境変数
      aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], # 環境変数
      region: 'ap-northeast-1',   # アジアパシフィック(東京)を選択した場合
      path_style: true
    }
end 
```
設定は↑みたいな感じ。
詳しいことは
[【Rails】 CarrierWaveチュートリアル](https://codesandbox.io/s/wizardly-sid-p39sxi?file=/src/App.tsx:0-1198)
をご覧ください。
マイグレーションでUserテーブルとFileカラムを作ります。
```
rails g migration User
```
```ruby:db/migrate/20220617022008_user.rb
class User < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string 'file'
      t.timestamps
    end
  end
end
```
```
rails db:migrate
```
次にモデルを作成して、CarrierWaveを紐づけます。
```
rails g model User
```
```ruby:app/models/user.rb
class User < ApplicationRecord
  mount_uploader :file, FileUploader
end
```
これで`file_uploader.rb`に紐づきます。
次にルートを設定します。
```ruby:config/routes.rb
Rails.application.routes.draw do
  post '/post_file' => 'users#post_file'
end
```
コントローラーを作成します。
```
rails g controller user
```
```ruby:app/controllers/user_controller.rb
class UsersController < ApplicationController
  def post_file
    User.create(file: params[:user][:file])
    render status: 200
  end 
end
```
これでaxiosで`.post("http://localhost:3000/post_file/", data)`でリクエストしたとき、`user_controller`が`post_file`のアクションを呼び出し、paramsに入ったfileをfileカラムに保存し、Userを作成します。
fileカラムはCarrierWaveに紐づいているのでファイル形式に再度コンバートされ、`carrierwave.rb`の設定に応じて保存先に保存されるという感じになります。
なんか不備あったらコメント欄で教えてください。
以上へっぽこでした。