Elixirで進捗状況を表示しながらダウンロードする方法について検討します。
やりたいこと
Bumblebee を使っているときにファイルをダウンロードするとこういうダウンロード進捗表示がでます。これをやってみたいです。コードは自分で書きます。
Bumblebeeのコード
プライベートの Bumblebee.Utils.HTTP モジュールにダウンロード関連のコードがありました。
Erlang の httpc モジュールと ProgressBar パッケージを使って実装されています。
ちなみに httpc の使い方はElixir Forum にまとめられています。
https://elixirforum.com/t/httpc-cheatsheet/50337
同じように httpc モジュールを使って実装しても良いのですが、個人的に日頃よく利用する Req を使って1から実装してみようと思います。
まずは、 Req をつかって簡単なGETリクエストする方法から始めます。ここではElixir のロゴの画像データをダウンロードの対象とします。
source_url = "https://elixir-lang.org/images/logo/logo.png"
Reqをつかって進捗表示なしにダウンロード
まずは、 Req をつかって進捗表示なしにダウンロードしてみます。
# データとしてダウンロード
<<_::binary>> = Req.get!(source_url).body
ローカルファイルとして保存したい場合は :output
オプションで保存先を指定します。
destination_path = Path.join(System.tmp_dir!(), "elixir_logo.png")
# ダウンロードしてファイルに保存
Req.get!(source_url, output: destination_path)
# ちゃんと読み込めるか検証
File.read!(destination_path)
進捗表示を追加するにはどうしたら良いのでしょうか。Bumblebee のコードから ProgressBar パッケージを利用できることはすでにわかっています。それをどのように Req と連携させるかを調べます。
Req の構成要素(3 つ)
Req は 3 つの主要部分で構成されています。
- Req - 高階層のAPI
- Req.Request - 低階層のAPIとリクエスト構造体
- Req.Steps - ひとつひとつの処理
カスタマイズは比較的容易にできそうです。
Req.Steps.run_finch/1
Req.Steps.run_finch/1 に手を加えることにより、リクエストのロジックを変更できることがわかりました。ドキュメントにわかりにくい部分がありますが、サンプルコードを読んでみて高階層のAPIに :finch_request
オプションに関数を注入して Req.Steps.run_finch/1 ステップを入れ替えることができるようです。
Finch とは 初期設定の Req が依存するHTTPクライアントだそうです。さらに Finch は Mint と NimblePool を使って性能を意識して実装されているそうです。
余談ですが、Elixirの関数に「闘魂」を注入する方法については以下の@torifukukaiouさんの記事がおすすめです。
https://qiita.com/torifukukaiou/items/c414310cde9b7099df55
Reqをつかって進捗表示付きダウンロードしてみる
このような形になりました。ポイントをいくつかあげます。
-
Req.get/2 に
:finch_request
オプションとしてリクエストを処理するカスタムロジック(関数)を注入します。 - Finch.stream/5 でリクエストの多重化が可能です。ストリームという概念に疎いので 「WEB+DB PRESS Vol.123」 を読み返しました。「イーチ、ニィー、サン、ぁッ ダー!!!」
- ストリームからは3パターンのメッセージが返ってくるようです。
-
{:status, status}
- the status of the http response -
{:headers, headers}
- the headers of the http response -
{:data, data}
- a streaming section of the http body
-
- 進捗表示に必要な情報はふたつ。
- データ全体のバイト数
- 受信完了したバイト数
- 進捗状況は記憶しておく必要があるので、Req.Response の
:private
フィールドに格納し、データを受信するたびに更新します。
defmodule MNishiguchi.Utils.HTTP do
def download(source_url, req_options \\ []) do
case Req.get(source_url, [finch_request: &finch_request/4] ++ req_options) do
{:ok, response} -> {:ok, response.body}
{:error, exception} -> {:error, exception}
end
end
def download!(source_url, req_options \\ []) do
Req.get!(source_url, [finch_request: &finch_request/4] ++ req_options).body
end
defp finch_request(req_request, finch_request, finch_name, finch_options) do
acc = Req.Response.new()
case Finch.stream(finch_request, finch_name, acc, &handle_message/2, finch_options) do
{:ok, response} -> {req_request, response}
{:error, exception} -> {req_request, exception}
end
end
defp handle_message({:status, status}, response), do: %{response | status: status}
defp handle_message({:headers, headers}, response) do
total_size =
Enum.find_value(headers, fn
{"content-length", v} -> String.to_integer(v)
{_k, _v} -> nil
end)
response
|> Map.put(:headers, headers)
|> Map.put(:private, %{total_size: total_size, downloaded_size: 0})
end
defp handle_message({:data, data}, response) do
new_downloaded_size = response.private.downloaded_size + byte_size(data)
ProgressBar.render(new_downloaded_size, response.private.total_size, suffix: :bytes)
response
|> Map.update!(:body, &(&1 <> data))
|> Map.update!(:private, &%{&1 | downloaded_size: new_downloaded_size})
end
end
以上のコードを IEx でランしてみます。
iex(5)> MNishiguchi.Utils.HTTP.download!(source_url)
|=== | 4% (1.36/34.95 KB)
|======= | 8% (2.73/34.95 KB)
|========== | 12% (4.10/34.95 KB)
|============= | 16% (5.47/34.95 KB)
|================ | 20% (6.84/34.95 KB)
|=================== | 23% (8.20/34.95 KB)
|====================== | 27% (9.57/34.95 KB)
|========================= | 31% (10.94/34.95 KB)
|============================ | 35% (12.31/34.95 KB)
|=================================== | 43% (15.04/34.95 KB)
|====================================== | 47% (16.41/34.95 KB)
|========================================= | 51% (17.78/34.95 KB)
|============================================= | 55% (19.15/34.95 KB)
|================================================ | 59% (20.52/34.95 KB)
|=================================================== | 63% (21.88/34.95 KB)
|====================================================== | 67% (23.25/34.95 KB)
|========================================================= | 70% (24.62/34.95 KB)
|============================================================ | 74% (25.99/34.95 KB)
|=============================================================== | 78% (27.36/34.95 KB)
|================================================================== | 82% (28.72/34.95 KB)
|====================================================================== | 86% (30.09/34.95 KB)
|========================================================================= | 90% (31.46/34.95 KB)
|============================================================================ | 94% (32.83/34.95 KB)
|=======================================================================================| 100% (34.95 KB)
:ok
Livebook でやるともっといい感じに進捗状況が更新されるはずです。
Bumblebee.Utils.HTTP.download/2
Bumblebee を使っているのであれば、Bumblebee.Utils.HTTP.download/2
で同じようなことができます。ドキュメントには載ってませんが利用可能です。
Bumblebee.Utils.HTTP.download(source_url, destination_path)
Nerves Livebook
せっかくいい感じのコードが書けたので Nerves Livebook に寄贈いたしました。
Elixirコミュニティ
本記事は以下のモクモク會での成果です。みなさんから刺激と元氣をいただき、ありがとうございました。
https://okazakirin-beam.connpass.com/
https://autoracex.connpass.com
もしご興味のある方はお氣輕にご參加ください。
https://qiita.com/piacerex/items/09876caa1e17169ec5e1
https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian
https://qiita.com/torifukukaiou/items/57a40119c9eefd056cae
https://qiita.com/piacerex/items/e0b6e46b1325bb931122
https://qiita.com/torifukukaiou/items/1edb3e961acf002478fd
https://qiita.com/piacerex/items/e5590fa287d3c89eeebf
https://qiita.com/torifukukaiou/items/4481f7884a20ab4b1bea
Top comments (0)