Files

276 lines
7.6 KiB
Elixir

defmodule Temp do
@type options :: nil | Path.t | map
@doc """
Returns `:ok` when the tracking server used to track temporary files started properly.
"""
@pdict_key :"$__temp_tracker__"
@spec track :: Agent.on_start
def track() do
case Process.get(@pdict_key) do
nil ->
start_tracker()
v ->
{:ok, v}
end
end
defp start_tracker() do
case GenServer.start_link(Temp.Tracker, nil, []) do
{:ok, pid} ->
Process.put(@pdict_key, pid)
{:ok, pid}
err ->
err
end
end
@doc """
Same as `track/0`, but raises an exception on failure. Otherwise, returns `:ok`
"""
@spec track! :: pid | no_return
def track!() do
case track() do
{:ok, pid} -> pid
{:error, err} -> raise Temp.Error, message: err
end
end
@doc """
Return the paths currently tracked.
"""
@spec tracked :: Set.t
def tracked(tracker \\ get_tracker!()) do
GenServer.call(tracker, :tracked)
end
@doc """
Cleans up the temporary files tracked.
"""
@spec cleanup(pid, Keyword.t) :: [Path.t]
def cleanup(tracker \\ get_tracker!(), opts \\ []) do
GenServer.call(tracker, :cleanup, opts[:timeout] || :infinity)
end
@doc """
Returns a `{:ok, path}` where `path` is a path that can be used freely in the
system temporary directory, or `{:error, reason}` if it fails to get the
system temporary directory.
This path is not tracked, so any file created will need manually removing, or
use `track_file/1` to have it removed automatically.
## Options
The following options can be used to customize the generated path
* `:prefix` - prepends the given prefix to the path
* `:suffix` - appends the given suffix to the path,
this is useful to generate a file with a particular extension
* `:basedir` - places the generated file in the designated base directory
instead of the system temporary directory
"""
@spec path(options) :: {:ok, Path.t} | {:error, String.t}
def path(options \\ nil) do
case generate_name(options, "f") do
{:ok, path, _} -> {:ok, path}
err -> err
end
end
@doc """
Same as `path/1`, but raises an exception on failure. Otherwise, returns a temporary path.
"""
@spec path!(options) :: Path.t | no_return
def path!(options \\ nil) do
case path(options) do
{:ok, path} -> path
{:error, err} -> raise Temp.Error, message: err
end
end
@doc """
Returns `{:ok, fd, file_path}` if no callback is passed, or `{:ok, file_path}`
if callback is passed, where `fd` is the file descriptor of a temporary file
and `file_path` is the path of the temporary file.
When no callback is passed, the file descriptor should be closed.
Returns `{:error, reason}` if a failure occurs.
The resulting file is automatically tracked if tracking is enabled.
## Options
See `path/1`.
"""
@spec open(options, nil | (File.io_device -> any)) :: {:ok, Path.t} | {:ok, File.io_device, Path.t} | {:error, any}
def open(options \\ nil, func \\ nil) do
case generate_name(options, "f") do
{:ok, path, options} ->
options = Map.put(options, :mode, options[:mode] || [:read, :write])
ret = if func do
File.open(path, options[:mode], func)
else
File.open(path, options[:mode])
end
case ret do
{:ok, res} ->
if tracker = get_tracker(), do: register_path(tracker, path)
if func, do: {:ok, path}, else: {:ok, res, path}
err -> err
end
err -> err
end
end
@doc """
Add a file to the tracker, so that it will be removed automatically or on Temp.cleanup.
"""
@spec track_file(any) :: {:error, :tracker_not_found} | {:ok, Path.t}
def track_file(path, tracker \\ get_tracker()) do
case is_nil(tracker) do
true -> {:error, :tracker_not_found}
false -> {:ok, register_path(tracker, path)}
end
end
@doc """
Same as `open/1`, but raises an exception on failure.
"""
@spec open!(options, nil | (File.io_device -> any)) :: Path.t | {File.io_device, Path.t} | no_return
def open!(options \\ nil, func \\ nil) do
case open(options, func) do
{:ok, res, path} -> {res, path}
{:ok, path} -> path
{:error, err} -> raise Temp.Error, message: err
end
end
@doc """
Returns `{:ok, dir_path}` where `dir_path` is the path is the path of the
created temporary directory.
Returns `{:error, reason}` if a failure occurs.
The directory is automatically tracked if tracking is enabled.
## Options
See `path/1`.
"""
@spec mkdir(options) :: {:ok, Path.t} | {:error, any}
def mkdir(options \\ %{}) do
case generate_name(options, "d") do
{:ok, path, _} ->
case File.mkdir path do
:ok ->
if tracker = get_tracker(), do: register_path(tracker, path)
{:ok, path}
err -> err
end
err -> err
end
end
@doc """
Same as `mkdir/1`, but raises an exception on failure. Otherwise, returns
a temporary directory path.
"""
@spec mkdir!(options) :: Path.t | no_return
def mkdir!(options \\ %{}) do
case mkdir(options) do
{:ok, path} ->
if tracker = get_tracker(), do: register_path(tracker, path)
path
{:error, err} -> raise Temp.Error, message: err
end
end
@spec generate_name(options, Path.t) :: {:ok, Path.t, map | Keyword.t} | {:error, String.t}
defp generate_name(options, default_prefix)
defp generate_name(options, default_prefix) when is_list(options) do
generate_name(Enum.into(options,%{}), default_prefix)
end
defp generate_name(options, default_prefix) do
case prefix(options) do
{:ok, path} ->
affixes = parse_affixes(options, default_prefix)
parts = [timestamp(), "-", :os.getpid(), "-", random_string()]
parts =
if affixes[:prefix] do
[affixes[:prefix], "-"] ++ parts
else
parts
end
parts = add_suffix(parts, affixes[:suffix])
name = Path.join(path, Enum.join(parts))
{:ok, name, affixes}
err -> err
end
end
defp add_suffix(parts, suffix)
defp add_suffix(parts, nil), do: parts
defp add_suffix(parts, ("." <> _suffix) = suffix), do: parts ++ [suffix]
defp add_suffix(parts, suffix), do: parts ++ ["-", suffix]
defp prefix(%{basedir: dir}), do: {:ok, dir}
defp prefix(_) do
case System.tmp_dir do
nil -> {:error, "no tmp_dir readable"}
path -> {:ok, path}
end
end
defp parse_affixes(nil, default_prefix), do: %{prefix: default_prefix}
defp parse_affixes(affixes, _) when is_bitstring(affixes), do: %{prefix: affixes, suffix: nil}
defp parse_affixes(affixes, default_prefix) when is_map(affixes) do
affixes
|> Map.put(:prefix, affixes[:prefix] || default_prefix)
|> Map.put(:suffix, affixes[:suffix] || nil)
end
defp parse_affixes(_, default_prefix) do
%{prefix: default_prefix, suffix: nil}
end
defp get_tracker do
Process.get(@pdict_key)
end
defp get_tracker!() do
case get_tracker() do
nil ->
raise Temp.Error, message: "temp tracker not started"
pid ->
pid
end
end
defp register_path(tracker, path) do
GenServer.call(tracker, {:add, path})
end
defp timestamp do
{ms, s, _} = :os.timestamp
Integer.to_string(ms * 1_000_000 + s)
end
defp random_string do
Integer.to_string(rand_uniform(0x100000000), 36) |> String.downcase
end
if :erlang.system_info(:otp_release) >= ~c"18" do
defp rand_uniform(num) do
:rand.uniform(num)
end
else
defp rand_uniform(num) do
:random.uniform(num)
end
end
end