JenkinsプラグインのGitHub pull request builder pluginを使ってみる

現在、GitHubのPull Requestでコードレビューし、問題なければマージするというフローで開発しているのですが、
コードは問題なさそうなのでマージしてみると、specが落ちている・・といったことがありました。

そこで、Pull Requestされた時点でそれをマージしspecを実行してくれる、そしてその結果を通知してくれる
といったことが自動でできれば良いなと考えていました。
そこで発見したのがGitHub pull request builder pluginというJenkinsのプラグインです。

このプラグインは、以下のようなことをやってくれます。

  • Pull Requestされた(またはそのPull Requestにコミットを積み重ねた)時にそれを検知し、自動でマージしビルドしてくれる

  (実際はcrontabで設定したタイミングで)

  • GitHubのPull Requestページに、その結果を反映してくれる(ビルドに成功したとか、失敗したとか)
  • GitHubのPull Requestページに、設定しているフレーズをコメントすると再ビルドしてくれる

設定

若干設定に戸惑いましたが、以下の流れでうまくいきました。
詳細はGitHub pull request builder plugin - Jenkins - Jenkins Wikiをご参照ください。

  1. 事前準備
    • 対象リポジトリのコラボレータにボットユーザを追加する
  1. プラグインをインストールする
  1. Jenkinsの全体設定でGitHub pull request builderの設定を行う
    • Jenkinsの管理 -> システムの設定 -> "Github pull requests builder" セクションへ
    • 事前準備で作成したGitHubのボットユーザのユーザ名とパスワードを設定する("Username"と"Password" フィールド)
      (パスワードではなくアクセストークンを使用したい場合は "高度な設定 -> Access Token" に登録する)
    • すべてのジョブに対するadminのGitHubユーザを登録する("Admin list" フィールド)
      (ここで登録したユーザが各ジョブで自動でadminとなる。各ジョブで変更することも可能)
    • 高度な設定から
      • whitelistに登録するフレーズを設定する("Add to white list phrase" フィールド)
        デフォルトは ".*add\W+to\W+whitelist.*" (Java正規表現で設定)
      • テストするPull Requestを許可するフレーズを設定する("Accept to test phrase" フィールド)
        デフォルトは ".*ok\W+to\W+test.*" (Java正規表現で設定)
      • 再ビルドするフレーズを設定する("Test phrase" フィールド)
        デフォルトは ".*test\W+this\W+please.*" (Java正規表現で設定)
      • crontabを設定する("Crontab line" フィールド)
        デフォルトは "*/5 * * * *" だったような・・・
      • (パスワードを使わない場合)アクセストークンを設定する("Access Token" フィールド)
  1. ジョブの設定を行う
    • 新しいジョブを作成する
    • "Repositories -> Repository URL" を設定する(e.g.:"git@github.com:username/repository_name.git")
    • 高度な設定から
      • "Repositories -> Refspec" に次の値を設定する

