chore: cleaned up bang-Operator usage
This commit is contained in:
@@ -31,13 +31,22 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.MCPConfig do
|
||||
def toggle_mcp_agent(socket, agent, reload, append_output) do
|
||||
case find_mcp_agent(agent) do
|
||||
%{id: agent_id, supported?: true} = config ->
|
||||
if mcp_configured?(config) do
|
||||
{:ok, _payload} = AgentConfig.remove_from_config(agent_id)
|
||||
reload.(socket, socket.assigns.workbench)
|
||||
else
|
||||
install_root = Application.app_dir(:bds)
|
||||
{:ok, _payload} = AgentConfig.add_to_config(agent_id, install_root: install_root)
|
||||
reload.(socket, socket.assigns.workbench)
|
||||
result =
|
||||
if mcp_configured?(config) do
|
||||
AgentConfig.remove_from_config(agent_id)
|
||||
else
|
||||
install_root = Application.app_dir(:bds)
|
||||
AgentConfig.add_to_config(agent_id, install_root: install_root)
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, _payload} ->
|
||||
reload.(socket, socket.assigns.workbench)
|
||||
|
||||
{:error, reason} ->
|
||||
socket
|
||||
|> append_output.(translated("MCP"), format_config_error(reason), nil, "error")
|
||||
|> reload.(socket.assigns.workbench)
|
||||
end
|
||||
|
||||
_other ->
|
||||
@@ -63,21 +72,34 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.MCPConfig do
|
||||
_error -> nil
|
||||
end
|
||||
|
||||
defp format_config_error({:read_config, path, reason}) do
|
||||
translated("Could not read MCP config %{path}: %{reason}", path: path, reason: inspect(reason))
|
||||
end
|
||||
|
||||
defp format_config_error({:write_config, path, reason}) do
|
||||
translated("Could not write MCP config %{path}: %{reason}", path: path, reason: inspect(reason))
|
||||
end
|
||||
|
||||
defp format_config_error({:create_config_dir, path, reason}) do
|
||||
translated("Could not create MCP config folder %{path}: %{reason}", path: path, reason: inspect(reason))
|
||||
end
|
||||
|
||||
defp format_config_error({:decode_config, path, _reason}) do
|
||||
translated("Could not parse MCP config %{path}", path: path)
|
||||
end
|
||||
|
||||
defp mcp_configured?(%{supported?: false}), do: false
|
||||
|
||||
defp mcp_configured?(%{id: agent_id}) do
|
||||
path = AgentConfig.config_path(agent_id, System.user_home!())
|
||||
|
||||
if File.exists?(path) do
|
||||
path
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
|> mcp_server_present?(agent_id)
|
||||
with true <- File.exists?(path),
|
||||
{:ok, contents} <- File.read(path),
|
||||
{:ok, config} <- Jason.decode(contents) do
|
||||
mcp_server_present?(config, agent_id)
|
||||
else
|
||||
false
|
||||
_other -> false
|
||||
end
|
||||
rescue
|
||||
_error -> false
|
||||
end
|
||||
|
||||
defp mcp_config_path(%{supported?: false}), do: nil
|
||||
|
||||
@@ -155,10 +155,10 @@ defmodule BDS.Generation do
|
||||
|
||||
@spec apply_validation(String.t(), [section()] | map()) :: {:ok, map()} | {:error, term()}
|
||||
def apply_validation(project_id, sections) when is_binary(project_id) and is_list(sections) do
|
||||
with {:ok, plan} <- plan_generation(project_id, sections) do
|
||||
with {:ok, plan} <- plan_generation(project_id, sections),
|
||||
{:ok, actual_files} <- disk_generated_files(project_id) do
|
||||
expected_outputs = build_outputs(plan)
|
||||
expected_paths = MapSet.new(Enum.map(expected_outputs, &elem(&1, 0)))
|
||||
actual_files = disk_generated_files(project_id)
|
||||
project = Projects.get_project!(project_id)
|
||||
now = Persistence.now_ms()
|
||||
|
||||
@@ -186,24 +186,26 @@ defmodule BDS.Generation do
|
||||
end
|
||||
end)
|
||||
|
||||
disk_generated_files(project_id)
|
||||
|> Map.keys()
|
||||
|> Enum.filter(fn relative_path ->
|
||||
path_section(relative_path) in plan.sections and not MapSet.member?(expected_paths, relative_path)
|
||||
end)
|
||||
|> Enum.each(fn relative_path ->
|
||||
_ = File.rm(output_path(project, relative_path))
|
||||
with {:ok, generated_files_on_disk} <- disk_generated_files(project_id) do
|
||||
generated_files_on_disk
|
||||
|> Map.keys()
|
||||
|> Enum.filter(fn relative_path ->
|
||||
path_section(relative_path) in plan.sections and not MapSet.member?(expected_paths, relative_path)
|
||||
end)
|
||||
|> Enum.each(fn relative_path ->
|
||||
_ = File.rm(output_path(project, relative_path))
|
||||
|
||||
Repo.delete_all(
|
||||
from generated_file in GeneratedFileHash,
|
||||
where:
|
||||
generated_file.project_id == ^project_id and
|
||||
generated_file.relative_path == ^relative_path
|
||||
)
|
||||
end)
|
||||
Repo.delete_all(
|
||||
from generated_file in GeneratedFileHash,
|
||||
where:
|
||||
generated_file.project_id == ^project_id and
|
||||
generated_file.relative_path == ^relative_path
|
||||
)
|
||||
end)
|
||||
|
||||
{:ok, generated_files} = list_generated_files(project_id)
|
||||
{:ok, %{sections: plan.sections, generated_files: generated_files}}
|
||||
{:ok, generated_files} = list_generated_files(project_id)
|
||||
{:ok, %{sections: plan.sections, generated_files: generated_files}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -521,18 +523,20 @@ defmodule BDS.Generation do
|
||||
|> Path.join("**/*")
|
||||
|> Path.wildcard(match_dot: false)
|
||||
|> Enum.filter(&File.regular?/1)
|
||||
|> Enum.map(fn path ->
|
||||
|> Enum.reduce_while({:ok, %{}}, fn path, {:ok, files} ->
|
||||
relative_path = Path.relative_to(path, html_root)
|
||||
|
||||
{relative_path,
|
||||
path
|
||||
|> File.read!()
|
||||
|> sha256()}
|
||||
case File.read(path) do
|
||||
{:ok, contents} ->
|
||||
{:cont, {:ok, Map.put(files, relative_path, sha256(contents))}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:halt, {:error, {:read_generated_file, path, reason}}}
|
||||
end
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
{:error, :enoent} ->
|
||||
%{}
|
||||
{:ok, %{}}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,26 +9,24 @@ defmodule BDS.MCP.AgentConfig do
|
||||
command = Keyword.get(opts, :command, default_command(opts))
|
||||
args = Keyword.get(opts, :args, default_args(opts))
|
||||
|
||||
File.mkdir_p!(Path.dirname(config_path))
|
||||
|
||||
config = read_config(config_path)
|
||||
updated = merge_config(agent, config, command, args)
|
||||
File.write!(config_path, Jason.encode!(updated, pretty: true))
|
||||
|
||||
{:ok, %{config_path: config_path, server_name: @server_name}}
|
||||
with :ok <- ensure_config_dir(config_path),
|
||||
{:ok, config} <- read_config(config_path),
|
||||
updated <- merge_config(agent, config, command, args),
|
||||
:ok <- write_config(config_path, updated) do
|
||||
{:ok, %{config_path: config_path, server_name: @server_name}}
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_config(agent, opts \\ []) when is_atom(agent) and is_list(opts) do
|
||||
home_dir = Keyword.get(opts, :home_dir, System.user_home!())
|
||||
config_path = config_path(agent, home_dir)
|
||||
|
||||
File.mkdir_p!(Path.dirname(config_path))
|
||||
|
||||
config = read_config(config_path)
|
||||
updated = remove_server_entry(agent, config)
|
||||
File.write!(config_path, Jason.encode!(updated, pretty: true))
|
||||
|
||||
{:ok, %{config_path: config_path, server_name: @server_name}}
|
||||
with :ok <- ensure_config_dir(config_path),
|
||||
{:ok, config} <- read_config(config_path),
|
||||
updated <- remove_server_entry(agent, config),
|
||||
:ok <- write_config(config_path, updated) do
|
||||
{:ok, %{config_path: config_path, server_name: @server_name}}
|
||||
end
|
||||
end
|
||||
|
||||
def config_path(:claude_code, home_dir), do: Path.join(home_dir, ".claude.json")
|
||||
@@ -53,12 +51,39 @@ defmodule BDS.MCP.AgentConfig do
|
||||
defp default_args(_opts), do: []
|
||||
|
||||
defp read_config(path) do
|
||||
if File.exists?(path) do
|
||||
path
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
else
|
||||
%{}
|
||||
cond do
|
||||
File.exists?(path) ->
|
||||
with {:ok, contents} <- read_config_file(path),
|
||||
{:ok, config} <- Jason.decode(contents) do
|
||||
{:ok, config}
|
||||
else
|
||||
{:error, %Jason.DecodeError{} = reason} -> {:error, {:decode_config, path, reason}}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:ok, %{}}
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_config_dir(config_path) do
|
||||
case File.mkdir_p(Path.dirname(config_path)) do
|
||||
:ok -> :ok
|
||||
{:error, reason} -> {:error, {:create_config_dir, Path.dirname(config_path), reason}}
|
||||
end
|
||||
end
|
||||
|
||||
defp read_config_file(path) do
|
||||
case File.read(path) do
|
||||
{:ok, contents} -> {:ok, contents}
|
||||
{:error, reason} -> {:error, {:read_config, path, reason}}
|
||||
end
|
||||
end
|
||||
|
||||
defp write_config(path, config) do
|
||||
case File.write(path, Jason.encode!(config, pretty: true)) do
|
||||
:ok -> :ok
|
||||
{:error, reason} -> {:error, {:write_config, path, reason}}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -19,64 +19,77 @@ defmodule BDS.Media.Rebuilder do
|
||||
|
||||
@type rebuild_opts :: keyword()
|
||||
|
||||
@spec rebuild_media_from_files(String.t(), rebuild_opts()) :: {:ok, [Media.t()]}
|
||||
@spec rebuild_media_from_files(String.t(), rebuild_opts()) :: {:ok, [Media.t()]} | {:error, term()}
|
||||
def rebuild_media_from_files(project_id, opts \\ []) do
|
||||
project = Projects.get_project!(project_id)
|
||||
on_progress = progress_callback(opts)
|
||||
|
||||
canonical_sidecars =
|
||||
project
|
||||
|> Projects.project_data_dir()
|
||||
|> Path.join("media")
|
||||
media_dir = project |> Projects.project_data_dir() |> Path.join("media")
|
||||
|
||||
canonical_results =
|
||||
media_dir
|
||||
|> list_matching_files("*.meta")
|
||||
|> Enum.filter(&Sidecars.canonical_sidecar?/1)
|
||||
|> Enum.filter(&Sidecars.binary_exists_for_sidecar?/1)
|
||||
|> Rebuild.parallel_map(&Sidecars.parse_canonical_sidecar(project, &1))
|
||||
|
||||
translation_sidecars =
|
||||
project
|
||||
|> Projects.project_data_dir()
|
||||
|> Path.join("media")
|
||||
|> list_matching_files("*.meta")
|
||||
|> Enum.filter(&Sidecars.translation_sidecar?/1)
|
||||
|> Rebuild.parallel_map(&Sidecars.parse_translation_sidecar(&1))
|
||||
with {:ok, canonical_sidecars} <- collect_sidecars(canonical_results) do
|
||||
translation_results =
|
||||
media_dir
|
||||
|> list_matching_files("*.meta")
|
||||
|> Enum.filter(&Sidecars.translation_sidecar?/1)
|
||||
|> Rebuild.parallel_map(&Sidecars.parse_translation_sidecar(&1))
|
||||
|
||||
total_files = length(canonical_sidecars) + length(translation_sidecars)
|
||||
:ok = report_rebuild_started(on_progress, total_files, "media files")
|
||||
with {:ok, translation_sidecars} <- collect_sidecars(translation_results) do
|
||||
total_files = length(canonical_sidecars) + length(translation_sidecars)
|
||||
:ok = report_rebuild_started(on_progress, total_files, "media files")
|
||||
|
||||
media_items =
|
||||
canonical_sidecars
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {sidecar, index} ->
|
||||
media = Sidecars.upsert_media_from_sidecar(project, sidecar, sync_search: false)
|
||||
:ok = report_rebuild_progress(on_progress, index, total_files, "media files")
|
||||
media
|
||||
end)
|
||||
media_items =
|
||||
canonical_sidecars
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {sidecar, index} ->
|
||||
media = Sidecars.upsert_media_from_sidecar(project, sidecar, sync_search: false)
|
||||
:ok = report_rebuild_progress(on_progress, index, total_files, "media files")
|
||||
media
|
||||
end)
|
||||
|
||||
canonical_media_by_binary_path =
|
||||
Map.new(media_items, fn media ->
|
||||
{Path.join(Projects.project_data_dir(project), media.file_path), media}
|
||||
end)
|
||||
canonical_media_by_binary_path =
|
||||
Map.new(media_items, fn media ->
|
||||
{Path.join(Projects.project_data_dir(project), media.file_path), media}
|
||||
end)
|
||||
|
||||
translation_sidecars
|
||||
|> Enum.with_index(length(canonical_sidecars) + 1)
|
||||
|> Enum.each(fn {sidecar, index} ->
|
||||
Sidecars.upsert_translation_from_sidecar(project, canonical_media_by_binary_path, sidecar,
|
||||
sync_search: false
|
||||
)
|
||||
translation_sidecars
|
||||
|> Enum.with_index(length(canonical_sidecars) + 1)
|
||||
|> Enum.each(fn {sidecar, index} ->
|
||||
Sidecars.upsert_translation_from_sidecar(project, canonical_media_by_binary_path, sidecar,
|
||||
sync_search: false
|
||||
)
|
||||
|
||||
:ok = report_rebuild_progress(on_progress, index, total_files, "media files")
|
||||
end)
|
||||
:ok = report_rebuild_progress(on_progress, index, total_files, "media files")
|
||||
end)
|
||||
|
||||
if Keyword.get(opts, :reindex_search, true) do
|
||||
:ok = report_rebuild_phase(on_progress, 0.99, "Refreshing media search index")
|
||||
if Keyword.get(opts, :reindex_search, true) do
|
||||
:ok = report_rebuild_phase(on_progress, 0.99, "Refreshing media search index")
|
||||
|
||||
:ok =
|
||||
Search.reindex_media(project.id,
|
||||
on_progress: scaled_progress_reporter(on_progress, 0.99, 1.0)
|
||||
)
|
||||
:ok =
|
||||
Search.reindex_media(project.id,
|
||||
on_progress: scaled_progress_reporter(on_progress, 0.99, 1.0)
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, media_items}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, media_items}
|
||||
defp collect_sidecars(results) do
|
||||
Enum.reduce_while(results, {:ok, []}, fn
|
||||
{:ok, sidecar}, {:ok, sidecars} -> {:cont, {:ok, [sidecar | sidecars]}}
|
||||
{:error, reason}, {:ok, _sidecars} -> {:halt, {:error, reason}}
|
||||
end)
|
||||
|> case do
|
||||
{:ok, sidecars} -> {:ok, Enum.reverse(sidecars)}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -67,28 +67,35 @@ defmodule BDS.Media.Sidecars do
|
||||
)
|
||||
end
|
||||
|
||||
@spec parse_canonical_sidecar(BDS.Projects.Project.t(), Path.t()) :: map()
|
||||
@spec parse_canonical_sidecar(BDS.Projects.Project.t(), Path.t()) ::
|
||||
{:ok, map()} | {:error, {:read_sidecar, Path.t(), File.posix()}}
|
||||
def parse_canonical_sidecar(project, sidecar_path) do
|
||||
{:ok, fields} = sidecar_path |> File.read!() |> Sidecar.parse_document()
|
||||
relative_sidecar_path = Path.relative_to(sidecar_path, Projects.project_data_dir(project))
|
||||
relative_file_path = String.trim_trailing(relative_sidecar_path, ".meta")
|
||||
with {:ok, contents} <- read_sidecar(sidecar_path),
|
||||
{:ok, fields} <- Sidecar.parse_document(contents) do
|
||||
relative_sidecar_path = Path.relative_to(sidecar_path, Projects.project_data_dir(project))
|
||||
relative_file_path = String.trim_trailing(relative_sidecar_path, ".meta")
|
||||
|
||||
%{
|
||||
fields: fields,
|
||||
relative_sidecar_path: relative_sidecar_path,
|
||||
relative_file_path: relative_file_path,
|
||||
filename: Path.basename(relative_file_path)
|
||||
}
|
||||
{:ok,
|
||||
%{
|
||||
fields: fields,
|
||||
relative_sidecar_path: relative_sidecar_path,
|
||||
relative_file_path: relative_file_path,
|
||||
filename: Path.basename(relative_file_path)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_translation_sidecar(Path.t()) :: map()
|
||||
@spec parse_translation_sidecar(Path.t()) ::
|
||||
{:ok, map()} | {:error, {:read_sidecar, Path.t(), File.posix()}}
|
||||
def parse_translation_sidecar(sidecar_path) do
|
||||
{:ok, fields} = sidecar_path |> File.read!() |> Sidecar.parse_document()
|
||||
|
||||
%{
|
||||
fields: fields,
|
||||
binary_path: binary_path_for_translation_sidecar(sidecar_path)
|
||||
}
|
||||
with {:ok, contents} <- read_sidecar(sidecar_path),
|
||||
{:ok, fields} <- Sidecar.parse_document(contents) do
|
||||
{:ok,
|
||||
%{
|
||||
fields: fields,
|
||||
binary_path: binary_path_for_translation_sidecar(sidecar_path)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
@spec upsert_media_from_sidecar(BDS.Projects.Project.t(), map(), keyword()) :: Media.t()
|
||||
@@ -192,10 +199,12 @@ defmodule BDS.Media.Sidecars do
|
||||
project = Projects.get_project!(media.project_id)
|
||||
sidecar_path = Path.join(Projects.project_data_dir(project), media.sidecar_path)
|
||||
|
||||
if File.exists?(sidecar_path) do
|
||||
{:ok, upsert_media_from_sidecar(project, parse_canonical_sidecar(project, sidecar_path), sync_search: true)}
|
||||
else
|
||||
{:error, :not_found}
|
||||
case parse_existing_canonical_sidecar(project, sidecar_path) do
|
||||
{:ok, sidecar} ->
|
||||
{:ok, upsert_media_from_sidecar(project, sidecar, sync_search: true)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -232,23 +241,23 @@ defmodule BDS.Media.Sidecars do
|
||||
translation_sidecar_path(media, translation.language)
|
||||
)
|
||||
|
||||
if File.exists?(sidecar_path) do
|
||||
sidecar = parse_translation_sidecar(sidecar_path)
|
||||
case parse_existing_translation_sidecar(sidecar_path) do
|
||||
{:ok, sidecar} ->
|
||||
case BDS.Media.upsert_media_translation(
|
||||
media.id,
|
||||
DocumentFields.fetch!(sidecar.fields, "language"),
|
||||
%{
|
||||
title: DocumentFields.get(sidecar.fields, "title"),
|
||||
alt: DocumentFields.get(sidecar.fields, "alt"),
|
||||
caption: DocumentFields.get(sidecar.fields, "caption")
|
||||
}
|
||||
) do
|
||||
{:ok, updated_translation} -> {:ok, updated_translation}
|
||||
error -> error
|
||||
end
|
||||
|
||||
case BDS.Media.upsert_media_translation(
|
||||
media.id,
|
||||
DocumentFields.fetch!(sidecar.fields, "language"),
|
||||
%{
|
||||
title: DocumentFields.get(sidecar.fields, "title"),
|
||||
alt: DocumentFields.get(sidecar.fields, "alt"),
|
||||
caption: DocumentFields.get(sidecar.fields, "caption")
|
||||
}
|
||||
) do
|
||||
{:ok, updated_translation} -> {:ok, updated_translation}
|
||||
error -> error
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -259,10 +268,12 @@ defmodule BDS.Media.Sidecars do
|
||||
project = Projects.get_project!(project_id)
|
||||
sidecar_path = Path.join(Projects.project_data_dir(project), relative_path)
|
||||
|
||||
if File.exists?(sidecar_path) do
|
||||
{:ok, upsert_media_from_sidecar(project, parse_canonical_sidecar(project, sidecar_path), sync_search: true)}
|
||||
else
|
||||
{:error, :not_found}
|
||||
case parse_existing_canonical_sidecar(project, sidecar_path) do
|
||||
{:ok, sidecar} ->
|
||||
{:ok, upsert_media_from_sidecar(project, sidecar, sync_search: true)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -272,35 +283,35 @@ defmodule BDS.Media.Sidecars do
|
||||
project = Projects.get_project!(project_id)
|
||||
sidecar_path = Path.join(Projects.project_data_dir(project), relative_path)
|
||||
|
||||
if File.exists?(sidecar_path) do
|
||||
sidecar = parse_translation_sidecar(sidecar_path)
|
||||
case parse_existing_translation_sidecar(sidecar_path) do
|
||||
{:ok, sidecar} ->
|
||||
case Repo.get(Media, DocumentFields.get(sidecar.fields, "translationFor")) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
|
||||
case Repo.get(Media, DocumentFields.get(sidecar.fields, "translationFor")) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
media ->
|
||||
case Repo.get_by(Translation,
|
||||
translation_for: media.id,
|
||||
language: DocumentFields.fetch!(sidecar.fields, "language")
|
||||
) do
|
||||
nil ->
|
||||
BDS.Media.upsert_media_translation(
|
||||
media.id,
|
||||
DocumentFields.fetch!(sidecar.fields, "language"),
|
||||
%{
|
||||
title: DocumentFields.get(sidecar.fields, "title"),
|
||||
alt: DocumentFields.get(sidecar.fields, "alt"),
|
||||
caption: DocumentFields.get(sidecar.fields, "caption")
|
||||
}
|
||||
)
|
||||
|
||||
media ->
|
||||
case Repo.get_by(Translation,
|
||||
translation_for: media.id,
|
||||
language: DocumentFields.fetch!(sidecar.fields, "language")
|
||||
) do
|
||||
nil ->
|
||||
BDS.Media.upsert_media_translation(
|
||||
media.id,
|
||||
DocumentFields.fetch!(sidecar.fields, "language"),
|
||||
%{
|
||||
title: DocumentFields.get(sidecar.fields, "title"),
|
||||
alt: DocumentFields.get(sidecar.fields, "alt"),
|
||||
caption: DocumentFields.get(sidecar.fields, "caption")
|
||||
}
|
||||
)
|
||||
_translation ->
|
||||
{:error, :conflict}
|
||||
end
|
||||
end
|
||||
|
||||
_translation ->
|
||||
{:error, :conflict}
|
||||
end
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -326,4 +337,27 @@ defmodule BDS.Media.Sidecars do
|
||||
|> String.trim_trailing(".meta")
|
||||
|> File.exists?()
|
||||
end
|
||||
|
||||
defp parse_existing_canonical_sidecar(project, sidecar_path) do
|
||||
if File.exists?(sidecar_path) do
|
||||
parse_canonical_sidecar(project, sidecar_path)
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_existing_translation_sidecar(sidecar_path) do
|
||||
if File.exists?(sidecar_path) do
|
||||
parse_translation_sidecar(sidecar_path)
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp read_sidecar(sidecar_path) do
|
||||
case File.read(sidecar_path) do
|
||||
{:ok, contents} -> {:ok, contents}
|
||||
{:error, reason} -> {:error, {:read_sidecar, sidecar_path, reason}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -193,7 +193,7 @@ defmodule BDS.Posts do
|
||||
end
|
||||
end
|
||||
|
||||
@spec rebuild_posts_from_files(String.t(), rebuild_opts()) :: {:ok, [Post.t()]}
|
||||
@spec rebuild_posts_from_files(String.t(), rebuild_opts()) :: {:ok, [Post.t()]} | {:error, term()}
|
||||
defdelegate rebuild_posts_from_files(project_id, opts \\ []), to: RebuildFromFiles
|
||||
|
||||
@spec discard_post_changes(String.t()) ::
|
||||
@@ -211,9 +211,12 @@ defmodule BDS.Posts do
|
||||
full_path = Path.join(Projects.project_data_dir(project), post.file_path)
|
||||
|
||||
if File.exists?(full_path) do
|
||||
restored_post = RebuildFromFiles.upsert_post_from_file(post.project_id, project, full_path)
|
||||
:ok = PostLinks.sync_post_links(restored_post)
|
||||
{:ok, restored_post}
|
||||
with {:ok, restored_post} <- RebuildFromFiles.upsert_post_from_file(post.project_id, project, full_path) do
|
||||
:ok = PostLinks.sync_post_links(restored_post)
|
||||
{:ok, restored_post}
|
||||
else
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
@@ -259,9 +262,12 @@ defmodule BDS.Posts do
|
||||
full_path = Path.join(Projects.project_data_dir(project), post.file_path)
|
||||
|
||||
if File.exists?(full_path) do
|
||||
repaired_post = RebuildFromFiles.upsert_post_from_file(post.project_id, project, full_path)
|
||||
:ok = PostLinks.sync_post_links(repaired_post)
|
||||
{:ok, repaired_post}
|
||||
with {:ok, repaired_post} <- RebuildFromFiles.upsert_post_from_file(post.project_id, project, full_path) do
|
||||
:ok = PostLinks.sync_post_links(repaired_post)
|
||||
{:ok, repaired_post}
|
||||
else
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
@@ -14,64 +14,66 @@ defmodule BDS.Posts.RebuildFromFiles do
|
||||
alias BDS.Repo
|
||||
alias BDS.Search
|
||||
|
||||
@spec rebuild_posts_from_files(String.t(), keyword()) :: {:ok, [Post.t()]}
|
||||
@spec rebuild_posts_from_files(String.t(), keyword()) :: {:ok, [Post.t()]} | {:error, term()}
|
||||
def rebuild_posts_from_files(project_id, opts \\ []) do
|
||||
project = Projects.get_project!(project_id)
|
||||
on_progress = progress_callback(opts)
|
||||
|
||||
rebuild_files =
|
||||
rebuild_results =
|
||||
project
|
||||
|> Projects.project_data_dir()
|
||||
|> Path.join("posts")
|
||||
|> TranslationValidation.list_matching_files("*.md")
|
||||
|> Rebuild.parallel_map(&parse_rebuild_file(project, &1))
|
||||
|
||||
total_files = length(rebuild_files)
|
||||
:ok = report_rebuild_started(on_progress, total_files, "post files")
|
||||
with {:ok, rebuild_files} <- collect_rebuild_files(rebuild_results) do
|
||||
total_files = length(rebuild_files)
|
||||
:ok = report_rebuild_started(on_progress, total_files, "post files")
|
||||
|
||||
{translation_files, post_files} =
|
||||
Enum.split_with(rebuild_files, &TranslationValidation.translation_rebuild_file?/1)
|
||||
{translation_files, post_files} =
|
||||
Enum.split_with(rebuild_files, &TranslationValidation.translation_rebuild_file?/1)
|
||||
|
||||
posts =
|
||||
post_files
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {file, index} ->
|
||||
post =
|
||||
upsert_post_from_rebuild_file(project_id, file,
|
||||
sync_search: false,
|
||||
sync_embeddings: false
|
||||
)
|
||||
posts =
|
||||
post_files
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {file, index} ->
|
||||
post =
|
||||
upsert_post_from_rebuild_file(project_id, file,
|
||||
sync_search: false,
|
||||
sync_embeddings: false
|
||||
)
|
||||
|
||||
:ok = report_rebuild_progress(on_progress, index, total_files, "post files")
|
||||
post
|
||||
end)
|
||||
|
||||
translation_files
|
||||
|> Enum.with_index(length(post_files) + 1)
|
||||
|> Enum.each(fn {file, index} ->
|
||||
upsert_post_translation_from_rebuild_file(project_id, file, sync_search: false)
|
||||
:ok = report_rebuild_progress(on_progress, index, total_files, "post files")
|
||||
post
|
||||
end)
|
||||
|
||||
translation_files
|
||||
|> Enum.with_index(length(post_files) + 1)
|
||||
|> Enum.each(fn {file, index} ->
|
||||
upsert_post_translation_from_rebuild_file(project_id, file, sync_search: false)
|
||||
:ok = report_rebuild_progress(on_progress, index, total_files, "post files")
|
||||
end)
|
||||
if Keyword.get(opts, :reindex_search, true) do
|
||||
:ok = report_rebuild_phase(on_progress, 0.97, "Refreshing post search index")
|
||||
|
||||
if Keyword.get(opts, :reindex_search, true) do
|
||||
:ok = report_rebuild_phase(on_progress, 0.97, "Refreshing post search index")
|
||||
:ok =
|
||||
Search.reindex_posts(project_id,
|
||||
on_progress: scaled_progress_reporter(on_progress, 0.97, 0.99)
|
||||
)
|
||||
end
|
||||
|
||||
:ok =
|
||||
Search.reindex_posts(project_id,
|
||||
on_progress: scaled_progress_reporter(on_progress, 0.97, 0.99)
|
||||
)
|
||||
if Keyword.get(opts, :rebuild_embeddings, true) do
|
||||
:ok = report_rebuild_phase(on_progress, 0.99, "Refreshing post embeddings")
|
||||
|
||||
{:ok, _rebuilt_post_ids} =
|
||||
Embeddings.rebuild_project(project_id,
|
||||
on_progress: scaled_progress_reporter(on_progress, 0.99, 1.0)
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, posts}
|
||||
end
|
||||
|
||||
if Keyword.get(opts, :rebuild_embeddings, true) do
|
||||
:ok = report_rebuild_phase(on_progress, 0.99, "Refreshing post embeddings")
|
||||
|
||||
{:ok, _rebuilt_post_ids} =
|
||||
Embeddings.rebuild_project(project_id,
|
||||
on_progress: scaled_progress_reporter(on_progress, 0.99, 1.0)
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, posts}
|
||||
end
|
||||
|
||||
@spec import_orphan_post_file(String.t(), String.t()) ::
|
||||
@@ -81,20 +83,20 @@ defmodule BDS.Posts.RebuildFromFiles do
|
||||
full_path = Path.join(Projects.project_data_dir(project), relative_path)
|
||||
|
||||
if File.exists?(full_path) do
|
||||
rebuild_file = parse_rebuild_file(project, full_path)
|
||||
with {:ok, rebuild_file} <- parse_rebuild_file(project, full_path) do
|
||||
if TranslationValidation.translation_rebuild_file?(rebuild_file) do
|
||||
{:error, :unsupported_file}
|
||||
else
|
||||
fields =
|
||||
rebuild_file.fields
|
||||
|> Map.put("id", unique_post_id(Map.get(rebuild_file.fields, "id")))
|
||||
|> Map.put(
|
||||
"slug",
|
||||
Slugs.unique_for_import(project_id, Map.fetch!(rebuild_file.fields, "slug"))
|
||||
)
|
||||
|
||||
if TranslationValidation.translation_rebuild_file?(rebuild_file) do
|
||||
{:error, :unsupported_file}
|
||||
else
|
||||
fields =
|
||||
rebuild_file.fields
|
||||
|> Map.put("id", unique_post_id(Map.get(rebuild_file.fields, "id")))
|
||||
|> Map.put(
|
||||
"slug",
|
||||
Slugs.unique_for_import(project_id, Map.fetch!(rebuild_file.fields, "slug"))
|
||||
)
|
||||
|
||||
{:ok, upsert_post_from_rebuild_file(project_id, %{rebuild_file | fields: fields})}
|
||||
{:ok, upsert_post_from_rebuild_file(project_id, %{rebuild_file | fields: fields})}
|
||||
end
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
@@ -108,33 +110,33 @@ defmodule BDS.Posts.RebuildFromFiles do
|
||||
full_path = Path.join(Projects.project_data_dir(project), relative_path)
|
||||
|
||||
if File.exists?(full_path) do
|
||||
rebuild_file = parse_rebuild_file(project, full_path)
|
||||
with {:ok, rebuild_file} <- parse_rebuild_file(project, full_path) do
|
||||
if TranslationValidation.translation_rebuild_file?(rebuild_file) do
|
||||
source_post_id = Map.fetch!(rebuild_file.fields, "translationFor")
|
||||
language = TranslationValidation.normalize_language(Map.fetch!(rebuild_file.fields, "language"))
|
||||
|
||||
if TranslationValidation.translation_rebuild_file?(rebuild_file) do
|
||||
source_post_id = Map.fetch!(rebuild_file.fields, "translationFor")
|
||||
language = TranslationValidation.normalize_language(Map.fetch!(rebuild_file.fields, "language"))
|
||||
case Repo.get(Post, source_post_id) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
|
||||
case Repo.get(Post, source_post_id) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
%Post{} = post ->
|
||||
if TranslationValidation.normalize_language(post.language) == language or
|
||||
Repo.get_by(Translation, translation_for: source_post_id, language: language) do
|
||||
{:error, :conflict}
|
||||
else
|
||||
fields = Map.put(rebuild_file.fields, "id", Ecto.UUID.generate())
|
||||
|
||||
%Post{} = post ->
|
||||
if TranslationValidation.normalize_language(post.language) == language or
|
||||
Repo.get_by(Translation, translation_for: source_post_id, language: language) do
|
||||
{:error, :conflict}
|
||||
else
|
||||
fields = Map.put(rebuild_file.fields, "id", Ecto.UUID.generate())
|
||||
|
||||
{:ok,
|
||||
upsert_post_translation_from_rebuild_file(
|
||||
project_id,
|
||||
%{rebuild_file | fields: fields},
|
||||
sync_search: true
|
||||
)}
|
||||
{:ok,
|
||||
upsert_post_translation_from_rebuild_file(
|
||||
project_id,
|
||||
%{rebuild_file | fields: fields},
|
||||
sync_search: true
|
||||
)}
|
||||
end
|
||||
end
|
||||
else
|
||||
{:error, :unsupported_file}
|
||||
end
|
||||
else
|
||||
{:error, :unsupported_file}
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
@@ -143,8 +145,9 @@ defmodule BDS.Posts.RebuildFromFiles do
|
||||
|
||||
@doc false
|
||||
def upsert_post_from_file(project_id, project, path) do
|
||||
rebuild_file = parse_rebuild_file(project, path)
|
||||
upsert_post_from_rebuild_file(project_id, rebuild_file)
|
||||
with {:ok, rebuild_file} <- parse_rebuild_file(project, path) do
|
||||
{:ok, upsert_post_from_rebuild_file(project_id, rebuild_file)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -244,14 +247,15 @@ defmodule BDS.Posts.RebuildFromFiles do
|
||||
|
||||
@doc false
|
||||
def parse_rebuild_file(project, path) do
|
||||
contents = File.read!(path)
|
||||
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
||||
|
||||
%{
|
||||
path: path,
|
||||
relative_path: Path.relative_to(path, Projects.project_data_dir(project)),
|
||||
fields: fields
|
||||
}
|
||||
with {:ok, contents} <- read_rebuild_file(path),
|
||||
{:ok, %{fields: fields}} <- Frontmatter.parse_document(contents) do
|
||||
{:ok,
|
||||
%{
|
||||
path: path,
|
||||
relative_path: Path.relative_to(path, Projects.project_data_dir(project)),
|
||||
fields: fields
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -317,4 +321,22 @@ defmodule BDS.Posts.RebuildFromFiles do
|
||||
id
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_rebuild_files(results) do
|
||||
Enum.reduce_while(results, {:ok, []}, fn
|
||||
{:ok, rebuild_file}, {:ok, rebuild_files} -> {:cont, {:ok, [rebuild_file | rebuild_files]}}
|
||||
{:error, reason}, {:ok, _rebuild_files} -> {:halt, {:error, reason}}
|
||||
end)
|
||||
|> case do
|
||||
{:ok, rebuild_files} -> {:ok, Enum.reverse(rebuild_files)}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp read_rebuild_file(path) do
|
||||
case File.read(path) do
|
||||
{:ok, contents} -> {:ok, contents}
|
||||
{:error, reason} -> {:error, {:read_rebuild_file, path, reason}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,9 +117,8 @@ defmodule BDS.Posts.Translations do
|
||||
project = Projects.get_project!(translation.project_id)
|
||||
full_path = Path.join(Projects.project_data_dir(project), translation.file_path)
|
||||
|
||||
if File.exists?(full_path) do
|
||||
rebuild_file = RebuildFromFiles.parse_rebuild_file(project, full_path)
|
||||
|
||||
with true <- File.exists?(full_path),
|
||||
{:ok, rebuild_file} <- RebuildFromFiles.parse_rebuild_file(project, full_path) do
|
||||
{:ok,
|
||||
RebuildFromFiles.upsert_post_translation_from_rebuild_file(
|
||||
translation.project_id,
|
||||
@@ -127,7 +126,8 @@ defmodule BDS.Posts.Translations do
|
||||
sync_search: true
|
||||
)}
|
||||
else
|
||||
{:error, :not_found}
|
||||
false -> {:error, :not_found}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user