yamotonalds's blog

Webアプリケーション開発における技術メモが中心です。たまにWebサービス、興味を持ったデバイス、自作PCに関する話題もあるかも。Amazon好きなのでAmazon.co.jpアソシエイト使ってます。

i18n-jsを使っているRailsアプリケーションがHerokuにpush失敗する

Railsアプリケーションでi18n-jsを使ってみたところ、Herokuにpushする際にエラーが発生した。
ログを見たところ rake assets:precompile に失敗している模様。

発生したバージョンは以下の通り。

ちゃんと解決しているのかは不明だが、次のGitHub Issueに書いてあるようにすると今のところうまくいっている。

Usage w/ asset pipeline + Heroku possible? · Issue #81 · fnando/i18n-js · GitHub

具体的には、
ターミナルで以下のコマンドを実行。

heroku labs:enable user-env-compile

config/application.rbに以下を追加。

config.assets.initialize_on_precompile = true

.gitignoreに以下を追加。

/public/javascripts/translations.js

502 Bad Gatewayがセッションクリアで直った

よく見るサイトがある日「502 Bad Gateway」が発生するようになって見れなくなった。
しばらく時間をおいたりリロードしたりしても効果無しだったが、セッションクリアすると見れるようになった(おそらくブラウザ再起動でもOK)。

Sticky Session使ってて割り振られてたサーバが無くなったのだろうか。
だとしたらブラウザが長時間起動しっぱなしだったのが悪かったか(基本的にPCを使わないときはスタンバイ(スリープ)にしているので)。

LIAN LIのV358でPC組んでみた

展開型キューブケース、LIAN LI PC-V358シリーズでPCを組んだのでその感想など。

経緯

Windows PCを買い替えにあたり、次のはコンパクトなキューブPCにすることにした。 ただ、自作は普通のケースの物を数回した程度なのであまり中が狭いとリスクが高く、3Dゲームをする可能性もあったので、それなりのグラフィックボードが積めてメンテナンス性の高いケースを探した。 で、見つけたのがこれ。

PC-V358 Series | PCケース&パーツ、オーディオのパイオニア-株式会社ディラック

コンパクトではないけれど展開ギミックに興味をそそられた。

組み立てを終えて

良かった点
  • 見た目がシンプル
  • サイドパネルはネジ無しで開閉可能
  • 電源やHDDが下配置なので重心が安定
  • マザボが上配置かつ前面が展開するのでいじりやすい
  • 12cmの前面FAN2、背面FAN1が標準搭載
  • 33cmまでのグラボが載せれたり、3.5inchベイ * 6、2.5inchベイ * 2、と拡張性が高い
悪かった点
  • 前面からのびてるケーブルが短くてマザボにギリギリ届くのと届かないのがある(USB3.0ケーブルは挿すと引っ張られてちょっと斜めになる。電源ボタンケーブルは3,4cm届かない)
  • FANのケーブルも短め(4ピンへの変換ケーブルが付属してるからそっち使うのが前提かも)
  • 上下の仕切りに開いてるケーブル用の穴の位置が微妙(下に3.5inchベイと電源があるところに開いてる)
注意点
  • 前面展開時、光学ドライブのケーブルを40cmくらいのにすれば抜く必要は無いが電源ボタンケーブルは完全に届かないので抜いてから展開する必要がある。
  • キューブケースとはいえ結構な大きさなので机の上には置けない可能性が高い。
  • RESETボタンはない
  • 光学ドライブベイがスリムタイプ(SlimlineSATAケーブル等を別途購入する必要がある)

総評(感想)

パーツ取り付けは非常にしやすかった。見た目も良い。 サイズは小さいとは言い難いので確認漏れの無いように。 折角前面展開できるのだからケーブルに余裕が欲しかった。 付属FANはすべて下配置なのでグラボを増設するなら側面FANも着けるべきだと思われる。

LIAN LI Micro-ATXケース(電源なし) ブラック PC-V358B

LIAN LI Micro-ATXケース(電源なし) ブラック PC-V358B

送信したメールの内容が確認できるLetter Openerが便利

