There are tons of methods for organising binaries into a particular project in different programming languages. But, I am writing this method particularly to illustrate the pure usage of elixir and joy of learning it! This tutorial is for intermediate users with enough experience in using elixir. I hope you enjoy it!
How can you leverage it?
It could be anyone of the following:
- a
mix
task - a
genserver
- a
function
- a
escript
etc.,
In my particular use case, I wanted a mix task, so I will be illustrating the use case with Mix Tasks. Feel free to try and explore different options.
Mix Task:
We all would have started using mix tasks, the moment we installed elixir in our system. The basic mix task that helped me my elixir journey is mix help
. The first thing that I wanted to learn is how to create a simple mix task. I am not going to ellaborate on how to write a mix task, but rather focus on the main functionality.
Requirements:
- Erlang ~> 20
- Elixir ~> 1.6
- A simple elixir project (create it using
mix new
) - Create a basic mix task (Reference)
Getting Started
# Your example mix task should be like the one below
defmodule Mix.Tasks.Hello do
@shortdoc ~S("Example mix task for showing hello world")
use Mix.Task
def run(_) do
#################
Code hidden......
#################
end
end
Add {:httpoison, "~> 1.3"}
to your new sample project’s deps
in mix.exs
. You can get to know the latest version of HTTpoison using mix hex.info httpoison
.
Writing a Basic Download Service using HTTPoison
First of all you need to know how much would it take for the download to complete. As we are not creating something competing to wget, curl or even IDM / XDM, I will keep it very simple, I am going to download something small with a timeout of 5 minutes, so that my task will wait for 5 minutes for the download to complete.
- A dummy download link
- Timeout: 5 minutes (5 * 60 * 1000)
With the above things ready, we are nearly ready to get into coding. Make sure to either keep the download url in config or use the OptionsParser
in Elixir.
def download(url, path) do
{:ok, file} = File.open("path/filename", [:write, :exclusive])
...to continue
end
One main thing to remember is that, since it is a mix task, you are supposed to start HTTpoison before starting the download. If you are writing a mix application, just starting HTTPoison in the mix.exs
is enough.
def download(url, path) do
HTTPoison.start() #Mandatory if you are using it in a mix task
{:ok, file} = File.open("path/filename", [:write, :exclusive])
end
Using HTTPoison get
HTTPoison.get(url, ["Accept": "application/octet-stream"], [follow_redirect: true, stream_to: self(), recv_timeout: 5 * 60 * 1000])
When you just need to hit an endpoint, the syntax is as simple as HTTPoison.get(url, [])
but since, we want to download, I am using the necessary headers and follow_redirect
is optional because, some sites redirect you to cloud storage services, where you will get the file and you can also control, you redirection limit. The stream_to
is the interesting part, which is the thing that let’s us download without blocking the IO. So, the above command gives a tuple in the form {:ok, %HTTPoison.AsyncResponse{id: ref}}
. The tuple contains a reference to the downloading process.
Now the code will look like the following:
def download(url, path) do
HTTPoison.start()
{:ok, file} = File.open("path/filename", [:write, :exclusive])
with {:ok, %HTTPoison.AsyncResponse{id: ref}} <- HTTPoison.get(url, ["Accept": "application/octet-stream"],
[follow_redirect: true, stream_to: self(), recv_timeout: 5 * 60 * 1000]) do
write_data(ref, file, path)
else
error ->
File.close(file)
File.rm_rf!("path/filename")
end
end
Further steps
The HTTPoison provides certain structs to understand the response from the server it hits and provides easier interface to interpret each type of struct and consume them.
alias HTTPoison.{AsyncHeaders, AsyncRedirect, AsyncResponse, AsyncStatus, AsyncChunk, AysncEnd}
The above aliases and our function in the previous snippet write_data(ref, file, path)
are closely related.
defp write_data(ref, file, path) do
receive do
%AsyncStatus{code: 200} ->
write(ref, file, path)
%AsyncStatus{code: error_code} ->
File.close(file)
%AsyncRedirect{headers: headers, to: to, id: ^ref} ->
download(to, path)
%AsyncChunk{chunk: chunk, id: ^ref} ->
IO.binwrite(file, chunk)
write(ref, file, path)
%AsyncEnd{id: ^ref} ->
File.close(file)
end
end
The write_data
is just a receive do
loop with pattern matching.
- first pattern checks for connection success code
200
and then it again sends to the same function - the next, it checks for codes other than success and closes the file if it is executed
- the redirect pattern, checks for any redirects and get the redirect url and starts the download with the new URL
- the chunk is the real hero of the day, which accumulates our downloaded data in chunks and writes to the specified file
- the end indicates the completion of streaming from the process.
Finally our download task is ready
defmodule Mix.Tasks.Hello do
@shortdoc ~S("Example mix task for showing hello world")
use Mix.Task
alias HTTPoison.{AsyncHeaders, AsyncRedirect, AsyncResponse, AsyncStatus, AsyncChunk, AysncEnd}
def run(_) do
download("#{URL}", "#{SAVE_TO_PATH}")
end
def download(url, path) do
HTTPoison.start()
{:ok, file} = File.open("path/filename", [:write, :exclusive])
with {:ok, %HTTPoison.AsyncResponse{id: ref}} <- HTTPoison.get(url, ["Accept": "application/octet-stream"],
[follow_redirect: true, stream_to: self(), recv_timeout: 5 * 60 * 1000]) do
write_data(ref, file, path)
else
error ->
File.close(file)
File.rm_rf!("path/filename")
end
end
defp write_data(ref, file, path) do
receive do
%AsyncStatus{code: 200} ->
write(ref, file, path)
%AsyncStatus{code: error_code} ->
File.close(file)
%AsyncRedirect{headers: headers, to: to, id: ^ref} ->
download(to, path)
%AsyncChunk{chunk: chunk, id: ^ref} ->
IO.binwrite(file, chunk)
write(ref, file, path)
%AsyncEnd{id: ^ref} ->
File.close(file)
end
end
end
This is our basic downloader and is ready for use. Please, keep in mind the above code is not production ready, and don’t put system critical uses using the above sample code, but it is usable after some mission critical tweaks. All the best!