lib/kino/fs.ex

defmodule Kino.FS do
  @moduledoc """
  Provides access to notebook files.
  """

  defmodule ForbiddenError do
    @moduledoc """
    Exception raised when access to a notebook file is forbidden.
    """

    defexception [:name]

    @impl true
    def message(exception) do
      "forbidden access to file #{inspect(exception.name)}"
    end
  end

  @doc """
  Accesses notebook file with the given name and returns a local path
  to read its contents from.

  This invocation may take a while, in case the file is downloaded
  from a URL and is not in the cache.

  > #### File operations {: .info}
  >
  > You should treat the file as read-only. To avoid unnecessary
  > copies the path may potentially be pointing to the original file,
  > in which case any write operations would be persisted. This
  > behaviour is not always the case, so you should not rely on it
  > either.
  """
  @spec file_path(String.t()) :: String.t()
  def file_path(name) when is_binary(name) do
    case Kino.Bridge.get_file_entry_path(name) do
      {:ok, path} ->
        path

      {:error, :forbidden} ->
        raise ForbiddenError, name: name

      {:error, message} when is_binary(message) ->
        raise message

      {:error, reason} when is_atom(reason) ->
        raise "failed to access file path, reason: #{inspect(reason)}"
    end
  end

  @doc """
  Accesses notebook file with the given name and returns a specification
  of the file location.

  This does not copy any files and moves the responsibility of reading
  the file to the caller. If you need to read a file directly, use
  `file_path/1`.
  """
  @spec file_spec(String.t()) :: FSS.entry()
  def file_spec(name) do
    case Kino.Bridge.get_file_entry_spec(name) do
      {:ok, spec} ->
        file_spec_to_fss(spec)

      {:error, :forbidden} ->
        raise ForbiddenError, name: name

      {:error, message} when is_binary(message) ->
        raise message

      {:error, reason} when is_atom(reason) ->
        raise "failed to access file spec, reason: #{inspect(reason)}"
    end
  end

  defp file_spec_to_fss(%{type: :local} = file_spec) do
    FSS.Local.from_path(file_spec.path)
  end

  defp file_spec_to_fss(%{type: :url} = file_spec) do
    case FSS.HTTP.parse(file_spec.url) do
      {:ok, entry} -> entry
      {:error, error} -> raise error
    end
  end

  defp file_spec_to_fss(%{type: :s3} = file_spec) do
    case FSS.S3.parse("s3:///" <> file_spec.key,
           config: [
             region: file_spec.region,
             endpoint: file_spec.bucket_url,
             access_key_id: file_spec.access_key_id,
             secret_access_key: file_spec.secret_access_key,
             # Token field is only available on Livebook v0.12 onwards
             token: Map.get(file_spec, :token)
           ]
         ) do
      {:ok, entry} -> entry
      {:error, error} -> raise error
    end
  end
end