こんにちは。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
関数自体は小さくなっているのでまとめて読んでみましょう。
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
関数は以下のようになっています。
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
関数ですが以下のようになっています。
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
関数もここで読んでみましょう。
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
関数を読んでいきましょう
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
で実際にファイルを取得しoffset
とlength
を指定していきます。
今回はCowboy2を利用しているのでCowboy2のAdapterが入っています。
Plug.Adapters.Cowboy2.Conn
send_file
Cowboy2のAdapter内のsend_file
を読んでいきたいと思います(今回のメイン)
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の実装がどうなっているのか気になるので読んでいこうと思っています。