#Safari、IEでファイルをダウンロードする際の文字化対策
既存moduleの動作を上書きしてしまうことで解決できる。
class ApplicationController < ActionController::Base
def send_file_headers!(options)
super(options)
match = /(.+); filename="(.+)"/.match(headers['Content-Disposition'])
encoded = URI.encode_www_form_component(match[2])
headers['Content-Disposition'] = "#{match[1]}; filename*=UTF-8''#{encoded}" unless encoded == match[2]
end
end
ApplicationControllerにこのメソッドを定義すれば終わり!
以下は解説になります
Railsで日本語のファイル名をDLしようとするとSafariやIEで文字化けする
この様な状況に陥ってないでしょうか。
- send_fileメソッドを用いてファイルを送ると日本語が文字化けする
- ローカル環境ではうまくいくけれど、AWSサーバなどの本番環境だけで文字化けする
- Chromeでは日本語で正常にDLできる
- SafariとIEでDLすると文字化けする
- サーバの文字コードやファイル名などをチェックしても問題なさそう
send_fileを用いてファイルをダウンロードさせようとすると、SafariやChromeにて文字化けすることがある。
def dowmload_file file_name, type:
file_path = Rails.root.join('public', 'file', "解説書.pdf")
send_file file_path, filename: "解説書.pdf", type: type
end
原因:Railsで実装されているDataStreaming Moduleが対応していない
send_fileメソッドを実行した際に呼び出される、ActionControllerのDataStreamingモジュールがSafariとIEの際に、
文字コードをエンコードせずに送信するためこのバグが起きている様です。
公式リファレンス :ActionController->DataStreaming
http://api.rubyonrails.org/classes/ActionController/DataStreaming.html
非推奨:alias_method_chainでのmodule書き換え
alias_method_chainを用いて既存のDataStreaming Moduleの処理を書き換えます。
このmoduleをシステムに読み込ませてDLするとあら不思議、日本語でDLできます。
module ActionController
module DataStreaming
def send_file_headers_with_utf8!(options)
send_file_headers_without_utf8!(options)
match = /(.+); filename="(.+)"/.match(headers['Content-Disposition'])
encoded = URI.encode_www_form_component(match[2])
headers['Content-Disposition'] = "#{match[1]}; filename*=UTF-8''#{encoded}" unless encoded == match[2]
end
alias_method_chain :send_file_headers!, :utf8
end
end
この様なモンキーパッチを一時的にシステムに組み込むことでRailsのバージョンが上がったて修正された際には
この部分のみ取り除くことでRails自体のソースコードに手を出すという闇から逃げられます。
#alias_method_chainはRails5から非推奨なのでprependで書き換えた
module自体を書き換えることで、次回以降にprependやincludeした際にも変更が有効になります
=>つまり複数箇所でmoduleをincludeやprependするならこの方式で書きましょう。
class ApplicationController < ActionController::Base
#ActionController::Baseのソースを読むとここで様々なmoduleをincludeしている
module DataStreamPatch
def send_file_headers!(options)
super(options)
match = /(.+); filename="(.+)"/.match(headers['Content-Disposition'])
encoded = URI.encode_www_form_component(match[2])
headers['Content-Disposition'] = "#{match[1]}; filename*=UTF-8''#{encoded}" unless encoded == match[2]
end
end
module ActionController
module DataStreaming
prepend DataStreamPatch
end
end
#moduleの動作を書き換えた上で再度includeする必要がある。
include ActionController::DataStreaming
....
end
#複数箇所でmoduleを利用しない場合はover writeで実装したほうが単純明快
もし複数箇所でこのmoduleを別々にprependしたりという用途で使わない場合はApplicationControllerにこのように定義してしまうのが
単純明快で良い!
class ApplicationController < ActionController::Base
def send_file_headers!(options)
super(options)
match = /(.+); filename="(.+)"/.match(headers['Content-Disposition'])
encoded = URI.encode_www_form_component(match[2])
headers['Content-Disposition'] = "#{match[1]}; filename*=UTF-8''#{encoded}" unless encoded == match[2]
end
end
紆余曲折した結果今提供しているサービスではこの実装で動いています。
#より詳細な解説はこの記事を参照ください
なぜこのような実装になるのか、なぜこれで解決するのかはこの記事に全てまとめました。
是非こちらを参照ください。