Railsでファイルの複数のアップロードとテーブルの登録・更新を習得する
前回のcreateに次いでdelete_allも問題なく行えたので、今度はRailsでのファイルのアップロードについてできるようになっとこうと思いました。
単体でのアップロードだとソースのコピペになっちまって何の身にもならんので、複数のアップロードをね。
まぁ基本はPHPとほぼ同じだろうな~と思いながらやっていたけど、「なんでや」となったところもあったから習作とメモ。
行おうとしたことは以下となる。
- セッションユーザーごとにファイルをアップロードできるようにする
- その際は複数ファイルのアップロードを行う
- 保存先のディレクトリは「Railsアプリの場所/public/ユーザー/日付時刻」
- アップロードと同時に、テーブルの登録・更新処理を行う
- そのテーブルはユーザーとレコード番号の複合キーを持つ
- おまけでファイルの形式はjpg、png、gifの画像だけ
- 今回は1画面下でのファイル操作とそれに伴うテーブル操作の習得が主な目的なので、とりあえずはライブラリ的なものは作らずに入れ替え形式で行う
なんともまぁ習作的。
だけど、実際に使用するであろうコードを全てなめていかなければならないし、実運用については今回は完全に度外視。
若い頃にPHPをチュートリアルしてたときを思い出す。
あの頃は「ファイルのアップロードってなんぞや?俺はメールフォームを作るときの添付ファイルが付けたいんや!」とか、今思えば支離滅裂なことを言っていた。ファイルのアップロード無くして添付ファイルなんてつけられやしねーのになwww
まぁそれはいいとして、今回はアップロードされた画像を管理するためのテーブルが必要なので、先にそのモデルを作らなければいけない。イメージはこんな感じ。
user_id | file_id | file_name | file_date |
cattlemute | XX | hoge.jpg | 201710180000 |
user_idとfile_idが今回の主キー(複合キー)となる。
file_idとかしちゃってるけど要はレコード番号である。でも気にしない。
なので、まずはコマンドラインからrails generate modelで「upload」というモデルを作成し、以下のように設定。
class CreateUploads < ActiveRecord::Migration[5.1] def change create_table :files, :id => false, :force => true do |t| t.string :user_id, :null => false, :limit => 30 t.integer :file_id, :null => false, :limit => 2 t.string :file_name, :limit => 255 t.string :file_date, :limit => 14 end add_index :uploads, [:user_id, :file_id], :unique => true # 複合キーを付ける end end
自分も含めてわかっているとは思うけど、念のため、class CreateFilesとかadd_index :filesとか複数形になってるけど実際にgenerate時に指定するモデル名は「file」の単数形なので注意。
標準のidを付けないCreateとしては多分こんなところでいいだろう。
注目するべきはadd_indexの第二引数。ここでuser_idとfile_idを複合キーとして設定している。
(これ全部プロの人から見たら当然なんだろうけど、腐れコーダーの習得用のメモなので許してちょ)
次に、アップロードするファイルを選択するためのビュー「upload.html.erb」を作成する。
<%= form_tag({:controller => 'users', :action => 'upload'}, :multipart => true) do # multipartは忘れずに入れる %> <table> <% for i in 0..@file_count_regix do # ゼロスタート %> <tr> <th>ファイル<%= i + 1 # ファイル1,2...と表記 %></th> <td> <%= file_field :file_data, i %> </td> </tr> <% end %> </table> <%= submit_tag 'アップロード' %> <% end %>
コントローラーは「users」、アクションは自分自身の「upload」。
よくあるパターンで@file_count_regixの回数分だけゼロ値からループ。この変数はコントローラー側で定義する。
で、今回の要となるコントローラーの記述。
def upload # ログインチェックいれとこうね @file_count = 3 # ファイルの数を指定(ビューで表示される数とテーブルのレコード登録・更新の数) @file_count_regix = @file_count - 1 # ゼロ値スタート用 if !params[:file_data].nil? then post_file = params[:file_data] receive_file = Array.new # paramsの写し先指定 accept_file = ['.jpg', '.gif', '.png'] # アップロードを許可するファイルの種類 file_path = '' file_path_base = 'public/images' # 書き出すための基本のディレクトリ file_path_user = session[:user] # 今回はユーザーセッションを指定しておく file_path_date = DateTime.now.strftime('%Y%m%d%H%M%S') # ディレクトリ兼テーブル側のfile_dateのカラム delete_file_path = '' file_path = file_path_base + '/' + file_path_user + '/' + file_path_date + '/' # 上記を結合 for i in 0..@file_count_regix do if !post_file[i.to_s].nil? then receive_file[i] = post_file[i.to_s] else receive_file[i] = '' # ループの帳尻合わせと、空送信部分の判定条件を得るためにダミーを入れる。 end end for i in 0..@file_count_regix do file_name = nil file_date = nil if receive_file[i] != '' then # 空送信ではないときのみ実行する file_name = receive_file[i].original_filename file_date = file_path_date if accept_file.include?(File.extname(file_name).downcase) then # ファイルの種類チェック、これはほぼリファレンス通り FileUtils.mkdir_p(file_path) # ディレクトリがないときのみ作るようにする File.open("#{file_path}#{receive_file[i].original_filename}", "wb") do |f| # Railsから発行されたファイルのバイナリを開く f.write(receive_file[i].read) # 一時ファイルより、アップロード公開先へ実ファイルを書き出す end else # 不許可のファイルは名前も日付もnull戻し file_name = nil file_date = nil end end record_check = Upload.select('user_id, file_id, file_date, file_name').find_by(:user_id => session[:user], :file_id => i) # 複合キーからレコードの存在チェック if !record_check.nil? && receive_file[i] != '' then # 新規にファイルがアップロードされた場合、前回アップロード時の当該ファイルを削除する delete_file_user = record_check.user_id.nil? ? '' : record_check.user_id delete_file_date = record_check.file_date.nil? ? '' : record_check.file_date delete_file_path = file_path_base + '/' + delete_file_user + '/' + delete_file_date + '/' delete_file_name = record_check.file_name begin # ファイルを手動で消したとかで削除しようとしたファイルがないときにエラーするので例外処理でエスケープしておく if !delete_file_name.nil? then File.delete delete_file_path + delete_file_name # ファイルを削除 end rescue # 例外処理の内容を記載 end end if record_check.nil? then # レコードがないときは登録と見なしてINSERTする Upload.create( :user_id => session[:user], :file_id => i, :file_name => file_name, # 上で不許可のファイルはnull戻ししてるので空となる :file_date => file_date # 同上 ) else if receive_file[i] != '' then # 空送信されてなければUPDATEする。 Upload.where( :user_id => session[:user], :file_id => i ).update_all( :file_name => file_name, :file_date => file_date ) end end end end end
大凡は#のコメント部分でざっくりと書いておいたが、上記のソースコードの中に書ききれなかった部分だけピックアップして課題事項。
- アップロード後にテーブルへデータのキックを行うが、空送信しただけでもテーブルのレコードそのものは登録される。
もしこの場合で登録を行わない場合は、if record_check.nil? thenのところに条件を追加すればいい・・・はず。
その際、update_allのとこの条件も変える必要アリ。 - 既に一度ファイルがアップロードされていて、その次に不許可ファイルがアップロードされた場合、file_nameとfile_dateは空に更新される。
この空の更新を行わない場合については、適宜条件を付け加える。 - ビューへのファイルの表示はuploadsテーブルをselectして「public/images/ユーザー(テーブルのuser_idを参照)/日付時刻(テーブルのfile_dateを参照)」で読み出せばいいだけなので今回は割愛した。
- 同時アップロード時の同名ファイルは上書きされる。
ソースコードとしてはこんなところ。
for→forと続いているところにちょっとだけ無駄を感じるが、このほうが見やすいのでとりあえずこれで良し。
クソ言語VBみたいにコード書く前から糞重いからって、処理速度重視で可読性をなくしてもただのオレオレコードになるし。
飽くまで上記全コードは習得用なので、もし、よくあるアップローダーのようなファイルのライブラリを作る場合は、モデルも含めてコードを掻い摘んで組み替える感じで行けるはず。
今回のファイルのアップロードに関するポイントの覚書きとまとめ。
# form_tagで:multipart => trueとなっているとファイル送信もできる post_file = params[【受け取るファイルデータ】] # テキストデータとファイルデータを同一フォームから別々に取る際の例 post_data = params[:text_data] post_file = params[:file_data]
paramsそのものにフォームのテキストデータとの明確な違いというものはない。
フォームにenctype=”multipart/form-data”の属性がついていれば、paramsでファイルのバイナリも送信内容に含まれて入ってくる。
もし、テキストデータとファイルを同一フォーム内で分けたい場合、フォーム側に2種類のvalueのパターンを作っておいてやればいいだろう。
FileUtils.mkdir_p(【作成するディレクトリ】)
FileUtilsクラスを使用して、mkdir_pメソッドにてディレクトリがなければ作成する。
Oracle9のPLSQLにも似たようなのあったな(うろ覚え)。
File.open(【ファイルのフルパス】, 【モード】) do |f| f.write(【ファイルオブジェクト】.read) end
一般的なプログラミング言語と同じ感じ。
File.openで所定のディレクトリに書き込み先ファイルを作成して、writeで書き込む。
【ファイルオブジェクト】.readってのはPHPみたく送信されたファイルのバイナリなんかを読んでるんだろう。
File.delete 【ファイルのフルパス】
読んで名の通り、ファイルの削除。
ファイルがなかった場合、エラーを返してくるので注意。
begin # 検証コード rescue # 例外発生時 end
例外処理を行う。書いたコードの中では、ファイルの削除時のファイルが既に削除されているエラーの対策に、エスケープ的な意味合いで使用している。rescue~endは中身空でも改行は必須。
ネストが過ぎるとあれだけど明確に任意のエラーが発生するところでは使っても大丈夫なはず。
(まさにJavaScriptでレガシーブラウザ対応の際のイベントリスナーをtry{}catch(e){}で分ける的な使い方だな、これ。)
DateTime.now.strftime(【フォーマット】)
現在の日付・時刻をフォーマットに準じて取得。もはやお馴染み。
record_check = 【モデル】.select(【selectの要件】).find_by(【where条件】)
SQLの基本、セレクト文。いや、それが言いたいんじゃない。
セレクトした時に条件に合致するものがない場合、上記の「record_check」には「nil」が入ってくる。
テーブルのレコードの存在チェックに便利。
で、ここからはソースコードを書いてたときに「なんでや?」となった部分。こぼれ話。
実は、このparams[:file_data]は、アップロードのコードを作成しようと手を付けた当初、countメソッドでハッシュの数を取ろうとしたんだが、NoMethodErrorが出たんだよね。
undefined method `count’ for #<ActionController::Parameters:変数アドレス>とか。
普通のハッシュでやったときは動いてたから「あれ?」となった。
paramsがメソッドそのものだから、ハッシュのように見えてハッシュじゃないのな。だからcountメソッドがそもそも使えないんだろう、とか勝手に思ってる。
そして、このparamsの落とし穴だけど、フォームで送信した中身のある要素だけしか取れてこないんだな、これ。空の要素の場合はその要素はなかったことになるようだ。
PHPの$_POSTだと枠さえ用意されていれば要素の中身が無くても空文字で飛んでくるんだけどな。
この辺はRuby on Railsでコーディングするときに注意しとくべきだねー。