Redmine 1.2をMacにインストールしてみた

引っ越し関連のTODOを管理しようと思い、以前インストールしたまま埃をかぶっていたRedmineを引っ張りだしてみたところ、ローカル環境のRubyのバージョンが1.9になっていたりRailsのバージョンが3になっていたりしてまったく動かなくなっていたので、RVMを使ってRedmine用のgemsetを新しく作ってみました。そのときのメモです。

構築した環境

今回は下記のような環境を構築してみました。

Redmine Redmine 1.2
OS Mac OS X 10.6
データベース SQLite
Webサーバ Apache
Ruby 1.8.7
Rails 2.3.11

データベースは、設定が面倒くさいのでMySQLではなくSQLiteにしました。
Webサーバは、せっかくMacにはデフォルトでApacheが入っているので、MongrelではなくApacheを使うことにしました。

Redmine用のgemsetの作成

RVMを使ってRedmine用のgemsetを作成します。

$ rvm use ruby-1.8.7-p352
$ rvm gemset create redmine-1.2
$ rvm gemset use redmine-1.2

gemパッケージのインストール

Redmineのインストールに必要なgemパッケージをインストールします。

Rails 2.3.11のインストール

Redmine 1.2はRails 2.3.11でしか動かないので、Rails 2.3.11をインストールします。

$ gem install rails -v=2.3.11

gemsetにデフォルトで入っていたgemのバーションが1.8.10でしたが、Rails 2.3.11はgem 1.8未満でしか動かないらしいので*1、gemをダウングレードします。

利用可能なバーションを調べてみます。

$ gem search -rda ruby
rubygems-update (1.8.11, 1.8.10, 1.8.9, 1.8.8, 1.8.7, 1.8.6, 1.8.5, 1.8.4, 1.8.3, 1.8.2, 1.8.1, 1.8.0, 1.7.2, 1.7.1, 1.7.0, 1.6.2, 1.6.1, 1.6.0, 1.5.3, 1.5.2, 1.5.0, 1.4.2, 1.4.1, 1.4.0, 1.3.7, 1.3.6, 1.3.5, 1.3.4, 1.3.3, 1.3.2, 1.3.1, 1.3.0, 1.2.0, 1.1.1, 1.1.0, 1.0.1, 1.0.0, 0.9.5, 0.9.4, 0.9.3, 0.9.2, 0.9.1, 0.9.0, 0.8.11, 0.8.10, 0.8.8, 0.8.6, 0.8.5, 0.8.4, 0.8.3)
    Authors: Jim Weirich, Chad Fowler, Eric Hodel
    Rubyforge: http://rubyforge.org/projects/rubygems
    Homepage: http://rubygems.org

    RubyGems is a package management framework for Ruby

1.7系では1.7.2が最新のようなので、これをインストールします。

$ gem install rubygems-update -v=1.7.2
$ update_rubygems
$ gem -v
1.7.2
Rack 1.1.1のインストール

Rack 1.1.1をインストールします。
Rails 2.3.11インストール時にいっしょにインストールされた1.1.2はアンインストールします。

$ gem install rack -v=1.1.1
$ gem uninstall rack -v=1.1.2
Rake 0.8.7のインストール

Rake 0.8.7をインストールします。

$ gem install rake -v=0.8.7

いつの間にか入っていたRake 0.9.2をアンインストールしようとしたところ、下記のエラーが発生しました。

$ gem uninstall rake -v=0.9.2
ERROR:  While executing gem ... (Gem::InstallError)
    cannot uninstall, check `gem list -d rake`

いわれたとおりをgem list -d rakeを実行してみます。

$ gem list -d rake

*** LOCAL GEMS ***

rake (0.9.2, 0.8.7)
    Author: Jim Weirich
    Rubyforge: http://rubyforge.org/projects/rake
    Homepage: http://rake.rubyforge.org
    Installed at (0.9.2): /Users/<アカウント名>/.rvm/gems/ruby-1.8.7-p352@global
                 (0.8.7): /Users/<アカウント名>/.rvm/gems/ruby-1.8.7-p352@redmine-1.2.1

    Ruby based make-like utility.

