概要
- 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 |