commit 14997bdab950299e15478b9e6c087e45eeb590aa
parent 2c8423ef88201d9429501c0c57db4c742f4ad316
Author: srfsh <dev@srf.sh>
Date: Wed, 31 Aug 2022 14:33:59 +0300
Zenflows.Web.File: init
This module adds support for file uploads and downloads (serve).
Diffstat:
2 files changed, 99 insertions(+), 0 deletions(-)
diff --git a/src/zenflows/web/file.ex b/src/zenflows/web/file.ex
@@ -0,0 +1,96 @@
+defmodule Zenflows.Web.File do
+@moduledoc """
+Plug router that deals with file uploads and downloads (serve).
+"""
+
+use Plug.Router
+
+alias Ecto.Multi
+alias Plug.{Conn, Conn.Utils}
+alias Zenflows.DB.Repo
+alias Zenflows.{File, Restroom}
+
+plug :match
+plug :dispatch
+
+post "/" do
+ with :ok <- check_content_type(conn),
+ {:ok, conn, hash} <- fetch_hash(conn) do
+ Multi.new()
+ |> Multi.put(:hash, hash)
+ |> Multi.run(:one, &File.Domain.one/2)
+ |> Multi.run(:check, fn _, %{one: %{size: size, hash: hash}} ->
+ {conn, bin} = read_multipart_body(conn, size)
+ with ^size <- byte_size(bin),
+ {:ok, hash_left} <- Base.url_decode64(hash, padding: false),
+ hash_right = :crypto.hash(:sha512, bin),
+ true <- Restroom.byte_equal?(hash_left, hash_right) do
+ {:ok, {conn, bin}}
+ else _ ->
+ {:error, conn}
+ end
+ end)
+ |> Multi.update(:update, fn %{one: file, check: {_, bin}} ->
+ Ecto.Changeset.change(file, bin: Base.encode64(bin))
+ end)
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{check: {conn, _}}} -> send_resp(conn, 201, "Created")
+ {:error, :one, _, _} -> send_resp(conn, 404, "Not Found")
+ {:error, :check, conn, _} -> send_resp(conn, 422, "Unprocessable Content")
+ {:error, :update, _, %{check: {conn, _}}} -> send_resp(conn, 501, "Internal Server Error")
+ end
+ end
+end
+
+get "/:hash" do
+ hash = Map.fetch!(conn.path_params, "hash")
+ case File.Domain.one(hash: hash) do
+ {:ok, file} ->
+ conn
+ |> put_resp_content_type(file.mime_type)
+ |> send_resp(200, file.bin)
+ _ -> send_resp(conn, 404, "Not Found")
+ end
+end
+
+match _ do
+ send_resp(conn, 404, "Not Found")
+end
+
+@spec check_content_type(Conn.t()) :: :ok | Conn.t() | no_return()
+defp check_content_type(conn) do
+ with [hdr] <- get_req_header(conn, "content-type"),
+ {:ok, "multipart", "form-data", %{}} <- Utils.content_type(hdr) do
+ :ok
+ else _ ->
+ send_resp(conn, 415, "Unsupported Media Type")
+ end
+end
+
+@spec fetch_hash(Conn.t()) :: {:ok, Conn.t(), String.t()} | Conn.t() | no_return()
+defp fetch_hash(conn) do
+ with {:ok, hdrs, conn} <- read_part_headers(conn),
+ [val] <- for({"content-disposition", val} <- hdrs, do: val),
+ %{"name" => hash} <- Utils.params(val) do
+ {:ok, conn, hash}
+ else _ ->
+ send_resp(conn, 422, "Unprocessable Content")
+ end
+end
+
+@spec read_multipart_body(Conn.t(), non_neg_integer()) :: {Conn.t(), binary()}
+defp read_multipart_body(conn, size) do
+ read_multipart_body(conn, size, "")
+end
+
+@spec read_multipart_body(Conn.t(), non_neg_integer(), binary())
+ :: {Conn.t(), binary()}
+defp read_multipart_body(conn, size, acc) do
+ case read_part_body(conn, length: size, read_length: size) do
+ {:ok, body, conn} -> {conn, body}
+ {:more, read, conn} -> read_multipart_body(conn, size, <<acc::binary, read::binary>>)
+ {:done, conn} -> {conn, acc}
+ end
+end
+end
diff --git a/src/zenflows/web/router.ex b/src/zenflows/web/router.ex
@@ -37,6 +37,9 @@ plug :dispatch
context: %{authenticate_calls?: true},
]
+forward "/api/file",
+ to: Zenflows.Web.File
+
forward "/api",
to: Absinthe.Plug,
init_opts: @init_opts