%% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. %% @doc Create temporary files and directories. Requires crypto to be started. -module(mochitemp). -export([gettempdir/0]). -export([mkdtemp/0, mkdtemp/3]). -export([rmtempdir/1]). %% -export([mkstemp/4]). -define(SAFE_CHARS, {$a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l, $m, $n, $o, $p, $q, $r, $s, $t, $u, $v, $w, $x, $y, $z, $A, $B, $C, $D, $E, $F, $G, $H, $I, $J, $K, $L, $M, $N, $O, $P, $Q, $R, $S, $T, $U, $V, $W, $X, $Y, $Z, $0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $_}). -define(TMP_MAX, 10000). -include_lib("kernel/include/file.hrl"). %% TODO: An ugly wrapper over the mktemp tool with open_port and sadness? %% We can't implement this race-free in Erlang without the ability %% to issue O_CREAT|O_EXCL. I suppose we could hack something with %% mkdtemp, del_dir, open. %% mkstemp(Suffix, Prefix, Dir, Options) -> %% ok. rmtempdir(Dir) -> case file:del_dir(Dir) of {error, eexist} -> ok = rmtempdirfiles(Dir), ok = file:del_dir(Dir); ok -> ok end. rmtempdirfiles(Dir) -> {ok, Files} = file:list_dir(Dir), ok = rmtempdirfiles(Dir, Files). rmtempdirfiles(_Dir, []) -> ok; rmtempdirfiles(Dir, [Basename | Rest]) -> Path = filename:join([Dir, Basename]), case filelib:is_dir(Path) of true -> ok = rmtempdir(Path); false -> ok = file:delete(Path) end, rmtempdirfiles(Dir, Rest). mkdtemp() -> mkdtemp("", "tmp", gettempdir()). mkdtemp(Suffix, Prefix, Dir) -> mkdtemp_n(rngpath_fun(Suffix, Prefix, Dir), ?TMP_MAX). mkdtemp_n(RngPath, 1) -> make_dir(RngPath()); mkdtemp_n(RngPath, N) -> try make_dir(RngPath()) catch throw:{error, eexist} -> mkdtemp_n(RngPath, N - 1) end. make_dir(Path) -> case file:make_dir(Path) of ok -> ok; E={error, eexist} -> throw(E) end, %% Small window for a race condition here because dir is created 777 ok = file:write_file_info(Path, #file_info{mode=8#0700}), Path. rngpath_fun(Prefix, Suffix, Dir) -> fun () -> filename:join([Dir, Prefix ++ rngchars(6) ++ Suffix]) end. rngchars(0) -> ""; rngchars(N) -> [rngchar() | rngchars(N - 1)]. rngchar() -> rngchar(crypto:rand_uniform(0, tuple_size(?SAFE_CHARS))). rngchar(C) -> element(1 + C, ?SAFE_CHARS). %% @spec gettempdir() -> string() %% @doc Get a usable temporary directory using the first of these that is a directory: %% $TMPDIR, $TMP, $TEMP, "/tmp", "/var/tmp", "/usr/tmp", ".". gettempdir() -> gettempdir(gettempdir_checks(), fun normalize_dir/1). gettempdir_checks() -> [{fun os:getenv/1, ["TMPDIR", "TMP", "TEMP"]}, {fun gettempdir_identity/1, ["/tmp", "/var/tmp", "/usr/tmp"]}, {fun gettempdir_cwd/1, [cwd]}]. gettempdir_identity(L) -> L. gettempdir_cwd(cwd) -> {ok, L} = file:get_cwd(), L. gettempdir([{_F, []} | RestF], Normalize) -> gettempdir(RestF, Normalize); gettempdir([{F, [L | RestL]} | RestF], Normalize) -> case Normalize(F(L)) of false -> gettempdir([{F, RestL} | RestF], Normalize); Dir -> Dir end. normalize_dir(False) when False =:= false orelse False =:= "" -> %% Erlang doesn't have an unsetenv, wtf. false; normalize_dir(L) -> Dir = filename:absname(L), case filelib:is_dir(Dir) of false -> false; true -> Dir end. %% %% Tests %% -include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). pushenv(L) -> [{K, os:getenv(K)} || K <- L]. popenv(L) -> F = fun ({K, false}) -> %% Erlang doesn't have an unsetenv, wtf. os:putenv(K, ""); ({K, V}) -> os:putenv(K, V) end, lists:foreach(F, L). gettempdir_fallback_test() -> ?assertEqual( "/", gettempdir([{fun gettempdir_identity/1, ["/--not-here--/"]}, {fun gettempdir_identity/1, ["/"]}], fun normalize_dir/1)), ?assertEqual( "/", %% simulate a true os:getenv unset env gettempdir([{fun gettempdir_identity/1, [false]}, {fun gettempdir_identity/1, ["/"]}], fun normalize_dir/1)), ok. gettempdir_identity_test() -> ?assertEqual( "/", gettempdir([{fun gettempdir_identity/1, ["/"]}], fun normalize_dir/1)), ok. gettempdir_cwd_test() -> {ok, Cwd} = file:get_cwd(), ?assertEqual( normalize_dir(Cwd), gettempdir([{fun gettempdir_cwd/1, [cwd]}], fun normalize_dir/1)), ok. rngchars_test() -> crypto:start(), ?assertEqual( "", rngchars(0)), ?assertEqual( 10, length(rngchars(10))), ok. rngchar_test() -> ?assertEqual( $a, rngchar(0)), ?assertEqual( $A, rngchar(26)), ?assertEqual( $_, rngchar(62)), ok. mkdtemp_n_failonce_test() -> crypto:start(), D = mkdtemp(), Path = filename:join([D, "testdir"]), %% Toggle the existence of a dir so that it fails %% the first time and succeeds the second. F = fun () -> case filelib:is_dir(Path) of true -> file:del_dir(Path); false -> file:make_dir(Path) end, Path end, try %% Fails the first time ?assertThrow( {error, eexist}, mkdtemp_n(F, 1)), %% Reset state file:del_dir(Path), %% Succeeds the second time ?assertEqual( Path, mkdtemp_n(F, 2)) after rmtempdir(D) end, ok. mkdtemp_n_fail_test() -> {ok, Cwd} = file:get_cwd(), ?assertThrow( {error, eexist}, mkdtemp_n(fun () -> Cwd end, 1)), ?assertThrow( {error, eexist}, mkdtemp_n(fun () -> Cwd end, 2)), ok. make_dir_fail_test() -> {ok, Cwd} = file:get_cwd(), ?assertThrow( {error, eexist}, make_dir(Cwd)), ok. mkdtemp_test() -> crypto:start(), D = mkdtemp(), ?assertEqual( true, filelib:is_dir(D)), ?assertEqual( ok, file:del_dir(D)), ok. rmtempdir_test() -> crypto:start(), D1 = mkdtemp(), ?assertEqual( true, filelib:is_dir(D1)), ?assertEqual( ok, rmtempdir(D1)), D2 = mkdtemp(), ?assertEqual( true, filelib:is_dir(D2)), ok = file:write_file(filename:join([D2, "foo"]), <<"bytes">>), D3 = mkdtemp("suffix", "prefix", D2), ?assertEqual( true, filelib:is_dir(D3)), ok = file:write_file(filename:join([D3, "foo"]), <<"bytes">>), ?assertEqual( ok, rmtempdir(D2)), ?assertEqual( {error, enoent}, file:consult(D3)), ?assertEqual( {error, enoent}, file:consult(D2)), ok. gettempdir_env_test() -> Env = pushenv(["TMPDIR", "TEMP", "TMP"]), FalseEnv = [{"TMPDIR", false}, {"TEMP", false}, {"TMP", false}], try popenv(FalseEnv), popenv([{"TMPDIR", "/"}]), ?assertEqual( "/", os:getenv("TMPDIR")), ?assertEqual( "/", gettempdir()), {ok, Cwd} = file:get_cwd(), popenv(FalseEnv), popenv([{"TMP", Cwd}]), ?assertEqual( normalize_dir(Cwd), gettempdir()) after popenv(Env) end, ok. -endif.