defmodule Livebook.FileSystem.Git do use Ecto.Schema import Ecto.Changeset alias Livebook.FileSystem # File system backed by an Git repository. @type t :: %__MODULE__{ id: String.t(), repo_url: String.t(), branch: String.t(), key: String.t(), external_id: String.t() | nil, hub_id: String.t() } embedded_schema do field :repo_url, :string field :branch, :string field :key, :string, redact: true field :external_id, :string field :hub_id, :string end @doc """ Returns an `%Ecto.Changeset{}` for tracking file system changes. """ @spec change_file_system(t(), map()) :: Ecto.Changeset.t() def change_file_system(git, attrs \\ %{}) do changeset(git, attrs) end defp changeset(git, attrs) do git |> cast(attrs, [:repo_url, :branch, :key, :external_id, :hub_id]) |> validate_format(:repo_url, ~r/^git@[\w\.\-]+:(?:v\d+\/)?[\w\.\-\/]+\.git$/, message: "must be a valid repo URL" ) |> validate_required([:repo_url, :branch, :key, :hub_id]) |> put_id() end defp put_id(changeset) do hub_id = get_field(changeset, :hub_id) repo_url = get_field(changeset, :repo_url) if get_field(changeset, :id) do changeset else put_change(changeset, :id, FileSystem.Utils.id("git", hub_id, repo_url)) end end @doc false def git_dir(%__MODULE__{id: id}), do: Path.join(Livebook.Config.tmp_path(), id) @doc false def key_path(%__MODULE__{id: id}), do: Path.join(Livebook.Config.tmp_path(), "#{id}_key") end defimpl Livebook.FileSystem, for: Livebook.FileSystem.Git do alias Livebook.FileSystem alias Livebook.FileSystem.Git def type(_file_system) do :global end def default_path(_file_system) do "/" end def list(file_system, path, _recursive) do with {:ok, []} <- Git.Client.list_files(file_system, path) do FileSystem.Utils.posix_error(:enoent) end end def read(file_system, path) do Git.Client.read_file(file_system, path) end def write(_file_system, _path, _content), do: raise("not implemented") def access(_file_system, _path) do {:ok, :read} end def create_dir(_file_system, _path), do: raise("not implemented") def remove(_file_system, _path), do: raise("not implemented") def copy(_file_system, _source_path, _destination_path), do: raise("not implemented") def rename(_file_system, _source_path, _destination_path), do: raise("not implemented") def etag_for(file_system, path) do FileSystem.Utils.assert_regular_path!(path) Git.Client.etag(file_system, path) end def exists?(file_system, path) do Git.Client.exists?(file_system, path) end def resolve_path(_file_system, dir_path, subject) do FileSystem.Utils.resolve_unix_like_path(dir_path, subject) end def write_stream_init(_file_system, _path, _opts), do: raise("not implemented") def write_stream_chunk(_file_system, _state, _chunk), do: raise("not implemented") def write_stream_finish(_file_system, _state), do: raise("not implemented") def write_stream_halt(_file_system, _state), do: raise("not implemented") def read_stream_into(file_system, path, collectable) do try do Git.Client.stream_file(file_system, path, collectable) rescue error in File.Error -> FileSystem.Utils.posix_error(error.reason) end end def load(file_system, %{"hub_id" => _} = fields) do load(file_system, %{ id: fields["id"], repo_url: fields["repo_url"], branch: fields["branch"], key: fields["key"], external_id: fields["external_id"], hub_id: fields["hub_id"] }) end def load(file_system, fields) do %{ file_system | id: fields.id, repo_url: fields.repo_url, branch: fields.branch, key: fields.key, external_id: fields.external_id, hub_id: fields.hub_id } end def dump(file_system) do file_system |> Map.from_struct() |> Map.take([:id, :repo_url, :branch, :key, :external_id, :hub_id]) end def external_metadata(file_system) do %{name: file_system.repo_url, error_field: "repo_url"} end def mount(file_system) do if mounted?(file_system) do FileSystem.Git.Client.fetch(file_system) else FileSystem.Git.Client.init(file_system) end end def unmount(file_system) do path = FileSystem.Git.git_dir(file_system) key_path = FileSystem.Git.key_path(file_system) if File.exists?(key_path) do File.rm!(key_path) end case File.rm_rf(path) do {:ok, _} -> :ok {:error, reason, _file} -> FileSystem.Utils.posix_error(reason) end end defp mounted?(file_system) do file_system |> FileSystem.Git.git_dir() |> File.exists?() end end