From bc7207fa7096ec9737afb49708234d8d94222aa0 Mon Sep 17 00:00:00 2001 From: pnezis Date: Sun, 8 Mar 2026 15:21:23 +0200 Subject: [PATCH] Preserve directory permissions in `File.cp_r/3` The documentation states that `cp_r/3` maintains "modes" when copying, but directory permissions were not being preserved - only file permissions. --- lib/elixir/lib/file.ex | 30 +++++++++++++++++----------- lib/elixir/test/elixir/file_test.exs | 20 +++++++++++++++++++ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index 9e334dbb631..d6d7388142b 100644 --- a/lib/elixir/lib/file.ex +++ b/lib/elixir/lib/file.ex @@ -1277,18 +1277,24 @@ defmodule File do {:ok, files} -> case mkdir(dest) do success when success in [:ok, {:error, :eexist}] -> - Enum.reduce_while(files, [dest | acc], fn x, acc -> - case do_cp_r( - Path.join(src, x), - Path.join(dest, x), - on_conflict, - dereference, - acc - ) do - {:error, _, _} = error -> {:halt, error} - acc -> {:cont, acc} - end - end) + case copy_file_mode(src, dest) do + :ok -> + Enum.reduce_while(files, [dest | acc], fn x, acc -> + case do_cp_r( + Path.join(src, x), + Path.join(dest, x), + on_conflict, + dereference, + acc + ) do + {:error, _, _} = error -> {:halt, error} + acc -> {:cont, acc} + end + end) + + {:error, reason} -> + {:error, reason, src} + end {:error, reason} -> {:error, reason, dest} diff --git a/lib/elixir/test/elixir/file_test.exs b/lib/elixir/test/elixir/file_test.exs index 9411a928e6c..846789db58e 100644 --- a/lib/elixir/test/elixir/file_test.exs +++ b/lib/elixir/test/elixir/file_test.exs @@ -869,6 +869,26 @@ defmodule FileTest do assert src_mode == dest_mode end + test "cp_r preserves directory mode" do + src = tmp_path("tmp/src_dir") + dest = tmp_path("tmp/dest_dir") + inner = Path.join(src, "inner") + + File.mkdir_p!(inner) + File.chmod!(inner, 0o700) + + try do + File.cp_r!(src, dest) + + %File.Stat{mode: src_mode} = File.stat!(inner) + %File.Stat{mode: dest_mode} = File.stat!(Path.join(dest, "inner")) + assert src_mode == dest_mode + after + File.rm_rf!(src) + File.rm_rf!(dest) + end + end + @tag :unix test "cp_r skips sockets and other special files" do # We use tmpdir because macOS has a limit on socket paths