Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 95 additions & 31 deletions lib/temp.ex
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
defmodule Temp do
@type options :: nil | Path.t | map
@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
@spec track :: Agent.on_start()
def track() do
case Process.get(@pdict_key) do
nil ->
start_tracker()

v ->
{:ok, v}
end
Expand All @@ -22,6 +23,7 @@ defmodule Temp do
{:ok, pid} ->
Process.put(@pdict_key, pid)
{:ok, pid}

err ->
err
end
Expand All @@ -41,16 +43,15 @@ defmodule Temp do
@doc """
Return the paths currently tracked.
"""
@spec tracked :: Set.t
@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]
@spec cleanup(pid, Keyword.t()) :: [Path.t()]
def cleanup(tracker \\ get_tracker!(), opts \\ []) do
GenServer.call(tracker, :cleanup, opts[:timeout] || :infinity)
end
Expand All @@ -75,7 +76,7 @@ defmodule Temp do
* `: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}
@spec path(options) :: {:ok, Path.t()} | {:error, String.t()}
def path(options \\ nil) do
case generate_name(options, "f") do
{:ok, path, _} -> {:ok, path}
Expand All @@ -86,7 +87,7 @@ defmodule Temp do
@doc """
Same as `path/1`, but raises an exception on failure. Otherwise, returns a temporary path.
"""
@spec path!(options) :: Path.t | no_return
@spec path!(options) :: Path.t() | no_return
def path!(options \\ nil) do
case path(options) do
{:ok, path} -> path
Expand All @@ -107,30 +108,38 @@ defmodule Temp do

See `path/1`.
"""
@spec open(options, nil | (File.io_device -> any)) :: {:ok, Path.t} | {:ok, File.io_device, Path.t} | {:error, any}
@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

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

err ->
err
end
err -> err

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}
@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}
Expand All @@ -141,7 +150,7 @@ defmodule Temp do
@doc """
Same as `open/1`, but raises an exception on failure.
"""
@spec open!(options, pid | nil) :: Path.t | {File.io_device, Path.t} | no_return
@spec open!(options, pid | nil) :: 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}
Expand All @@ -150,7 +159,6 @@ defmodule Temp do
end
end


@doc """
Returns `{:ok, dir_path}` where `dir_path` is the path is the path of the
created temporary directory.
Expand All @@ -162,77 +170,132 @@ defmodule Temp do

See `path/1`.
"""
@spec mkdir(options) :: {:ok, Path.t} | {:error, any}
@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
case File.mkdir(path) do
:ok ->
if tracker = get_tracker(), do: register_path(tracker, path)
{:ok, path}
err -> err

err ->
err
end
err -> err

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
@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

{:error, err} ->
raise Temp.Error, message: err
end
end

@doc """
Removes all passed paths from the tracker for the current processes,
and gives them to the tracker at heir_pid.
Returns `:ok` if successful.
Returns `{:error, reason}` if a failure occurs.
"""
@spec handoff(Path.t() | [Path.t()], pid()) :: :ok | {:error, String.t()}
def handoff(paths, heir_pid, tracker \\ get_tracker()) do
case tracker do
nil ->
{:error, "no tracker"}

tracker_pid ->
if Process.alive?(heir_pid) do
paths =
if !is_list(paths) do
[paths]
else
paths
end

GenServer.call(tracker_pid, {:handoff, paths, heir_pid})
else
{:error, "dead heir pid"}
end
end
end

@doc """
Same as `handoff/3`, but raises an exception on failure. Otherwise, returns `:ok`.
"""
@spec handoff!(Path.t() | [Path.t()], pid()) :: :ok | no_return()
def handoff!(paths, heir_pid, tracker \\ get_tracker()) do
case handoff(paths, heir_pid, tracker) do
{:error, reason} -> raise Temp.Error, message: reason
:ok -> :ok
end
end

@spec generate_name(options, Path.t) :: {:ok, Path.t, map | Keyword.t} | {:error, String.t}
@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)
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

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 = 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
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
Expand All @@ -245,6 +308,7 @@ defmodule Temp do
case get_tracker() do
nil ->
raise Temp.Error, message: "temp tracker not started"

pid ->
pid
end
Expand All @@ -255,12 +319,12 @@ defmodule Temp do
end

defp timestamp do
{ms, s, _} = :os.timestamp
{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
Integer.to_string(rand_uniform(0x100000000), 36) |> String.downcase()
end

if :erlang.system_info(:otp_release) >= '18' do
Expand Down
44 changes: 41 additions & 3 deletions lib/temp/tracker.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
defmodule Temp.Tracker do
use GenServer
@type state :: MapSet.t() | HashSet.t()

if :application.get_key(:elixir, :vsn) |> elem(1) |> to_string() |> Version.match?("~> 1.1") do
defp set(), do: MapSet.new
def set(), do: MapSet.new()
def set(list), do: MapSet.new(list)
defdelegate union(set1, set2), to: MapSet
defdelegate put(set, value), to: MapSet
defdelegate difference(set1, set2), to: MapSet
defdelegate intersection(set1, set2), to: MapSet
else
defp set(), do: HashSet.new
def set(), do: HashSet.new()

def set(list) do
set_helper(list, set())
end

defp set_helper([], cur_set), do: cur_set

defp set_helper([head | tail], cur_set) do
set_helper(tail, put(cur_set, head))
end

defdelegate union(set1, set2), to: HashSet
defdelegate put(set, value), to: HashSet
defdelegate difference(set1, set2), to: HashSet
defdelegate intersection(set1, set2), to: HashSet
end

@spec start_link(any()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, nil)
end

def init(_args) do
@spec init(any()) :: {:ok, state()}
def init(_) do
Process.flag(:trap_exit, true)
{:ok, set()}
end
Expand All @@ -18,6 +43,10 @@ defmodule Temp.Tracker do
{:reply, item, put(state, item)}
end

def handle_call({:receive, passed_over_set}, _from, state) do
{:reply, :ok, union(passed_over_set, state)}
end

def handle_call(:tracked, _from, state) do
{:reply, state, state}
end
Expand All @@ -27,6 +56,14 @@ defmodule Temp.Tracker do
{:reply, removed, Enum.into(failed, set())}
end

def handle_call({:handoff, paths, heir_pid}, _from, state) do
paths_set = set(paths)
new_state = difference(state, paths_set)
passed_over_set = intersection(state, paths_set)
GenServer.call(heir_pid, {:receive, passed_over_set})
{:reply, :ok, new_state}
end

def terminate(_reason, state) do
cleanup(state)
:ok
Expand All @@ -41,6 +78,7 @@ defmodule Temp.Tracker do
_ -> {removed, [path | failed]}
end
end)

{:lists.reverse(removed), :lists.reverse(failed)}
end
end
Loading