Rails+CarrierwaveにGzip圧縮するProcessを追加する

独自のProcessを追加する方法が知りたかったので調べてみました。

# ポイント

ポイントは、

1. `/tmp` フォルダにアップロードされたファイル（`current_path`）を加工して上書きする
2. ファイル名は`full_filename`メソッドを上書きして変更する
3. Processでは`/tmp`内への保存まででよく、モデルがsaveされると`store!`（`/uploads`や`S3`へ転送）される

といった感じでした。

## 注意点
`store`される時にバージョンのファイル名が特定できないと転送されないので、Processの中でファイル名を変えたりとかはNGな感じです。かっこ悪いけど都度`full_filename`に定義した方がよさそうです。
まあ、Processでファイル名を変えると他のProcessと共存しにくくなるので、これでいいのかもです。

# サンプルソース

[ダウンロード / Gist](https://gist.github.com/goosys/864fba5dcc8a765e1f313f6a48250835)

```ruby:config/initializers/extensions/carrierwave/processings/gzip.rb
module CarrierWave
  module Gzip
    extend ActiveSupport::Concern

    module ClassMethods
      def gzip_compress
        process :gzip
      end
    end

    def gzip_compress
      # move upload to local cache
      cache_stored_file! if !cached?

      directory = File.dirname( current_path )

      # move upload to tmp file - encoding result will be saved to
      # original file name
      tmp_path   = File.join( directory, "tmpfile" )
      File.rename current_path, tmp_path

      # compress
      content = File.read(tmp_path)
      gzip_content = ActiveSupport::Gzip.compress(content)
      File.open(current_path, "wb") do |f|
        f.write(gzip_content)
      end

      # delete tmp file
      File.delete tmp_path
    end

    private
      def prepare!
        cache_stored_file! if !cached?
      end
  end
end
```

# 使い方

```ruby:app/uploaders/image_uploader.rb
class ImageUploader < CarrierWave::Uploader::Base

  include CarrierWave::RMagick
  include CarrierWave::Gzip

  storage :file

  # Gzip version
  version :gz do
    process :gzip_compress
    def full_filename(for_file)
      for_file + ".gz"
    end
  end

  # 既存のprocessとの組み合わせも可能
  # Thumbnail & Gzip version
  version :thumb do
    process :resize_to_fit => [100, 100]
    process :gzip_compress
    def full_filename(for_file)
      super(for_file) + ".gz"
    end
  end

  # gz版があればgzのURLを、なければoriginalのURLを返す
  def gzurl
    if self.gz.present?
      self.gz.url
    else
      self.url
    end
  end
end
```

この設定でファイルをアップロードすると、アップロードフォルダはこんな感じになります。

```
original.png
original.png.gz
thumb_original.png.gz
```

あとはWEBサーバーで、
`Content-Encoding: gzip`出してあげたり、gzなしのリクエストでも`Accept-Encoding:gzip`があればgzありのファイルを返してあげたりしてあればよさげです。

# ついでに

本番ではなかなかRailsから静的ファイルを配信したりしないですが、開発環境だと`rails s`してすぐ動作確認したい方もいると思うので、Railsからもちゃんと`Content-Encoding: gzip`を出しておければいいかなと思います。
そこで、ミドルウェアというものを作ってヘッダーを差し込んでみます。

## やってみる

[ダウンロード / Gist](https://gist.github.com/goosys/622a64b4bc399c666592f07479cb1975)

```ruby:config/initializers/middlewares/gzip_header.rb
module Rack
  class GzipHeader

    def initialize(app, options = {})
      @app = app
      @start_with = options[:start_with] || '/'
      @end_with   = options[:end_with]   || '.gz'
    end

    def call(env)
      status, headers, body = result = @app.call(env)

      case status
      when 200  # OK
        path = URI.unescape(env['PATH_INFO'])
        if path.start_with?(@start_with) && path.end_with?(@end_with)
          enc = env['HTTP_ACCEPT_ENCODING']
          if enc && enc.include?('gzip')
            result[1]['Content-Encoding'] = 'gzip'
          end
        end
      end
      result
    end

  end
end
```

## 使ってみる

```ruby:config/application.rb
module App
  class Application < Rails::Application
    config.middleware.insert_before ActionDispatch::Static, 
      "Rack::GzipHeader", {
        start_with: "/uploads/", 
        end_with:   ".gz"
      }

```

これで、「`/uploads/`以下の`.gz`ファイルが`Accept-Encoding:gzip`でリクエストされていたら`Content-Encoding: gzip`をヘッダーに追加する」ことが出来ました。