file.ex (2790B)
1 defmodule Zenflows.Web.File do 2 @moduledoc """ 3 Plug router that deals with file uploads and downloads (serve). 4 """ 5 6 use Plug.Router 7 8 alias Ecto.Multi 9 alias Plug.{Conn, Conn.Utils} 10 alias Zenflows.DB.Repo 11 alias Zenflows.{File, Restroom} 12 13 plug :match 14 plug :dispatch 15 16 post "/" do 17 with :ok <- check_content_type(conn), 18 {:ok, conn, hash} <- fetch_hash(conn) do 19 Multi.new() 20 |> Multi.put(:hash, hash) 21 |> Multi.run(:one, &File.Domain.one/2) 22 |> Multi.run(:check, fn _, %{one: %{size: size, hash: hash}} -> 23 {conn, bin} = read_multipart_body(conn, size) 24 with ^size <- byte_size(bin), 25 {:ok, hash_left} <- Base.url_decode64(hash, padding: false), 26 hash_right = :crypto.hash(:sha512, bin), 27 true <- Restroom.byte_equal?(hash_left, hash_right) do 28 {:ok, {conn, bin}} 29 else _ -> 30 {:error, conn} 31 end 32 end) 33 |> Multi.update(:update, fn %{one: file, check: {_, bin}} -> 34 Ecto.Changeset.change(file, bin: Base.encode64(bin)) 35 end) 36 |> Repo.transaction() 37 |> case do 38 {:ok, %{check: {conn, _}}} -> send_resp(conn, 201, "Created") 39 {:error, :one, _, _} -> send_resp(conn, 404, "Not Found") 40 {:error, :check, conn, _} -> send_resp(conn, 422, "Unprocessable Content") 41 {:error, :update, _, %{check: {conn, _}}} -> send_resp(conn, 501, "Internal Server Error") 42 end 43 end 44 end 45 46 get "/:hash" do 47 hash = Map.fetch!(conn.path_params, "hash") 48 case File.Domain.one(hash: hash) do 49 {:ok, file} -> 50 conn 51 |> put_resp_content_type(file.mime_type) 52 |> send_resp(200, file.bin) 53 _ -> send_resp(conn, 404, "Not Found") 54 end 55 end 56 57 match _ do 58 send_resp(conn, 404, "Not Found") 59 end 60 61 @spec check_content_type(Conn.t()) :: :ok | Conn.t() | no_return() 62 defp check_content_type(conn) do 63 with [hdr] <- get_req_header(conn, "content-type"), 64 {:ok, "multipart", "form-data", %{}} <- Utils.content_type(hdr) do 65 :ok 66 else _ -> 67 send_resp(conn, 415, "Unsupported Media Type") 68 end 69 end 70 71 @spec fetch_hash(Conn.t()) :: {:ok, Conn.t(), String.t()} | Conn.t() | no_return() 72 defp fetch_hash(conn) do 73 with {:ok, hdrs, conn} <- read_part_headers(conn), 74 [val] <- for({"content-disposition", val} <- hdrs, do: val), 75 %{"name" => hash} <- Utils.params(val) do 76 {:ok, conn, hash} 77 else _ -> 78 send_resp(conn, 422, "Unprocessable Content") 79 end 80 end 81 82 @spec read_multipart_body(Conn.t(), non_neg_integer()) :: {Conn.t(), binary()} 83 defp read_multipart_body(conn, size) do 84 read_multipart_body(conn, size, "") 85 end 86 87 @spec read_multipart_body(Conn.t(), non_neg_integer(), binary()) 88 :: {Conn.t(), binary()} 89 defp read_multipart_body(conn, size, acc) do 90 case read_part_body(conn, length: size, read_length: size) do 91 {:ok, body, conn} -> {conn, body} 92 {:more, read, conn} -> read_multipart_body(conn, size, <<acc::binary, read::binary>>) 93 {:done, conn} -> {conn, acc} 94 end 95 end 96 end