1
4

More than 3 years have passed since last update.

API モードの Rails でファイルをアップロード & ダウンロードするサンプル

Posted at

API モードの Rails でファイルをアップロードするためのサンプルです。
バージョンは以下のとおりです。

  • Ruby 3.0.1
  • Rails 6.1.3

Carrierwave を使うパターンと、 Active Storage を使うパターンを紹介します。

Carrierwave をインストール

Carrierwave はファイルのアップロード機能を簡単に追加できるようにするための gem です。

Gemfile に gem 'carrierwave' を追記して、 bundle install を実行します。

User モデルを作成

# rails g scaffold User name:string email:string
# rails db:migrate

ファイルアップロード用のモデルを作成する

# rails g uploader user_image

app/uploaders/user_image_uploader.rbが作られます。

ファイル名を格納するカラムを追加

# rails g migration add_filename_to_users user_image:string
# rails db:migrate

User モデルに uploader を設定します。

app/models/user.rb
class User < ApplicationRecord
  mount_uploader :user_image, UserImageUploader
end

コントローラーを編集

コントローラーを以下のように編集します。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: %i[ show update destroy ]

  # GET /users
  # GET /users.json
  def index
    @users = User.all
  end

  # GET /users/1
  # GET /users/1.json
  def show
  end

  # POST /users
  # POST /users.json
  def create
    @user = User.new(user_params)

    if @user.save
      render json: {msg: 'The image is successfully uploaded.'}
    else
      render json: {user: @user.errors}, status: unprocessable_entity
    end
  end

  # PATCH/PUT /users/1
  # PATCH/PUT /users/1.json
  def update
    if @user.update(user_params)
      render :show, status: :ok, location: @user
    else
      render json: @user.errors, status: :unprocessable_entity
    end
  end

  # DELETE /users/1
  # DELETE /users/1.json
  def destroy
    @user.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def user_params
      params.permit(:name, :email, :user_image)
    end
end

リクエストを送ってみる

curl -X POST \
 http://localhost:3000/users \
 -F email=testing@example.com \
 -F name=test \
 -F user_image=@/Users/name/Documents/work/manjiro.jpg

1行にすると以下です。

curl -X POST http://localhost:3000/users -F email=testing@example.com  -F name=test -F user_image=@/Users/name/Documents/work/manjiro.jpg

curl を実行すると、サーバー上のpublic/uploads/user/user_image 以下にリクエストを投げたファイルがアップロードされます。

Active Storage によるファイルアップロード

Active Storage は Rails 5.2 から提供された公式のファイルアップロード機能です。

Active Storage を使ったファイルアップロード機能について検証していきます。

以下のコマンドで Rails に Active Storage をインストールします。

# bin/rails active_storage:install
# bin/rails db:migrate

次に scaffold を利用して image という Active Storage 用の属性を持つ person というモデルを作成します。

# bin/rails g model user username:string avatar:attachment
# bin/rails db:migrate

生成された person モデルを以下のように編集します。

app/models/person.rb
class User < ApplicationRecord
  has_one_attached :avatar
  validates :username, presence: true
end

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

# bin/rails g controller api/users create show

config/routes.rbを以下のように書き換えます。

config/routes.rb
Rails.application.routes.draw do
  namespace :api, defaults: { format: :json } do
    resources :users, only: %i[create show] do
      get :avatar, on: :member
    end
  end
end

app/controllers/api/people_controller.rbを以下のように書きます。

app/controllers/api/users_controller.rb
class Api::UsersController < ApplicationController
  def create
    user = User.new(create_params)

    if user.save
      render json: success_json(user), status: :created
    else
      render json:error_json(user), status: :unprocessable_entity
    end
  end

  def show
    user = User.find_by(id: params[:id])

    if user.present?
      render json: success_json(user), status: :ok
    else
      head :not_found
    end
  end

  private

  def create_params
    params.require(:user).permit(:username, :avatar)
  end

  def success_json(user)
    {
      user: {
        id: user.id,
        username: user.username
      }
    }
  end

  def error_json(user)
    { errors: user.errors.full_messages }
  end
end

curl でリクエストを投げてみます。

$ curl --include --request POST http://localhost:3000/api/users --form "user[username]=Sano Manjiro" --form "user[avatar]=@/Users/fj/Documents/work/manjiro.jpg"

HTTP/1.1 100 Continue

