Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AEWeb - Directory listing #705

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ config :archethic, ArchethicWeb.FaucetController, enabled: true
# watchers to your application. For example, we use it
# with webpack to recompile .js and .css sources.
config :archethic, ArchethicWeb.Endpoint,
explorer_url:
Path.join([
"https://",
"#{System.get_env("ARCHETHIC_DOMAIN_NAME", "localhost")}:#{System.get_env("ARCHETHIC_HTTPS_PORT", "5000")}",
"explorer"
]),
http: [port: System.get_env("ARCHETHIC_HTTP_PORT", "4000") |> String.to_integer()],
server: true,
debug_errors: true,
Expand Down
6 changes: 6 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ config :archethic, ArchethicWeb.FaucetController,
# which you should run after static files are built and
# before starting your production server.
config :archethic, ArchethicWeb.Endpoint,
explorer_url:
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
Path.join([
"https://",
"#{System.get_env("ARCHETHIC_DOMAIN_NAME", "mainnet.archethic.net")}:#{System.get_env("ARCHETHIC_HTTPS_PORT", "50000")}",
"explorer"
]),
http: [:inet6, port: System.get_env("ARCHETHIC_HTTP_PORT", "40000") |> String.to_integer()],
url: [host: nil, port: System.get_env("ARCHETHIC_HTTP_PORT", "40000") |> String.to_integer()],
cache_static_manifest: "priv/static/cache_manifest.json",
Expand Down
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,6 @@ config :archethic, Archethic.Utils.DetectNodeResponsiveness, timeout: 1_000
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :archethic, ArchethicWeb.Endpoint,
explorer_url: "",
http: [port: 4002],
server: false
13 changes: 11 additions & 2 deletions lib/archethic_web/controllers/aeweb_root_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,23 @@ defmodule ArchethicWeb.AEWebRootController do
{:ok, file_content, encodage, mime_type, cached?, etag} ->
WebHostingController.send_response(conn, file_content, encodage, mime_type, cached?, etag)

{:error, :not_found} ->
{:error, {:is_a_directory, transaction}} ->
{:ok, listing_html, encodage, mime_type, cached?, etag} =
WebHostingController.dir_listing(conn.request_path, params, transaction, cache_headers)

WebHostingController.send_response(conn, listing_html, encodage, mime_type, cached?, etag)

{:error, :file_not_found} ->
# If file is not found, returning default file (url can be handled by index file)
case url_path do
[] ->
send_resp(conn, 404, "Not Found")

["index.html"] ->
send_resp(conn, 400, "Not Found")

_path ->
params = Map.put(params, "url_path", [])
params = Map.put(params, "url_path", ["index.html"])
index(conn, params)
end

Expand Down
246 changes: 183 additions & 63 deletions lib/archethic_web/controllers/api/web_hosting_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule ArchethicWeb.API.WebHostingController do
require Logger

def web_hosting(conn, params = %{"url_path" => []}) do
# /web_hosting/:addr redirects to /web_hosting/:addr/
if String.last(conn.request_path) != "/" do
redirect(conn, to: conn.request_path <> "/")
else
Expand All @@ -38,12 +39,18 @@ defmodule ArchethicWeb.API.WebHostingController do
{:error, :website_not_found} ->
send_resp(conn, 404, "Cannot find website content")

{:error, :not_found} ->
{:error, :file_not_found} ->
send_resp(conn, 404, "Cannot find file content")

{:error, :invalid_encodage} ->
send_resp(conn, 400, "Invalid file encodage")

{:error, {:is_a_directory, transaction}} ->
{:ok, listing_html, encodage, mime_type, cached?, etag} =
dir_listing(conn.request_path, params, transaction, get_cache_headers(conn))

send_response(conn, listing_html, encodage, mime_type, cached?, etag)

{:error, _} ->
send_resp(conn, 404, "Not Found")
end
Expand All @@ -57,41 +64,153 @@ defmodule ArchethicWeb.API.WebHostingController do
cached? :: boolean(), etag :: binary()}
| {:error, :invalid_address}
| {:error, :invalid_content}
| {:error, :not_found}
| {:error, :file_not_found}
| {:error, :is_a_directory}
| {:error, :invalid_encodage}
| {:error, any()}
def get_website(params = %{"address" => address}, cache_headers) do
url_path = Map.get(params, "url_path", [])
bchamagne marked this conversation as resolved.
Show resolved Hide resolved

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(cache_headers, last_address, url_path),
{:ok, file_content, encodage} <- get_file_content(file, cached?, url_path) do
{:ok, file_content, encodage, mime_type, cached?, etag}
{:ok, transaction} <- Archethic.get_last_transaction(address) do
with {:ok, json_content} <- Jason.decode(transaction.data.content),
{:ok, file, mime_type} <- get_file(json_content, url_path),
{cached?, etag} <- get_cache(cache_headers, transaction.address, url_path),
{:ok, file_content, encodage} <- get_file_content(file, cached?, url_path) do
{:ok, file_content, encodage, mime_type, cached?, etag}
else
:encodage_error ->
{:error, :invalid_encodage}

:file_error ->
{:error, :file_not_found}

{:error, %Jason.DecodeError{}} ->
{:error, :invalid_content}

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

{:error, :malformed} ->
# malformed file will return 404 as described in test "should return Cannot find file content"
{:error, :file_not_found}

{:error, :is_a_directory} ->
# return the transaction so the dir_listing function do not need to do the I/O
{:error, {:is_a_directory, transaction}}
end
else
er when er in [:error, false] ->
{:error, :invalid_address}

