2011年12月28日水曜日

nginxとunicorn上でrails3.1アプリを動かす。ついでにcapistranoを使ってデプロイ

かなりハマったので、メモ。

まずアプリ側。
Gemfileに以下を追加。

group :deployment do
gem 'capistrano'
gem 'capistrano_colors'
end

gem 'therubyracer'
gem 'unicorn'

で、bundle install。

次にcapistrano設定ファイルを作成
# capify .

#config/deploy.rb

# capistranoの出力がカラーになる
require 'capistrano_colors'

# cap deploy時に自動で bundle install が実行される
require "bundler/capistrano"

#rvm setting
set :rvm_type, :user
$:.unshift(File.expand_path('./lib', ENV['rvm_path']))
require "rvm/capistrano"
set :rvm_ruby_string, '1.9.2@rails3.1' #ここにgemset名を入力

set :user, "サーバーのユーザー名"
set :port, 22 #サーバーのポート番号
set :use_sudo, false #sudoをするかどうか。
ssh_options[:forward_agent] = true

#repository setting
set :application, "sample" #アプリケーション名
set :scm, :git #gitを使う
set :repository, "ssh://user@example.com:22/home/user/git/sample.git"
set :deploy_to, "/home/user/sample/"
default_environment["LD_LIBRARY_PATH"] = "$LD_LIBRARY_PATH:/usr/local/lib"



# Or: `accurev`, `bzr`, `cvs`, `darcs`, `git`, `mercurial`, `perforce`, `subversion` or `none`

role :web, "example.com" # Your HTTP server, Apache/etc
role :app, "example.com" # This may be the same as your `Web` server
role :db, "example.com", :primary => true # This is where Rails migrations will run

#sqlite3を使う場合、dbをshareフォルダに入れる。
task :db_setup, :roles => [:db] do
run "mkdir -p -m 775 #{shared_path}/db"
end

namespace :deploy do
task :start, :roles => :app do
run "cd #{current_path}; bundle exec unicorn_rails -c config/unicorn.rb -E production -D"
end
task :restart, :roles => :app do
if File.exist? "/tmp/unicorn.pid"
run "kill -s USR2 `cat /tmp/unicorn.pid`"
end
end
task :stop, :roles => :app do
run "kill -s QUIT `cat /tmp/unicorn.pid`"
end
end

namespace :assets do
task :precompile, :roles => :web do
run "cd #{current_path} && RAILS_ENV=production bundle exec rake assets:precompile"
end
task :cleanup, :roles => :web do
run "cd #{current_path} && RAILS_ENV=production bundle exec rake assets:clean"
end
end
after :deploy, "assets:precompile" #デプロイ後にassets compileをするように。
set :normalize_asset_timestamps, false #rails3.1対策


次にunicornの設定
#config/unicorn.rb

application = 'sample'

# ワーカーの数
worker_processes 2

# ソケット
listen "/tmp/unicorn.sock"
pid "/tmp/unicorn.pid"

# ログ
if ENV['RAILS_ENV'] == 'production'
shared_path = "/home/user/#{application}/shared"
stderr_path = "#{shared_path}/log/unicorn.stderr.log"
stdout_path = "#{shared_path}/log/unicorn.stdout.log"
end

# ダウンタイムなくす
preload_app true

