defmodule Plug.Upload do
@moduledoc """
A server (a `GenServer` specifically) that manages uploaded files.
Uploaded files are stored in a temporary directory
and removed from that directory after the process that
requested the file dies.
During the request, files are represented with
a `Plug.Upload` struct that contains three fields:
* `:path` - the path to the uploaded file on the filesystem
* `:content_type` - the content type of the uploaded file
* `:filename` - the filename of the uploaded file given in the request
**Note**: as mentioned in the documentation for `Plug.Parsers`, the `:plug`
application has to be started in order to upload files and use the
`Plug.Upload` module.
"""
defstruct [:path, :content_type, :filename]
@type t :: %__MODULE__{
path: Path.t,
filename: binary,
content_type: binary | nil
}
@doc """
Requests a random file to be created in the upload directory
with the given prefix.
"""
@spec random_file(binary) ::
{:ok, binary} |
{:too_many_attempts, binary, pos_integer} |
{:no_tmp, [binary]}
def random_file(prefix) do
GenServer.call(plug_server, {:random, prefix})
end
@doc """
Requests a random file to be created in the upload directory
with the given prefix. Raises on failure.
"""
@spec random_file!(binary) :: binary | no_return
def random_file!(prefix) do
case random_file(prefix) do
{:ok, path} ->
path
{:too_many_attempts, tmp, attempts} ->
raise "tried #{attempts} times to create an uploaded file at #{tmp} but failed. What gives?"
{:no_tmp, _tmps} ->
raise "could not create a tmp directory to store uploads. Set PLUG_TMPDIR to a directory with write permission"
end
end
defp plug_server do
Process.whereis(__MODULE__) ||
raise "could not find process Plug.Upload. Have you started the :plug application?"
end
use GenServer
@doc """
Starts the upload handling server.
"""
def start_link() do
GenServer.start_link(__MODULE__, :ok, [name: __MODULE__])
end
## Callbacks
@temp_env_vars ~w(PLUG_TMPDIR TMPDIR TMP TEMP)s
@max_attempts 10
@doc false
def init(:ok) do
tmp = Enum.find_value @temp_env_vars, "/tmp", &System.get_env/1
cwd = Path.join(File.cwd!, "tmp")
ets = :ets.new(:plug_uploads, [:private])
{:ok, {[tmp, cwd], ets}}
end
@doc false
def handle_call({:random, prefix}, {pid, _ref}, {tmps, ets} = state) do
case find_tmp_dir(pid, tmps, ets) do
{:ok, tmp, paths} ->
{:reply, open_random_file(prefix, tmp, 0, pid, ets, paths), state}
{:no_tmp, _} = error ->
{:reply, error, state}
end
end
def handle_call(msg, from, state) do
super(msg, from, state)
end
@doc false
def handle_info({:DOWN, _ref, :process, pid, _reason}, {_, ets} = state) do
case :ets.lookup(ets, pid) do
[{pid, _tmp, paths}] ->
:ets.delete(ets, pid)
Enum.each paths, &:file.delete/1
[] ->
:ok
end
{:noreply, state}
end
def handle_info(msg, state) do
super(msg, state)
end
## Helpers
defp find_tmp_dir(pid, tmps, ets) do
case :ets.lookup(ets, pid) do
[{^pid, tmp, paths}] ->
{:ok, tmp, paths}
[] ->
if tmp = ensure_tmp_dir(tmps) do
:erlang.monitor(:process, pid)
:ets.insert(ets, {pid, tmp, []})
{:ok, tmp, []}
else
{:no_tmp, tmps}
end
end
end
defp ensure_tmp_dir(tmps) do
{mega, _, _} = :os.timestamp
subdir = "/plug-" <> i(mega)
Enum.find_value(tmps, &write_tmp_dir(&1 <> subdir))
end
defp write_tmp_dir(path) do
case File.mkdir_p(path) do
:ok -> path
{:error, _} -> nil
end
end
defp open_random_file(prefix, tmp, attempts, pid, ets, paths) when attempts < @max_attempts do
path = path(prefix, tmp)
case :file.write_file(path, "", [:write, :raw, :exclusive, :binary]) do
:ok ->
:ets.update_element(ets, pid, {3, [path|paths]})
{:ok, path}
{:error, reason} when reason in [:eexist, :eaccess] ->
open_random_file(prefix, tmp, attempts + 1, pid, ets, paths)
end
end
defp open_random_file(_prefix, tmp, attempts, _pid, _ets, _paths) do
{:too_many_attempts, tmp, attempts}
end
@compile {:inline, i: 1}
defp i(integer), do: Integer.to_string(integer)
defp path(prefix, tmp) do
{_mega, sec, micro} = :os.timestamp
scheduler_id = :erlang.system_info(:scheduler_id)
tmp <> "/" <> prefix <> "-" <> i(sec) <> "-" <> i(micro) <> "-" <> i(scheduler_id)
end
end