{:error, %Jason.DecodeError{}} ->
{:error, :invalid_content}

{:error, reason} when reason in [:transaction_not_exists, :transaction_invalid] ->
{:error, :website_not_found}

{:file_not_found, _url} ->
{:error, :not_found}
error ->
error
end
end

@spec dir_listing(
request_path :: String.t(),
params :: map(),
transaction :: Transaction.t(),
cached_headers :: list()
) ::
{:ok, listing_html :: binary() | nil, encodage :: nil | binary(), mime_type :: binary(),
cached? :: boolean(), etag :: binary()}
def dir_listing(request_path, params, transaction, cache_headers) do
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
url_path = Map.get(params, "url_path", [])

with {:ok, json_content} <- Jason.decode(transaction.data.content),
{cached?, etag} <- get_cache(cache_headers, transaction.address, url_path) do
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
json_content_subset =
case url_path do
[] ->
json_content

_ ->
{:ok, subset} = Pathex.view(json_content, get_json_path(url_path))
subset
end

{current_working_dir, parent_dir_href} =
case url_path do
[] ->
{"/", nil}

_ ->
{
Path.join(["/" | url_path]),
%{href: request_path |> Path.join("..") |> Path.expand()}
}
end

assigns =
Map.keys(json_content_subset)
|> Enum.map(fn key ->
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
case json_content_subset[key] do
%{"address" => address} ->
{:file, key, address}

_ ->
{:dir, key}
end
end)
# sort directory last, then DESC order (it will be accumulated in reverse order below)
|> Enum.sort(fn
{:file, a, _}, {:file, b, _} ->
a > b

:encodage_error ->
{:error, :invalid_encodage}
{:dir, a}, {:dir, b} ->
a > b

:file_error ->
{:error, :not_found}
{:file, _, _}, {:dir, _} ->
true

{:dir, _}, {:file, _, _} ->
false
end)
|> Enum.reduce(%{dirs: [], files: []}, fn
{:file, name, addresses}, %{dirs: dirs_acc, files: files_acc} ->
item = %{
href: %{href: Path.join(request_path, name)},
last_modified: get_transaction_timestamp(transaction),
addresses: addresses,
name: name
}

%{dirs: dirs_acc, files: [item | files_acc]}

{:dir, name}, %{dirs: dirs_acc, files: files_acc} ->
item = %{
href: %{href: Path.join(request_path, name)},
last_modified: get_transaction_timestamp(transaction),
name: name
}

%{files: files_acc, dirs: [item | dirs_acc]}
end)
|> Enum.into(%{
cwd: current_working_dir,
parent_dir_href: parent_dir_href,
reference_transaction_href: %{
href:
Path.join([
Keyword.fetch!(
Application.get_env(:archethic, ArchethicWeb.Endpoint),
:explorer_url
),
"transaction",
Base.encode16(transaction.address)
])
}
})

{:ok, Phoenix.View.render_to_iodata(ArchethicWeb.DirListingView, "index.html", assigns),
nil, "text/html", cached?, etag}
else
error ->
error
end
Expand Down Expand Up @@ -145,33 +264,43 @@ defmodule ArchethicWeb.API.WebHostingController do
end
end

# API without path returns default index.html file
# or the only file if there is only one
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}
defp get_file(json_content, path), do: get_file(json_content, path, nil)

_ ->
json_path = get_json_path(url_path)
url = Path.join(url_path)
{json_path, url}
end
# case when we're parsing a reference tx
defp get_file(file = %{"address" => _}, [], previous_path_item) do
{:ok, file, MIME.from_path(previous_path_item)}
end

# case when we're parsing a storage tx
defp get_file(file, [], previous_path_item) when is_binary(file) do
{:ok, file, MIME.from_path(previous_path_item)}
end

# case when we're on a directory
defp get_file(json_content, [], _previous_path_item) when is_map(json_content) do
case Map.get(json_content, "index.html") do
nil ->
# make sure it is a directory instead of a malformed file
if Enum.all?(Map.values(json_content), &is_map/1) do
{:error, :is_a_directory}
else
{:error, :malformed}
end

file ->
{:ok, file, "text/html"}
end
end

case Pathex.view(json_content, json_path) do
{:ok, file} ->
{:ok, file, MIME.from_path(url)}
# recurse until we are on the end of path
defp get_file(json_content, [path_item | rest], _previous_path_item) do
case Map.get(json_content, path_item) do
nil ->
#
{:error, :file_not_found}

:error ->
{:file_not_found, url}
json_content_subset ->
get_file(json_content_subset, rest, path_item)
end
end

Expand All @@ -185,26 +314,6 @@ defmodule ArchethicWeb.API.WebHostingController do
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)
file_content = Map.get(json_content, file_name)

if !is_map(file_content) or Map.has_key?(file_content, "address") do
file_name
else
"index.html"
end

_ ->
"index.html"
end
end

defp get_cache(cache_headers, last_address, url_path) do
etag =
case Enum.empty?(url_path) do
Expand Down Expand Up @@ -256,4 +365,15 @@ defmodule ArchethicWeb.API.WebHostingController do
end

defp get_file_content(_, _, _), do: :file_error

defp get_transaction_timestamp(transaction) do
case transaction.validation_stamp do
nil ->
# should happen only in the tests
DateTime.utc_now()

validation_stamp ->
validation_stamp.timestamp
end
end
end
Loading