/src/mochitemp.erl

http://github.com/basho/mochiweb · Erlang · 329 lines · 249 code · 37 blank · 43 comment · 1 complexity · 7e587bafa9204c1a4d9eed3985f0ef21 MD5 · raw file

  1. %% @author Bob Ippolito <bob@mochimedia.com>
  2. %% @copyright 2010 Mochi Media, Inc.
  3. %%
  4. %% Permission is hereby granted, free of charge, to any person obtaining a
  5. %% copy of this software and associated documentation files (the "Software"),
  6. %% to deal in the Software without restriction, including without limitation
  7. %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8. %% and/or sell copies of the Software, and to permit persons to whom the
  9. %% Software is furnished to do so, subject to the following conditions:
  10. %%
  11. %% The above copyright notice and this permission notice shall be included in
  12. %% all copies or substantial portions of the Software.
  13. %%
  14. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  17. %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  20. %% DEALINGS IN THE SOFTWARE.
  21. %% @doc Create temporary files and directories. Requires crypto to be started.
  22. -module(mochitemp).
  23. -export([gettempdir/0]).
  24. -export([mkdtemp/0, mkdtemp/3]).
  25. -export([rmtempdir/1]).
  26. %% -export([mkstemp/4]).
  27. -define(SAFE_CHARS, {$a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l, $m,
  28. $n, $o, $p, $q, $r, $s, $t, $u, $v, $w, $x, $y, $z,
  29. $A, $B, $C, $D, $E, $F, $G, $H, $I, $J, $K, $L, $M,
  30. $N, $O, $P, $Q, $R, $S, $T, $U, $V, $W, $X, $Y, $Z,
  31. $0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $_}).
  32. -define(TMP_MAX, 10000).
  33. -include_lib("kernel/include/file.hrl").
  34. %% TODO: An ugly wrapper over the mktemp tool with open_port and sadness?
  35. %% We can't implement this race-free in Erlang without the ability
  36. %% to issue O_CREAT|O_EXCL. I suppose we could hack something with
  37. %% mkdtemp, del_dir, open.
  38. %% mkstemp(Suffix, Prefix, Dir, Options) ->
  39. %% ok.
  40. rmtempdir(Dir) ->
  41. case file:del_dir(Dir) of
  42. {error, eexist} ->
  43. ok = rmtempdirfiles(Dir),
  44. ok = file:del_dir(Dir);
  45. ok ->
  46. ok
  47. end.
  48. rmtempdirfiles(Dir) ->
  49. {ok, Files} = file:list_dir(Dir),
  50. ok = rmtempdirfiles(Dir, Files).
  51. rmtempdirfiles(_Dir, []) ->
  52. ok;
  53. rmtempdirfiles(Dir, [Basename | Rest]) ->
  54. Path = filename:join([Dir, Basename]),
  55. case filelib:is_dir(Path) of
  56. true ->
  57. ok = rmtempdir(Path);
  58. false ->
  59. ok = file:delete(Path)
  60. end,
  61. rmtempdirfiles(Dir, Rest).
  62. mkdtemp() ->
  63. mkdtemp("", "tmp", gettempdir()).
  64. mkdtemp(Suffix, Prefix, Dir) ->
  65. mkdtemp_n(rngpath_fun(Suffix, Prefix, Dir), ?TMP_MAX).
  66. mkdtemp_n(RngPath, 1) ->
  67. make_dir(RngPath());
  68. mkdtemp_n(RngPath, N) ->
  69. try make_dir(RngPath())
  70. catch throw:{error, eexist} ->
  71. mkdtemp_n(RngPath, N - 1)
  72. end.
  73. make_dir(Path) ->
  74. case file:make_dir(Path) of
  75. ok ->
  76. ok;
  77. E={error, eexist} ->
  78. throw(E)
  79. end,
  80. %% Small window for a race condition here because dir is created 777
  81. ok = file:write_file_info(Path, #file_info{mode=8#0700}),
  82. Path.
  83. rngpath_fun(Prefix, Suffix, Dir) ->
  84. fun () ->
  85. filename:join([Dir, Prefix ++ rngchars(6) ++ Suffix])
  86. end.
  87. rngchars(0) ->
  88. "";
  89. rngchars(N) ->
  90. [rngchar() | rngchars(N - 1)].
  91. rngchar() ->
  92. rngchar(crypto:rand_uniform(0, tuple_size(?SAFE_CHARS))).
  93. rngchar(C) ->
  94. element(1 + C, ?SAFE_CHARS).
  95. %% @spec gettempdir() -> string()
  96. %% @doc Get a usable temporary directory using the first of these that is a directory:
  97. %% $TMPDIR, $TMP, $TEMP, "/tmp", "/var/tmp", "/usr/tmp", ".".
  98. gettempdir() ->
  99. gettempdir(gettempdir_checks(), fun normalize_dir/1).
  100. gettempdir_checks() ->
  101. [{fun os:getenv/1, ["TMPDIR", "TMP", "TEMP"]},
  102. {fun gettempdir_identity/1, ["/tmp", "/var/tmp", "/usr/tmp"]},
  103. {fun gettempdir_cwd/1, [cwd]}].
  104. gettempdir_identity(L) ->
  105. L.
  106. gettempdir_cwd(cwd) ->
  107. {ok, L} = file:get_cwd(),
  108. L.
  109. gettempdir([{_F, []} | RestF], Normalize) ->
  110. gettempdir(RestF, Normalize);
  111. gettempdir([{F, [L | RestL]} | RestF], Normalize) ->
  112. case Normalize(F(L)) of
  113. false ->
  114. gettempdir([{F, RestL} | RestF], Normalize);
  115. Dir ->
  116. Dir
  117. end.
  118. normalize_dir(False) when False =:= false orelse False =:= "" ->
  119. %% Erlang doesn't have an unsetenv, wtf.
  120. false;
  121. normalize_dir(L) ->
  122. Dir = filename:absname(L),
  123. case filelib:is_dir(Dir) of
  124. false ->
  125. false;
  126. true ->
  127. Dir
  128. end.
  129. %%
  130. %% Tests
  131. %%
  132. -ifdef(TEST).
  133. -include_lib("eunit/include/eunit.hrl").
  134. pushenv(L) ->
  135. [{K, os:getenv(K)} || K <- L].
  136. popenv(L) ->
  137. F = fun ({K, false}) ->
  138. %% Erlang doesn't have an unsetenv, wtf.
  139. os:putenv(K, "");
  140. ({K, V}) ->
  141. os:putenv(K, V)
  142. end,
  143. lists:foreach(F, L).
  144. gettempdir_fallback_test() ->
  145. ?assertEqual(
  146. "/",
  147. gettempdir([{fun gettempdir_identity/1, ["/--not-here--/"]},
  148. {fun gettempdir_identity/1, ["/"]}],
  149. fun normalize_dir/1)),
  150. ?assertEqual(
  151. "/",
  152. %% simulate a true os:getenv unset env
  153. gettempdir([{fun gettempdir_identity/1, [false]},
  154. {fun gettempdir_identity/1, ["/"]}],
  155. fun normalize_dir/1)),
  156. ok.
  157. gettempdir_identity_test() ->
  158. ?assertEqual(
  159. "/",
  160. gettempdir([{fun gettempdir_identity/1, ["/"]}], fun normalize_dir/1)),
  161. ok.
  162. gettempdir_cwd_test() ->
  163. {ok, Cwd} = file:get_cwd(),
  164. ?assertEqual(
  165. normalize_dir(Cwd),
  166. gettempdir([{fun gettempdir_cwd/1, [cwd]}], fun normalize_dir/1)),
  167. ok.
  168. rngchars_test() ->
  169. crypto:start(),
  170. ?assertEqual(
  171. "",
  172. rngchars(0)),
  173. ?assertEqual(
  174. 10,
  175. length(rngchars(10))),
  176. ok.
  177. rngchar_test() ->
  178. ?assertEqual(
  179. $a,
  180. rngchar(0)),
  181. ?assertEqual(
  182. $A,
  183. rngchar(26)),
  184. ?assertEqual(
  185. $_,
  186. rngchar(62)),
  187. ok.
  188. mkdtemp_n_failonce_test() ->
  189. crypto:start(),
  190. D = mkdtemp(),
  191. Path = filename:join([D, "testdir"]),
  192. %% Toggle the existence of a dir so that it fails
  193. %% the first time and succeeds the second.
  194. F = fun () ->
  195. case filelib:is_dir(Path) of
  196. true ->
  197. file:del_dir(Path);
  198. false ->
  199. file:make_dir(Path)
  200. end,
  201. Path
  202. end,
  203. try
  204. %% Fails the first time
  205. ?assertThrow(
  206. {error, eexist},
  207. mkdtemp_n(F, 1)),
  208. %% Reset state
  209. file:del_dir(Path),
  210. %% Succeeds the second time
  211. ?assertEqual(
  212. Path,
  213. mkdtemp_n(F, 2))
  214. after rmtempdir(D)
  215. end,
  216. ok.
  217. mkdtemp_n_fail_test() ->
  218. {ok, Cwd} = file:get_cwd(),
  219. ?assertThrow(
  220. {error, eexist},
  221. mkdtemp_n(fun () -> Cwd end, 1)),
  222. ?assertThrow(
  223. {error, eexist},
  224. mkdtemp_n(fun () -> Cwd end, 2)),
  225. ok.
  226. make_dir_fail_test() ->
  227. {ok, Cwd} = file:get_cwd(),
  228. ?assertThrow(
  229. {error, eexist},
  230. make_dir(Cwd)),
  231. ok.
  232. mkdtemp_test() ->
  233. crypto:start(),
  234. D = mkdtemp(),
  235. ?assertEqual(
  236. true,
  237. filelib:is_dir(D)),
  238. ?assertEqual(
  239. ok,
  240. file:del_dir(D)),
  241. ok.
  242. rmtempdir_test() ->
  243. crypto:start(),
  244. D1 = mkdtemp(),
  245. ?assertEqual(
  246. true,
  247. filelib:is_dir(D1)),
  248. ?assertEqual(
  249. ok,
  250. rmtempdir(D1)),
  251. D2 = mkdtemp(),
  252. ?assertEqual(
  253. true,
  254. filelib:is_dir(D2)),
  255. ok = file:write_file(filename:join([D2, "foo"]), <<"bytes">>),
  256. D3 = mkdtemp("suffix", "prefix", D2),
  257. ?assertEqual(
  258. true,
  259. filelib:is_dir(D3)),
  260. ok = file:write_file(filename:join([D3, "foo"]), <<"bytes">>),
  261. ?assertEqual(
  262. ok,
  263. rmtempdir(D2)),
  264. ?assertEqual(
  265. {error, enoent},
  266. file:consult(D3)),
  267. ?assertEqual(
  268. {error, enoent},
  269. file:consult(D2)),
  270. ok.
  271. gettempdir_env_test() ->
  272. Env = pushenv(["TMPDIR", "TEMP", "TMP"]),
  273. FalseEnv = [{"TMPDIR", false}, {"TEMP", false}, {"TMP", false}],
  274. try
  275. popenv(FalseEnv),
  276. popenv([{"TMPDIR", "/"}]),
  277. ?assertEqual(
  278. "/",
  279. os:getenv("TMPDIR")),
  280. ?assertEqual(
  281. "/",
  282. gettempdir()),
  283. {ok, Cwd} = file:get_cwd(),
  284. popenv(FalseEnv),
  285. popenv([{"TMP", Cwd}]),
  286. ?assertEqual(
  287. normalize_dir(Cwd),
  288. gettempdir())
  289. after popenv(Env)
  290. end,
  291. ok.
  292. -endif.