mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-10 06:01:44 +08:00
Include notebook name in the autosaved notebook path (#748)
* Include notebook name in the autosaved notebook path * Add test for persisting unsaved notebooks
This commit is contained in:
parent
40e3a61e00
commit
4b5ea87b3d
3 changed files with 69 additions and 20 deletions
|
|
@ -71,7 +71,8 @@ defmodule Livebook.Session do
|
||||||
created_at: DateTime.t(),
|
created_at: DateTime.t(),
|
||||||
runtime_monitor_ref: reference() | nil,
|
runtime_monitor_ref: reference() | nil,
|
||||||
autosave_timer_ref: reference() | nil,
|
autosave_timer_ref: reference() | nil,
|
||||||
save_task_pid: pid() | nil
|
save_task_pid: pid() | nil,
|
||||||
|
saved_default_file: FileSystem.File.t() | nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
|
|
@ -99,6 +100,9 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
* `:images` - a map from image name to its binary content, an alternative
|
* `:images` - a map from image name to its binary content, an alternative
|
||||||
to `:copy_images_from` when the images are in memory
|
to `:copy_images_from` when the images are in memory
|
||||||
|
|
||||||
|
* `:autosave_path` - a local directory to save notebooks without a file into.
|
||||||
|
Defaults to `Livebook.Config.autosave_path/1`
|
||||||
"""
|
"""
|
||||||
@spec start_link(keyword()) :: {:ok, pid} | {:error, any()}
|
@spec start_link(keyword()) :: {:ok, pid} | {:error, any()}
|
||||||
def start_link(opts) do
|
def start_link(opts) do
|
||||||
|
|
@ -399,7 +403,9 @@ defmodule Livebook.Session do
|
||||||
created_at: DateTime.utc_now(),
|
created_at: DateTime.utc_now(),
|
||||||
runtime_monitor_ref: nil,
|
runtime_monitor_ref: nil,
|
||||||
autosave_timer_ref: nil,
|
autosave_timer_ref: nil,
|
||||||
save_task_pid: nil
|
autosave_path: opts[:autosave_path],
|
||||||
|
save_task_pid: nil,
|
||||||
|
saved_default_file: nil
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
|
|
@ -688,9 +694,9 @@ defmodule Livebook.Session do
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:save_finished, pid, result}, %{save_task_pid: pid} = state) do
|
def handle_info({:save_finished, pid, result, file, default?}, %{save_task_pid: pid} = state) do
|
||||||
state = %{state | save_task_pid: nil}
|
state = %{state | save_task_pid: nil}
|
||||||
{:noreply, handle_save_finished(state, result)}
|
{:noreply, handle_save_finished(state, result, file, default?)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(_message, state), do: {:noreply, state}
|
def handle_info(_message, state), do: {:noreply, state}
|
||||||
|
|
@ -973,7 +979,7 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_save_notebook_async(state) do
|
defp maybe_save_notebook_async(state) do
|
||||||
file = notebook_autosave_file(state)
|
{file, default?} = notebook_autosave_file(state)
|
||||||
|
|
||||||
if file && should_save_notebook?(state) do
|
if file && should_save_notebook?(state) do
|
||||||
pid = self()
|
pid = self()
|
||||||
|
|
@ -982,7 +988,7 @@ defmodule Livebook.Session do
|
||||||
{:ok, pid} =
|
{:ok, pid} =
|
||||||
Task.start(fn ->
|
Task.start(fn ->
|
||||||
result = FileSystem.File.write(file, content)
|
result = FileSystem.File.write(file, content)
|
||||||
send(pid, {:save_finished, self(), result})
|
send(pid, {:save_finished, self(), result, file, default?})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
%{state | save_task_pid: pid}
|
%{state | save_task_pid: pid}
|
||||||
|
|
@ -992,12 +998,12 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_save_notebook_sync(state) do
|
defp maybe_save_notebook_sync(state) do
|
||||||
file = notebook_autosave_file(state)
|
{file, default?} = notebook_autosave_file(state)
|
||||||
|
|
||||||
if file && should_save_notebook?(state) do
|
if file && should_save_notebook?(state) do
|
||||||
content = LiveMarkdown.Export.notebook_to_markdown(state.data.notebook)
|
content = LiveMarkdown.Export.notebook_to_markdown(state.data.notebook)
|
||||||
result = FileSystem.File.write(file, content)
|
result = FileSystem.File.write(file, content)
|
||||||
handle_save_finished(state, result)
|
handle_save_finished(state, result, file, default?)
|
||||||
else
|
else
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|
@ -1008,33 +1014,52 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp notebook_autosave_file(state) do
|
defp notebook_autosave_file(state) do
|
||||||
state.data.file || default_notebook_file(state)
|
file = state.data.file || default_notebook_file(state)
|
||||||
|
default? = state.data.file == nil
|
||||||
|
{file, default?}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp default_notebook_file(session) do
|
defp default_notebook_file(state) do
|
||||||
if path = Livebook.Config.autosave_path() do
|
if path = state.autosave_path || Livebook.Config.autosave_path() do
|
||||||
dir = path |> FileSystem.Utils.ensure_dir_path() |> FileSystem.File.local()
|
dir = path |> FileSystem.Utils.ensure_dir_path() |> FileSystem.File.local()
|
||||||
notebook_rel_path = path_with_timestamp(session.session_id, session.created_at)
|
notebook_rel_path = default_notebook_path(state)
|
||||||
FileSystem.File.resolve(dir, notebook_rel_path)
|
FileSystem.File.resolve(dir, notebook_rel_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp path_with_timestamp(session_id, date_time) do
|
defp default_notebook_path(state) do
|
||||||
|
title_str =
|
||||||
|
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 characters from the session id, which
|
# use a few trailing characters from the session id,
|
||||||
# is random already
|
# which are random already
|
||||||
random_str = String.slice(session_id, 0..3)
|
random_str = String.slice(state.session_id, -4..-1)
|
||||||
|
|
||||||
[date_str, time_str, _] =
|
[date_str, time_str, _] =
|
||||||
date_time
|
state.created_at
|
||||||
|> DateTime.to_iso8601()
|
|> DateTime.to_iso8601()
|
||||||
|> String.replace(["-", ":"], "_")
|
|> String.replace(["-", ":"], "_")
|
||||||
|> String.split(["T", "."])
|
|> String.split(["T", "."])
|
||||||
|
|
||||||
"#{date_str}/#{time_str}_#{random_str}.livemd"
|
"#{date_str}/#{time_str}_#{title_str}_#{random_str}.livemd"
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_save_finished(state, result) do
|
defp handle_save_finished(state, result, file, default?) do
|
||||||
|
state =
|
||||||
|
if default? do
|
||||||
|
if state.saved_default_file && state.saved_default_file != file do
|
||||||
|
FileSystem.File.remove(state.saved_default_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
%{state | saved_default_file: file}
|
||||||
|
else
|
||||||
|
state
|
||||||
|
end
|
||||||
|
|
||||||
case result do
|
case result do
|
||||||
:ok ->
|
:ok ->
|
||||||
handle_operation(state, {:mark_as_not_dirty, self()})
|
handle_operation(state, {:mark_as_not_dirty, self()})
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ defmodule Livebook.Session.Data do
|
||||||
notebook: notebook,
|
notebook: notebook,
|
||||||
origin: nil,
|
origin: nil,
|
||||||
file: nil,
|
file: nil,
|
||||||
dirty: false,
|
dirty: true,
|
||||||
section_infos: initial_section_infos(notebook),
|
section_infos: initial_section_infos(notebook),
|
||||||
cell_infos: initial_cell_infos(notebook),
|
cell_infos: initial_cell_infos(notebook),
|
||||||
input_values: initial_input_values(notebook),
|
input_values: initial_input_values(notebook),
|
||||||
|
|
|
||||||
|
|
@ -583,6 +583,30 @@ defmodule Livebook.SessionTest do
|
||||||
assert DateTime.compare(session.created_at, DateTime.utc_now()) == :lt
|
assert DateTime.compare(session.created_at, DateTime.utc_now()) == :lt
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag :tmp_dir
|
||||||
|
test "session without a file is persisted to autosave path", %{tmp_dir: tmp_dir} do
|
||||||
|
session = start_session(autosave_path: tmp_dir)
|
||||||
|
|
||||||
|
notebook_glob = Path.join(tmp_dir, "**/*.livemd")
|
||||||
|
|
||||||
|
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||||
|
|
||||||
|
Session.save(session.pid)
|
||||||
|
assert_receive {:operation, {:mark_as_not_dirty, _}}
|
||||||
|
|
||||||
|
assert [notebook_path] = Path.wildcard(notebook_glob)
|
||||||
|
assert Path.basename(notebook_path) =~ "untitled_notebook"
|
||||||
|
|
||||||
|
# After the name is changed we should save to a different file
|
||||||
|
Session.set_notebook_name(session.pid, "Cat's guide to life")
|
||||||
|
|
||||||
|
Session.save(session.pid)
|
||||||
|
assert_receive {:operation, {:mark_as_not_dirty, _}}
|
||||||
|
|
||||||
|
assert [notebook_path] = Path.wildcard(notebook_glob)
|
||||||
|
assert Path.basename(notebook_path) =~ "cats_guide_to_life"
|
||||||
|
end
|
||||||
|
|
||||||
defp start_session(opts \\ []) do
|
defp start_session(opts \\ []) do
|
||||||
session_id = Utils.random_id()
|
session_id = Utils.random_id()
|
||||||
{:ok, pid} = Session.start_link(Keyword.merge([id: session_id], opts))
|
{:ok, pid} = Session.start_link(Keyword.merge([id: session_id], opts))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue