276 lines
7.6 KiB
Elixir
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
|