defmodule Temp do @type options :: nil | Path.t | map @doc """ Returns an agent `pid` used to track temporary files """ @spec track :: Agent.on_start def track do Agent.start_link(fn -> HashSet.new end) end @doc """ Same as `track/1`, but raises an exception on failure. Otherwise, returns the tracker `pid`. """ @spec track! :: pid def track! do case track do {:ok, tracker} -> tracker err -> raise Temp.Error, message: err end end @doc """ Return the paths currently tracked by the tracker agent. """ @spec tracked(pid) :: Set.t def tracked(tracker) do Agent.get tracker, fn paths -> paths end end @doc """ Cleans up the temporary files tracked by the passed agent `pid` """ @spec cleanup(pid) :: :ok | {:error, any} def cleanup(tracker) do result = Agent.get tracker, fn paths -> Enum.reduce paths, {:ok, :tmp}, fn(path, acc) -> case acc do {:ok, _} -> File.rm_rf(path) err -> err end end end case result do {:ok, _} -> Agent.update tracker, fn _ -> HashSet.new end err -> err end 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. """ @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 def path!(options \\ nil) do case path(options) do {:ok, path} -> path err -> 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. A tracker agent pid (created by `track/1`) can be passed as a second argument to keep track of the created file. Returns `{:error, reason}` if a failure occurs. """ @spec open(options, nil | (File.io_device -> any), nil | pid) :: {:ok, File.io_device, Path.t} | {:error, any} def open(options \\ nil, func \\ nil, tracker \\ nil) do case generate_name(options, "f") do {:ok, path, options} -> options = Dict.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, do: register_path(tracker, path) if func, do: {:ok, path}, else: {:ok, res, path} err -> err end err -> err end end @doc """ Same as `open/1`, but raises an exception on failure. """ @spec open!(options, pid | nil) :: {File.io_device, Path.t} def open!(options \\ nil, func \\ nil) do case open(options, func) do {:ok, res, path} -> {res, 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. A tracker agent pid (created by `track/1`) can be passed as a second argument to keep track of the created directory. Returns `{:error, reason}` if a failure occurs. """ @spec mkdir(options, nil | pid) :: {:ok, Path.t} | {:error, any} def mkdir(options \\ %{}, tracker \\ nil) do case generate_name(options, "d") do {:ok, path, _} -> case File.mkdir path do :ok -> if 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, nil | pid) :: Path.t def mkdir!(options \\ %{}, tracker \\ nil) do case mkdir(options) do {:ok, path} -> if 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} | {:error, String.t} defp generate_name(options, default_prefix) do case prefix(options) do {:ok, path} -> affixes = parse_affixes(options, default_prefix) name = Path.join(path, [ affixes[:prefix], "-", timestamp, "-", :os.getpid, "-", random_string, "-", affixes[:suffix] ] |> Enum.join) {:ok, name, affixes} err -> err end end @spec prefix(nil | map) :: {:ok, Path.t} | {:error, String.t} 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 @spec parse_affixes(options, Path.t) :: map defp parse_affixes(nil, default_prefix), do: %{prefix: default_prefix} defp parse_affixes(affixes, _) when is_bitstring(affixes), do: %{prefix: affixes, suffix: ""} defp parse_affixes(affixes, default_prefix) when is_map(affixes) do affixes |> Dict.put(:prefix, affixes[:prefix] || default_prefix) |> Dict.put(:suffix, affixes[:suffix] || "") end defp parse_affixes(_, default_prefix) do %{prefix: default_prefix, suffix: ""} end defp register_path(tracker, path) do Agent.update(tracker, &Set.put(&1, 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(:random.uniform(0x100000000), 36) |> String.downcase end end