# https://github.com/adobe/elixir-styler/issues/43#issuecomment-1555951813 opts_schema = NimbleOptions.new!( path: [ type: :string, required: true, doc: "Path to the final file that is atomically written." ], overwrite?: [ type: :boolean, default: true, doc: "Overwrite the target file if it already exists?" ], tmp_dir: [ type: :string, default: ".", doc: "Directory in which the temporary files are written." ] ) defmodule AtomicWrites do @moduledoc """ Performs atomic file writes. ## Options #{NimbleOptions.docs(opts_schema)} ## LWW Atomic Writes ```elixir AtomicWrites.write("Atomically written content.", path: "example.txt") ``` ## FWW Atomic Writes ```elixir AtomicWrites.write("Atomically written content.", path: "example.txt", overwrite?: false) ``` ## Serialized Atomic Writes ``` elixir alias AtomicWrites.AtomicFile {:ok, pid} = AtomicFile.start_link(path: "example.txt") AtomicFile.write(pid, "Serialized, atomically written content.") ``` ## Installation The package can be installed by adding `atomic_writes` to your list of dependencies in `mix.exs`: ```elixir def deps do [ {:atomic_writes, "~> 1.0"} ] end ``` ## License AtomicWrites is released under the [`Apache License 2.0`](https://github.com/elliotekj/atomic_writes/blob/main/LICENSE). ## About This package was written by [Elliot Jackson](https://elliotekj.com). - Blog: [https://elliotekj.com](https://elliotekj.com) - Email: elliot@elliotekj.com """ @opts_schema opts_schema @doc """ Atomically write to the path. """ @spec write(iodata(), Keyword.t()) :: :ok | {:error, atom()} def write(content, opts) do with {:ok, opts} <- validate_opts(opts), opts <- expand_opts(opts), {:ok, tmp_file_path} <- preflight(opts), :ok <- File.write(tmp_file_path, content) do result = maybe_move_file(tmp_file_path, opts[:path], opts[:overwrite?]) spawn(fn -> File.rm(tmp_file_path) end) result else e -> e end end @doc false def opts_schema, do: @opts_schema @doc false def validate_opts(opts) do case Keyword.get(opts, :valid?) == true do true -> {:ok, opts} false -> NimbleOptions.validate(opts, @opts_schema) end end @doc false def expand_opts(opts) do opts |> Keyword.put(:valid?, true) |> Keyword.put(:tmp_dir, Path.expand(opts[:tmp_dir])) |> Keyword.put(:path, Path.expand(opts[:path])) end defp uniq_filename, do: UUID.uuid4() <> ".atomicwrite" defp preflight(opts) do tmp_filename = uniq_filename() tmp_file_path = Path.join([opts[:tmp_dir], tmp_filename]) dest_dir_path = Path.dirname(opts[:path]) with :ok <- File.mkdir_p(opts[:tmp_dir]), :ok <- File.mkdir_p(dest_dir_path) do {:ok, tmp_file_path} else e -> e end end defp maybe_move_file(tmp_file_path, dest_file_path, overwrite?) do case File.exists?(dest_file_path) && overwrite? == false do true -> {:error, :eexist} false -> File.rename(tmp_file_path, dest_file_path) end end end