メール送信が必要な機能を実装しているときに Letter Opener というライブラリを見つけた。

今までは開発中にメール送信が行われると自分個人のGmailから自分のメールアドレスに送信されるようにSMTP設定等を設定していたが、Letter Openerを使うと送信されたメールの内容をブラウザ上で確認できる。

セットアップ手順はLetter OpenerのREADMEに書かれている通りすごく簡単なので割愛。 設定後にローカル開発環境のブラウザで操作中にメール送信処理が走ると別タブでメールの内容が表示されるようになる。

HTMLとテキストを含むマルチパートメールの場合も右上のリンクから簡単に切り替えることができて便利。

開発環境で直接ブラウザを使っていないといけないのでVMを使って開発している場合やリモート開発の場合は使えないがローカルだけで開発している場合には使ってみる価値はあると思う。

ActiveMerchantでPayPalの定期支払い

PayPalの定期支払い(Recurring Payments)を実装したのだけれど嵌って大変だったのでメモ。

前提条件

  • activemerchant (1.34.0)

処理の流れ

公式ページを見るのが確実だった。

Integrating Recurring Payments | PayPal Developer

APIの呼び出し順としては、

  1. SetExpressCheckout
  2. GetExpressCheckoutDetails
  3. DoExpressCheckoutPayment
  4. CreateRecurringPaymentsProfile

となる。これをActiveMerchantのメソッドに置き換えると、

  1. setup_authorization
  2. details_for
  3. purchase
  4. recurring

となる。通常の決済では setup_purchase だったのが setup_authorization に変わっている点に注意。 また、パラメータも通常の決済とは異なる。

setup_response = gateway.setup_authorization(product.price * 100,
  ip: request.remote_ip,
  return_url: return_url,
  cancel_return_url: cancel_return_url,
  allow_note: false,
  no_shipping: true,
  items: [{
    name: product.name,
    number: product.id,
    quantity: 1,
    amount: product.price * 100,
  }],
  custom: product.id,
  billing_agreement: {
    type: "RecurringPayments",
    description: "商品名・商品の説明等"
  }
)

billing_agreementcustom というパラメータが増えている。customの方は後述のprofile_idと共に商品IDをDBに保存していれば不要。

recurring_response = gateway.recurring(product.price * 100, nil, {
    token: token,
    period: 'Day',
    frequency: 1,
    start_date: Time.zone.now + 1.days,
    description: '商品名・商品の説明等',
    initial_amount: product.price * 100,
    currency: "JPY",
    auto_bill_outstanding: true
})

tokenはsetup時に取得できるToken。初回支払いはpurchaseで行うのでstart_dateperiodに合わせてずらしている。descriptionは前述のbilling_agreementdescriptionと一致させなければならないので注意。 auto_bill_outstandingをtrueにすることで無期限の定期支払いにできる。

recurringが成功すると

recurring_response.params['profile_id']

でprofile_idを取得できる。この後説明するIPNではprofile_idを頼りにどの定期支払いに関する情報か判断するためDBに保存しておく。

定期支払いされた通知を受け取る

Profileを作成すると定期支払いが行われるようになるが、支払いが行われたことを知るための仕組みとしてIPNというものが用意されている。

PayPal側の設定

IPNを有効にするにはPayPalにログインして、「マイアカウント」→「個人設定」→「販売の設定」→「即時支払い通知の設定」をクリック。 通知先URLの入力と有効化のラジオボタンを選択する。これで支払い時にここで入力したURLに情報がPOSTされるようになる。

デフォルトではPOSTされるデータのエンコードがShift-JISになるようなのでUTF-8に変更しておく。設定場所は、「マイアカウント」→「個人設定」→「販売の設定」→「言語のエンコード」から「詳細オプション」をクリック。プルダウンを両方とも「UTF-8」にして保存する。

PayPalのSandboxで試す場合、Sandboxのアカウントでも同様に設定するのを忘れないこと。

通知が届いた後の処理

通知が届いたときの処理はActiveMerchantのドキュメントがほぼそのまま使える。

Class: ActiveMerchant::Billing::Integrations::Paypal::Notification

