Packages
livebook
0.11.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/controllers/session_controller.ex
defmodule LivebookWeb.SessionController do
use LivebookWeb, :controller
alias Livebook.{Sessions, Session, FileSystem}
alias LivebookWeb.Helpers.Codec
def show_file(conn, %{"id" => id, "name" => name}) do
with {:ok, session} <- Sessions.fetch_session(id),
{:ok, file_entry} <- fetch_file_entry(session, name),
true <- file_entry.type == :attachment do
file = FileSystem.File.resolve(session.files_dir, file_entry.name)
serve_static(conn, file)
else
_ ->
send_resp(conn, 404, "Not found")
end
end
defp fetch_file_entry(session, name) do
file_entries = Session.get_notebook_file_entries(session.pid)
Enum.find_value(file_entries, :error, fn file_entry ->
if file_entry.name == name do
{:ok, file_entry}
end
end)
end
# Legacy endpoint for resolving images/
# TODO: remove on Livebook v0.12
def show_image(conn, %{"id" => id, "name" => name}) do
case Sessions.fetch_session(id) do
{:ok, session} ->
images_dir = FileSystem.File.resolve(session.files_dir, "../images/")
file = FileSystem.File.resolve(images_dir, name)
serve_static(conn, file)
{:error, _} ->
send_resp(conn, 404, "Not found")
end
end
def download_file(conn, %{"id" => id, "name" => name}) do
with {:ok, session} <- Sessions.fetch_session(id),
{:ok, path} <- Session.fetch_file_entry_path(session.pid, name) do
send_download(conn, {:file, path}, filename: name)
else
_ ->
send_resp(conn, 404, "Not found")
end
end
def download_source(conn, %{"id" => id, "format" => format}) do
case Sessions.fetch_session(id) do
{:ok, session} ->
notebook = Session.get_notebook(session.pid)
file_name = Session.file_name_for_download(session)
send_notebook_source(conn, notebook, file_name, format)
{:error, _} ->
send_resp(conn, 404, "Not found")
end
end
defp send_notebook_source(conn, notebook, file_name, "livemd" = format) do
opts = [include_outputs: conn.params["include_outputs"] == "true"]
{source, _warnings} = Livebook.LiveMarkdown.notebook_to_livemd(notebook, opts)
send_download(conn, {:binary, source},
filename: file_name <> "." <> format,
content_type: "text/plain"
)
end
defp send_notebook_source(conn, notebook, file_name, "exs" = format) do
source = Livebook.Notebook.Export.Elixir.notebook_to_elixir(notebook)
send_download(conn, {:binary, source},
filename: file_name <> "." <> format,
content_type: "text/plain"
)
end
defp send_notebook_source(conn, _notebook, _file_name, _format) do
send_resp(conn, 400, "Invalid format, supported formats: livemd, exs")
end
defp serve_static(conn, file) do
with {:ok, cache_state, conn} <- put_cache_header(conn, file),
{:ok, conn} <- serve_with_cache(conn, file, cache_state) do
conn
else
{:error, message} -> send_resp(conn, 404, Livebook.Utils.upcase_first(message))
end
end
defp put_cache_header(conn, file) do
with {:ok, etag} <- FileSystem.File.etag_for(file) do
conn =
conn
|> put_resp_header("cache-control", "public")
|> put_resp_header("etag", etag)
if etag in get_req_header(conn, "if-none-match") do
{:ok, :fresh, conn}
else
{:ok, :stale, conn}
end
end
end
defp serve_with_cache(conn, file, :stale) do
filename = FileSystem.File.name(file)
with {:ok, content} <- FileSystem.File.read(file) do
conn
|> put_content_type(filename)
|> send_resp(200, content)
|> then(&{:ok, &1})
end
end
defp serve_with_cache(conn, _file, :fresh) do
{:ok, send_resp(conn, 304, "")}
end
def show_asset(conn, %{"id" => id, "hash" => hash, "file_parts" => file_parts}) do
asset_path = Path.join(file_parts)
# The request comes from a cross-origin iframe
conn = allow_cors(conn)
# This route include session id, while we want the browser to
# cache assets across sessions, so we only ensure the asset
# is available and redirect to the corresponding route without
# session id
if ensure_asset?(id, hash, asset_path) do
conn
|> cache_permanently()
|> put_status(:moved_permanently)
|> redirect(to: ~p"/public/sessions/assets/#{hash}/#{file_parts}")
else
send_resp(conn, 404, "Not found")
end
end
def show_cached_asset(conn, %{"hash" => hash, "file_parts" => file_parts}) do
asset_path = Path.join(file_parts)
# The request comes from a cross-origin iframe
conn = allow_cors(conn)
case lookup_asset(hash, asset_path) do
{:ok, local_asset_path} ->
conn =
conn
|> put_content_type(asset_path)
|> cache_permanently()
local_asset_path_gz = local_asset_path <> ".gz"
if accept_encoding?(conn, "gzip") and File.exists?(local_asset_path_gz) do
conn
|> put_resp_header("content-encoding", "gzip")
|> put_resp_header("vary", "Accept-Encoding")
|> send_file(200, local_asset_path_gz)
else
send_file(conn, 200, local_asset_path)
end
:error ->
send_resp(conn, 404, "Not found")
end
end
def show_input_audio(conn, %{"token" => token}) do
{live_view_pid, input_id} = LivebookWeb.SessionHelpers.verify_input_token!(token)
case GenServer.call(live_view_pid, {:get_input_value, input_id}) do
{:ok, session_id, value} ->
path = Livebook.Session.registered_file_path(session_id, value.file_ref)
conn =
conn
|> cache_permanently()
|> put_resp_header("accept-ranges", "bytes")
case value.format do
:pcm_f32 ->
%{size: file_size} = File.stat!(path)
total_size = Codec.pcm_as_wav_size(file_size)
case parse_byte_range(conn, total_size) do
{range_start, range_end} when range_start > 0 or range_end < total_size - 1 ->
stream =
Codec.encode_pcm_as_wav_stream!(
path,
file_size,
value.num_channels,
value.sampling_rate,
range_start,
range_end - range_start + 1
)
conn
|> put_content_range(range_start, range_end, total_size)
|> send_stream(206, stream)
_ ->
stream =
Codec.encode_pcm_as_wav_stream!(
path,
file_size,
value.num_channels,
value.sampling_rate,
0,
total_size
)
conn
|> put_resp_header("content-length", Integer.to_string(total_size))
|> send_stream(200, stream)
end
:wav ->
%{size: total_size} = File.stat!(path)
case parse_byte_range(conn, total_size) do
{range_start, range_end} when range_start > 0 or range_end < total_size - 1 ->
conn
|> put_content_range(range_start, range_end, total_size)
|> send_file(206, path, range_start, range_end - range_start + 1)
_ ->
send_file(conn, 200, path)
end
end
:error ->
send_resp(conn, 404, "Not found")
end
end
def show_input_image(conn, %{"token" => token}) do
{live_view_pid, input_id} = LivebookWeb.SessionHelpers.verify_input_token!(token)
case GenServer.call(live_view_pid, {:get_input_value, input_id}) do
{:ok, session_id, value} ->
path = Livebook.Session.registered_file_path(session_id, value.file_ref)
conn
|> cache_permanently()
|> send_file(200, path)
:error ->
send_resp(conn, 404, "Not found")
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 ensure_asset?(session_id, hash, asset_path) do
case lookup_asset(hash, asset_path) do
{:ok, _local_asset_path} ->
true
:error ->
with {:ok, session} <- Sessions.fetch_session(session_id),
:ok <- Session.fetch_assets(session.pid, hash) do
true
else
_ -> false
end
end
end
defp lookup_asset(hash, asset_path) do
with {:ok, local_asset_path} <- Session.local_asset_path(hash, asset_path),
true <- File.exists?(local_asset_path) do
{:ok, local_asset_path}
else
_ -> :error
end
end
defp allow_cors(conn) do
put_resp_header(conn, "access-control-allow-origin", "*")
end
defp cache_permanently(conn) do
put_resp_header(conn, "cache-control", "public, max-age=31536000")
end
defp put_content_type(conn, path) do
content_type = MIME.from_path(path)
put_resp_header(conn, "content-type", content_type)
end
defp parse_byte_range(conn, total_size) do
with [range] <- get_req_header(conn, "range"),
%{"bytes" => bytes} <- Plug.Conn.Utils.params(range),
{range_start, range_end} <- start_and_end(bytes, total_size) do
{range_start, range_end}
else
_ -> :error
end
end
defp start_and_end("-" <> rest, total_size) do
case Integer.parse(rest) do
{last, ""} when last > 0 and last <= total_size -> {total_size - last, total_size - 1}
_ -> :error
end
end
defp start_and_end(range, total_size) do
case Integer.parse(range) do
{first, "-"} when first >= 0 ->
{first, total_size - 1}
{first, "-" <> rest} when first >= 0 ->
case Integer.parse(rest) do
{last, ""} when last >= first -> {first, min(last, total_size - 1)}
_ -> :error
end
_ ->
:error
end
end
defp put_content_range(conn, range_start, range_end, total_size) do
put_resp_header(conn, "content-range", "bytes #{range_start}-#{range_end}/#{total_size}")
end
defp send_stream(conn, status, stream) do
conn = send_chunked(conn, status)
Enum.reduce_while(stream, conn, fn chunk, conn ->
case Plug.Conn.chunk(conn, chunk) do
{:ok, conn} ->
{:cont, conn}
{:error, :closed} ->
{:halt, conn}
end
end)
end
end