SHOYAN BLOG

I am a pragmatic programmer.

Capistrano/wheneverで Cannot Load Such Fileがでる

wheneverを導入するためCapfilerequire "whenever/capistrano"と定義して bundle exec cap -T とすると以下のエラーがでた。

1
LoadError: cannot load such file -- /Users/shoyan/app/vendor/bundle/ruby/2.2.0/gems/whenever-0.9.5/lib/whenever/tasks/whenever.rake

実際にvendor/bundle/ruby/2.2.0/gems/whenever-0.9.5/lib/whenever/tasks/whenever.rake を確認してみると、たしかにそのファイルが存在しない。

wheneverのリポジトリを見てみると、シンボリックリンクがはってあった。

bundle install したときにはシンボリックリンクがはられないようだ。

手元のRubyのバージョンは、2.2.4だった。

ruby2.3.1で実行してみると Gem::Package::PathError が発生した。

1
2
ERROR:  While executing gem ... (Gem::Package::PathError)
    installing into parent path /Users/shoyan/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/capistrano/v3/tasks/whenever.rake of /Users/shoyan/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/whenever-0.9.5 is not allowed

このエラーはRuby2.3.2では直っていることが期待される。

回避策

Capfileでの指定はやめて、lib/capistrano/tasks 以下にwhenever.taskを作成した。
これでcapのタスク自体は実行できるようになった。
しかし、ruby2.3.1の環境ではbundle installがこけるのでwheneverの導入自体ができないという状況。

その後

bundle installがこけるバグは以下のPRがでており0.9.6で修正されていた。

また、0.9.5では cap whenever:update_crontabinstance variable @_env not defined が発生してコマンドが正常に実行できないバグがあったが、0.9.7で修正されている。

実はこのバグは自分が踏んでおり、その修正案をPRしてCHANGELOGに名前が刻まれた。

SSHKitを実際に使ってみて理解する

Capistrano/sshkitを紹介します。
https://github.com/capistrano/sshkit

SSHKitはリモートサーバーに対してコマンドを実行するためのツールキットです。
CapistranoやCapistranoプラグインではSSHKitが使われています。

インストール

1
gem install sshkit

コマンドのサンプル

実際に使ってみて理解していきます。
使うには、sshkitをロードする必要があります。

1
2
require 'sshkit'
include SSHKit::DSL

ホスト名を取得する

まずはログインしてホスト名を取得してみましょう。

