defmodule Litestream.Downloader do
@moduledoc """
This module is used to download the built Litestream binaries.
"""
require Logger
@latest_litestream_version "0.3.9"
@supported_litestream_versions ["0.3.9", "0.3.8", "0.3.7"]
@valid_litestream_versions %{
# All the SHA hashes for version 0.3.9
{"0.3.9", :darwin, :amd64} => "74599a34dc440c19544f533be2ef14cd4378ec1969b9b4fcfd24158946541869",
{"0.3.9", :linux, :amd64} => "806e1cca4a2a105a36f219a4c212a220569d50a8f13f45f38ebe49e6699ab99f",
{"0.3.9", :linux, :arm64} => "61acea9d960633f6df514972688c47fa26979fbdb5b4e81ebc42f4904394c5c5",
# All the SHA hashes for version 0.3.8
{"0.3.8", :darwin, :amd64} => "d359a4edd1cb98f59a1a7c787bbd0ed30c6cc3126b02deb05a0ca501ff94a46a",
{"0.3.8", :linux, :amd64} => "530723d95a51ee180e29b8eba9fee8ddafc80a01cab7965290fb6d6fc31381b3",
{"0.3.8", :linux, :arm64} => "1d6fb542c65b7b8bf91c8859d99f2f48b0b3251cc201341281f8f2c686dd81e2",
# All the SHA hashes for version 0.3.7
{"0.3.7", :darwin, :amd64} => "fdfd811df081949fdac2f09af8ad624c37c02b98c0e777f725f69e67be270745",
{"0.3.7", :linux, :amd64} => "e9daf0b73d7b5d75eac22bb9f0a93945e3efce0f1ff5f3a6b57f4341da4609cf",
{"0.3.7", :linux, :arm64} => "1c0c1c6a2346fb67d69e594b6342e1d13f078d2b02a2c8bae4b84ea188b12579"
}
defguardp is_valid_version(version) when version in @supported_litestream_versions
@doc """
Get the latest version of Litestream (that is supported by the library).
"""
@spec latest_version :: String.t()
def latest_version do
@latest_litestream_version
end
@doc """
This function will download the desired Litestream version and store it in the
provided directory..
"""
@spec download_litestream(
version :: String.t(),
download_directory :: String.t(),
bin_directory :: String.t()
) :: {:ok, String.t()} | {:error, String.t()}
def download_litestream(litestream_version, download_directory, bin_directory)
when is_valid_version(litestream_version) do
litestream_version = get_download_version(litestream_version)
download_url = build_download_url(litestream_version)
archive_file_name =
download_url
|> URI.parse()
|> Map.get(:path)
|> Path.basename()
binary_file_name =
if String.ends_with?(archive_file_name, ".zip") do
archive_file_name
|> String.trim_trailing(".zip")
else
archive_file_name
|> String.trim_trailing(".tar.gz")
end
# Set constants for where files will be located
archive_download_path = "#{download_directory}/#{archive_file_name}"
binary_path = "#{bin_directory}/#{binary_file_name}"
# Download the litestream, verify it, and unarchive it
with :ok <- do_download_litestream(download_url, archive_download_path),
:ok <- verify_archive_download(archive_download_path, litestream_version) do
unarchive_litestream(archive_download_path, binary_path)
end
end
defp do_download_litestream(download_url, archive_file_path) do
if File.exists?(archive_file_path) do
Logger.info("Litestream archive already present")
:ok
else
Logger.info("Fetching Litestream archive")
# Ensure that the necessary applications have been started
{:ok, _} = Application.ensure_all_started(:inets)
{:ok, _} = Application.ensure_all_started(:ssl)
if proxy = System.get_env("HTTP_PROXY") || System.get_env("http_proxy") do
Logger.debug("Using HTTP_PROXY: #{proxy}")
%{host: host, port: port} = URI.parse(proxy)
:httpc.set_options([{:proxy, {{String.to_charlist(host), port}, []}}])
end
if proxy = System.get_env("HTTPS_PROXY") || System.get_env("https_proxy") do
Logger.debug("Using HTTPS_PROXY: #{proxy}")
%{host: host, port: port} = URI.parse(proxy)
:httpc.set_options([{:https_proxy, {{String.to_charlist(host), port}, []}}])
end
# https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets
cacertfile = CAStore.file_path() |> String.to_charlist()
http_options = [
ssl: [
verify: :verify_peer,
cacertfile: cacertfile,
depth: 2,
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
]
]
options = [body_format: :binary]
case :httpc.request(:get, {download_url, []}, http_options, options) do
{:ok, {{_, 200, _}, _headers, body}} ->
File.write!(archive_file_path, body)
error ->
raise "Could not fetch Litestream from #{download_url}: #{inspect(error)}"
end
:ok
end
end
defp get_download_version(litestream_version) do
arch_str = :erlang.system_info(:system_architecture)
[arch | _] = arch_str |> List.to_string() |> String.split("-")
case {:os.type(), arch, :erlang.system_info(:wordsize) * 8} do
# TODO: When Litestream supports ARM macOS builds
# {{:unix, :darwin}, arch, 64} when arch in ~w(arm aarch64) -> {litestream_version, :darwin, :arm64}
{{:unix, :darwin}, arch, 64} when arch in ~w(arm aarch64) -> {litestream_version, :darwin, :amd64}
{{:unix, :darwin}, "x86_64", 64} -> {litestream_version, :darwin, :amd64}
{{:unix, :linux}, "aarch64", 64} -> {litestream_version, :linux, :arm64}
{{:unix, _osname}, arch, 64} when arch in ~w(x86_64 amd64) -> {litestream_version, :linux, :amd64}
unsupported_arch -> raise "Unsupported architecture: #{inspect(unsupported_arch)}"
end
end
defp unarchive_litestream(archive_download_path, binary_path) do
if String.ends_with?(archive_download_path, ".zip") do
# Extract the zip file
zip_contents = File.read!(archive_download_path)
{:ok, [{_file_name, unzipped_contents}]} = :zip.extract(zip_contents, [:memory])
# Set exec permissions to Litestream
File.write(binary_path, unzipped_contents)
File.chmod!(binary_path, 0o755)
{:ok, binary_path}
else
# Extract the tarball
tarball_contents = File.read!(archive_download_path)
{:ok, [{_file_name, untarred_contents}]} = :erl_tar.extract({:binary, tarball_contents}, [:memory, :compressed])
# Set exec permissions to Litestream
File.write(binary_path, untarred_contents)
File.chmod!(binary_path, 0o755)
{:ok, binary_path}
end
end
defp verify_archive_download(archive_download_path, litestream_version) do
# Get the known SHA256 value
known_sha = Map.fetch!(@valid_litestream_versions, litestream_version)
# Read the archive file and compute the SHA256 value
archive_contents = File.read!(archive_download_path)
computed_sha =
:sha256
|> :crypto.hash(archive_contents)
|> Base.encode16()
|> String.downcase()
if known_sha == computed_sha do
:ok
else
{:error, "Invalid SHA256 value computed for #{archive_download_path}"}
end
end
defp build_download_url({version, :darwin, arch}) do
"https://github.com/benbjohnson/litestream/releases/download/v#{version}/litestream-v#{version}-darwin-#{arch}.zip"
end
defp build_download_url({version, :linux, arch}) do
"https://github.com/benbjohnson/litestream/releases/download/v#{version}/litestream-v#{version}-linux-#{arch}.tar.gz"
end
end