Rails + unicorn で nginx-upload-moduleを使ってみた

この記事は、[nginx Advent Calendar 2015](http://qiita.com/advent-calendar/2015/nginx)の5日目の記事です。

# nginx-upload-module
nginxでは、クライアントから `multipart/form-data` でファイルのアップロードを始めると `/tmp` 配下に一時ファイルを作ります。nginxは、クライアントがファイルをアップロードし終わると、ミドルウェアに一時ファイルからreadして受け渡します。
この仕組みだと、サーバ内で同じファイルを何度もコピーし直すこととなり、数MBのファイルでは問題ありませんが、GB単位のファイルになるとクライアントがタイムアウトしたり、ディスクの容量が足りなくなったりと問題が生じてきます。

これを解決するのが `nginx-upload-module`です。`nginx-upload-module` はnginxでファイルを取得し、そのパスやファイル名などをミドルウェアに渡します。ファイルそのものではなくパスやファイル名のみを渡すので、サーバ内でのファイルの受け渡しがなくなり、前述のような問題が起きません。

この `nginx-upload-module` ですが、2008年から更新は止まっていて、現在の nginx で使えるかどうかは書いてありませんが、今回は *nginx-1.9.6* で試してみました。
https://github.com/vkholodkov/nginx-upload-module

今回やっていること&目的は以下のブログに書いてあることとほぼ同じですので、基本ここを参考にしていきます。この記事では、この設定ではまったことなどを書いていきます。

> Railsで大きなファイルを扱う際のポイント
> http://techracho.bpsinc.jp/baba/2014_10_08/19139

## 環境

* CentOS 6.7
* nginx 1.9.6
* nginx-upload-module 2.2.0

## ダウンロードとコンパイル
必要なソースコード等をダウンロードし、`--add-module` オプションを付けてnginxをコンパイルします。

```shell-session
$ git clone -b release-1.9.6 https://github.com/nginx/nginx.git
$ git clone https://github.com/vkholodkov/nginx-upload-module.git'
$ cd nginx
$ auto/configure (略) --add-module=../nginx-upload-module"
$ make -j4
$ sudo make install
```

## 設定

今回は、以下のような設定をしました。

```nginx
    location / {
      try_files $uri @application;

      if ($args ~ ngx_upload_module=on ) {
        upload_pass @application;
        upload_pass_form_field ".*";
        upload_store /tmp;
        upload_store_access user:rw group:rw all:rw;
        upload_resumable on;
        upload_set_form_field "$upload_field_name[filename]"   "$upload_file_name";
        upload_set_form_field "$upload_field_name[type]"       "$upload_content_type";
        upload_set_form_field "$upload_field_name[tempfile]"   "$upload_tmp_path";
        upload_aggregate_form_field "$upload_field_name[md5]"  "$upload_file_md5";
        upload_aggregate_form_field "$upload_field_name[size]" "$upload_file_size";
        upload_cleanup 200-599;
      }
    }
```

どのようなオプションが設定できるかは、参考の記事や [公式のドキュメント](http://www.grid.net.ru/nginx/upload.en.html) を見ると良いです。

ここでは、特に注意したいところについて書きます。

## 引数でモジュールの使用をスイッチする

参考の記事や多くの情報では、nginxの設定に `nginx-upload-module` を使いたいパスの`location`を書けというのが多いですが、これだと汎用性が低く、他のリソースにも適用したい時に不便です。

なので、if文を用いてURLに`ngx_upload_module` 引数が付いている場合のみ、`nginx-upload-module`を使用します。これにより、新しく対応したいリソースが増えた場合にもnginxの設定を触る必要はありません。

この場合、Rails側のビューファイルも変更してあげる必要があります。
`form_for`を利用している場合は、`url`オプションを使って、パラメーターを渡してあげます。

```haml
= form_for @upload, url: uploads_path(ngx_upload_module: 'on') do |f|
```

## 自動的にファイルを削除する
今回作ったアプリケーションでは、サーバが受け取ったあとはGoogle Cloud Storageにアップロードされるため、サーバ内に保存しておく必要がありません。

`nginx-upload-module`はそのまま使用するとファイルを残してしまうので、あっという間にディスクがいっぱいになってしまいました。
最初は試行錯誤していたのですが、`upload_cleanup`というオプションがあり、ステータスコードを指定することでそのステータスコードの場合のみ削除できるようです。
本来は、エラー時などに消す目的のようですが、今回はアップロードファイルは全て削除したいので、以下のように `200-599` と指定して常に削除するようにします。

```nginx
upload_cleanup 200-599;
```

## コントローラー
ほぼ参考記事と同じで、リファクタリングをした程度なので割愛します
ちなみに、開発環境や`nginx-upload-module`を使っていない場合でも動作するようになっています

```rb
  def upload_params
    params.require(:uploads).permit(:name).merge(file: upload_file, upload_filename: upload_filename)
  end

  def upload_file
    file = params[:uploads][:file]
    if file.is_a?(Hash) && file[:tempfile]
      file[:tempfile]
    else
      file.path
    end
  end

  def upload_filename
    file = params[:uploads][:file]
    if file.is_a?(Hash) && file[:filename]
      file[:filename]
    else
      file.original_filename
    end
  end
```

# まとめ
`nginx-upload-module`を用いて、アップロードファイルを効率良く処理する方法でした。
正直、`nginx-upload-module`がどれだけメンテされてるかわからないですし、GB単位の大きいファイルをアップロードさせる必要がある場合のみ対処すれば良いと思います。

今回はアップロードの話でしたが、ダウンロードの場合は、`X-Sendfile`や`X-Accel-Redirect`ヘッダーを用いるとRailsで認証を行い、nginxがファイルを送るといったことも可能です。