Current section

Files

Jump to
polly src polly.erl
Raw

src/polly.erl

-module(polly).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([add_dir/2, max_depth/2, interval/2, filter/2, default_filter/1, new/0, stop/1, watch_with/3, watch/2]).
-export_type([event/0, fs_error/0, options/0, watcher/1, vfs/0, hydration_result/0, stat/0, file_type/0, file_access/0]).
-type event() :: {created, binary()} | {changed, binary()} | {deleted, binary()}.
-type fs_error() :: eacces | enoent | enotdir | {e_unknown, binary()}.
-opaque options() :: {options,
integer(),
list(binary()),
integer(),
fun((binary()) -> boolean())}.
-opaque watcher(GLX) :: {watcher, fun(() -> nil)} | {gleam_phantom, GLX}.
-type vfs() :: {file, binary(), integer()} |
{folder, binary(), integer(), list(vfs())}.
-type hydration_result() :: {success, vfs(), list(event())} |
skipped |
{errored, binary(), fs_error()}.
-type stat() :: {stat,
integer(),
file_type(),
file_access(),
integer(),
integer(),
integer(),
integer(),
integer(),
integer(),
integer(),
integer(),
integer(),
integer()}.
-type file_type() :: device | directory | symlink | other | regular.
-type file_access() :: read | write | read_write | none.
-spec add_dir(options(), binary()) -> options().
add_dir(Options, Path) ->
erlang:setelement(3, Options, [Path | erlang:element(3, Options)]).
-spec max_depth(options(), integer()) -> options().
max_depth(Options, Max_depth) ->
case {erlang:element(4, Options) < 0,
erlang:element(4, Options) > Max_depth} of
{true, _} ->
erlang:setelement(4, Options, Max_depth);
{false, true} ->
erlang:setelement(4, Options, Max_depth);
{false, false} ->
Options
end.
-spec interval(options(), integer()) -> options().
interval(Options, Interval) ->
case Interval > 0 of
true ->
erlang:setelement(2, Options, Interval);
false ->
Options
end.
-spec filter(options(), fun((binary()) -> boolean())) -> options().
filter(Options, Filter) ->
erlang:setelement(5, Options, Filter).
-spec default_filter(binary()) -> boolean().
default_filter(Path) ->
Is_hidden = begin
_pipe = filepath:base_name(Path),
gleam@string:starts_with(_pipe, <<"."/utf8>>)
end,
Is_hidden =:= false.
-spec new() -> options().
new() ->
{options, 1000, [], -1, fun default_filter/1}.
-spec stop(watcher(any())) -> nil.
stop(Watcher) ->
(erlang:element(2, Watcher))().
-spec 'try'(
binary(),
{ok, GML} | {error, fs_error()},
fun((GML) -> hydration_result())
) -> hydration_result().
'try'(Path, Result, Then) ->
case Result of
{ok, Value} ->
Then(Value);
{error, Err} ->
{errored, Path, Err}
end.
-spec do_created(binary(), vfs(), list(event())) -> list(event()).
do_created(Path, Vfs, Acc) ->
Full_path = filepath:join(Path, erlang:element(2, Vfs)),
Acc@1 = [{created, Full_path} | Acc],
case Vfs of
{file, _, _} ->
Acc@1;
{folder, _, _, Children} ->
gleam@list:fold(
Children,
Acc@1,
fun(Acc@2, Child) -> do_created(Full_path, Child, Acc@2) end
)
end.
-spec created(binary(), vfs()) -> list(event()).
created(Path, Vfs) ->
lists:reverse(do_created(Path, Vfs, [])).
-spec do_deleted(binary(), vfs(), list(event())) -> list(event()).
do_deleted(Path, Vfs, Acc) ->
Full_path = filepath:join(Path, erlang:element(2, Vfs)),
Acc@1 = [{deleted, Full_path} | Acc],
case Vfs of
{file, _, _} ->
Acc@1;
{folder, _, _, Children} ->
gleam@list:fold(
Children,
Acc@1,
fun(Acc@2, Child) -> do_deleted(Full_path, Child, Acc@2) end
)
end.
-spec deleted(binary(), vfs()) -> list(event()).
deleted(Path, Vfs) ->
do_deleted(Path, Vfs, []).
-spec get_modkey(stat()) -> integer().
get_modkey(Stat) ->
gleam@int:max(erlang:element(6, Stat), erlang:element(7, Stat)).
-spec map_error({ok, GNE} | {error, fs_error()}) -> {ok, GNE} |
{error, fs_error()}.
map_error(Result) ->
gleam@result:map_error(Result, fun(Err) -> case Err of
eacces ->
Err;
enotdir ->
Err;
enoent ->
Err;
_ ->
{e_unknown, gleam@string:inspect(Err)}
end end).
-spec readdir(binary()) -> {ok, list(binary())} | {error, fs_error()}.
readdir(Path) ->
_pipe = polly_ffi:readdir(Path),
_pipe@1 = map_error(_pipe),
gleam@result:map(
_pipe@1,
fun(_capture) ->
gleam@list:sort(_capture, fun gleam@string:compare/2)
end
).
-spec lstat(binary()) -> {ok, stat()} | {error, fs_error()}.
lstat(Path) ->
map_error(polly_ffi:lstat(Path)).
-spec diff_children(
fun((binary()) -> boolean()),
integer(),
binary(),
binary(),
integer(),
list(vfs()),
list(binary()),
list(vfs()),
list(event())
) -> hydration_result().
diff_children(
Filter,
Depth,
Path,
Name,
Modkey,
Old_children,
New_entries,
New_children,
Events
) ->
case {Old_children, New_entries} of
{[], []} ->
{success,
{folder, Name, Modkey, lists:reverse(New_children)},
Events};
{[First_old | Rest_old], [First_new | Rest_new]} ->
case gleam@string:compare(erlang:element(2, First_old), First_new) of
eq ->
case diff(Filter, Depth, Path, First_old, Events) of
{success, New_vfs, Events@1} ->
New_children@1 = [New_vfs | New_children],
diff_children(
Filter,
Depth,
Path,
Name,
Modkey,
Rest_old,
Rest_new,
New_children@1,
Events@1
);
skipped ->
Events@2 = lists:append(
deleted(Path, First_old),
Events
),
diff_children(
Filter,
Depth,
Path,
Name,
Modkey,
Rest_old,
Rest_new,
New_children,
Events@2
);
{errored, _, _} = Err ->
Err
end;
gt ->
case hydrate(Filter, Depth, Path, First_new) of
{success, New_vfs@1, _} ->
Events@3 = lists:append(
created(Path, New_vfs@1),
Events
),
New_children@2 = [New_vfs@1 | New_children],
diff_children(
Filter,
Depth,
Path,
Name,
Modkey,
Old_children,
Rest_new,
New_children@2,
Events@3
);
skipped ->
diff_children(
Filter,
Depth,
Path,
Name,
Modkey,
Old_children,
Rest_new,
New_children,
Events
);
{errored, _, _} = Err@1 ->
Err@1
end;
lt ->
Events@4 = lists:append(deleted(Path, First_old), Events),
diff_children(
Filter,
Depth,
Path,
Name,
Modkey,
Rest_old,
New_entries,
New_children,
Events@4
)
end;
{[], [First_new@1 | Rest_new@1]} ->
case hydrate(Filter, Depth, Path, First_new@1) of
{success, New_vfs@2, _} ->
Events@5 = lists:append(created(Path, New_vfs@2), Events),
New_children@3 = [New_vfs@2 | New_children],
diff_children(
Filter,
Depth,
Path,
Name,
Modkey,
Old_children,
Rest_new@1,
New_children@3,
Events@5
);
skipped ->
diff_children(
Filter,
Depth,
Path,
Name,
Modkey,
Old_children,
Rest_new@1,
New_children,
Events
);
{errored, _, _} = Err@2 ->
Err@2
end;
{[First_old@1 | Rest_old@1], []} ->
Events@6 = lists:append(deleted(Path, First_old@1), Events),
diff_children(
Filter,
Depth,
Path,
Name,
Modkey,
Rest_old@1,
New_entries,
New_children,
Events@6
)
end.
-spec diff(
fun((binary()) -> boolean()),
integer(),
binary(),
vfs(),
list(event())
) -> hydration_result().
diff(Filter, Depth, Path, Vfs, Events) ->
Full_path = filepath:join(Path, erlang:element(2, Vfs)),
gleam@bool:guard(
not Filter(Full_path),
skipped,
fun() -> case lstat(Full_path) of
{ok, Stat} ->
gleam@bool:guard(
erlang:element(4, Stat) /= read_write,
skipped,
fun() -> case {erlang:element(3, Stat), Vfs} of
{regular, {file, Name, Old_key}} ->
New_key = get_modkey(Stat),
case New_key =:= Old_key of
true ->
{success, Vfs, Events};
false ->
{success,
{file, Name, New_key},
[{changed, Full_path} | Events]}
end;
{directory, {folder, _, _, _}} when Depth =:= 0 ->
{success, Vfs, Events};
{directory, {folder, Name@1, _, Old_children}} when Depth =/= 0 ->
New_key@1 = get_modkey(Stat),
'try'(
Full_path,
readdir(Full_path),
fun(New_entries) ->
diff_children(
Filter,
Depth - 1,
Full_path,
Name@1,
New_key@1,
Old_children,
New_entries,
[],
Events
)
end
);
{_, _} ->
case hydrate_stat(
Filter,
Depth,
erlang:element(2, Vfs),
Full_path,
Stat
) of
{success, New_vfs, _} ->
Events@1 = lists:append(
deleted(Path, Vfs),
created(Path, New_vfs)
),
{success, New_vfs, Events@1};
_ = Res ->
Res
end
end end
);
{error, eacces} ->
skipped;
{error, enoent} ->
skipped;
{error, E} ->
{errored, Path, E}
end end
).
-spec hydrate_children(
fun((binary()) -> boolean()),
integer(),
binary(),
list(binary()),
list(vfs())
) -> {ok, list(vfs())} | {error, {binary(), fs_error()}}.
hydrate_children(Filter, Depth, Path, Children, Acc) ->
case Children of
[] ->
{ok, lists:reverse(Acc)};
[First | Rest] ->
case hydrate(Filter, Depth, Path, First) of
{success, Vfs, _} ->
hydrate_children(Filter, Depth, Path, Rest, [Vfs | Acc]);
skipped ->
hydrate_children(Filter, Depth, Path, Rest, Acc);
{errored, Path@1, Err} ->
{error, {Path@1, Err}}
end
end.
-spec hydrate(fun((binary()) -> boolean()), integer(), binary(), binary()) -> hydration_result().
hydrate(Filter, Depth, Path, Name) ->
Full_path = filepath:join(Path, Name),
gleam@bool:guard(
not Filter(Full_path),
skipped,
fun() -> case lstat(Full_path) of
{ok, Stat} ->
hydrate_stat(Filter, Depth, Name, Full_path, Stat);
{error, eacces} ->
skipped;
{error, enoent} ->
skipped;
{error, E} ->
{errored, Full_path, E}
end end
).
-spec hydrate_stat(
fun((binary()) -> boolean()),
integer(),
binary(),
binary(),
stat()
) -> hydration_result().
hydrate_stat(Filter, Depth, Name, Full_path, Stat) ->
gleam@bool:guard(
erlang:element(4, Stat) /= read_write,
skipped,
fun() -> case erlang:element(3, Stat) of
regular ->
{success, {file, Name, get_modkey(Stat)}, []};
directory when Depth =:= 0 ->
{success, {folder, Name, get_modkey(Stat), []}, []};
directory when Depth =/= 0 ->
'try'(
Full_path,
readdir(Full_path),
fun(Entries) ->
case hydrate_children(
Filter,
Depth - 1,
Full_path,
Entries,
[]
) of
{ok, Children} ->
{success,
{folder,
Name,
get_modkey(Stat),
Children},
[]};
{error, {Path, Reason}} ->
{errored, Path, Reason}
end
end
);
_ ->
skipped
end end
).
-spec watch_with(options(), GMF, fun((GMF, event()) -> GMF)) -> {ok,
watcher(GMF)} |
{error, {binary(), fs_error()}}.
watch_with(Options, Initial, Callback) ->
gleam@result:'try'(
(gleam@list:try_map(
erlang:element(3, Options),
fun(Path) ->
case hydrate(
erlang:element(5, Options),
erlang:element(4, Options),
Path,
<<""/utf8>>
) of
{success, Vfs, _} ->
{ok, {Path, {some, Vfs}}};
skipped ->
{error, {Path, enoent}};
{errored, Path@1, Err} ->
{error, {Path@1, Err}}
end
end
)),
fun(Roots) ->
State = {Roots, Initial},
Stop = polly_ffi:repeatedly(
erlang:element(2, Options),
State,
fun(State@1) ->
{Roots@1, User_state} = State@1,
gleam@list:fold(
Roots@1,
{[], User_state},
fun(_use0, _use1) ->
{New_roots, User_state@1} = _use0,
{Path@2, Vfs@1} = _use1,
case Vfs@1 of
{some, Vfs@2} ->
case diff(
erlang:element(5, Options),
erlang:element(4, Options),
Path@2,
Vfs@2,
[]
) of
{success, Vfs@3, Events} ->
User_state@2 = gleam@list:fold(
Events,
User_state@1,
Callback
),
{[{Path@2, {some, Vfs@3}} |
New_roots],
User_state@2};
_ ->
Events@1 = deleted(Path@2, Vfs@2),
User_state@3 = gleam@list:fold(
Events@1,
User_state@1,
Callback
),
{[{Path@2, none} | New_roots],
User_state@3}
end;
none ->
case hydrate(
erlang:element(5, Options),
erlang:element(4, Options),
Path@2,
<<""/utf8>>
) of
{success, Vfs@4, _} ->
Events@2 = created(Path@2, Vfs@4),
User_state@4 = gleam@list:fold(
Events@2,
User_state@1,
Callback
),
{[{Path@2, {some, Vfs@4}} |
New_roots],
User_state@4};
_ ->
{[{Path@2, Vfs@1} | New_roots],
User_state@1}
end
end
end
)
end
),
{ok, {watcher, Stop}}
end
).
-spec watch(options(), fun((event()) -> any())) -> {ok, watcher(nil)} |
{error, {binary(), fs_error()}}.
watch(Options, Callback) ->
watch_with(
Options,
nil,
fun(_, Event) ->
Callback(Event),
nil
end
).