DEV Community

Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on

1

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

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs