Create nested maps from file path

Please I am trying to create nested maps from file path.

Home/Folder 1/ Folder 2/ Folder 3/ File.ex

How do i do that.
Thanks!

Hi @lilsaintdenzel welcome!

Can you provide example input and output data structures?

Also, what have you tried so far?

Hi @benwilson512

type or paste code here
defmodule MapDirStructure do
  @moduledoc """
  trying to map a directory structure
  """

  def parallel(p, f) when is_function(f) do
    # IO.inspect(p)
    # IO.inspect(f)

    p
    |> Enum.map(fn ele -> f.(ele) end)
  end

  def dir2obj(path \\ ".") do
    IO.inspect(path)

    path
    |> File.stat!()
    |> case do
      %File.Stat{type: :regular} ->
        %{String.to_charlist(path) => Path.basename(path)}

      _ ->
        File.ls!(path)
        |> parallel(fn p -> dir2obj(Path.join([path, p])) end)
        |> Enum.reduce(%{}, fn map, acc -> Map.merge(acc, map) end)
    end
  end


end

MapDirStructure.dir2obj(“Home/Folder 1/ Folder 2/ Folder 3”)

The chunk of code above prints. this

%{ 'Home/Folder 1/ Folder 2/ Folder 3/ => "File.ex"}

I want to print something like this

%{“Home” => %{“Folder 1” => %{“Folder 2”} =>%{“Folder 3” : [“File.ex”] } } },

All you need to do is to change a String path into List and transform it like:

defmodule Example do
  def sample(path) when is_binary(path), do: path |> Path.split() |> sample()
  def sample([file]), do: file
  def sample([directory | list]), do: %{directory => sample(list)}
end

iex> Example.sample("Home/Folder 1/Folder 2/Folder 3/File.ex")
%{"Home" => %{"Folder 1" => %{"Folder 2" => %{"Folder 3" => "File.ex"}}}}

Helpful resources:

  1. Kernel.is_binary/1
  2. Path.split/1
3 Likes

Thank You @Eiji

I have played a bit with “this and that” and you may find this code interesting as well:

defmodule Example do
  def print(contents, parent_path \\ "/")

  def print(directory, parent_path) when is_map(directory) do
    Enum.each(directory, fn {path, contents} ->
      print(path, parent_path)
      full_path = Path.join(parent_path, path)
      print(contents, full_path)
    end)
  end

  def print(contents, parent_path) when is_list(contents) do
    Enum.each(contents, &print(&1, parent_path))
  end

  def print(path, parent_path) when is_binary(path) do
    parent_path |> Path.join(path) |> IO.puts()
  end

  def sample(path \\ "."), do: path |> Path.expand() |> do_sample() |> IO.inspect()

  def do_sample(path, tree_root \\ true) do
    if File.regular?(path) do
      Path.basename(path)
    else
      contents =
        path
        |> File.ls!()
        |> Enum.map(&(path |> Path.join(&1) |> do_sample(false)))
        |> merge_contents()

      key = if tree_root, do: path, else: Path.basename(path)
      %{key => contents}
    end
  end

  defp merge_contents(list) do
    list
    |> Enum.reduce({%{}, []}, fn
      file, {directories, files} when is_binary(file) ->
        {directories, [file | files]}

      directory, {directories, files} when is_map(directory) ->
        {Map.merge(directories, directory), files}
    end)
    |> case do
      # empty directory - empty list
      {directories, []} when directories == %{} -> []
      # one file in directory - file string
      {directories, [file]} when directories == %{} -> file
      # multiple files in directory - files list
      {directories, files} when directories == %{} -> files
      # sub directories in directory and no files
      # merged maps of all sub directories
      {directories, []} -> directories
      # sub directories in directory and any number files
      # merged maps of all sub directories as list's head item and files as list's tail items
      {directories, files} -> [directories | files]
    end
  end
end

tree = Example.sample()
Example.print(tree)
1 Like

lilsaintdenzel

17m

Please I have been trying to traverse these nested maps to delete files and that are in different folders but same files. In other words removing duplicates but not in the same file path and leaving the most recent file.

Am stucked on how to approach these problem.

I don’t think that tree is good for this. Personally I would build a flat list of tuples. The first element would be a full path to file and the second its mtime.

Here you go:

defmodule Example do
  def find_old_duplicates(files, acc \\ [])

  def find_old_duplicates([], acc), do: acc

  def find_old_duplicates([file | files], acc) do
    {duplicates, rest} = Enum.split_with(files, &duplicate?(&1, file))
    [_ | duplicates] = Enum.sort_by([file | duplicates], &elem(&1, 1), {:desc, DateTime})
    find_old_duplicates(rest, acc ++ Enum.map(duplicates, &elem(&1, 0)))
  end

  defp duplicate?({first_path, _first_mtime}, {second_path, _second_mtime}) do
    {_output, status} = System.cmd("cmp", ["-s", first_path, second_path])
    status == 0
  end

  def recursive_list_files(path \\ ".", acc \\ []) do
    if File.regular?(path) do
      expanded_path = Path.expand(path)
      mtime = File.stat!(path, time: :posix).mtime
      [{expanded_path, DateTime.from_unix!(mtime)} | acc]
    else
      path
      |> File.ls!()
      |> Enum.reduce(acc, &(path |> Path.join(&1) |> recursive_list_files(&2)))
    end
  end
end

iex> files = Example.recursive_list_files()
# returns a list of {path, mtime} tuples
iex> Example.find_old_duplicates(files)
# returns list of paths

Note: Here I have used cmp tool. You may use other tool or Elixir code for comparing files depending on your OS and requirements.

You can also read File.Stat documentation to know more about information gathered from files.

2 Likes

Thank You

I tried this approach the issue I encountered some file duplicates in different folders have same timestamps. I am trying to find timestamps for folders in elixir. In order words the goal is to delete the file duplicate found in different folder with older timestamp( when the file timestamps are the same).

I searched for how to check for Folder timestamp in elixir couldn’t find anything, please do you have any idea how to get timestamps for folders in elixir

(Edit: sorry, thought this was a duplicate but it’s just similar).

1 Like

You can do it in exactly same way that I was doing it with file in last code example.