DEV Community

Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on

Elixirで進捗表示ダウンロード

Elixirで進捗状況を表示しながらダウンロードする方法について検討します。

Run in Livebook

やりたいこと

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"
Enter fullscreen mode Exit fullscreen mode

Reqをつかって進捗表示なしにダウンロード

まずは、 Req をつかって進捗表示なしにダウンロードしてみます。

 

# データとしてダウンロード
<<_::binary>> = Req.get!(source_url).body
Enter fullscreen mode Exit fullscreen mode

ローカルファイルとして保存したい場合は :output オプションで保存先を指定します。

destination_path = Path.join(System.tmp_dir!(), "elixir_logo.png")

# ダウンロードしてファイルに保存
Req.get!(source_url, output: destination_path)

# ちゃんと読み込めるか検証
File.read!(destination_path)
Enter fullscreen mode Exit fullscreen mode

進捗表示を追加するにはどうしたら良いのでしょうか。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クライアントだそうです。さらに FinchMint と 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
Enter fullscreen mode Exit fullscreen mode

以上のコードを 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
Enter fullscreen mode Exit fullscreen mode

Livebook でやるともっといい感じに進捗状況が更新されるはずです。

Run in Livebook

Bumblebee.Utils.HTTP.download/2

Bumblebee を使っているのであれば、Bumblebee.Utils.HTTP.download/2 で同じようなことができます。ドキュメントには載ってませんが利用可能です。

Bumblebee.Utils.HTTP.download(source_url, destination_path)
Enter fullscreen mode Exit fullscreen mode

Nerves Livebook

せっかくいい感じのコードが書けたので Nerves Livebook に寄贈いたしました。

https://github.com/livebook-dev/nerves_livebook/blob/9515bd61b4da6b30c6165b33f9a0ae56880ddc44/priv/samples/tflite.livemd

Elixirコミュニティ

本記事は以下のモクモク會での成果です。みなさんから刺激と元氣をいただき、ありがとうございました。

https://youtu.be/c0LP23SM7BU

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

https://note.com/awesomey/n/n4d8c355bc8f7

Top comments (0)