before_fork do |server, worker|
if defined?(ActiveRecord::Base)
ActiveRecord::Base.connection.disconnect!
end
old_pid = "/tmp/unicorn.pid.oldbin"
if File.exists?(old_pid) && server.pid != old_pid
begin
Process.kill("QUIT", File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
end

after_fork do |server, worker|
if defined?(ActiveRecord::Base)
ActiveRecord::Base.establish_connection
end
end


environmentsのproduction内で
config.serve_static_assets = true
にする。
※こうしないとcssがロードされない。

あとdatabase.ymlのproductionを
database: ../../shared/db/production.sqlite3
にする。



次はサーバ側。
nginxをyumでインストールするために、repoに追加。
# sudo vim /etc/yum.repo.d/nginx.repo


[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/rhel/$releasever/$basearch/
gpgcheck=0
enabled=1


で、インストール。
#sudo yum install nginx

nginxの設定ファイルはこんな感じ。
#sudo vim /etc/nginx/conf.d/sample.conf


upstream unicorn {
server unix:/tmp/unicorn.sock;
}

server {
listen 80;
server_name example.com;

root /home/user/sample/current/public;
error_log /home/user/sample/current/log/error.log;

location / {
if (-f $request_filename) { break; }
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://unicorn;
}

}


あとは、capistranoでデプロイして、nginxを起動するだけ。必要があればchkconfigで自動起動設定をする。
capistranoでデプロイするときは、
#cap deploy:setup
#cap db_setup
#cap deploy:cold
の順で。


参考:
http://d.hatena.ne.jp/ntaku/20111112/1321093327
http://aerial.st/archive/2011/06/16/nginx-unicorn-rails-on-mac/

2011年12月8日木曜日

jQuery UIを使って、オートコンプリート機能を実装してみる@rails3.1

facebookなどの検索BOXで途中まで入力すれば、結果の一部が出てくるあれです。

rails3.1からjQueryがデフォルトになったのですが、jQuery UIはまだ有効になっていないので有効にする。

application.jsの//= require jqueryの下あたりに以下を追記
//= require jquery-ui

で、ビューに検索BOXをJSで作る。
  1. <input type="text" id="textbox1"></input>  
  2. <script type="text/javascript" charset="utf-8">  
  3.   $(function(){  
  4.     $("#textbox1").autocomplete({  
  5.       source : "/auto_complete"  
  6.     });  
  7.   })  
  8. </script>  



sourceの部分をルーティングで設定する。
get 'auto_complete' => 'api#auto_complete'

コントローラーはこんな感じで
  1. def auto_complete  
  2.   if request.xhr?  
  3.     data = Array.new  
  4.     data_items = Data.where('name like ?'"%#{params[:term]}%")  
  5.     data_items.each do |f|  
  6.       data << f.name  
  7.     end  
  8.     return render data  
  9.   end  
  10. end  



検索BOXで入力したキーワードは都度、sourceで設定したアドレスに対して、params[:term]で送られます。
で、結果をjsonで返せば動作するのですが、そのまま返すと不必要なデータもそのまま送ってしまうので、必要なカラムのデータだけ送るよう、配列を作りなおしてます。
もう少しうまいやり方もありそうですが。。。
あと、request.xhr?と指定すると、ajax以外からのアクセスを弾いてくれるようです。

ちなみにCSSを使う場合には、assetsのcssフォルダ下にjquery-ui-1.8.16.custom.cssをおいて、jsを呼び出すのと同じように
*= require_jquery-ui-1.8.16.custom
をかけばOK
画像は、assets/image/jquery-uiの下にimageフォルダごとコピーすればOK。

参考:http://d.hatena.ne.jp/naoty_k/20110925/1316969446
http://blog.livedoor.jp/satoyansoft/archives/65458957.html

Railsでapiっぽいのを作って、iOSアプリと連携してみる

iOS(iphone)アプリで位置情報を取得、それをrailsアプリに送信してDBに登録するようにしてみます。
なおiOSについてはまだまだ勉強不足のため、「まるごと学ぶiPhoneアプリ制作教室」内に記載してあったコードを参考にしています。
またrailsアプリ内には位置情報の取得まで作っておきますが、iOS部分では省きます。

まずはiOSアプリの方。
ViewController.m
  1. #import "ViewController.h"  
  2. #import "Location.h"  
  3. #import "JSON.h"  
  4.   
  5. @implementation ViewController  
  6. @synthesize codeTextField;  
  7.   
  8. - (void)didReceiveMemoryWarning  
  9. {  
  10.     [super didReceiveMemoryWarning];  
  11.     // Release any cached data, images, etc that aren't in use.  
  12. }  
  13.   
  14. #pragma mark - View lifecycle  
  15.   
  16. - (void)viewDidLoad  
  17. {  
  18.     [super viewDidLoad];  
  19.  // Do any additional setup after loading the view, typically from a nib.  
  20. }  
  21.   
  22. - (void)viewDidUnload  
  23. {  
  24.     [self setCodeTextField:nil];  
  25.     [super viewDidUnload];  
  26.     // Release any retained subviews of the main view.  
  27.     // e.g. self.myOutlet = nil;  
  28. }  
  29.   
  30. - (void)viewWillAppear:(BOOL)animated  
  31. {  
  32.     [super viewWillAppear:animated];  
  33. }  
  34.   
  35. - (void)viewDidAppear:(BOOL)animated  
  36. {  
  37.     [super viewDidAppear:animated];  
  38. }  
  39.   
  40. - (void)viewWillDisappear:(BOOL)animated  
  41. {  
  42.  [super viewWillDisappear:animated];  
  43. }  
  44.   
  45. - (void)viewDidDisappear:(BOOL)animated  
  46. {  
  47.  [super viewDidDisappear:animated];  
  48. }  
  49.   
  50. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation  
  51. {  
  52.     // Return YES for supported orientations  
  53.     return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);  
  54. }  
  55.   
  56. - (NSString *)getCurrentDate {  
  57.     NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];  
  58.     NSString *dateFormat = @"yyyy/MM/dd-mm:ss:SSS";  
  59.     [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"JST"]];  
  60.     [dateFormatter setDateFormat:dateFormat];  
  61.     NSString *date = [dateFormatter stringFromDate:[NSDate date]];  
  62.     return date;  
  63. }  
  64.   
  65. - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {  
  66.       
  67.       
  68.     Location *myLocation = [[[Location alloc] init] autorelease];  
  69.     myLocation.latitude = [NSString stringWithFormat:@"%f", newLocation.coordinate.latitude];  
  70.     myLocation.longitude = [NSString stringWithFormat:@"%f", newLocation.coordinate.longitude];  
  71.     myLocation.time = [self getCurrentDate];  
  72.     myLocation.identificationCode = [codeTextField text];  
  73.       
  74.     NSDictionary *locationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:  
  75.                                         myLocation.latitude, @"latitude",  
  76.                                         myLocation.longitude, @"longitude",  
  77.                                         myLocation.time, @"time",  
  78.                                         myLocation.identificationCode, @"identificationCode",  
  79.                                         nil];  
  80.     NSString* jsonString = [locationDictionary JSONRepresentation];  
  81.     NSLog(@"JSON: %@", jsonString);  
  82.       
  83.     NSURL *serviceURL = [NSURL URLWithString:@"http://0.0.0.0:3000/location.json"];  
  84.     NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:serviceURL];  
  85.     [req setHTTPMethod:@"POST"];  
  86.     [req addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];  
  87.     [req setHTTPBody:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];  
  88.       
  89.     NSURLResponse *resp= nil;  
  90.     NSError *error= nil;  
  91.     NSData *result = [NSURLConnection sendSynchronousRequest:req returningResponse:&resp error:&error];  
  92.       
  93.     if (error) {  
  94.         NSLog(@"error!");  
  95.     } else {  
  96.         NSLog(@"Result:%@", result);  
  97.     }  
  98.       
  99. }  
  100. - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{  
  101. }  
  102. - (IBAction)logStartButton:(id)sender {  
  103.     if (locationManager == nil) {  
  104.         locationManager = [[CLLocationManager alloc] init];  
  105.     }  
  106.     locationManager.delegate = self;  
  107.     [locationManager startUpdatingLocation];  
  108. }  
  109. - (void)dealloc {  
  110.     [codeTextField release];  
  111.     [super dealloc];  
  112. }  
  113. @end  


