src/support/z_path.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2009-2022 Marc Worrell
%% @doc Defines all paths for files and directories of a site.

%% Copyright 2009-2022 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(z_path).
-author("Marc Worrell <marc@worrell.nl").

-export([
         site_dir/1,
         site_source_dir/1,
         module_dir/1,
         media_preview/1,
         media_archive/1,
         abspath/2,
         files_subdir/2,
         files_subdir_ensure/2,
         zotonic_apps/0,
         build_lib_dir/0,
         get_path/0
        ]).

-include("../../include/zotonic.hrl").

%% @doc Return the path to the site folder of the given context.
%%      The site must have been compiled.
-spec site_dir( z:context() | atom() ) -> file:filename_all() | {error, bad_name}.
site_dir(#context{ site = Site }) ->
    site_dir(Site);
site_dir(Site) when is_atom(Site) ->
    code:lib_dir(Site).

%% @doc Find the source directory of a site.
-spec site_source_dir( z:context() | atom() ) -> file:filename_all() | {error, bad_name}.
site_source_dir(#context{ site = Site }) ->
    site_dir(Site);
site_source_dir(Site) when is_atom(Site) ->
    LibDir = z_utils:lib_dir(),
    Dirs = [
        filename:join([zotonic_apps(), Site]),
        filename:join([LibDir, "apps_user", Site]),
        filename:join([LibDir, "apps", Site]),
        filename:join([LibDir, "_checkouts", Site])
    ],
    case lists:dropwhile(fun(D) -> not filelib:is_dir(D) end, Dirs) of
        [] ->
            case code:lib_dir(Site) of
                {error, bad_name} = E -> E;
                Dir -> maybe_follow_link(Dir)
            end;
        [D|_] ->
            D
    end.

maybe_follow_link(Dir) ->
    case file:read_link(filename:join(Dir, "src")) of
        {ok, "../" ++ _ = LinkDir} ->
            filename:join([Dir, LinkDir, ".."]);
        {ok, "/" ++ _ = LinkDir} ->
            filename:join([LinkDir, ".."]);
        {error, _} ->
            Dir
    end.

%% @doc Return the path to the given module in the given context.
%%      The module must have been compiled, so it is present in the code
%%      path.
-spec module_dir(atom()) -> file:filename_all() | {error, bad_name}.
module_dir(Module) ->
    code:lib_dir(Module).

%% @doc Return the path to the media preview directory
-spec media_preview(z:context()) -> file:filename_all() | {error, bad_name}.
media_preview(Context) ->
    files_subdir("preview", Context).

%% @doc Return the path to the media archive directory
-spec media_archive(z:context()) -> file:filename_all() | {error, bad_name}.
media_archive(Context) ->
    files_subdir("archive", Context).

%% @doc Return the absolute path to a file in the 'file' directory
-spec abspath( file:filename_all(), z:context() ) -> file:filename_all() | {error, bad_name}.
abspath(Path, Context) ->
    files_subdir(Path, Context).

%% @doc Return the path to a files subdirectory
-spec files_subdir(file:filename_all(), z:context()) -> file:filename_all() | {error, bad_name}.
files_subdir(SubDir, #context{ site = Site }) ->
    case site_dir(Site) of
        {error, _} = Error ->
            Error;
        Dir ->
            PrivDir = filename:join([Dir, "priv", "files"]),
            case filelib:is_dir(PrivDir) of
                true ->
                    % Old sites with the files stored in the sites's priv dir
                    filename:join([ PrivDir, SubDir ]);
                false ->
                    % Normally store files in the system data_dir
                    case z_config:get(data_dir) of
                        undefined ->
                            {error, bad_name};
                        DataDir ->
                            filename:join([DataDir, "sites", Site, "files", SubDir])
                    end
            end
    end.

%% @doc Return the path to a files subdirectory and ensure that the directory is present
-spec files_subdir_ensure(file:filename_all(), z:context()) -> file:filename_all() | {error, bad_name}.
files_subdir_ensure(SubDir, Context) ->
    case files_subdir(SubDir, Context) of
        {error, _} = Error ->
            Error;
        Dir ->
            case z_filelib:ensure_dir(filename:join([Dir, ".empty"])) of
                ok -> Dir;
                {error, _} = Error -> Error
            end
    end.

%% @doc The directory of the user-defined sites
-spec zotonic_apps() -> file:filename_all().
zotonic_apps() ->
    z_config:get(zotonic_apps).


%% @doc Get the path to the root dir of the Zotonic install.
%%      If the env var 'ZOTONIC' is not set, then return the current working dir.
-spec get_path() -> file:filename_all().
get_path() ->
    case os:getenv("ZOTONIC") of
        false ->
            {ok, Cwd} = file:get_cwd(),
            Cwd;
        ZotonicDir ->
            ZotonicDir
    end.

%% @doc Return the _build/default/lib directory
-spec build_lib_dir() -> file:filename_all().
build_lib_dir() ->
    filename:dirname(code:lib_dir(zotonic_core)).