CarrierWaveのファイル名変換(original_filename)の挙動

ファイルアップロード機構にCarrierWaveを利用しています。
アップロードしたファイル名は、

self.file_column.file.original_filename

のように取得出来ますが、デフォルトでは日本語を利用出来ません。
ファイル名に日本語を利用する場合、
./config/initializers/carrierwave.rb

CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/

のように設定します。
ここまではググればすぐ出てくる情報なのですが、
「Simple (1).csv」というファイル名が、「Simple__1_.csv」に変換されることが分かりました。

CarrierWaveのコードを見てみると、先の設定は、
./vendor/bundles/ruby/2.1.0/gems/carrierwave-0.10.0/lib/carrierwave/sanitized_file.rb

    def sanitize(name)
      name = name.gsub("\\", "/") # work-around for IE
      name = File.basename(name)
      name = name.gsub(sanitize_regexp,"_")
      name = "_#{name}" if name =~ /\A\.+\z/
      name = "unnamed" if name.size == 0
      return name.mb_chars.to_s
    end

上記の、

name = name.gsub(sanitize_regexp,"_")

で変換されます。
指定した正規表現を「_」に変換するわけです。
[:word:]はPOSIX文字クラスで「単語構成文字」を表現する正規表現になりますので、ここを調整すると修正出来そうです。

http://docs.ruby-lang.org/ja/1.9.3/doc/spec=2fregexp.html
調べたところ、

[:print:] 表示可能な文字(空白を含む)

という文字クラスがあり、これを使えばファイル名に制限を無くすことが出来るはず。

./config/initializers/carrierwave.rb

CarrierWave::SanitizedFile.sanitize_regexp = /[^[:print:]]/

としたところ予想通り変換を回避する事が出来たのですが、念のため色々な記号をファイル名に使い試したところ、「+」が「半角スペース」に変換されてしまう事が分かりました。

どうもコードの意図通りに動いていないようです。

pry(main)> "+".gsub("[^[:print:]]","_")
=> "+"

やはり変換されない。

./vendor/bundles/ruby/2.1.0/gems/carrierwave-0.10.0/lib/carrierwave/sanitized_file.rb

    def sanitize(name)
      name = name.gsub("\\", "/") # work-around for IE
      name = File.basename(name)
      name = name.gsub(sanitize_regexp,"_")
      name = "_#{name}" if name =~ /\A\.+\z/
      name = "unnamed" if name.size == 0
      return name.mb_chars.to_s
    end

に渡ってくるパラメータ(name)を見てみたところ、すでに「+」が半角スペースになっていました。
これはコアの動きっぽいと感じつつ、小一時間デバックをしてみたところ原因が分かりました。

./vendor/bundles/ruby/2.1.0/gems/rack-1.5.2/lib/rack/multipart/parser.rb

      def get_filename(head)
        filename = nil
        if head =~ RFC2183
          filename = Hash[head.scan(DISPPARM)]['filename']
          filename = $1 if filename and filename =~ /^"(.*)"$/
        elsif head =~ BROKEN_QUOTED
          filename = $1
        elsif head =~ BROKEN_UNQUOTED
          filename = $1
        end
        if filename && filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
          filename = Utils.unescape(filename)
        end
        if filename && filename !~ /\\[^\\"]/
          filename = filename.gsub(/\\(.)/, '\1')
        end
        filename
      end

上記の、

filename = Utils.unescape(filename)

が犯人でした。
このメソッドの実態は、
./vendor/bundles/ruby/2.1.0/gems/rack-1.5.2/lib/rack/utils.rb

    if defined?(::Encoding)
      def unescape(s, encoding = Encoding::UTF_8)
        URI.decode_www_form_component(s, encoding)
      end
    else
      def unescape(s, encoding = nil)
        URI.decode_www_form_component(s, encoding)
      end
    end
    module_function :unescape

で、この中の、

URI.decode_www_form_component

が変換処理を行っていました。
http://docs.ruby-lang.org/ja/2.0.0/method/URI/s/decode_www_form_component.html
ドキュメントを見ると、「"+" という文字は空白文字にデコードします」とあります。

すっきりしました。

ファイル名に"+"が使えないのはRails(Rack)の仕様という事でよさそうです。