この例の

notify.item_id

で前述したcustomパラメータの値が、

notify.params['recurring_payment_id']

でprofile_idが取得できる。これらの値から支払いを特定し、購入処理を行う。 なお、定期支払い以外の支払い情報もIPNで通知されるようになっているので

notify.type == 'recurring_payment'

等で判定して処理を行う必要がある。

とりあえずこれで定期支払いは実装できたがWebで見つかるActiveMerchantの情報は古かったりpaypal_recurringというgemの情報だったりで混乱した(ただしpaypal_recurring gemのソースコードAPIのパラメータ周りで非常に参考になった)。 数ヶ月後の自分を含めた誰かの参考になるといいなぁ。

おまけ(IPN Simulator)

PayPalの開発者サイトにIPNのシミュレータがある。ただ定期支払いのIPNには対応していなかったりパラメータの自由度が低かったりするので参考程度に。

Payment Notifications | PayPal Developer

なお、このシミュレータの IPN handler URL に指定するURLは80番ポートでないとIPN送信が成功しないので注意が必要。

Railsで開発時のサーバのポートはpオプションで指定可能だが、Well-Knownポートを指定する場合はsudoが必要だった。 自分の場合はbundlerやrvmを使っていたので、

rvmsudo bundle exec rails server -p 80

というコマンドでサーバを起動する必要があった。

ElasticBeanstalkで大きめのデータをPOSTするとInternalServerError(500)が発生する

AWSのElastic BeanstalkでRubyのアプリケーションを動かしていたところ、ある程度大きなデータ(リクエストのContent-Lengthが大きい)をPOSTするとInternal Server Error (HTTPエラーコード 500)になる不具合が発生した。

アプリケーションログにはエラーはおろかアクセスすら記録されていないし何事かと思ってたらPassengerの方のログにエラーが記録されていた。

# /var/app/support/logs/passenger.log
2013/xx/xx xx:xx:xx [crit] 1745#0: *170726 open() "/tmp/passenger-standalone.1667/client_body_temp/0000000003" failed (2: No such file or directory), client: xxx.xxx.xxx.xxx, server: _, request: "POST /xxx/xxx HTTP/1.1", host: "www.example.com", referrer: "http://www.example.com/xxx"

Passengerのことはまだよく知らないのだけれどどうやら大きなデータをPOSTされたときに作られる一時ファイルの読み込みに失敗しているようだ。
サーバにログインして /tmp の中を見たけれど passenger.x.x.xxxxx というディレクトリはあったが確かに passenger-standalone.xxx というディレクトリは無い。
エラーメッセージで検索かけて見つかったのが次のページ。

Issue Uploading Files from Rails app hosted on Elastic Beanstalk - Stack Overflow

なるほど。cronで /tmp の中を掃除するときにPassengerに必要なファイルまで消してしまっているということか。
BeanstalkのWebコンソールからアプリケーションをRestartさせたら /tmp に passenger-standalone.xxx ディレクトリができていた。
これがtmpwatchで消されているようだ(親ディレクトリごと削除されていることを考えると上記のPassengerのエラーは読み込みではなく書き込み時のエラーかも)。

解決方法

本来はAmazon側で解決すべき問題だけどそれを待ってもいられない。
上記のページにあるように /etc/cron.daily/tmpwatch で /tmp 内のPassenger関連ディレクトリを除外するようにした。

/etc/cron.daily/tmpwatch
#! /bin/sh
flags=-umc
/usr/sbin/tmpwatch "$flags" -x /tmp/.X11-unix -x /tmp/.XIM-unix \
        -x /tmp/.font-unix -x /tmp/.ICE-unix -x /tmp/.Test-unix \
        -X '/tmp/hsperfdata_*' -X '/tmp/passenger*' 10d /tmp
/usr/sbin/tmpwatch "$flags" 30d /var/tmp
for d in /var/{cache/man,catman}/{cat?,X11R6/cat?,local/cat?}; do
    if [ -d "$d" ]; then
        /usr/sbin/tmpwatch "$flags" -f 30d "$d"
    fi