HTTP/1.1 201 Created
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
ETag: W/"d564049a8614c4dc929aa52b97ea8326"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 6819fa9e-334e-49b2-a91c-6157bb3427e2
X-Runtime: 1.534877
Content-Length: 63

{
  "user": {
    "id": 1,
    "username": "Sano Manjiro"
  }
}%

Sequel Ace でデータベースを見てみました。

スクリーンショット 2021-05-23 23.07.49.png

リクエストで投げた画像ファイルが active_storage_blobs に格納されています。

active_storage_blobs の カラム key と同じ名前のファイルが /storage/ap/8q/以下に格納されています。ファイルの実態はここにあります。

/storage/ap/8q/以下に格納するのはデフォルトの仕様で、「どこにファイルを格納するか?」は config/storage.ymlで設定します。

デフォルトでは以下のようになっています。

config/storage.yml
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

パラメーターが足りないリクエストを投げたときのエラーを試してみましょう。

$ curl --include --request POST http://localhost:3000/api/users --form "user[avatar]=@/Users/fj/Documents/work/manjiro.jpg"
HTTP/1.1 100 Continue

HTTP/1.1 422 Unprocessable Entity
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
Cache-Control: no-cache
X-Request-Id: 77dd2cb4-dd7d-4bdd-8875-6460e31d052b
X-Runtime: 0.023324
Content-Length: 51

{
  "errors": [
    "Username can't be blank"
  ]
}%

コントローラに以下のメソッドを追加して、アップロードしたイメージをダウンロードできるようにしてみます。

controller/users_controller.rb
  def avatar
    user = User.find_by(id: params[:id])

    if user&.avatar&.attached?
      redirect_to rails_blob_url(user.avatar)
    else
      head :not_found
    end
  end

user が avatar を持っているか確認してみます。

$ curl --head http://localhost:3000/api/users/1/avatar
HTTP/1.1 302 Found

実際に avatar をダウンロードしてみます。

$ curl --location http://localhost:3000/api/users/1/avatar > downloaded_avatar.png

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   258    0   258    0     0   4031      0 --:--:-- --:--:-- --:--:--  4031
100   584    0   584    0     0   3089      0 --:--:-- --:--:-- --:--:--  3089
100 10948  100 10948    0     0  46786      0 --:--:-- --:--:-- --:--:-- 46786

RSpec でテストを書く

# rails g rspec:install
# bin/rails g rspec:request api/user
Running via Spring preloader in process 10803
      create  spec/requests/api/users_spec.rb
# mkdir -p spec/fixtures/files

## テスト用のイメージファイルを格納する 
# touch spec/fixtures/files/avatar.jpg

RSpec を書きます。

spec/requests/api/users_spec.rb
require 'rails_helper'

RSpec.describe "Api::Users", type: :request do
  describe 'POST /api/users' do
    subject { post '/api/users', params: params }

    let(:params) {{ user: { username: username, avatar: avatar }}}
    let(:username) {'Hanagaki Takemichi'}
    let(:avatar) { fixture_file_upload('avatar.jpg') }

    context 'valid request' do
      it 'returns status created' do
        subject
        expect(response).to have_http_status :created
      end

      it 'returns a JSON response' do
        subject
        expect(JSON.parse(response.body)).to eql(
         'user' => {
           'id' => User.last.id,
           'username' => 'Hanagaki Takemichi'
         })
      end

      it 'creates a user' do
        expect { subject }.to change { User.count }.from(0).to(1)
      end

      it 'creates a blog' do
        expect { subject }.to change { ActiveStorage::Blob.count }.from(0).to(1)
      end
    end

    context 'missing username' do
      let(:username) { nil }

      it 'returns status unprocessable entity' do
        subject
        expect(response).to have_http_status :unprocessable_entity
      end

      it 'returns a JSON response' do
        subject
        expect(JSON.parse(response.body)).to eql(
         'errors' => ['Username can\'t be blank']
        )
      end

      it 'does not create a user' do
        expect { subject }.not_to change { User.count }.from(0)
      end

      it 'does not create a blob' do
        expect { subject }.not_to change { ActiveStorage::Blob.count }.from(0)
      end
    end
  end
end

テスト用のデータベースを作成して、 rspec を走らせます。

# RAILS_ENV=test bundle exec rake db:migrate
# bundle exec rspec
1
4
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
1
4