Subversion Repositories SE.SVN

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
12 7u83 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
 
22
%% @doc Create temporary files and directories. Requires crypto to be started.
23
 
24
-module(mochitemp).
25
-export([gettempdir/0]).
26
-export([mkdtemp/0, mkdtemp/3]).
27
-export([rmtempdir/1]).
28
%% -export([mkstemp/4]).
29
-define(SAFE_CHARS, {$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
                     $A, $B, $C, $D, $E, $F, $G, $H, $I, $J, $K, $L, $M,
32
                     $N, $O, $P, $Q, $R, $S, $T, $U, $V, $W, $X, $Y, $Z,
33
                     $0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $_}).
34
-define(TMP_MAX, 10000).
35
 
36
-include_lib("kernel/include/file.hrl").
37
 
38
%% TODO: An ugly wrapper over the mktemp tool with open_port and sadness?
39
%%       We can't implement this race-free in Erlang without the ability
40
%%       to issue O_CREAT|O_EXCL. I suppose we could hack something with
41
%%       mkdtemp, del_dir, open.
42
%% mkstemp(Suffix, Prefix, Dir, Options) ->
43
%%    ok.
44
 
45
rmtempdir(Dir) ->
46
    case file:del_dir(Dir) of
47
        {error, eexist} ->
48
            ok = rmtempdirfiles(Dir),
49
            ok = file:del_dir(Dir);
50
        ok ->
51
            ok
52
    end.
53
 
54
rmtempdirfiles(Dir) ->
55
    {ok, Files} = file:list_dir(Dir),
56
    ok = rmtempdirfiles(Dir, Files).
57
 
58
rmtempdirfiles(_Dir, []) ->
59
    ok;
60
rmtempdirfiles(Dir, [Basename | Rest]) ->
61
    Path = filename:join([Dir, Basename]),
62
    case filelib:is_dir(Path) of
63
        true ->
64
            ok = rmtempdir(Path);
65
        false ->
66
            ok = file:delete(Path)
67
    end,
68
    rmtempdirfiles(Dir, Rest).
69
 
70
mkdtemp() ->
71
    mkdtemp("", "tmp", gettempdir()).
72
 
73
mkdtemp(Suffix, Prefix, Dir) ->
74
    mkdtemp_n(rngpath_fun(Suffix, Prefix, Dir), ?TMP_MAX).
75
 
76
 
77
 
78
mkdtemp_n(RngPath, 1) ->
79
    make_dir(RngPath());
80
mkdtemp_n(RngPath, N) ->
81
    try make_dir(RngPath())
82
    catch throw:{error, eexist} ->
83
            mkdtemp_n(RngPath, N - 1)
84
    end.
85
 
86
make_dir(Path) ->
87
    case file:make_dir(Path) of
88
        ok ->
89
            ok;
90
        E={error, eexist} ->
91
            throw(E)
92
    end,
93
    %% Small window for a race condition here because dir is created 777