どうやらRVMのglobalにインストールされているようです。
というわけで、gemsetをglobalに変更してから再度試してみたところ、無事アンインストール成功しました。

$ rvm gemset use global
$ gem list
$ gem uninstall rake

gemsetをもとに戻しておきます。

$ rvm gemset use redmine-1.2.1
i18n 0.4.2のインストール

i18n 0.4.2をインストールします。

$ gem install i18n -v=0.4.2
SQLiteドライバのインストール

sqlite3パッケージをインストールします。

$ gem install sqlite3
Passengerのインストール

Passengerをインストールします。

$ gem install passenger

最終的なgemsetは下記のようになりました。

$ gem list

*** LOCAL GEMS ***

actionmailer (2.3.11)
actionpack (2.3.11)
activerecord (2.3.11)
activeresource (2.3.11)
activesupport (2.3.11)
i18n (0.4.2)
rack (1.1.1)
rails (2.3.11)
rake (0.8.7)
rubygems-update (1.7.2)
sqlite3 (1.3.4)

Redmineのインストール

今回はSVNのソースからインストールしました。

$ svn export http://redmine.rubyforge.org/svn/branches/1.2-stable/

Apache上で動作させるので、エクスポートしたソースを~/Sites配下に移動しておきます。

$ mv 1.2-stable ~/Sites/redmine

データベースの設定を行います。

$ cd ~/Sites/redmine/
$ cp config/database.yml.example config/database.yml
$ cat config/database.yml
production:
  adapter: sqlite3
  database: db/production.sqlite3

セッションデータ暗号化用鍵の生成とテーブル作成を行います。

$ rake generate_session_store
$ rake db:migrate RAILS_ENV=production
PassengerのApache用モジュールのインストール

インストーラを実行してPassengerのApache用モジュールをインストールします。

$ passenger-install-apache2-module

インストールが完了する下記のように表示されます。
あとでApacheに設定するのでコピペしておきます。

LoadModule passenger_module /Users/<アカウント名>/.rvm/gems/ruby-1.8.7-p352@redmine-1.2/gems/passenger-3.0.9/ext/apache2/mod_passenger.so
PassengerRoot /Users/<アカウント名>/.rvm/gems/ruby-1.8.7-p352@redmine-1.2/gems/passenger-3.0.9
PassengerRuby /Users/<アカウント名>/.rvm/wrappers/ruby-1.8.7-p352@redmine-1.2/ruby

Apacheの設定

Passengerの設定の追加

/etc/apache2/other/passenger.confを作成し、下記設定を追加します。
設定内容は公式サイトを参考にしました。

# Passengerの基本設定。
# passenger-install-apache2-module --snippet を実行して表示される設定を使用。
# 環境によって設定値が異なりますので以下の3行はそのまま転記しないでください。
#
LoadModule passenger_module /Users/<アカウント名>/.rvm/gems/ruby-1.8.7-p352@redmine-1.2/gems/passenger-3.0.9/ext/apache2/mod_passenger.so
PassengerRoot /Users/<アカウント名>/.rvm/gems/ruby-1.8.7-p352@redmine-1.2/gems/passenger-3.0.9
PassengerRuby /Users/<アカウント名>/.rvm/wrappers/ruby-1.8.7-p352@redmine-1.2/ruby

# Passengerが追加するHTTPヘッダを削除するための設定。
#
Header always unset "X-Powered-By"
Header always unset "X-Rack-Cache"
Header always unset "X-Content-Digest"
Header always unset "X-Runtime"

# 必要に応じてPassengerのチューニングのための設定を追加。
#
PassengerMaxPoolSize 20
PassengerMaxInstancesPerApp 4
PassengerPoolIdleTime 3600
PassengerUseGlobalQueue on
PassengerHighPerformance on
PassengerStatThrottleRate 10
RailsSpawnMethod smart
RailsAppSpawnerIdleTime 86400
RailsFrameworkSpawnerIdleTime 0
バーチャルホストの設定の追加

