概要
- Railsで画像ファイルをアップロードする処理を整理する
- ユーザーのプロフィール画面のアップロードと更新を例にする
- 更新時、前の画像は削除する
- 画像ファイル名の衝突をさけるため、ランダム文字列を生成してファイル名にする
フォームからのアップロード情報を受け取る
ビュー
以下のビューが表示されるとファイル選択要素が表示される。
file_field
に対するキーは:upload_file
と設定しているが、これはモデルに存在しないキーでもよい。
ここでは登録済みユーザーのプロフィール画像を編集する想定で、PATCHメソッドでupdate
コントローラーに送信される。
1 2 3 4 |
<%= form_with(model: @user, url: update_user_path(user_in_session), local: true) do |f| %> <%= f.file_field(:upload_file) %> <button>この内容で設定</button> <% end %> |
コントローラー
フォームから送られたアップロードファイルに関するデータはparamsに格納されていて、以下で取得できる。
params[:モデル][:file_fieldで設定したキー]
ここではアップロードファイルが指定されているときにupload_file
にアップロードファイルの内容を保存し、その中のoriginal_filename
の値をupload_file_name
に保存している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class UsersController < ApplicationController ..... def update # セッション中のユーザーに対する処理 @user = User.find(user_in_session.id) # アップロードファイル <- file_field upload_file = params[:user][:upload_file] if upload_file.present? # アップロードファイルの元の名前 upload_file_name = upload_file.original_filename end end ..... end |
UploadedFileクラス
file_field
からparams
に保存されるのはUploadedFile
クラスのインスタンス。
たとえば上のupload_file
の内容は以下の様になっていて、original_filename
やcontent_type
がインスタンス変数として参照できる。
1 2 3 4 5 |
#<ActionDispatch::Http::UploadedFile:0x0000000005a9f1c8 @tempfile=#<Tempfile:/tmp/RackMultipart20210329-327-1fatvrf.jpg>, @original_filename="image_car_on_beach.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"user[upload_file]\"; filename=\"image_car_on_beach.jpg\"\r\nContent-Type: image/jpeg\r\n"> |
また、画像ファイルの実体はread
メソッドで読みだすことができて、これをファイルの保存時に使う(read
メソッドは1回しか呼び出せないらしい)。
permitの設定は不要
フォームから得られるUploadedFileオブジェクトをparamsで取得しているが、これに対してrequire.permit
に含める必要はない。
Railsのモデルの仕組みによって自動的に入力・保存するものではないため。
ファイルの書き込み
基本手順
- アプリケーションの中のファイル保存ディレクトリーを取得
- 上記とファイル名を組み合わせたフルパスを取得
- 画像ファイルの書き込み
コントローラー
コントローラーのupdate
アクションのみ示す。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def update # セッション中のユーザーに対する処理 @user = User.find(user_in_session.id) # アップロードファイル <- file_field upload_file = params[:user][:upload_file] if upload_file.present? # アップロードファイルの元の名前 upload_file_name = upload_file.original_filename # プロフィール画像を保存するディレクトリー upload_dir = Rails.root.join("public", "user_images") # アップロードするファイルのフルパス upload_file_path = upload_dir + upload_file_name # 前のファイルを削除するためのパス old_file_path = upload_dir + @user.image_file_name # アップロードファイルの書き込み File.binwrite(upload_file_path, upload_file.read) end end |
流れは以下のとおり。
まず事前にプロジェクトディレクトリー下のpublicに必要に応じてサブディレクトリーをつくっておく
/home/.../ex_bbs/public/user_images
Rails.root
はプロジェクトディレクトリーのフルパスを保持したPath
オブジェクト
1 |
#<Pathname:/home/.../ex_bbs> |
Rails.root.join()
でパスにディレクトリーをつなげていく
upload_dir = Rails.root.join("public", "user_images")
1 |
#<Pathname:/home/,,,/ex_bbs/public/user_images> |
upload_dir
にファイル名をつなげてファイルのフルパスにする
upload_file_path = upload_dir + upload_file_name
1 |
#<Pathname:/home/,,,/ex_bbs/public/user_images/image_car_on_beach.jpg> |
ファイルのフルパスとファイルの実体を指定して、画像ファイルを書き込む。
File.binwrite(upload_file_path, upload_file.read)
データベースへの登録
基本手順
- フォームから得られたparamsにヘルパーでパーミッションを適用
- その結果にアップロードファイルのファイル名をマージして登録用のパラメーターとする
- 登録用パラメーターでデータベースに登録
コントローラー
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def update # セッション中のユーザーに対する処理 @user = User.find(user_in_session.id) # アップロードファイル <- file_field upload_file = params[:user][:upload_file] if upload_file.present? ..... # フォームから得られたparamsにファイル名の項目を追加 new_user_params = user_params.merge({image_file_name: upload_file_name}) end if @user.update(new_user_params) redirect_to profile_path(user_in_session) else flash.now[:danger] = "設定更新できませんでした" <span class="crayon-i">render</span> <span class="crayon-i">action</span><span class="crayon-sy">:</span> <span class="crayon-sy">:</span><span class="crayon-i">edit</span> <span class="crayon-st">and</span> <span class="crayon-st">return</span> end end |
パラメーターヘルパー
コントローラーで多用されるヘルパーは以下のような形。
1 2 3 4 5 |
# フォーム入力のパラメーターを取得するメソッド private def user_params params.require(:user).permit(:name, :email, :password, :comment) end |
フォームから得られたパラメーター(user_params
の戻り値)は以下のような内容。画像選択以外の要素がないのでパラメーターは空になっている。
1 2 |
Unpermitted parameter: :upload_file <ActionController::Parameters {} permitted: true> |
パラメーターの調整
フォームで得られた:upload_file
からデータベースに登録するファイル名を取り出し、パラメーターにマージする。
new_user_params = user_params.merge({image_file_name: upload_file_name})
マージ後のnew_user_paramsの内容は以下のとおりで、空だった内容に:image_file_name
が追加されている。
1 |
<ActionController::Parameters {"image_file_name"=>"image_car_on_beach.jpg"} permitted: true> |
データベースの更新
ユーザーモデルのupdate
に調整したパラメーターを与えてデータベースを更新する。
@user.update(new_user_params)
ファイルの削除
データ更新の際に、現在登録されているファイルを削除する場合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
if upload_file.present? # 画像ファイルのファイル名をランダム文字列に upload_file_name = upload_file.original_filename # プロフィール画像を保損するディレクトリー upload_dir = Rails.root.join("public", "user_images") # アップロードするファイルのフルパス upload_file_path = upload_dir + upload_file_name # プロフィール画像がある場合は削除処理に入る if @user.image_file_name.present? # 今のファイルを削除するためのパス old_file_path = upload_dir + @user.image_file_name # 今のファイルを削除 File.delete(old_file_path) end # アップロードファイルの書き込み File.binwrite(upload_file_path, upload_file.read) # フォームから得られたparamsにファイル名の項目を追加 new_user_params = user_params.merge({image_file_name: upload_file_name}) end |
ファイル名の重複防止~ランダム文字列
アップロードファイル名にoriginal_filename
を使うと、ユーザー側で指定したファイル名が衝突する可能性がある。その場合、あとから登録した画像で上書きされてしまう。
そこで、ファイル名にランダム文字列を使って、実質上衝突が起こらないようにする。
同じ機能は投稿記事のコントローラーでも利用し、アプリケーションに共通なので、application_helper.rb
に記述する。
app/helpers/application_helper.rb
1 2 3 4 5 6 |
module ApplicationHelper def random_string(n) chars = ("0".."9").to_a.join + ("A".."Z").to_a.join + ("a".."z").to_a.join (Array.new(n).map! {|e| chars[rand(chars.length)]}).join end end |
app/controllers/application_controller.rb
1 2 3 4 5 6 7 8 |
class ApplicationController < ActionController::Base protect_from_forgery with: :exception # 共通で使われるヘルパーのモジュールをインクルード include ApplicationHelper # ユーザーセッションの管理に関するメソッドモジュールをインクルード include UsersHelper end |
そして、アップロード画像のファイル名をランダム文字列とする。このファイル名はデータベースの更新と画像ファイルの保存に共通して使われる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def update # セッション中のユーザーに対する処理 @user = User.find(user_in_session.id) # アップロードファイル <- file_field upload_file = params[:user][:upload_file] if upload_file.present? # アップロードファイルの元の名前 upload_file_name = random_string(20) ..... end |
こうすることで、複数のユーザーが同じファイル名で画像をアップロードしても、ユーザーごとに異なる名前となるため、ファイル名の衝突がおきない。
まとめ
画像ファイルのアップロードのためのファイル群をまとめておく。ユーザーのプロフィール画像をアップロードするケースを例にする。
editアクション
- ユーザー操作などでルーティングされ、画像アップロードを含むページを表示する
- ビューで使うセッション中のユーザーのインスタンスをインスタンス変数に保存する
app/controllers/users_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class UsersController < ApplicationController before_action :authorize, except: [:sign_up, :sign_in, :sign_up_process, :sign_in_process] ..... def edit # ビューで表示させるユーザー @user = User.find(user_in_session.id) end ..... end |
ビュー
- 画像アップロードを含むページ
- ボタンを押すとPATCHメソッドで
update
アクションにデータが送られるようルーティングしている
app/views/users/edit.html.erb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<div class="user_settings"> <h1><%= @user.name %>さんのユーザー設定</h1> <div class="user_settings_form"> <%= form_with(model: @user, url: update_user_path(user_in_session), local: true) do |f| %> <h2>プロフィール画像</h2> <div class="profile_image"> <% if @user.image_file_name == nil %> <%= image_tag "https://dummyimage.com/200x200/0b0e4d/23f534.png&text=Profile+Image" %> <% else %> <%= image_tag "/user_images/#{@user.image_file_name}" %> <% end %> </div> <%= f.file_field(:upload_file, class: "input file") %> <h2>プロフィールコメント</h2> <%= f.text_area(:comment, class: "input comment", placeholder: "プロフィールコメント") %> <button>この内容で設定</button> <% end %> </div> </div> |
updateアクション
フォームからのデータを受け取り、以下の処理を行う。
- 画像の保存(古い画像の削除)
- データベースのファイル名の更新
app/controllers/users_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
class UsersController < ApplicationController before_action :authorize, except: [:sign_up, :sign_in, :sign_up_process, :sign_in_process] ..... def update # セッション中のユーザーに対する処理 @user = User.find(user_in_session.id) # アップロードファイル <- file_field upload_file = params[:user][:upload_file] if upload_file.present? # アップロードファイルの元の名前 upload_file_name = random_string(20) # プロフィール画像を保損するディレクトリー upload_dir = Rails.root.join("public", "user_images") # アップロードするファイルのフルパス upload_file_path = upload_dir + upload_file_name # プロフィール画像がある場合は削除処理に入る if @user.image_file_name.present? # 前のファイルを削除するためのパス old_file_path = upload_dir + @user.image_file_name # 今のファイルを削除 File.delete(old_file_path) end # アップロードファイルの書き込み File.binwrite(upload_file_path, upload_file.read) # フォームから得られたparamsにファイル名の項目を追加 new_user_params = user_params.merge({image_file_name: upload_file_name}) end # 登録成功ならプロフィールページへ if @user.update(new_user_params) redirect_to profile_path(user_in_session) and return # 登録失敗なら再度ユーザー設定ページへ else flash.now[:danger] = "設定更新できませんでした" render :edit_user, layout: 'application' and return end end # フォーム入力のパラメーターを取得するメソッド private def user_params params.require(:user).permit(:name, :email, :password, :comment) end end |
ランダム文字列ヘルパー
ファイル名の衝突を防ぐランダム文字列生成のヘルパー。
app/helpers/application_helper.rb
1 2 3 4 5 6 |
module ApplicationHelper def random_string(n) chars = ("0".."9").to_a.join + ("A".."Z").to_a.join + ("a".."z").to_a.join (Array.new(n).map! {|e| chars[rand(chars.length)]}).join end end |