ViewController.h
  1. #import <UIKit/UIKit.h>  
  2. #import "CoreLocation/CoreLocation.h"  
  3.   
  4. @interface ViewController : UIViewController <CLLocationManagerDelegate> {  
  5. @private  
  6.     UITextField *codeTextField;  
  7.     UIButton *logStartButton;  
  8.     CLLocationManager *locationManager;  
  9. }  
  10. @property (retain, nonatomic) IBOutlet UITextField *codeTextField;  
  11. - (IBAction)logStartButton:(id)sender;  
  12.   
  13. @end  


Location.h
  1. #import <Foundation/Foundation.h>  
  2.   
  3. @interface Location : NSObject {  
  4.     NSString *latitude;//緯度  
  5.     NSString *longitude;//経度  
  6.     NSString *time;//時間  
  7.     NSString *identificationCode;//自分を特定するためのIDコード  
  8. }  
  9. @property (nonatomic, assign) NSString *latitude;  
  10. @property (nonatomic, assign) NSString *longitude;  
  11. @property (nonatomic, assign) NSString *time;  
  12. @property (nonatomic, assign) NSString *identificationCode;  
  13.   
  14. @end  

  1. #import "Location.h"  
  2.   
  3. @implementation Location  
  4. @synthesize latitude;  
  5. @synthesize longitude;  
  6. @synthesize time;  
  7. @synthesize identificationCode;  
  8.   
  9. @end  


Location.m
  1. #import "Location.h"  
  2.   
  3. @implementation Location  
  4. @synthesize latitude;  
  5. @synthesize longitude;  
  6. @synthesize time;  
  7. @synthesize identificationCode;  
  8.   
  9. @end  

CoreLocationライブラリは別途入れてください。
またここから「JSON v2.3.2 (iOS)」をダウンロードして、その中からClassesフォルダを同じプロジェクトファイル内にコピーしておいてください。
なおserviceURL = [NSURL URLWithString:@"http://0.0.0.0:3000/location.json"]のドメイン部分は自分なりに。
xibも適当にボタンとテキストフィールドを。名称は、それぞれ「logStartButton」「codeTextField」で。
わからないときは、「まるごと学ぶiPhoneアプリ制作教室」を参考にしてください。
サンプルコードがここにあったりします。

次にrailsの方。
ApiController
  1. class ApiController < ApplicationController  
  2.   
  3.   def post  
  4.     location = Location.new(params[:api])  
  5.     respond_to do |format|  
  6.       if location.save  
  7.         format.json { head :ok }  
  8.       else  
  9.         format.json { render json: location.errors, status: :unprocessable_entity }  
  10.       end  
  11.     end  
  12.   end  
  13.   
  14.   def get  
  15.     @location = Location.where(:identificationCode => params[:identificationCode]).limit(5)  
  16.   
  17.     respond_to do |format|  
  18.       format.json { render json: @location }  
  19.     end  
  20.   end  
  21.   
  22. end  

ルーティングとして以下を追加。
  1. post 'location(.:format)' => 'api#post'  
  2. get 'location(.:format)' => 'api#get'  

データベースに以下のカラムを作る
「latitude」
「longitude」
「time」
「identificationCode」
で、マイグレして起動して、iPhoneアプリを起動し、ボタンを押せば、1秒ごとにrailsのDBに位置情報とID、時間が登録されていくはずです。
ポイントは、jsonデータをpostメソッドで送信するとそれぞれのparams[:項目名]で取得できること。
それができれば簡単ではないかと。

今回簡易的にするため外のpostメソッドをそのまま受け入れましたが、セキュリティ的には問題あるので、実用にはもうちょい工夫が必要そうです。