%%% vi:ts=4 sw=4 et %%%------------------------------------------------------------------- %%% @copyright (C) 2011, Erlware LLC %%% @doc %%% Helper functions for working with files. %%% @end %%%------------------------------------------------------------------- -module(ec_file). -export([ exists/1, copy/2, copy/3, insecure_mkdtemp/0, mkdir_path/1, mkdir_p/1, find/2, is_symlink/1, is_dir/1, type/1, real_dir_path/1, remove/1, remove/2, md5sum/1, sha1sum/1, read/1, write/2, write_term/2 ]). -export_type([ option/0 ]). -include_lib("kernel/include/file.hrl"). -define(CHECK_PERMS_MSG, "Try checking that you have the correct permissions and try again~n"). %%============================================================================ %% Types %%============================================================================ -type option() :: recursive. %%%=================================================================== %%% API %%%=================================================================== -spec exists(file:filename()) -> boolean(). exists(Filename) -> case file:read_file_info(Filename) of {ok, _} -> true; {error, _Reason} -> false end. %% @doc copy an entire directory to another location. -spec copy(file:name(), file:name(), Options::[option()]) -> ok | {error, Reason::term()}. copy(From, To, []) -> copy(From, To); copy(From, To, [recursive] = Options) -> case is_dir(From) of false -> copy(From, To); true -> make_dir_if_dir(To), copy_subfiles(From, To, Options) end. %% @doc copy a file including timestamps,ownership and mode etc. -spec copy(From::file:filename(), To::file:filename()) -> ok | {error, Reason::term()}. copy(From, To) -> case file:copy(From, To) of {ok, _} -> case file:read_file_info(From) of {ok, FileInfo} -> case file:write_file_info(To, FileInfo) of ok -> ok; {error, WFError} -> {error, {write_file_info_failed, WFError}} end; {error, RFError} -> {error, {read_file_info_failed, RFError}} end; {error, Error} -> {error, {copy_failed, Error}} end. %% @doc return an md5 checksum string or a binary. Same as unix utility of %% same name. -spec md5sum(string() | binary()) -> string(). md5sum(Value) -> hex(binary_to_list(erlang:md5(Value))). %% @doc return an sha1sum checksum string or a binary. Same as unix utility of %% same name. -ifdef(deprecated_crypto). -spec sha1sum(string() | binary()) -> string(). sha1sum(Value) -> hex(binary_to_list(crypto:sha(Value))). -else. -spec sha1sum(string() | binary()) -> string(). sha1sum(Value) -> hex(binary_to_list(crypto:hash(sha, Value))). -endif. %% @doc delete a file. Use the recursive option for directories. %%
%% Example: remove("./tmp_dir", [recursive]).
%% 
-spec remove(file:name(), Options::[option()]) -> ok | {error, Reason::term()}. remove(Path, Options) -> case lists:member(recursive, Options) of false -> file:delete(Path); true -> remove_recursive(Path, Options) end. %% @doc delete a file. -spec remove(file:name()) -> ok | {error, Reason::term()}. remove(Path) -> remove(Path, []). %% @doc indicates witha boolean if the path supplied refers to symlink. -spec is_symlink(file:name()) -> boolean(). is_symlink(Path) -> case file:read_link_info(Path) of {ok, #file_info{type = symlink}} -> true; _ -> false end. is_dir(Path) -> case file:read_file_info(Path) of {ok, #file_info{type = directory}} -> true; _ -> false end. %% @doc returns the type of the file. -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 gets the real path of a directory. This is mostly useful for %% resolving symlinks. Be aware that this temporarily changes the %% current working directory to figure out what the actual path %% is. That means that it can be quite slow. -spec real_dir_path(file:name()) -> file:name(). real_dir_path(Path) -> {ok, CurCwd} = file:get_cwd(), ok = file:set_cwd(Path), {ok, RealPath} = file:get_cwd(), ok = file:set_cwd(CurCwd), filename:absname(RealPath). %% @doc make a unique temporary directory. Similar function to BSD stdlib %% function of the same name. -spec insecure_mkdtemp() -> TmpDirPath::file:name(). insecure_mkdtemp() -> random:seed(os:timestamp()), UniqueNumber = erlang:integer_to_list(erlang:trunc(random:uniform() * 1000000000000)), TmpDirPath = filename:join([tmp(), lists:flatten([".tmp_dir", UniqueNumber])]), case mkdir_path(TmpDirPath) of ok -> TmpDirPath; Error -> Error end. %% @doc Makes a directory including parent dirs if they are missing. -spec mkdir_p(file:name()) -> ok | {error, Reason::term()}. 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 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). %% @doc read a file from the file system. Provide UEX exeption on failure. -spec read(FilePath::file:filename()) -> {ok, binary()} | {error, Reason::term()}. read(FilePath) -> %% Now that we are moving away from exceptions again this becomes %% a bit redundant but we want to be backwards compatible as much %% as possible in the api. file:read_file(FilePath). %% @doc write a file to the file system. Provide UEX exeption on failure. -spec write(FileName::file:filename(), Contents::string()) -> ok | {error, Reason::term()}. write(FileName, Contents) -> %% Now that we are moving away from exceptions again this becomes %% a bit redundant but we want to be backwards compatible as much %% as possible in the api. file:write_file(FileName, Contents). %% @doc write a term out to a file so that it can be consulted later. -spec write_term(file:filename(), term()) -> ok | {error, Reason::term()}. write_term(FileName, Term) -> write(FileName, lists:flatten(io_lib:fwrite("~p. ", [Term]))). %% @doc Finds files and directories that match the regexp supplied in %% the TargetPattern regexp. -spec find(FromDir::file:name(), TargetPattern::string()) -> [file:name()]. find([], _) -> []; find(FromDir, TargetPattern) -> case is_dir(FromDir) of false -> case re:run(FromDir, TargetPattern) of {match, _} -> [FromDir]; _ -> [] end; true -> FoundDir = case re:run(FromDir, TargetPattern) of {match, _} -> [FromDir]; _ -> [] end, List = find_in_subdirs(FromDir, TargetPattern), FoundDir ++ List end. %%%=================================================================== %%% Internal Functions %%%=================================================================== -spec find_in_subdirs(file:name(), string()) -> [file:name()]. find_in_subdirs(FromDir, TargetPattern) -> lists:foldl(fun(CheckFromDir, Acc) when CheckFromDir == FromDir -> Acc; (ChildFromDir, Acc) -> case find(ChildFromDir, TargetPattern) of [] -> Acc; Res -> Res ++ Acc end end, [], sub_files(FromDir)). -spec remove_recursive(file:name(), Options::list()) -> ok | {error, Reason::term()}. remove_recursive(Path, Options) -> case is_dir(Path) of false -> file:delete(Path); true -> lists:foreach(fun(ChildPath) -> remove_recursive(ChildPath, Options) end, sub_files(Path)), file:del_dir(Path) end. -spec tmp() -> file:name(). tmp() -> case erlang:system_info(system_architecture) of "win32" -> "./tmp"; _SysArch -> "/tmp" end. %% Copy the subfiles of the From directory to the to directory. -spec copy_subfiles(file:name(), file:name(), [option()]) -> {error, Reason::term()} | ok. 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)). -spec make_dir_if_dir(file:name()) -> ok | {error, Reason::term()}. make_dir_if_dir(File) -> case is_dir(File) of true -> ok; false -> mkdir_path(File) end. %% @doc convert a list of integers into hex. -spec hex(string() | non_neg_integer()) -> string(). hex(L) when is_list (L) -> lists:flatten([hex(I) || I <- L]); hex(I) when I > 16#f -> [hex0((I band 16#f0) bsr 4), hex0((I band 16#0f))]; hex(I) -> [$0, hex0(I)]. hex0(10) -> $a; hex0(11) -> $b; hex0(12) -> $c; hex0(13) -> $d; hex0(14) -> $e; hex0(15) -> $f; hex0(I) -> $0 + I. sub_files(From) -> {ok, SubFiles} = file:list_dir(From), [filename:join(From, SubFile) || SubFile <- SubFiles]. %%%=================================================================== %%% Test Functions %%%=================================================================== -ifdef(DEV_ONLY). -include_lib("eunit/include/eunit.hrl"). setup_test() -> Dir = insecure_mkdtemp(), mkdir_path(Dir), ?assertMatch(false, is_symlink(Dir)), ?assertMatch(true, filelib:is_dir(Dir)). md5sum_test() -> ?assertMatch("cfcd208495d565ef66e7dff9f98764da", md5sum("0")). sha1sum_test() -> ?assertMatch("b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", sha1sum("0")). file_test() -> Dir = insecure_mkdtemp(), TermFile = filename:join(Dir, "ec_file/dir/file.term"), TermFileCopy = filename:join(Dir, "ec_file/dircopy/file.term"), filelib:ensure_dir(TermFile), filelib:ensure_dir(TermFileCopy), write_term(TermFile, "term"), ?assertMatch({ok, <<"\"term\". ">>}, read(TermFile)), copy(filename:dirname(TermFile), filename:dirname(TermFileCopy), [recursive]). teardown_test() -> Dir = insecure_mkdtemp(), remove(Dir, [recursive]), ?assertMatch(false, filelib:is_dir(Dir)). setup_base_and_target() -> BaseDir = insecure_mkdtemp(), DummyContents = <<"This should be deleted">>, SourceDir = filename:join([BaseDir, "source"]), ok = file:make_dir(SourceDir), Name1 = filename:join([SourceDir, "fileone"]), Name2 = filename:join([SourceDir, "filetwo"]), Name3 = filename:join([SourceDir, "filethree"]), NoName = filename:join([SourceDir, "noname"]), ok = file:write_file(Name1, DummyContents), ok = file:write_file(Name2, DummyContents), ok = file:write_file(Name3, DummyContents), ok = file:write_file(NoName, DummyContents), {BaseDir, SourceDir, {Name1, Name2, Name3, NoName}}. exists_test() -> BaseDir = insecure_mkdtemp(), SourceDir = filename:join([BaseDir, "source1"]), NoName = filename:join([SourceDir, "noname"]), ok = file:make_dir(SourceDir), Name1 = filename:join([SourceDir, "fileone"]), ok = file:write_file(Name1, <<"Testn">>), ?assertMatch(true, exists(Name1)), ?assertMatch(false, exists(NoName)). real_path_test() -> BaseDir = "foo", Dir = filename:absname(filename:join(BaseDir, "source1")), LinkDir = filename:join([BaseDir, "link"]), ok = mkdir_p(Dir), file:make_symlink(Dir, LinkDir), ?assertEqual(Dir, real_dir_path(LinkDir)), ?assertEqual(directory, type(Dir)), ?assertEqual(symlink, type(LinkDir)), TermFile = filename:join(BaseDir, "test_file"), ok = write_term(TermFile, foo), ?assertEqual(file, type(TermFile)), ?assertEqual(true, is_symlink(LinkDir)), ?assertEqual(false, is_symlink(Dir)). find_test() -> %% Create a directory in /tmp for the test. Clean everything afterwards {BaseDir, _SourceDir, {Name1, Name2, Name3, _NoName}} = setup_base_and_target(), Result = find(BaseDir, "file[a-z]+\$"), ?assertMatch(3, erlang:length(Result)), ?assertEqual(true, lists:member(Name1, Result)), ?assertEqual(true, lists:member(Name2, Result)), ?assertEqual(true, lists:member(Name3, Result)), remove(BaseDir, [recursive]). -endif.