diff --git a/lib/livebook/content_loader.ex b/lib/livebook/content_loader.ex index 3ce3fc977..5151e066c 100644 --- a/lib/livebook/content_loader.ex +++ b/lib/livebook/content_loader.ex @@ -3,6 +3,11 @@ defmodule Livebook.ContentLoader do alias Livebook.Utils.HTTP + @typedoc """ + A location from where content gets loaded. + """ + @type location :: {:file, FileSystem.File.t()} | {:url, String.t()} + @doc """ Rewrite known URLs, so that they point to plain text file rather than HTML. @@ -81,4 +86,51 @@ defmodule Livebook.ContentLoader do {:error, "failed to download notebook from the given URL"} end end + + @doc """ + Loads a notebook content from the given location. + """ + @spec fetch_content_from_location(location()) :: {:ok, String.t()} | {:error, String.t()} + def fetch_content_from_location(location) + + def fetch_content_from_location({:file, file}) do + case Livebook.FileSystem.File.read(file) do + {:ok, content} -> {:ok, content} + {:error, message} -> {:error, "failed to read #{file.path}, reason: #{message}"} + end + end + + def fetch_content_from_location({:url, url}) do + url + |> rewrite_url() + |> fetch_content() + end + + @doc """ + Normalizes the given URL into a location. + """ + @spec url_to_location(String.t()) :: location() + def url_to_location(url) + + def url_to_location("file://" <> path) do + path = Path.expand(path) + file = Livebook.FileSystem.File.local(path) + {:file, file} + end + + def url_to_location(url), do: {:url, url} + + @doc """ + Resolves the given relative path with regard to the given location. + """ + @spec resolve_location(location(), String.t()) :: location() + def resolve_location(location, relative_path) + + def resolve_location({:url, url}, relative_path) do + {:url, Livebook.Utils.expand_url(url, relative_path)} + end + + def resolve_location({:file, file}, relative_path) do + {:file, Livebook.FileSystem.File.resolve(file, relative_path)} + end end diff --git a/lib/livebook/session.ex b/lib/livebook/session.ex index 3818d475e..35bb99cb9 100644 --- a/lib/livebook/session.ex +++ b/lib/livebook/session.ex @@ -58,7 +58,7 @@ defmodule Livebook.Session do @type t :: %__MODULE__{ id: id(), pid: pid(), - origin: {:file, FileSystem.File.t()} | {:url, String.t()} | nil, + origin: Livebook.ContentLoader.location() | nil, notebook_name: String.t(), file: FileSystem.File.t() | nil, images_dir: FileSystem.File.t(), diff --git a/lib/livebook_web/live/home_live.ex b/lib/livebook_web/live/home_live.ex index cc7367d67..761374c83 100644 --- a/lib/livebook_web/live/home_live.ex +++ b/lib/livebook_web/live/home_live.ex @@ -159,12 +159,13 @@ defmodule LivebookWeb.HomeLive do end def handle_params(%{"url" => url}, _url, %{assigns: %{live_action: :public_import}} = socket) do - url - |> Livebook.ContentLoader.rewrite_url() - |> Livebook.ContentLoader.fetch_content() + origin = Livebook.ContentLoader.url_to_location(url) + + origin + |> Livebook.ContentLoader.fetch_content_from_location() |> case do {:ok, content} -> - socket = import_content(socket, content, origin: {:url, url}) + socket = import_content(socket, content, origin: origin) {:noreply, socket} {:error, _message} -> diff --git a/lib/livebook_web/live/home_live/import_url_component.ex b/lib/livebook_web/live/home_live/import_url_component.ex index bf39e38c8..15e8ab055 100644 --- a/lib/livebook_web/live/home_live/import_url_component.ex +++ b/lib/livebook_web/live/home_live/import_url_component.ex @@ -1,7 +1,7 @@ defmodule LivebookWeb.HomeLive.ImportUrlComponent do use LivebookWeb, :live_component - alias Livebook.{ContentLoader, Utils} + alias Livebook.Utils @impl true def mount(socket) do @@ -58,12 +58,13 @@ defmodule LivebookWeb.HomeLive.ImportUrlComponent do end defp do_import(socket, url) do - url - |> ContentLoader.rewrite_url() - |> ContentLoader.fetch_content() + origin = Livebook.ContentLoader.url_to_location(url) + + origin + |> Livebook.ContentLoader.fetch_content_from_location() |> case do {:ok, content} -> - send(self(), {:import_content, content, [origin: {:url, url}]}) + send(self(), {:import_content, content, [origin: origin]}) socket {:error, message} -> diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 8cc9d12bb..60ef6f8a4 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -6,7 +6,7 @@ defmodule LivebookWeb.SessionLive do import Livebook.Utils, only: [access_by_id: 1] alias LivebookWeb.SidebarHelpers - alias Livebook.{Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown, FileSystem} + alias Livebook.{Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown} alias Livebook.Notebook.Cell alias Livebook.JSInterop @@ -961,11 +961,7 @@ defmodule LivebookWeb.SessionLive do |> redirect_to_self() resolution_location -> - origin = - case resolution_location do - {:url, url} -> {:url, Livebook.Utils.expand_url(url, relative_path)} - {:file, file} -> {:file, FileSystem.File.resolve(file, relative_path)} - end + origin = Livebook.ContentLoader.resolve_location(resolution_location, relative_path) case session_id_by_location(origin) do {:ok, session_id} -> @@ -996,7 +992,7 @@ defmodule LivebookWeb.SessionLive do defp location(%{origin: origin}), do: origin defp open_notebook(socket, origin) do - case load_content(origin) do + case Livebook.ContentLoader.fetch_content_from_location(origin) do {:ok, content} -> {notebook, messages} = Livebook.LiveMarkdown.Import.notebook_from_markdown(content) @@ -1015,19 +1011,6 @@ defmodule LivebookWeb.SessionLive do end end - defp load_content({:file, file}) do - case FileSystem.File.read(file) do - {:ok, content} -> {:ok, content} - {:error, message} -> {:error, "failed to read #{file.path}, reason: #{message}"} - end - end - - defp load_content({:url, url}) do - url - |> Livebook.ContentLoader.rewrite_url() - |> Livebook.ContentLoader.fetch_content() - end - defp file_and_notebook(fork?, origin, notebook) defp file_and_notebook(false, {:file, file}, notebook), do: {file, notebook} defp file_and_notebook(true, {:file, _file}, notebook), do: {nil, Notebook.forked(notebook)} diff --git a/test/livebook_web/live/home_live_test.exs b/test/livebook_web/live/home_live_test.exs index 57cccd3d5..717bc5267 100644 --- a/test/livebook_web/live/home_live_test.exs +++ b/test/livebook_web/live/home_live_test.exs @@ -264,6 +264,19 @@ defmodule LivebookWeb.HomeLiveTest do assert render(view) =~ "My notebook" end + @tag :tmp_dir + test "imports notebook from local file URL", %{conn: conn, tmp_dir: tmp_dir} do + notebook_path = Path.join(tmp_dir, "notebook.livemd") + File.write!(notebook_path, "# My notebook") + notebook_url = "file://" <> notebook_path + + assert {:error, {:live_redirect, %{to: to}}} = + live(conn, "/import?url=#{URI.encode_www_form(notebook_url)}") + + {:ok, view, _} = live(conn, to) + assert render(view) =~ "My notebook" + end + test "redirects to the import form on error", %{conn: conn} do bypass = Bypass.open()