Sintra 日記

 [Home]

Sinatra はRubyの軽量アプリケーションサーバです。

コンソールに出力するのではなく、ブラウザに表示することで、ちょっとした見栄えのいいアプリケーションを書くのに向いています。

インストール

インストール方法は、ネットのどこかに詳しく書いてありますが、gemを使いこんな感じでイン ストールできます。

$ sudo gem install sinatra

ちゃんとインストールされたかは、次のようにして確認します。

$ gem list

ついでに、mysql や nokogori もインストールしておきます。

$ sudo gem install mysql
$ sudo gem install nokogiri

nokogiriは、HTMLやXMLを処理するときに使用できます。

 

簡単な例 (1)

よく Hello World! が簡単な例として出ていますが、ここでそれを書いてもつまらないので、 もうちょっとプログラムらしい例を書いてみました。具体的には、環境変数 PATH を見やすく表示 するものです。

# conding: utf-8
require "sinatra"

get '/' do
  ENV['PATH'].split(':').join("\n")
end

実行ですが、次のようにするだけです。
ところで、上のコードで return ENV['PATH'].split(':').join("\n") としても同じです。ruby では、最後の式の値がメソッドの戻り値になるため、return を省略しているだけです。

$ ruby show_path.rb

すると、下の画像のようなメッセージが表示されます。
この状態で、ブラウザからこのページを開きます。

$ ruby show_path.rb

この例では、サーバのアドレスが "192.168.1.35" とすると、"http://192.168.1.35:4567/" とします。

browser show_path

ですが、期待に反して見やすく表示されません。
これは、Sinatra が返す内容がデフォルトではHTMLになるためです。
この場合、テキストを返したいので、一行を追加します。

# conding: utf-8
require "sinatra"

get '/' do
   content_type 'text/plain'  # <== 追加
   ENV['PATH'].split(':').join("\n")
end

コードを直したら、Sinatra を再起動する必要があります。CTRL+Cを押すと停止するので、も う一度、 "ruby show_path.rb" を実行します。

Sinatra End

ブラウザで再表示を行うと、ちゃんと、見やすく表示されました。

ShowPath OK

 

簡単な例 (2)

単純に結果を表示するだけでなく、ユーザが指定した何かを表示するものを作ってみます。
具体的には、ユーザが指定したディレクトリの内容一覧を表示するアプリケーションを作ります 。

ディレクトリの指定方法ですが、get のパラメータとして与えます。パラメータにはワイルドカ ード ('*') が使えるので、ディレクトリをワイルドカードとして指定します。'*'は任意の文字列に一致するので、入力したパスはすべてパラメータとして与えられます。

# coding: utf-8
require 'sinatra'

get '/*' do |dir|
  content_type 'text/plain'
  dir = dir.nil? ? '/' : '/' + dir
  items = Array.new
  Dir.foreach(dir) do |p|
    items.push(p) unless p.match(/^\.+/)
  end
  items.join("\n")
end

これを実行して、ブラウザでルート (/) を開くと、下のように表示されます。

dir root

ユーザ (user) のホームディレクトリ (/home/user) を表示するには、次のようにします。

home_user_dir

このままだと、不正なパスを入力すると、実行エラーが発生してしまうので、エラーハンドラを追加します。

与えられたディレクトリが存在しないときは、エラーコード 404 (Not Found) を返し、エラーハンドラがエラーメッセージを表示するようにしました。

# coding: utf-8
require 'sinatra'

# Not Found error
error 404 do
  'Error: Bad path.'
end

get '/*' do |dir|
  content_type 'text/plain'
  dir = dir.nil? ? '/' : '/' + dir
  return 404 unless test(?d, dir)
  items = Array.new
  Dir.foreach(dir) do |p|
    items.push(p) unless p.match(/^\.+/)
  end
  items.join("\n")
end

前の例ではパスにワイルドカードを使いましたが、コマンドのような文字列を指定することも できます。次の例では、ls と cat というコマンドを受け付けるアプリを作成してみました。

ソースを下に示します。

# coding: utf-8
require 'sinatra'
require 'mylogger'

Home = '/home/user/doc'

# Not Found error
error 404 do
  'Error: Bad path.'
end

