Phoenix/Elixir シンプルなファイルアップロード

# 概要
勉強のため、Phoenix1.6にライブラリを追加せずに、デフォルト設定のままファイルアップロードのコードを書いてみた。

昔のバージョン（1.3）の公式ドキュメントではチュートリアルにファイルアップロードの項目があったみたいなんだけど、
1.6.xには見当たらなかった？ので、せっかくなので記事にしてみた。
（どっか他の場所にあったらすみません。そっ閉じして下さい）

# まずはPhoenixプロジェクトの作成
環境はこんな感じです。

```
$ elixir -v
Erlang/OTP 24 [erts-12.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit] [dtrace]

Elixir 1.13.1 (compiled with Erlang/OTP 24)
```

端末の任意の場所で以下のコマンドを実行
※ここで`--database sqlite3`としているのは、SQLite3をDBとして使っているため。
後でアップロードしたファイルの格納先のパスを保存したりしようかなと思います。（予定は未定）

```
$ mix phx.new my_app --database sqlite3
<省略>
Fetch and install dependencies? [Yn] Y #Yを選択
<省略>
$
```

終了したら、案内に従って以下を実行

```
$ cd my_app
$ mix ecto.create
<省略>
$ iex -S mix phx.server
```

終了したらブラウザで　[http://127.0.0.1:4000/](http://127.0.0.1:4000/)にアクセス。
画面が表示されたらOK

![スクリーンショット 2022-01-30 11.17.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/49bf2a4e-aa17-12cf-5755-72bb0c76bd82.png)

# 下準備 ファイルの新規作成と編集
とりあず、最低限３つのファイル（controller, view ,template）を作成し、
1つのファイル(router)を編集します。

![スクリーンショット 2022-02-05 22.17.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/9bfcaf8c-5798-9ab4-488a-c850b8bbf4e1.png)

### 1.templateの作成
アップロードフォーム画面を作成します。

`my_app/lib/my_app_web/templates`ディレクトリの下に、`upload`ディレクトリを作成し、
その中に`form.html.heex`を作成します。
`upload`ディレクトリを作るのを忘れずに
![スクリーンショット 2022-02-05 22.38.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/52c0d0ed-8e32-4058-d8f9-fbdd0c9790d2.png)

```eex
<%= form_for @conn, "/upload", [as: :upload, multipart: true], fn f -> %>

<div>
<%= file_input f, :input_file %>
</div>

<div>
<%= submit "送信" %>
</div>
<% end %>
```

### ２.viewの作成
`my_app/lib/my_app_web/views`ディレクトリの下に`upload_view.ex`を作成
記述は以下で完成です。

```elixir
defmodule MyAppWeb.UploadView do
  use MyAppWeb, :view
end
```

### ３.コントローラの作成
`my_app/lib/my_app_web/controllers`ディレクトリの下に`upload_controller.ex`を作成
この時点ではまだアップロードのロジックは書いてません。後で順を追って追記していきます。
まずファイルアップロードのフォームを表示させます。

```elixir
defmodule MyAppWeb.UploadController do
  use MyAppWeb, :controller

  #フォームの表示
  def form(conn, _params) do
    render(conn, "form.html")
  end
end
```

### 4.router.exにルーティング設定追加
`my_app/lib/my_app_web/router.ex`を編集してパスとコントローラとその関数を紐づけます。
![スクリーンショット 2022-02-05 22.48.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/f2bd3814-5f84-2c9a-c96d-e5b9dca34cf6.png)

`scope "/", MyAppWeb do `の中に一行追加します。

```elixir
### 省略 ###
  scope "/", MyAppWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/upload-form", UploadController, :form       #この行を追加
  end
### 省略 ###
```

ここまで終わったらブラウザで　[http://127.0.0.1:4000/upload-form](http://127.0.0.1:4000/upload-form)にアクセス。
シンプル~~すぎる~~画面が表示されました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/708e1f5e-afe8-7b28-f246-a8de4ef5b605.png)

ちょっとここでhtmlを開いてみてみます。

```html
<form action="/upload" enctype="multipart/form-data" method="post">
  <input name="_csrf_token" type="hidden" value="awJacXg0DmQiSXMsFhQ0XTkJJRwyFyku4xh82ezRFz5btux-XmSVKDGF">

  <div>
   <input id="upload_input_file" name="upload[input_file]" type="file">
  </div>

  <div>
   <button type="submit">送信</button>
  </div>
</form>
```
htmlを見るに、ファイルは`/upload`にパラメータ名`upload[input_file]`でPOSTで送信されることが
わかります。
それを受け取るロジックを書いていきます。

※ちなみに`<input name="_csrf_token" ~ `は[CSRF(クロスサイト・リクエスト・フォージェリ)](https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_6.html)対策のために自動的に生成されるコードです。本題から外れていくので詳しくは触れません。

# アップロードロジックの実装その１
### 1.コントローラーの編集
`upload_controller.ex`に関数を追加します。
一旦これだけにしておきます。受け取った内容を`IO.inspect`でコンソールに出してみます。
画面には"OK"だけ返します。

```elixir
def upload_action(conn, %{"upload" => upload}) do
    IO.inspect upload
    json(conn, "OK")
end
```
ちなみに、ここで、actionという名前にしてますが、他のフレームワークのようにactionとつけなければ
ならない規約はありません。なんでもいいです。

### 2.routerの編集
`router.ex`にルーティング設定を追加します。

```elixir
### 省略 ###
  scope "/", MyAppWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/upload-form", UploadController, :form       #追加した行
    post "/upload", UploadController, :upload_action  #この行をさらに追加
  end
### 省略 ###
```
### アップロードしてみる

ファイルはなんでもいいんですが、一応公開記事なので無難なものに汗
↓これをアップロードしてみます。

![logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/c91a6826-78ba-0037-ef0a-8f71880cd4ee.png)



ファイルを選択し、送信。すると。。
![スクリーンショット 2022-02-06 0.08.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/d51749cd-3103-76cd-7704-c06f78e7c11d.png)

”OK”が返ってきました。
![スクリーンショット 2022-02-06 0.18.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/8554db21-93dc-07fb-2010-c53ac6fa8ae4.png)

ここでコンソールをチェックしてみます。

```
%{
  "input_file" => %Plug.Upload{
    content_type: "image/png",
    filename: "logo.png",
    path: "/var/folders/cp/kv9jk2xx06gg24_0_38kbl900000gp/T//plug-1644/multipart-1644074153-276432714408854-4"
  }
}
[info] Sent 200 in 8ms
```
変数`upload`は、Mapであり、中身は"input_file"というキー名で`Plug.Upload`構造体が格納されているようです。`filename`はアップロードしたファイル名、`path`は保存されたファイル名であることの察しがつきます。

（　思ったより情報少ないですね。ファイルサイズとかはpathに記載されてるファイルにアクセスして自分で取得せよってことなんでしょうかね)


