Current section

Files

Jump to
bb_liveview lib bb live_view plugs static.ex
Raw

lib/bb/live_view/plugs/static.ex

# SPDX-FileCopyrightText: 2025 James Harton
#
# SPDX-License-Identifier: Apache-2.0
defmodule BB.LiveView.Plugs.Static do
@moduledoc """
Serves static assets bundled with the BB LiveView library.
Assets are served from the library's `priv/static` directory with appropriate
cache headers based on content hashing.
"""
@behaviour Plug
import Plug.Conn
@impl Plug
def init(opts), do: opts
@impl Plug
def call(conn, _opts) do
file = conn.params["file"]
case serve_asset(file) do
{:ok, content, content_type} ->
conn
|> put_resp_header("content-type", content_type)
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|> send_resp(200, content)
|> halt()
:error ->
conn
|> send_resp(404, "Not found")
|> halt()
end
end
defp serve_asset(file) when is_binary(file) do
# Prevent directory traversal
if String.contains?(file, "..") or String.contains?(file, "/") do
:error
else
path = Path.join(static_dir(), file)
if File.exists?(path) do
{:ok, File.read!(path), content_type(file)}
else
:error
end
end
end
defp serve_asset(_), do: :error
defp static_dir do
:bb_liveview
|> :code.priv_dir()
|> to_string()
|> Path.join("static")
end
defp content_type(file) do
case Path.extname(file) do
".css" -> "text/css; charset=utf-8"
".js" -> "application/javascript; charset=utf-8"
".map" -> "application/json"
_ -> "application/octet-stream"
end
end
@doc """
Returns the path to an asset file for use in templates.
The path includes a hash of the file contents for cache busting.
"""
@spec asset_path(String.t()) :: String.t()
def asset_path(file) do
hash = asset_hash(file)
"/__bb_assets__/#{file}?v=#{hash}"
end
defp asset_hash(file) do
path = Path.join(static_dir(), file)
if File.exists?(path) do
path
|> File.read!()
|> :erlang.md5()
|> Base.encode16(case: :lower)
|> String.slice(0, 8)
else
"dev"
end
end
end