Phoenix 1.4-rc.1のChange Logの部分を読む①

こんにちは。Phoenix 1.4-rc.0が出ましたね。brunchがwebpackになったりcowboy2系がサポートされたりと色々と自分的には嬉しい変更が多いです。ですのでPhoenix 1.4-rc.0でどういう部分が変わったのか、Change Logを見ながらコードベースで読んでいき、備忘録的に残していきたいと思います。そして、rc.1も出ましたので、rc.0のChange Logを読み終わったらrc.1での変更点も読んでいきたいと思います。

**※ 間違いが場合がある可能性があります、その場合はコメントいただけますと嬉しいです**

## 読んだChange Log

まずはControllerの変更から、

```
[Controller] Support partial file downloads with :offset and :length options to send_download/3
```
の部分を読んでみようと思います。
変更点として、ファイル等のダウンロードにサイズとオフセットが追加されました。ここを読んでいきたいと思います。

## コード

では早速読んでいきたいと思います。

### 読んだ部分

まずはファイルベースと関数です、

```
Phoenix
    phoenix/controller.ex
        - send_download
        - prepare_send_download
Plug
    plug/conn.ex
        - put_resp_content_type
    plug/adapter/cowboy2/conn.ex
        - send_file
```

### Phoenix
#### send_download
一番最初に呼ばれるのは`send_download`になります（っといより、Change Logにここ変更したって書いてるし）。
`send_download`関数自体は小さくなっているのでまとめて読んでみましょう。


```elixir:phoenix/controller.ex
  def send_download(conn, {:file, path}, opts) do
    filename = opts[:filename] || Path.basename(path)
    offset = opts[:offset] || 0
    length = opts[:length] || :all
    conn
    |> prepare_send_download(filename, opts)
    |> send_file(conn.status || 200, path, offset, length)
  end

  def send_download(conn, {:binary, contents}, opts) do
    filename = opts[:filename] || raise ":filename option is required when sending binary download"
    conn
    |> prepare_send_download(filename, opts)
    |> send_resp(conn.status || 200, contents)
  end
```
第二引数のパターンマッチで別れていますね。`offset`と`length`オプションがあるのは上の方のみです。
やってることは単純、`filename`、`offset`、`length`オプションを取り出します。
その後に`prepare_send_download`関数と`send_file`関数を読んで終了。単純ですね！

#### prepare_send_download

ではでは、ここらでメインとなりうる`prepare_send_download`関数は以下のようになっています。

```elixir:controller.ex
  defp prepare_send_download(conn, filename, opts) do
    content_type = opts[:content_type] || MIME.from_path(filename)
    encoded_filename = URI.encode_www_form(filename)
    warn_if_ajax(conn)
    conn
    |> put_resp_content_type(content_type, opts[:charset])
    |> put_resp_header("content-disposition", ~s[attachment; filename="#{encoded_filename}"])
  end
```
オプションでcontent_typeを指定していたら、オプションのcontent_typeを利用し、オプションで指定していなかったらファイルからcontent_typeを取得します。
次にファイル名をURL用にフォーマとします。
`warn_if_ajax`関数ではリクエストが`ajax`なのか判定します。判断基準はリクエストヘッダー内に`x-requested-with`があるかどうかと`x-requested-with`の値が`XMLHttpRequest`, `xmlhttpreques`かどうかでチェックします。

次に`put_resp_content_type`に入るのですがここからは`Plug`ライブラリで実装されている部分になります。

### Plug

さて、みんな大好き`Plug`です。今回利用しているPlugのバージョンは`1.6.4`なのでバージョンを揃えて読んでいきます。

#### put_resp_content_type

`put_resp_content_type` 関数ですが以下のようになっています。

```elixir:plug/conn.ex
  def put_resp_content_type(conn, content_type, charset \\ "utf-8")

  def put_resp_content_type(conn, content_type, nil) when is_binary(content_type) do
    put_resp_header(conn, "content-type", content_type)
  end

  def put_resp_content_type(conn, content_type, charset)
      when is_binary(content_type) and is_binary(charset) do
    put_resp_header(conn, "content-type", "#{content_type}; charset=#{charset}")
  end
```
`charset`が`nil`の場合は、上のようが呼ばれてヘッダーに`content-type`ヘッダーが追加されるだけですね。もしオプションで`charset`をつけている場合は下の関数が呼ばれて`content-type`ヘッダーに`charset`がついて送信されます。

#### put_resp_header

後から出ていきますがついでに`put_resp_header`関数もここで読んでみましょう。