get '/ls/' do
  content_type 'text/plain'
  items = Array.new
  Dir.foreach(Home) do |p|
    items.push(p) unless p.match(/^\.+/)
  end
  items.join("\n")
end

get '/ls/:dir' do |dir|
  content_type 'text/plain'
  dir = Home + '/' + dir 
  return 404 unless test(?d, dir)
  items = Array.new
  Dir.foreach(dir) do |p|
    items.push(p) unless p.match(/^\.+/)
  end
  items.join("\n")
end

get '/cat/:file' do |file|
  return 404 unless test(?f, Home + '/' + file)
  content_type 'text/plain'
  File.read(Home + '/' + file)
end

get '/' do
  redirect '/ls/'
end

このアプリの実行例ですが、こんな感じになります。

ls
ls コマンドの例 (パラメータ指定なし)
ls yum
ls コマンドの例 (パラメータ指定あり)
dir2 cat
cat コマンドの例

この例では、次のようなハンドラを用意しています。

  1. get '/'
  2. get '/ls/'
  3. get '/ls/:dir/'
  4. get '/cat/:file'

1. は、 2. へ飛ばします。
2. は、 定数 Home で定義したディレクトリの内容一覧を返します。
3. は、 :dir で指定した Homeディレクトリの サブディレクトリの内容一覧を返します。
4. は、:fileで指定した Homeディレクトリのにあるテキストファイルの内容を返します。

ところで、 :dir や :file に '/' を含めるとエラーになります。'/' はパス名として使用されるため、別のパスとみなされてしまうためです。

HTMLのページを作る

今までの例はテキストを返すアプリでしたが、本来、SinatraのページはHTMLがデフォルトであ り、見栄えをよくするためにHTMLを使う方法を見ていきます。

一番簡単な方法は、get の中でHTMLを作成しそのままメソッドの戻り値とする方法ですが、よ ほど短くないと見づらいプログラムになるし、再利用もできません。そのため、普通、「テンプレ ート」を別に作って、その中にプログラムで作成した値や文字列を埋め込むようにします。

テンプレートの形式は、いろいろサポートされていますが、標準は Erb というもので、特別なライブラリをインストールせずに使用できます。あと、よく例として出てくるのが Haml というもので、Erb より簡潔で見やすいテンプレートが作れます。ただし、Erb はHTML の知識があれば簡単に作れますが、Haml はその文法を覚える必要があります(あまり難しくはないですが)。

まず、直接、get メソッドの中にHTMLを書く例です。

html_test

上の画面は、下に示すコードで、get '/' do .. end で直接、HTMLを書いています。

# coding: utf-8
require "sinatra"
require "haml"

# root
get '/' do
<<EOS
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>HTML test</title>
<style>
 h1,h2,h3,h4 {
  color: #005555;
  padding: 6px;
 }
 ul {
  list-style-type: square;
 }
 
 body {
   margin-left, margin-right: 4%;
   background-color: #f0f0e0;
 }
</style>
</head>

<body>
<h2>HTML test</h2>
<hr />
<p>
<ul>
<li><a href="/internal/">Internal (Erb) template</a></li>
<li><a href="/erb/">External (Erb) template</a></li>
<li><a href="/haml/">External (Haml) template</a></li>
<li><a href="/static.html">Static HTML</a></li>
</ul>
</p>
</body>
</html>
EOS
end

get '/internal/' do
  @title = "Internal template"
  erb :internal
end

get '/erb/' do
  @title = "erb template"
  erb :erb
end

get '/haml/' do
  @title = "haml template"
  haml :haml
end

__END__
@@internal
<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8" />
 <title><%= @title %></title>
 <style>
  h1, h2 {
    color: red;
  padding: 6px;
  }
 </style>
</head>

<body>
<h1><%= @title %></h1>
<hr />
<p><a href="/">[ Home ]</a></p>

</body>
</html>
internal

ルートページ(get '/') のメニューで、"Internal (Erb) template "メニューのテンプレートは 、__END__ の後に書かれています。@@internal が識別のための名前で、 get '/internal/' .. end の中で、erb :internal として呼び出しています。<%= @title %>がコードを埋め込むタグで、@title変数をそのまま埋め込んでいます。

