読者です 読者をやめる 読者になる 読者になる

49hack

見習いエンジニアが魔法使いになるまで

carrierwaveを使ってS3に画像をアップロードする

画像のアップロードにcarrierwaveを使ってみたのでメモがてらまとめます。 ほかにも、比較的実装が容易なpaperclipやcarrierwaveの後継であるrefileも使ってみましたが、保存先のディレクトリ指定、ファイル名変更、確認画面作成などの要件が満たせなかったので断念しました。

gemのインストール

Gemfileに必要なgemを記述してbundle installでインストールします。

# Gemfile
gem 'carrierwave',github: 'carrierwaveuploader/carrierwave'
gem "fog"
gem 'rmagick'

サーバー複数台構成でELB使ってる場合はキャッシュはローカルではなくS3にしましょう。例えば、確認画面に遷移したときに違うサーバーを見に行って該当ファイルがない!エラー!みたいな不具合が防げます。

キャッシュのS3対応をする場合、carrierwavegithubから取得する必要があります。

また、fogはS3へのアップロード、rmagickはサイズなどの画像情報取得に使います。

アップローダーの作成

アップローダーを作成します。

rails g uploader image
# => app/uploaders/image_uploader.rb

アップローダーのマウントとinitializerの設定

今回はUserモデルのavaterプロパティにマウントしてみます。

独自にfile_sizeのバリデーションも追加してます。

# app/models/user.rb
class User < ActiveRecord::Base
    mount_uploader :avater, ImageUploader

    validate :file_size

    # 5MB以上のファイルはUPLOADできないようにしてみる
    def file_size
        upload_limit = 5.megabytes.to_i
        if photo.file.size > upload_limit
            errors.add(:avater, "のサイズが大きすぎます。")
        end
    end
end
# /config/initializers/carrierwave.rb
CarrierWave.configure do |config|
    config.fog_credentials = {
        provider: 'AWS',
        aws_access_key_id: ****,
        aws_secret_access_key: ****,
        region: 'ap-northeast-1',  # Tokyoの場合
    }
    config.cache_storage = :fog # キャッシュにS3を指定

    # テストとかで同じとこにUPLOADされたくないのでバケットを分けます
    case Rails.env
    when 'production'
        config.fog_directory  = 'bucket_production'
    when 'staging'
        config.fog_directory  = 'bucket_staging'
    when 'development'
        config.fog_directory  = 'bucket_development'
    when 'test'
        config.fog_directory  = 'bucket_test'
    end
end

アップローダーをいろいろカスタマイズ

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

    # 画像のサイズとか取得するためにRMagickをinclude
    include CarrierWave::RMagick
    
    # before_createみたいなもの
    process :store_dimensions, :store_extension

    # ストレージにS3を指定
    storage :fog

    # 画像ごとに保存するディレクトリを変えたいのでオーバーライド
    def store_dir
        # 例えばidごとにディレクトリを分けてみる
        "avater/#{model.id}"
    end

    # ファイル名を書き換える
    def filename
        # 例えば avater_1.jpg みたいなファイル名にしてみる
        "avater_#{model.id}.#{file.extension}" if original_filename
    end

    # キャッシュ先のディレクトリを指定
    def cache_dir
        "cache"
    end

    # RMagickを使って画像の幅、高さを取得する
    def store_dimensions
        if file && model
            img = ::Magick::Image::read(file.file).first
            model.width = img.columns
            model.height = img.rows
        end
    end
    
    # 画像の拡張子を取得する
    def store_extension
        if file && model
            model.extension = file.extension
        end
    end
end

画像名に日本語が使えるようにする

デフォルトだとファイル名に日本語が使われている場合filenameが"____"に変換されてしまうのでinitializerに一行追加する。

# /config/initializers/carrierwave.rb
CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/

確認画面をつくってみる

なにかとめんどくさい確認画面をつくります。(確認画面って日本だけの文化?海外の記事でもなかなか情報がなくて苦戦しました。。)

キャッシュをS3にしている場合、インスタンスをnewしてファイルをアタッチした時点でキャッシュに画像が保存されています。

***_cacheを使うことで再アップロードしなくてもファイルの情報を使いまわすことができます。これをファイルのバリデーションエラーや確認画面に利用します。

モデル.saveした時点で、画像がcache_dirから削除されstore_dirに保存されます。

すべて書くのが面倒なので一部だけ書きます。

# app/views/hoge/confirm.html.slim
...
    = form_for @user, url: "/path/to/create_user" do |f|
        = f.hidden_field avater_cache
        = image_tag @user.avater.url
        = f.submit "アップロード"
...
# app/controllers/hoge_controller.rb
...
    def confirm
        @user = User.new(user_params)
    end

    def create_user
        user = User.create(user_params)
    end

    private
    def user_params
        params.permit(
            ...
            :avater,
            :avater_cache
            ...
        )
    end
...

carrierwaveを使ってみて

今回、画像アップロード機能を実装するにあたり、paperclip→refile→carrierwaveの順で試してみました。

paperclipは実装がかなりカンタンでしたが、確認画面をつくるところで自前実装が必要だとわかり断念しました。。refileに関してはファイルがすべてprivateでしかUPLOADできなかったり?、まだまだ情報が少なかったので諦めました。笑

それに比べてcarrierwaveは自由度が高く、かつ情報を集めやすくて、とっても素敵なライブラリでした。

次回以降もお世話になろうと思います。

参考