LoginSignup
7
7

More than 5 years have passed since last update.

[Crystal][Kemal] Kemalで躓いた小石たち

Last updated at Posted at 2016-02-08

はしがき

とりあえずニュアンスで手を出してみて,難しいことはエラーが出てから考える」という主義のため,何か始めるたびにいたる所で躓きまくります。

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 などを受け取れる管理用コマンドとして実装)

  1. アプリの起動時にプロセスIDを記録

    kemalapp.cr
    File.open("kemalapp.pid") do |pid_file|
     pid_file.puts Process.pid
    end
    
  2. ユーティリティアプリでPIDファイルに記録されたプロセスIDでアプリが起動しているかどうかチェック

    kemalsrv.cr
    pid = File.read("./kemalapp.pid").to_i
    actived = `ps -c -p #{pid}` =~ /kemalapp/
    
  3. アプリが起動していれば一旦終了

    kemalsrv.cr
    if actived
      Process.kill(Signal::INT, pid)
    end
    
  4. ログファイルが空でなければリネーム

    kemalsrv.cr
    if 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
    
  5. Kemalアプリを再起動

    kemalsrv.cr
      Process.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 の実装ってバリバリのマクロ依存になってるんですよね。Kemalrender マクロはもちろん,標準ライブラリの 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 を実装したりすれば解決できるのかしら?



  1. ルートパスへのリクエスト時に Kemal::StaticFileHandler での処理をスキップする 

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7