[Rails]CarrierWaveの学習

# gem「CarrierWave」とは
Railsアプリから簡単にファイルをアップロードでき、またアップロードしたファイルを扱うための便利なメソッドが使えるgem。


## 利用イメージ
gemをインストールした上で、以下の仕込みを行う。

- アップロード対象ファイル(以降fileと呼ぶ)のuploaderを作成
- fileを扱うmodelにuploaderを連携、アップロード情報を扱うカラム(以降upload_infoと呼ぶ)を設定
- fileを扱うテーブルにカラムupload_infoを設定(string型)

すると、gemによって拡張されたインスタンスメソッドupload_infoを使い、
``model.upload_info = file`` とすると、
uploaderによってfileがアップロードされ、
テーブルのupload_infoカラムにアップロード先のファイルパスが書き込まれる。

また、その他のgemで拡張されたインスタンスメソッドで色々便利にfileを扱えるようになる。


# 導入
前提: Rails6系の場合

## gemのインストール

```
# Gemfileへ以下を記述
gem 'carrierwave', '~> 2.0'
```

# 利用手順
## ①前提

あらかじめ、以下を決定しておく必要がある。

- アップロード情報を扱うテーブルとカラム名
- アップロード先

ここでは、犬の成長記録を管理するテーブル``dog_records``があり、
その中に犬の画像ファイルのアップロード情報を扱うカラム``dog_image``があるものとする。
また、アップロード先は、Railsアプリケーションが動作するマシン上の``/dog_images/``であるものとする。


以降の実装するものと、その動作イメージを以下に示す。

![carrierwave-ページ4.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59542/8deb49f5-da30-4adf-bfc6-1908e86a44e4.png)





## ②アップローダーの生成

```
# アップローダーファイルの生成
rails generate uploader dog_image
=> app/uploders/dog_image_uploader.rb が生成される
```

## ③アップローダーの設定

```dog_image_uploader.rb
class DogImageUploader < CarrierWave::Uploader::Base
  # アップロード先を記述(ローカルのパスでもS3でも可らしい)
  def store_dir
    # 相対パスで記載すると、"public/uploads/dog_images" にアップロードされる。
    'uploads/dog_images'
  end
end
```

## ④アップロード情報のカラムとアップローダーとの連携

```dog_record.rb
class DogRecord < ApplicationRecord
  # 「dog_image」カラムに「DogImageUploader」を連携
  mount_uploader :dog_image, DogImageUploader
end
```

## ⑤テーブルにアップロード情報のカラムを追加
``dog_records``テーブルに``dog_image``カラムをstring型で追加する。

## ⑥ファイルのアップロードをするAPIのアクションメソッドの設定
### 前提
APIでのファイルの送信方法は、主に以下2つがあるらしい。

**(A) multipart/form-data を利用して送信**
**(B) Base64化してJSONに文字列としてセットして送信**

いろいろな情報があり、実装コスト、ファイルサイズのコスト、処理スピードなどいろいろな意見があったが、
イマイチ反対のことをいっている部分があり、これらについてよくわからなかった。

・・・が、(B)の方法を取らないと、そもそもJSONでデータを送れないので、
**JSONでやり取りする体系をとっているAPI群をサービスとして提供する場合、ファイル送信を扱うAPIだけ特異点になってしまう**
**画像ファイル以外にもデータを送りたいときにJSONでないとやりづらそう**
**(B)でやる場合のサーバー側の実装を見てみたらすごく単純だったが、(A)の方はいまいちよくわからなかった**

という3点から(B)を採用することにした。


### 本題
※クライアント側からPOSTメソッドでリクエストボディに「Base64で文字列化した画像データ」を挿入してリクエスト
→サーバー側で受け取って、Base64画像データをデコードして画像ファイルを取り出し、アップローダーに紐付いているカラムにセットする

という流れになる。

また、ここでは``dog_records_controller.rb``の``upload``アクションメソッドで、ファイルをアップロードするものを例とする。
また、リクエストボディで送信するパラメータは以下のようなものとする。

```
{
  "image": {
    "file_name": "<保存するファイル名>"
    "base64": "<Base64エンコードした画像ファイルの文字列>"
  }
}
```


