(追記、その2も見よ)
Rails(PostgreSQL)で、画像アップロードサービスを作ってみる。
添付ファイルの取り扱いには attachment-fuプラグインを使い、
更に画像のバリデーションやサムネイル作成には RMagckを用いる。
Railsアプリケーション作成
[207] rails -d postgresql photo
[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 すると全部消えるね。
写真はサムネイル共々一緒に入ってるフォルダごと、テーブルレコードもサムネイルのレコードもちゃんと消える。