├── test ├── test_helper.exs └── temp_test.exs ├── lib ├── temp │ ├── error.ex │ └── tracker.ex └── temp.ex ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── config └── config.exs ├── mix.exs ├── mix.lock └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /lib/temp/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Temp.Error do 2 | defexception message: "Could not make temp file" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | temp-*.tar 24 | 25 | # Temporary files for e.g. tests 26 | /tmp 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | name: Build and test 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Elixir 21 | uses: erlef/setup-beam@61e01a43a562a89bfc54c7f9a378ff67b03e4a21 # v1.16.0 22 | with: 23 | elixir-version: '1.17.2' 24 | otp-version: '27.0' 25 | - name: Restore dependencies cache 26 | uses: actions/cache@v3 27 | with: 28 | path: deps 29 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 30 | restore-keys: ${{ runner.os }}-mix- 31 | - name: Install dependencies 32 | run: mix deps.get 33 | - name: Run tests 34 | run: mix test 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Daniel Perez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Temp.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/tuvistavie/elixir-temp" 5 | @version "0.4.9" 6 | 7 | def project do 8 | [ 9 | app: :temp, 10 | version: @version, 11 | elixir: "~> 1.9", 12 | name: "temp", 13 | source_url: @source_url, 14 | homepage_url: @source_url, 15 | package: package(), 16 | description: description(), 17 | build_embedded: Mix.env() == :prod, 18 | start_permanent: Mix.env() == :prod, 19 | deps: deps(), 20 | docs: [source_ref: "#{@version}", extras: ["README.md"], main: "readme"] 21 | ] 22 | end 23 | 24 | def application do 25 | [extra_applications: [:logger]] 26 | end 27 | 28 | defp deps() do 29 | [ 30 | {:earmark, "~> 1.0", only: :dev}, 31 | {:ex_doc, "~> 0.19", only: :dev} 32 | ] 33 | end 34 | 35 | defp description do 36 | "An Elixir module to easily create and use temporary files and directories." 37 | end 38 | 39 | defp package do 40 | [ 41 | files: ["lib", "mix.exs", "README.md", "LICENSE"], 42 | maintainers: ["Daniel Perez"], 43 | licenses: ["MIT"], 44 | links: %{"GitHub" => @source_url} 45 | ] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/temp/tracker.ex: -------------------------------------------------------------------------------- 1 | defmodule Temp.Tracker do 2 | use GenServer 3 | 4 | if :application.get_key(:elixir, :vsn) |> elem(1) |> to_string() |> Version.match?("~> 1.1") do 5 | defp set(), do: MapSet.new 6 | defdelegate put(set, value), to: MapSet 7 | else 8 | defp set(), do: HashSet.new 9 | defdelegate put(set, value), to: HashSet 10 | end 11 | 12 | def init(_args) do 13 | Process.flag(:trap_exit, true) 14 | {:ok, set()} 15 | end 16 | 17 | def handle_call({:add, item}, _from, state) do 18 | {:reply, item, put(state, item)} 19 | end 20 | 21 | def handle_call(:tracked, _from, state) do 22 | {:reply, state, state} 23 | end 24 | 25 | def handle_call(:cleanup, _from, state) do 26 | {removed, failed} = cleanup(state) 27 | {:reply, removed, Enum.into(failed, set())} 28 | end 29 | 30 | def terminate(_reason, state) do 31 | cleanup(state) 32 | :ok 33 | end 34 | 35 | defp cleanup(state) do 36 | {removed, failed} = 37 | state 38 | |> Enum.reduce({[], []}, fn path, {removed, failed} -> 39 | case File.rm_rf(path) do 40 | {:ok, _} -> {[path | removed], failed} 41 | _ -> {removed, [path | failed]} 42 | end 43 | end) 44 | {:lists.reverse(removed), :lists.reverse(failed)} 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 4 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 5 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 7 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 8 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elixir-temp 2 | 3 | [![Elixir CI](https://github.com/danhper/elixir-temp/actions/workflows/ci.yml/badge.svg)](https://github.com/danhper/elixir-temp/actions/workflows/ci.yml) 4 | [![Module Version](https://img.shields.io/hexpm/v/temp.svg)](https://hex.pm/packages/temp) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/temp/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/temp.svg)](https://hex.pm/packages/temp) 7 | [![License](https://img.shields.io/hexpm/l/temp.svg)](https://github.com/danhper/elixir-temp/blob/master/LICENSE) 8 | [![Last Updated](https://img.shields.io/github/last-commit/danhper/elixir-temp.svg)](https://github.com/danhper/elixir-temp/commits/master) 9 | 10 | An Elixir module to easily create and use temporary files and directories. 11 | The module is inspired by [node-temp](https://github.com/bruce/node-temp). 12 | 13 | ## Installation 14 | 15 | Add the dependency to your `mix.exs` deps: 16 | 17 | ```elixir 18 | defp deps do 19 | [{:temp, "~> 0.4"}] 20 | end 21 | ``` 22 | 23 | ## Usage 24 | 25 | ### Getting a temporary path 26 | 27 | ```elixir 28 | # just get a path 29 | {:ok, tmp_path} = Temp.path 30 | # with a prefix 31 | {:ok, tmp_path} = Temp.path "my-prefix" 32 | # with prefix and suffix 33 | {:ok, tmp_path} = Temp.path %{prefix: "my-prefix", suffix: "my-suffix"} 34 | # in a non-default tmp_dir 35 | {:ok, tmp_path} = Temp.path %{prefix: "my-prefix", suffix: "my-suffix", basedir: "/my-tmp"} 36 | # error on fail 37 | tmp_path = Temp.path! 38 | ``` 39 | 40 | ### Using a temporary directory 41 | 42 | Note that you can use all the options available for `Temp.path` as the first argument. 43 | 44 | ```elixir 45 | # tmp dir 46 | {:ok, dir_path} = Temp.mkdir "my-dir" 47 | IO.puts dir_path 48 | File.write Path.join(dir_path, "file_in_my_dir"), "some content" 49 | # remove when done 50 | File.rm_rf dir_path 51 | ``` 52 | 53 | You can use the `Temp.mkdir!` if you prefer to have an error on failure. 54 | 55 | ### Using a temporary file 56 | 57 | Note that you can use all the options available for `Temp.path` as the first argument. 58 | 59 | ```elixir 60 | # tmp file 61 | {:ok, fd, file_path} = Temp.open "my-file" 62 | IO.puts file_path 63 | IO.write fd, "some content" 64 | File.close fd 65 | # remove when done 66 | File.rm file_path 67 | ``` 68 | 69 | You can also pass a function to `open` and use the file descriptor in it. In this case, the file will be closed automatically. 70 | 71 | ```elixir 72 | # tmp file 73 | {:ok, file_path} = Temp.open "my-file", &IO.write(&1, "some content") 74 | IO.puts file_path 75 | IO.puts File.read!(file_path) 76 | # remove when done 77 | File.rm file_path 78 | ``` 79 | 80 | ### Tracking temporary files 81 | 82 | By default, you have to cleanup the files by yourself, however, you can tell 83 | `Temp` to track the temporary files automatically. 84 | You just need to call `Temp.track` (or the bang version `Temp.track!`) and you are done. 85 | Temporary files will be cleaned up automatically when the process exits. 86 | You can also call `Temp.cleanup` if you want to clean them before the process exits. 87 | Here is an example of how to use it: 88 | 89 | ```elixir 90 | Temp.track! 91 | 92 | dir_path = Temp.mkdir! "my-dir" 93 | File.write Path.join(dir_path, "file_in_my_dir"), "some content" 94 | 95 | file_path = Temp.open! "my-file", &IO.write(&1, "some content") 96 | IO.puts file_path 97 | 98 | IO.puts inspect(Temp.tracked) 99 | 100 | # cleanup 101 | Temp.cleanup 102 | 103 | dir_path = Temp.mkdir 104 | # this will be cleaned up on exit 105 | ``` 106 | 107 | ## License 108 | 109 | This source code is licensed under the MIT License. Copyright (c) 2015, Daniel 110 | Perez. All rights reserved. 111 | -------------------------------------------------------------------------------- /test/temp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TempTest do 2 | use ExUnit.Case 3 | 4 | test :path do 5 | {:ok, path} = Temp.path 6 | assert File.exists?(Path.dirname(path)) 7 | assert String.starts_with?(Path.basename(path), "f-") 8 | refute String.ends_with?(Path.basename(path), "-") 9 | 10 | path = Temp.path! 11 | assert File.exists?(Path.dirname(path)) 12 | assert path != Temp.path! 13 | 14 | path = Temp.path!(basedir: "foo") 15 | assert Path.dirname(path) == "foo" 16 | 17 | path = Temp.path!(basedir: "bar", prefix: "my-prefix") 18 | assert Path.dirname(path) == "bar" 19 | assert String.starts_with?(Path.basename(path), "my-prefix-") 20 | 21 | path = Temp.path!(basedir: "other", prefix: "my-prefix", suffix: "my-suffix") 22 | assert Path.dirname(path) == "other" 23 | assert String.starts_with?(Path.basename(path), "my-prefix-") 24 | assert String.ends_with?(Path.basename(path), "-my-suffix") 25 | 26 | path = Temp.path!(suffix: ".txt") 27 | assert String.ends_with?(path, ".txt") 28 | refute String.ends_with?(path, "-.txt") 29 | end 30 | 31 | test :open do 32 | {:ok, file, path} = Temp.open 33 | assert File.exists?(path) 34 | assert String.starts_with?(Path.basename(path), "f-") 35 | refute String.ends_with?(Path.basename(path), "-") 36 | IO.write(file, "foobar") 37 | File.close(file) 38 | assert File.read!(path) == "foobar" 39 | File.rm!(path) 40 | 41 | {:ok, path} = Temp.open("bar", fn f -> IO.write(f, "foobar") end) 42 | assert File.exists?(path) 43 | assert File.read!(path) == "foobar" 44 | File.rm!(path) 45 | 46 | {err, _} = Temp.open(basedir: "/") 47 | assert err == :error 48 | end 49 | 50 | test :open! do 51 | {file, path} = Temp.open! 52 | assert File.exists?(path) 53 | assert String.starts_with?(Path.basename(path), "f-") 54 | refute String.ends_with?(Path.basename(path), "-") 55 | IO.write(file, "foobar") 56 | File.close(file) 57 | assert File.read!(path) == "foobar" 58 | File.rm!(path) 59 | 60 | path = Temp.open!("bar", fn f -> IO.write(f, "foobar") end) 61 | assert File.exists?(path) 62 | assert File.read!(path) == "foobar" 63 | File.rm!(path) 64 | 65 | assert_raise Temp.Error, fn -> 66 | Temp.open! %{basedir: "/"} 67 | end 68 | end 69 | 70 | test :mkdir do 71 | {:ok, dir} = Temp.mkdir 72 | assert File.exists?(dir) 73 | assert String.starts_with?(Path.basename(dir), "d-") 74 | refute String.ends_with?(Path.basename(dir), "-") 75 | File.rmdir!(dir) 76 | 77 | dir = Temp.mkdir!("abc") 78 | assert File.exists?(dir) 79 | assert String.starts_with?(Path.basename(dir), "abc") 80 | File.rmdir!(dir) 81 | 82 | {osfamily, _} = :os.type 83 | unless osfamily == :win32 do 84 | {err, _} = Temp.mkdir(basedir: "/") 85 | assert err == :error 86 | end 87 | end 88 | 89 | test :track do 90 | assert {:ok, tracker} = Temp.track 91 | {:ok, dir} = Temp.mkdir(nil) 92 | assert File.exists?(dir) 93 | 94 | {:ok, path} = Temp.open("bar", &IO.write(&1, "foobar")) 95 | assert File.exists?(path) 96 | 97 | assert Enum.count(Temp.tracked) == 2 98 | 99 | parent = self() 100 | spawn_link fn -> 101 | send parent, {:count, Temp.tracked(tracker) |> Enum.count} 102 | end 103 | assert_receive {:count, 2} 104 | 105 | assert Enum.count(Temp.cleanup) == 2 106 | refute File.exists?(dir) 107 | refute File.exists?(path) 108 | assert Enum.count(Temp.tracked) == 0 109 | 110 | # check cleanup can be called multiple times safely 111 | {:ok, dir} = Temp.mkdir nil 112 | assert File.exists?(dir) 113 | assert Enum.count(Temp.cleanup) == 1 114 | refute File.exists?(dir) 115 | 116 | {:ok, dir} = Temp.mkdir(nil) 117 | spawn_link fn -> 118 | send(parent, {:cleaned, Temp.cleanup(tracker) |> Enum.count}) 119 | end 120 | assert_receive {:cleaned, 1} 121 | refute File.exists?(dir) 122 | end 123 | 124 | test :track_file do 125 | assert {:ok, tracker} = Temp.track 126 | 127 | path_of_tmp_file = "test/tmp_file_created_by_programmer" 128 | File.write!(path_of_tmp_file, "Make Elixir Gr8 Again") 129 | 130 | assert File.exists?(path_of_tmp_file) 131 | 132 | Temp.track_file(path_of_tmp_file, tracker) 133 | 134 | Temp.cleanup(tracker) 135 | 136 | refute File.exists?(path_of_tmp_file) 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/temp.ex: -------------------------------------------------------------------------------- 1 | defmodule Temp do 2 | @type options :: nil | Path.t | map 3 | 4 | @doc """ 5 | Returns `:ok` when the tracking server used to track temporary files started properly. 6 | """ 7 | 8 | @pdict_key :"$__temp_tracker__" 9 | 10 | @spec track :: Agent.on_start 11 | def track() do 12 | case Process.get(@pdict_key) do 13 | nil -> 14 | start_tracker() 15 | v -> 16 | {:ok, v} 17 | end 18 | end 19 | 20 | defp start_tracker() do 21 | case GenServer.start_link(Temp.Tracker, nil, []) do 22 | {:ok, pid} -> 23 | Process.put(@pdict_key, pid) 24 | {:ok, pid} 25 | err -> 26 | err 27 | end 28 | end 29 | 30 | @doc """ 31 | Same as `track/0`, but raises an exception on failure. Otherwise, returns `:ok` 32 | """ 33 | @spec track! :: pid | no_return 34 | def track!() do 35 | case track() do 36 | {:ok, pid} -> pid 37 | {:error, err} -> raise Temp.Error, message: err 38 | end 39 | end 40 | 41 | @doc """ 42 | Return the paths currently tracked. 43 | """ 44 | @spec tracked :: Set.t 45 | def tracked(tracker \\ get_tracker!()) do 46 | GenServer.call(tracker, :tracked) 47 | end 48 | 49 | 50 | @doc """ 51 | Cleans up the temporary files tracked. 52 | """ 53 | @spec cleanup(pid, Keyword.t) :: [Path.t] 54 | def cleanup(tracker \\ get_tracker!(), opts \\ []) do 55 | GenServer.call(tracker, :cleanup, opts[:timeout] || :infinity) 56 | end 57 | 58 | @doc """ 59 | Returns a `{:ok, path}` where `path` is a path that can be used freely in the 60 | system temporary directory, or `{:error, reason}` if it fails to get the 61 | system temporary directory. 62 | 63 | This path is not tracked, so any file created will need manually removing, or 64 | use `track_file/1` to have it removed automatically. 65 | 66 | ## Options 67 | 68 | The following options can be used to customize the generated path 69 | 70 | * `:prefix` - prepends the given prefix to the path 71 | 72 | * `:suffix` - appends the given suffix to the path, 73 | this is useful to generate a file with a particular extension 74 | 75 | * `:basedir` - places the generated file in the designated base directory 76 | instead of the system temporary directory 77 | """ 78 | @spec path(options) :: {:ok, Path.t} | {:error, String.t} 79 | def path(options \\ nil) do 80 | case generate_name(options, "f") do 81 | {:ok, path, _} -> {:ok, path} 82 | err -> err 83 | end 84 | end 85 | 86 | @doc """ 87 | Same as `path/1`, but raises an exception on failure. Otherwise, returns a temporary path. 88 | """ 89 | @spec path!(options) :: Path.t | no_return 90 | def path!(options \\ nil) do 91 | case path(options) do 92 | {:ok, path} -> path 93 | {:error, err} -> raise Temp.Error, message: err 94 | end 95 | end 96 | 97 | @doc """ 98 | Returns `{:ok, fd, file_path}` if no callback is passed, or `{:ok, file_path}` 99 | if callback is passed, where `fd` is the file descriptor of a temporary file 100 | and `file_path` is the path of the temporary file. 101 | When no callback is passed, the file descriptor should be closed. 102 | Returns `{:error, reason}` if a failure occurs. 103 | 104 | The resulting file is automatically tracked if tracking is enabled. 105 | 106 | ## Options 107 | 108 | See `path/1`. 109 | """ 110 | @spec open(options, nil | (File.io_device -> any)) :: {:ok, Path.t} | {:ok, File.io_device, Path.t} | {:error, any} 111 | def open(options \\ nil, func \\ nil) do 112 | case generate_name(options, "f") do 113 | {:ok, path, options} -> 114 | options = Map.put(options, :mode, options[:mode] || [:read, :write]) 115 | ret = if func do 116 | File.open(path, options[:mode], func) 117 | else 118 | File.open(path, options[:mode]) 119 | end 120 | case ret do 121 | {:ok, res} -> 122 | if tracker = get_tracker(), do: register_path(tracker, path) 123 | if func, do: {:ok, path}, else: {:ok, res, path} 124 | err -> err 125 | end 126 | err -> err 127 | end 128 | end 129 | 130 | @doc """ 131 | Add a file to the tracker, so that it will be removed automatically or on Temp.cleanup. 132 | """ 133 | @spec track_file(any) :: {:error, :tracker_not_found} | {:ok, Path.t} 134 | def track_file(path, tracker \\ get_tracker()) do 135 | case is_nil(tracker) do 136 | true -> {:error, :tracker_not_found} 137 | false -> {:ok, register_path(tracker, path)} 138 | end 139 | end 140 | 141 | @doc """ 142 | Same as `open/1`, but raises an exception on failure. 143 | """ 144 | @spec open!(options, nil | (File.io_device -> any)) :: Path.t | {File.io_device, Path.t} | no_return 145 | def open!(options \\ nil, func \\ nil) do 146 | case open(options, func) do 147 | {:ok, res, path} -> {res, path} 148 | {:ok, path} -> path 149 | {:error, err} -> raise Temp.Error, message: err 150 | end 151 | end 152 | 153 | 154 | @doc """ 155 | Returns `{:ok, dir_path}` where `dir_path` is the path is the path of the 156 | created temporary directory. 157 | Returns `{:error, reason}` if a failure occurs. 158 | 159 | The directory is automatically tracked if tracking is enabled. 160 | 161 | ## Options 162 | 163 | See `path/1`. 164 | """ 165 | @spec mkdir(options) :: {:ok, Path.t} | {:error, any} 166 | def mkdir(options \\ %{}) do 167 | case generate_name(options, "d") do 168 | {:ok, path, _} -> 169 | case File.mkdir path do 170 | :ok -> 171 | if tracker = get_tracker(), do: register_path(tracker, path) 172 | {:ok, path} 173 | err -> err 174 | end 175 | err -> err 176 | end 177 | end 178 | 179 | @doc """ 180 | Same as `mkdir/1`, but raises an exception on failure. Otherwise, returns 181 | a temporary directory path. 182 | """ 183 | @spec mkdir!(options) :: Path.t | no_return 184 | def mkdir!(options \\ %{}) do 185 | case mkdir(options) do 186 | {:ok, path} -> 187 | if tracker = get_tracker(), do: register_path(tracker, path) 188 | path 189 | {:error, err} -> raise Temp.Error, message: err 190 | end 191 | end 192 | 193 | @spec generate_name(options, Path.t) :: {:ok, Path.t, map | Keyword.t} | {:error, String.t} 194 | defp generate_name(options, default_prefix) 195 | defp generate_name(options, default_prefix) when is_list(options) do 196 | generate_name(Enum.into(options,%{}), default_prefix) 197 | end 198 | defp generate_name(options, default_prefix) do 199 | case prefix(options) do 200 | {:ok, path} -> 201 | affixes = parse_affixes(options, default_prefix) 202 | parts = [timestamp(), "-", :os.getpid(), "-", random_string()] 203 | parts = 204 | if affixes[:prefix] do 205 | [affixes[:prefix], "-"] ++ parts 206 | else 207 | parts 208 | end 209 | parts = add_suffix(parts, affixes[:suffix]) 210 | name = Path.join(path, Enum.join(parts)) 211 | {:ok, name, affixes} 212 | err -> err 213 | end 214 | end 215 | 216 | defp add_suffix(parts, suffix) 217 | defp add_suffix(parts, nil), do: parts 218 | defp add_suffix(parts, ("." <> _suffix) = suffix), do: parts ++ [suffix] 219 | defp add_suffix(parts, suffix), do: parts ++ ["-", suffix] 220 | 221 | defp prefix(%{basedir: dir}), do: {:ok, dir} 222 | defp prefix(_) do 223 | case System.tmp_dir do 224 | nil -> {:error, "no tmp_dir readable"} 225 | path -> {:ok, path} 226 | end 227 | end 228 | 229 | defp parse_affixes(nil, default_prefix), do: %{prefix: default_prefix} 230 | defp parse_affixes(affixes, _) when is_bitstring(affixes), do: %{prefix: affixes, suffix: nil} 231 | defp parse_affixes(affixes, default_prefix) when is_map(affixes) do 232 | affixes 233 | |> Map.put(:prefix, affixes[:prefix] || default_prefix) 234 | |> Map.put(:suffix, affixes[:suffix] || nil) 235 | end 236 | defp parse_affixes(_, default_prefix) do 237 | %{prefix: default_prefix, suffix: nil} 238 | end 239 | 240 | defp get_tracker do 241 | Process.get(@pdict_key) 242 | end 243 | 244 | defp get_tracker!() do 245 | case get_tracker() do 246 | nil -> 247 | raise Temp.Error, message: "temp tracker not started" 248 | pid -> 249 | pid 250 | end 251 | end 252 | 253 | defp register_path(tracker, path) do 254 | GenServer.call(tracker, {:add, path}) 255 | end 256 | 257 | defp timestamp do 258 | {ms, s, _} = :os.timestamp 259 | Integer.to_string(ms * 1_000_000 + s) 260 | end 261 | 262 | defp random_string do 263 | Integer.to_string(rand_uniform(0x100000000), 36) |> String.downcase 264 | end 265 | 266 | if :erlang.system_info(:otp_release) >= ~c"18" do 267 | defp rand_uniform(num) do 268 | :rand.uniform(num) 269 | end 270 | else 271 | defp rand_uniform(num) do 272 | :random.uniform(num) 273 | end 274 | end 275 | end 276 | --------------------------------------------------------------------------------