Skip to content

Commit 5435cdf

Browse files
committed
Better handling of base temporary directories
Create a new temporary directory with defined permissions for each pid. Ensuring that the creation is new prevents race conditions where the directory already exists. Ideally we would pass the mode when creating the directory, but that is not possible with the Elixir nor Erlang standard libraries. When cleaning up for a pid, optimistically attempt to delete the temporary directory using rmdir. This won't delete the directory if any files are still there, which occurs if they have been given away. When cleaning up for a pid, look for any temporary directories for files we have been gifted. Remove them if pid in question is no longer using the temporary directory. Allow the application to configure the mode for the temporary directory.
1 parent 9878f71 commit 5435cdf

4 files changed

Lines changed: 217 additions & 38 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ following Mix config:
5959
# config/config.exs
6060
config :briefly,
6161
directory: [{:system, "TMPDIR"}, {:system, "TMP"}, {:system, "TEMP"}, "/tmp"],
62+
directory_mode: 0o755,
6263
default_prefix: "briefly",
6364
default_extname: ""
6465
```

lib/briefly/entry.ex

Lines changed: 90 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,34 @@ defmodule Briefly.Entry do
4141
@doc false
4242
def cleanup(pid) do
4343
case :ets.lookup(@dir_table, pid) do
44-
[{^pid, _tmp}] ->
44+
[{^pid, tmp}] ->
45+
path_entries = :ets.lookup(@path_table, pid)
46+
47+
secondaries =
48+
Enum.reduce(
49+
path_entries,
50+
MapSet.new(),
51+
fn path_entry = {_pid, path_value}, seen ->
52+
delete_path(path_entry)
53+
if path_value.original_owner != pid do
54+
MapSet.put(seen, %{path_value | path: nil})
55+
else
56+
seen
57+
end
58+
end
59+
)
60+
61+
File.rmdir(tmp)
62+
63+
Enum.each(secondaries, fn %{original_owner: owner_pid, root_dir: dir} ->
64+
if !pid_registered?(owner_pid) do
65+
File.rmdir(dir)
66+
end
67+
end)
68+
69+
:ets.delete(@path_table, pid)
4570
:ets.delete(@dir_table, pid)
46-
47-
entries = :ets.lookup(@path_table, pid)
48-
Enum.each(entries, &delete_path/1)
49-
50-
for {_pid, path} <- entries, do: path
71+
for {_pid, path} <- path_entries, do: path
5172

5273
[] ->
5374
[]
@@ -58,19 +79,19 @@ defmodule Briefly.Entry do
5879
def give_away(path, to_pid, from_pid)
5980
when is_binary(path) and is_pid(to_pid) and is_pid(from_pid) do
6081
with true <- pid_registered?(from_pid),
61-
true <- path_owner?(from_pid, path) do
82+
path_entry when not is_nil(path_entry) <- path_entry_if_owner(from_pid, path) do
6283
if pid_registered?(to_pid) do
63-
:ets.insert(@path_table, {to_pid, path})
64-
:ets.delete_object(@path_table, {from_pid, path})
84+
:ets.insert(@path_table, {to_pid, path_entry})
85+
:ets.delete_object(@path_table, {from_pid, path_entry})
6586
:ok
6687
else
6788
server = server()
6889

6990
{:ok, tmps} = GenServer.call(server, :roots)
7091
{:ok, tmp} = generate_tmp_dir(tmps)
71-
:ok = GenServer.call(server, {:give_away, to_pid, tmp, path})
92+
:ok = GenServer.call(server, {:give_away, to_pid, tmp, path_entry})
7293

73-
:ets.delete_object(@path_table, {from_pid, path})
94+
:ets.delete_object(@path_table, {from_pid, path_entry})
7495

7596
:ok
7697
end
@@ -101,12 +122,12 @@ defmodule Briefly.Entry do
101122
{:reply, {:ok, dirs}, dirs}
102123
end
103124

104-
def handle_call({:give_away, pid, tmp, path}, _from, dirs) do
125+
def handle_call({:give_away, pid, tmp, path_entry}, _from, dirs) do
105126
# Since we are writing on behalf of another process, we need to make sure
106127
# the monitor and writing to the tables happen within the same operation.
107128
Process.monitor(pid)
108129
:ets.insert_new(@dir_table, {pid, tmp})
109-
:ets.insert(@path_table, {pid, path})
130+
:ets.insert(@path_table, {pid, path_entry})
110131

111132
{:reply, :ok, dirs}
112133
end
@@ -123,6 +144,17 @@ defmodule Briefly.Entry do
123144
def terminate(_reason, _state) do
124145
folder = fn entry, :ok -> delete_path(entry) end
125146
:ets.foldl(folder, :ok, @path_table)
147+
148+
dir_folder = fn {_key, dir}, :ok ->
149+
case File.rmdir(dir) do
150+
:ok -> :ok
151+
{:error, :eexist} -> :ok
152+
{:error, :enoent} -> :ok
153+
{:error, :enotdir} -> :ok
154+
end
155+
end
156+
157+
:ets.foldl(dir_folder, :ok, @dir_table)
126158
end
127159

128160
## Helpers
@@ -146,20 +178,33 @@ defmodule Briefly.Entry do
146178
end
147179

148180
defp generate_tmp_dir(tmp_roots) do
149-
{mega, _, _} = :os.timestamp()
150-
subdir = "briefly-#{mega}"
181+
subdir = path(%{prefix: "briefly-", extname: ""})
151182

152-
if tmp = Enum.find_value(tmp_roots, &write_tmp_dir(Path.join(&1, subdir))) do
183+
if tmp = Enum.find_value(tmp_roots, &write_tmp_dir({&1, subdir})) do
153184
{:ok, tmp}
154185
else
155186
{:error, %Briefly.NoRootDirectoryError{tmp_dirs: tmp_roots}}
156187
end
157188
end
158189

159-
defp write_tmp_dir(path) do
160-
case File.mkdir_p(path) do
161-
:ok -> path
162-
{:error, _} -> nil
190+
defp write_tmp_dir({root, path}) do
191+
fullpath = Path.join(root, path)
192+
193+
case File.mkdir_p(root) do
194+
{:error, _} ->
195+
nil
196+
197+
:ok ->
198+
case File.mkdir(fullpath) do
199+
{:error, _} ->
200+
nil
201+
202+
:ok ->
203+
case File.chmod(fullpath, Briefly.Config.directory_mode()) do
204+
{:error, _} -> nil
205+
:ok -> fullpath
206+
end
207+
end
163208
end
164209
end
165210

@@ -168,7 +213,8 @@ defmodule Briefly.Entry do
168213

169214
case File.mkdir_p(path) do
170215
:ok ->
171-
:ets.insert(@path_table, {self(), path})
216+
value = %{path: path, root_dir: tmp, original_owner: self()}
217+
:ets.insert(@path_table, {self(), value})
172218
{:ok, path}
173219

174220
{:error, reason} when reason in [:eexist, :eacces] ->
@@ -185,7 +231,8 @@ defmodule Briefly.Entry do
185231

186232
case File.open(path, [:read, :write, :exclusive]) do
187233
{:ok, device_pid} ->
188-
:ets.insert(@path_table, {self(), path})
234+
value = %{path: path, root_dir: tmp, original_owner: self()}
235+
:ets.insert(@path_table, {self(), value})
189236
{:ok, path, device_pid}
190237

191238
{:error, reason} when reason in [:eexist, :eacces] ->
@@ -207,7 +254,8 @@ defmodule Briefly.Entry do
207254

208255
case :file.write_file(path, "", [:write, :raw, :exclusive, :binary]) do
209256
:ok ->
210-
:ets.insert(@path_table, {self(), path})
257+
value = %{path: path, root_dir: tmp, original_owner: self()}
258+
:ets.insert(@path_table, {self(), value})
211259
{:ok, path}
212260

213261
{:error, reason} when reason in [:eexist, :eacces] ->
@@ -223,20 +271,21 @@ defmodule Briefly.Entry do
223271
{:error, last_error}
224272
end
225273

226-
defp path(options, tmp) do
274+
defp path(options) do
227275
time = :erlang.monotonic_time() |> to_string |> String.trim("-")
228276

229-
folder =
230-
Enum.join(
231-
[
232-
prefix(options),
233-
time,
234-
random_padding()
235-
],
236-
"-"
237-
) <> extname(options)
238-
239-
Path.join([tmp, folder])
277+
Enum.join(
278+
[
279+
prefix(options),
280+
time,
281+
random_padding()
282+
],
283+
"-"
284+
) <> extname(options)
285+
end
286+
287+
defp path(options, tmp) do
288+
Path.join([tmp, path(options)])
240289
end
241290

242291
defp random_padding(length \\ 20) do
@@ -256,12 +305,15 @@ defmodule Briefly.Entry do
256305
:ets.member(@dir_table, pid)
257306
end
258307

259-
defp path_owner?(pid, path) do
308+
defp path_entry_if_owner(pid, path) do
260309
owned_paths = :ets.lookup(@path_table, pid)
261-
Enum.any?(owned_paths, fn {_pid, p} -> p == path end)
310+
311+
Enum.find_value(owned_paths, fn {_pid, %{path: p} = entry} ->
312+
if p == path, do: entry
313+
end)
262314
end
263315

264-
defp delete_path({_pid, path}) do
316+
defp delete_path({_pid, %{path: path}}) do
265317
File.rm_rf(path)
266318
:ok
267319
end

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ defmodule Briefly.Mixfile do
5656
defp default_env do
5757
[
5858
directory: [{:system, "TMPDIR"}, {:system, "TMP"}, {:system, "TEMP"}, "/tmp"],
59+
directory_mode: 0o755,
5960
default_prefix: "briefly",
6061
default_extname: ""
6162
]

test/lib/briefly_test.exs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,131 @@ defmodule Test.Briefly do
320320
refute File.exists?(path1)
321321
end
322322
end
323+
324+
test "cleans up gifted temporary directories when appropriate" do
325+
parent = self()
326+
327+
{receive1_pid, receive1_ref} =
328+
spawn_monitor(fn ->
329+
receive do
330+
:exit -> nil
331+
end
332+
end)
333+
334+
{receive2_pid, receive2_ref} =
335+
spawn_monitor(fn ->
336+
receive do
337+
:exit -> nil
338+
end
339+
end)
340+
341+
{give_pid, give_ref} =
342+
spawn_monitor(fn ->
343+
{:ok, path1} = Briefly.create()
344+
send(parent, {:path1, path1})
345+
File.open!(path1)
346+
347+
{:ok, path2} = Briefly.create()
348+
send(parent, {:path2, path2})
349+
File.open!(path2)
350+
351+
:ok = Briefly.give_away(path1, receive1_pid)
352+
:ok = Briefly.give_away(path2, receive1_pid)
353+
354+
send(parent, :given_away)
355+
356+
receive do
357+
:continue -> :ok
358+
end
359+
360+
{:ok, path3} = Briefly.create()
361+
send(parent, {:path3, path3})
362+
File.open!(path3)
363+
364+
:ok = Briefly.give_away(path3, receive2_pid)
365+
end)
366+
367+
path1 =
368+
receive do
369+
{:path1, path} -> path
370+
after
371+
1_000 -> flunk("didn't get a path")
372+
end
373+
374+
path2 =
375+
receive do
376+
{:path2, path} -> path
377+
after
378+
1_000 -> flunk("didn't get a path")
379+
end
380+
381+
receive do
382+
:given_away -> nil
383+
after
384+
1_000 -> flunk("didn't get signal for given_away")
385+
end
386+
387+
send(receive1_pid, :exit)
388+
389+
receive do
390+
{:DOWN, ^receive1_ref, :process, ^receive1_pid, :normal} ->
391+
# force sync by creating file in unknown process
392+
parent = self()
393+
394+
spawn(fn ->
395+
{:ok, _} = Briefly.create()
396+
send(parent, :continue)
397+
end)
398+
399+
receive do
400+
:continue -> :ok
401+
end
402+
403+
cleanup_wait_loop(receive1_pid)
404+
405+
refute File.exists?(path1)
406+
refute File.exists?(path2)
407+
assert File.exists?(Path.dirname(path1))
408+
end
409+
410+
send(give_pid, :continue)
411+
412+
path3 =
413+
receive do
414+
{:path3, path} -> path
415+
after
416+
1_000 -> flunk("didn't get a path")
417+
end
418+
419+
receive do
420+
{:DOWN, ^give_ref, :process, ^give_pid, :normal} ->
421+
{:ok, _} = Briefly.create()
422+
423+
assert File.exists?(path3)
424+
end
425+
426+
send(receive2_pid, :exit)
427+
428+
receive do
429+
{:DOWN, ^receive2_ref, :process, ^receive2_pid, :normal} ->
430+
# force sync by creating file in unknown process
431+
parent = self()
432+
433+
spawn(fn ->
434+
{:ok, _} = Briefly.create()
435+
send(parent, :continue)
436+
end)
437+
438+
receive do
439+
:continue -> :ok
440+
end
441+
442+
cleanup_wait_loop(receive2_pid)
443+
444+
refute File.exists?(path3)
445+
refute File.exists?(Path.dirname(path3))
446+
end
447+
end
323448
end
324449

325450
test "returns an io device if device is requested" do

0 commit comments

Comments
 (0)