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
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
- 出版社/メーカー: LIANLI
- 発売日: 2013/09/18
- メディア: Personal Computers
- この商品を含むブログを見る
送信したメールの内容が確認できる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の呼び出し順としては、
- SetExpressCheckout
- GetExpressCheckoutDetails
- DoExpressCheckoutPayment
- CreateRecurringPaymentsProfile
となる。これをActiveMerchantのメソッドに置き換えると、
- setup_authorization
- details_for
- purchase
- 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_agreement と custom というパラメータが増えている。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_dateはperiodに合わせてずらしている。descriptionは前述のbilling_agreementのdescriptionと一致させなければならないので注意。 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 [キータ]
内容の古い部分があったりこちらの要件に合わない部分があったりしたので少し変更しながら実装した。
前提
既にDeviseでユーザー認証をしているところに外部認証を追加してゆく。
Twitter Developersにアプリケーションを登録する
まず Twitter Developers にログインし、右上のアイコンメニューから My applications を選択する。
Create a new application を選択し、各項目を入力する。後から変更できるのでサクッと登録してしまって良い。Callback URL にはコールバックされないので本番のURLにしていても良い。
アプリケーションが登録できたら Details タブの OAuth settings に表示されている Consumer key と Consumer 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認証からのユーザー登録ができるようになった。