Support file scheme when importing from URL (#706)

* Add test

* Support file scheme when importing from URL
This commit is contained in:
Jonatan Kłosko 2021-11-12 15:49:22 +01:00 committed by GitHub
parent d78a3cf865
commit 4d92aeba2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 80 additions and 30 deletions

View file

@ -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

View file

@ -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(),

View file

@ -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} ->

View file

@ -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} ->

View file

@ -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)}

View file

@ -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()