Current section

Files

Jump to
relx src rlx_file_utils.erl
Raw

src/rlx_file_utils.erl

-module(rlx_file_utils).
-export([mkdtemp/0,
mkdir_p/1,
wildcard_paths/1,
copy/2,
copy/3,
ensure_writable/1,
copy_file_info/3,
exists/1,
write/2,
write_term/2,
is_symlink/1,
is_dir/1,
type/1,
symlink_or_copy/2,
remove/1,
remove/2,
print_path/1,
path_from_ancestor/2,
format_error/1]).
-include("relx.hrl").
-include_lib("kernel/include/file.hrl").
-spec mkdtemp() -> file:name() | {error, term()}.
mkdtemp() ->
UniqueNumber = erlang:integer_to_list(rand:uniform(1000000000000)),
TmpDirPath = filename:join(tmp(), [".tmp_dir-", UniqueNumber]),
case mkdir_p(TmpDirPath) of
ok ->
TmpDirPath;
Error ->
Error
end.
-spec tmp() -> file:name().
tmp() ->
case erlang:system_info(system_architecture) of
"win32" ->
case os:getenv("TEMP") of
false ->
"./tmp";
Val ->
Val
end;
_SysArch ->
case os:getenv("TMPDIR") of
false ->
"/tmp";
Val ->
Val
end
end.
%% @doc Makes a directory including parent dirs if they are missing.
-spec mkdir_p(string()) -> ok | {error, Reason::file:posix()}.
mkdir_p(Path) ->
%% We are exploiting a feature of ensuredir that that creates all
%% directories up to the last element in the filename, then ignores
%% that last element. This way we ensure that the dir is created
%% and not have any worries about path names
DirName = filename:join([filename:absname(Path), "tmp"]),
filelib:ensure_dir(DirName).
%% @doc expand wildcards and names in the given paths
-spec wildcard_paths([file:filename_all()]) -> [string()].
wildcard_paths(Paths) ->
[filename:absname(Expanded) || Path <- Paths, Expanded <- wildcard(Path)].
%% In case the given directory does not expand,
%% we return it back in a list so we trigger the
%% proper error reportings.
-spec wildcard(file:filename_all()) -> [string()].
wildcard(Path) when is_binary(Path) ->
wildcard(binary_to_list(Path));
wildcard(Path) when is_list(Path) ->
case filelib:wildcard(Path) of
[] -> [Path];
Paths -> Paths
end.
-spec exists(file:filename()) -> boolean().
exists(Filename) ->
case file:read_file_info(Filename) of
{ok, _} ->
true;
{error, _Reason} ->
false
end.
-spec write(file:name(), string()) -> ok | {error, term()}.
write(FileName, Contents) ->
file:write_file(FileName, Contents).
-spec write_term(file:filename(), term()) -> ok | {error, term()}.
write_term(FileName, Term) ->
write(FileName, lists:flatten(io_lib:fwrite("~p. ", [Term]))).
-spec is_symlink(file:name()) -> boolean().
is_symlink(Path) ->
case file:read_link_info(Path) of
{ok, #file_info{type = symlink}} ->
true;
_ ->
false
end.
symlink_or_copy(Source, Target) ->
case file:make_symlink(Source, Target) of
ok ->
ok;
{error, eexist} ->
{error, eexist};
{error, Err} ->
case {os:type(), Err} of
{{win32, _}, eperm} ->
% We get eperm on Windows if we do not have
% SeCreateSymbolicLinkPrivilege
% Try the next alternative
win32_make_junction_or_copy(Source, Target);
_ ->
% On other systems we try to copy next
cp_r(Source, Target)
end
end.
cp_r(Source, Target) ->
copy(Source, Target, [{recursive, true}, {file_info, [mode, time, owner, group]}]).
win32_make_junction_or_copy(Source, Target) ->
case filelib:is_dir(Source) of
true ->
win32_make_junction(Source, Target);
_ ->
cp_r(Source, Target)
end.
win32_make_junction(Source, Target) ->
% The mklink will fail if the target already exists, check for that first
case file:read_link_info(Target) of
{error, enoent} ->
win32_make_junction_cmd(Source, Target);
{ok, #file_info{type = symlink}} ->
case file:read_link(Target) of
{ok, Source} ->
ok;
{ok, _} ->
ok = file:del_dir(Target),
win32_make_junction_cmd(Source, Target);
{error, Reason} ->
{error, {readlink, Reason}}
end;
{ok, #file_info{type = _Type}} ->
% Directory already exists, so we overwrite the copy
cp_r(Source, Target);
Error ->
Error
end.
win32_make_junction_cmd(Source, Target) ->
S = unicode:characters_to_list(Source),
T = unicode:characters_to_list(Target),
Cmd = "cmd /c mklink /j \"" ++ filename:nativename(T) ++ "\" \"" ++ filename:nativename(S) ++ "\"",
case os:cmd(Cmd) of
"Junction created " ++ _ ->
ok;
_ ->
% When not English output, success out will not match, so just recheck link target
% if target is source, then also do nothing
case file:read_link(Target) of
{ok, Source} ->
ok;
_ ->
cp_r(Source, Target)
end
end.
-spec copy(file:name(), file:name()) -> ok | {error, term()}.
copy(From, To) ->
copy_(From, To, [{file_info, [mode, time, owner, group]}]).
%% @doc copy an entire directory to another location.
copy(From, To, Options) ->
case proplists:get_value(recursive, Options, false) of
true ->
case is_dir(From) of
false ->
copy_(From, To, Options);
true ->
make_dir_if_dir(To),
copy_subfiles(From, To, Options)
end;
false ->
copy_(From, To, Options)
end.
copy_(From, To, Options) ->
case file:copy(From, To) of
{ok, _} ->
copy_file_info(To, From, proplists:get_value(file_info, Options, []));
{error, Error} ->
{error, {copy_failed, From, To, Error}}
end.
ensure_writable(File) ->
{ok, #file_info{mode=CurrentMode}} = file:read_file_info(File),
case file:change_mode(File, CurrentMode bor 8#00200) of
ok ->
ok;
{error, Reason} ->
erlang:error(?RLX_ERROR({ensure_writable_failed, File, Reason}))
end.
copy_file_info(To, From, FileInfoToKeep) ->
case file:read_file_info(From) of
{ok, FileInfo} ->
case write_file_info(To, FileInfo, FileInfoToKeep) of
[] ->
ok;
Errors ->
{error, {write_file_info_failed_for, From, To, Errors}}
end;
{error, RFError} ->
{error, {read_file_info_failed, From, To, RFError}}
end.
write_file_info(To, FileInfo, FileInfoToKeep) ->
WriteInfoFuns = [{mode, fun try_write_mode/2},
{time, fun try_write_time/2},
{group, fun try_write_group/2},
{owner, fun try_write_owner/2}],
lists:foldl(fun(Info, Acc) ->
case proplists:get_value(Info, WriteInfoFuns, undefined) of
undefined ->
Acc;
F ->
case F(To, FileInfo) of
ok ->
Acc;
{error, Reason} ->
[{Info, Reason} | Acc]
end
end
end, [], FileInfoToKeep).
try_write_mode(To, #file_info{mode=Mode}) ->
file:write_file_info(To, #file_info{mode=Mode}).
try_write_time(To, #file_info{atime=Atime, mtime=Mtime}) ->
file:write_file_info(To, #file_info{atime=Atime, mtime=Mtime}).
try_write_owner(To, #file_info{uid=OwnerId}) ->
file:write_file_info(To, #file_info{uid=OwnerId}).
try_write_group(To, #file_info{gid=OwnerId}) ->
file:write_file_info(To, #file_info{gid=OwnerId}).
is_dir(Path) ->
case file:read_file_info(Path) of
{ok, #file_info{type = directory}} ->
true;
_ ->
false
end.
-spec make_dir_if_dir(file:name()) -> ok | {error, term()}.
make_dir_if_dir(File) ->
case is_dir(File) of
true ->
ok;
false ->
mkdir_path(File)
end.
-spec type(file:name()) -> file | symlink | directory | undefined.
type(Path) ->
case filelib:is_regular(Path) of
true ->
file;
false ->
case is_symlink(Path) of
true ->
symlink;
false ->
case is_dir(Path) of
true ->
directory;
false ->
undefined
end
end
end.
%% @doc Makes a directory including parent dirs if they are missing.
-spec mkdir_path(file:name()) -> ok | {error, Reason::term()}.
mkdir_path(Path) ->
mkdir_p(Path).
copy_subfiles(From, To, Options) ->
Fun =
fun(ChildFrom) ->
ChildTo = filename:join([To, filename:basename(ChildFrom)]),
copy(ChildFrom, ChildTo, Options)
end,
lists:foreach(Fun, sub_files(From)).
sub_files(From) ->
{ok, SubFiles} = file:list_dir(From),
[filename:join(From, SubFile) || SubFile <- SubFiles].
%% @doc delete a file. Use the recursive option for whole directory trees.
-spec remove(file:name(), [] | [recursive]) -> ok | {error, term()}.
remove(Path, Options) ->
case lists:member(recursive, Options) of
false -> remove(Path);
true -> remove_recursive(Path, Options)
end.
%% @doc delete a file or directory, including symlinks and junctions
-spec remove(file:name()) -> ok | {error, term()}.
remove(Path) ->
case is_symlink(Path) of
true -> remove_symlink(Path);
false ->
case is_dir(Path) of
true -> file:del_dir(Path);
false -> file:delete(Path)
end
end.
-spec remove_recursive(file:name(), list()) -> ok | {error, term()}.
remove_recursive(Path, Options) ->
case is_dir(Path) of
false ->
remove(Path);
true ->
lists:foreach(fun(ChildPath) ->
remove_recursive(ChildPath, Options)
end, sub_files(Path)),
file:del_dir(Path)
end.
-spec remove_symlink(file:name()) -> ok | {error, term()}.
remove_symlink(Path) ->
case os:type() of
{win32, _} ->
% On windows we remove symlinks and junctions differently if they refer to directories.
case filelib:is_dir(Path) of
true -> file:del_dir(Path);
false -> file:delete(Path)
end;
_ ->
% On other systems we use can use file:delete() in all cases.
file:delete(Path)
end.
%% returns the path for printing to logs. if get_cwd succeeds it
%% attempts to return the relative path. It falls back on juts
%% returning the original path.
print_path(OriginalPath) ->
case file:get_cwd() of
{ok, Cwd} ->
case rlx_file_utils:path_from_ancestor(OriginalPath, Cwd) of
{ok, ""} ->
OriginalPath;
{ok, Path} ->
Path;
_ ->
OriginalPath
end;
_ ->
OriginalPath
end.
%% for a given path return the path relative to a base directory
-spec path_from_ancestor(string(), string()) -> {ok, string()} | {error, badparent}.
path_from_ancestor(Target, To) ->
path_from_ancestor_(filename:split(canonical_path(Target)),
filename:split(canonical_path(To))).
path_from_ancestor_([Part|Target], [Part|To]) -> path_from_ancestor_(Target, To);
path_from_ancestor_([], []) -> {ok, ""};
path_from_ancestor_(Target, []) -> {ok, filename:join(Target)};
path_from_ancestor_(_, _) -> {error, badparent}.
%% reduce a filepath by removing all incidences of `.' and `..'
-spec canonical_path(string()) -> string().
canonical_path(Dir) ->
Canon = canonical_path([], filename:split(filename:absname(Dir))),
filename:nativename(Canon).
canonical_path([], []) -> filename:absname("/");
canonical_path(Acc, []) -> filename:join(lists:reverse(Acc));
canonical_path(Acc, ["."|Rest]) -> canonical_path(Acc, Rest);
canonical_path([_|Acc], [".."|Rest]) -> canonical_path(Acc, Rest);
canonical_path([], [".."|Rest]) -> canonical_path([], Rest);
canonical_path(Acc, [Component|Rest]) -> canonical_path([Component|Acc], Rest).
format_error({ensure_writable_failed, File, Reason}) ->
io_lib:format("Making file ~ts writable failed with reason:~n~ts",
[File, file:format_error(Reason)]);
format_error({write_file_info_failed_for, From, To, Errors}) ->
io_lib:format("Writing file info during copy of ~ts to ~ts failed with reasons:~n~ts",
[From, To, [[file:format_error(Error), "\n"] || Error <- Errors]]);
format_error({copy_failed, From, To, Reason}) ->
io_lib:format("Copying ~ts to ~ts failed with reason: ~ts", [From, To, file:format_error(Reason)]);
format_error({read_file_info_failed, From, To, Reason}) ->
io_lib:format("Reading file info failed during copy of ~ts ~ts with reason: ~ts",
[From, To, file:format_error(Reason)]).