Railsでファイルをアップロードする

## 前提
自分の理解を深めるためのメモです。

## やりたいこと
Railsでファイルをアップロードしたい。

## 実装
### コントローラで受け取る
![スクリーンショット 2024-05-21 午後0.17.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/5c59f271-6b99-4a22-18b3-fbf9f5c4323d.png)

POSTリクエストで送られるので、subject_dataコントローラのcreateアクションに送られる。
![スクリーンショット 2024-05-21 午後0.18.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/a3abca32-26f1-b70b-8d78-035312a97759.png)

```ruby:subject_data_controller.rb
class SubjectDataController < ApplicationController
  def create
    uploaded_file = params[:file]
    binding.pry
  end
end
```
フォームで`<%= file_field_tag :file %>`と設定したので、`params[:file]`で受け取れる。
```ruby
pry(#<SubjectDataController>)> uploaded_file
=> #<ActionDispatch::Http::UploadedFile:0x0000000122986098
 @content_type="text/rtf",
 @headers="Content-Disposition: form-data; name=\"file\"; filename=\"ファイルアップロードのサンプル.rtf\"\r\nContent-Type: text/rtf\r\n",
 @original_filename="ファイルアップロードのサンプル.rtf",
 @tempfile=#<File:/var/folders/l_/r0xf9zf9603bhy8ym0lqxsdc0000gn/T/RackMultipart20240521-14058-8hvjxb.rtf>>
```

### 指定のディレクトリに格納する
```ruby
uploaded_file = params[:file]
excel_file_path = Rails.root.join("public/uploads/#{uploaded_file.original_filename}")
File.open(excel_file_path, 'w+b') do |file|
  file.write(uploaded_file.read)
end
```
`Rails.root`でアプリのルートディレクトリを取得できる。
![スクリーンショット 2024-05-21 午後1.55.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/b457cf60-81fb-ff54-96b0-f08854549bca.png)

続く`join`はRubyの`join`メソッドではなく、Railsの`Rails.root.join`でファイルパスを取得できる。

ファイルで送られたファイルデータを`params[:file]`で受け取り、それを`uploaded_file`に代入した。中身を見てみると、original_filenameプロパティーがある。それを使ってファイル名を取得する。
![スクリーンショット 2024-05-21 午後1.57.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/16fe791b-f626-4eda-d7d9-31af3d93132c.png)
そしてファイルパスを`excel_file_path`に代入。（必須ではないがコードが読みやすくなると考えて入れた）

```ruby
File.open(excel_file_path, 'w+b') do |file|
  file.write(uploaded_file.read)
end
```
`File.open`の第１引数はファイルパス、第２引数はモード。
`File.open`は指定のファイルパスからファイルを取得するイメージだが、ファイルが存在しない場合も使える。
ファイルが存在しない場合は、新しくファイルを作成してオープンする。（今回はこっち）

:::note alert
読み込みモードだとエラーになります。書き込みモードのときだけでした。
:::

> ファイルを書き込みモードでオープンします。オープン時にファイルがすでに存在していればその内容を空にします。
https://docs.ruby-lang.org/ja/latest/method/Kernel/m/open.html

開いたファイルを`|file|`とブロックで受け取って処理を渡す理由は、ブロックだと処理が終わったあと自動でクローズしてくれるかららしい。

新しく作成してオープンしたファイルは中身が空なので、さっきフォームから受け取ったファイルの内容を文字列として読み取って、その内容で上書きする。

ファイルに上書きするところの処理で、文字コードのエラーが出た。
![スクリーンショット 2024-05-21 午後1.18.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/3591d630-8c52-1b63-a1d5-816d3375b3a6.png)

なぜだろう？と思っていたが、そうか、`File.open`の第2引数の読み取りモードとか書き取りモードとか設定するところで、バイナリーモードを外していたからだ。さっきはバイナリーモードを設定していたからエラーにならなかったんだ。

> これらのいずれに対しても "b" フラグを ("r+b"のように) つけることができます (整数なら File::BINARY )。この場合、バイナリモードでオープンします (ただし、DOS/Windowsのようにシステムがテキスト／バイナリでファイルを区別する場合に限ります)
https://docs.ruby-lang.org/ja/latest/method/Kernel/m/open.html

![スクリーンショット 2024-05-21 午後1.36.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/02e2ba35-f470-0a5a-3335-16b50c6fa70f.png)

## 完成したコード
```ruby:subject_data_controller.rb
def create
  uploaded_file = params[:file]
  excel_file_path = Rails.root.join("public/uploads/#{uploaded_file.original_filename}")
  File.open(excel_file_path, 'w+b') do |file|
    file.write(uploaded_file.read)
  end
end
```

```erb:subject_data/index.rb
<%= form_with url: "/subject_data", multipart: true do |form| %>
  <%= file_field_tag :file %>
  <%= form.submit %>
<% end %>
```

## 参考
https://railsguides.jp/form_helpers.html#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89

https://qiita.com/jnchito/items/418277508dce263cf4f0