Rails4: production環境でアセットが読み込まれない

今作っている Rails アプリを試しに production 環境で動かしてみたところ、css などのアセットが読み込まれない、という事態に遭遇した。ちゃんと rake assets:precompile してあるにも関わらず、だ。

いろいろとググっくてみると、次のページで答えを見つけた。

 cf. Rails3 css、js 404 Not Found! 静的ファイルが読み込まれない! – bismar’s blog

config/environments/production.rb の次の行が原因らしい。

# Disable Rails's static asset server (Apache or nginx will already do this).
config.serve_static_assets = false

config.serve_static_assets の値が false になっている。コメントにもあるけど要するに、production 環境では Apache や Nginx が静的ファイルを配信するだろうから Rails 自身は配信しない、ということらしい。
ともあれ、これを true に変更することで無事 css が読み込まれるようになった。

国名コードから国名を調べる

一昨日の続き。
IP アドレスから国名コードを調べることはできた。でも、JP とか US くらいならわかるけど、ほかは国名コードを出力されても、結局どこの国なんだかわからない。例えばおとといの cidr.txt の最後はこうなっている。

takatoh@nightschool $ tail cidr.txt
ZW	196.43.96.0/19
ZW	196.43.199.0/24
ZW	196.44.176.0/20
ZW	196.201.1.0/24
ZW	196.220.96.0/19
ZW	196.223.26.0/24
ZW	197.157.204.0/22
ZW	197.214.120.0/21
ZW	197.221.224.0/19
ZW	197.231.212.0/22

ZW ってどこの国だ?

国名コードと国名のデータベース

国名コードの一覧は例えば Wikipedia にもあるので、それを調べれば ZW がどこの国かはわかる。けど、やりたいのはそんなことじゃなくて、コマンド一発で国名を出力して欲しいんだ。
まずは国名コードと国名を対応付けるデータベースが必要だ。データベースと言ってもそんな大げさなものじゃなくて CSV ファイルでいいんだけども。とはいえ、Wikipedia のページでは表になってるので、それをスクレイピングするなんてめんどくさくてやってられない。ところが、「国名コード CSV」でググったら、あるじゃないか、CSV。Qiita で公開してくれている人がいた。ありがとう、いい人だ。

 cf. 国コード一覧CSV ISO 3166-1 – Qiita

というわけで、ここのページをエディタにコピペして、country_code.csv ファイルに保存した。

Rubyスクリプト

データさえ準備できれば、あとは検索するスクリプトさえ書けばいい。こんなの大して考えるまでもない。ちょちょいと書いた。

# coding: utf-8

require 'csv'

def main
  cc_table = load_country_code("country_code.csv")
  cc = ARGV.shift
  puts cc_table[cc]
end

def load_country_code(csv)
  table = {}
  File.open("country_code.csv", "r") do |f|
    f.gets
    f.each do |line|
      fields = line.parse_csv
      table[fields[4]] = fields[0]
    end
  end
  table
end

main

country_code.csv がスクリプトを実行するディレクトリにあることを前提としている。

さて、試してみよう。

takatoh@nightschool $ ruby cc_to_country.rb JP
日本
takatoh@nightschool $ ruby cc_to_country.rb ZW
ジンバブエ

というわけで、ZW はジンバブエでした。

IPアドレスがどこの国に割り当てられたものかを調べる

昨日の延長上の話。ある IP アドレスが、どのサブネットに属しているかがわかれば、あとはそのサブネットがどの国に割り当てられたものかがわかればいい。

cidr.txt

何はともあれ、IP アドレスの国別割り当て状況がわからないと話にならない。でも、世の中良くしたもんで、データを公開してくれている人がいた。ありがたい。

 cf. 世界の国別 IPv4 アドレス割り当てリスト

このページから、cidr.txt をダウンロードした。
CIDR ってのは Classless Inter-Domain Routing の略で、サブネットマスクを使って IP アドレスの割り当てとルーティングをする仕組みのこと。また、例えば 192.168.1.0/24 というサブネットの表記を CIDR表記というようだ。簡単な説明はここ。

 cf. CIDRとは – IT用語辞典 e-Words