done

ただ、今のインスタンスを直接いじってもインスタンスが変わったときに戻ってしまう。
なので「Custom AMIが必要か…」と思っていたら .ebextensions 以下の設定ファイルで対応可能だった。

EC2 インスタンス上のソフトウェアのカスタマイズ - AWS Elastic Beanstalk

アプリケーションのソースコードに以下のファイルを追加。

# .ebextensions/overwrite_cron_tmpwatch.config
files:
  "/etc/cron.daily/tmpwatch":
    mode: "000755"
    owner: root
    group: root
    content: |+
      #! /bin/sh
      flags=-umc
      /usr/sbin/tmpwatch "$flags" -x /tmp/.X11-unix -x /tmp/.XIM-unix \
        -x /tmp/.font-unix -x /tmp/.ICE-unix -x /tmp/.Test-unix \
        -X '/tmp/hsperfdata_*' -X '/tmp/passenger*' 10d /tmp
      /usr/sbin/tmpwatch "$flags" 30d /var/tmp
      for d in /var/{cache/man,catman}/{cat?,X11R6/cat?,local/cat?}; do
          if [ -d "$d" ]; then
        /usr/sbin/tmpwatch "$flags" -f 30d "$d"
      fi
      done

commands:
  delete_backup_file:
    command: "rm /etc/cron.daily/tmpwatch.bak"
    ignoreErrors: true

rmしているのはfilesでファイルを作成するときに元のファイルがバックアップされるため。

これでなんとかなったけど、大きなデータかつデプロイ直後には発生しない不具合とかわかりにくいので勘弁願いたい。

OmniauthでTwitter認証(OAuth認証)

Twitterによる外部認証をやってみることにした。

Deviseを使っているのでOmniauthで楽にできそう。

以下のページを参考にした。

 

OmniAuth: Overview · plataformatec/devise Wiki · GitHub

ASCIIcasts - “Episode 235 - OmniAuth Part 1”

ASCIIcasts - “Episode 236 - OmniAuth Part 2”

Ruby - deviseでfacebook,twitter認証 - Qiita [キータ]

 

内容の古い部分があったりこちらの要件に合わない部分があったりしたので少し変更しながら実装した。

前提

  • rails 3.2.13
  • devise 2.2.4
  • omniauth 1.1.4
  • omniauth-twitter 1.0.0

既にDeviseでユーザー認証をしているところに外部認証を追加してゆく。

Twitter Developersにアプリケーションを登録する

まず Twitter Developers にログインし、右上のアイコンメニューから My  applications を選択する。

Create a new application を選択し、各項目を入力する。後から変更できるのでサクッと登録してしまって良い。Callback URL にはコールバックされないので本番のURLにしていても良い。

アプリケーションが登録できたら Details タブの OAuth settings に表示されている Consumer keyConsumer secret をメモする。

また、 Settings タブの Application Type にある Allow this application to be used to Sign in with Twitter にチェックを入れる。これにチェックを入れていないと /oauth/authenticate の代わりに /oauth/authorize に飛ばされるため、ログインの度に毎回アプリケーションにアクセスを許可するかどうか聞かれることになる。

 omniauth-twitterをインストールする

Twitterアプリケーションの登録と設定変更が完了したら後はソースコードを書くだけ。

まずはomniauth-twitterをインストールする。

# Gemfile
# ...
gem 'omniauth-twitter'
# ...

omniauthは依存関係で自動的にインストールされるので記述するのはomniauth-twitterだけでOK。Facebook等、他の認証プロバイダを使用する場合は対応するomniauth-○○を記述する。

インストールコマンドを実行してインストールは完了。

>bundle install

 Omniauthの設定

次にTwitterにアクセスするため config/initializers/devise.rb に以下の記述を追加。

config.omniauth :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']

以降、サーバー・テスト実行時には環境変数TWITTER_KEY, TWITTER_SECRETにTwitterアプリケーション作成時にメモしたConsumer key,Consumer secretをセットしておくこと。

Userクラスに以下の記述を追加。

