-module(fio@recursive). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/fio/recursive.gleam"). -export([list_recursive/1, copy_directory/2]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -file("src/fio/recursive.gleam", 13). -spec inode_key(fio@types:file_info(), binary()) -> binary(). inode_key(Info, Fallback_path) -> case erlang:element(5, Info) of 0 -> Fallback_path; _ -> <<<<<<"dev:"/utf8, (erlang:integer_to_binary(erlang:element(8, Info)))/binary>>/binary, ";ino:"/utf8>>/binary, (erlang:integer_to_binary(erlang:element(5, Info)))/binary>> end. -file("src/fio/recursive.gleam", 45). -spec do_list_recursive( binary(), binary(), gleam@set:set(binary()), list(binary()) ) -> {ok, list(binary())} | {error, fio@error:fio_error()}. do_list_recursive(Root, Current_rel, Visited, Acc) -> Current_dir = case Current_rel of <<""/utf8>> -> Root; Rel -> fio@path:join(Root, Rel) end, gleam@result:'try'( fio@internal@io:list_directory(Current_dir), fun(Items) -> gleam@list:try_fold( Items, Acc, fun(Inner_acc, Item) -> Item_rel = case Current_rel of <<""/utf8>> -> Item; Rel@1 -> fio@path:join(Rel@1, Item) end, Full_path = fio@path:join(Root, Item_rel), gleam@result:'try'( fio@internal@io:is_directory(Full_path), fun(Is_dir) -> case Is_dir of false -> {ok, [Item_rel | Inner_acc]}; true -> gleam@result:'try'( fio@internal@io:file_info(Full_path), fun(Info) -> Key = inode_key(Info, Full_path), case gleam@set:contains( Visited, Key ) of true -> {ok, [Item_rel | Inner_acc]}; false -> do_list_recursive( Root, Item_rel, gleam@set:insert( Visited, Key ), [Item_rel | Inner_acc] ) end end ) end end ) end ) end ). -file("src/fio/recursive.gleam", 29). ?DOC( " Recursively list files and directories (paths relative to `path`).\n" "\n" " Uses a flat string accumulator for O(n) traversal.\n" "\n" " **Symlink loop safety**: before descending into any directory, its real\n" " `(dev, inode)` pair (obtained via `stat`, which follows symlinks) is\n" " checked against the `visited` set. If already seen, the entry is listed\n" " but not descended into, breaking any A->B->A or deeper circular chains.\n" ). -spec list_recursive(binary()) -> {ok, list(binary())} | {error, fio@error:fio_error()}. list_recursive(Path) -> gleam@result:'try'( fio@internal@io:is_directory(Path), fun(Is_dir) -> case Is_dir of true -> gleam@result:'try'( fio@internal@io:file_info(Path), fun(Root_info) -> Visited = gleam@set:from_list( [inode_key(Root_info, Path)] ), gleam@result:'try'( do_list_recursive( Path, <<""/utf8>>, Visited, [] ), fun(Acc) -> {ok, lists:reverse(Acc)} end ) end ); false -> {error, enotdir} end end ). -file("src/fio/recursive.gleam", 102). -spec do_copy_directory(binary(), binary(), gleam@set:set(binary())) -> {ok, nil} | {error, fio@error:fio_error()}. do_copy_directory(Src, Dest, Visited) -> gleam@result:'try'( fio@internal@io:create_directory_all(Dest), fun(_) -> gleam@result:'try'( fio@internal@io:list_directory(Src), fun(Items) -> gleam@list:try_each( Items, fun(Item) -> Src_path = fio@path:join(Src, Item), Dest_path = fio@path:join(Dest, Item), gleam@result:'try'( fio@internal@io:is_directory(Src_path), fun(Is_dir_item) -> case Is_dir_item of false -> fio@internal@io:copy_file( Src_path, Dest_path ); true -> gleam@result:'try'( fio@internal@io:file_info( Src_path ), fun(Info) -> Key = inode_key( Info, Src_path ), case gleam@set:contains( Visited, Key ) of true -> {ok, nil}; false -> do_copy_directory( Src_path, Dest_path, gleam@set:insert( Visited, Key ) ) end end ) end end ) end ) end ) end ). -file("src/fio/recursive.gleam", 90). ?DOC( " Recursively copy `src` directory into `dest` (creates parents).\n" "\n" " **Symlink loop safety**: directory symlinks whose resolved inode has\n" " already been visited are skipped silently rather than followed forever.\n" ). -spec copy_directory(binary(), binary()) -> {ok, nil} | {error, fio@error:fio_error()}. copy_directory(Src, Dest) -> gleam@result:'try'( fio@internal@io:is_directory(Src), fun(Is_dir) -> case Is_dir of false -> {error, enotdir}; true -> gleam@result:'try'( fio@internal@io:file_info(Src), fun(Root_info) -> Visited = gleam@set:from_list( [inode_key(Root_info, Src)] ), do_copy_directory(Src, Dest, Visited) end ) end end ).