Skip to content

Commit

Permalink
New API for web hosting (#337)
Browse files Browse the repository at this point in the history
* Add Pathex dependency
* Add new web_hosting API
* Format content in explorer
  • Loading branch information
Neylix committed May 30, 2022
1 parent 165fc8c commit ed208ea
Show file tree
Hide file tree
Showing 6 changed files with 486 additions and 1 deletion.
174 changes: 174 additions & 0 deletions lib/archethic_web/controllers/api/web_hosting_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
defmodule ArchethicWeb.API.WebHostingController do
@moduledoc false

use ArchethicWeb, :controller

alias Archethic

alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData

alias Archethic.Crypto

use Pathex

require Logger

@spec web_hosting(Plug.Conn.t(), map) :: Plug.Conn.t()
def web_hosting(conn, %{"address" => address, "url_path" => url_path}) do
with {:ok, address} <- Base.decode16(address, case: :mixed),
true <- Crypto.valid_address?(address),
{:ok, %Transaction{address: last_address, data: %TransactionData{content: content}}} <-
Archethic.get_last_transaction(address),
{:ok, json_content} <- Jason.decode(content),
{:ok, file, mime_type} <- get_file(json_content, url_path),
{cached?, etag} <- get_cache(conn, last_address, url_path),
{:ok, file_content, encodage} <- get_file_content(file, cached?) do
conn =
conn
|> put_resp_content_type(mime_type, "utf-8")
|> put_resp_header("content-encoding", "gzip")
|> put_resp_header("cache-control", "public")
|> put_resp_header("etag", etag)

if cached? do
send_resp(conn, 304, "")
else
case encodage do
"gzip" ->
send_resp(conn, 200, file_content)

_ ->
send_resp(conn, 200, :zlib.gzip(file_content))
end
end
else
# Base.decode16 || Crypto.valid_address
er when er in [:error, false] ->
send_resp(conn, 400, "Invalid address")

# Jason.decode
{:error, %Jason.DecodeError{}} ->
send_resp(conn, 400, "Invalid transaction content")

# Archethic.get_last_transaction
{:error, _} ->
send_resp(conn, 400, "Invalid address")

{:file_not_found, url} ->
send_resp(conn, 404, "File #{url} does not exist")

:encodage_error ->
send_resp(conn, 400, "Invalid file encodage")

:file_error ->
send_resp(conn, 400, "Cannot find file content")

_reason ->
send_resp(conn, 404, "Not Found")
end
end

# API without path returns default index.html file
# or the only file if there is only one
@spec get_file(json_content :: map(), url_path :: list()) ::
{:ok, map(), binary()} | {:file_not_found, binary()}
defp get_file(json_content, url_path) do
{json_path, url} =
case Enum.count(url_path) do
0 ->
file_name = get_single_file_name(json_content)
json_path = path(file_name)
{json_path, file_name}

1 ->
file_name = Enum.at(url_path, 0)
json_path = path(file_name)
{json_path, file_name}

_ ->
json_path = get_json_path(url_path)
url = Path.join(url_path)
{json_path, url}
end

case Pathex.view(json_content, json_path) do
{:ok, file} ->
{:ok, file, MIME.from_path(url)}

:error ->
{:file_not_found, url}
end
end

defp get_json_path(url_path) do
Enum.reduce(url_path, nil, fn value, acc ->
if acc == nil do
path(value)
else
acc ~> path(value)
end
end)
end

defp get_single_file_name(json_content) do
keys = Map.keys(json_content)

case Enum.count(keys) do
1 ->
# Control if it is a file or a folder
file_name = Enum.at(keys, 0)

if Map.get(json_content, file_name) |> Map.has_key?("content") do
file_name
else
"index.html"
end

_ ->
"index.html"
end
end

@spec get_cache(conn :: Plug.Conn.t(), last_address :: binary(), url_path :: list()) ::
{boolean(), binary()}
defp get_cache(conn, last_address, url_path) do
etag =
case Enum.empty?(url_path) do
true ->
Base.encode16(last_address, case: :lower)

false ->
Base.encode16(last_address, case: :lower) <> Path.join(url_path)
end

cached? =
case List.first(get_req_header(conn, "if-none-match")) do
got_etag when got_etag == etag ->
true

_ ->
false
end

{cached?, etag}
end

# All file are encoded in base64 in JSON content
@spec get_file_content(file :: map(), cached? :: boolean()) ::
{:ok, binary(), binary() | nil} | :encodage_error | :file_error
defp get_file_content(_file, _cached? = true), do: {:ok, nil, nil}

defp get_file_content(file = %{"content" => content}, _cached = false) do
try do
file_content = Base.url_decode64!(content, padding: false)
encodage = Map.get(file, "encodage")
{:ok, file_content, encodage}
rescue
_ ->
:encodage_error
end
end

defp get_file_content(_, _), do: :file_error
end
2 changes: 2 additions & 0 deletions lib/archethic_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ defmodule ArchethicWeb.Router do
:last_transaction_content
)

get("/web_hosting/:address/*url_path", ArchethicWeb.API.WebHostingController, :web_hosting)

post("/origin_key", ArchethicWeb.API.OriginKeyController, :origin_key)

post("/transaction", ArchethicWeb.API.TransactionController, :new)
Expand Down
4 changes: 4 additions & 0 deletions lib/archethic_web/views/explorer_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ defmodule ArchethicWeb.ExplorerView do
Jason.Formatter.pretty_print_to_iodata(content)
end

def format_transaction_content(:hosting, content) do
Jason.Formatter.pretty_print_to_iodata(content)
end

def format_transaction_content(_, content), do: content

defp format_origin_shared_secrets_content(family, keys) do
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ defmodule Archethic.MixProject do
{:flow, "~> 1.0"},
{:broadway, "~> 1.0"},
{:knigge, "~> 1.4"},
{:ex_json_schema, "~> 0.9.1", override: true}
{:ex_json_schema, "~> 0.9.1", override: true},
{:pathex, "~> 2.0"}
]
end

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"nimble_options": {:hex, :nimble_options, "0.3.7", "1e52dd7673d36138b1a5dede183b5d86dff175dc46d104a8e98e396b85b04670", [:mix], [], "hexpm", "2086907e6665c6b6579be54ef5001928df5231f355f71ed258f80a55e9f63633"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"},
"observer_cli": {:hex, :observer_cli, "1.7.1", "c9ca1f623a3ef0158283a3c37cd7b7235bfe85927ad6e26396dd247e2057f5a1", [:mix, :rebar3], [{:recon, "~>2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "4ccafaaa2ce01b85ddd14591f4d5f6731b4e13b610a70fb841f0701178478280"},
"pathex": {:hex, :pathex, "2.0.0", "b129359b22bf2005d5ed6bf41beb7aed64ca83d8510a0624a596a3fa23d34a1c", [:mix], [], "hexpm", "ffe038484ad8313fd91bec3ed51d0f821a4ba3eb6f2a1b14d9ab2685c5c3c876"},
"phoenix": {:hex, :phoenix, "1.5.13", "d4e0805ec0973bed80d67302631130fb47d75b1a0b7335a0b23c4432b6ce55ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1a7c4f1900e6e60bb60ae6680e48418e3f7c360d58bcb9f812487b6d0d281a0f"},
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"},
Expand Down
Loading

0 comments on commit ed208ea

Please sign in to comment.