基礎Ruby on Rails #22 Chapter13 ファイルのアップロード

前 [基礎Ruby on Rails #21 Chapter12 アセット・パイプライン](https://qiita.com/tseno/items/a917be7a6d3f4dd9a80d)
次 [基礎Ruby on Rails #23 Chapter13 複数画像のアップロード／順番の入れ替え](https://qiita.com/tseno/items/5fcc1625625de0acc851)

## Active Storageとは

### Active Storageのしくみ

- Rails 5.2で導入された機能
- Amazon S3、Google Cloud Storage、Microsoft Azure Storage等のクラウドスレレージへ簡単にアップロードできる。
- クラウドストレージに保存されたファイルを配信する手段も提供する。
- 事前署名付きURLにより配信され、有効期限（5分）が設定されている。一般に公開された状態にはならない。

### ディスクサービス

- Active Storageは、ローカルサーバーのディスクにファイルをアップロードすることもできる。アップロード先をディスクサービスという。
- ディスクサービスも個別のURLが与えられ、リダイレクションが行われるが、有効期限は設定されない。

## セットアップ手順

### ImageMagickのインストール

- Active Storageには画像のサイズを変換する機能があり、利用するにはImage Magickが必要なので、インストールする。

```sh:WSL/Ubuntu
$ sudo apt-get install -y imagemagick
```

```sh:Mac
$ brew install imagemagick
```

### Gemパッケージmini_magickの導入

- mini_magickを導入するために、Gemfileのコメントアウトを外す。

```rb:Gemfile（一部）
# Use ActiveStorage variant
gem 'mini_magick', '~> 4.8'
```

```sh
$ bundle install
```

### マイグレーションスクリプトの生成と実行

- 以下のコマンドを実行して、Active Storage Tableを作成して、マイグレーションスクリプトを実行する。

```sh
$ bin/rails active_storage:install
Copied migration 20181015125809_create_active_storage_tables.active_storage.rb from active_storage
```

```sh
$ bin/rails db:migrate
Copied migration 20181015125809_create_active_storage_tables.active_storage.rb from active_storage
== 20181015125809 CreateActiveStorageTables: migrating ========================
-- create_table(:active_storage_blobs)
   -> 0.0022s
-- create_table(:active_storage_attachments)
   -> 0.0014s
== 20181015125809 CreateActiveStorageTables: migrated (0.0048s) ===============
```

- 以上で、Active Storageの準備が整った。

# プロフィール画像のアップロードと表示

## Memberモデルの拡張

### クラスメソッドhas_one_attached、クラスメソッドattribute

- 属性`profile_picture`
    - Active Storageが提供するクラスメソッドhas_one_attachedを使うと、モデルオブジェクトに対して1個のファイルを添付できるようになる。
    - 1個のファイルを添付するための属性をモデルクラスに追加する。`has_one_attached :profile_picture`を追加する。属性`profile_picture`が追加される。
- 属性`new_profile_picture`
    - モデルに読み可能な属性である、`attribute :new_profile_picture`を追加する。あたかも`new_profile_picture`があるようにプログラミングできる。一時的に保存するために使用する。

```rb:app/models/member.rb（一部）
class Member < ApplicationRecord
  has_secure_password

  has_many :entries, dependent: :destroy
  has_one_attached :profile_picture
  attribute :new_profile_picture
```

## 「プロフィール画像」フィールドの設置

- tableタグ内の最上位に、以下のファイルアップロード用部品を追加する。

```erb:app/views/shared/_member_form.html.erb（一部）
  <tr>
    <th><%= form.label :new_profile_picture %></th>
    <td><%= form.file_field :new_profile_picture %></td>
  </tr>
```

- 日本語ローケールテキストに、`new_profile_picture: プロフィール画像`を追加する。

```erb:config/locales/ja.yml（一部）
    attributes:
      member:
        new_profile_picture: プロフィール画像
```

- MembersControllerのストロング・パラメーターに`:new_profile_picture`を追加する。

```rb:app/controllers/members_controller.rb（一部）
  private def member_params
    attrs = [
        :new_profile_picture,
```

- AccountsControllerも同様、ストロング・パラメーターに`:new_profile_picture`を追加する。

```rb:app/controllers/accounts_controller.rb（一部）
  private def account_params
    params.require(:account).permit(
        :new_profile_picture,
```

- まだアップロード機能は実装していないが、とりあえず適当な画像を選択して、更新してもエラーが出なければOK。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/afd18787-35c3-2f13-cc69-1f3fe28074df.png)

## 画像アップロード機能の実装

- モデルクラスに、以下のメソッドを追加する。これで画像アップロード機能が動くようになる。
- いきなり`profile_picture`に入れてしまうと、アップロード時に即保存されてしまうので、`new_profile_picture`と`profile_picture`に分けて、バリデーションを働かせる必要がある。

```rb:app/models/member.rb（一部）
  before_save do
    if new_profile_picture
      self.profile_picture = new_profile_picture
      # self.profile_picture.attach(new_profile_picture) でも可
    end
  end
```

## アップロードされた画像の表示

- `@member.profile_picture.attached?`は、プロフィール画像が添付されているかどうか。
- `.variant(resize: "128x128")`は、128x128ピクセル内で最も大きいサイズで出力する。

```erb:app/views/members/_body.html.erb（一部）
<table class="attr">
  <tr>
    <th>プロフィール画像</th>
    <td>
      <% if @member.profile_picture.attached? %>
        <%= image_tag @member.profile_picture.variant(resize: "128x128") %>
      <% end %>
    </td>
  </tr>
```

- 画像が保存されたことを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/0dde36d1-7264-b3ba-6b7c-48f80d7b5d3b.png)

