Skip to content
Merged
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
29 changes: 20 additions & 9 deletions lib/elixir/lib/file.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,13 @@ defmodule File do
directories removed in no specific order, `{:error, reason, file}`
otherwise.

## Options

* `:each_directory` - (since v1.20.0) a callback invoked for each
directory before its contents are deleted. The callback receives the
directory path as a binary. It is useful, for example, to grant write
permission to a directory before attempting to delete it.

## Examples

File.rm_rf("samples")
Expand All @@ -1630,24 +1637,28 @@ defmodule File do
File.rm_rf("/tmp")
#=> {:error, :eperm, "/tmp"}
"""
@spec rm_rf(Path.t()) :: {:ok, [binary]} | {:error, posix | :badarg, binary}
def rm_rf(path) do
@spec rm_rf(Path.t(), each_directory: (Path.t() -> term)) ::
{:ok, [binary]} | {:error, posix | :badarg, binary}
def rm_rf(path, options \\ []) do
{major, _} = :os.type()
each_directory = Keyword.get(options, :each_directory, fn _ -> :ok end)

path
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.rm_rf/1")
|> do_rm_rf([], major)
|> do_rm_rf([], major, each_directory)
end

defp do_rm_rf(path, acc, major) do
defp do_rm_rf(path, acc, major, each_directory) do
case safe_list_dir(path, major) do
{:ok, files} when is_list(files) ->
each_directory.(path)

acc =
Enum.reduce(files, acc, fn file, acc ->
# In case we can't delete, continue anyway, we might succeed
# to delete it on Windows due to how they handle symlinks.
case do_rm_rf(Path.join(path, file), acc, major) do
case do_rm_rf(Path.join(path, file), acc, major, each_directory) do
{:ok, acc} -> acc
{:error, _, _} -> acc
end
Expand Down Expand Up @@ -1723,7 +1734,7 @@ defmodule File do
end

@doc """
Same as `rm_rf/1` but raises a `File.Error` exception in case of failures,
Same as `rm_rf/2` but raises a `File.Error` exception in case of failures,
otherwise returns the list of files or directories removed.

## Examples
Expand All @@ -1737,9 +1748,9 @@ defmodule File do
File.rm_rf!("/tmp")
** (File.Error) could not remove files and directories recursively from "/tmp": not owner
"""
@spec rm_rf!(Path.t()) :: [binary]
def rm_rf!(path) do
case rm_rf(path) do
@spec rm_rf!(Path.t(), each_directory: (Path.t() -> term)) :: [binary]
def rm_rf!(path, options \\ []) do
case rm_rf(path, options) do
{:ok, files} ->
files

Expand Down
40 changes: 40 additions & 0 deletions lib/elixir/test/elixir/file_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,46 @@ defmodule FileTest do
assert File.rm_rf(dir) == {:ok, [dir, subdir]}
end

test "rm_rf with each_directory callback" do
fixture = tmp_path("tmp")
File.mkdir(fixture)
File.cp_r!(fixture_path("cp_r"), fixture)
me = self()

{:ok, files} =
File.rm_rf(fixture, each_directory: fn path -> send(me, {:dir, path}) end)

assert length(files) == 7

assert_received {:dir, ^fixture}
assert_received {:dir, _}
assert_received {:dir, _}
end

@tag :unix
test "rm_rf with each_directory callback for read-only dir" do
dir = tmp_path("tmp")
subdir = Path.join(dir, "read-only")
File.mkdir_p!(subdir)
File.write!(Path.join(subdir, "file.txt"), "hello")
File.chmod!(subdir, 0o444)

me = self()

{:ok, files} =
File.rm_rf(dir,
each_directory: fn path ->
send(me, {:dir, path})
File.chmod(path, 0o755)
end
)

assert length(files) == 3
refute File.exists?(dir)
assert_received {:dir, ^dir}
assert_received {:dir, ^subdir}
end

test "rm_rf with unknown" do
fixture = tmp_path("tmp.unknown")
assert File.rm_rf(fixture) == {:ok, []}
Expand Down
2 changes: 1 addition & 1 deletion lib/mix/test/mix/release_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ defmodule Mix.ReleaseTest do
end

setup do
File.rm_rf!(tmp_path("mix_release"))
File.rm_rf!(tmp_path("mix_release"), each_directory: &File.chmod(&1, 0o755))
File.mkdir_p!(tmp_path("mix_release"))
:ok
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mix/test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ defmodule MixTest.Case do
dest = tmp_path(String.replace(tmp, ":", "_"))
flag = String.to_charlist(tmp_path())

File.rm_rf!(dest)
File.rm_rf!(dest, each_directory: &File.chmod(&1, 0o755))
File.mkdir_p!(dest)
File.cp_r!(src, dest)

Expand Down
Loading