defmodule Nerves.Utils.HTTPClient do
use GenServer
@progress_steps 50
def start_link() do
{:ok, _} = Application.ensure_all_started(:nerves)
start_httpc()
GenServer.start_link(__MODULE__, [])
end
def stop(pid) do
GenServer.stop(pid)
end
def get(_, _, _ \\ [])
def get(_pid, %URI{host: nil, path: path}, _opts) do
path
|> Path.expand()
|> File.read()
end
def get(pid, %URI{} = uri, opts) do
url = URI.to_string(uri)
get(pid, url, opts)
end
def get(pid, url, opts), do: GenServer.call(pid, {:get, url, opts}, :infinity)
def init([]) do
{:ok,
%{
url: nil,
content_length: 0,
buffer: "",
buffer_size: 0,
filename: "",
caller: nil,
number_of_redirects: 0,
progress?: true,
get_opts: []
}}
end
def handle_call({:get, _url, _opts}, _from, %{number_of_redirects: n} = s) when n > 5 do
GenServer.reply(s.caller, {:error, :too_many_redirects})
{:noreply, %{s | url: nil, number_of_redirects: 0, caller: nil}}
end
def handle_call({:get, url, opts}, from, s) do
progress? = Keyword.get(opts, :progress?, true)
user_headers = Keyword.get(opts, :headers, []) |> Enum.map(&tuple_to_charlist/1)
headers = [
{'User-Agent', 'Nerves/#{Nerves.version()}'},
{'Content-Type', 'application/octet-stream'} | user_headers
]
http_opts =
[timeout: :infinity, autoredirect: false]
|> Keyword.merge(Nerves.Utils.Proxy.config(url))
|> Keyword.merge(Keyword.get(opts, :http_opts, []))
:httpc.request(
:get,
{String.to_charlist(url), headers},
http_opts,
[stream: :self, receiver: self(), sync: false],
:nerves
)
{:noreply, %{s | url: url, caller: from, get_opts: opts, progress?: progress?}}
end
def handle_info({:http, {_ref, {:error, {:failed_connect, _}} = err}}, s) do
GenServer.reply(s.caller, err)
end
def handle_info({:http, {_, :stream_start, headers}}, s) do
content_length =
case Enum.find(headers, fn {key, _} -> key == 'content-length' end) do
nil ->
0
{_, content_length} ->
{content_length, _} =
content_length
|> to_string()
|> Integer.parse()
content_length
end
filename =
case Enum.find(headers, fn {key, _} -> key == 'content-disposition' end) do
nil ->
Path.basename(s.url)
{_, filename} ->
filename
|> to_string
|> String.split(";")
|> List.last()
|> String.trim()
|> String.trim("filename=")
end
{:noreply, %{s | content_length: content_length, filename: filename}}
end
def handle_info({:http, {_, :stream, data}}, s) do
size = byte_size(data) + s.buffer_size
buffer = s.buffer <> data
if progress?(s) do
put_progress(size, s.content_length)
end
{:noreply, %{s | buffer_size: size, buffer: buffer}}
end
def handle_info({:http, {_, :stream_end, _headers}}, s) do
if progress?(s) do
IO.write(:stderr, "\n")
end
GenServer.reply(s.caller, {:ok, s.buffer})
{:noreply, %{s | filename: "", content_length: 0, buffer: "", buffer_size: 0, url: nil}}
end
def handle_info({:http, {_ref, {{_, status_code, reason}, headers, _body}}}, s)
when div(status_code, 100) == 3 do
case Enum.find(headers, fn {key, _} -> key == 'location' end) do
{'location', next_location} ->
next_get_opts = Keyword.drop(s.get_opts, [:headers])
handle_call({:get, List.to_string(next_location), next_get_opts}, s.caller, %{
s
| buffer: "",
buffer_size: 0,
number_of_redirects: s.number_of_redirects + 1
})
_ ->
GenServer.reply(s.caller, {:error, format_error(status_code, reason)})
end
end
def handle_info({:http, {_ref, {{_, status_code, reason}, _headers, _body}}}, s) do
GenServer.reply(s.caller, {:error, format_error(status_code, reason)})
{:noreply, s}
end
def put_progress(size, max) do
fraction = size / max
completed = trunc(fraction * @progress_steps)
percent = trunc(fraction * 100)
unfilled = @progress_steps - completed
IO.write(
:stderr,
"\r|#{String.duplicate("=", completed)}#{String.duplicate(" ", unfilled)}| #{percent}% (#{bytes_to_mb(size)} / #{bytes_to_mb(max)}) MB"
)
end
defp format_error(status_code, reason) do
"Status #{to_string(status_code)} #{to_string(reason)}"
end
defp start_httpc() do
:inets.start(:httpc, profile: :nerves)
opts = [
max_sessions: 8,
max_keep_alive_length: 4,
max_pipeline_length: 4,
keep_alive_timeout: 120_000,
pipeline_timeout: 60_000
]
:httpc.set_options(opts, :nerves)
end
defp bytes_to_mb(bytes) do
trunc(bytes / 1024 / 1024)
end
defp progress?(%{progress?: progress?}) do
System.get_env("NERVES_LOG_DISABLE_PROGRESS_BAR") == nil and progress?
end
defp tuple_to_charlist({k, v}) do
{to_charlist(k), to_charlist(v)}
end
end