Current section
Files
Jump to
Current section
Files
lib/utils/archive/archive.ex
defmodule Gitly.Utils.Archive do
alias Gitly.Utils.Archive.Type, as: ArchiveType
alias Gitly.Utils.Archive.Extractor, as: Extractor
alias Gitly.Utils.Git.Provider, as: GitProvider
alias Gitly.Utils.FS, as: FS
@moduledoc """
A module to handle archive operations.
This module provides functionality for building archive URLs, creating archive paths,
downloading archives, and extracting them.
"""
@doc """
Builds an archive URL based on the given input.
## Parameters
* `input` - A map containing archive information (must include `:host`).
* `format` - The desired archive format (default: `:tar_gz`).
## Returns
* A string containing the built URL, or `{:error, reason}` if the operation fails.
## Examples
iex> input = %{host: "github.com", owner: "elixir-lang", repo: "elixir", ref: "main"}
iex> Gitly.Utils.Archive.build_archive_url(input)
"https://github.com/elixir-lang/elixir/archive/main.tar.gz"
"""
@spec build_archive_url(map(), ArchiveType.ext()) :: {:error, String.t()} | String.t()
def build_archive_url(%{host: host} = input, format \\ :tar_gz) do
with type <- GitProvider.from_string(host),
ext <- ArchiveType.from_type(format),
url <- GitProvider.build_url(type, input, ext) do
url
end
end
@doc """
Creates an archive path based on the given input.
## Parameters
* `input` - A map containing archive information (must include `:host`, `:owner`, `:repo`, and `:ref`).
* `opts` - A keyword list of options:
* `:root` - The root path (default: result of `FS.root_path()`).
* `:format` - The archive format (default: `:tar_gz`).
## Returns
* A string containing the created archive path.
## Examples
iex> input = %{host: "github.com", owner: "elixir-lang", repo: "elixir", ref: "main"}
iex> Gitly.Utils.Archive.create_archive_path(input, root: "/tmp")
"/tmp/github/elixir-lang/elixir/main.tar.gz"
"""
@spec create_archive_path(map(), Gitly.opts()) :: Path.t()
def create_archive_path(%{host: host, owner: owner, repo: repo, ref: ref}, opts \\ []) do
with root <- Keyword.get(opts, :root, FS.root_path()),
format <- Keyword.get(opts, :format, :tar_gz),
provider = host |> String.split(".") |> List.first(),
fmt = ArchiveType.from_type(format),
archive_path = [root, provider, owner, repo, ref <> fmt] |> Path.join() |> Path.expand() do
archive_path
end
end
@doc """
Downloads an archive from the given URL and saves it to the given path.
## Parameters
* `url` - The URL of the archive to download.
* `path` - The local path where the archive should be saved.
* `opts` - A keyword list of options:
* `:retry` - The retry strategy (default: `:transient`).
## Returns
* `{:ok, path}` if the download was successful.
* `{:error, reason}` if the download failed.
## Examples
iex> Gitly.Utils.Archive.download("https://example.com/archive.zip", "/tmp/archive.zip")
{:ok, "/tmp/archive.zip"}
"""
@spec download(String.t(), Path.t(), Keyword.t()) :: {:ok, Path.t()} | {:error, String.t()}
def download(url, path, opts \\ []) do
retry = Keyword.get(opts, :retry, :transient)
try do
with true <- Gitly.Utils.Net.is_online?(),
:ok <- FS.ensure_dir_exists(path),
opts = [
into: stream_to_file!(path),
redirect_log_level: false,
compressed: true,
decode_body: false,
retry: retry
],
%Req.Response{status: 200} <- Req.get!(url, opts) do
{:ok, path}
else
%Req.Response{status: code} ->
File.rm(path)
{:error, "Failed to download archive: #{code}"}
{:error, reason} ->
{:error, reason}
false ->
{:error, "Failed to download archive: offline"}
end
rescue
e in RuntimeError -> {:error, e.message}
e -> {:error, "Failed to download archive: #{inspect(e)}"}
end
end
@doc """
Extracts an archive from the given file path to the given destination.
## Parameters
* `file_path` - The path to the archive file.
* `dest` - The destination directory where the archive should be extracted.
* `opts` - A keyword list of options:
* `:force` - Whether to force extraction even if the destination exists (default: `false`).
* `:overwrite` - Whether to overwrite existing files (default: `false`).
## Returns
* `{:ok, dest}` if the extraction was successful.
* `{:error, reason}` if the extraction failed.
## Examples
iex> Gitly.Utils.Archive.extract("/tmp/archive.zip", "/tmp/extracted")
{:ok, "/tmp/extracted"}
"""
@spec extract(Path.t(), Path.t(), Gitly.opts()) :: {:ok, Path.t()} | {:error, String.t()}
def extract(file_path, dest, opts \\ []) do
force = Keyword.get(opts, :force, false)
overwrite = Keyword.get(opts, :overwrite, false)
with {:ok, temp_dir} <- Briefly.create(directory: true),
{:ok, _} <- Extractor.extract(file_path, temp_dir),
{:ok, _} <-
FS.maybe_rm_rf(
dest,
FS.rm?(dest) and (force || overwrite)
),
{:ok, _} <-
FS.maybe_move(
temp_dir,
dest,
not File.exists?(dest)
) do
{:ok, dest}
else
{:error, reason} -> {:error, reason}
_ -> {:error, "Failed to extract archive"}
end
after
Briefly.cleanup()
end
# Private functions
@doc false
defp stream_to_file!(file_path) do
if File.dir?(file_path) do
raise "Cannot open directory as file: #{file_path}"
end
case File.open(file_path, [:write, :binary]) do
{:ok, file} ->
fn
{:data, data}, acc ->
IO.binwrite(file, data)
{:cont, acc}
:done, acc ->
# coveralls-ignore-next-line
File.close(file)
{:cont, acc}
_, _ ->
# coveralls-ignore-next-line
File.close(file)
{:halt, :error}
end
{:error, reason} ->
raise "Failed to open file #{file_path}: #{inspect(reason)}"
end
end
end