94
    ok = file:write_file_info(Path, #file_info{mode=8#0700}),
95
    Path.
96
 
97
rngpath_fun(Prefix, Suffix, Dir) ->
98
    fun () ->
99
            filename:join([Dir, Prefix ++ rngchars(6) ++ Suffix])
100
    end.
101
 
102
rngchars(0) ->
103
    "";
104
rngchars(N) ->
105
    [rngchar() | rngchars(N - 1)].
106
 
107
rngchar() ->
108
    rngchar(mochiweb_util:rand_uniform(0, tuple_size(?SAFE_CHARS))).
109
 
110
rngchar(C) ->
111
    element(1 + C, ?SAFE_CHARS).
112
 
113
%% @spec gettempdir() -> string()
114
%% @doc Get a usable temporary directory using the first of these that is a directory:
115
%%      $TMPDIR, $TMP, $TEMP, "/tmp", "/var/tmp", "/usr/tmp", ".".
116
gettempdir() ->
117
    gettempdir(gettempdir_checks(), fun normalize_dir/1).
118
 
119
gettempdir_checks() ->
120
    [{fun os:getenv/1, ["TMPDIR", "TMP", "TEMP"]},
121
     {fun gettempdir_identity/1, ["/tmp", "/var/tmp", "/usr/tmp"]},
122
     {fun gettempdir_cwd/1, [cwd]}].
123
 
124
gettempdir_identity(L) ->
125
    L.
126
 
127
gettempdir_cwd(cwd) ->
128
    {ok, L} = file:get_cwd(),
129
    L.
130
 
131
gettempdir([{_F, []} | RestF], Normalize) ->
132
    gettempdir(RestF, Normalize);
133
gettempdir([{F, [L | RestL]} | RestF], Normalize) ->
134
    case Normalize(F(L)) of
135
        false ->
136
            gettempdir([{F, RestL} | RestF], Normalize);
137
        Dir ->
138
            Dir
139
    end.
140
 
141
normalize_dir(False) when False =:= false orelse False =:= "" ->
142
    %% Erlang doesn't have an unsetenv, wtf.
143
    false;
144
normalize_dir(L) ->
145
    Dir = filename:absname(L),
146
    case filelib:is_dir(Dir) of
147
        false ->
148
            false;
149
        true ->
150
            Dir
151
    end.
152
 
153
%%
154
%% Tests
155
%%
156
-ifdef(TEST).
157
-include_lib("eunit/include/eunit.hrl").
158
 
159
pushenv(L) ->
160
    [{K, os:getenv(K)} || K <- L].
161
popenv(L) ->
162
    F = fun ({K, false}) ->
163
                %% Erlang doesn't have an unsetenv, wtf.
164
                os:putenv(K, "");
165
            ({K, V}) ->
166
                os:putenv(K, V)
167
        end,
168
    lists:foreach(F, L).
169
 
170
gettempdir_fallback_test() ->
171
    ?assertEqual(
172
       "/",
173
       gettempdir([{fun gettempdir_identity/1, ["/--not-here--/"]},
174
                   {fun gettempdir_identity/1, ["/"]}],
175
                  fun normalize_dir/1)),
176
    ?assertEqual(
177
       "/",
178
       %% simulate a true os:getenv unset env
179
       gettempdir([{fun gettempdir_identity/1, [false]},
180
                   {fun gettempdir_identity/1, ["/"]}],
181
                  fun normalize_dir/1)),
182
    ok.
183
 
184
gettempdir_identity_test() ->
185
    ?assertEqual(
186
       "/",
187
       gettempdir([{fun gettempdir_identity/1, ["/"]}], fun normalize_dir/1)),
188
    ok.
189
 
190
gettempdir_cwd_test() ->
191
    {ok, Cwd} = file:get_cwd(),
192
    ?assertEqual(
193
       normalize_dir(Cwd),
194
       gettempdir([{fun gettempdir_cwd/1, [cwd]}], fun normalize_dir/1)),
195
    ok.
196
 
197
rngchars_test() ->
198
    crypto:start(),
199
    ?assertEqual(
200
       "",
201
       rngchars(0)),
202
    ?assertEqual(
203
       10,
204
       length(rngchars(10))),
205
    ok.
206
 
207
rngchar_test() ->
208
    ?assertEqual(
209
       $a,
210
       rngchar(0)),
211
    ?assertEqual(
212
       $A,
213
       rngchar(26)),
214
    ?assertEqual(
215
       $_,
216
       rngchar(62)),
217
    ok.
218
 
219
mkdtemp_n_failonce_test() ->
220
    crypto:start(),
221
    D = mkdtemp(),
222
    Path = filename:join([D, "testdir"]),
223
    %% Toggle the existence of a dir so that it fails
224
    %% the first time and succeeds the second.
225
    F = fun () ->
226
                case filelib:is_dir(Path) of
227
                    true ->
228
                        file:del_dir(Path);
229
                    false ->
230
                        file:make_dir(Path)
231
                end,
232
                Path
233
        end,
234
    try
235
        %% Fails the first time
236
        ?assertThrow(
237
           {error, eexist},
238
           mkdtemp_n(F, 1)),
239
        %% Reset state
240
        file:del_dir(Path),
241
        %% Succeeds the second time
242
        ?assertEqual(
243
           Path,
244
           mkdtemp_n(F, 2))
245
    after rmtempdir(D)
246
    end,
247
    ok.
248
 
249
mkdtemp_n_fail_test() ->
250
    {ok, Cwd} = file:get_cwd(),
251
    ?assertThrow(
252
       {error, eexist},
253
       mkdtemp_n(fun () -> Cwd end, 1)),
254
    ?assertThrow(
255
       {error, eexist},
256
       mkdtemp_n(fun () -> Cwd end, 2)),
257
    ok.
258
 
259
make_dir_fail_test() ->
260
    {ok, Cwd} = file:get_cwd(),
261
    ?assertThrow(
262
      {error, eexist},
263
      make_dir(Cwd)),
264
    ok.
265
 
266
mkdtemp_test() ->
267
    crypto:start(),
268
    D = mkdtemp(),
269
    ?assertEqual(
270
       true,
271
       filelib:is_dir(D)),
272
    ?assertEqual(
273
       ok,
274
       file:del_dir(D)),
275
    ok.
276
 
277
rmtempdir_test() ->
278
    crypto:start(),
279
    D1 = mkdtemp(),
280
    ?assertEqual(
281
       true,
282
       filelib:is_dir(D1)),
283
    ?assertEqual(
284
       ok,
285
       rmtempdir(D1)),
286
    D2 = mkdtemp(),
287
    ?assertEqual(
288
       true,
289
       filelib:is_dir(D2)),
290
    ok = file:write_file(filename:join([D2, "foo"]), <<"bytes">>),
291
    D3 = mkdtemp("suffix", "prefix", D2),
292
    ?assertEqual(
293
       true,
294
       filelib:is_dir(D3)),
295
    ok = file:write_file(filename:join([D3, "foo"]), <<"bytes">>),
296
    ?assertEqual(
297
       ok,
298
       rmtempdir(D2)),
299
    ?assertEqual(
300
       {error, enoent},
301
       file:consult(D3)),
302
    ?assertEqual(
303
       {error, enoent},
304
       file:consult(D2)),
305
    ok.
306
 
307
gettempdir_env_test() ->
308
    Env = pushenv(["TMPDIR", "TEMP", "TMP"]),
309
    FalseEnv = [{"TMPDIR", false}, {"TEMP", false}, {"TMP", false}],
310
    try
311
        popenv(FalseEnv),
312
        popenv([{"TMPDIR", "/"}]),
313
        ?assertEqual(
314
           "/",
315
           os:getenv("TMPDIR")),
316
        ?assertEqual(
317
           "/",
318
           gettempdir()),
319
        {ok, Cwd} = file:get_cwd(),
320
        popenv(FalseEnv),
321
        popenv([{"TMP", Cwd}]),
322
        ?assertEqual(
323
           normalize_dir(Cwd),
324
           gettempdir())
325
    after popenv(Env)
326
    end,
327
    ok.
328
 
329
-endif.