著作一覧 |
大体400MBくらいのファイルをクライアントから上げてもらってそれをRails側で処理する必要が出てきた。
仕組み上、400MBの送信がクライアント-nginxとnginx-Unicornで必要となる。このときnginxとUnicornが同じマシンであればこの間の通信は無駄なので避けたほうが良い。
というわけで検索したらRailsで大きなファイルを扱う際のポイントという記事を見つけたのだが、今は2018年だ。nginx-upload-moduleはapt-getでインストールできるが、実際に動作しているnginx()では動かなかった。検索すると、自分でソースにパッチしてmakeすれば良いというようなのは見つかったが、それはデプロイを考えた場合避けたい。
さらに検索するとNginxを使ってファイルアップロードを高速化する方法というのが見つかった。
こちらは、nginxにリクエストボディをファイル出力させて、それをRails側で利用する(出力したファイルのファイル名はリクエストヘッダで受け取る)ことにしていて、筋が良さそうだ。
しかし、想定がリクエストボディに直接ファイルの内容を出力(ブラウザーを介さずプログラムtoプログラムでのファイルアップロード)ことを想定しているように見える(あと、nginx.confとRails側でヘッダ変数名の書き間違いがあるのでそのままで使えない)。
以下のようにした。
まずnginx側でリクエストボディをそのままファイルとして残すことは変えない。
location /upload { limit_except POST { deny all; } client_body_in_file_only clean; client_body_temp_path /tmp/; client_body_buffer_size 128K; client_max_body_size 500M; proxy_set_header X-File $request_body_file; proxy_set_body 0; proxy_pass_request_body off; proxy_pass http://unix:/tmp/sockets/unicorn.sock; }
client_body_in_file_only clean;
で仮にRails側の処理中に例外ですっ飛んでもnginx側でファイルを消させるようにした。Rails側で消せるのであれば、client_body_in_file_only on;
で良い。proxy_set_body
でUnicornに転送するリクエストボディは0にした(意味わからないが、offとすれば良いと書いてあるのを見てoffと記述したらリクエストボディにoffという文字列が送られてきた。proxy_set_headerでContent-Length:0としてみたが、それだとrackがIOエラーで死ぬ(リクエストボディが残っているからだ)。どうにかproxy_set_bodyで空文字列を設定できないかと試したが無理っぽいので意味はないが0としてみたが、?とか*とかのほうが良かったかも。proxy_pass_request_body off;
はどうも有効ではないが、残してある。
Unicoornへ送るリクエストボディの設定については、nginxのproxy_moduleの説明だとproxy_pass_request_bodyとproxy_set_headerで良さそうなのだが実際にはうまく動作しなかった。
これを書いていて、ふとrackのバグじゃないかと気づいたが、上の設定で落ち着いているのでとりあえずこのままだ。
参考:proxy_moduleの説明のproxy_pass_request_bodyの記述例(rackがうまく処理しない)
location /x-accel-redirect-here/ { proxy_method GET; proxy_pass_request_body off; proxy_set_header Content-Length "";
とりあえず、これでnginxは/uploadに対しては、/tmpに00000001.tmpみたいな名前のファイルを作って、リクエストヘッダのX-File変数に実ファイル名を入れてくるのでRails側でファイル処理が可能となる。
問題は、該当ファイルのパーミッションがwww-dataのrw-------となることだ。
Railsをsudoersで動かしているのでsystem "sudo chmod a+rw #{request.headers['X-File']}"
で逃げた。
次はこのファイルの読み方だが、ブラウザーがファイルをアップロードしてくるのでマルチパートで読む必要がある。調べると、multipart-parserというGemがあった。SAXのように読みながらイベントを通知するタイプのようなので、それなりの速度は維持できるだろう。2012年に更新されてから静かだが、1度作れば落ち着くタイプなのでむしろ更新が無いのは良い知らせだろう。
が、使い方がさっぱりわからん。
しょうがないので、unit testのソースを見て使い方を調べる。ParserとReaderの2つのクラスのいずれかを使って処理を記述すれば良いということがわかった。バウンダリーを与えてReaderを作り(Parserをそのまま利用するより楽そうだ)、readerのon_errorとon_partにブロックを与えて全体を処理し、各パートの中身はon_partのブロックパラメータのon_dataで行えば良いということがわかった。
試しにユニットテストを実行してみた。すると、テストが通らない。
見るとfixtureがおかしい。しょうがないのでプルリクを投げた(今日マージされたのでこれ書いている)。
コントローラは以下のように処理する。
require 'multipart_parser/reader' ... # ファイルアップロード受信処理 def upload # バウンダリーを与えてReaderのインスタンスを生成する。 reader = MultipartParser::Reader.new(extract_boundary(request.headers['Content-Type'])) # SAXというよりも、XHRの使い方に似ている。 reader.on_error do |msg| # エラーの場合のイベント処理 logger.debug("on_error:#{msg}") end # パーティションを見つけた場合のイベント処理 reader.on_part do |part| logger.debug("on part:#{part.filename}, #{part.name}, #{part.mime}"); # RailsのCSRF対策用パラメータ if part.name == 'authenticity_token' # パーティション内の読み取りでon_dataイベントが通知される part.on_data do |token| unless valid_authenticity_token?(session, token) # 403を返したほうが良いかも知れないが412を返す(まともに使えば出るはずないのでいい加減) render html: '412 Precondition Failed', status: 412 return else logger.debug('good token') end end # パーティションの終了通知 part.on_end do logger.debug('end of authenticity_token part') end else #ここではauthenticity_tokenとfileしか見ないが、他のパラメータもあればその処理もあるはず file_model = FileModel.new(part.filename) part.on_datda do |file_line| # ファイル処理 file_model.add(file_line) end part.on_end do logger.debug('end of file') file_model.do_fantastic end end end # ファイルパース開始 File.open(request.headers['X-File'], 'r').each_line do |line| # 1行単位にリーダーに与える reader.write(line) end.close head :no_content end BOUNDARY_MARK = 'boundary=' def extract_boundary(ctype) ctype[ctype.index(BOUNDARY_MARK) + BOUNDARY_MARK.length..-1] end
ジェズイットを見習え |