画像ファイルをアップロードして自由に表示したい!その4

複数の画像ファイルを扱えるようにしてみた。ファイルを保存する時に、id番号のフォルダを作成して、その中にファイルを保存する。ファイルやフォルダに関する処理はモデルで処理することにした。
ファイルは以下のように保存される。

モデル

  • ファイルを保存する時は、id番号のディレクトリ/ファイル名のように階層管理することにした。
  • destroy_filesは、ファイルをフォルダ後と削除するメソッド。
  • delete_file(name)は、ファイルを一つだけnameで指定して、削除するメソッド。
  • file_entriesは、id番号のフォルダに含まれるファイル名を配列で返すメソッド。
  • file_stat(name)は、ファイル情報を保持したFile::Statオブジェクトを返すメソッド。edit.rhtmlでファイル情報を表示する時に利用する。
  • rescue nilは、エラーを無視するために利用している。
# ------ app/models/software.rb ------
class Software < ActiveRecord::Base
  after_save :save_file
  after_destroy :destroy_files
  
  #validates_presence_of :file_name
  def validate
    errors.add(:file, "アップロードするファイルを指定してください。") if @file.nil? || @file.size == 0
    # content_typeが、image/ で始まっていなければ、エラーにする。
    errors.add(:file, "画像ファイルではありません。") unless @file.nil? || /^image\// =~ @file.content_type
  end
  
  # Tempfileオブジェクトをインスタンス変数に保存する。
  # new()や、update_attributes()の時、呼び出される。
  def temp_file=(file)
    return if file.nil? || file.size == 0
    @file = file
    #@old_file_path = file_path
    #self.file_name = file.original_filename
    #self.file_type = file.content_type
    #self.file_data = file.read
  end
  
  # ファイルを保存する。
  # saveメソッドの後、呼び出される。
  def save_file
    #File.delete(@old_file_path) rescue new_directory
    new_directory
    File.open(file_path + @file.original_filename, "wb") do |f|
      f.write(@file.read) 
    end
  end
  
  # idディレクトリと、その中身を全て削除する。
  # destroyメソッドの後に呼び出される。
  # 削除されたレコードのid、file_name等のフィールドにアクセス可能。
  # 削除するファイルやディレクトリが存在しないとエラーになるので、rescue nilでエラーを無視する。
  def destroy_files
    file_entries.each do |f|
      delete_file(f)
    end
    Dir.delete(file_path) rescue nil
  end
  
  # 引数nameのファイルを削除する。
  def delete_file(name)
    File.delete(file_path + name) rescue nil
  end

  # idに紐付くファイルを全て取得する。
  # 「.」と「..」もファイルに含まれてしまうため、正規表現で「.」で始まるファイル名を除外する。
  def file_entries
    Dir.entries(file_path).delete_if {|f| /^\./ =~ f}
  end
  
  # ファイル情報を保持したオブジェクトを返す。
  def file_stat(name)
    File.stat(file_path + name)
  end
  
  # RAILS_ROOTからのディレクトリを取得する。
  # "#{RAILS_ROOT}/piblic"としなくてもOKのようだ。
  def file_path
    "public" + image_source_path
  end
  
  # image_tagがファイルを参照するルールに合わせたパスを返す。
  # デフォルトは、public/images/から参照する。
  #   例)"files/1_test.png"  >> "public/images/files/1_test.png"
  # もし先頭が/で始まれば、pblic/から参照する。
  #   例)"/files/1_test.png" >> "public/files/1_test.png"
  # 新規作成の時、idがnilなので、rescue nilでエラーを無視する。
  def image_source_path
    "/files/#{id}/" rescue nil
  end
  
  # idディレクトリを新規作成する。
  # ディレクトリが既に存在する時、エラーが発生するので、rescue nilでエラーを無視する。
  def new_directory
    Dir.mkdir(file_path) rescue nil
  end
end

コントローラー

  • ファイルを一つだけ指定して、削除するメソッド delete_file を追加した。
# ------ app/controllers/softwares_controller.rb ------
class SoftwaresController < ApplicationController
...(途中省略)...
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
  verify :method => :post, :only => [ :destroy, :create, :update, :delete_file ],
         :redirect_to => { :action => :list }
...(途中省略)...
  def delete_file
    @software = Software.find(params[:id])
    @software.delete_file(params[:file])
    redirect_to :action => 'edit', :id => @software
  end

ビュー

  • @software.file_entries.each do |f| で、id番号のフォルダ中のファイルを全て取得して、表示している。
  • @software.file_stat(f).size で、ファイルサイズを取得できる。他にもいろいろな情報を取得できる。
  • link_to "#{f}を削除する"... で、ファイルを一つだけ削除するリンクを設定している。パラメーターにファイル名をセットしてdelete_fileアクションを実行している。
<%#------ app/views/softwares/edit.rhtml ------%>
<h1>Editing software</h1>

<%= start_form_tag({:action => 'update', :id => @software}, :multipart => true) %>
  <% @software.file_entries.each do |f| %>
    <p>
      ファイル名   :<%= f %><br />
      サイズ     :<%= @software.file_stat(f).size %><br />
      最終アクセス日時:<%= @software.file_stat(f).mtime %><br />
      タイプ     :<%= @software.file_stat(f).ftype %><br />
      <b><%= link_to "#{f}を削除する", 
                     {:action => 'delete_file', :id => @software, :file => "#{f}"}, 
                     :confirm => 'Are you sure?', :post => true %></b><br />
      <%= image_tag "#{@software.image_source_path}#{f}", :alt => f %><hr />
    </p>
  <% end %>

  <%= render :partial => 'form' %>
  <%= submit_tag 'Edit' %>
<%= end_form_tag %>

<%#= link_to 'Show', :action => 'show', :id => @software %>
<%= link_to 'Back', :action => 'list' %>
  • リスト表示についても、小さな画像を複数表示するようにしてみた。
<%#------ app/views/softwares/list.rhtml ------%>
...(途中省略)...
<% for software in @softwares %>
  <tr>
  <% for column in Software.content_columns %>
    <td>
    <% if column.name == 'file_data' %>
      <% software.file_entries.each do |f| %>
        <%= image_tag "#{software.image_source_path}#{f}", :alt => f, :size => '32x32' %>
        <%= f %>
        <br />
      <% end %>
    <% else %>
      <%=h software.send(column.name) %>
    <% end %>
    </td>
  <% end %>
    <td><%#= link_to 'Show', :action => 'show', :id => software %></td>
    <td><%= link_to 'Edit', { :action => 'edit', :id => software }, :post => true %></td>
    <td><%= link_to 'Destroy', { :action => 'destroy', :id => software }, :confirm => 'Are you sure?', :post => true %></td>
  </tr>
<% end %>
...(途中省略)...
  • ファイルをフォルダで階層管理しようとすると、処理がちょっと複雑になる。画像管理用のテーブルを作成して、1レコード1ファイルに対応させてしまった方が簡単かもしれない。