file_columnでファイル名を自由に変更する

In: Ruby / Rails| technology

19 5月 2008

file_columnプラグインは、非常に便利でありがたいのですが。
一点、どうにも不満な点がありました。

それは、ファイル名があくまでアップロードされたファイル名にしかできないこと。

確かに、単純にブログとかでアップロードするならそれでいいと思うんです。
実際、WordPressとかもそうだし。使う側が認識すればいいだけの話。

でも、仕様として、そうでない場面もあるだろう、と、僕は思うのです。
たとえば、ログインユーザー名+タイムスタンプ+拡張子で管理したいとか、そういう要望だって、出す奴は出すと思うのです。

ということで、いろいろ試行錯誤しているうちにできるようになりましたので、そのメモ。


※結論から先に読みたい人はここから飛ばれるとよいかと思います。

考え方

file_columnのアップロード時のロジックはこんな流れになっています。

  1. アップロード直後、いったん画像を保存先ディレクトリのtmp配下に蓄積する。
  2. ActiveRecordのsaveメソッドが走ると、画像は:magickで指定したルールの通りに加工され、ルールに基づいて作られたパスで保存される。
  3. tmpにあった画像を削除する。

このとき、1.の段階でファイル名は2バイト文字は「_」(アンダースコア)でサニタイジングされ、英数字はそのままファイル名として採用されます。

そのため、ファイル名を変えるタイミングは、この辺りになるかと思われます。

  1. 1.でファイル名のサニタイジングがかかる段階で手を加える
  2. 2.でsaveメソッドがかかる前にファイル名をどうにかする

ちなみに、前者をいじることで、ファイル名をランダム文字列にしたり、2バイト文字も許容するつくりにしたりなさっている方もいらっしゃるので、併せて紹介させていただきます。

で、今回、やろうとしているのは後者。
つまるところ、ファイルがアップロードされ、saveメソッドが走る前までに、画像の名前を変えてしまえばいいじゃない、ということです。

実際のコード

結論から言うと、こんな形になりました。

  def create
    @image = Image.new(params[:image])

    unless @image.valid?
      # 登録完了の文言を設定し、初期表示アクションにリダイレクトする.
      render :action => "index"
      return
    end

    # 画像名をシステム側に都合よく変更して再読み込みさせる.
    # e.g. => #{hh}#{mm}photo.(jpg|png|gif)
    hh = sprintf("%02i", @image.hour)
    mm = sprintf("%02i", @image.minute)
    file_information = @image.split_extension(@image.scenery)
    renamed_file = "#{hh}#{mm}photo.#{file_information[1]}"
    old_filename = @image.scenery
    new_filename = File.join(File.dirname(@image.scenery), renamed_file)
    File.rename(old_filename, new_filename)
    # getするとStringだけど、setするときはFileオブジェクトでないとダメ.
    @image.scenery = File.new(new_filename, "r")

    # 画像オブジェクトをDBに登録する.
    @image.save!

    # 登録完了の文言を設定し、初期表示アクションにリダイレクトする.
    flash[:notice] = "投稿を受け付けました。反映されるまでしばらくお待ちください。"
    redirect_to :action => "index"
  rescue => detail
    # エラーログ・画面用のエラーを取得して、初期表示アクションにレンダリングする.
    flash[:error] = "想定外のエラーによりアップロードに失敗しました。管理者にご連絡ください。"
    logger.error(detail.message)
    logger.error(detail.backtrace.join("\n"))
    redirect_to :action => "index"
  end

Tokyo24で使ったソースのかなりの部分の引用で、細かいところは微妙かもしれませんが。。
あのアプリでは、画像ファイルを登録時刻+拡張子で管理したかったので、こんな形になっています。

ちなみに、途中で出てくる「@image.split_extension(@image.scenery)」は、file_columnの中にあった、ファイル名と拡張子を分割するメソッドです。
プラグインはいじりたくなかったので、file_columnのソースをパクってモデルの中に突っ込みました(笑)。

ここでのポイントは2つ。

ひとつめ。
ファイルを取得する際、ファイル名は「@image.scenery」(sceneryは、モデルでfile_columnの指定をしているカラム名と読み替えてください)とすると、絶対パスでファイル名が取れます。
また、「@image.scenery_relative_path」とやると、相対パスでファイル名が取れます。
これらは、file_column側で裏で自動生成してくれています。
これを使えば、ファイル名とアップロードしたファイルオブジェクトが取得できるので、あとは、ファイル名をrenameしてしまえ、というわけ。

ふたつめ。
実際のファイル名を変えても、file_column経由で登録されるファイル名は、古いまんまなので、正しいファイル名を指定しなおします。
しかしここで、単純にカラムにファイル名を入れようすると、こんなエラーになります。


Do not know how to handle a string with value 'hogehoge.jpg' that was passed to a file_column. Check if the form's encoding has been set to 'multipart/form-data'.

正しく入れるべきは、Stringではなく、ファイルオブジェクト。
そんなわけで20行目のような形でFile.newをしています。

結論として

file_columnに関しては、以下、2点をポイントとして押さえておけば、いろいろ応用が利きそうです。

  1. file_columnで指定したカラムのgetterからは画像の絶対パスが入った文字列が返ってくる
  2. file_columnで指定したカラムのsetterにはファイルオブジェクトを入れないとダメ

これをうまく使えば、マイグレーションで後からファイルアドレスが変わったりしても、なんとかやれるはず。
とはいえ、結構無理やりやってる印象が拭えませんので、ほかにうまいやり方があったら教えてもらえると嬉しいです。

Via:

あと、file_columnプラグインの中のコメントほぼ全部(笑)


Comment Form

About this blog

ゆるーく、ふわーっと、興味のままに。

自分のかたわらに置いておくメモ代わり。

Photostream