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:
Julian Gomez 2022-02-03 14:17:30 -06:00 committed by GitHub
parent be1fce326c
commit 358bdb3267
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 67 additions and 14 deletions

View file

@ -157,6 +157,27 @@ defmodule Livebook.Session do
GenServer.call(pid, :get_notebook, @timeout)
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 """
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,
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
def save(pid) do
@ -1237,11 +1259,7 @@ defmodule Livebook.Session do
end
defp default_notebook_path(state) do
title_str =
state.data.notebook.name
|> String.downcase()
|> String.replace(~r/\s+/, "_")
|> String.replace(~r/[^\w]/, "")
title_str = notebook_name_to_file_name(state.data.notebook.name)
# We want a random, but deterministic part, so we
# 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"
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
state =
if default? do

View file

@ -18,34 +18,35 @@ defmodule LivebookWeb.SessionController 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, format)
send_notebook_source(conn, notebook, file_name, format)
:error ->
send_resp(conn, 404, "Not found")
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"]
source = Livebook.LiveMarkdown.Export.notebook_to_markdown(notebook, opts)
send_download(conn, {:binary, source},
filename: "notebook.livemd",
filename: file_name <> "." <> format,
content_type: "text/plain"
)
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)
send_download(conn, {:binary, source},
filename: "notebook.exs",
filename: file_name <> "." <> format,
content_type: "text/plain"
)
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")
end

View file

@ -1,6 +1,8 @@
defmodule LivebookWeb.SessionLive.ExportElixirComponent do
use LivebookWeb, :live_component
alias Livebook.Session
@impl true
def update(assigns, socket) do
socket = assign(socket, assigns)
@ -28,7 +30,7 @@ defmodule LivebookWeb.SessionLive.ExportElixirComponent do
<div class="flex flex-col space-y-1">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-700 font-semibold">
.exs
<%= Session.file_name_for_download(@session) <> ".exs" %>
</span>
<div class="flex justify-end space-x-2">
<span class="tooltip left" data-tooltip="Copy source">

View file

@ -1,6 +1,8 @@
defmodule LivebookWeb.SessionLive.ExportLiveMarkdownComponent do
use LivebookWeb, :live_component
alias Livebook.Session
@impl true
def update(assigns, socket) do
socket = assign(socket, assigns)
@ -35,7 +37,7 @@ defmodule LivebookWeb.SessionLive.ExportLiveMarkdownComponent do
<div class="flex flex-col space-y-1">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-700 font-semibold">
.livemd
<%= Session.file_name_for_download(@session) <> ".livemd" %>
</span>
<div class="flex justify-end space-x-2">
<span class="tooltip left" data-tooltip="Copy source">

View file

@ -9,6 +9,25 @@ defmodule Livebook.SessionTest do
%{session: session}
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
test "sends an attributes update to subscribers", %{session: session} do
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")