cidr.txt は国名コードと割り当てられているサブネットの CIDR表記の一覧になっている。冒頭の10行はこうなっている。

AD 85.94.160.0/19
AD 91.187.64.0/19
AD 109.111.96.0/19
AD 185.4.52.0/22
AD 194.158.64.0/19
AE 2.48.0.0/14
AE 5.30.0.0/15
AE 5.32.0.0/17
AE 5.38.0.0/17
AE 5.53.96.0/21

国名コードと CIDR表記のサブネットはタブ文字で区切られている。
ちなみに AD はアンドラ、AE はアラブ首長国連邦だそうだ。

Ruby版

cidr.txt を読み込んでハッシュに保存しておき、入力された IP アドレスが属するサブネットを探して、見つかったらその国名コードを出力する。cidr.txt が実行ディレクトリにおいてあるのを前提としている。

# coding: utf-8

def main
  cidr = load_cidr("cidr.txt")
  addr = ARGV.shift
  cidr.each do |subnet, country|
    if in_subnet?(addr, subnet)
      puts country
      break
    end
  end
end

def load_cidr(file)
  cidr = {}
  File.open(file) do |f|
    f.each do |line|
      co, subnet = line.chomp.split("\t")
      cidr[subnet] = co
    end
  end
  cidr
end

def ip_to_b(addr)
  addr.split(".").map{|x| sprintf("%08d", x.to_i.to_s(2).to_i)}.join("")
end

def in_subnet?(addr, subnet)
  addr_b = ip_to_b(addr)
  subnet_addr, mask = subnet.split("/")
  subnet_addr_b = ip_to_b(subnet_addr)
  mask = mask.to_i
  addr_b.slice(0...mask) == subnet_addr_b.slice(0...mask)
end


main

IP アドレスの属するサブネットの検索が力技だけど、まあいいか。
実行結果。

takatoh@nightschool $ ruby -rresolv -e 'puts Resolv.getaddress("blog.panicblanket.com")'
210.224.185.170
takatoh@nightschool $ ruby addr_to_co.rb 210.224.185.170
JP

というわけで、このブログの IP アドレスは日本のものだってことが分かった。

Python版

同じやり方。

# coding: utf-8

import sys

def main():
    cidr = load_cidr("cidr.txt")
    addr = sys.argv[1]
    for subnet, co in cidr.items():
        if is_in_subnet(addr, subnet):
            print co
            break

def load_cidr(file):
    cidr = {}
    f = open(file)
    for line in f:
        co, subnet = line.rstrip().split("\t")
        cidr[subnet] = co
    return cidr

def ip_to_b(addr):
    return "".join(map(lambda x: "%08d" % int(bin(int(x))[2:]), addr.split(".")))

def is_in_subnet(addr, subnet):
    addr_b = ip_to_b(addr)
    subnet_addr, mask = subnet.split("/")
    subnet_addr_b = ip_to_b(subnet_addr)
    mask = int(mask)
    return addr_b[:mask] == subnet_addr_b[:mask]


main()
takatoh@nightschool $ python addr_to_co.py 210.224.185.170
JP

IPアドレスがサブネットに含まれるかどうかを判定する

Ruby版

# coding: utf-8

def ip_to_b(addr)
  addr.split(".").map{|x| sprintf("%08d", x.to_i.to_s(2).to_i)}.join("")
end

def in_subnet?(addr, subnet)
  addr_b = ip_to_b(addr)
  subnet_addr, mask = subnet.split("/")
  subnet_addr_b = ip_to_b(subnet_addr)
  mask = mask.to_i
  addr_b.slice(0...mask) == subnet_addr_b.slice(0...mask)
end

addr = "192.168.1.5"
subnet = "192.168.1.0/24"

puts in_subnet?(addr, subnet)

addr2 = "127.0.0.1"

puts in_subnet?(addr2, subnet)
takatoh@nightschool $ ruby is_in_subnet.rb
true
false

