Skip to main content

src/graffeo_config.erl

-module(graffeo_config).
-moduledoc """
Application-level configuration for graffeo's on-disk backends.

The primary export is `data_dir/0`, which resolves where DETS (and
future Mnesia) files are stored. Resolution chain:

1. Explicit `data_dir` from `sys.config` / `application:set_env` —
   used as-is.
2. `code:priv_dir(graffeo)/data` — if the directory is writable.
3. `graffeo_data/` under the current working directory — Mnesia-style
   writable fallback.
4. The OS user-cache directory — last resort.

**Production note:** `priv/` is often read-only in an OTP release.
Production deployments should set `data_dir` explicitly in
`sys.config`. The priv default is a dev/test/example convenience.
""".

-export([data_dir/0, resolve_candidates/1, build_candidates/1]).

-doc "Resolve the data directory for on-disk backends.".
-spec data_dir() -> file:filename_all().
data_dir() ->
    case application:get_env(graffeo, data_dir, default) of
        default ->
            Candidates = build_candidates(code:priv_dir(graffeo)),
            {ok, Dir} = resolve_candidates(Candidates),
            Dir;
        Dir when is_list(Dir) ->
            ensure(Dir);
        Dir when is_binary(Dir) ->
            ensure(binary_to_list(Dir))
    end.

-doc """
Build the ordered candidate list from a `code:priv_dir/1` result.

Exported for testability: pass `{error, bad_name}` to simulate a
missing priv directory and exercise the fallback chain.
""".
-spec build_candidates(file:filename_all() | {error, term()}) ->
    [file:filename_all()].
build_candidates({error, _}) ->
    ["graffeo_data", filename:basedir(user_cache, "graffeo")];
build_candidates(PrivDir) ->
    [
        filename:join(PrivDir, "data"),
        "graffeo_data",
        filename:basedir(user_cache, "graffeo")
    ].

-doc """
Return the first writable candidate directory.

Tries each directory in order; returns `{ok, Dir}` for the first one
where `filelib:ensure_dir/1` succeeds, or `error` if none are writable.
""".
-spec resolve_candidates([file:filename_all()]) ->
    {ok, file:filename_all()} | error.
resolve_candidates([]) ->
    error;
resolve_candidates([Dir | Rest]) ->
    case filelib:ensure_dir(filename:join(Dir, "probe")) of
        ok -> {ok, Dir};
        {error, _} -> resolve_candidates(Rest)
    end.

-spec ensure(file:filename_all()) -> file:filename_all().
ensure(Dir) ->
    ok = filelib:ensure_dir(filename:join(Dir, "probe")),
    Dir.