では、ファイルを確認してみましょう。chromeでアクセス。しかし
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/ebcf80b7-cb4f-24f2-e172-f5cfe564d940.png)

アクセスできません。どうやら一時的に保存されるだけで処理が終了すると削除されてしまうようです。
なので、ファイルを他の場所に雑にコピーすることにします。

# アップロードロジックの実装その2
### コントローラを編集

`upload_controller.ex`の`def upload_action`を編集します。

```elixir
def upload_action(conn, %{"upload" => upload}) do

    file = upload["input_file"]
    cp_filename = "copy_" <> file.filename
    File.copy(file.path, cp_filename)
    map = Map.from_struct(file)
    result = Map.put(map, :save_filename, cp_filename)
    json(conn, result)
    
end
```
`File.copy`だけいいんですが、せっかくなんで画面にファイル情報を返却させることにしました。
`map = Map.from_struct(file)`で構造体をMapに変換し、（構造体からJsonに直接変換できない）
そのMap中に、保存したファイル名情報を追加したものをjson形式に変換して画面に返却します。


### 再びアップロード

フォームに戻り、画像を選択して送信

![スクリーンショット 2022-02-06 0.50.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/93017bc2-e71a-f296-f66e-28ccd7ac6af5.png)

想定通り画面にファイル情報が返ってきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/aea4b15e-7810-f519-1afb-849c886f6610.png)

端末を確認すると。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2245296/5517871c-e354-61ff-50f7-fbc932480ec7.png)

アップロードしたファイルが保存されています。

DBに保存したファイルのパスを格納したり、UUIDで保存するようにしたり色々出来そうですね。

でも、少し調べてみたらファイルアップロード関連のライブラリも結構あるようなんですよね。
多分、ライブラリ使った方がいいことがあるんでしょうね。