devise :omniauthable, :omniauth_providers => [:twitter]

 Controllerの作成とRouting設定

 次にControllerを作成する。Deviseには Devise::OmniauthCallbacksController というクラスが既にあるが、外部認証完了時にいろいろ処理を行うためサブクラスを作成した。

# app/controllers/mydevise/omniauth_callbacks_controller.rb
class Mydevise::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def twitter
    render text: request.env["omniauth.auth"]
  end
end

 ルーティング設定も変更する。

# config/routes.rb
devise_for :users, controllers: { omniauth_callbacks: "mydevise/omniauth_callbacks" }

 Twitter認証のためのリンクは

<%= link_to "Sign in with Twitter", user_omniauth_authorize_path(:twitter) %>

 で作れるけど app/views/devise/shared/_links.erb を使っていればSign in/upの画面に自動的にリンクが表示される。

ここまでできれば実際にTwitter認証をしてみると画面に認証によって得られたデータが表示されるはず。

認証データを保持するモデルを作成する

次に認証データの保存先となるモデルを作成する。Userモデルにカラムを追加してる例もあるけれど、一人のユーザーが複数の認証プロバイダを使用できるようにするためと小さく分割できるところは分割した方が管理が楽だと思ったのでRailscasts(ASCIIcasts)と同様に認証モデルを作ることにした。

>rails g model authentication provider:string uid:string user_id:integer
# app/models/authentication.rb
class Authentication < ActiveRecord::Base
  belongs_to :user
  attr_accessible :provider, :uid, :user

  validates :provider, presence: true, uniqueness: { scope: :uid }, length: { maximum: 255 }
  validates :uid, presence: true, length: { maximum: 255 }
  validates :user, presence: true
end
# app/models/user.rb
class User < ActiveRecord::Base
  has_many :authentications, dependent: :destroy, autosave: true
  # ...
end

仕上げ

後はOmniauthCallbacksController側で認証モデルを保存したり保存された認証モデルからユーザーモデルをロードしてログインさせたりするだけ。

# app/controllers/mydevise/omniauth_callbacks_controller.rb
class Mydevise::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def twitter
    omniauth = request.env["omniauth.auth"]
    auth = Authentication.find_by_provider_and_uid(omniauth["provider"], omniauth["uid"])

    if auth
      sign_in_and_redirect auth.user, :event => :authentication
      set_flash_message(:notice, :success, :kind => "Twitter") if is_navigational_format?
    elsif current_user
      current_user.authentications.create!(provider: omniauth["provider"], uid: omniauth["uid"])
      redirect_to root_url
      set_flash_message(:notice, :success, :kind => "Twitter") if is_navigational_format?
    else
      session[:omniauth] = omniauth.except('extra')
      redirect_to new_user_registration_url
      set_flash_message(:notice, :success, :kind => "Twitter") if is_navigational_format?
      flash[:notice] += "続けて以下の項目を入力してください。"
    end
  end
end

Railscasts(ASCIIcasts)にもあるけれど注意するのは新規ユーザー を作成する場合。Twitter認証ではユーザーのメールアドレスが取得できないようなのでユーザーを作成する代わりにセッションに認証データを保持しておいてユーザーの新規登録画面にリダイレクトしている。

新規登録画面側ではセッションから認証データを取得してモデルを構築する。

# config/routes.rb
devise_for :users, controllers: { 
  omniauth_callbacks: "mydevise/omniauth_callbacks",
  registrations: "mydevise/registrations"
}
# app/controllers/mydevise/registrations_controller.rb
class Mydevise::RegistrationsController < Devise::RegistrationsController
  def create
    super
    session[:omniauth] = nil unless @user.new_record?
  end

  private
  def build_resource(*args)
    super
    if session[:omniauth]
      @user.apply_omniauth(session[:omniauth])
    end
  end
end

 apply_omniauth、view、password_required?に関してはRailscasts(ASCIIcasts)通りなので割愛。build_resourceでのバリデーションは不要なので行わないようにした。

 

これでTwitter認証によるログイン、既存ユーザーのTwitter認証追加、Twitter認証からのユーザー登録ができるようになった。