ちょっとトリッキーなのは、ip_to_b メソッドの sprintf のところ。頭に 0 をつけて8桁にするために、2進数(の文字列)にしたものを1と0だけからなる10進数だとみなして、%08d というフォーマットに変換している。これで IPアドレスの . で区切られた4つの部分のそれぞれが8桁(言い換えると8ビット)になる。
結果は上に示したとおり。192.18.1.5 はサブネット 192.168.1.0/24 に含まれるけど、127.0.0.1 は含まれない。

Python版

やってることは Ruby 版と同じ。

# coding: utf-8

def ip_to_b(addr):
    return "".join(map(lambda x: "%08d" % int(bin(int(x))[2:]), addr.split(".")))

def is_in_subnet(addr, subnet):
    addr_b = ip_to_b(addr)
    subnet_addr, mask = subnet.split("/")
    subnet_addr_b = ip_to_b(subnet_addr)
    mask = int(mask)
    return addr_b[:mask] == subnet_addr_b[:mask]

addr = "192.168.1.5"
subnet = "192.168.1.0/25"

print is_in_subnet(addr, subnet)

addr2 = "127.0.0.1"

print is_in_subnet(addr2, subnet)

当然結果も同じ。

takatoh@nightschool $ python is_in_subnet.py
True
False

10進数の数値から2進数表現の文字列に変換する

Ruby では Fixnum#to_s メソッドに 2 を引数として与えると、2進数表現の文字列になる。
こんな機能あったんだ、知らなかった。

2.1.1 :001 > 5.to_s
 => "5" 
2.1.1 :002 > 5.to_s(2)
 => "101"

引数は 2 じゃなくてもいい。整数 n を与えると n を基数とした数値表現になる。

2.1.1 :003 > 5.to_s(10)
 => "5" 
2.1.1 :004 > 15.to_s(16)
 => "f" 
2.1.1 :005 > 36.to_s(36)
 => "10" 
2.1.1 :006 > 36.to_s(37)
ArgumentError: invalid radix 37
	from (irb):6:in `to_s'
	from (irb):6
	from /home/takatoh/.rvm/rubies/ruby-2.1.1/bin/irb:11:in `<main>'

基数36までは OK で、37になるとダメみたいだ。

一方、Python では2進数に変換する bin 関数がある。

>>> bin(5)
'0b101'

頭に 0b がついてしまうので、とってやる。

>>> bin(5)[2:]
'101'

ちなみに、8進数にするには oct、16進数にするには hex 関数がある。

>>> oct(15)
'017'
>>> hex(15)
'0xf'

Rails 4をローカルタイムで運用する

いろいろ調べたけど、解決法はあっさり簡単だったのでメモしておく。
前提としては、データベースへの保存は UTC、アプリでの入出力はローカルタイムで行う。

サンプルアプリの作成

簡単なサンプルアプリを作る。

takatoh@nightschool $ rails new sampleapp
takatoh@nightschool $ cd sampleapp
takatoh@nightschool $ rails g scaffold items name:string release:datetime
takatoh@nightschool $ rake db:migrate

サーバを起動して http://localhost:3000/items にアクセスすると下のような画面になる。

sampleapp-1

データを入力してみる

試しに一つ、データを入力してみよう。New Item リンクをクリックして新規データ入力のページに移動。

sampleapp-2

今、日本ではは午前10時51分だ。時刻が UTC で表示されているのがわかる。
データをひとつ入力してみたあとが↓これ。時刻は UTC で表示されている。

sampleapp-3

試しに、コンソールで確認してみよう。

takatoh@nightschool $ rails c
Loading development environment (Rails 4.1.4)
2.1.1 :001 > Item.find(1)
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."id" = ? LIMIT 1  [["id", 1]]
 => #<Item id: 1, name: "Sampe item 1", release: "2014-11-22 01:51:00", created_at: "2014-11-22 01:51:30", updated_at: "2014-11-22 01:51:30">

release や created_at、 updated_at の値が 01:51:30 になっている。UTC とは書いていないけど、時刻からデータベースにも UTC で保存されているのがわかる。

アプリの入出力を日本時間にするには

config/application.rb に次の行を追加する。サンプルがコメントアウトされているので、その次辺りに入れればいいだろう。

config.time_zone = 'Asia/Tokyo'

書き加えたら、サーバを起動しなおして、itemsページにアクセスしてみよう。

