mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-12 06:25:56 +08:00
Export with title or file name (#870)
* Export file with title or file name * Export with title or file name * Add Session.file_name_for_download/1 * Compute the name without calling the server Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
parent
be1fce326c
commit
358bdb3267
5 changed files with 67 additions and 14 deletions
|
@ -157,6 +157,27 @@ defmodule Livebook.Session do
|
||||||
GenServer.call(pid, :get_notebook, @timeout)
|
GenServer.call(pid, :get_notebook, @timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the file name for download.
|
||||||
|
|
||||||
|
Note that the name doesn't have any extension.
|
||||||
|
|
||||||
|
If the notebook has an associated file, the same name is used,
|
||||||
|
otherwise it is computed from the notebook title.
|
||||||
|
"""
|
||||||
|
@spec file_name_for_download(t()) :: String.t()
|
||||||
|
def file_name_for_download(session)
|
||||||
|
|
||||||
|
def file_name_for_download(%{file: nil} = session) do
|
||||||
|
notebook_name_to_file_name(session.notebook_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def file_name_for_download(session) do
|
||||||
|
session.file
|
||||||
|
|> FileSystem.File.name()
|
||||||
|
|> Path.rootname()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Fetches assets matching the given hash.
|
Fetches assets matching the given hash.
|
||||||
|
|
||||||
|
@ -406,7 +427,8 @@ defmodule Livebook.Session do
|
||||||
If there's a file set and the notebook changed since the last save,
|
If there's a file set and the notebook changed since the last save,
|
||||||
it will be persisted to said file.
|
it will be persisted to said file.
|
||||||
|
|
||||||
Note that notebooks are automatically persisted every @autosave_interval milliseconds.
|
Note that notebooks are automatically persisted every @autosave_interval
|
||||||
|
milliseconds.
|
||||||
"""
|
"""
|
||||||
@spec save(pid()) :: :ok
|
@spec save(pid()) :: :ok
|
||||||
def save(pid) do
|
def save(pid) do
|
||||||
|
@ -1237,11 +1259,7 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp default_notebook_path(state) do
|
defp default_notebook_path(state) do
|
||||||
title_str =
|
title_str = notebook_name_to_file_name(state.data.notebook.name)
|
||||||
state.data.notebook.name
|
|
||||||
|> String.downcase()
|
|
||||||
|> String.replace(~r/\s+/, "_")
|
|
||||||
|> String.replace(~r/[^\w]/, "")
|
|
||||||
|
|
||||||
# We want a random, but deterministic part, so we
|
# We want a random, but deterministic part, so we
|
||||||
# use a few trailing characters from the session id,
|
# use a few trailing characters from the session id,
|
||||||
|
@ -1257,6 +1275,17 @@ defmodule Livebook.Session do
|
||||||
"#{date_str}/#{time_str}_#{title_str}_#{random_str}.livemd"
|
"#{date_str}/#{time_str}_#{title_str}_#{random_str}.livemd"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp notebook_name_to_file_name(notebook_name) do
|
||||||
|
notebook_name
|
||||||
|
|> String.downcase()
|
||||||
|
|> String.replace(~r/\s+/, "_")
|
||||||
|
|> String.replace(~r/[^\w]/, "")
|
||||||
|
|> case do
|
||||||
|
"" -> "untitled_notebook"
|
||||||
|
name -> name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp handle_save_finished(state, result, file, default?) do
|
defp handle_save_finished(state, result, file, default?) do
|
||||||
state =
|
state =
|
||||||
if default? do
|
if default? do
|
||||||
|
|
|
@ -18,34 +18,35 @@ defmodule LivebookWeb.SessionController do
|
||||||
case Sessions.fetch_session(id) do
|
case Sessions.fetch_session(id) do
|
||||||
{:ok, session} ->
|
{:ok, session} ->
|
||||||
notebook = Session.get_notebook(session.pid)
|
notebook = Session.get_notebook(session.pid)
|
||||||
|
file_name = Session.file_name_for_download(session)
|
||||||
|
|
||||||
send_notebook_source(conn, notebook, format)
|
send_notebook_source(conn, notebook, file_name, format)
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
send_resp(conn, 404, "Not found")
|
send_resp(conn, 404, "Not found")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp send_notebook_source(conn, notebook, "livemd") do
|
defp send_notebook_source(conn, notebook, file_name, "livemd" = format) do
|
||||||
opts = [include_outputs: conn.params["include_outputs"] == "true"]
|
opts = [include_outputs: conn.params["include_outputs"] == "true"]
|
||||||
source = Livebook.LiveMarkdown.Export.notebook_to_markdown(notebook, opts)
|
source = Livebook.LiveMarkdown.Export.notebook_to_markdown(notebook, opts)
|
||||||
|
|
||||||
send_download(conn, {:binary, source},
|
send_download(conn, {:binary, source},
|
||||||
filename: "notebook.livemd",
|
filename: file_name <> "." <> format,
|
||||||
content_type: "text/plain"
|
content_type: "text/plain"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp send_notebook_source(conn, notebook, "exs") do
|
defp send_notebook_source(conn, notebook, file_name, "exs" = format) do
|
||||||
source = Livebook.Notebook.Export.Elixir.notebook_to_elixir(notebook)
|
source = Livebook.Notebook.Export.Elixir.notebook_to_elixir(notebook)
|
||||||
|
|
||||||
send_download(conn, {:binary, source},
|
send_download(conn, {:binary, source},
|
||||||
filename: "notebook.exs",
|
filename: file_name <> "." <> format,
|
||||||
content_type: "text/plain"
|
content_type: "text/plain"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp send_notebook_source(conn, _notebook, _format) do
|
defp send_notebook_source(conn, _notebook, _file_name, _format) do
|
||||||
send_resp(conn, 400, "Invalid format, supported formats: livemd, exs")
|
send_resp(conn, 400, "Invalid format, supported formats: livemd, exs")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule LivebookWeb.SessionLive.ExportElixirComponent do
|
defmodule LivebookWeb.SessionLive.ExportElixirComponent do
|
||||||
use LivebookWeb, :live_component
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
alias Livebook.Session
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(assigns, socket) do
|
def update(assigns, socket) do
|
||||||
socket = assign(socket, assigns)
|
socket = assign(socket, assigns)
|
||||||
|
@ -28,7 +30,7 @@ defmodule LivebookWeb.SessionLive.ExportElixirComponent do
|
||||||
<div class="flex flex-col space-y-1">
|
<div class="flex flex-col space-y-1">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-sm text-gray-700 font-semibold">
|
<span class="text-sm text-gray-700 font-semibold">
|
||||||
.exs
|
<%= Session.file_name_for_download(@session) <> ".exs" %>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex justify-end space-x-2">
|
<div class="flex justify-end space-x-2">
|
||||||
<span class="tooltip left" data-tooltip="Copy source">
|
<span class="tooltip left" data-tooltip="Copy source">
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule LivebookWeb.SessionLive.ExportLiveMarkdownComponent do
|
defmodule LivebookWeb.SessionLive.ExportLiveMarkdownComponent do
|
||||||
use LivebookWeb, :live_component
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
alias Livebook.Session
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(assigns, socket) do
|
def update(assigns, socket) do
|
||||||
socket = assign(socket, assigns)
|
socket = assign(socket, assigns)
|
||||||
|
@ -35,7 +37,7 @@ defmodule LivebookWeb.SessionLive.ExportLiveMarkdownComponent do
|
||||||
<div class="flex flex-col space-y-1">
|
<div class="flex flex-col space-y-1">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-sm text-gray-700 font-semibold">
|
<span class="text-sm text-gray-700 font-semibold">
|
||||||
.livemd
|
<%= Session.file_name_for_download(@session) <> ".livemd" %>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex justify-end space-x-2">
|
<div class="flex justify-end space-x-2">
|
||||||
<span class="tooltip left" data-tooltip="Copy source">
|
<span class="tooltip left" data-tooltip="Copy source">
|
||||||
|
|
|
@ -9,6 +9,25 @@ defmodule Livebook.SessionTest do
|
||||||
%{session: session}
|
%{session: session}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "file_name_for_download/1" do
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "uses associated file name if one is attached", %{tmp_dir: tmp_dir} do
|
||||||
|
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||||
|
file = FileSystem.File.resolve(tmp_dir, "my_notebook.livemd")
|
||||||
|
session = start_session(file: file)
|
||||||
|
|
||||||
|
assert Session.file_name_for_download(session) == "my_notebook"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "defaults to notebook name", %{session: session} do
|
||||||
|
Session.set_notebook_name(session.pid, "Cat's guide to life!")
|
||||||
|
# Get the updated struct
|
||||||
|
session = Session.get_by_pid(session.pid)
|
||||||
|
|
||||||
|
assert Session.file_name_for_download(session) == "cats_guide_to_life"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "set_notebook_attributes/2" do
|
describe "set_notebook_attributes/2" do
|
||||||
test "sends an attributes update to subscribers", %{session: session} do
|
test "sends an attributes update to subscribers", %{session: session} do
|
||||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||||
|
|
Loading…
Add table
Reference in a new issue