attachment-fu/RMagick で画像アップロードサービス

(追記、その2も見よ)

Rails(PostgreSQL)で、画像アップロードサービスを作ってみる。
添付ファイルの取り扱いには attachment-fuプラグインを使い、
更に画像のバリデーションやサムネイル作成には RMagckを用いる。

Railsアプリケーション作成

[207] rails -d postgresql photo 

新しく出来た photoディレクトリにて svn登録

[249] svn import -m 'start rails/photo' . svn+ssh://localhost/home/hs9587/svnrepos/photo/trunk
 <略>
[249] ..
[250] mv photo .photo
[251] svn co svn+ssh://localhost/home/hs9587/svnrepos/photo/trunk photo
 <略>
[252] cd photo
[255] svn up
リビジョン 1 です。

削除と無視

[256] svn remove log/*
D         log/development.log
D         log/production.log
D         log/server.log
D         log/test.log
[257] svn ci -m 'rm logfile'
削除しています              log/development.log
削除しています              log/production.log
削除しています              log/server.log
削除しています              log/test.log

リビジョン 2 をコミットしました。
[258] svn up
リビジョン 2 です。
[259] svn propset svn:ignore "*.log" log/
属性 'svn:ignore' を 'log' にセットしました
[260] svn ci -m 'ignore log/*.log'
送信しています              log

リビジョン 3 をコミットしました。
[261] svn up
リビジョン 3 です。
[262] svn move config/database.yml config/database.yml.org
A         config/database.yml.org
D         config/database.yml
[263] svn ci -m 'move datgabase.yml'
削除しています              config/database.yml
追加しています              config/database.yml.org

リビジョン 4 をコミットしました。
[264] svn up
リビジョン 4 です。
[265] svn propset svn:ignore "database.yml" config
属性 'svn:ignore' を 'config' にセットしました
[266] svn ci -m 'ignore datgabase.yml'
送信しています              config

リビジョン 5 をコミットしました。
[267] svn up
リビジョン 5 です。
[268] cp config/database.yml.org config/database.yml
[279] svn propset svn:ignore "*" tmp/cache
属性 'svn:ignore' を 'tmp/cache' にセットしました
[280] svn propset svn:ignore "*" tmp/sessions
属性 'svn:ignore' を 'tmp/sessions' にセットしました
[281] svn propset svn:ignore "*" tmp/sockets
属性 'svn:ignore' を 'tmp/sockets' にセットしました
[282] svn ci -m 'ignore tmp'
送信しています              tmp/cache
送信しています              tmp/sessions
送信しています              tmp/sockets

リビジョン 6 をコミットしました。
[283] svn up
リビジョン 6 です。

database.yml調整、「host: localhost」とユーザー、パスワード。
ユーザーは専用の「photo」ユーザを作る方向で。
で、「rake db:create」して、「ruby script/server」

About your application’s environment

Ruby version 1.8.7 (i486-linux) 
RubyGems version 1.2.0 
Rails version 2.1.1 
Active Record version 2.1.1 
Action Pack version 2.1.1 
Active Resource version 2.1.1 
Action Mailer version 2.1.1 
Active Support version 2.1.1 
Application root /home/hs9587/rails/photo 
Environment development 
Database adapter postgresql 
Database schema version 0 

プラグイン

RSpec(on Rails) と、所要の attachment-fu

ruby script/plugin install http://rspec.rubyforge.org/svn/trunk/rspec
ruby script/plugin install http://rspec.rubyforge.org/svn/trunk/rspec_on_rails
ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu

そして svn

[353] cp config/amazon_s3.yml config/amazon_s3.yml.org
[354] svn propset svn:ignore "amazon_s3.yml" config
属性 'svn:ignore' を 'config' にセットしました
[355] svn ci -m 'amazon_s3 from attachment-fu'
送信しています              config

リビジョン 7 をコミットしました。
[356] svn up
リビジョン 7 です。
[357] svn add vendor/plugins/*
 <略>
[358] svn ci -m 'plugins: rspec, rspec_on_rails, sttachment_fu'
 <略>
[360] svn up
リビジョン 8 です。
[367] svn st
?      tmp/attachment_fu
?      config/amazon_s3.yml.org
?      config/database.yml
[368] svn propset svn:ignore "attachment_fu" tmp/
属性 'svn:ignore' を 'tmp' にセットしました
[369] svn propset svn:ignore "database.yml" /config
svn: '/' は作業コピーではありません
svn: ファイル '/.svn/entries' を開けません: そのようなファイルやディレクトリはありません
[370] svn propset svn:ignore "database.yml" config/
属性 'svn:ignore' を 'config' にセットしました
[371] svn add config/amazon_s3.yml.org
A         config/amazon_s3.yml.org
[372] svn ci -m 'ignore files'
送信しています              config
追加しています              config/amazon_s3.yml.org
送信しています              tmp
ファイルのデータを送信中です.
リビジョン 9 をコミットしました。

ignore系ちょっと混乱してるかも。

[391] svn propset svn:ignore "*.yml" config/
属性 'svn:ignore' を 'config' にセットしました
[392] svn st
 M     config
[393] svn ci -m 'ignore files'
送信しています              config

リビジョン 13 をコミットしました。
[394] svn st
[395]

RSpecを準備

[399] ruby script/generate rspec
 <略>
[401] svn add * 
 <略 これは流石に警告ある、既に管理下とか>
[402] svn ci -m 'rspec'
追加しています              spec
追加しています              spec/rcov.opts
追加しています              spec/spec.opts
追加しています              spec/spec_helper.rb
追加しています              stories
追加しています              stories/all.rb
追加しています              stories/helper.rb
ファイルのデータを送信中です.....
リビジョン 14 をコミットしました。
[403] svn up
リビジョン 14 です。

一応 spec してみる

[405] rake spec
(in /home/hs9587/rails/photo)
rake aborted!
no such file to load -- /home/hs9587/rails/photo/db/schema.rb

(See full trace by running task with --trace)

まだモデルも何も無いから。

Photo作成

[408] ruby script/generate rspec_scaffold photo size:integer content_type:string filename:string name:string name_kana:string number:integer
 <略>

いろいろ

[426] rake db:migrate 
[429] rake spec
(in /home/hs9587/rails/photo)
NOTICE:  CREATE TABLE will create implicit sequence "photos_id_seq" for serial column "photos.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "photos_pkey" for table "photos"
DEPRECATION WARNING: config.action_view.cache_template_extensions option has been deprecated and has no affect. Please remove it from your config files.  See http://www.rubyonrails.org/deprecation for details. (called from /home/hs9587/rails/photo/vendor/plugins/rspec_on_rails/lib/spec/rails/example/rails_example_group.rb:4)
.....................................................

Finished in 1.128269 seconds

53 examples, 0 failures

DEPRECATION WARNING 対応、vendor/plugins/rspec_on_rails/lib/spec/rails/example/rails_example_group.rb: 4行目をコメントアウト

svn は適宜

RCov

カバレッジ計測ツールも使う

sudo gem install rcov
rake spec:rcov

「TOTAL 107 70 100.0% 100.0% 」
svn propset svn:ignore coverage .」も。

attachment-fuを使って

モデルとそのスペック

has_attachment を使う、保存先はファイルシステム、バリデーションやサムネイルちょっと

models/photo.rb

class Photo < ActiveRecord::Base
  rails_conf = Rails::Configuration.new
  data_conf = rails_conf.database_configuration[rails_conf.environment]
  has_attachment :storage => :file_system \
    , :path_prefix => data_conf['photo_path'] \
    , :content_type => :image \
    , :max_size => 1.megabytes \
    , :thumbnails => {:thumb => '60x', :small => '240x'}
  validates_as_attachment
end

models/photo_spec.rb

require File.dirname(__FILE__) + '/../spec_helper'

describe Photo do
  before(:all){ @var = nil }
  before(:each) do
    @photo = Photo.new
    @photo.content_type = 'image/jpeg'
    @photo.size = 1.kilobytes
  end

  it 'shoukd not ve valid' do
    Photo.new.should_not be_valid
  end # it 'shoukd not ve valid' do

  it "should be valid" do
    pending 'how do we new valid Photo.instance' do
      @photo.should be_valid
    end # pending 'how do we new valid Photo.instance' do
  end

  it 'should have attachment' do
    @photo.should respond_to(:attachment_options)
    lambda{ @var = @photo.attachment_options }.should_not raise_error
    @var.should be_instance_of(Hash)
    @var.should have_key(:processor)
    @var.should have_key(:path_prefix)
    @var.should have_key(:thumbnails)
    @var.should have_key(:size)
    @var.should have_key(:storage)

    @photo.should respond_to(:save_attachment?)
    lambda{ @var = @photo.save_attachment? }.should_not raise_error
    @var.should be_false
  end # it 'should have attachment' do

  it 'should be strage: file system db/test/photos' do
    @var = @photo.attachment_options
    @var[:storage].should == :file_system
    @var[:path_prefix].should == 'db/test/photos'

    rails_conf = Rails::Configuration.new
    data_conf = rails_conf.database_configuration[rails_conf.environment]
    @var[:path_prefix].should == data_conf['photo_path']
  end # it 'should be strage: file system db/test/photos' do

  it 'should have attachment options for vaidation' do
    @var = @photo.attachment_options
    @var[:max_size].should == 1.megabytes
    @var[:content_type].should have(6).items
    @var[:content_type].should include('image/jpeg')
    @var[:content_type].should include('image/pjpeg')
    @var[:content_type].should include('image/gif')
    @var[:content_type].should include('image/png')
    @var[:content_type].should include('image/x-png')
    @var[:content_type].should include('image/jpg')
  end # it 'should have attachment options for vaidation' do

  it 'should have attachment options for thumbnails' do
    @var = @photo.attachment_options
    @var[:thumbnails].should be_instance_of(Hash)
    @var[:thumbnails].keys.should include(:thumb)
    @var[:thumbnails].keys.should include(:small)
    @var[:thumbnails][:thumb].should == '60x'
    @var[:thumbnails][:small].should == '240x'
  end # it 'should have attachment options for thumbnails' do

  it 'should have thumbnails' do
    @photo.should respond_to(:thumbnails)
    lambda{ @var = @photo.thumbnails }.should_not raise_error
    @var.should respond_to(:find)
    lambda{ @var.find :first, :conditions => {}}.should_not raise_error
    pending 'photo.find :first :conditions ... arg 2 or 1 ?'
  end # it 'should have thumbnails' do
end

validなモデルをどうやって new したものかよく分かっていないのでその辺のチェックが出来てない。
has_attachment を書いてるかどうかは attachment_optionsメソッドがあるかどうかでチェック。

haveマッチャーからは「DEPRECATION WARNING: Inflector is deprecated! 云々」警告が出るけど、今は無視。
そのうち RSpecプラグインの方で対応する事に期待。

ビュー(new)とそのスペック

views/photos/new.html.erb

  <p>
    <%= f.label :uploaded_data, "Photo" %><br />
    <%= f.file_field :uploaded_data %>
  </p>

file_field な multipartフォームにする。
一方ジェネレータが作った size、content_type、filename のtext_field は取る。
edit.html.erb でもその辺編集用のところはなしにして値の表示のみに。

views/photos/new.html.erb_spec.rb

  it "should render new form" do
    render "/photos/new.html.erb"

    #response.should have_tag("form[action=?][method=post]", photos_path) do
    response.should have_tag("form[action=?][method=post][enctype=multipart/form-data]", photos_path) do
      #with_tag("input#photo_size[name=?]", "photo[size]")
      #with_tag("input#photo_content_type[name=?]", "photo[content_type]")
      #with_tag("input#photo_filename[name=?]", "photo[filename]")
      with_tag("input#photo_name[name=?]", "photo[name]")
      with_tag("input#photo_name_kana[name=?]", "photo[name_kana]")
      with_tag("input#photo_number[name=?]", "photo[number]")

      with_tag("input#photo_uploaded_data[name=?]", "photo[uploaded_data]")
    end
  end

コントローラとそのスペック
show_photoを作る。

controllers/photos_controller.rb

  # GET /photos/1/show_photo
  def show_photo
    @photo = Photo.find(params[:id])
    send_data @photo.send(:current_data) \
      , :disposition => 'inline' \
      , :type => @photo.content_type
  end # def show_photo

current_data がプロテクテッドメソッドなので sendする。
モデルの方で send_dataの引数をまとめて返すようなメソッド作るべきだろうか? 謎。

controllers/photos_controller_spec.rb

  describe "handling GET /photos/1/show_photo" do

    before(:each) do
      @photo = mock_model(Photo, :show_photo => 'The photo' \
        , :current_data => 'photo data', :content_type => 'image/pjpeg')
      Photo.stub!(:find).and_return(@photo)
    end # before(:each) do

    def do_get
      get :show_photo, :id => '1'
    end # def do_get

    it 'should be successful' do
      do_get
      response.should be_success
    end # it 'should be successful' do

    it 'should find the photo requested' do
      Photo.should_receive(:find).with('1').and_return(@photo)
      do_get
    end # it 'should find the photo requested' do

    it 'should have a current photo data' do
      @photo.should_receive(:current_data).with().and_return('photo data')
      @photo.should_receive(:content_type).with().and_return('image/pjpeg')
      do_get
    end # it 'should have a current photo data' do

    it 'should send data with the photo data' do
      #@photo.should_receive(:show_photo).and_return('The photo')
      do_get
      response.body.should == 'photo data'
      response.headers['type'].should == 'image/pjpeg'
      response.headers['Content-Disposition'].should == 'inline'
    end # it 'should send data with the photo data' do
  end # describe "handling GET /photos/1/show_photo" do

send_data の引数をスペック出来なかった、コメントアウトしてる辺り。なんでだろう、挙動が不安定な感じ。

ビュー(edit)とそのスペック

画像情報は表示のみ、編集しない

views/photos/edit.html.erb

  <p>
    <%= f.label :size %><br />
    <%#= f.text_field :size %>
    <%=h @photo.size %>
  </p>
  <p>
    <%= f.label :content_type %><br />
    <%#= f.text_field :content_type %>
    <%=h @photo.content_type %>
  </p>
  <p>
    <%= f.label :filename %><br />
    <%#= f.text_field :filename %>
    <%= @photo.filename %>
  </p>

views/photos/edit.html.erb_spec.rb

  it "should render edit form" do
    render "/photos/edit.html.erb"

    response.should have_tag("form[action=#{photo_path(@photo)}][method=post]") do
      #with_tag('input#photo_size[name=?]', "photo[size]")
      #with_tag('input#photo_content_type[name=?]', "photo[content_type]")
      #with_tag('input#photo_filename[name=?]', "photo[filename]")
      with_tag('input#photo_name[name=?]', "photo[name]")
      with_tag('input#photo_name_kana[name=?]', "photo[name_kana]")
      with_tag('input#photo_number[name=?]', "photo[number]")
    end

    response.should have_tag 'p', /\ASize\s*1\z/
    response.should have_tag 'p', /\AContent type\s*MyString\z/
    response.should have_tag 'p', /\AFilename\s*MyString\z/
  end

with_tag ではなく、フォームののhave_tagブロックの外側で have_tag p。

show_photo のRESTfull風ルートとそのスペック

views/photos/show.html.erb_spec.rb「it "should render attributes in

" do」内

    response.should have_tag('img')
    #response.should have_tag('img[src=?]', "/photos/#{@photo.id}/show_photo")
    response.should have_tag('img[src=?]', "/photos/#{@photo.id}/show_small")
    response.should have_tag('a[href=?]', "/photos/#{@photo.id}/show_photo")

controllers/photos_routing_spec.rb

    it "should map { :controller => 'photos', :action => 'show_photo', :id => 1 } to /photos/1/show_photo" do
      route_for(:controller => "photos", :action => "show_photo", :id => 1).should == "/photos/1/show_photo"
    end
    it "should generate params { :controller => 'photos', action => 'show_photo', id => '1' } from GET /photos/1/show_photo" do
      params_from(:get, "/photos/1/show_photo").should == {:controller => "photos", :action => "show_photo", :id => "1"}
    end

で、routeの設定。

config/routes.rb
  map.resources :photos \
    , :member => {:show_photo => :get, :show_thumb => :get, :show_small => :get}

destroy

destroyではちゃんとファイルも消える。
サムネイルも、また、サムネイルの分のDBレコードも。

というか、create時に本体のレコードと同時にサムネイルのレコードも作るようになってる。

サムネール

一覧用の thumb と一枚用の small 二個

DBマイグレート

[886] ruby script/generate migration add_thumbnail_and_parent_id_to_photos thumbnail:string parent_id:integer
      exists  db/migrate
      create  db/migrate/20080923065744_add_thumbnail_and_parent_id_to_photos.rb
photo_development=> SELECT id,filename,thumbnail,parent_id from photos;
 id |      filename      | thumbnail | parent_id
----+--------------------+-----------+-----------
  5 | halo.3.4.jpg       |           |
  6 | halo.3.4_small.jpg | small     |         5
  7 | halo.3.4_thumb.jpg | thumb     |         5
[794] ls development/photos/0000/0005
halo.3.4.jpg  halo.3.4_small.jpg  halo.3.4_thumb.jpg

こんな感じ、DBテーブルにはサムネイルも個々に1レコードとして、
だから parent_idは必須カラム。
一方画像ファイルはまとめて parent_id のディレクトリに。

そしてそうすると、scaffold出来合いの indexリストでは、
サムネイル画像まで全部リストアップするのでなんとかしないといけない。

サムネールの表示

show_thumb と show_small コントローラメソッドとそのスペックも同様に作ってみた。
が、なんだかよく出来ないので、ビュー方面をして script/server の姿を見よう。

取敢えず一覧に thumb、showでは small を使う。

コントローラで findするのもコントローラメソッド増やすの良くないような気がするので、この辺りは何か整理する予定。

データ項目 destroy すると全部消えるね。
写真はサムネイル共々一緒に入ってるフォルダごと、テーブルレコードもサムネイルのレコードもちゃんと消える。

拡張子偽装

拡張子偽装でアップロードしてみる

  • .jpg -> .png : pjpeg になってる
  • .txt -> .png : Content type is not included in the list

ブラウザ(IE)が拡張子だけじゃなくて中まで見て Content type セットしてるかな、そんな雰囲気を感じる。