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

# <i class="fa fa-file-text-o"></i> はしがき

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

**Ruby** っぽさに惹かれて **Crystal** に手を出した時も[そう](http://qiita.com/arcage/items/d948e8e79d9b87bb84b3)でしたが，今回 **Kemal** で個人サイト（Markdownベースの簡易CMS）を再構築した際にも大いに躓きました。

というわけで，何となく便利そうだなと **Kemal** に手を出してモノの見事に蹴躓き，四苦八苦した挙句に泥臭くも解決できたり，結局できなかったりしたアレコレについてまとめてみました。

# <i class="fa fa-smile-o"></i> 何とかなったこと

## <i class="fa fa-code"></i> リクエストパスの複数階層にマッチするルート設定
今回作成した簡易CMSは「リクエストパスに対応したMarkdown形式のコンテンツファイルを，HTML化した上で ecr テンプレートに埋め込んで表示する」というだけのものです。

コンテンツファイルはあらかじめ指定されたコンテンツディレクトリに収められていて，コンテンツディレクトリ以下のディレクトリ階層がそのままリクエストパスに対応するような動きのため，ルートパス以下の複数階層全てにマッチするルートを指定できると非常にありがたい。

というわけで，[別のコンテンツ](http://qiita.com/arcage/items/0efcbcd9899e9ee11123)でも書きましたが，**Kemal** のインスパイア元である **Sinatra** のドキュメントなども参考にしつつ試行錯誤してみたところ，

```rb
get "/*path" do |env|
  env.params["path"]
end
```

みたいな感じでなんとかなりました。

## <i class="fa fa-code"></i> 404ページの変更

**Kemal** でリクエストパスに対応したStaticファイルもマッチするルートも存在しない場合に表示される404ページ。トルコ帽とお髭がカワイイのですが，サイトの雰囲気に合わせて変更しようとしてもソース中でハードコーディングされているのでお手軽に変更ができません。

このように **Kemal** 本体のエラー処理に手を入れるのはかなり敷居が高いのですが，要はオリジナルのエラーページコンテンツをステータスコード404と一緒に返せれば良いわけです。

**Kemal** ではルート処理のブロックに渡されるパラメータ（一般的に`env`で受ける場合が多い）の `#response` を呼ぶと，`HTTP::Server::Response` オブジェクトを取得できるので，`HTTP::Server::Response#status_code=` メソッドを使用してステータスコードを指定することができます。

つまり，ルート設定の最後に全リクエストパスにマッチするルートを用意して，その中でステータスコード404を指定してやると，そこまでにマッチしなかったリクエストパスに対してオリジナルの404ページを表示できます。

```rb
get "/*" do |env|
  env.response.status_code = 404
  render_original_error_404()
end
```

当然他のステータスコードも指定できますので，リクエストヘッダに `If-Modified-Since:` が指定されていたらコンテンツのタイムスタンプを元に `304` を返す，とかもできると思います。

## <i class="fa fa-code"></i> Kemal.logのローテーション

**Kemal** はデフォルトでアクセスログを標準出力に吐き出しますが，`Kemal.config.env = "production"` と指定すると，実行時のカレントディレクトリに kemal.log が作られてそちらにログが書き出されるようになります。ファイル名は固定で放っておくとどんどん kemal.log が大きくなってしまうので，定期的にローテーションしてやりたくなります。

最終的に，以下のような手順で一旦 **Kemal** アプリ（kemalapp(仮)）を終了し，ログファイルをrenameしたのちに再起動するというユーティリティアプリ（kemalsrv(仮)）作成し，cron で定期的に実行することにしました。（実際にはパラメータとして `start|stop|restart|status` などを受け取れる管理用コマンドとして実装）

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

    ```rb:kemalapp.cr
    File.open("kemalapp.pid") do |pid_file|
     pid_file.puts Process.pid
    end
    ```

2. ユーティリティアプリでPIDファイルに記録されたプロセスIDでアプリが起動しているかどうかチェック

    ```rb:kemalsrv.cr
    pid = File.read("./kemalapp.pid").to_i
    actived = `ps -c -p #{pid}` =~ /kemalapp/
    ```

3. アプリが起動していれば一旦終了

    ```rb:kemalsrv.cr
    if actived
      Process.kill(Signal::INT, pid)
    end
    ```

4. ログファイルが空でなければリネーム

    ```rb: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アプリを再起動

    ```rb:kemalsrv.cr
      Process.new("./kemalapp")
    ```

# <i class="fa fa-frown-o"></i> 結局できなかったこと

## <i class="fa fa-exclamation-circle"></i> ecr テンプレートの動的指定

出力HTMLのレンダリングに使用する ecr テンプレート名をコンテンツファイルごとに指定できれば，テンプレートの種類に拡張性が持たせられるんではないか，と思ったわけです。

```rb:理想（動きません）
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グループでの発言](https://groups.google.com/forum/#!msg/crystal-lang/QydWhOSATSc/rwAatgqsegkJ)を見ると，マクロの展開は通常の変数や式が解釈される前に完了してしまうようなので，マクロに変数経由で値を渡すことは根本的に難しいのかなぁと。

## <i class="fa fa-exclamation-circle"></i> public ディレクトリの DirectoryIndex 処理変更

css や画像など静的コンテンツをお手軽に公開できる public ディレクトリですが，リクエストパスとしてpublic 配下のディレクトリを指定すると，ディレクトリ内のファイル一覧が表示されます。これは個人的にはあまり嬉しくない仕様です。Apache の DirectoryIndex 指定のように，特定のコンテンツを表示させるか，もしくはいっそ403（Forbidden）を返して欲しいところ。

ただ，調べてみると public ディレクトリを処理する `Kemal::StaticFileHandler` は，一部の例外処理[^※]を加えた以外は，`HTTP::StaticFileHandler` そのもののようで，この`HTTP::StaticFileHandler` では，リクエストパスがディレクトリだった場合，問答無用でディレクトリ内のファイルリストを表示する以外の選択肢がありません。

というわけでひとまずこの件はペンディング。

`HTTP::StaticFileHandler` を拡張してオリジナルの `StaticFileHandler` を実装したりすれば解決できるのかしら？

---
[^※]: ルートパスへのリクエスト時に `Kemal::StaticFileHandler` での処理をスキップする