lib/nerves/artifact/resolvers/github_api.ex

defmodule Nerves.Artifact.Resolvers.GithubAPI do
  @behaviour Nerves.Artifact.Resolver

  @base_url "https://api.github.com/"
  @required_opts [:username, :token, :tag]

  alias Nerves.Utils
  alias Nerves.Utils.HTTPClient

  def get({org_proj, opts}) do
    case validate_opts(opts) do
      {:ok, opts} ->
        fetch_artifact(org_proj, opts)

      {:error, missing} ->
        missing = Enum.join(missing, ", ")
        artifact_name = opts[:artifact_name]

        error = """
        Skipping: #{artifact_name}
        Required options missing: [#{missing}]
        """

        {:error, error}
    end
  end

  defp fetch_artifact(org_proj, opts) do
    artifact_name = opts[:artifact_name]
    {:ok, http_pid} = HTTPClient.start_link()
    token = get_token(opts, artifact_name)
    username = get_username(opts, artifact_name)
    tag = get_tag(opts, artifact_name)

    credentials = Base.encode64(username <> ":" <> token)
    auth_header = {"Authorization", "Basic " <> credentials}
    accept_header = {"Accept", "application/octet-stream"}
    base_url = opts[:base_url] || @base_url

    url = Path.join([base_url, "repos", org_proj, "releases", "tags", tag])

    Nerves.Utils.Shell.info("  Downloading artifacts from #{url}")

    url = URI.encode(url)

    result =
      with {:ok, data} <- HTTPClient.get(http_pid, url, headers: [auth_header], progress?: false),
           %{"assets" => assets} <- Utils.json_decode(data),
           {:ok, url} <- get_asset_url(assets, artifact_name),
           {:ok, data} <- HTTPClient.get(http_pid, url, headers: [auth_header, accept_header]) do
        {:ok, data}
      end

    Nerves.Utils.HTTPClient.stop(http_pid)
    result
  end

  def validate_opts(opts) do
    keys = Keyword.keys(opts)

    case Enum.split_with(@required_opts, &(&1 in keys)) do
      {_, [_ | _] = missing} -> {:error, missing}
      _ -> {:ok, opts}
    end
  end

  defp get_asset_url(assets, artifact_name) do
    ret =
      Enum.find(assets, fn %{"name" => name} ->
        String.equivalent?(artifact_name, name)
      end)

    case ret do
      nil -> {:error, "No artifact found"}
      %{"url" => url} -> {:ok, url}
    end
  end

  defp get_username(opts, artifact_name) do
    case Keyword.get(opts, :username) do
      nil ->
        Mix.raise("""
        GitHub username not set for artifact: #{artifact_name}.

        Ensure that you have your GitHub username set correctly
        in your environment. You might need to export an environmental
        variable.

        For example:

        export GITHUB_USER=<your_username>

        For correctly setting up your environment please see the documentation for the artifact you are
        trying to download.
        """)

      username ->
        username
    end
  end

  defp get_token(opts, artifact_name) do
    case Keyword.get(opts, :token) do
      nil ->
        Mix.raise("""
        GitHub token not set for artifact: #{artifact_name}

        Ensure that you have your GitHub token set correctly
        in your environment. You might need to export an environmental
        variable.

        For example:

        export GITHUB_TOKEN=<your_token>

        For correctly setting up your environment please see the documentation for the artifact you are
        trying to download.
        """)

      token ->
        token
    end
  end

  defp get_tag(opts, artifact_name) do
    case Keyword.get(opts, :tag) do
      nil ->
        Mix.raise("""
        GitHub release tag not set for artifact: #{artifact_name}.

        Ensure that you have set the tag field in the artifact sites
        configuration for this artifact.

        For example:
          {:github_api, "github_org/my_custom_system", username: System.get_env("GITHUB_USER"), token: System.get_env("GITHUB_TOKEN"), tag: @version}
        """)

      tag ->
        tag
    end
  end
end