Skip to content

Commit d02e61d

Browse files
authored
Add each_directory to File.rm_rf, closes #15308 (#15311)
1 parent f2f2da9 commit d02e61d

4 files changed

Lines changed: 62 additions & 11 deletions

File tree

lib/elixir/lib/file.ex

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,6 +1619,13 @@ defmodule File do
16191619
directories removed in no specific order, `{:error, reason, file}`
16201620
otherwise.
16211621
1622+
## Options
1623+
1624+
* `:each_directory` - (since v1.20.0) a callback invoked for each
1625+
directory before its contents are deleted. The callback receives the
1626+
directory path as a binary. It is useful, for example, to grant write
1627+
permission to a directory before attempting to delete it.
1628+
16221629
## Examples
16231630
16241631
File.rm_rf("samples")
@@ -1630,24 +1637,28 @@ defmodule File do
16301637
File.rm_rf("/tmp")
16311638
#=> {:error, :eperm, "/tmp"}
16321639
"""
1633-
@spec rm_rf(Path.t()) :: {:ok, [binary]} | {:error, posix | :badarg, binary}
1634-
def rm_rf(path) do
1640+
@spec rm_rf(Path.t(), each_directory: (Path.t() -> term)) ::
1641+
{:ok, [binary]} | {:error, posix | :badarg, binary}
1642+
def rm_rf(path, options \\ []) do
16351643
{major, _} = :os.type()
1644+
each_directory = Keyword.get(options, :each_directory, fn _ -> :ok end)
16361645

16371646
path
16381647
|> IO.chardata_to_string()
16391648
|> assert_no_null_byte!("File.rm_rf/1")
1640-
|> do_rm_rf([], major)
1649+
|> do_rm_rf([], major, each_directory)
16411650
end
16421651

1643-
defp do_rm_rf(path, acc, major) do
1652+
defp do_rm_rf(path, acc, major, each_directory) do
16441653
case safe_list_dir(path, major) do
16451654
{:ok, files} when is_list(files) ->
1655+
each_directory.(path)
1656+
16461657
acc =
16471658
Enum.reduce(files, acc, fn file, acc ->
16481659
# In case we can't delete, continue anyway, we might succeed
16491660
# to delete it on Windows due to how they handle symlinks.
1650-
case do_rm_rf(Path.join(path, file), acc, major) do
1661+
case do_rm_rf(Path.join(path, file), acc, major, each_directory) do
16511662
{:ok, acc} -> acc
16521663
{:error, _, _} -> acc
16531664
end
@@ -1723,7 +1734,7 @@ defmodule File do
17231734
end
17241735

17251736
@doc """
1726-
Same as `rm_rf/1` but raises a `File.Error` exception in case of failures,
1737+
Same as `rm_rf/2` but raises a `File.Error` exception in case of failures,
17271738
otherwise returns the list of files or directories removed.
17281739
17291740
## Examples
@@ -1737,9 +1748,9 @@ defmodule File do
17371748
File.rm_rf!("/tmp")
17381749
** (File.Error) could not remove files and directories recursively from "/tmp": not owner
17391750
"""
1740-
@spec rm_rf!(Path.t()) :: [binary]
1741-
def rm_rf!(path) do
1742-
case rm_rf(path) do
1751+
@spec rm_rf!(Path.t(), each_directory: (Path.t() -> term)) :: [binary]
1752+
def rm_rf!(path, options \\ []) do
1753+
case rm_rf(path, options) do
17431754
{:ok, files} ->
17441755
files
17451756

lib/elixir/test/elixir/file_test.exs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,6 +1473,46 @@ defmodule FileTest do
14731473
assert File.rm_rf(dir) == {:ok, [dir, subdir]}
14741474
end
14751475

1476+
test "rm_rf with each_directory callback" do
1477+
fixture = tmp_path("tmp")
1478+
File.mkdir(fixture)
1479+
File.cp_r!(fixture_path("cp_r"), fixture)
1480+
me = self()
1481+
1482+
{:ok, files} =
1483+
File.rm_rf(fixture, each_directory: fn path -> send(me, {:dir, path}) end)
1484+
1485+
assert length(files) == 7
1486+
1487+
assert_received {:dir, ^fixture}
1488+
assert_received {:dir, _}
1489+
assert_received {:dir, _}
1490+
end
1491+
1492+
@tag :unix
1493+
test "rm_rf with each_directory callback for read-only dir" do
1494+
dir = tmp_path("tmp")
1495+
subdir = Path.join(dir, "read-only")
1496+
File.mkdir_p!(subdir)
1497+
File.write!(Path.join(subdir, "file.txt"), "hello")
1498+
File.chmod!(subdir, 0o444)
1499+
1500+
me = self()
1501+
1502+
{:ok, files} =
1503+
File.rm_rf(dir,
1504+
each_directory: fn path ->
1505+
send(me, {:dir, path})
1506+
File.chmod(path, 0o755)
1507+
end
1508+
)
1509+
1510+
assert length(files) == 3
1511+
refute File.exists?(dir)
1512+
assert_received {:dir, ^dir}
1513+
assert_received {:dir, ^subdir}
1514+
end
1515+
14761516
test "rm_rf with unknown" do
14771517
fixture = tmp_path("tmp.unknown")
14781518
assert File.rm_rf(fixture) == {:ok, []}

lib/mix/test/mix/release_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ defmodule Mix.ReleaseTest do
2727
end
2828

2929
setup do
30-
File.rm_rf!(tmp_path("mix_release"))
30+
File.rm_rf!(tmp_path("mix_release"), each_directory: &File.chmod(&1, 0o755))
3131
File.mkdir_p!(tmp_path("mix_release"))
3232
:ok
3333
end

lib/mix/test/test_helper.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ defmodule MixTest.Case do
175175
dest = tmp_path(String.replace(tmp, ":", "_"))
176176
flag = String.to_charlist(tmp_path())
177177

178-
File.rm_rf!(dest)
178+
File.rm_rf!(dest, each_directory: &File.chmod(&1, 0o755))
179179
File.mkdir_p!(dest)
180180
File.cp_r!(src, dest)
181181

0 commit comments

Comments
 (0)