sampleapp-4

今度は時刻が日本時間(+0900)で表示された!
じゃあ、新しいデータを入力するときはどうか。

sampleapp-5

ここも日本時間になっている。

データベースではどうか

コンソールで確かめてみよう。

Loading development environment (Rails 4.1.4)
2.1.1 :001 > item = Item.find(2)
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."id" = ? LIMIT 1  [["id", 2]]
 => #<Item id: 2, name: "SAmple item 2", release: "2014-11-22 02:07:00", created_at: "2014-11-22 02:08:15", updated_at: "2014-11-22 02:08:15"> 
2.1.1 :002 > item
 => #<Item id: 2, name: "SAmple item 2", release: "2014-11-22 02:07:00", created_at: "2014-11-22 02:08:15", updated_at: "2014-11-22 02:08:15">

release や created_at、updated_at の時刻は 02:08:15 になっている。データベースには相変わらず UTC で保存されているのがわかる。
ちなみに、取得した Item クラスのインスタンス item からは日本時間に変換された時刻が返ってくる。

2.1.1 :003 > item.release
 => Sat, 22 Nov 2014 11:07:00 JST +09:00

まとめ

Rails 4 をローカル時間に合わせるには、config/application.rb に1行追記するだけでいい。あとは Rails が全てうまくやってくれる。

Rubyでタイムゾーンの名前から、UTCからのオフセットを表す文字列を得る

タイトル長いな。

Time.new の7つ目の引数には、UTC からのオフセットを表す文字列を指定できる。こんなふうに。

2.1.1 :001 > Time.new(2014, 11, 20, 21, 30, 0, "-06:00")
 => 2014-11-20 21:30:00 -0600

上の、”-06:00″が UTC からのオフセットを表す文字列だ。これをタイムゾーンの名前から得たい、というのが今回の趣旨。また ActiveSupport を使って書いた。

require 'active_support/time'

def utc_offset_str(timezone)
  offset = Time.now.in_time_zone(timezone).utc_offset
  h = offset / (60 * 60)
  m = (offset - h * 60 * 60) / 60
  f = h &gt; 0 ? '+' : '-'

  sprintf("%s%02d:%02d", f, h.abs, m)
end

['Asia/Tokyo', 'America/Chicago'].each do |zone|
  puts zone
  puts utc_offset_str(zone)
end

Time#utc_offset で UTC からのオフセット(秒)が得られるので、それを使って目的の文字列を作っている。実行結果は次の通り。それにしてもベタなコードだなぁ。

takatoh@nightschool $ ruby utc_offset_str.rb
Asia/Tokyo
+09:00
America/Chicago
-06:00

おまけ。タイムゾーン名の一覧は↓ここで発見した。

 cf. タイムゾーンの一覧

さらにおまけ。
ActiveSupport の in_time_zone って ‘Asia/Tokyo’ じゃなくてただの ‘Tokyo’ でも受け付けるくせに ‘JST’ だとエラーになる。

2.1.1 :001 > require 'active_support/time'
 => true 
2.1.1 :002 > Time.now.in_time_zone('Tokyo')
 => Thu, 20 Nov 2014 21:44:24 JST +09:00 
2.1.1 :003 > Time.now.in_time_zone('JST')
ArgumentError: Invalid Timezone: JST
	from /home/takatoh/.rvm/gems/ruby-2.1.1/gems/activesupport-4.1.4/lib/active_support/core_ext/time/zones.rb:71:in `rescue in find_zone!'
	from /home/takatoh/.rvm/gems/ruby-2.1.1/gems/activesupport-4.1.4/lib/active_support/core_ext/time/zones.rb:55:in `find_zone!'
	from /home/takatoh/.rvm/gems/ruby-2.1.1/gems/activesupport-4.1.4/lib/active_support/core_ext/date_and_time/zones.rb:20:in `in_time_zone'
	from (irb):3
	from /home/takatoh/.rvm/rubies/ruby-2.1.1/bin/irb:11:in `<main>'

RubyのTimeオブジェクトをto_sしてTime.parseすると違う時刻になる?

タイトルわかりにくいな。要するにこういうこと。