## シードデータ

- `db/seeds/development/profile.png`を置く。
- 以下を、シードデータの一番下に追記する。

```rb:db/seeds/development/members.rb（一部）
filename = "profile.png"
path = Rails.root.join(__dir__, filename)
m = Member.find_by!(number: 10)

File.open(path) do |f|
  m.profile_picture.attach(io: f, filename: filename)
end
```

```sh
$ bin/rails db:rebuild
```

- ファイルが入っていることを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/2878e85b-77b5-0c93-fd9d-31bc933d3372.png)

## ファイルのデータ形式に関するバリデーション

- 画像以外のファイルが登録されたとき、表示時にエラーが出るのでバリデーションを追加する。
- モデルの親クラス`ApplicationRecord`に、`ALLOWED_CONTENT_TYPES`という配列の定数を追加する。

```rb:app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  ALLOWED_CONTENT_TYPES = %q{
    image/jpeg
    image/png
    image/gif
    image/bmp
  }
end
```

- 以下のように、バリデーションを追加する。

```rb:app/models/member.rb（一部）
  validate if: :new_profile_picture do
    if new_profile_picture.respond_to?(:content_type)
      unless new_profile_picture.content_type.in?(ALLOWED_CONTENT_TYPES)
        errors.add(:new_profile_picture, :invalid_image_type)
      end
    else
      errors.add(:new_profile_picture, :invalid)
    end
  end
```

- 日本語ローケールテキストに、`new_profile_picture: プロフィール画像`を追加する。

```erb:config/locales/ja.yml（一部）
    errors:
      messages:

        invalid_image_type: にはJPEG、PNG、GIF、BMP形式の画像を指定してください。
```

- バリデーションが動いたことが確認できた。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/b49e8e08-c0db-cc36-e33c-5f76af7abef3.png)

# プロフィール画像の削除

## 添付ファイルの削除

- モデルオブジェクトに添付されたファイルを削除するには、`purge`メソッドを使う。

```sh
$ bin/rails c
irb(main):002:0> m.profile_picture.attached?
#(省略)
=> true
irb(main):003:0> m.profile_picture.purge
  Disk Storage (0.4ms) Deleted file from key: GMqCoLJjozzq5KVq3jMxUffM
  Disk Storage (0.6ms) Deleted files by key prefix: variants/GMqCoLJjozzq5KVq3jMxUffM/
#(省略)
=> nil
irb(main):004:0> m.profile_picture.attached?
=> false
```

- 削除されていることが確認した。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/171b1dc6-b481-8ec2-cac8-ac67bf6c82aa.png)

- モデルに画像削除のための新項目`remove_profile_picture`を追加する。
- `elsif remove_profile_picture`がtrueになったとき、`self.profile_picture.purge`（削除）の処理を追加。

```rb:app/models/member.rb（一部）
  has_many :entries, dependent: :destroy
  has_one_attached :profile_picture
  attribute :new_profile_picture
  attribute :remove_profile_picture, :boolean
#（省略）
  before_save do
    if new_profile_picture
      self.profile_picture = new_profile_picture
      # self.profile_picture.attach(new_profile_picture) でも可
    elsif remove_profile_picture
      self.profile_picture.purge
    end
  end
```

## チェックボックスの設置

- もし画像が添付されている場合は、その画像とともに削除用にチェックボックスを設置する。

```erb:app/views/shared/_member_form.html.erb（一部）
  <tr>
    <th><%= form.label :new_profile_picture %></th>
    <td>
      <%= form.file_field :new_profile_picture %>
      <% if @member.profile_picture.attached? %>
        <div>
          <%= image_tag @member.profile_picture.variant(resize: "128x128") %>
          <%= form.check_box :remove_profile_picture %>
          <%= form.label :remove_profile_picture %>
        </div>
      <% end %>
    </td>
  </tr>
```

- ロケールテキストに、日本語でのラベル名`remove_profile_picture: 画像を削除`を追加する。

```rb:config/locales/ja.yml（一部）
    attributes:
      member:
        new_profile_picture: プロフィール画像
        remove_profile_picture: 画像を削除
```

- `:remove_profile_picture`のストロング・パラメータを追加する。AccountsControllerとMembersControllerに追加する。

```rb:app/controllers/accounts_controller.rb（一部）
  private def account_params
    params.require(:account).permit(
        :new_profile_picture,
        :remove_profile_picture,
        :number,
        :name,
        :full_name,
        :sex,
        :birthday,
        :email
    )
  end
```

```rb:app/controllers/members_controller.rb（一部）
  private def member_params
    attrs = [
        :new_profile_picture,
        :remove_profile_picture,
        :number,
        :name,
        :full_name,
        :sex,
        :birthday,
        :email,
        :administrator
    ]
```

- 画像を削除チェックボックスをオンにして更新すると、画像が削除される。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/5d8aa799-c4fe-daf1-0a30-fd511554414e.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/4bacf961-f292-fa68-931f-1ba874f1f711.png)

### まとめ

- 画像ファイルのアップロードは`profile_picture`、`new_profile_picture`、`remove_profile_picture`の3つの項目を使うことで、添付と削除が行えることがわかった。

参考
[改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)](https://www.amazon.co.jp/%E6%94%B9%E8%A8%824%E7%89%88-Ruby-Rails-IMPRESS-KISO/dp/429500460X/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=1536666358&sr=1-1)