Packages
livebook
0.4.0
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook_web/plugs/static_plug.ex
defmodule LivebookWeb.StaticPlug.File do
@moduledoc false
defstruct [:content, :digest]
@type t :: %__MODULE__{content: binary(), digest: String.t()}
end
defmodule LivebookWeb.StaticPlug.Provider do
@moduledoc false
@type segments :: list(String.t())
@type compression :: :gzip | nil
@doc """
Returns file data for the given path (given as list of segments) and compression type.
"""
@callback get_file(segments(), compression()) :: LivebookWeb.StaticPlug.File.t() | nil
@doc """
Parses static files location usually passed as the `:from` option
when configuring provider.
See `Plug.Static` for more details.
"""
@spec static_path({atom(), binary()} | atom() | binary()) :: binary()
def static_path(from)
def static_path({app, path}) when is_atom(app) and is_binary(path) do
Path.join(Application.app_dir(app), path)
end
def static_path(path) when is_binary(path), do: path
def static_path(app) when is_atom(app), do: static_path({app, "priv/static"})
end
defmodule LivebookWeb.StaticPlug do
@moduledoc false
# This is a simplified version of `Plug.Static` meant
# to serve static files using the given provider.
#
# ## Options
#
# * `:file_provider` (**required**) - a module implementing `LivebookWeb.StaticPlug.Provider`
# behaviour, responsible for resolving file requests
#
# * `:at`, `:gzip` - same as `Plug.Static`
@behaviour Plug
import Plug.Conn
@allowed_methods ~w(GET HEAD)
@impl true
def init(opts) do
file_provider = Keyword.fetch!(opts, :file_provider)
%{
file_provider: file_provider,
at: opts |> Keyword.fetch!(:at) |> Plug.Router.Utils.split(),
gzip?: Keyword.get(opts, :gzip, false)
}
end
@impl true
def call(
%Plug.Conn{method: method} = conn,
%{file_provider: file_provider, at: at, gzip?: gzip?} = options
)
when method in @allowed_methods do
segments = subset(at, conn.path_info)
case encoding_with_file(conn, file_provider, segments, gzip?) do
{encoding, file} ->
serve_static(conn, encoding, file, segments, options)
:error ->
conn
end
end
def call(conn, _options) do
conn
end
defp serve_static(conn, content_encoding, file, segments, options) do
case put_cache_header(conn, file) do
{:stale, conn} ->
filename = List.last(segments)
content_type = MIME.from_path(filename)
conn
|> put_resp_header("content-type", content_type)
|> maybe_add_encoding(content_encoding)
|> maybe_add_vary(options)
|> send_resp(200, file.content)
|> halt()
{:fresh, conn} ->
conn
|> maybe_add_vary(options)
|> send_resp(304, "")
|> halt()
end
end
defp maybe_add_encoding(conn, nil), do: conn
defp maybe_add_encoding(conn, ce), do: put_resp_header(conn, "content-encoding", ce)
# If we serve gzip at any moment, we need to set the proper vary
# header regardless of whether we are serving gzip content right now.
# See: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
defp maybe_add_vary(conn, %{gzip?: true}) do
update_in(conn.resp_headers, &[{"vary", "Accept-Encoding"} | &1])
end
defp maybe_add_vary(conn, _options), do: conn
defp put_cache_header(conn, file) do
etag = etag_for_file(file)
conn =
conn
|> put_resp_header("cache-control", "public")
|> put_resp_header("etag", etag)
if etag in get_req_header(conn, "if-none-match") do
{:fresh, conn}
else
{:stale, conn}
end
end
defp etag_for_file(file) do
<<?", file.digest::binary, ?">>
end
defp encoding_with_file(conn, file_provider, segments, gzip?) do
cond do
file = gzip? and accept_encoding?(conn, "gzip") && file_provider.get_file(segments, :gzip) ->
{"gzip", file}
file = file_provider.get_file(segments, nil) ->
{nil, file}
true ->
:error
end
end
defp accept_encoding?(conn, encoding) do
encoding? = &String.contains?(&1, [encoding, "*"])
Enum.any?(get_req_header(conn, "accept-encoding"), fn accept ->
accept |> Plug.Conn.Utils.list() |> Enum.any?(encoding?)
end)
end
defp subset([h | expected], [h | actual]), do: subset(expected, actual)
defp subset([], actual), do: actual
defp subset(_, _), do: []
end