```elixir:plug/conn.ex
  def put_resp_header(%Conn{adapter: adapter, resp_headers: headers} = conn, key, value)
      when is_binary(key) and is_binary(value) do
    validate_header_key_if_test!(adapter, key)
    validate_header_value!(key, value)
    %{conn | resp_headers: List.keystore(headers, key, 0, {key, value})}
  end
```
`validate_header_key_if_test`関数でヘッダーのキーが正しいかチェックしてます（バイナリになっているかとか、大文字になっていないかとか）。
`validate_header_value`関数で値が正しいかどうかチェックします。
最終的に作らてたヘッダーのリストに追加し`conn`へ追加すればヘッダーの追加が完了です。

ここまでで、Phoenix側の`prepare_send_download`関数は読み終えました。
次に`send_file`関数の中を読んでいきたいと思います。

#### send_file

おさらいで`send_file`が呼ばれたときの引数を見ておきましょう。

```elixir
send_file(conn.status || 200, path, offset, length) # 第一引数はパイプでconnが入ってきます。
```

`send_file`関数自体も`Plug.Conn`モジュール内で定義されています。
では早速`send_file`関数を読んでいきましょう

```elixir:plug/conn.ex
  def send_file(conn, status, file, offset \\ 0, length \\ :all)

  def send_file(%Conn{state: state}, status, _file, _offset, _length)
      when not (state in @unsent) do
    _ = Plug.Conn.Status.code(status)
    raise AlreadySentError
  end

  def send_file(
        %Conn{adapter: {adapter, payload}, owner: owner} = conn,
        status,
        file,
        offset,
        length
      )
      when is_binary(file) do
    if file =~ "\0" do
      raise ArgumentError, "cannot send_file/5 with null byte"
    end

    conn =
      run_before_send(%{conn | status: Plug.Conn.Status.code(status), resp_body: nil}, :set_file)

    {:ok, body, payload} =
      adapter.send_file(payload, conn.status, conn.resp_headers, file, offset, length)

    send(owner, @already_sent)
    %{conn | adapter: {adapter, payload}, state: :file, resp_body: body}
  end
```
ここでも２つ、パターンマッチで関数がわけられていますが、上の方はエラー処理なんで今回は無視。

まずは最初のif文でファイル名が`"\0"`かどうかチェック
`run_before_send`関数でbefore_sendっとして登録されている関数を実行します。今回はbefore_sendの中は省略して次に行きたいと思います。

`adapter.send_file`で実際にファイルを取得し`offset`と`length`を指定していきます。
今回はCowboy2を利用しているのでCowboy2のAdapterが入っています。

### Plug.Adapters.Cowboy2.Conn
#### send_file

Cowboy2のAdapter内の`send_file`を読んでいきたいと思います（今回のメイン）

```elixir:plug/adapters/cowboy2/conn.ex
  def send_file(req, status, headers, path, offset, length) do
    %File.Stat{type: :regular, size: size} = File.stat!(path)

    length =
      cond do
        length == :all -> size
        is_integer(length) -> length
      end

    body = {:sendfile, offset, length, path}
    headers = to_headers_map(headers)
    req = :cowboy_req.reply(status, headers, body, req)
    {:ok, nil, req}
  end
```

まずは、`File.stat!(path)`でファイル情報を読み込みます。

```elixir
    length =
      cond do
        length == :all -> size
        is_integer(length) -> length
      end
```
の部分でファイルの長さを指定します。`length`が`:all`だった場合は上記でサイズを取得しているのでファイルサイズをそのまま渡します。
`headers = to_headers_map(headers)`でリストになってるheaderをマップにして返します。
`:cowboy_req.reply(status, headers, body, req)`で最終的なレスポンスの値を返します。
（ここから先はcowboyになるので別記事で書いていきたいと思います。）

これで一通りのsend_downloadの処理が終わり、残りはcowboyでのレスポンスのみとなります。

## 読んだ感想

基本的にはパラメータを引きずり回してcowboyに渡す、流れですね。
そしてほとんどPlugを読んでたという、、笑
Plug.ConnとAdapterのところがHTTPでのheaderやbodyとかを作っていってるみたいですね。


## 次回に向けて

cowboyへ最終的に値を渡して終わりになりましたので、その先の処理を読んでいきたいです。

あとは次のChange logである`[Controller] Add additional security headers to put_secure_browser_headers (x-content-type-options, x-download-options, and x-permitted-cross-domain-policies)`
を読んでみようと思います。

PlugのCowboy2 AdapterもAdapterの実装がどうなっているのか気になるので読んでいこうと思っています。