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 を設定します。
class User < ApplicationRecord
mount_uploader :user_image, UserImageUploader
end
コントローラーを編集
コントローラーを以下のように編集します。
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 モデルを以下のように編集します。
class User < ApplicationRecord
has_one_attached :avatar
validates :username, presence: true
end
コントローラーを作成します。
# bin/rails g controller api/users create show
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
を以下のように書きます。
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 でデータベースを見てみました。
リクエストで投げた画像ファイルが active_storage_blobs
に格納されています。
active_storage_blobs の カラム key
と同じ名前のファイルが /storage/ap/8q/
以下に格納されています。ファイルの実態はここにあります。
/storage/ap/8q/
以下に格納するのはデフォルトの仕様で、「どこにファイルを格納するか?」は 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"
]
}%
コントローラに以下のメソッドを追加して、アップロードしたイメージをダウンロードできるようにしてみます。
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 を書きます。
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