lib/helper/library_maker.ex

defmodule MishkaInstaller.Helper.LibraryMaker do
  @moduledoc """
  You can find some utility functions in this module which help you to download dependencies from Hex and GitHub and prepare them as library.

  ### Testing resource
  - MishkaInstaller.Helper.Sender.package("hex", %{"app" => "req"})
  - https://hex.pm/api/packages/req/releases/0.1.0
  - https://api.github.com/repos/mishka-group/mishka_installer/tarball/0.0.3
  - https://stackoverflow.com/questions/30267943/elixir-download-a-file-image-from-a-url
  """
  alias MishkaInstaller.Helper.Sender
  @request_name DownloaderClientApi

  @spec run(:github | :hex, String.t(), String.t()) ::
          list | {:error, atom(), atom} | {:ok, :run, binary}
  def run(type, app, version) do
    with {:ok, :download, _, app_info, pkg} <- download(type, app, version),
         file_path <- "#{extensions_path()}/#{app_info["app"]}-#{app_info["version"]}",
         {:ok, :extract} <- extract(:tar, "#{app_info["app"]}-#{app_info["version"]}"),
         {:ok, files} <-
           File.ls("#{extensions_path()}/#{app_info["app"]}-#{app_info["version"]}"),
         extracted_file_type <- Enum.member?(files, "contents.tar.gz"),
         _final_lib_path <-
           extracted_to_normal_project(app_info, file_path, files, extracted_file_type) do
      {:ok, :package, pkg}
    end
  end

  @doc """
  A function which download a package from Hex/Github and save it in the `deployment/extensions` folder.
  """
  @spec download(:github | :hex, String.t(), String.t()) ::
          list | {:error, atom(), atom} | {:ok, :download, :github | :hex, map(), map | list()}
  def download(:hex, app, version) do
    with {:ok, :package, %{"releases" => releases} = pkg} <-
           MishkaInstaller.Helper.Sender.package("hex", %{"app" => app}),
         {:ok, :select_hex_release, release_info} <- select_hex_release(releases, version),
         {:ok, :get_hex_releas_data, app_info} <- get_hex_releas_data(release_info),
         {:ok, :download_tar_from_hex, downloaded_app_body} <-
           download_tar_from_hex(app, app_info["version"]),
         {:ok, :file_write} <- file_write(app, app_info["version"], downloaded_app_body),
         {:ok, :checksum?} <- checksum?(app, app_info) do
      {:ok, :download, :hex, %{"app" => "#{app}", "version" => app_info["version"]}, pkg}
    else
      {:error, _section, result} -> {:error, :package, result}
    end
  rescue
    _e in Protocol.UndefinedError ->
      {:error, :package, :invalid_version}

    _ ->
      {:error, :package, :unhandled}
  end

  def download(:github, url, version) do
    with {:ok, :select_github_release, pkg, tar_url} <-
           select_github_release(url, version),
         {:ok, :download_tar_from_github, downloaded_app_body} <-
           download_tar_from_github(tar_url),
         {:ok, :file_write} <- file_write("#{pkg[:app]}", pkg[:version], downloaded_app_body) do
      {:ok, :download, :github, %{"app" => "#{pkg[:app]}", "version" => pkg[:version]}, pkg}
    else
      {:error, _section, result} -> {:error, :package, result}
    end
  rescue
    _e in Protocol.UndefinedError ->
      {:error, :package, :invalid_version}

    _ ->
      {:error, :package, :unhandled}
  end

  # erl_tar:extract("rel/project-1.0.tar.gz", [compressed]);
  @spec extract(:tar, String.t()) :: {:error, :extract} | {:ok, :extract}
  def extract(:tar, archived_file) do
    extract_output =
      :erl_tar.extract(
        ~c'#{extensions_path()}/#{archived_file}.tar.gz',
        [:compressed, {:cwd, ~c'#{extensions_path()}/#{archived_file}'}]
      )

    case extract_output do
      :ok -> {:ok, :extract}
      {:error, term} -> {:error, :extract, term}
    end
  end

  @spec change_uploaded_file(String.t(), String.t()) :: [binary]
  def change_uploaded_file(file_path, app_name) do
    new_lib_path =
      Path.join(MishkaInstaller.get_config(:project_path), [
        "deployment/",
        "extensions/#{app_name}"
      ])

    String.replace(file_path, Path.extname(file_path), "")
    |> File.rename(new_lib_path)

    File.rm_rf!(file_path)
  end

  defp get_hex_releas_data(app_info) do
    Finch.build(:get, app_info["url"])
    |> Finch.request(@request_name)
    |> case do
      {:ok, %Finch.Response{status: 200, body: body}} ->
        {:ok, :get_hex_releas_data, Jason.decode!(body)}

      _ ->
        {:error, :get_hex_releas_data, :not_found}
    end
  end

  defp select_hex_release(releases, "latest") when length(releases) > 0,
    do: {:ok, :select_hex_release, List.first(releases)}

  defp select_hex_release(releases, version) when length(releases) > 0 do
    releases
    |> Enum.find(&(&1["version"] == version))
    |> case do
      nil -> {:error, :select_hex_release, :not_found}
      data -> {:ok, :select_hex_release, data}
    end
  end

  defp select_hex_release(_releases, _version), do: {:error, :select_hex_release, :not_found}

  defp download_tar_from_hex(app, version) do
    Finch.build(:get, "https://repo.hex.pm/tarballs/#{app}-#{version}.tar")
    |> Finch.request(@request_name)
    |> case do
      {:ok, %Finch.Response{status: 200, body: body}} -> {:ok, :download_tar_from_hex, body}
      _ -> {:error, :download_tar_from_hex, :not_found}
    end
  end

  defp extensions_path() do
    Path.join(MishkaInstaller.get_config(:project_path), ["deployment/", "extensions"])
  end

  defp file_write(app, version, downloaded_app_body) do
    case File.write("#{extensions_path()}/#{app}-#{version}.tar.gz", downloaded_app_body) do
      {:error, posix} -> {:error, :file_write, posix}
      _ -> {:ok, :file_write}
    end
  end

  defp checksum?(app, app_info) do
    (MishkaInstaller.checksum("#{extensions_path()}/#{app}-#{app_info["version"]}.tar.gz") ==
       app_info["checksum"])
    |> case do
      false -> {:error, :checksum?, :unequal}
      true -> {:ok, :checksum?}
    end
  end

  # - https://elixirforum.com/t/how-to-download-a-file-with-finch-which-is-redirected/50368
  defp select_github_release(url, "latest") do
    case Sender.package("github_latest_release", url) do
      {:ok, :package, %{"tag_name" => version}} -> select_github_release(url, version)
      _ -> {:error, :select_github_release, :not_found}
    end
  end

  defp select_github_release(url, version) do
    case Sender.package("github", %{"url" => url, "tag" => version}) do
      {:error, :package, :not_found} ->
        {:error, :select_github_release, :not_found}

      data ->
        {:ok, :select_github_release, data,
         "#{String.replace(MishkaInstaller.trim_url(url), "https://github.com/", "https://codeload.github.com/")}/legacy.tar.gz/refs/tags/#{version}"}
    end
  end

  defp download_tar_from_github(url) do
    Finch.build(:get, url)
    |> Finch.request(@request_name)
    |> case do
      {:ok, %Finch.Response{status: 200, body: body}} -> {:ok, :download_tar_from_github, body}
      _ -> {:error, :download_tar_from_github, :not_found}
    end
  end

  defp extracted_to_normal_project(app_info, lib_path, files, true) do
    File.rm_rf!(Path.join(extensions_path(), ["#{app_info["app"]}"]))
    Enum.map(files, &if(&1 != "contents.tar.gz", do: File.rm_rf!("#{lib_path}/#{&1}")))
    :erl_tar.extract(~c'#{lib_path}/contents.tar.gz', [:compressed, {:cwd, ~c'#{lib_path}'}])

    ["#{lib_path}/contents.tar.gz", "#{lib_path}.tar.gz"]
    |> Enum.map(&File.rm_rf!(&1))

    File.rename(lib_path, Path.join(extensions_path(), ["#{app_info["app"]}"]))
    Path.join(extensions_path(), ["#{app_info["app"]}"])
  end

  defp extracted_to_normal_project(app_info, lib_path, files, false) do
    File.rm_rf!(Path.join(extensions_path(), ["#{app_info["app"]}"]))
    Enum.map(files, &if(!File.dir?("#{lib_path}/#{&1}"), do: File.rm_rf!("#{lib_path}/#{&1}")))

    nested_dir =
      File.ls!("#{extensions_path()}/#{app_info["app"]}-#{app_info["version"]}") |> List.first()

    File.cp_r!(
      Path.join(lib_path, ["#{nested_dir}"]),
      Path.join(extensions_path(), ["#{app_info["app"]}"])
    )

    [lib_path, "#{lib_path}.tar.gz"]
    |> Enum.map(&File.rm_rf!(&1))

    Path.join(extensions_path(), ["#{app_info["app"]}-#{app_info["version"]}."])
  end
end