ルートページ(get '/') のメニューで、"Exernal (Erb) template "メニューのテンプレートは、 別ファイルとして"views/erb.erb"というファイルに書かれています。get '/erb/' .. end の中で、erb :erb として呼び出しています。<%= @title %>がコードを埋め込むタグで、@title変数をそのまま埋め込んでいます。

このerbテンプレートのように、すべてのテンプレートファイルは、viewsサブディレクトリに置きます。なお、このテストアプリでは、ディレクトリ構造が下のようになっています。views/がテンプレート用、public/がスタティックファイル用です。

/
views/
 erb.erb
 haml.haml
 public/
 static.html
 img/
  siesta.gif

ファイル "erb.erb"は次のようになっています。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title><%= @title %></title>
<style>
  h1, h2 {
    color: #604000;
    padding: 6px;
  }
</style>
</head>

<body>
<h2><%= @title %></h2>
<hr />
<p>
<a href="/">[ Home ]</a>
</p>

</body>
</html>
erb

ルートページ(get '/') のメニューで、"Exernal (Haml) template "メニューのテンプレートは 、Erbの場合と同様に別ファイルとして"views/haml.haml"というファイルに書かれています。get '/haml/' .. end の中で、haml :haml として呼び出しています。%h2= @titleがコードを埋め込む部分で、@title変数をそのまま埋め込んでいます。

!!! XML utf-8と!!! は、XHTMLのための宣言を作成する部分です。

!!! XML utf-8
!!!
%html
 %head
  %meta{ 'http-equiv' => 'content', :content => 'text/html; charset=utf-8'}
 %body
  %h2= @title
  %hr
  %p
   %a{ 'href'=>'/'}
    [ Home ]
haml

ルートページ(get '/') のメニュー最後は、static.htmlへのリンクです。このファイルは、サブディレクトリ public/ の下に置きます。このディレクトリには、HTML以外の画像ファイルやCSS ファイル、JavaScriptファイルなども置いておきます。

<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<title>Static</title>
<link rel="stylesheet" href="/default.css" />
</head>

<body>
<h2>Static HTML</h2>
<hr />
<p>
<a href="/">[ Home ]</a>
</p>
<img src="/img/siesta.gif" alt="siesta" />
<br />

</body>
</html>
static

 

やや複雑なアプリの場合 (helpers)

やや複雑なアプリの場合、ヘルパーメソッドや外部のモジュールやクラスを利用したくなりま す。

次の例は、ヘルパーメソッドの使用例で、サービスのログを表示する例です。このサンプルを実 際に動かすためには、ログファイルの読み取りが許可されていることと、OS や ディストリビューション によりログファイルの名前や位置が異なるので、パス名を変更する必要があります。この例は、CentOS 5.x のものです。

この例では、今までの例と異なり Sinatra::Base からクラスを派生させています。こうしておけば、include や extend が使えます。ヘルパメソッドは、helpers do .. end の中に記述して います。

さらに、プログラムの最後に、SinaApp.run! という文が必要です。

# coding: utf-8
require "sinatra"

MaxLines = 100

class SinaApp < Sinatra::Base
  helpers do
    def read_log(path)
      return "Error! cant't read the log." unless test(?R, path)
      buff = Array.new
      f = nil
      begin
        f = open(path)
        s = f.gets
        while s
          buff.push(s)
          buff.shift if buff.size > MaxLines
          s = f.gets
        end
      ensure
        f.close unless f.nil?
      end
      return buff.join
    end
  end
  
  error 404 do
    content_type 'text/plain'
    'Such log does not exists!'
  end
  
  get '/:log' do |log|
    content_type 'text/plain'
    case log
    when 'httpd'
      path = '/var/log/httpd/error_log'
    when /apache|apache2/
      path = '/var/log/apache2/error.log'
    when 'samba'
      path = '/var/log/samba/nmbd.log'
    when 'vsftpd'
      path = '/var/log/vsftpd.log'
    else
      return 404
    end
    return read_log(path)
  end
  
  get '/' do
    redirect '/httpd'
  end
end

SinaApp.run!

下の画像は、このアプリの実行例です。

logview

 

コンフィギュレーション

1回だけ実行する動作、例えば、設定ファイルの読み込みなどは configuration メソッドを使って行います。このとき、設定値などを取得したら、set メソッドを使ってシンボルに値を設定します(インスタンス変数などに格納してもうまくいきません)。

