Skip to content

Commit

Permalink
Return exceptions for all filesystem errors
Browse files Browse the repository at this point in the history
The following changes are aimed at simplifying the Briefly API:

`Briefly.create()` and `Briefly.create(opts)` now return either `{:ok, path}` or
`{:error, Exception.t()}`. The following exceptions may be returned:

  - `%Briefly.NoRootDirectoryError{}` - Returned if Briefly is unable to create a root temporary
    directory.

  - `%Briefly.WriteError{}` - Returned when a temporary entry cannot be created. The exception
    contains the POSIX error code the caused the failure.

The `:too_many_attempts` error is not directly actionable so it is no longer surfaced. Since Briefly
still performs several write attempts when faced with `:eexist` and `:eaccess` error codes, too many
attempts can be inferred when either of those codes are returned.

The error messages raised by `Briefly.create!()` have been moved to the Exception modules and aside
from no longer printing the number of attempts they provide the same information.

For example, if you have this:

```elixir
case Briefly.create() do
  {:ok, path} -> path
  {:no_space, _} -> raise "no space"
  {:too_many_attempts, _} -> raise "too many attempts"
  {:no_tmp, _} -> raise "no tmp dirs"
end
```

...then change it to this:

```elixir
case Briefly.create() do
  {:ok, path} -> path
  {:error, %Briefly.WriteError{code: :enospc}} -> raise "no space"
  {:error, %Briefly.WriteError{code: c}} when c in [:eexist, :eacces] -> raise "too many attempts"
  {:error, %Briefly.NoRootDirectoryError{}} -> raise "no tmp dirs"
  {:error, exception} -> raise exception
end
```
  • Loading branch information
mcrumm committed Nov 7, 2023
1 parent 1f61817 commit 6b98fe9
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 37 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ If you were previously using `:monitor_pid` like this:
:ok = Briefly.give_away(path, pid)
```

### Exceptions

The following exceptions may now be returned from `Briefly.create/1`:

- `%Briefly.NoRootDirectoryError{}` - returned when a root temporary directory could not be accessed.

- `%Briefly.WriteError{}` - returned when an entry cannot be created.

## v0.4.1 (2023-01-11)

- Fix an issue with custom tmp dirs without a trailing slash ([#24](https://github.com/CargoSense/briefly/pull/24)) @srgpqt
Expand Down
18 changes: 4 additions & 14 deletions lib/briefly.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ defmodule Briefly do
@doc """
Requests a temporary file to be created with the given options.
"""
@spec create(create_opts) ::
{:ok, binary}
| {:no_space, binary}
| {:too_many_attempts, binary, pos_integer}
| {:no_tmp, [binary]}
@spec create(create_opts) :: {:ok, binary} | {:error, Exception.t()}
def create(opts \\ []) do
opts
|> Enum.into(%{})
Expand All @@ -37,17 +33,11 @@ defmodule Briefly do
Requests a temporary file to be created with the given options
and raises on failure.
"""
@spec create!(create_opts) :: binary | no_return
@spec create!(create_opts) :: binary
def create!(opts \\ []) do
case create(opts) do
{:ok, path} ->
path

{:too_many_attempts, tmp, attempts} ->
raise "tried #{attempts} times to create a temporary file at #{tmp} but failed. What gives?"

{:no_tmp, _tmps} ->
raise "could not create a tmp directory to store temporary files. Set the :briefly :directory application setting to a directory with write permission"
{:ok, path} -> path
{:error, exception} when is_exception(exception) -> raise exception
end
end

Expand Down
38 changes: 15 additions & 23 deletions lib/briefly/entry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,8 @@ defmodule Briefly.Entry do
end

def create(%{} = options) do
case ensure_tmp() do
{:ok, tmp} ->
open(options, tmp, 0)

{:no_tmp, _} = error ->
error
with {:ok, tmp} <- ensure_tmp() do
open(options, tmp, 0, nil)
end
end

Expand Down Expand Up @@ -154,7 +150,7 @@ defmodule Briefly.Entry do
if tmp = Enum.find_value(tmp_roots, &write_tmp_dir(Path.join(&1, subdir))) do
{:ok, tmp}
else
{:no_tmp, tmp_roots}
{:error, %Briefly.NoRootDirectoryError{tmp_dirs: tmp_roots}}
end
end

Expand All @@ -165,46 +161,42 @@ defmodule Briefly.Entry do
end
end

defp open(%{directory: true} = options, tmp, attempts) when attempts < @max_attempts do
defp open(%{directory: true} = options, tmp, attempts, _) when attempts < @max_attempts do
path = path(options, tmp)

case File.mkdir_p(path) do
:ok ->
:ets.insert(@path_table, {self(), path})
{:ok, path}

{:error, :enospc} ->
{:no_space, path}

{:error, reason} when reason in [:eexist, :eacces] ->
open(options, tmp, attempts + 1)
last_error = %Briefly.WriteError{code: reason, entry_type: :directory, tmp_dir: tmp}
open(options, tmp, attempts + 1, last_error)

error ->
error
{:error, code} ->
{:error, %Briefly.WriteError{code: code, entry_type: :directory, tmp_dir: tmp}}
end
end

defp open(options, tmp, attempts) when attempts < @max_attempts do
defp open(options, tmp, attempts, _) when attempts < @max_attempts do
path = path(options, tmp)

case :file.write_file(path, "", [:write, :raw, :exclusive, :binary]) do
:ok ->
:ets.insert(@path_table, {self(), path})
{:ok, path}

{:error, :enospc} ->
{:no_space, path}

{:error, reason} when reason in [:eexist, :eacces] ->
open(options, tmp, attempts + 1)
last_error = %Briefly.WriteError{code: reason, entry_type: :file, tmp_dir: tmp}
open(options, tmp, attempts + 1, last_error)

error ->
error
{:error, code} ->
{:error, %Briefly.WriteError{code: code, entry_type: :file, tmp_dir: tmp}}
end
end

defp open(_prefix, tmp, attempts) do
{:too_many_attempts, tmp, attempts}
defp open(_options, _tmp, _attempts, last_error) do
{:error, last_error}
end

defp path(options, tmp) do
Expand Down
38 changes: 38 additions & 0 deletions lib/briefly/exceptions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
defmodule Briefly.NoRootDirectoryError do
@moduledoc """
Returned when none of the root temporary directories could be accessed.
"""
@type t :: %__MODULE__{
:tmp_dirs => [String.t()]
}
defexception [:tmp_dirs]

@impl true
def message(_) do
"could not create a directory to store temporary files." <>
" Set the :briefly :directory application setting to a directory with write permission"
end
end

defmodule Briefly.WriteError do
@moduledoc """
Returned when a temporary file cannot be written.
"""
@type t :: %__MODULE__{
:code => :file.posix() | :badarg | :terminated | :system_limit,
:entry_type => :directory | :file,
:tmp_dir => String.t()
}
defexception [:code, :entry_type, :tmp_dir]

@impl true
def message(%{code: code} = e) when code in [:eexist, :eacces] do
"tried to create a temporary #{e.entry_type} in #{e.tmp_dir} but failed." <>
" Set the :briefly :directory application setting to a directory with write permission"
end

@impl true
def message(e) do
"could not write #{e.entry_type} in #{e.tmp_dir}, got: #{inspect(e.code)}"
end
end

0 comments on commit 6b98fe9

Please sign in to comment.