```dog_records_controller.rb
def upload
  # Base64文字列化された画像をデコードしてデータ(バイナリ)を取り出す
  dog_image_binary = Base64.decode64(params[:image][base64])
  # ファイル名を取り出す
  dog_image_file_name = params[:image][file_name]

  # ファイル名を名前と拡張子に分離
  dog_image_file_basename = File.basename(dog_image_file_name, ".*")
  dog_image_file_extname = File.extname(dog_image_file_name)

  
  # <ファイル名>で空っぽのテンポラリファイルを作成
  # テンポラリファイルは「Dir.tmpdir」に保存される。
  # 自動的に削除はされないので、不要になった時点で「unlink」メソッドで削除する)
  dog_image_temp_file = Tempfile.new([dog_image_file_basename,dog_image_file_extname])

  # テンポラリファイルをバイナリモードにする
  dog_image_temp_file.binmode
  
  # バイナリ状態の画像データを書き込む
  dog_image_temp_file.write(dog_image_binary)

  # 対象テーブルのレコードを扱うモデルのインスタンスを生成
  dog_record = DogRecord.new()

  # carrierwaveのuploaderが紐づくカラムに画像データファイル(File型)の値をセットする
  dog_record.dog_image = dog_image_temp_file

  # 対象テーブルにレコードをsaveする
  # ・・・すると、ファイルが「store_dir」へアップロードされ、「dog_records」テーブルの「dog_image」カラムへアップロード情報が書き込まれる
  dog_record.save!
end
```

## ⑦アップロード情報を扱う

DBに保存したアップロード情報は、以下のように操作できる(例として)

```
# DogRecordのインスタンスを取得
dog_record = DogRecord.first

# アップロードファイルのURLを取得
dog_record.dog_image.url

# アップロードファイルのパスを取得
dog_record.dog_image.current_path

# アップロードファイル名を取得
dog_record.dog_image_identifier

# アップロードファイル情報があるか確認
dog_record.dog_image.file.nil?

# アップロードファイル情報を削除
dog_record.remove_dog_image!
dog_record.save
```

# 動作確認

```
# クライアント側
## 1. 適当な画像ファイルをローカルに用意してBase64でエンコード
base64 <画像ファイルのパス>
=>
表示されたBase64文字列をコピペしておく

## 2. POSTメソッドでbase64で文字列化した画像データを挿入してリクエスト(postmanなりcurlなり)
## ※以下のJSONキーバリューをリクエストボディへセット
{
  "image": {
    "file_name": "<保存するファイル名>"
    "base64": "<Base64エンコードした画像ファイルの文字列>"
  }
}

# サーバー側
## 3. public/<store_dirのパス>に画像が保存されていることを確認する
```

## 参考

- [GitHub](https://github.com/carrierwaveuploader/carrierwave)

**APIでのファイルアップロード方法**

- [WebAPI でファイルをアップロードする方法アレコレ](https://qiita.com/mserizawa/items/7f1b9e5077fd3a9d336b)
- [画像データをサーバーにPOSTする](https://raahii.github.io/posts/files-upload/)
- [POST時の Content-Type: application/json vs multipart/form-data vs application/x-www-form-urlencoded](https://qiita.com/ikamirin/items/4ab8599ec00d9ce3890d)
- [ファイルをS3にアップロードするときはサーバー?クライアント?](https://kenny27.hatenablog.com/entry/2019/06/07/233541)
- [[Rails6]CarrierWaveを基本に忠実に使ってみる~キャッシュ・S3編~](https://yuruli.info/using-carrierwave-2-on-rails-6/)

**ファイルの扱い**

- https://docs.ruby-lang.org/ja/latest/class/Tempfile.html
- [Ruby Tempfileの使い方](https://keruuweb.com/ruby-tempfile%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9/)
- [RubyのTempfileに拡張子を設定する](https://koeda.me/posts/d481c99e820e44c38473894aa7e22223rubytempfile)
- [Rubyでファイルの名前と拡張子を取得する](https://www.xmisao.com/2013/11/29/ruby-file-basename-extname.html)