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