diff --git a/lib/bds/desktop/shell_commands.ex b/lib/bds/desktop/shell_commands.ex
index 04a9de8..5edc195 100644
--- a/lib/bds/desktop/shell_commands.ex
+++ b/lib/bds/desktop/shell_commands.ex
@@ -196,6 +196,14 @@ defmodule BDS.Desktop.ShellCommands do
end)
end
+ defp dispatch("rebuild_media_links", project, _params) do
+ queue_task(project, "rebuild_media_links", "Rebuild Media Links", "Maintenance", fn report ->
+ {:ok, result} = BDS.Media.rebuild_media_links(project.id, on_progress: report)
+ report.(1.0, "Media links rebuilt")
+ %{project_id: project.id, counts: %{media_links: result.links}}
+ end)
+ end
+
defp dispatch("regenerate_missing_thumbnails", project, _params) do
queue_task(
project,
diff --git a/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex b/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex
index 4674b8f..1ca5204 100644
--- a/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex
+++ b/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex
@@ -375,7 +375,8 @@
-
+
+
diff --git a/lib/bds/media.ex b/lib/bds/media.ex
index 687d9e4..182fb7d 100644
--- a/lib/bds/media.ex
+++ b/lib/bds/media.ex
@@ -66,6 +66,12 @@ defmodule BDS.Media do
defdelegate link_media_to_post(media_id, post_id), to: BDS.Media.Linking
defdelegate unlink_media_from_post(media_id, post_id), to: BDS.Media.Linking
+ defdelegate rebuild_media_links(project_id), to: BDS.Media.Linking,
+ as: :rebuild_links_from_sidecars
+
+ defdelegate rebuild_media_links(project_id, opts), to: BDS.Media.Linking,
+ as: :rebuild_links_from_sidecars
+
defdelegate rebuild_media_from_files(project_id), to: BDS.Media.Rebuilder
defdelegate rebuild_media_from_files(project_id, opts), to: BDS.Media.Rebuilder
diff --git a/lib/bds/media/linking.ex b/lib/bds/media/linking.ex
index f7170d1..6c65ea1 100644
--- a/lib/bds/media/linking.ex
+++ b/lib/bds/media/linking.ex
@@ -5,6 +5,13 @@ defmodule BDS.Media.Linking do
import Ecto.Query
+ import BDS.Media.FileOps,
+ only: [
+ progress_callback: 1,
+ report_rebuild_progress: 4,
+ report_rebuild_started: 3
+ ]
+
alias BDS.Media.Media
alias BDS.Media.Sidecars
alias BDS.Persistence
@@ -134,6 +141,45 @@ defmodule BDS.Media.Linking do
def restore_post_links(_project_id, _media_id, _post_ids), do: :ok
+ @doc """
+ Rebuild the `post_media` junction for a project from every media sidecar's
+ `linkedPostIds`.
+
+ Media sidecars are the source of truth for gallery associations (matching the
+ old bDS `rebuildFromSidecars`). Existing project links are cleared and then
+ re-created from the on-disk sidecars, so galleries reflect the project's
+ metadata even if the junction table drifted or was never populated. Links to
+ posts that are not present in the database are skipped.
+ """
+ @spec rebuild_links_from_sidecars(String.t(), keyword()) ::
+ {:ok, %{media: non_neg_integer(), links: non_neg_integer()}}
+ def rebuild_links_from_sidecars(project_id, opts \\ []) when is_binary(project_id) do
+ project = Projects.get_project!(project_id)
+ on_progress = progress_callback(opts)
+
+ media_list =
+ Repo.all(from m in Media, where: m.project_id == ^project_id, order_by: [asc: m.id])
+
+ total = length(media_list)
+ :ok = report_rebuild_started(on_progress, total, "media links")
+
+ {_cleared, _} =
+ Repo.delete_all(from pm in PostMedia, where: pm.project_id == ^project_id)
+
+ media_list
+ |> Enum.with_index(1)
+ |> Enum.each(fn {media, index} ->
+ post_ids = Sidecars.read_linked_post_ids(project, media)
+ restore_post_links(project_id, media.id, post_ids)
+ :ok = report_rebuild_progress(on_progress, index, total, "media links")
+ end)
+
+ links =
+ Repo.aggregate(from(pm in PostMedia, where: pm.project_id == ^project_id), :count)
+
+ {:ok, %{media: total, links: links}}
+ end
+
defp ensure_link(project_id, media_id, post_id) do
already_linked? =
Repo.exists?(
diff --git a/lib/bds/media/sidecars.ex b/lib/bds/media/sidecars.ex
index 209cd31..229a0f4 100644
--- a/lib/bds/media/sidecars.ex
+++ b/lib/bds/media/sidecars.ex
@@ -349,6 +349,29 @@ defmodule BDS.Media.Sidecars do
|> File.exists?()
end
+ @doc """
+ Read the `linkedPostIds` list persisted in a media item's canonical sidecar.
+
+ The media sidecar is the source of truth for gallery associations (matching
+ the old bDS `linkedPostIds` field). Returns `[]` when the sidecar is missing
+ or has no linked posts.
+ """
+ @spec read_linked_post_ids(BDS.Projects.Project.t(), Media.t()) :: [String.t()]
+ def read_linked_post_ids(project, %Media{} = media) do
+ sidecar_path = Path.join(Projects.project_data_dir(project), media.sidecar_path)
+
+ case parse_existing_canonical_sidecar(project, sidecar_path) do
+ {:ok, sidecar} ->
+ sidecar.fields
+ |> DocumentFields.get("linkedPostIds", [])
+ |> List.wrap()
+ |> Enum.filter(&is_binary/1)
+
+ {:error, _reason} ->
+ []
+ end
+ end
+
defp parse_existing_canonical_sidecar(project, sidecar_path) do
if File.exists?(sidecar_path) do
parse_canonical_sidecar(project, sidecar_path)
diff --git a/priv/gettext/de/LC_MESSAGES/ui.po b/priv/gettext/de/LC_MESSAGES/ui.po
index 59ab5f2..f89f7a1 100644
--- a/priv/gettext/de/LC_MESSAGES/ui.po
+++ b/priv/gettext/de/LC_MESSAGES/ui.po
@@ -1757,7 +1757,7 @@ msgid "Open"
msgstr "Öffnen"
#: lib/bds/desktop/menu_bar.ex:154
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:382
#, elixir-autogen, elixir-format
msgid "Open Data Folder"
msgstr "Datenordner öffnen"
@@ -2049,16 +2049,11 @@ msgid "Rebuild Database"
msgstr "Datenbank neu aufbauen"
#: lib/bds/desktop/menu_bar.ex:184
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
#, elixir-autogen, elixir-format
msgid "Rebuild Embedding Index"
msgstr "Embedding-Index neu aufbauen"
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
-#, elixir-autogen, elixir-format
-msgid "Rebuild Links"
-msgstr "Links neu aufbauen"
-
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:375
#, elixir-autogen, elixir-format
msgid "Rebuild Media From Files"
@@ -2116,7 +2111,7 @@ msgstr "Übersetzung aktualisieren"
msgid "Regenerate Calendar"
msgstr "Kalender neu erzeugen"
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
#, elixir-autogen, elixir-format
msgid "Regenerate Missing Thumbnails"
msgstr "Fehlende Vorschaubilder neu erzeugen"
@@ -3533,3 +3528,13 @@ msgstr "Vorgeschlagene Tags"
#, elixir-autogen, elixir-format
msgid "Force Render Site"
msgstr "Website vollständig neu generieren"
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#, elixir-autogen, elixir-format
+msgid "Rebuild Media Links"
+msgstr "Medien-Links neu aufbauen"
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
+#, elixir-autogen, elixir-format
+msgid "Rebuild Post Links"
+msgstr "Beitrags-Links neu aufbauen"
diff --git a/priv/gettext/en/LC_MESSAGES/ui.po b/priv/gettext/en/LC_MESSAGES/ui.po
index a96b062..d1bb83e 100644
--- a/priv/gettext/en/LC_MESSAGES/ui.po
+++ b/priv/gettext/en/LC_MESSAGES/ui.po
@@ -1757,7 +1757,7 @@ msgid "Open"
msgstr ""
#: lib/bds/desktop/menu_bar.ex:154
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:382
#, elixir-autogen, elixir-format
msgid "Open Data Folder"
msgstr ""
@@ -2049,16 +2049,11 @@ msgid "Rebuild Database"
msgstr ""
#: lib/bds/desktop/menu_bar.ex:184
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
#, elixir-autogen, elixir-format
msgid "Rebuild Embedding Index"
msgstr ""
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
-#, elixir-autogen, elixir-format
-msgid "Rebuild Links"
-msgstr ""
-
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:375
#, elixir-autogen, elixir-format
msgid "Rebuild Media From Files"
@@ -2116,7 +2111,7 @@ msgstr ""
msgid "Regenerate Calendar"
msgstr ""
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
#, elixir-autogen, elixir-format
msgid "Regenerate Missing Thumbnails"
msgstr ""
@@ -3533,3 +3528,13 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Force Render Site"
msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Rebuild Media Links"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Rebuild Post Links"
+msgstr ""
diff --git a/priv/gettext/es/LC_MESSAGES/ui.po b/priv/gettext/es/LC_MESSAGES/ui.po
index 6592061..9ec3c2f 100644
--- a/priv/gettext/es/LC_MESSAGES/ui.po
+++ b/priv/gettext/es/LC_MESSAGES/ui.po
@@ -1757,7 +1757,7 @@ msgid "Open"
msgstr "Abrir"
#: lib/bds/desktop/menu_bar.ex:154
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:382
#, elixir-autogen, elixir-format
msgid "Open Data Folder"
msgstr "Abrir carpeta de datos"
@@ -2049,16 +2049,11 @@ msgid "Rebuild Database"
msgstr "Reconstruir base de datos"
#: lib/bds/desktop/menu_bar.ex:184
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
#, elixir-autogen, elixir-format
msgid "Rebuild Embedding Index"
msgstr "Reconstruir índice de embeddings"
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
-#, elixir-autogen, elixir-format
-msgid "Rebuild Links"
-msgstr "Reconstruir enlaces"
-
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:375
#, elixir-autogen, elixir-format
msgid "Rebuild Media From Files"
@@ -2116,7 +2111,7 @@ msgstr "Actualizar traducción"
msgid "Regenerate Calendar"
msgstr "Regenerar calendario"
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
#, elixir-autogen, elixir-format
msgid "Regenerate Missing Thumbnails"
msgstr "Regenerar miniaturas faltantes"
@@ -3533,3 +3528,13 @@ msgstr "Etiquetas sugeridas"
#, elixir-autogen, elixir-format
msgid "Force Render Site"
msgstr "Forzar la regeneración del sitio"
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#, elixir-autogen, elixir-format
+msgid "Rebuild Media Links"
+msgstr "Reconstruir enlaces de medios"
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
+#, elixir-autogen, elixir-format
+msgid "Rebuild Post Links"
+msgstr "Reconstruir enlaces de entradas"
diff --git a/priv/gettext/fr/LC_MESSAGES/ui.po b/priv/gettext/fr/LC_MESSAGES/ui.po
index f5371e6..8d47583 100644
--- a/priv/gettext/fr/LC_MESSAGES/ui.po
+++ b/priv/gettext/fr/LC_MESSAGES/ui.po
@@ -1757,7 +1757,7 @@ msgid "Open"
msgstr "Ouvrir"
#: lib/bds/desktop/menu_bar.ex:154
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:382
#, elixir-autogen, elixir-format
msgid "Open Data Folder"
msgstr "Ouvrir le dossier de données"
@@ -2049,16 +2049,11 @@ msgid "Rebuild Database"
msgstr "Reconstruire la base de données"
#: lib/bds/desktop/menu_bar.ex:184
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
#, elixir-autogen, elixir-format
msgid "Rebuild Embedding Index"
msgstr "Reconstruire l’index d’embeddings"
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
-#, elixir-autogen, elixir-format
-msgid "Rebuild Links"
-msgstr "Reconstruire les liens"
-
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:375
#, elixir-autogen, elixir-format
msgid "Rebuild Media From Files"
@@ -2116,7 +2111,7 @@ msgstr "Actualiser la traduction"
msgid "Regenerate Calendar"
msgstr "Régénérer le calendrier"
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
#, elixir-autogen, elixir-format
msgid "Regenerate Missing Thumbnails"
msgstr "Régénérer les vignettes manquantes"
@@ -3533,3 +3528,13 @@ msgstr "Tags suggérés"
#, elixir-autogen, elixir-format
msgid "Force Render Site"
msgstr "Forcer la régénération du site"
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#, elixir-autogen, elixir-format
+msgid "Rebuild Media Links"
+msgstr "Reconstruire les liens des médias"
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
+#, elixir-autogen, elixir-format
+msgid "Rebuild Post Links"
+msgstr "Reconstruire les liens des articles"
diff --git a/priv/gettext/it/LC_MESSAGES/ui.po b/priv/gettext/it/LC_MESSAGES/ui.po
index 8194f41..f212b35 100644
--- a/priv/gettext/it/LC_MESSAGES/ui.po
+++ b/priv/gettext/it/LC_MESSAGES/ui.po
@@ -1757,7 +1757,7 @@ msgid "Open"
msgstr "Apri"
#: lib/bds/desktop/menu_bar.ex:154
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:382
#, elixir-autogen, elixir-format
msgid "Open Data Folder"
msgstr "Apri cartella dati"
@@ -2049,16 +2049,11 @@ msgid "Rebuild Database"
msgstr "Ricostruisci database"
#: lib/bds/desktop/menu_bar.ex:184
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
#, elixir-autogen, elixir-format
msgid "Rebuild Embedding Index"
msgstr "Ricostruisci indice embeddings"
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
-#, elixir-autogen, elixir-format
-msgid "Rebuild Links"
-msgstr "Ricostruisci i link"
-
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:375
#, elixir-autogen, elixir-format
msgid "Rebuild Media From Files"
@@ -2116,7 +2111,7 @@ msgstr "Aggiorna traduzione"
msgid "Regenerate Calendar"
msgstr "Rigenera calendario"
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
#, elixir-autogen, elixir-format
msgid "Regenerate Missing Thumbnails"
msgstr "Rigenera miniature mancanti"
@@ -3533,3 +3528,13 @@ msgstr "Tag suggeriti"
#, elixir-autogen, elixir-format
msgid "Force Render Site"
msgstr "Forza la rigenerazione del sito"
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#, elixir-autogen, elixir-format
+msgid "Rebuild Media Links"
+msgstr "Ricostruisci i link dei media"
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
+#, elixir-autogen, elixir-format
+msgid "Rebuild Post Links"
+msgstr "Ricostruisci i link degli articoli"
diff --git a/priv/gettext/ui.pot b/priv/gettext/ui.pot
index cec9442..b599f1b 100644
--- a/priv/gettext/ui.pot
+++ b/priv/gettext/ui.pot
@@ -1770,7 +1770,7 @@ msgid "Open"
msgstr ""
#: lib/bds/desktop/menu_bar.ex:154
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:382
#, elixir-autogen, elixir-format
msgid "Open Data Folder"
msgstr ""
@@ -2062,16 +2062,11 @@ msgid "Rebuild Database"
msgstr ""
#: lib/bds/desktop/menu_bar.ex:184
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:381
#, elixir-autogen, elixir-format
msgid "Rebuild Embedding Index"
msgstr ""
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
-#, elixir-autogen, elixir-format
-msgid "Rebuild Links"
-msgstr ""
-
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:375
#, elixir-autogen, elixir-format
msgid "Rebuild Media From Files"
@@ -2129,7 +2124,7 @@ msgstr ""
msgid "Regenerate Calendar"
msgstr ""
-#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:380
#, elixir-autogen, elixir-format
msgid "Regenerate Missing Thumbnails"
msgstr ""
@@ -3546,3 +3541,13 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Force Render Site"
msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:379
+#, elixir-autogen, elixir-format
+msgid "Rebuild Media Links"
+msgstr ""
+
+#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:378
+#, elixir-autogen, elixir-format
+msgid "Rebuild Post Links"
+msgstr ""
diff --git a/test/bds/media/link_restore_fresh_test.exs b/test/bds/media/link_restore_fresh_test.exs
new file mode 100644
index 0000000..fd1c4f0
--- /dev/null
+++ b/test/bds/media/link_restore_fresh_test.exs
@@ -0,0 +1,62 @@
+defmodule BDS.Media.LinkRestoreFreshTest do
+ use ExUnit.Case, async: false
+
+ import Ecto.Query
+
+ alias BDS.Media
+ alias BDS.Media.Media, as: MediaRecord
+ alias BDS.Posts.PostMedia
+ alias BDS.Repo
+
+ setup do
+ :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
+ Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
+ temp_dir = Path.join(System.tmp_dir!(), "bds-freshlink-#{System.unique_integer([:positive])}")
+ File.mkdir_p!(temp_dir)
+ on_exit(fn -> File.rm_rf(temp_dir) end)
+
+ {:ok, project} = BDS.Projects.create_project(%{name: "Fresh", data_path: temp_dir})
+ {:ok, _} = BDS.Metadata.update_project_metadata(project.id, %{main_language: "en"})
+
+ {:ok, post} =
+ BDS.Posts.create_post(%{
+ project_id: project.id,
+ title: "Gallery Post",
+ content: "[[gallery]]",
+ language: "en"
+ })
+
+ source = Path.join(temp_dir, "fresh.jpg")
+ File.write!(source, "fake jpeg")
+ {:ok, media} = Media.import_media(%{project_id: project.id, source_path: source})
+ {:ok, _} = Media.link_media_to_post(media.id, post.id)
+
+ %{project: project, post: post, media: media}
+ end
+
+ test "fresh media rebuild (media record gone, only files on disk) restores links", %{
+ project: project,
+ post: post,
+ media: media
+ } do
+ # Sidecar on disk should carry the link.
+ sidecar_path = Path.join(project.data_path, media.sidecar_path)
+ assert File.exists?(sidecar_path)
+ contents = File.read!(sidecar_path)
+ assert contents =~ "linkedPostIds"
+ assert contents =~ post.id
+
+ # Simulate a fresh DB: drop the media record AND its links entirely.
+ Repo.delete_all(from pm in PostMedia, where: pm.media_id == ^media.id)
+ Repo.delete_all(from m in MediaRecord, where: m.id == ^media.id)
+ assert Repo.aggregate(from(m in MediaRecord, where: m.id == ^media.id), :count) == 0
+
+ assert {:ok, _} = BDS.Media.rebuild_media_from_files(project.id)
+
+ # Media re-inserted from sidecar.
+ assert Repo.aggregate(from(m in MediaRecord, where: m.project_id == ^project.id), :count) == 1
+
+ # Link restored so the post's media list is populated again.
+ assert Repo.aggregate(from(pm in PostMedia, where: pm.post_id == ^post.id), :count) == 1
+ end
+end
diff --git a/test/bds/media/link_restore_test.exs b/test/bds/media/link_restore_test.exs
index 6b4e84b..44635a5 100644
--- a/test/bds/media/link_restore_test.exs
+++ b/test/bds/media/link_restore_test.exs
@@ -69,4 +69,51 @@ defmodule BDS.Media.LinkRestoreTest do
assert {:ok, _} = BDS.Media.rebuild_media_from_files(project.id)
assert link_count(post.id, media.id) == 1
end
+
+ test "rebuild_media_links rebuilds gallery links from sidecars", %{
+ project: project,
+ post: post,
+ media: media
+ } do
+ # Simulate a project whose junction table was never populated even though
+ # the media sidecar references the post (the reported gallery regression).
+ Repo.delete_all(from pm in PostMedia, where: pm.media_id == ^media.id)
+ assert link_count(post.id, media.id) == 0
+
+ assert {:ok, %{media: 1, links: 1}} = BDS.Media.rebuild_media_links(project.id)
+
+ assert link_count(post.id, media.id) == 1
+ end
+
+ test "rebuild_media_links drops stale junction links not present in sidecars", %{
+ project: project,
+ post: post,
+ media: media
+ } do
+ {:ok, other_post} =
+ BDS.Posts.create_post(%{
+ project_id: project.id,
+ title: "Unrelated Post",
+ content: "no gallery here",
+ language: "en"
+ })
+
+ # A junction row that drifted from the sidecar: the media is linked to a
+ # post its sidecar does not reference. The sidecar is the source of truth,
+ # so rebuild must drop this link.
+ Repo.insert!(%PostMedia{
+ id: Ecto.UUID.generate(),
+ project_id: project.id,
+ post_id: other_post.id,
+ media_id: media.id,
+ sort_order: 5,
+ created_at: 0
+ })
+
+ assert {:ok, %{links: 1}} = BDS.Media.rebuild_media_links(project.id)
+
+ # The real (sidecar-backed) link survives, the drifted link is gone.
+ assert link_count(post.id, media.id) == 1
+ assert link_count(other_post.id, media.id) == 0
+ end
end