/etc/apache2/httpd.confを下記のように編集し、バーチャルホストの設定を有効にします。

$ diff httpd.conf httpd.conf.org 
465c465
< Include /private/etc/apache2/extra/httpd-vhosts.conf
---
> #Include /private/etc/apache2/extra/httpd-vhosts.conf

/etc/apache2/extra/httpd-vhosts.confの最後に下記設定を追記します。

<VirtualHost *:80>
    DocumentRoot /Users/<アカウント名>/Sites/redmine/public
    ServerName myredmine.example.com
</VirtualHost>

/etc/hostsにMacIPアドレス(僕の場合は192.168.1.4)を追記します。

192.168.1.4     myredmine.example.com

Apacheの再起動後、ブラウザでhttp://myredmine.example.comにアクセスするとRedmineのトップ画面が表示されました。満足。

あれ、引っ越しのTODOが1つも終わってない…。

Node.js + Socket.IOでチャットアプリを作ってみた

Node.js + Socket.IOを使って簡単なチャットアプリを作ってみました。
ベースは『パーフェクトJavaScript (PERFECT SERIES 4)』の「17章 WebSocket」に出てきたサンプルコードです。
今回はこのサンプルコードをSocket.IOを使って書き直してみました。
ソースコードGitHubにアップしています。

サーバサイド

server.js

var io = require('socket.io').listen(8080);

io.sockets.on('connection', function(socket) {
  console.log('onconnection:', socket);

  // クライアントからのイベント'all'を受信する
  socket.on('all', function(data) {
    // イベント名'msg'で受信メッセージを
    // 自分を含む全クライアントにブロードキャストする
    io.sockets.emit('msg', data);
  });

  // クライアントからのイベント'others'を受信する
  socket.on('others', function(data) {
    // イベント名'msg'で受信メッセージを
    // 自分以外の全クライアントにブロードキャストする
    socket.broadcast.emit('msg', data);
  });

  socket.on('disconnect', function() {
    console.log('disconn');
  });
});

サーバサイドの実装です。
イベント名でメッセージの返信先を切り替えています。
イベント名が'all'であればメッセージ送信者を含む全クライアントにブロードキャスト、'others'であればメッセージ送信者以外の全クライアントにブロードキャストしています。

クライアントサイド

client.js

var socket = io.connect('http://localhost:8080');
var timer;

$(document).ready(function() {
  $('#text').keydown(function(event) {
    // エンターキーで発言をサーバに送信する
    if (event.keyCode === 13) {
      // イベント名'all'でメッセージをサーバに送信する
      socket.emit('all', {
        action: 'post',
        user: $('#user').val(),
        css: $('#css').val(),
        text: $('#text').val()
      });

    // タイピング中というステータスをサーバに送信する
    } else {
      // イベント名'others'でメッセージをサーバに送信する
      socket.emit('others', {
        action: 'typing',
        user: $('#user').val()
      });
    }
  });

  // サーバからのイベント'msg'を受信する
  socket.on('msg', function(data) {
    switch (data.action) {
      case 'post': // 発言の描画
        $('<li></li>').text(data.user + ': ' + data.text)
                      .attr('style', data.css)
                      .appendTo('body');
        break;
      case 'typing': // タイピング中ステータスの描画
        $('#typing').text(data.user + 'さんがタイピング中です...');
        clearTimeout(timer);
        timer = setTimeout(function() { $('#typing').empty(); }, 3000);
        break;
    }
  });
});

クライアントサイド(JavaScript)の実装です。
発言はイベント名'all'で送信しています。
タイピング中ステータスは相手にだけ表示させたいので、イベント名'others'で送信しています。

chat.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>simple chat client powered by socket.io</title>
    <script src="http://localhost:8080/socket.io/socket.io.js"></script>
    <script src="jquery-1.6.4.min.js"></script>
    <script src="client.js"></script>
  </head>
  <body>
    <input id="user">
    <input id="css">
    <input id="text">
    <div id="typing"></div>
  </body>
</html>

クライアントサイド(HTML)の実装です。
シンプルなチャット画面を表示します。

実行

