zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

upload.ex (7264B)


      1 defmodule Plug.UploadError do
      2   defexception [:message]
      3 end
      4 
      5 defmodule Plug.Upload do
      6   @moduledoc """
      7   A server (a `GenServer` specifically) that manages uploaded files.
      8 
      9   Uploaded files are stored in a temporary directory
     10   and removed from that directory after the process that
     11   requested the file dies.
     12 
     13   During the request, files are represented with
     14   a `Plug.Upload` struct that contains three fields:
     15 
     16     * `:path` - the path to the uploaded file on the filesystem
     17     * `:content_type` - the content type of the uploaded file
     18     * `:filename` - the filename of the uploaded file given in the request
     19 
     20   **Note**: as mentioned in the documentation for `Plug.Parsers`, the `:plug`
     21   application has to be started in order to upload files and use the
     22   `Plug.Upload` module.
     23 
     24   ## Security
     25 
     26   The `:content_type` and `:filename` fields in the `Plug.Upload` struct are
     27   client-controlled. These values should be validated, via file content
     28   inspection or similar, before being trusted.
     29   """
     30 
     31   use GenServer
     32   defstruct [:path, :content_type, :filename]
     33 
     34   @type t :: %__MODULE__{
     35           path: Path.t(),
     36           filename: binary,
     37           content_type: binary | nil
     38         }
     39 
     40   @dir_table __MODULE__.Dir
     41   @path_table __MODULE__.Path
     42   @max_attempts 10
     43   @temp_env_vars ~w(PLUG_TMPDIR TMPDIR TMP TEMP)s
     44 
     45   @doc """
     46   Requests a random file to be created in the upload directory
     47   with the given prefix.
     48   """
     49   @spec random_file(binary) ::
     50           {:ok, binary}
     51           | {:too_many_attempts, binary, pos_integer}
     52           | {:no_tmp, [binary]}
     53   def random_file(prefix) do
     54     case ensure_tmp() do
     55       {:ok, tmp} ->
     56         open_random_file(prefix, tmp, 0)
     57 
     58       {:no_tmp, tmps} ->
     59         {:no_tmp, tmps}
     60     end
     61   end
     62 
     63   @doc """
     64   Assign ownership of the given upload file to another process.
     65 
     66   Useful if you want to do some work on an uploaded file in another process
     67   since it means that the file will survive the end of the request.
     68   """
     69   @spec give_away(t | binary, pid, pid) :: :ok | {:error, :unknown_path}
     70   def give_away(upload, to_pid, from_pid \\ self())
     71 
     72   def give_away(%__MODULE__{path: path}, to_pid, from_pid) do
     73     give_away(path, to_pid, from_pid)
     74   end
     75 
     76   def give_away(path, to_pid, from_pid)
     77       when is_binary(path) and is_pid(to_pid) and is_pid(from_pid) do
     78     with [{^from_pid, _tmp}] <- :ets.lookup(@dir_table, from_pid),
     79          true <- path_owner?(from_pid, path) do
     80       case :ets.lookup(@dir_table, to_pid) do
     81         [{^to_pid, _tmp}] ->
     82           :ets.insert(@path_table, {to_pid, path})
     83           :ets.delete_object(@path_table, {from_pid, path})
     84 
     85           :ok
     86 
     87         [] ->
     88           server = plug_server()
     89           {:ok, tmp} = generate_tmp_dir()
     90           :ok = GenServer.call(server, {:give_away, to_pid, tmp, path})
     91           :ets.delete_object(@path_table, {from_pid, path})
     92           :ok
     93       end
     94     else
     95       _ ->
     96         {:error, :unknown_path}
     97     end
     98   end
     99 
    100   defp ensure_tmp() do
    101     pid = self()
    102 
    103     case :ets.lookup(@dir_table, pid) do
    104       [{^pid, tmp}] ->
    105         {:ok, tmp}
    106 
    107       [] ->
    108         server = plug_server()
    109         GenServer.cast(server, {:monitor, pid})
    110 
    111         with {:ok, tmp} <- generate_tmp_dir() do
    112           true = :ets.insert_new(@dir_table, {pid, tmp})
    113           {:ok, tmp}
    114         end
    115     end
    116   end
    117 
    118   defp generate_tmp_dir() do
    119     tmp_roots = :persistent_term.get(__MODULE__)
    120     {mega, _, _} = :os.timestamp()
    121     subdir = "/plug-" <> i(mega)
    122 
    123     if tmp = Enum.find_value(tmp_roots, &make_tmp_dir(&1 <> subdir)) do
    124       {:ok, tmp}
    125     else
    126       {:no_tmp, tmp_roots}
    127     end
    128   end
    129 
    130   defp make_tmp_dir(path) do
    131     case File.mkdir_p(path) do
    132       :ok -> path
    133       {:error, _} -> nil
    134     end
    135   end
    136 
    137   defp open_random_file(prefix, tmp, attempts) when attempts < @max_attempts do
    138     path = path(prefix, tmp)
    139 
    140     case :file.write_file(path, "", [:write, :raw, :exclusive, :binary]) do
    141       :ok ->
    142         :ets.insert(@path_table, {self(), path})
    143         {:ok, path}
    144 
    145       {:error, reason} when reason in [:eexist, :eacces] ->
    146         open_random_file(prefix, tmp, attempts + 1)
    147     end
    148   end
    149 
    150   defp open_random_file(_prefix, tmp, attempts) do
    151     {:too_many_attempts, tmp, attempts}
    152   end
    153 
    154   defp path(prefix, tmp) do
    155     sec = :os.system_time(:second)
    156     rand = :rand.uniform(999_999_999_999)
    157     scheduler_id = :erlang.system_info(:scheduler_id)
    158     tmp <> "/" <> prefix <> "-" <> i(sec) <> "-" <> i(rand) <> "-" <> i(scheduler_id)
    159   end
    160 
    161   defp path_owner?(pid, path) do
    162     owned_paths = :ets.lookup(@path_table, pid)
    163     Enum.any?(owned_paths, fn {_pid, p} -> p == path end)
    164   end
    165 
    166   @compile {:inline, i: 1}
    167   defp i(integer), do: Integer.to_string(integer)
    168 
    169   @doc """
    170   Requests a random file to be created in the upload directory
    171   with the given prefix. Raises on failure.
    172   """
    173   @spec random_file!(binary) :: binary | no_return
    174   def random_file!(prefix) do
    175     case random_file(prefix) do
    176       {:ok, path} ->
    177         path
    178 
    179       {:too_many_attempts, tmp, attempts} ->
    180         raise Plug.UploadError,
    181               "tried #{attempts} times to create an uploaded file at #{tmp} but failed. " <>
    182                 "Set PLUG_TMPDIR to a directory with write permission"
    183 
    184       {:no_tmp, _tmps} ->
    185         raise Plug.UploadError,
    186               "could not create a tmp directory to store uploads. " <>
    187                 "Set PLUG_TMPDIR to a directory with write permission"
    188     end
    189   end
    190 
    191   defp plug_server do
    192     Process.whereis(__MODULE__) ||
    193       raise Plug.UploadError,
    194             "could not find process Plug.Upload. Have you started the :plug application?"
    195   end
    196 
    197   @doc false
    198   def start_link(_) do
    199     GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
    200   end
    201 
    202   ## Callbacks
    203 
    204   @impl true
    205   def init(:ok) do
    206     Process.flag(:trap_exit, true)
    207     tmp = Enum.find_value(@temp_env_vars, "/tmp", &System.get_env/1) |> Path.expand()
    208     cwd = Path.join(File.cwd!(), "tmp")
    209     :persistent_term.put(__MODULE__, [tmp, cwd])
    210 
    211     :ets.new(@dir_table, [:named_table, :public, :set])
    212     :ets.new(@path_table, [:named_table, :public, :duplicate_bag])
    213     {:ok, %{}}
    214   end
    215 
    216   @impl true
    217   def handle_call({:give_away, pid, tmp, path}, _from, state) do
    218     # Since we are writing in behalf of another process, we need to make sure
    219     # the monitor and writing to the tables happen within the same operation.
    220     Process.monitor(pid)
    221     :ets.insert_new(@dir_table, {pid, tmp})
    222     :ets.insert(@path_table, {pid, path})
    223 
    224     {:reply, :ok, state}
    225   end
    226 
    227   @impl true
    228   def handle_cast({:monitor, pid}, state) do
    229     Process.monitor(pid)
    230     {:noreply, state}
    231   end
    232 
    233   @impl true
    234   def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
    235     case :ets.lookup(@dir_table, pid) do
    236       [{pid, _tmp}] ->
    237         :ets.delete(@dir_table, pid)
    238 
    239         @path_table
    240         |> :ets.lookup(pid)
    241         |> Enum.each(&delete_path/1)
    242 
    243         :ets.delete(@path_table, pid)
    244 
    245       [] ->
    246         :ok
    247     end
    248 
    249     {:noreply, state}
    250   end
    251 
    252   def handle_info(_msg, state) do
    253     {:noreply, state}
    254   end
    255 
    256   @impl true
    257   def terminate(_reason, _state) do
    258     folder = fn entry, :ok -> delete_path(entry) end
    259     :ets.foldl(folder, :ok, @path_table)
    260   end
    261 
    262   defp delete_path({_pid, path}) do
    263     :file.delete(path)
    264     :ok
    265   end
    266 end