+refs/pull/*:refs/remotes/origin/pr/*

    • "Branches to build -> Branch Specifier" に次の値を設定する

${sha1}

    • "ビルド・トリガ" セクションの "Github pull requests builder" にチェックを入れる
    • adminのユーザを設定する("Admin list" フィールド)
      Jenkinsの全体の設定で登録したadminがデフォルトで入る(たぶん・・・)
    • 高度な設定から
      • crontabを設定する("Crontab line" フィールド)
        Jenkinsの全体の設定で登録したcrontabがデフォルトで入る
      • whitelistを登録する("White list" フィールド)

結果

上記の設定を行った後、Pull Requestを出してみてください。
crontabに設定したタイミングでPull Requestを検知し、ビルドを実行してくれるはずです。

GitHubのPull Requestページは以下のようになります。

  • ビルド中

  • ビルドが成功した場合

  • ビルドが失敗した場合

  • 各コミットにグリーン、レッドマークがつく


ビルドのステータスによって色分けされ、とても分かりやすいですね^^
これにより、Pull Requestのビルド結果をマージする前に知ることができ、冒頭で書いたような問題が解消されました。

Pull Requestをトリガーにして何かをするという場合、とても良いプラグインだと思います。
後は結果をIRC等で通知しても面白そうですね。

注意点とフレーズについて

Pull Requestを出したユーザがadminまたはwhitelistに登録されていない場合は、自動でビルドされません。
この時、ボットが "Can one of the admins verify this patch?" とコメントします。

adminユーザがビルドして良いと判断したとき、 下記2つのどちらかのフレーズをコメントし、ビルドを実行します。
(adminユーザ以外がこの2つのフレーズをコメントしても動きません)

  • ok to test
    Pull Requestがテストされるのを許可し、ビルドを実行する
  • add to whitelist
    whitelistにPull Requestの作成者を追加、Pull Requestがテストされるのを許可し、ビルドを実行する

再度ビルドを実行したい場合、下記フレーズをコメントすれば、再ビルドしてくれます。
(これはadminユーザまたはwhitelistに登録されたユーザが実行できます)

  • test this please
    再度ビルドを実行する

whitelist周りの設定が煩わしい場合は、ジョブの設定の段階でwhitelistを登録しておくと良いと思います。

備考

Jenkins1.442からは、プラグインを再起動しないでインストールできるようになりましたが、
今回Github pull requests builder pluginの設定を行う際、それができずエラーとなってしまいました。

原因はPersona Pluginによるものでした。Jenkinsにおいて画像を扱うプラグインのようです。
これをインストールしていると「再起動せずにインストール」ができません。

この現象が問題となる場合は、Persona Pluginを外した方がいいかもしれませんね。


.zshrcを設定してみる

zshの設定を見直してみました。
今使っている.zshrcを載せてみたいと思います。
本来、環境変数やパスの設定は.zshenvに記述する方が良いらしいですが、今回は.zshrcにまとめます。

環境
  • OS
    • ProductName: Mac OS X
    • ProductVersion: 10.7.2
    • BuildVersion: 11C74
.zshrc
# (d) is default on

# ------------------------------
# General Settings
# ------------------------------
export EDITOR=vim        # エディタをvimに設定
export LANG=ja_JP.UTF-8  # 文字コードをUTF-8に設定
export KCODE=u           # KCODEにUTF-8を設定
export AUTOFEATURE=true  # autotestでfeatureを動かす

bindkey -e               # キーバインドをemacsモードに設定
#bindkey -v              # キーバインドをviモードに設定

setopt no_beep           # ビープ音を鳴らさないようにする
setopt auto_cd           # ディレクトリ名の入力のみで移動する 
setopt auto_pushd        # cd時にディレクトリスタックにpushdする
setopt correct           # コマンドのスペルを訂正する
setopt magic_equal_subst # =以降も補完する(--prefix=/usrなど)
setopt prompt_subst      # プロンプト定義内で変数置換やコマンド置換を扱う
setopt notify            # バックグラウンドジョブの状態変化を即時報告する
setopt equals            # =commandを`which command`と同じ処理にする

### Complement ###
autoload -U compinit; compinit # 補完機能を有効にする
setopt auto_list               # 補完候補を一覧で表示する(d)
setopt auto_menu               # 補完キー連打で補完候補を順に表示する(d)
setopt list_packed             # 補完候補をできるだけ詰めて表示する
setopt list_types              # 補完候補にファイルの種類も表示する
bindkey "^[[Z" reverse-menu-complete  # Shift-Tabで補完候補を逆順する("\e[Z"でも動作する)
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' # 補完時に大文字小文字を区別しない

### Glob ###
setopt extended_glob # グロブ機能を拡張する
unsetopt caseglob    # ファイルグロブで大文字小文字を区別しない

### History ###
HISTFILE=~/.zsh_history   # ヒストリを保存するファイル
HISTSIZE=10000            # メモリに保存されるヒストリの件数
SAVEHIST=10000            # 保存されるヒストリの件数
setopt bang_hist          # !を使ったヒストリ展開を行う(d)
setopt extended_history   # ヒストリに実行時間も保存する
setopt hist_ignore_dups   # 直前と同じコマンドはヒストリに追加しない
setopt share_history      # 他のシェルのヒストリをリアルタイムで共有する
setopt hist_reduce_blanks # 余分なスペースを削除してヒストリに保存する

# マッチしたコマンドのヒストリを表示できるようにする
autoload history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey "^P" history-beginning-search-backward-end
bindkey "^N" history-beginning-search-forward-end

# すべてのヒストリを表示する
function history-all { history -E 1 }


# ------------------------------
# Look And Feel Settings
# ------------------------------
### Ls Color ###
# 色の設定
export LSCOLORS=Exfxcxdxbxegedabagacad
# 補完時の色の設定
export LS_COLORS='di=01;34:ln=01;35:so=01;32:ex=01;31:bd=46;34:cd=43;34:su=41;30:sg=46;30:tw=42;30:ow=43;30'
# ZLS_COLORSとは?
export ZLS_COLORS=$LS_COLORS
# lsコマンド時、自動で色がつく(ls -Gのようなもの?)
export CLICOLOR=true
# 補完候補に色を付ける
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}

### Prompt ###
# プロンプトに色を付ける
autoload -U colors; colors
# 一般ユーザ時
tmp_prompt="%{${fg[cyan]}%}%n%# %{${reset_color}%}"
tmp_prompt2="%{${fg[cyan]}%}%_> %{${reset_color}%}"
tmp_rprompt="%{${fg[green]}%}[%~]%{${reset_color}%}"
tmp_sprompt="%{${fg[yellow]}%}%r is correct? [Yes, No, Abort, Edit]:%{${reset_color}%}"

# rootユーザ時(太字にし、アンダーバーをつける)
if [ ${UID} -eq 0 ]; then
  tmp_prompt="%B%U${tmp_prompt}%u%b"
  tmp_prompt2="%B%U${tmp_prompt2}%u%b"
  tmp_rprompt="%B%U${tmp_rprompt}%u%b"
  tmp_sprompt="%B%U${tmp_sprompt}%u%b"
fi

PROMPT=$tmp_prompt    # 通常のプロンプト
PROMPT2=$tmp_prompt2  # セカンダリのプロンプト(コマンドが2行以上の時に表示される)
RPROMPT=$tmp_rprompt  # 右側のプロンプト
SPROMPT=$tmp_sprompt  # スペル訂正用プロンプト
# SSHログイン時のプロンプト
[ -n "${REMOTEHOST}${SSH_CONNECTION}" ] &&
  PROMPT="%{${fg[white]}%}${HOST%%.*} ${PROMPT}"
;

### Title (user@hostname) ###
case "${TERM}" in
kterm*|xterm*|)
  precmd() {
    echo -ne "\033]0;${USER}@${HOST%%.*}\007"
  }
  ;;
esac


# ------------------------------
# Other Settings
# ------------------------------
### RVM ###
if [[ -s ~/.rvm/scripts/rvm ]] ; then source ~/.rvm/scripts/rvm ; fi

### Macports ###
case "${OSTYPE}" in
  darwin*)
    export PATH=/opt/local/bin:/opt/local/sbin:$PATH
    export MANPATH=/opt/local/share/man:/opt/local/man:$MANPATH
  ;;
esac

### Aliases ###
alias r=rails
alias v=vim

# cdコマンド実行後、lsを実行する
#function cd() {
#  builtin cd $@ && ls;
#}


この中でも特筆できるような設定をピックアップしてみたいと思います。

Shift-Tabで補完候補を逆順する

bindkey "^[[Z" reverse-menu-complete  # Shift-Tabで補完候補を逆順する("\e[Z"でも動作する)

補完機能を有効にしたことで、Tabキーで補完候補が表示される訳ですが、
Tabを連打してお目当ての補完候補を通り過ぎてしまうことがあります。
そこでこの設定をしておくと、Shift-Tabで逆方向に補完してくれるようになります。

ちなみに

設定しているとき、"^[[Z"って何ってなんだ?となったんですが、まあ要するにShift-Tabのことですww
こういったキーコード?を調べる方法があります。
Ctrl-Vを押した後、調べたいキーを押せば、該当のキーコードが表示されます。

Ctrl-V押下後、上矢印キー
> ^[[A
Ctrl-V押下後、Shift-Tabキー
> ^[[Z

ただコメントに括弧して書いているのですが、"\e[Z"でも動作するようです。
どういうことなのかよく分からないな・・・(;´・ω・)ゝ
分かる方がいましたら、是非ご教授ください!

補完時に大文字小文字を区別しない

zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

例えばDocuments, downloadsというディレクトリがあったとき、
"cd d"までタイプしTabを押すとDocuments, downloads両方を補完してくれる訳です。
ただし、"cd D"でTabを押すとdownloadsは候補にはなりません。
使い勝手としてはまったく問題ないです。

> cd d <Tab>
Documents   downloads

> cd D <Tab>
Documents

マッチしたコマンドのヒストリを表示できるようにする

autoload history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey "^P" history-beginning-search-backward-end
bindkey "^N" history-beginning-search-forward-end

この設定をすると、例えば以前にvimコマンドをタイプしたことがあれば、vimのヒストリを遡ることができます。

> history
  1 vim a.txt
  2 cd Documents
  3 vim b.txt
 
> vim <Ctrl-P>
> vim b.txt
> vim b.txt <Ctrl-P>
> vim a.txt

この設定をしない状態(デフォルト)だと、Ctrl-Pはup-line-or-history, Ctrl-Nはdown-line-or-historyなはずなので、
コマンドの履歴を遡るだけです。("cd Documents"とかも表示されます)
要するにこの場合で言えば、vimコマンドに特化して、ヒストリを表示できるということですね。
もちろん、何もタイプしない状態でCtrl-Pを押せば、通常通りのコマンドのヒストリを表示できます。

lsコマンドの結果に色を付ける

### Ls Color ###
# 色の設定
export LSCOLORS=Exfxcxdxbxegedabagacad
# 補完時の色の設定
export LS_COLORS='di=01;34:ln=01;35:so=01;32:ex=01;31:bd=46;34:cd=43;34:su=41;30:sg=46;30:tw=42;30:ow=43;30'
# ZLS_COLORSとは?
export ZLS_COLORS=$LS_COLORS
# lsコマンド時、自動で色がつく(ls -Gのようなもの?)
export CLICOLOR=true
# 補完候補に色を付ける
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}

lsコマンドの結果に色を付けます。
設定内容はコメントに記述してある通りです。
よく分からなかったのもありますがw
色の設定をしたい場合は、LSCOLORSやLS_COLORSを修正すれば大丈夫です。

プロンプトを変更する・色を付ける

### Prompt ###
# プロンプトに色を付ける
autoload -U colors; colors

# 一般ユーザ時
tmp_prompt="%{${fg[cyan]}%}%n%# %{${reset_color}%}"
tmp_prompt2="%{${fg[cyan]}%}%_> %{${reset_color}%}"
tmp_rprompt="%{${fg[green]}%}[%~]%{${reset_color}%}"
tmp_sprompt="%{${fg[yellow]}%}%r is correct? [Yes, No, Abort, Edit]:%{${reset_color}%}"

# rootユーザ時(太字にし、アンダーバーをつける)
if [ ${UID} -eq 0 ]; then
  tmp_prompt="%B%U${tmp_prompt}%u%b"
  tmp_prompt2="%B%U${tmp_prompt2}%u%b"
  tmp_rprompt="%B%U${tmp_rprompt}%u%b"
  tmp_sprompt="%B%U${tmp_sprompt}%u%b"
fi

PROMPT=$tmp_prompt    # 通常のプロンプト
PROMPT2=$tmp_prompt2  # セカンダリのプロンプト(コマンドが2行以上の時に表示される)
RPROMPT=$tmp_rprompt  # 右側のプロンプト
SPROMPT=$tmp_sprompt  # スペル訂正用プロンプト

まず、"autoload -U colors; colors"でcolors関数をロードし、プロンプトに色を付けます。
次に、通常プロンプト・セカンダリプロンプト・右側のプロンプト・スペル訂正用のプロンプトを設定しています。
色を設定する場合、"%{$fg[red]%}"で開始し、"%{$reset_color%}"でリセットするようにしましょう。
後は、rootユーザであればプロンプトを太字にし、アンダーバーを付けるといったことをやっています。

ターミナルのタイトルを変更する

### Title (user@hostname) ###
case "${TERM}" in
kterm*|xterm*|)
  precmd() {
    echo -ne "\033]0;${USER}@${HOST%%.*}\007"
  }
  ;;  
esac

ターミナルのタイトルを"ユーザ名@ホスト名"に変更します。
組み込み関数のprecmd()を使用し、プロンプト表示前にターミナルにシーケンス文字を送信しています。
うーん、よく分からなかったのですが、"\033]0;"と"\007"で囲めばターミナルのタイトル部分に文字列をechoするということなのかな・・・?


一応、以上の設定である程度満足できています。
もっと改善の余地があると思いますが、余裕があったら掘り下げてやってみようと思います。

githubにもアップしています。
dotfiles/.zshrc at master · tiwakawa/dotfiles · GitHub

以上です。

dotfilesをgithubで管理し、Vundleを導入する

環境が新しくなったので、これを機に設定ファイル類の管理を見直してみました。
調べてみると大抵は以下のように管理されているようなので、合わせてやってみます。

  1. dotfilesをgithubで管理する(今回は.zshrcと.vimrcです)
  2. vimプラグインをvundleで管理する

dotfilesをgithubで管理する

毎回設定ファイル等をコピーしたりするのが煩わしいので、githubで管理するようにします。
手順は以下の通りです。

# dotfilesディレクトリを作成
mkdir ~/dotfiles
cd ~/dotfiles

# dotfilesをgit管理する
git init

# 「.」ファイルをdotfilesに移動
mv ~/.zshrc ~/dotfiles/
mv ~/.vimrc ~/dotfiles/
mv ~/.vim ~/dotfiles/

ここまでで、dotfilesをgit管理下にできたと思います。
後は、dotfiles配下のファイルをホームディレクトリに対してシンボリックリンクを張ればOKです。

今回は.zshrcと.vimrcだけですが、今後dotfileが増えてくるとリンクを張るのも大変なので、リンクを張るためのMakefileを作成します。
面倒な場合は、ln -snf ~/dotfiles/.zshrc ~/などでも良いと思います。

Makefile

DOT_FILES = .zshrc .vimrc .vim

all: zsh vim 

zsh: $(foreach f, $(filter .zsh%, $(DOT_FILES)), link-dot-file-$(f))

vim: $(foreach f, $(filter .vim%, $(DOT_FILES)), link-dot-file-$(f))
  
.PHONY: clean
clean: $(foreach f, $(DOT_FILES), unlink-dot-file-$(f))
  

link-dot-file-%: %
  @echo "Create Symlink $< => $(HOME)/$<"
  @ln -snf $(CURDIR)/$< $(HOME)/$<

unlink-dot-file-%: %
  @echo "Remove Symlink $(HOME)/$<"
  @$(RM) $(HOME)/$<

使い方は以下の通りです。

make # .zshrc, .vimrc, .vimのシンボリックリンクを作成
make zsh # .zshrcのシンボリックリンクを作成
make vim # .vimrc, .vimのシンボリックリンクを作成
make clean # .zshrc, .vimrc, .vimのシンボリックリンクを削除

後は、githubにコミットするだけです。

git add ./
git commit -m 'first commit'
git push -u origin master

実際に使用する際は、git pullしてmakeを実行すればいけると思います。


vimプラグインをvundleで管理する

vimプラグインの管理は面倒なので、vundleを使用します。
RailsのBundleにインスパイアされているそう。

dotfilesはすでにgit管理下にあるので、まずvundleをsubmoduleとして追加します。

cd ~/dotfiles
git submodule add git://github.com/gmarik/vundle.git .vim/vundle.git

これでvundleが使えるので、.vimrcにvundleの設定とインストールしたいプラグインを記述します。

set nocompatible
filetype off
set rtp+=~/.vim/vundle.git/
call vundle#rc()

" プラグインを記述する
Bundle 'Shougo/neocomplcache'
Bundle 'Shougo/unite.vim'
Bundle 'pangloss/vim-javascript'
Bundle 'tpope/vim-rails'
Bundle 'vim-ruby/vim-ruby'
Bundle 'vim-scripts/surround.vim'
・・・

" Vundleの処理後、ftpluginとindentを読み込む
filetype plugin indent on

.vimrcを編集したら、vimを開き以下を実行しプラグインをインストールします。

:BundleInstall

ちなみに以下のようなコマンドもあります。

# プラグインの更新
:BundleInstall!

# プラグインの削除
# 削除の場合、.vimrcから削除し、下記コマンドを実行しましょう(bundle配下のディレクトリを削除してくれます)
:BundleClean

インストールが完了すると、~/dotfiles/.vim/bundleにプラグインが格納されます。
プラグインはいちいちgitで管理する必要はないので、.gitignoreに記述し対象外としましょう。

echo '.vim/bundle' > .gitignore

最後にgitに追加して、コミットすれば完了です。

git add ./
git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	new file:   .gitignore
#	new file:   .gitmodules
#	new file:   .vim/vundle.git
#	modified:   .vimrc

git commit -m 'install vundle'
git push -u origin master

.vimrcの細かい設定などは GitHub - tiwakawa/dotfiles: dotfiles を参照頂ければと思います。

実際に使用する際は、git pullしてmakeを実行すればいけると思います。

※ただし以下の点に注意してください。

gitから落とした直後の状態だと、submoduleであるvundleは空の状態になっていますので、
.vimrcに記述されているBundleを認識することができません。
以下のコマンドでvundleをインストールするようにしてください。

cd ~/dotfiles
git submodule init
git submodule update

これで、BundleInstall等のコマンドが実行できるようになります。

最後に困ったこと

今回、vundleを導入するタイミングでneocomplcacheをインストールしました。
これは補完をうまいことやってくれるプラグインです。

ただ、インストール後にちょっと問題が起こりました。
vimを起動し挿入モードにした途端、以下のようなエラーが発生してしまうのです。

Error detected while processing InsertEnter Auto commands for "*":
E117: Unknown function: 52_on_insert_enter

neocomplcacheのバグなのかよく分からなかったのですが、とりあえず下記ファイルの67,68行目を修正することで対応しました。

~/dotfiles/.vim/bundle/neocomplcache/autoload/neocomplcache.vim

  66 "    以下をコメントアウト
  67 "    autocmd neocomplcache InsertEnter *
  68 "          \ call s:on_insert_enter()

メッセージの通り、確かにs:on_insert_enter()関数は存在しないからなぁ。。。
何か詳細が分かる方がいたらコメント頂けると助かります(´・ω・`)

以上です。

追記します

1日で直ってましたww
- Fixed error. · Shougo/neocomplcache.vim@6032f7c · GitHub

Rubyで並列に処理を実行する

Rubyスクリプトを組んでバッチとして動かしているのですが、これがとても時間が掛かってしまっています。。。
CSVファイルを作成するバッチなのですが、それに必要なデータを取得するいくつかのSQLが重いのです。

そこで、複数あるSQLを並列処理させることで改善しました。
以下のコードになります。

# プロセスIDを格納する配列を用意
pids = []

# 子プロセスを生成その1
# forkにブロックを指定すると、生成したプロセスでブロックを処理する
pids << fork do
  @data1 = exec_sql1
end

# 子プロセスを生成その2
pids << fork do
  @data2 = exec_sql2
end

・・・


# すべての子プロセスが終了するのを待つ
# 戻り値 => [子プロセスID, Process::Statusのオブジェクト]
results = Process.waitall

# 正常に終了しているかチェック
results.each do |r|
  raise unless pids.include?(r[0]) && r[1].success?
end

内容は簡単です。

処理毎にforkして子プロセスを生成し、それに処理させる訳です。
Process.waitallですべての子プロセスが終了するのを待ち、最後に正常終了のチェックをしています。
この場合、すべての子プロセスが終了するのを待つため、一番時間の掛かる処理まで待つことになります。
ただそれでも、一つ一つシングルで処理するよりは早くなるため、マシンスペックに余裕がある場合は試してみても良いかもしれません。
そもそもデータを取得するSQLの最適化を図るべきですが。。。

スレッドでの処理も検討しましたが、Ruby1.8はグリーンスレッドであるため、マルチ処理の恩恵を受けることはできません。
Ruby1.9はネイティブスレッドなので可能なようです。

canvasにローカルの画像ファイルを描画するには

適当なお絵描きサイトをHTML5, canvas, javascriptで作ってました。

その時にやりたかったのが、canvas内にローカルの画像ファイルを描画し、

その画像を背景にお絵描きすることだったんですが、意外につまづいたので、メモしておきます。


仕様の概要はこんな感じです

  • input type="file"を使ってローカルファイルを参照すること
  • 参照した画像ファイルをcanvasに描画すること
  • 画像ファイルをサーバにアップロードしないこと(ローカルで完結)


ソースは以下の通り

HTML

  <div id="drawArea">
    <canvas id="myCanvas" width="300" height="300"></canvas>
  </div>
  <div id="uploadArea">
    <input id="uploadFile" name="image" type="file" />
  </div>

javascript

  $("#uploadFile").change(function() {
    var canvas = $("#myCanvas");
    var ctx = canvas[0].getContext("2d");

    // 選択されたファイルを取得
    var file = this.files[0];

    // 画像ファイル以外は処理中止
    if (!file.type.match(/^image\/(png|jpeg|gif)$/)) return;

    var image = new Image();
    var reader = new FileReader();

    // File APIを使用し、ローカルファイルを読み込む
    reader.onload = function(evt) {

      // 画像がloadされた後に、canvasに描画する
      image.onload = function() {
        ctx.drawImage(image, 0, 0);
      }

      // 画像のURLをソースに設定
      image.src = evt.target.result;
    }

    // ファイルを読み込み、データをBase64でエンコードされたデータURLにして返す
    reader.readAsDataURL(file);
  });


つまづいたところは以下の部分ですね。

  // 画像がloadされた後に、canvasに描画する

  // ** 失敗 **
  // 最初はこう書いてた 
  // このタイミングだと、imageのloadが終了していないため、canvasに何も描画されなかった(´・ω・`)
  ctx.drawImage(image, 0, 0);

  // ** 成功 **
  // imageのloadが完了してから、drawImageすれはOK
  image.onload = function() {
    ctx.drawImage(image, 0, 0);
  }

div要素の中に画像を追加するといった場合は問題ありませんが、

$("div").append(image); // これでOK

canvasに画像を描画する時は、ロードが完了してからという処理を加えないとダメです。


ちなみに、本来javascriptはローカルファイルを参照できませんが、
HTML5のFile API(FileReader)のおかげでこれができるようになりました。

ドラッグアンドドロップした画像をcanvasに描画する

canvasにローカルの画像ファイルを描画するには - 技術めもとほぼ同じ処理です。

画像の選択をinput type="file"で対応していたのを、ドラッグアンドドロップでできるようにしただけです。


仕様の概要はこんな感じです


ソースは以下の通り

HTML

  <div id="drawArea">
    <canvas id="myCanvas" width="300" height="300"></canvas>
  </div>
  <div id="uploadArea">
    <input id="uploadFile" name="image" type="file" />
  </div>

javascript

  // ドラッグ要素がドロップ要素に重なっている間
  window.addEventListener("dragover", function(evt) {
    evt.preventDefault();  // ブラウザのデフォルトの画像表示処理をOFF
  }, false);

  // ドロップ時
  window.addEventListener("drop", function(evt) {
    var canvas = $("#myCanvas");
    var ctx = canvas[0].getContext("2d");

    evt.preventDefault();  // ブラウザのデフォルトの画像表示処理をOFF
    var file = evt.dataTransfer.files[0];

    if (!file.type.match(/^image\/(png|jpeg|gif)$/)) return;

    var image = new Image();
    var reader = new FileReader();

    reader.onload = function(evt) {
      image.onload = function() {
        ctx.drawImage(image, 0, 0);
      }
      image.src = evt.target.result;
    }
    reader.readAsDataURL(file);
  });

before_filterの実行順番について

before_filterで複数のメソッドを指定した場合、指定した順番で実行されます。
考えてみれば、当たり前だ(´・ω・`)

以前に作ったSNSアプリで、足あととアクセス拒否機能があった訳なんですが、
以下のように記述されていました。

class HogeController < ApplicationController
  before_filter :footprint, :refuse

  private
  # 足あと
  def footprint
  end

  # アクセス拒否
  def refuse
  end
end

上記のようにbefore_filterを記述すると、
足あとをつけて、アクセス拒否するという流れで処理されるため、
必ず、足あとがつくという結果に・・・
(もちろん、アクセス拒否している場合は、足あとはついて欲しくないわけです)

ということでこの場合は、refuseが先に実行されるように記述する必要があります。

また、before_filterで後続のメソッドを実行させたくない場合、
render, redirect_toメソッドでレスポンスを作るか、return falseすることで、
対応します。

class HogeController < ApplicationController
  before_filter :refuse, :footprint # refuseを先に

  private
  # 足あと
  def footprint
  end

  # アクセス拒否
  def refuse
    if refuse?
      # 拒否専用ページをレンダリング
      render 'layouts/refuse.html.erb', :layout => true
    end
  end
end

以上で、対応完了。