nodeコマンドでサーバを起動させます。

$ node server.js

サーバを起動したら、複数のブラウザでchat.htmlを開きます。
チャット画面から発言を送信すると、Socket.IOサーバ経由で発言がブロードキャストされ、すべてのブラウザのチャット画面に発言が表示されます。

まとめ

Node.js + Socket.IOを使ってみた感想です。

  • サーバサイドでは考慮すべき点がほとんどなかった(クライアントから受け取るイベント名とメッセージの返信先の指定くらい)
  • クライアントサイドでJSONの組み立て方とロジックを変更するだけで、いろいろな機能追加が簡単に実現できた

パーフェクトJavaScript (PERFECT SERIES 4)

パーフェクトJavaScript (PERFECT SERIES 4)

Google+ APIを試してみた(OAuth 2.0編)

前回の続きです。
今回は、OAuth 2.0のアクセストークンを使って公開ストリームを取得するアプリをRailsで作ってみました。
ソースコードGitHubにアップしています。

OAuth 2.0の仕様はこちら。

OAuth - Google+ Platform — Google Developers

OAuth 2.0を利用するには、事前にGoogle APIコンソールにてクライアントID、クライアントシークレット、リダイレクトURIなどを登録しておく必要があります(この辺りを参考)。
登録するリダイレクトURIlocalhostでもOKです。
ただし、ここで登録したURLと実際にアプリでリダイレクトさせるURIが一致しないと、認証APIを実行したときにエラーになるので注意が必要です。
今回はリダイレクトURIを下記のように登録してみました。

localhost:3000/activities/oauth2callback
ビュー

app/views/activities/index.html.erb

<h1>Listing activities</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Published</th>
    <th>URL</th>
  </tr>

<% if @activities.present? && @activities["items"].present? %>
  <% @activities["items"].each do |activity| %>
    <tr>
      <td><%= activity["title"] %></td>
      <td><%= activity["published"] %></td>
      <td><%= activity["url"] %></td>
    </tr>
  <% end %>
<% end %>
</table>

なんの変哲もないビューです。

コントローラ

app/controllers/activities_controller.rb

def index
  # アクセストークン未取得または有効期限切れの場合はOAuth 2.0で認証する
  redirect_to "https://accounts.google.com/o/oauth2/auth?"\
    "client_id=#{CLIENT_ID}&"\
    "redirect_uri=#{REDIRECT_URI}&"\
    "scope=https://www.googleapis.com/auth/plus.me&"\
    "response_type=code" and return if cookies[:access_token].blank?

  # 公開ストリームを取得する
  uri = URI.parse("https://www.googleapis.com/plus/v1/people/me/activities/public?"\
    "access_token=#{cookies[:access_token]}")
  https = Net::HTTP.new(uri.host, 443)
  https.use_ssl = true
  https.verify_mode = OpenSSL::SSL::VERIFY_NONE
  response = https.start do |https|
    request = Net::HTTP::Get.new(uri.path << '?' << uri.query)
    https.request(request)
  end

  @activities = ActiveSupport::JSON.decode(response.body)
end

認証済みの場合は公開ストリーム取得APIを実行しますが、未認証の場合はアクセス許可画面にリダイレクトさせます(認証情報をクッキーに保存しています)。
CLIENT_ID、REDIRECT_URIは、事前登録したクライアントIDとリダイレクトURIです。
注意点は、サーバ側の実装なのでresponse_typeをcodeにすること。

リダイレクトURIのアクションの実装です。

