用意する環境
- Ubuntu 14.04
- Ruby2.2
- bundle 1.8.4
- MySQL 5.5
- ImageMagick(
apt-get install imagemagick
すればおk)
要件
簡素な画像アップローダーサイト「gazo」をつくる(Gyazoでゎなぃ
- 画像を投稿できて一覧表示できるようにする
- 画像投稿には必ずコメントが必要
- 一覧表示に負荷がかからないよう、各画像にサムネイルと縮小版の画像も自動生成
最終的なディレクトリ構造
あくまで最終
├── app.rb ├── config.ru ├── db │ ├── database.yml │ ├── migrate │ │ └── 20150622020738_create_photos.rb │ └── schema.rb ├── Gemfile ├── Gemfile.lock ├── public │ └── uploads │ ├── xxxxx.jpg │ ├── xxxxx.jpg │ │ :: │ └── tmp ├── Rakefile └── views ├── index.slim ├── layout.slim └── new.slim
下準備
ライブラリを揃える
好きなディレクトリ上(今回は/home/user/gazo)にGemfileを生成
cd /home/user/gazo bundle init
これから先、特に断りがない場合はこのディレクトリ上からの説明とする
生成されたGemfileに必要なライブラリを記述していく
source "https://rubygems.org" # 言わずと知れたsinatra gem 'sinatra' # HTML表示のテンプレートに使う gem "slim" # 開発においてrubyをいちいち再起動しなくても更新した部分が反映される gem 'sinatra-contrib' # 画像ファイル管理に使う gem 'carrierwave' # 以下DB関連 gem 'mysql2' gem 'activerecord', require: 'active_record' gem "sinatra-activerecord" gem "rake"
ライブラリインストール実行
bundle
ActiveRecordの設定
Ruby側からデータベースに接続できるようにする
db/database.ymlを作成して設定項目を記述する。ymlファイルのインデントにタブ(\t
)を使用するとエラーになるので必ず半角スペースでインデントすること。
development: adapter: mysql2 database: gazo #データベース名 host: localhost username: root #ユーザー名 password: pass #パスワード encoding: utf8
これからデータベースを操作するためにrakeコマンドを叩く。その為にはRakefileというのが必要なので作成して以下にする
require 'sinatra/activerecord' require 'sinatra/activerecord/rake' require './app'
メインプログラムであるapp.rbを作成し以下にする
require 'bundler' Bundler.require configure do ActiveRecord::Base.configurations = YAML.load_file('db/database.yml') ActiveRecord::Base.establish_connection(Sinatra::Application.environment) end
ここまででbundle exec rake -T
とすると以下の様なコマンド一覧が出現するはず
$ bundle exec rake -T rake db:create # Creates the database from DATABASE_URL or config/database.yml for the current RAI... rake db:create_migration # Create a migration (parameters: NAME, VERSION) rake db:drop # Drops the database from DATABASE_URL or config/database.yml for the current RAILS... rake db:fixtures:load # Load fixtures into the current environment's database rake db:migrate # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog) rake db:migrate:status # Display status of migrations rake db:rollback # Rolls the schema back to the previous version (specify steps w/ STEP=n) rake db:schema:cache:clear # Clear a db/schema_cache.dump file rake db:schema:cache:dump # Create a db/schema_cache.dump file rake db:schema:dump # Create a db/schema.rb file that is portable against any DB supported by AR rake db:schema:load # Load a schema.rb file into the database rake db:seed # Load the seed data from db/seeds.rb rake db:setup # Create the database, load the schema, and initialize with the seed data (use db:r... rake db:structure:dump # Dump the database structure to db/structure.sql rake db:structure:load # Recreate the databases from the structure.sql file rake db:version # Retrieves the current schema version number
データベースの作成
ようやくデータベースを作成 以下のコマンドを叩くとdatabase.ymlで設定したデータベース「gazo
」が生成される
bundle exec rake db:create
この時点ではまだデータベースができただけ。その中にテーブルを作るべく、マイグレーションするためのファイルを生成
bundle exec rake db:create_migration NAME=create_photos
NAME
の指定をしないとNo NAME specified
って怒られる
するとdb/migrate/2015******_create_photos.rbが生成される。以下のようになっているはず
class CreateImages < ActiveRecord::Migration def change end end
テーブルphotosを作成するためにcreate_table
メソッドを追加するファイル名の管理に用いるfile、投稿コメントcomment両方共string(MySQLでいうVARCHAR(255)
)でおk
class CreatePhotos < ActiveRecord::Migration def change create_table(:photos) do |p| p.string :file p.string :comment end end end
マイグレーション実行!
bundle exec rake db:migrate
以下のように表示されれば成功
user@ubuntu:~/gazo$ bundle exec rake db:migrate == 20150622020738 CreatePhotos: migrating ===================================== -- create_table(:photos) -> 0.0418s == 20150622020738 CreatePhotos: migrated (0.0421s) ============================
ちゃんとテーブルが作成されているぞい
mysql> desc photos; +---------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | file | varchar(255) | YES | | NULL | | | comment | varchar(255) | YES | | NULL | | +---------+--------------+------+-----+---------+----------------+ 3 rows in set (0.00 sec)
画像アップロードの処理を作る
まずは中心部分の画像アップロードの部分を作る。具体的にはPOSTメソッドで画像ファイルとコメントと受け取り、それらを加工して今作ったphotosテーブルに格納する処理を実装する。デバッグにPostmanのようなデバッグツールがあると便利
PhotoUploaderクラスを作る
受け取った画像の処理を行うCarrierWave::Uploader::Baseを継承したPhotoUploaderクラスを作る。
class PhotoUploader < CarrierWave::Uploader::Base end
なんとこれだけ これだけでも動く
ただ今回は要件でサムネイル画像・縮小画像の自動生成があるのでそれらの処理を実装していく。
加えてファイル名が「新しい画像.jpg」等日本語含むと厄介なのでファイル名をこちら側で一意な名前に変更するべく、公式ドキュメントのTipsを参考に実装
class PhotoUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick #サムネイル画像の生成 version :thumb do process :resize_to_fill => [200, 200] end #縮小画像の生成 version :small do process :resize_to_limit => [900, 900] end #ファイル名を一意に ex.b52d259b93.jpg def filename "#{secure_token(10)}.#{file.extension}" if original_filename.present? end protected def secure_token(length=16) var = :"@#{mounted_as}_secure_token" model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.hex(length/2)) end end
今回はオリジナル画像に加えてサムネイル画像と縮小画像の2パターンを用意する。resize_to_fill
では中央から200px*200px切り抜いた画像を自動生成、resize_to_limit
はオリジナル画像の高さか横幅のどちらかが900pxを超える画像だった場合に片辺を900pxに合わせた画像を生成される。このへんは公式ドキュメントの方が圧倒的に詳しい
Imageクラスを作る
ActiveRecordからphotoテーブルを操作するべく、ActiveRecord::Baseクラスを継承したPhotoクラスを作る。
class Photo < ActiveRecord::Base mount_uploader :file, PhotoUploader validates :comment, :file, presence: true end
mount_uploader
でfileカラムとPhotoUploaderを紐付ける。validates``presence: true
を指定することでcommentカラムとfileカラムの両方が必須(NOT NULL的な)になる。
POSTメソッドの処理
最後にSinatra側で/new
でPOSTメソッドを受け取ったら処理するようにさせる。
post "/new" do photo = Photo.new(file: params[:image], comment: params[:comment]) if photo.save session[:responce] = {code: 200, messages: "成功しました"} else session[:responce] = {code: 400, messages: photo.errors.full_messages} end redirect back end
POSTメソッドのパラメータはparams[:comment]
のようにして受け取ることができる。
今までのコードを全てapp.rbに記述すると以下のようになる
require 'bundler' Bundler.require require 'carrierwave/orm/activerecord' configure do ActiveRecord::Base.configurations = YAML.load_file('db/database.yml') ActiveRecord::Base.establish_connection(Sinatra::Application.environment) enable :sessions end class PhotoUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick permissions 0666 directory_permissions 0777 storage :file #サムネイル画像の生成 version :thumb do process :resize_to_fill => [200, 200] end #縮小画像の生成 version :small do process :resize_to_limit => [900, 900] end #ファイル名を一意に ex. def filename "#{secure_token(10)}.#{file.extension}" if original_filename.present? end protected def secure_token(length=16) var = :"@#{mounted_as}_secure_token" model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.hex(length/2)) end end class Photo < ActiveRecord::Base mount_uploader :file, PhotoUploader validates :comment, :file, presence: true end post "/new" do photo = Photo.new(file: params[:photo], comment: params[:comment]) if photo.save session[:responce] = {code: 200, messages: "成功しました"} else session[:responce] = {code: 400, messages: photo.errors.full_messages} end redirect back end
実行
試しに画像をアップロードしてみる。実行は以下のようにして行う
bundle exec rackup -o 0.0.0.0
特に指定しない限り:development
モードで起動する。もちろん開発だからそれでいいのだが、:development
モードだとローカルホスト以外から一切アクセス出来ない。そこで-o 0.0.0.0
オプションを付けて外部からでもアクセスできるようにする。
また、デフォルトだとアクセスポートは9292だが-p 1129
のようにすると任意のポートで起動できる(ただし1000番以上)
あとはPostmanなり自作phpなりでPOSTメソッドを投げればおk
成功すればpublic/uploadsのディレクトリが生成されそこに画像が保存される。 またデータベースにも
mysql> select * from photos; +----+----------------+---------+ | id | file | comment | +----+----------------+---------+ | 1 | 588f7c1618.jpg | hello | | 2 | 7f3874e328.jpg | hello | +----+----------------+---------+
のように格納されているのがわかる
Web表示を実装する
投稿フォームの作成
心臓部分はできたので次は投稿画面を作成する
まずHTMLを表示するパーツであるviewsというディレクトリを作成し、その中にlayout.slimを作成、以下のようにする。
doctype html head meta charset="utf-8" title = @title link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" body .container == yield
同一ディレクトリにnew.slimも作成、以下のようにする
a.btn.btn-default href="/" TOPへ戻る h1 = @title - if !session[:responce].blank? - if session[:responce][:code] == 400 .alert.alert-danger ul - for e in session[:responce][:messages] li = e - else .alert.alert-success p = session[:responce][:messages] form method="post" action="" enctype="multipart/form-data" .form-group label コメント input.form-control type="text" name="comment" .form-group label 画像 input.form-control type="file" name="photo" button.btn.btn-default type="submit" アップロード
そしてapp.rbにGETメソッドで/newにアクセスした時のルーティングを追記
get "/new" do @title = "画像投稿" slim :new end
とりあえずこれでbundle exec rackup -o 0.0.0.0
して、192.168.xxx.xxx:9292/newにアクセスするとBootstrapバリバリのフォームが表示されるハズ
- slimテンプレートについてはここでは一切触れない。
- app.rb内で
slim :new
とすると- ①Sinatraはviews/new.slimを探す
- ②views/layout.slimもないか探す
- ③もしlayout.slimが無ければそのままnew.slimを表示、あればlayout.slim内の
== yield
の部分にnew.slimの内容が展開されて表示
- つまりlayout.slimを作るだけで自動的にWebサイトの骨組みとして認識してくれる
- viewsというディレクトリにHTMLを配置するのはSinatra&Raisの「しきたり」
- app.rb内で
@title
のように変数の頭にアットマークが付いているのはインスタンス変数であり、Sinatra::Aplicationのインスタンス変数になってくれるのでapp.rb→new.slimのデータの受け渡しが簡単にできる
投稿もできる。成功すれば保存されるし、失敗すれば以下の様なエラーが表示される
画像一覧を表示
先ほど同様にviews/index.slimを作成して以下のようにする
.jumbotron h1 = @title p 一世一代の画像アップローダーが、今ここに。 a.btn.btn-lg.btn-primary href="./new" 画像をアップロードする - if @photos .row - for photo in @photos .col-sm-6.col-md-3.well a href="./image/#{photo.id}" img.center-block src="#{photo.file.thumb.url}"
で、app.rbに以下追記
get "/" do @title = "Gazo" @photos = Photo.all.reverse_order slim :index end
@photos
でデータベースを全件取得しreverse_order
で逆順(つまり新しい順)に変更してindex.slimに渡している
画像個別表示
画像一覧からクリックすると画像とコメントが表示できるページもつくる
show.slim
a.btn.btn-default href="/" TOPへ戻る .panel.panel-default .panel-heading = @photo.comment .panel-body center a href="#{@photo.file.url}" img src="#{@photo.file.small.url}"
app.rbに/showのルーティング追加
get "/image/:id" do @photo = Photo.find_by(id: params[:id]) if @photo.nil? "Not found...." else @title = "画像" slim :show end end
終わりに
今回はSinatra+ActiveRecord周りをサラッとやっただけなのでセキュリティもデザインも機能性も有意性もあったもんではない。けど今回は備忘録が目的なのでこんなもんかな