5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-14

こんにちは。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関数自体は小さくなっているのでまとめて読んでみましょう。

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

第二引数のパターンマッチで別れていますね。offsetlengthオプションがあるのは上の方のみです。
やってることは単純、filenameoffsetlengthオプションを取り出します。
その後にprepare_send_download関数とsend_file関数を読んで終了。単純ですね!

prepare_send_download

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

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 関数ですが以下のようになっています。

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

charsetnilの場合は、上のようが呼ばれてヘッダーにcontent-typeヘッダーが追加されるだけですね。もしオプションでcharsetをつけている場合は下の関数が呼ばれてcontent-typeヘッダーにcharsetがついて送信されます。

put_resp_header

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

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が呼ばれたときの引数を見ておきましょう。

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

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

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

ここでも2つ、パターンマッチで関数がわけられていますが、上の方はエラー処理なんで今回は無視。

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

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

Plug.Adapters.Cowboy2.Conn

send_file

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

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)でファイル情報を読み込みます。

    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の実装がどうなっているのか気になるので読んでいこうと思っています。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?