# アプリケーション・クラス
class SinaApp < Sinatra::Base
  include TagMaker
  
  Config = "./public/config.ini"
  
  # 1回だけ実行される。
  configure do
    f = open(Config)
    while f.gets
      p = $_.split('=')
      if p.size == 2 then
        case p[0].strip
        when 'userid'
          set :userid, p[1].strip
        when 'password'
          set :password, p[1].strip
        when 'dbname'
          set :dbname, p[1].strip
        end
      end
    end
    f.close
  end

 

セッション

セッション変数を使うと、ページ間で値を共有できます。セッションを使うには、enable メソッドを使ってセッション機能を有効にしておく必要があります。

enable :sessions

セッション変数は、session という名前の連想配列に格納されます。キーはシンボル、値は文字列です。

# coding: utf-8
require "sinatra"

HttpdLog = '/home/user/doc/CentOS.txt'
Page = 30

# アプリケーションクラス
class SinaApp < Sinatra::Base
  enable :sessions
  
  helpers do
    def get_content(n)
      buff = []
      i = 0
      istart = n * Page
      iend = istart + Page
      f = open(HttpdLog)
      while f.gets
        i += 1
        if i >= istart and i <= iend then
          buff.push(format("%05d: %s", i, $_.chomp))
        end
      end
      f.close
      n += 1
      if buff.size < Page then
        session[:sessionTest] = '0'
      else
        session[:sessionTest] = n.to_s
      end
      return buff.join("<br />\n")
    end
  end
  
  # root
  get '/' do
    if session.has_key?(:sessionTest) then
      @n = session[:sessionTest].to_i
    else
      @n = 0
    end
    @content = get_content(@n)
    erb :sessionTest
  end
end

# 実行
SinaApp.run!

この中で使われているsessionTest というテンプレートは次のようになっています。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>page <%= @n %></title>
</head>

<body>
<h1>page <%= @n %></h1>
<p><a href="javascript:location.reload();">[ Next ]</a></p>
<p>
<%= @content %>
</p>
</body>
</html>

 

フォームとpostメソッド

フォームを使う場合、ユーザ入力ページは get メソッド、データ受信ページは post メソッドで処理します。フォームから送られてくるデータは、params という連想配列で受け取ることができます。

下のサンプルは、テキストボックスに入力したOSコマンドを実行し、結果を返す簡単なアプリです。

# coding: utf-8
require "sinatra"

Config = "./public/config.ini"

# アプリケーションクラス
class SinaApp < Sinatra::Base

  # root (get)
  get '/' do
    @title = "Command exec"
    @message = ""
    @value = ""
    @link = ""
    erb :form1
  end
  
  # root (post)
  post '/' do
    @title = "Command exec (result)"
    @message = `#{params[:command]}`
    @value = params[:command]
    @link = '<a href="/">New command</a>'
    erb :form1
  end
end

# 実行
SinaApp.run!

下のコードは上のアプリで使用するフォームのテンプレート(ERB) です。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title><%= @title %></title>
<link rel="stylesheet" href="default.css" />
</head>

<body>
<h1><%= @title %></h1>
<p>
<form method="post" action="/">
command: <input type="text" name="command" size="80" value="<%= @value %>" /><br />
<input type="submit" value=" exec " />
</form>
</p>
<pre style="padding: 4px;marin-left:5%;"><%= @message %></pre>
<p><%= @link %></p>
</body>
</html>
form1

 

フィルタ

前のhelpersの例のところで、ログを表示するとき、ディストリビューションが違うとログの位置が違うので、エラーになってしまいます。これを吸収するために、before メソッドを追加して実際にログファイルを開く前にパス名を変更するようにしたのが、次のサンプルです。

before do
  if test(?d, '/var/log/apache2') and request.path_info == '/httpd' then
    # Ubuntu / Debian
    request.path_info = '/apache'
  elsif test(?d, '/var/log/httpd') and request.path_info.match(/\/apache.*/) then
    # Redhat / CentOS
    request.path_info = '/httpd'
  else
    # Unknown
    ...
  end
end

 

 

 開設 2014年12月   著作権 2014-2015 bonk.red  連絡先: こちらからメッセージを送ってください。 (お仕事も大募集)

 このページの先頭へ..