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

アイコンや操作画面のスクリーンショットなど、jpegpngなどの画像ファイルをアップロードして、表示してみたくなった。画像ファイルはサイズが巨大になりがちなので、取り扱い方が、文字列や数値、日付とはちょっと異なる。いろいろな処理方法があるが、まずは、画像ファイルを直接データベースに登録する方法でやってみる。全体をシンプルに見渡したいので、新規プロジェクト名「file」を作成して以下のようにテストしてみた。

fileプロジェクトの新規作成

  • ターミナルで新規プロジェクトを作成(データベースにsqlite3を指定)
cd ~/railsapp
rails file -d sqlite3
  • 上記プロジェクトをRadRailsで読み取るために、RadRailsでも新規プロジェクト名「file」を作成した。
    • ただし、「Railsアプリケーションのスケルトンを作成する。」のチェックは外した。
  • RadRailsにfileプロジェクトが読み込まれたら、「ジェネレーター」タブでmodel softwareを実行。
  • そのあと、マイグレーションファイルは、以下のように設定。
#------ db/migrate/001_create_softwares.rb ------
class CreateSoftwares < ActiveRecord::Migration
  def self.up
    create_table :softwares do |t|
      t.column :file_name, :string
      t.column :file_type, :string      
      t.column :file_data, :binary
    end
  end

  def self.down
    drop_table :softwares
  end
end
  • 「Rakeタスク」タブでdb:migrateを実行。
  • 「ジェネレーター」タブでscaffold softwareを実行。

以上が、scaffoldまでのお決まりの手順。

データベースに画像ファイルを登録

ビュー
  • file_fieldで、アップロードするファイルを指定する入力フォームを作る。
  • ファイルをアップロードするには、start_form_tagで:multipart => trueの設定が必要。
    • いつもはstart_form_tagの()を省略できるが、ここでは省略するとエラーになる。
  • file_fieldで送信されるものは、オブジェクトのようだ。
    • クラス名はTempfileになっていた。*1
    • params[:software][:temp_file]として送信される。
<%#------ app/views/softwares/new.rhtml ------%>
<h1>New software</h1>

<%= start_form_tag({:action => 'create'}, :multipart => true) %>
  <p>
    <label for="software_temp_file">File</label><br/>
    <%= file_field 'software', 'temp_file'  %>
  </p>
  <%= submit_tag "Create" %>
<%= end_form_tag %>

<%= link_to 'Back', :action => 'list' %>
コントローラー
  • コントローラーにconvert_temp_fileメソッドを定義して、パラメータとして受け取ったTempfileオブジェクトを使い易い状態に変換する。
    • データベースにはfile_name、file_type、file_dataフィールドしかない。
    • file_filedからは、params[software][temp_file]としてオブジェクトを受け取る。
    • フィールド名に合わせたパラメータに変換した後は、params[software][temp_file]を削除しておかないとエラーになる。(存在しないフィールドに書き込めないため)
    • Tempfileオブジェクトのままデータベースに保存して、表示する時に必要なデータを取り出すという考えもあるが、Tempfileオブジェクトをそのままデータベースに保存することは出来なかった...。*2
  • 変換したら、いつも通りにデータベースに書き込む。
#------ app/controller/softwares_controller.rb ------
class SoftwaresController < ApplicationController
...(途中省略)...
  def new
    @software = Software.new
  end

  def create
    convert_temp_file
    @software = Software.new(params[:software])
    if @software.save
      flash[:notice] = 'Software was successfully created.'
      redirect_to :action => 'list'
    else
      render :action => 'new'
    end
  end

private
  def convert_temp_file
    temp_file = params[:software][:temp_file]
    params[:software][:file_name] = temp_file.original_filename
    params[:software][:file_type] = temp_file.content_type
    params[:software][:file_data] = temp_file.read
    params[:software].delete(:temp_file)
  end

...(途中省略)...
  • Tempfileオブジェクトから情報を取り出すメソッド
original_filename file_fieldで指定したファイル名を返す。 例:stripe 1.png
content_type ファイルの種類を返す。 例:image/png
read ファイルのデータを返す。 画像ファイルのデータそのもの
size ファイルのサイズを返す。 例:83264

以上で、画像ファイルのアップロードが可能になった。

データベースに保存されている画像ファイルを表示

コントローラー
  • コントローラーに画像表示用のメソッドputs_imageを追加した。
  • send_dataがデータベースの中身をファイルとして送信してくれる。
#------ app/controller/softwares_controller.rb ------
class SoftwaresController < ApplicationController
...(途中省略)...
public
  def puts_image
    @software = Software.find(params[:id].to_i)
    send_data @software.file_data, 
              :filename    => @software.file_name, 
              :type        => @software.file_type, 
              :disposition => "inline"
  end
ビュー
  • 画像を表示するにはimage_tagを利用する。
    • 通常、画像ファイルまでのファイルディレクトリパスや、urlを指定する。
    • データベースの中身を表示したいときは、url_for()を使って、send_dataを処理するコントローラーのアクションとidを指定する。
  • ちょっとした問題が発生。image_tag url_for(:action => 'puts_image', :id => '1') のように指定すると...
    • <img src="softwares/puts_image/1.png?">と変換され、idの後ろに .png? が余分に付加されてしまう。
    • そこで、コントローラーでは、Software.find(params[:id].to_i) のようにto_iで整数に変換しておく。(上記コントローラー参照)
    • これでidが 1.png? となっていても、to_iによって1に変換され、正しいidが取り出せる。
<%#------ app/views/softwares/show.rhtml ------%>
<% for column in Software.content_columns %>
<p>
  <b><%= column.human_name %>:</b>
  <% if column.name == 'file_data' %> 
    <%= image_tag url_for(:action => 'puts_image', :id => @software), 
                  :alt => @software.file_name %>
  <% else %>
    <%=h @software.send(column.name) %>
  <% end %>
  
</p>
<% end %>
<p>
  <b>File size:</b> 
  <%= @software.file_data.size %> byte
</p>

<%= link_to 'Edit', :action => 'edit', :id => @software %> |
<%= link_to 'Back', :action => 'list' %>

以上で、画像ファイルを自由に表示できるようになった。

ついでにファイルとしてダウンロードする場合

コントローラー
  • send_dataのオプション:disposition => "inlineを指定しなければ、ファイルとしてダウンロードされる。
  def download_file
    @software = Software.find(params[:id])
    send_data @software.file_data, 
              :filename    => @software.file_name, 
              :type        => @software.file_type
  end
ビュー
  • link_toで上記コントローラーのアクションを指定したリンクを作っておけば、クリックすればダウンロードが始まる。
<%= link_to 'Download', :action => 'download_file', :id => @software %>

これで、ファイルのダウンロードにも対応できる。

参考

以下のページがとても参考になりました。感謝です。
RoR Wiki 翻訳 Wiki - HowtoUploadFiles

*1:Rubyリファレンスマニュアル - cgi

*2:試してないが、オブジェクトをバイト列に変換すれば可能かもしれない。7.24 Marshalの使い方を教えてください