はしがき
「とりあえずニュアンスで手を出してみて,難しいことはエラーが出てから考える」という主義のため,何か始めるたびにいたる所で躓きまくります。
Ruby っぽさに惹かれて Crystal に手を出した時もそうでしたが,今回 Kemal で個人サイト(Markdownベースの簡易CMS)を再構築した際にも大いに躓きました。
というわけで,何となく便利そうだなと Kemal に手を出してモノの見事に蹴躓き,四苦八苦した挙句に泥臭くも解決できたり,結局できなかったりしたアレコレについてまとめてみました。
何とかなったこと
リクエストパスの複数階層にマッチするルート設定
今回作成した簡易CMSは「リクエストパスに対応したMarkdown形式のコンテンツファイルを,HTML化した上で ecr テンプレートに埋め込んで表示する」というだけのものです。
コンテンツファイルはあらかじめ指定されたコンテンツディレクトリに収められていて,コンテンツディレクトリ以下のディレクトリ階層がそのままリクエストパスに対応するような動きのため,ルートパス以下の複数階層全てにマッチするルートを指定できると非常にありがたい。
というわけで,別のコンテンツでも書きましたが,Kemal のインスパイア元である Sinatra のドキュメントなども参考にしつつ試行錯誤してみたところ,
get "/*path" do |env|
env.params["path"]
end
みたいな感じでなんとかなりました。
404ページの変更
Kemal でリクエストパスに対応したStaticファイルもマッチするルートも存在しない場合に表示される404ページ。トルコ帽とお髭がカワイイのですが,サイトの雰囲気に合わせて変更しようとしてもソース中でハードコーディングされているのでお手軽に変更ができません。
このように Kemal 本体のエラー処理に手を入れるのはかなり敷居が高いのですが,要はオリジナルのエラーページコンテンツをステータスコード404と一緒に返せれば良いわけです。
Kemal ではルート処理のブロックに渡されるパラメータ(一般的にenv
で受ける場合が多い)の #response
を呼ぶと,HTTP::Server::Response
オブジェクトを取得できるので,HTTP::Server::Response#status_code=
メソッドを使用してステータスコードを指定することができます。
つまり,ルート設定の最後に全リクエストパスにマッチするルートを用意して,その中でステータスコード404を指定してやると,そこまでにマッチしなかったリクエストパスに対してオリジナルの404ページを表示できます。
get "/*" do |env|
env.response.status_code = 404
render_original_error_404()
end
当然他のステータスコードも指定できますので,リクエストヘッダに If-Modified-Since:
が指定されていたらコンテンツのタイムスタンプを元に 304
を返す,とかもできると思います。
Kemal.logのローテーション
Kemal はデフォルトでアクセスログを標準出力に吐き出しますが,Kemal.config.env = "production"
と指定すると,実行時のカレントディレクトリに kemal.log が作られてそちらにログが書き出されるようになります。ファイル名は固定で放っておくとどんどん kemal.log が大きくなってしまうので,定期的にローテーションしてやりたくなります。
最終的に,以下のような手順で一旦 Kemal アプリ(kemalapp(仮))を終了し,ログファイルをrenameしたのちに再起動するというユーティリティアプリ(kemalsrv(仮))作成し,cron で定期的に実行することにしました。(実際にはパラメータとして start|stop|restart|status
などを受け取れる管理用コマンドとして実装)
-
アプリの起動時にプロセスIDを記録
kemalapp.crFile.open("kemalapp.pid") do |pid_file| pid_file.puts Process.pid end
-
ユーティリティアプリでPIDファイルに記録されたプロセスIDでアプリが起動しているかどうかチェック
kemalsrv.crpid = File.read("./kemalapp.pid").to_i actived = `ps -c -p #{pid}` =~ /kemalapp/
-
アプリが起動していれば一旦終了
kemalsrv.crif actived Process.kill(Signal::INT, pid) end
-
ログファイルが空でなければリネーム
kemalsrv.crif File.file?("./kemal.log") && File.size("./kemal.log") > 0 File.rename("./kemal.log", "./log/" + Time.now.to_s("%Y%m%d%H%M%D") + ".log") end
-
Kemalアプリを再起動
kemalsrv.crProcess.new("./kemalapp")
結局できなかったこと
ecr テンプレートの動的指定
出力HTMLのレンダリングに使用する ecr テンプレート名をコンテンツファイルごとに指定できれば,テンプレートの種類に拡張性が持たせられるんではないか,と思ったわけです。
get "/*path" do |env|
path = env.params["path"]
content = site.content(path)
render content.template # => content.templateの返り値で実行時にecrファイルを指定
end
ただ,Crystal における ecr の実装ってバリバリのマクロ依存になってるんですよね。Kemal の render
マクロはもちろん,標準ライブラリの ecr 関連ソースも,ecr テンプレートを構文解析した結果をマクロでソースコード内に展開しているような感じの動きです。
でもって,コア開発者である Ary Borenszweig さんのGoogleグループでの発言を見ると,マクロの展開は通常の変数や式が解釈される前に完了してしまうようなので,マクロに変数経由で値を渡すことは根本的に難しいのかなぁと。
public ディレクトリの DirectoryIndex 処理変更
css や画像など静的コンテンツをお手軽に公開できる public ディレクトリですが,リクエストパスとしてpublic 配下のディレクトリを指定すると,ディレクトリ内のファイル一覧が表示されます。これは個人的にはあまり嬉しくない仕様です。Apache の DirectoryIndex 指定のように,特定のコンテンツを表示させるか,もしくはいっそ403(Forbidden)を返して欲しいところ。
ただ,調べてみると public ディレクトリを処理する Kemal::StaticFileHandler
は,一部の例外処理1を加えた以外は,HTTP::StaticFileHandler
そのもののようで,このHTTP::StaticFileHandler
では,リクエストパスがディレクトリだった場合,問答無用でディレクトリ内のファイルリストを表示する以外の選択肢がありません。
というわけでひとまずこの件はペンディング。
HTTP::StaticFileHandler
を拡張してオリジナルの StaticFileHandler
を実装したりすれば解決できるのかしら?
-
ルートパスへのリクエスト時に
Kemal::StaticFileHandler
での処理をスキップする ↩