Relative Path プラグインのStatic File Filter の使い方

Rails 勉強会@関西 第6回に参加したり、他の情報収集を行ったりして、/ 以外の Location で Rails アプリケーションを公開する方法について調べましたので、メモとして公開します。

まず新しく http://d.hatena.ne.jp/someeda/20060313 の記事について一歩さんに教えてもらいました。

このページではどのような Location に Rails アプリケーションを配備するかが分かっている状態でその Location の静的コンテンツなどを適切に表示可能なように routes.rb の編集やシンボリックリンクの作成を行うという方法です。

他に maiha さんの stylesheet controller という手法について氏久さんから教えてもらいました。http://wota.jp/ac/?date=20050808
stylesheet controller では、スタイルシートでよくある問題を解決する方法です。CSS では絶対パスで指定する必要があるプロパティがよくあります。そのとき、画像ファイルのパスが検証環境は /images/ で、運用環境のパスが /app1/images/ であったりする場合はいちいち書き換える必要があり大変面倒です。

それを解決するために、maiha さんの方法では stylesheet controller という専用のコントローラを用意して、スタイルシートの出力を View で定義してやるようにしています。

これも単純で良い方法でしょう。

私も同様の問題を解決するために Relative Path プラグインを作成しました。

好きな Location で Rails アプリケーションを運用可能にするために、すべてのリンクを相対パスに変更するようにしています。利用者からすると、Relative Path プラグインを利用する方が手間が少なく楽でしょう。それに、開発環境・運用環境の違いをうまく吸収してくれます。

スタイルシートの問題を解決するために Relative Path プラグインに Static File Filter 機能を実装しました。これは、MongrelWEBrickCSS 等の静的ファイルの出力時にその出力内容を動的に変更する機能です。
# Static File Filter は2ヶ月も前に作成した機能なのに何も説明を書かなくてすいませんでした。

この機能を作成しようと思う動機となった Streamlined の CSS には次のような記述があります。

 html .mac_os_x_nw {
        background-color: transparent;
        background-image: none;
        filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="../themes/mac_os_x/TL_Main.png", sizingMethod="crop");

この AlphaImageLoader の src の指定ではユーザが現在閲覧中のページからの相対パスもしくは、絶対パスでの指定を要求します。

しかし、絶対パスでの指定は避けたいところでした。

絶対パスの指定は単純な構成であれば対応が容易かもしれませんが、実際に運用するような Web ページでは次のような構成がよくあります。

  • サーバ1: Pound や Apache のリバースプロキシ /app1 をサーバ2に送る
  • サーバ2: Ruby On Rails のアプリケーション

この構成で、サーバ1の /app1/controller/action への要求をサーバ2の /controller/action によって処理するような場合です。

このとき、ブラウザが要求している URL は http://server1/app1/controller/action になりますが、Rails アプリケーションサーバにとっての REQUEST_URIhttp://server2/controller/action になります。

さらに問題を困難にする要求が現在閲覧中のページからの相対パスが必要という点です。

/windows_js/themes/mac_os_x.css を表示するときに /controller/action 経由の要求なのか、/controller/action/index 経由なのかによって、返すべき相対パスが異なるのです。

これらの要求を達成するには、CSS を取得するときに経由したページの絶対パスを取得できるようにするか、それか CSS ファイルへのリンクを書き換えて、現在ユーザが閲覧中のページを QUERY_STRING 等で渡すようにするかの2択でした。

現在閲覧中のページを QUERY_STRING で渡す方法が実現可能なアイデアであることは分かっていましたが、次の問題によってその案は採用しないことにしました。それは、

  • Streamlined のソースへの改変量を少なく済ませたい
    • Streamlined のバージョンアップへの対応を容易にする
  • Relative Path プラグインの利用者の手間を少なくしたい

からです。

そのため、Relative Path プラグインでの絶対パスの取得できるようにしています。

この絶対パスの取得ロジックは時間があるときにまた書きたいと思います。

具体的な Relative Path プラグインの使い方についてですが、Streamlined への対応のためなら下記のようにします。

RelativePath.register_filter /\.css/,%r(src="(\.\./themes.+?)") do |env,match|
  referer = env["HTTP_REFERER"]
  if (req_uri = env["CLIENT_REQUEST_URI"]) && referer then
    referer = URI(referer)
    rel_path = req_uri + match[1] - referer
    "src=\"#{rel_path}\""
  else
    match[0]
  end
end

RelaivePath.register_filter は2つの正規表現とブロックを引数とします。

最初の引数は、そのフィルタを適用するファイル名の条件を表す正規表現です。第二引数は、そのファイル内の変更したい箇所を示す正規表現です。ブロックには、2つ引数があり最初のものが、その実行時での 環境を示し、第二引数が 変更したい箇所の MatchData オブジェクトになります。

MongrelWEBrick のサーバが持っている環境です。そして、特殊な値として env["CLIENT_REQUEST_URI"] というその CSS ファイルがブラウザが要求するときの URI があります。

上記の例では、src="../themes/mac_os_x/T_Main.png" といった記述がその CSS ファイルからの相対パスで記述されています。その CSS ファイルからの相対パスを、現在表示中のページからの相対パスに変更するために、req_uri + match[1] - referer という演算を行っています。

req_uri + match[1] によって、その png ファイルへの絶対URI を取得でき、それから referer を引くことで現在表示中のページからの相対パスになります。

RelativePath.register_filter はブロックの返り値で、第二引数の正規表現にマッチした箇所全体を置換します。そのため、上記のように、何も置換しない場合はmatch[0] を返すことが必要です。この仕様は近いうちに nil がかえるときは match[0] で置換するというように仕様を変更するかもしれません。

RelativePath.register_filter は複数回実行でき、登録した順に実行されます。

Relative Path プラグイン関係でもう少し書きたいことがありますが、とりあえずこれまで。