From 9766ed773bbb46318e401a591e196559dfb812e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20M=C3=BCller?= Date: Mon, 29 May 2023 09:54:36 -0300 Subject: [PATCH] Safety check before running File.rm_rf! in doc gen (#1707) --- lib/ex_doc/formatter/epub.ex | 19 +++++++- lib/ex_doc/formatter/html.ex | 43 ++++++++++++++++--- test/ex_doc/formatter/epub_test.exs | 27 ++++++++++++ test/ex_doc/formatter/html/erlang_test.exs | 6 +++ .../formatter/html/search_items_test.exs | 6 +++ test/ex_doc/formatter/html_test.exs | 33 ++++++++++++++ test/test_helper.exs | 4 ++ 7 files changed, 131 insertions(+), 7 deletions(-) diff --git a/lib/ex_doc/formatter/epub.ex b/lib/ex_doc/formatter/epub.ex index 44a8c5175..d8f05e4a9 100644 --- a/lib/ex_doc/formatter/epub.ex +++ b/lib/ex_doc/formatter/epub.ex @@ -10,9 +10,14 @@ defmodule ExDoc.Formatter.EPUB do """ @spec run(list, ExDoc.Config.t()) :: String.t() def run(project_nodes, config) when is_map(config) do + parent = config.output config = normalize_config(config) - File.rm_rf!(config.output) - File.mkdir_p!(Path.join(config.output, "OEBPS")) + + HTML.setup_output( + parent, + &cleanup_output_dir(&1, config), + &create_output_dir(&1, config) + ) project_nodes = HTML.render_all(project_nodes, ".xhtml", config, highlight_tag: "samp") @@ -44,6 +49,16 @@ defmodule ExDoc.Formatter.EPUB do Path.relative_to_cwd(epub) end + defp create_output_dir(root, config) do + File.mkdir_p!(Path.join(config.output, "OEBPS")) + File.touch!(Path.join(root, ".ex_doc")) + end + + defp cleanup_output_dir(docs_root, config) do + File.rm_rf!(config.output) + create_output_dir(docs_root, config) + end + defp normalize_config(config) do output = config.output diff --git a/lib/ex_doc/formatter/html.ex b/lib/ex_doc/formatter/html.ex index 856818dd5..2c1ef3cbd 100644 --- a/lib/ex_doc/formatter/html.ex +++ b/lib/ex_doc/formatter/html.ex @@ -16,7 +16,7 @@ defmodule ExDoc.Formatter.HTML do config = %{config | output: Path.expand(config.output)} build = Path.join(config.output, ".build") - output_setup(build, config) + setup_output(config.output, &cleanup_output_dir(&1, config), &create_output_dir(&1, config)) project_nodes = render_all(project_nodes, ".html", config, []) extras = build_extras(config, ".html") @@ -146,18 +146,51 @@ defmodule ExDoc.Formatter.HTML do |> ExDoc.DocAST.highlight(language, opts) end - defp output_setup(build, config) do + def setup_output(root, cleanup, create) do + safety_path = Path.join(root, ".ex_doc") + + cond do + File.exists?(safety_path) and File.exists?(root) -> + cleanup.(root) + + not File.exists?(root) -> + create.(root) + + File.ls!(root) == [] -> + add_safety_file(root) + :ok + + true -> + IO.warn( + "ExDoc is outputting to an existing directory. " <> + "Beware documentation output may be mixed with other entries" + ) + end + end + + defp create_output_dir(root, _config) do + File.mkdir_p!(root) + add_safety_file(root) + end + + defp add_safety_file(root) do + File.touch!(Path.join(root, ".ex_doc")) + end + + defp cleanup_output_dir(docs_root, config) do + build = Path.join(docs_root, ".build") + if File.exists?(build) do build |> File.read!() |> String.split("\n", trim: true) - |> Enum.map(&Path.join(config.output, &1)) + |> Enum.map(&Path.join(docs_root, &1)) |> Enum.each(&File.rm/1) File.rm(build) else - File.rm_rf!(config.output) - File.mkdir_p!(config.output) + File.rm_rf!(docs_root) + create_output_dir(docs_root, config) end end diff --git a/test/ex_doc/formatter/epub_test.exs b/test/ex_doc/formatter/epub_test.exs index c1f02225c..58c8c330c 100644 --- a/test/ex_doc/formatter/epub_test.exs +++ b/test/ex_doc/formatter/epub_test.exs @@ -103,6 +103,33 @@ defmodule ExDoc.Formatter.EPUBTest do assert File.regular?(tmp_dir <> "/epub/another_dir/#{doc_config(context)[:project]}.epub") end + test "succeeds if trying to write into an empty existing directory", context do + config = doc_config(context) + + new_output = config[:output] <> "/new-dir" + File.mkdir_p!(new_output) + + new_config = Keyword.put(config, :output, new_output) + + refute ExUnit.CaptureIO.capture_io(:stderr, fn -> + generate_docs(new_config) + end) =~ "ExDoc is outputting to an existing directory" + end + + test "warns if trying to write into existing directory with files", context do + config = doc_config(context) + new_output = config[:output] <> "/new-dir" + + File.mkdir_p!(new_output) + File.touch!(Path.join(new_output, "dummy-file")) + + new_config = Keyword.put(config, :output, new_output) + + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + generate_docs(new_config) + end) =~ "ExDoc is outputting to an existing directory" + end + test "generates an EPUB file with a standardized structure", %{tmp_dir: tmp_dir} = context do generate_docs_and_unzip(context, doc_config(context)) diff --git a/test/ex_doc/formatter/html/erlang_test.exs b/test/ex_doc/formatter/html/erlang_test.exs index 9ac95267d..341fdb287 100644 --- a/test/ex_doc/formatter/html/erlang_test.exs +++ b/test/ex_doc/formatter/html/erlang_test.exs @@ -5,6 +5,12 @@ defmodule ExDoc.Formatter.HTML.ErlangTest do @moduletag :otp_eep48 @moduletag :tmp_dir + setup %{tmp_dir: tmp_dir} do + output = tmp_dir <> "/doc" + File.mkdir!(output) + File.touch!("#{output}/.ex_doc") + end + test "smoke test", c do erlc(c, :foo, ~S""" %% @doc diff --git a/test/ex_doc/formatter/html/search_items_test.exs b/test/ex_doc/formatter/html/search_items_test.exs index 2f8b4dce6..7af39cb6f 100644 --- a/test/ex_doc/formatter/html/search_items_test.exs +++ b/test/ex_doc/formatter/html/search_items_test.exs @@ -4,6 +4,12 @@ defmodule ExDoc.Formatter.HTML.SearchItemsTest do @moduletag :tmp_dir + setup %{tmp_dir: tmp_dir} do + output = tmp_dir <> "/doc" + File.mkdir!(output) + File.touch!("#{output}/.ex_doc") + end + test "Elixir module", c do modules = elixirc(c, ~S''' diff --git a/test/ex_doc/formatter/html_test.exs b/test/ex_doc/formatter/html_test.exs index 97654f7ef..ee8489c94 100644 --- a/test/ex_doc/formatter/html_test.exs +++ b/test/ex_doc/formatter/html_test.exs @@ -6,6 +6,12 @@ defmodule ExDoc.Formatter.HTMLTest do @moduletag :tmp_dir + setup %{tmp_dir: tmp_dir} do + output = tmp_dir <> "/html" + File.mkdir_p!(output) + File.touch!(output <> "/.ex_doc") + end + defp read_wildcard!(path) do [file] = Path.wildcard(path) File.read!(file) @@ -174,6 +180,33 @@ defmodule ExDoc.Formatter.HTMLTest do refute content_module =~ re[:index][:refresh] end + test "succeeds if trying to write into an empty existing directory", context do + config = doc_config(context) + + new_output = config[:output] <> "/new-dir" + File.mkdir_p!(new_output) + + new_config = Keyword.put(config, :output, new_output) + + refute ExUnit.CaptureIO.capture_io(:stderr, fn -> + generate_docs(new_config) + end) =~ "ExDoc is outputting to an existing directory" + end + + test "warns if trying to write into existing directory with files", context do + config = doc_config(context) + new_output = config[:output] <> "/new-dir" + + File.mkdir_p!(new_output) + File.touch!(Path.join(new_output, "dummy-file")) + + new_config = Keyword.put(config, :output, new_output) + + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + generate_docs(new_config) + end) =~ "ExDoc is outputting to an existing directory" + end + test "allows to set the authors of the document", %{tmp_dir: tmp_dir} = context do generate_docs(doc_config(context, authors: ["John Doe", "Jane Doe"])) content_index = File.read!(tmp_dir <> "/html/api-reference.html") diff --git a/test/test_helper.exs b/test/test_helper.exs index 9ce5e3e4b..bf13c7992 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -22,6 +22,10 @@ defmodule TestHelper do def elixirc(context, filename \\ "nofile", code) do dir = context.tmp_dir + output_dir = context.tmp_dir <> "/html" + File.mkdir_p!(output_dir) + File.write!(output_dir <> "/.ex_doc", "") + src_path = Path.join([dir, filename]) src_path |> Path.dirname() |> File.mkdir_p!() File.write!(src_path, code)