動かざることバグの如し

近づきたいよ 君の理想に

Sinatraを使って画像アップローダーを作る

用意する環境

要件

簡素な画像アップローダーサイト「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_uploaderfileカラム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 f:id:thr3a:20150622133438p:plain

成功すれば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バリバリのフォームが表示されるハズ

f:id:thr3a:20150622215931p:plain

  • slimテンプレートについてはここでは一切触れない。
  • app.rb内でslim :newとすると
    • Sinatraviews/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.rbnew.slimのデータの受け渡しが簡単にできる

投稿もできる。成功すれば保存されるし、失敗すれば以下の様なエラーが表示される f:id:thr3a:20150622220931p:plain

画像一覧を表示

先ほど同様に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

終わりに

今回はSinatraActiveRecord周りをサラッとやっただけなのでセキュリティもデザインも機能性も有意性もあったもんではない。けど今回は備忘録が目的なのでこんなもんかな