0
1

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

Last updated at Posted at 2022-09-30

やっはろー、へっぽこだよ。
Reactでファイルのアップロードをする機能を作ったら案外手こずったから記事にしてみました。
バックエンドはRailsでS3にファイルを格納してます。

ReactからファイルデータをRailsへ飛ばす

まずファイルを添付するボタンを作ります。
色々ありますが、今回はMaterial-UIを採用しました。
おしゃれかつ痒い所まで手が届く機能を備えてます。
まずはMaterial-UIをインストールします。

yarn add @mui/material @emotion/react @emotion/styled

次に以下のコードをコピペしてください。

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>
  );
}

コードサンドボックス:サンプル
そうすると↑を見て貰えば分かる通り、ファイル添付ボタンが現れたと思います。
仕組みを説明すると、添付ファイルを添付すると

typescript
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
  handleChangeFile(e);
}}

↑が発火し

typescript
const handleChangeFile = useMemo(
  () => (e: any) => {
    const file = e.target.files[0];
    setFile(file);
  },
  [file]
);

↑が実行され、選択した一番目のファイルがsetFileによってfileに格納されます。
それでfileをRailsに飛ばしておkって具合でいけばいいのですが、このままだとRailsがファイルを認識しません。
fileをファイル形式にコンバートしないといけないのです。
以下のコードを追加します。

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
formData.append("Railsのテーブル名[カラム名]", ファイルが格納された変数);

にします。
テーブル名はs抜きなので注意してください。
次にaxiosでRailsにデータを飛ばします。

yarn add axios

axiosをインストール。
以下のコードを追加してください。

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>
  );
}

コードサンドボックス:サンプル
コードを追加したら↑と同じ感じになっていると思います。
ボタンを押したらpostFileが発火して、axiosが動くという具合になっています。

typescript
const data = createFormData();

ここでコンバートされたファイルをdataに格納して

typescript
.post(`http://localhost:3000/post_file/`, data)

こういう感じでrailsにデータを送信します。
本番の場合はもちろんhttp://localhost:3000の部分が変わります。
ご自身の本番のURLを入れてください。
細かいaxiosの使い方は他記事を参考にしてください。

Railsでデータを受け取る

デフォルトでRailsは別のポートからpostリクエストを受け付けていません。
なので、rack-corsを使って許可します。

gemfile
+ gem 'rack-cors'

gemfileに↑を追加してbundle installを実行してください。
次にconfig/initializers配下にcors.rbを作成します。
設定ファイルの中身は↓みたいな感じ。

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にドメインを入れます。
次にcarrierwavefog-awsとをインストールします。

gemfile
  gem 'rack-cors'
+ gem 'carrierwave'
+ gem 'fog-aws'

carrierwaveはアップロード、ダウンロードなどをよしなににやってくれる、便利なやつです。
bundle exec rails g uploader Fileを実行してapp/uploaders/file_uploader.rbを作成。

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
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を作成します。

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チュートリアル
をご覧ください。
マイグレーションでUserテーブルとFileカラムを作ります。

rails g migration User
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
app/models/user.rb
class User < ApplicationRecord
  mount_uploader :file, FileUploader
end

これでfile_uploader.rbに紐づきます。
次にルートを設定します。

config/routes.rb
Rails.application.routes.draw do
  post '/post_file' => 'users#post_file'
end

コントローラーを作成します。

rails g controller user
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_controllerpost_fileのアクションを呼び出し、paramsに入ったfileをfileカラムに保存し、Userを作成します。
fileカラムはCarrierWaveに紐づいているのでファイル形式に再度コンバートされ、carrierwave.rbの設定に応じて保存先に保存されるという感じになります。
なんか不備あったらコメント欄で教えてください。
以上へっぽこでした。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1