zf

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

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:
Asrc/zenflows/web/file.ex | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/zenflows/web/router.ex | 3+++
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