2.1.1 :001 > require 'time'
 => true 
2.1.1 :002 > now = Time.now
 => 2014-11-19 22:27:36 +0900 
2.1.1 :003 > now2 = Time.parse(now.to_s)
 => 2014-11-19 22:27:36 +0900

Time.now して作ったインスタンス now をいったん文字列に変換して、Time.parse したのが now2。当然同じ時刻を示している。

2.1.1 :004 > now
 => 2014-11-19 22:27:36 +0900 
2.1.1 :005 > now2
 => 2014-11-19 22:27:36 +0900

と思いきや、== で比較してみると false が返ってくる。

2.1.1 :006 > now == now2
 => false

どういうことかと、うーんとなった。

で、ググってみると、同じことしてる人がいるもんだね。

 cf. 「Ruby」Timeクラスの比較 – タナカイチロウの日記

どうやら、Ruby の Time クラスは時刻を浮動小数点で持っていて、上のような操作をしてしまうと、厳密に同じ時刻を再現できないらしい。

2.1.1 :007 > now.to_f
 => 1416403656.075245 
2.1.1 :008 > now2.to_f
 => 1416403656.0

なるほどね〜。こりゃ、ハマりどころだわ。
ちなみに、Time.new で作ったインスタンスだとちゃんと true が返ってくる。

2.1.1 :009 > t = Time.new(2014, 11, 19, 22, 30, 0)
 => 2014-11-19 22:30:00 +0900 
2.1.1 :010 > t2 = Time.parse(t.to_s)
 => 2014-11-19 22:30:00 +0900 
2.1.1 :011 > t
 => 2014-11-19 22:30:00 +0900 
2.1.1 :012 > t2
 => 2014-11-19 22:30:00 +0900 
2.1.1 :013 > t == t2
 => true

任意のタイムゾーンの任意の時刻を得る

タイトルわかりにくいな。要するに、例えばシカゴ時間の2014年11月17日午前7時を表すオブジェクトがほしいってこと。

ActiveSupport の in_time_zone と Time#utc_offset を使って作った。

# encoding: utf-8

require 'active_support/time'

def time_in_zone(timezone, year, month, day, hour = 0, min = 0, sec = 0)
  t = Time.new(year, month, day, hour, min, sec).utc
  now = Time.now
  offset1 = now.utc_offset
  offset2 = now.in_time_zone(timezone).utc_offset
  (t + offset1 - offset2).in_time_zone(timezone)
end

puts time_in_zone('America/Chicago', 2014, 11, 17, 7, 0, 0)
puts time_in_zone('Asia/Tokyo', 2014, 11, 17, 7, 0, 0)
takatoh@nightschool $ ruby time_in_zone.rb
2014-11-17 07:00:00 -0600
2014-11-17 07:00:00 +0900

なんかオフセットを足したり引いたりしてるところがややこしくて時間がかかった。もっとスマートな方法はないもんかな。

11/20追記

Time.new の7つ目の引数に、UTC からのオフセットを表す文字列を与えてやると、同じようなことができる。

2.1.1 :001 > Time.new(2014, 11, 17, 7, 0, 0, "-06:00")
 => 2014-11-17 07:00:00 -0600

問題は、オフセット文字列じゃなくてタイムゾーンの名前から作りたいってことだ。どうやったらいいだろう?

UTCの時刻を任意のタイムゾーンに変換する

ActiveSupport::TimeWithZone を使うのが便利。active_support/time を require する。

2.1.1 :001 > require 'active_support/time'
 => true 

UCT 時刻を America/Chicago 時刻に変換してみる。

2.1.1 :002 > t = Time.now.utc
 => 2014-11-16 11:14:17 UTC 
2.1.1 :003 > tc = t.in_time_zone('America/Chicago')
 => Sun, 16 Nov 2014 05:14:17 CST -06:00 

tc のクラスは Time ではなくて、ActiveSupport::TimeWithZone。

2.1.1 :004 > tc.class
 => ActiveSupport::TimeWithZone

Time と比較もできる。

2.1.1 :005 > t == tc
 => true

TimeZone が違うと時刻表示は当然違うけど、UTC に直すと同じ時刻なので true になる。