1
2
3
4
on ['deploy@example.com] do |host|
 puts capture(:hostname)
end
=> example.com

on メソッドに対象のサーバーとブロックを渡します。
対象のサーバーは複数設定することも可能です。
ブロックにはサーバー上で実行するコマンドを設定します。
captureメソッドは渡された引数をコマンドとして実行し、結果をログに出力します。

特定のユーザーでコマンドを実行する

特定のユーザーでコマンドを実行する場合は、asメソッドで指定します。

1
2
3
4
5
on ['example.com'] do |host|
  as 'deploy' do
   puts capture(:whoami)
  end
end

特定のディレクトリを指定する

特定のディレクトリを指定する場合は、withinメソッドを指定します。

1
2
3
4
5
on ['deploy@example.com'] do |host|
  within '/var/log' do
    puts capture(:head, '-n5', 'messages')
  end
end

/var/loghead -n5 messages を実行します。

環境変数を指定する

withメソッドで環境変数を指定することができます。

1
2
3
4
5
on hosts do |host|
  with rack_env: :test do
    puts capture("env")
  end
end

rack_envに:test を設定しています。

ファイルをチェックして存在すればメッセージを表示、なければファイルを作成する

1
2
3
4
5
6
7
8
9
10
on ['deploy@example.com'] do |host|
  f = '/tmp/file'
  if test("[ -f #{f} ]")
    info "#{f} already exist on #{host}!"
  else
    execute :touch, f
  end
end
INFO [790b6aaa] Running /usr/bin/env touch /tmp/file as deploy@example.com
INFO [790b6aaa] Finished in 0.052 seconds with exit status 0 (successful).

testメソッドでファイルをチェックし、executeメソッドでtouchコマンドを実行しています。

ファイルをアップロードする

ファイルをアップロードすることもできます。

1
2
3
on ['deploy@example.com'] do |host|
  upload! 'README.md', '/tmp/README.md'
end

第1引数がローカルのファイルのパス、第2引数がサーバーに配置するファイルのパスです。

また、recursiveオプションをtrueに設定することでディレクトリをアップロードすることもできます。

1
2
3
on hosts do |host|
  upload! '.', '/tmp/mypwd', recursive: true
end

ローカルで実行する

ローカルで実行することもできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
run_locally do
  within '/tmp' do
    execute :whoami
  end
end

# もしくは

on(:local) do
  within '/tmp' do
    execute :whoami
  end
end

Rakeタスクで利用する

RakeタスクでSSHKitのDSLを使うこともできます。
この仕組みを利用してCapistranoのプラグインは作成されています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'sshkit'

SSHKit.config.command_map[:rake] = "./bin/rake"

desc "Deploy the site, pulls from Git, migrate the db and precompile assets, then restart Passenger."
task :deploy do
  include SSHKit::DSL

  on "example.com" do |host|
    within "/opt/sites/example.com" do
      execute :git, :pull
      execute :bundle, :install, '--deployment'
      execute :rake, 'db:migrate'
      execute :rake, 'assets:precompile'
      execute :touch, 'tmp/restart.txt'
    end
  end
end

サンプルがこちらにたくさんあるので、参考になると思います。

https://github.com/capistrano/sshkit/blob/master/EXAMPLES.md

base64エンコードのアルゴリズムをRubyで実装する

Base64とは英数字、記号を用いてマルチバイト文字やバイナリデータ(画像など)を扱うためのエンコード方式です。
具体的にはA–Z, a–z, 0–9 までの62文字と、記号2つ (+,/)、さらにパディング(余った部分を詰める)のための記号として = が用いられます。
7ビットの文字コードしか扱うことができない電子メールにおいてよく利用されています。

変換アルゴリズム

変換アルゴリズムは以下となります。

  1. 元データを6ビットずつに分割する(6ビットに満たない部分は0を追加して6ビットにする)。
  2. 各6ビットの値を変換表を使って4文字ずつに変換する(4文字に満たない部分は=記号を使って4文字にする)。

変換例

1. 元データ

文字列: “ABCDEFG”
2進数に変換する: “0100 0001 0100 0010 0100 0011 0100 0100 0100 0101 0100 0110 0100 0111”

rubyでのサンプルコード

1
"ABCDEFG".unpack("B*").pop.scan(/.{1,4}/).join(" ")

2. 6ビットずつに分割

“010000 010100 001001 000011 010001 000100 010101 000110 010001 11”

1
"ABCDEFG".unpack("B*").pop.scan(/.{1,6}/).join(" ")

3. 2ビット余るので、4ビット分0を追加して6ビットにする

“010000 010100 001001 000011 010001 000100 010101 000110 010001 110000”

1
list = "ABCDEFG".unpack("B*").pop.scan(/.{1,6}/).join(" ").split.map { |s| sprintf("%-06s", s).gsub(" ", "0")}.join(" ")

4. 変換表により、4文字ずつ変換

“QUJD”, “REVG”, “Rw”

1
2
3
4
5
6
7
# 変換表を作成する
keys = (0..63).map {|m| sprintf("%06s", m.to_s(2)).gsub(" ", "0")}
values = [('A'..'Z'), ('a'..'z'), ('0'..'9'), ['+', '/']].map { |a| a.to_a }.flatten
base64_table = Hash[[keys, values].transpose]

base64_list = list.map {|a| base64_table[a]}.join.scan(/.{1,4}/)
=> ["QUJD", "REVG", "Rw"]

5. 2文字余るので、2文字分 = 記号を追加して4文字にする

1
base64_list.map {|s| sprintf("%-4s", s).gsub(" ", "=")}

6. Base64文字列

“QUJDREVGRw==”

1
2
base64_str.scan(/.{1,4}/).map {|s| sprintf("%-4s", s).gsub(" ", "=")}.join
=> "QUJDREVGRw=="

簡易的なbase64_decodeメソッド

今までのロジックをメソッドにまとめて簡易的なbase64_decodeメソッドを作成しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base64
  def self.base64_encode(str)
    # 変換表を作成する
    keys = (0..63).map {|m| sprintf("%06s", m.to_s(2)).gsub(" ", "0")}
    values = [('A'..'Z'), ('a'..'z'), ('0'..'9'), ['+', '/']].map { |a| a.to_a }.flatten
    base64_table = Hash[[keys, values].transpose]

    binary = str.unpack("B*").pop.scan(/.{1,6}/).join(" ").split.map { |s| sprintf("%-06s", s).gsub(" ", "0") }
    base64_list = binary.map {|a| base64_table[a]}.join.scan(/.{1,4}/)
    base64_list.map {|s| sprintf("%-4s", s).gsub(" ", "=")}.join
  end
end

p Base64.base64_encode("ABCDEFG")
=> "QUJDREVGRw=="

RubyのBase64ライブラリでencodeした値と比べてみましょう。

1
2
3
require 'base64'
Base64.encode64("ABCDEFG")
=> "QUJDREVGRw==\n"

Rubyのencode64は最後に改行が入るようですが、encodeされた値は同じですね!

参考リンク

先週月曜日の日付を取得するアルゴリズム

先週の月曜日を求めるアルゴリズム

  • 先週(7日前)が月曜日である場合はその日付を返す
  • 月曜日でない場合
    • 月曜日より前であれば日付を1日たす
    • 月曜日より後であれば日付を1日ひく

Rubyで実装

1
2
3
4
5
6
7
8
9
def last_monday(date = Date.today - 7)
  return date if date.monday?
  if date.wday < 1
    date += 1
  else
    date -= 1
  end
  last_monday(date)
end

先週の金曜日を求める場合

1
2
3
4
5
6
7
8
9
def last_friday(date = Date.today - 7)
  return date if date.friday?
  if date.wday < 5
    date += 1
  else
    date -= 1
  end
  last_friday(date)
end

次の月曜日の日付を求めるアルゴリズム

  • 明日が月曜日かどうか
    • 月曜日であればその日を返す
    • 月曜日でなければ1日たす

Rubyで実装

1
2
3
4
def next_monday(date = Date.today + 1)
  return date if date.monday?
  next_monday(date + 1)
end

前回の月曜日の日付を求めるアルゴリズム

  • 昨日が月曜日かどうか
    • 月曜日であればその日を返す
    • 月曜日でなければ1日ひく

Rubyで実装

1
2
3
4
def prev_monday(date = Date.today - 1)
  return date if date.monday?
  prev_monday(date - 1)
end

実行結果

1
2
3
4
5
6
7
8
9
10
puts Date.today.strftime("%Y-%m-%d (%a)")
=> 2016-06-09 (Thu)
puts prev_monday.strftime("%Y-%m-%d (%a)")
=> 2016-06-06 (Mon)
puts next_monday.strftime("%Y-%m-%d (%a)")
=> 2016-06-13 (Mon)
puts last_monday.strftime("%Y-%m-%d (%a)")
=> 2016-05-30 (Mon)
puts last_friday.strftime("%Y-%m-%d (%a)")
=> 2016-06-03 (Fri)

ActionMailerの添付ファイルをRspecでテストする

ActionMailerで添付ファイルを送るようにしたのですが、そのテストをするときの情報があまりなかったのでまとめました。

以下のようにテストしました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
RSpec.describe AppMailer, type: :mailer do

  before(:all) do
    Archive::Zip.archive(
      "tmp/app.zip",
      "README.md")
  end

  after(:all) do
    FileUtils.rm "tmp/app.zip"
  end

  let(:mail) do
    AppMailer.send_email(zip_path)
  end

  it 'assings the attachment file' do
    attachment = mail.attachments[0]
    expect(attachment).to be_a_kind_of(Mail::Part)
    expect(attachment.content_type).to be_start_with('application/zip')
    expect(attachment.filename).to eq "app.zip"
  end
end

まず、before(:all)でテストに使うzipファイルを作成します。
zipファイルはarchive-zipを使って作成しています。
アーカイブするファイルは何でもよいですが、この記事ではREADME.mdとしています。
テストで作成したzipファイルはafter(:all)で消しています。
mail.attachments[0]に添付ファイルが入っているので、その種類やファイル名を確認しています。

参考リンク