diff --git a/lib/livebook/app.ex b/lib/livebook/app.ex index ebbc7bdc9..ce587e593 100644 --- a/lib/livebook/app.ex +++ b/lib/livebook/app.ex @@ -58,13 +58,17 @@ defmodule Livebook.App do * `:warnings` - a list of warnings to show for the initial deployment + * `:files_source` - a location to fetch notebook files from, see + `Livebook.Session.start_link/1` for more details + """ @spec start_link(keyword()) :: {:ok, pid} | {:error, any()} def start_link(opts) do notebook = Keyword.fetch!(opts, :notebook) warnings = Keyword.get(opts, :warnings, []) + files_source = Keyword.get(opts, :files_source) - GenServer.start_link(__MODULE__, {notebook, warnings}) + GenServer.start_link(__MODULE__, {notebook, warnings, files_source}) end @doc """ @@ -116,7 +120,8 @@ defmodule Livebook.App do @spec deploy(pid(), Livebook.Notebook.t(), keyword()) :: :ok def deploy(pid, notebook, opts \\ []) do warnings = Keyword.get(opts, :warnings, []) - GenServer.cast(pid, {:deploy, notebook, warnings}) + files_source = Keyword.get(opts, :files_source) + GenServer.cast(pid, {:deploy, notebook, warnings, files_source}) end @doc """ @@ -150,11 +155,12 @@ defmodule Livebook.App do end @impl true - def init({notebook, warnings}) do + def init({notebook, warnings, files_source}) do {:ok, %{ version: 1, notebook: notebook, + files_source: files_source, warnings: warnings, sessions: [], users: %{} @@ -191,11 +197,17 @@ defmodule Livebook.App do end @impl true - def handle_cast({:deploy, notebook, warnings}, state) do + def handle_cast({:deploy, notebook, warnings, files_source}, state) do true = notebook.app_settings.slug == state.notebook.app_settings.slug {:noreply, - %{state | notebook: notebook, version: state.version + 1, warnings: warnings} + %{ + state + | notebook: notebook, + version: state.version + 1, + warnings: warnings, + files_source: files_source + } |> start_eagerly() |> shutdown_old_versions() |> notify_update()} @@ -265,6 +277,7 @@ defmodule Livebook.App do opts = [ notebook: state.notebook, + files_source: state.files_source, mode: :app, app_pid: self(), auto_shutdown_ms: state.notebook.app_settings.auto_shutdown_ms, diff --git a/lib/livebook/apps.ex b/lib/livebook/apps.ex index b8cbba241..b31975a49 100644 --- a/lib/livebook/apps.ex +++ b/lib/livebook/apps.ex @@ -21,10 +21,13 @@ defmodule Livebook.Apps do * `:warnings` - a list of warnings to show for the new deployment + * `:files_source` - a location to fetch notebook files from, see + `Livebook.Session.start_link/1` for more details + """ @spec deploy(Livebook.Notebook.t(), keyword()) :: {:ok, pid()} | {:error, term()} def deploy(notebook, opts \\ []) do - opts = Keyword.validate!(opts, warnings: []) + opts = Keyword.validate!(opts, warnings: [], files_source: nil) slug = notebook.app_settings.slug name = name(slug) @@ -34,25 +37,29 @@ defmodule Livebook.Apps do :global.trans({{:app_registration, name}, node()}, fn -> case :global.whereis_name(name) do :undefined -> - with {:ok, pid} <- start_app(notebook, opts[:warnings]) do + with {:ok, pid} <- start_app(notebook, opts[:warnings], opts[:files_source]) do :yes = :global.register_name(name, pid) {:ok, pid} end pid -> - App.deploy(pid, notebook, warnings: opts[:warnings]) + App.deploy(pid, notebook, + warnings: opts[:warnings], + files_source: opts[:files_source] + ) + {:ok, pid} end end) pid -> - App.deploy(pid, notebook, warnings: opts[:warnings]) + App.deploy(pid, notebook, warnings: opts[:warnings], files_source: opts[:files_source]) {:ok, pid} end end - defp start_app(notebook, warnings) do - opts = [notebook: notebook, warnings: warnings] + defp start_app(notebook, warnings, files_source) do + opts = [notebook: notebook, warnings: warnings, files_source: files_source] case DynamicSupervisor.start_child(Livebook.AppSupervisor, {App, opts}) do {:ok, pid} -> @@ -191,7 +198,9 @@ defmodule Livebook.Apps do apps_path_hub_id = Livebook.Config.apps_path_hub_id() if apps_path_hub_id == nil or apps_path_hub_id == verified_hub_id do - deploy(notebook, warnings: warnings) + notebook_file = Livebook.FileSystem.File.local(path) + files_dir = Livebook.Session.files_dir_for_notebook(notebook_file) + deploy(notebook, warnings: warnings, files_source: {:dir, files_dir}) else Logger.warning( "Skipping app deployment at #{path}. The notebook is not verified to come from hub #{apps_path_hub_id}" diff --git a/lib/livebook/session.ex b/lib/livebook/session.ex index 566cc0a87..3a5951b24 100644 --- a/lib/livebook/session.ex +++ b/lib/livebook/session.ex @@ -1288,7 +1288,8 @@ defmodule Livebook.Session do # In the initial state app settings are empty, hence not valid, # so we double-check that we can actually deploy if Notebook.AppSettings.valid?(state.data.notebook.app_settings) do - {:ok, pid} = Livebook.Apps.deploy(state.data.notebook) + files_dir = files_dir_from_state(state) + {:ok, pid} = Livebook.Apps.deploy(state.data.notebook, files_source: {:dir, files_dir}) if ref = state.deployed_app_monitor_ref do Process.demonitor(ref, [:flush]) diff --git a/test/livebook/apps_test.exs b/test/livebook/apps_test.exs index b3873a269..2326f010a 100644 --- a/test/livebook/apps_test.exs +++ b/test/livebook/apps_test.exs @@ -160,5 +160,34 @@ defmodule Livebook.AppsTest do Livebook.App.close(app.pid) end + + @tag :tmp_dir + test "deploys notebook with attachment files", %{tmp_dir: tmp_dir} do + app_path = Path.join(tmp_dir, "app.livemd") + files_path = Path.join(tmp_dir, "files") + File.mkdir_p!(files_path) + image_path = Path.join(files_path, "image.jpg") + File.write!(image_path, "content") + + File.write!(app_path, """ + + + # App + """) + + Livebook.Apps.subscribe() + + Livebook.Apps.deploy_apps_in_dir(tmp_dir) + + assert_receive {:app_created, %{slug: "app"} = app} + + session_id = Livebook.App.get_session_id(app.pid) + {:ok, session} = Livebook.Sessions.fetch_session(session_id) + + assert Livebook.FileSystem.File.resolve(session.files_dir, "image.jpg") + |> Livebook.FileSystem.File.read() == {:ok, "content"} + + Livebook.App.close(app.pid) + end end end diff --git a/test/livebook/session_test.exs b/test/livebook/session_test.exs index 7fd5aa0d1..a4c3ecd4c 100644 --- a/test/livebook/session_test.exs +++ b/test/livebook/session_test.exs @@ -1277,6 +1277,34 @@ defmodule Livebook.SessionTest do assert_receive {:operation, {:set_deployed_app_slug, _client_id, nil}} end + + test "deploys notebook with attachment files" do + session = start_session() + + %{files_dir: files_dir} = session + image_file = FileSystem.File.resolve(files_dir, "image.jpg") + :ok = FileSystem.File.write(image_file, "content") + Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}]) + + Session.subscribe(session.id) + + slug = Utils.random_short_id() + app_settings = %{Notebook.AppSettings.new() | slug: slug} + Session.set_app_settings(session.pid, app_settings) + + Apps.subscribe() + + Session.deploy_app(session.pid) + assert_receive {:app_created, %{slug: ^slug, pid: app_pid}} + + session_id = App.get_session_id(app_pid) + {:ok, session} = Livebook.Sessions.fetch_session(session_id) + + assert FileSystem.File.resolve(session.files_dir, "image.jpg") + |> FileSystem.File.read() == {:ok, "content"} + + App.close(app_pid) + end end describe "apps" do