def oauth2callback
  # アクセスを許可しなかった場合はトップ画面にリダイレクトする
  redirect_to url_for(:controller => "tops", :action => "index") and return if params["code"].nil?

  # 認可コードをアクセストークンおよびリフレッシュトークンと交換する
  uri = URI.parse("https://accounts.google.com/o/oauth2/token")
  https = Net::HTTP.new(uri.host, 443)
  https.use_ssl = true
  https.verify_mode = OpenSSL::SSL::VERIFY_NONE
  response = https.start do |https|
    request = Net::HTTP::Post.new(uri.path)
    request.set_form_data({
      :code          => params[:code],
      :client_id     => CLIENT_ID,
      :client_secret => CLIENT_SECRET,
      :redirect_uri  => REDIRECT_URI,
      :grant_type    => "authorization_code"
    }, "&")
    https.request(request)
  end

  response_hash = ActiveSupport::JSON.decode(response.body)
  cookies[:access_token] = {
    :value => response_hash["access_token"],
    :expires => response_hash["expires_in"].seconds.from_now
  }
  @activity = Activity.new(:access_token => response_hash["access_token"])
  @activity.save

  redirect_to url_for(:activities)
end

アクセス許可画面にてアクセスを許可した場合は認可コードがリクエストパラメータとして渡されるので、これをアクセストークンおよびリフレッシュトークンと交換します。
取得したアクセストークンはクッキーなりDBなりに保存しておきましょう。

動作確認

http://localhost:3000/activitiesにアクセスしてみます。
すると、まだ認証していないのでアクセス許可画面にリダイレクトされます(Twitterなどでもおなじみの画面です)。

ここで「アクセスを許可」を選択すると、まずhttp://localhost:3000/activities/oauth2callbackにリダイレクトされて認証したあと、http://localhost:3000/activitiesにリダイレクトされます。
今度は認証済みなので、公開ストリームを取得できるはず。

おー、できました。
Google+を使っていないことがバレバレの公開ストリームですが、気にしないでください。

なお、アクセストークンの有効期限は1時間なので、1時間以内であれば再認証は不要です。

Google+ APIを試してみた

Google+には無関心の態を装いながらも、実は内心興味津々で、「誰か招待してくれないかなー」などと思いながら毎日を過ごしていましたが、最近になってようやく招待してもらうことができ、Google+を使いはじめることができました。

そんなわけで、早速最近公開されたばかりのGoogle+ APIを試してみました。

Google+ APIの認証方式には、APIキーによる認証方式とOAuth 2.0による認証方式の2種類があります。
今回は、OAuth 2.0について調べるのが面倒くさいのでAPIキーによる認証方式を試してみました。まだAPIキーを持っていない人は、この辺りを参考にしてAPIキーを取得してください。

言語はRubyです。
専用のライブラリもあるみたいですが、使い方がわからないので華麗にスルー(笑)。
どうせHTTPSでリクエストを送るだけなので、net/httpsを使って実装しました。

下記がソースコードです。

get_profile.rb

#!/usr/bin/ruby 
require 'net/https'

USER_ID = '102346903570321430420'
API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'    #  取得したAPIキー

uri = URI.parse("https://www.googleapis.com/plus/v1/people/#{USER_ID}?key=#{API_KEY}")

https = Net::HTTP.new(uri.host, 443)
https.use_ssl = true
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
response = https.start do |https|
  request = Net::HTTP::Get.new(uri.path << '?' << uri.query)
  https.request(request)
end

puts response.body

USER_IDで指定したユーザのプロフィールを取得します。

実行してみます。

$ ruby get_profile.rb 
{
 "kind": "plus#person",
 "id": "102346903570321430420",
 "displayName": "Yuta NAGAMIYA",
 "gender": "male",
 "aboutMe": "Ruby好きのプログラマです。",
 "url": "https://plus.google.com/102346903570321430420",
 "image": {
  "url": "https://lh3.googleusercontent.com/-GijUqtWTz08/AAAAAAAAAAI/AAAAAAAAACY/A-cPZmjZRUQ/photo.jpg"
 },
 "urls": [
  {
   "value": "http://twitter.com/ngmy"
  },
  {
   "value": "https://plus.google.com/102346903570321430420",
   "type": "profile"
  },
  {
   "value": "https://www.googleapis.com/plus/v1/people/102346903570321430420",
   "type": "json"
  }
 ],
 "placesLived": [
  {
   "value": "群馬県前橋市"
  },
  {
   "value": "神奈川県横浜市",
   "primary": true
  }
 ]
}

プロフィールを取得できました。簡単!

次回はOAuth 2.0を使った方法を試してみたいと思います。