From 94c6cbed303da53afe4f8c244c3565ee84e0cb34 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 20:18:17 +0200 Subject: [PATCH] Fix private thumbnail previews and expose paginated object listing --- lib/mave_core/embeds.ex | 17 ++++++--- lib/mave_core/media/storage.ex | 6 ++++ test/mave_core/embeds_test.exs | 19 ++++++++++ test/mave_core/media/storage_test.exs | 36 +++++++++++++++++++ .../live/dashboard/videos/show_test.exs | 31 ++++++++++++++++ 5 files changed, 104 insertions(+), 5 deletions(-) diff --git a/lib/mave_core/embeds.ex b/lib/mave_core/embeds.ex index 94eb515..879ea14 100644 --- a/lib/mave_core/embeds.ex +++ b/lib/mave_core/embeds.ex @@ -1404,14 +1404,21 @@ defmodule MaveCore.Embeds do end defp preview_poster_url(%Space{} = space, %Embed{} = embed, settings, cache_buster) do + private_upload? = uploaded_poster_settings?(settings) and MaveCore.Playback.protected?(embed) + url = - if generated_thumbnail_poster_settings?(settings) and not is_nil(cache_buster) do - thumbnail_url(space, embed, cache_buster) - else - SettingsSerializer.preview_poster_url(space, embed, settings) + cond do + private_upload? -> + thumbnail_url(space, embed, cache_buster) + + generated_thumbnail_poster_settings?(settings) and not is_nil(cache_buster) -> + thumbnail_url(space, embed, cache_buster) + + true -> + SettingsSerializer.preview_poster_url(space, embed, settings) end - if uploaded_poster_settings?(settings) do + if uploaded_poster_settings?(settings) and not private_upload? do with_cache_buster(url, cache_buster) else url diff --git a/lib/mave_core/media/storage.ex b/lib/mave_core/media/storage.ex index 48db20f..26fbb39 100644 --- a/lib/mave_core/media/storage.ex +++ b/lib/mave_core/media/storage.ex @@ -140,6 +140,12 @@ defmodule MaveCore.Media.Storage do |> handle_response() end + @doc "Lists current object keys under a prefix, following all result pages." + def list_keys(bucket, prefix, storage_profile \\ nil) + when is_binary(bucket) and is_binary(prefix) do + list_prefix_keys(bucket, prefix, storage_profile) + end + @doc "Fetches at most `max_bytes` from the beginning of an object." def get_prefix(bucket, path, storage_profile, max_bytes) when is_binary(bucket) and is_binary(path) do diff --git a/test/mave_core/embeds_test.exs b/test/mave_core/embeds_test.exs index b421e1b..3701228 100644 --- a/test/mave_core/embeds_test.exs +++ b/test/mave_core/embeds_test.exs @@ -2399,6 +2399,25 @@ defmodule MaveCore.EmbedsTest do assert payload.thumbnail == versioned_thumbnail_url assert payload.preview_poster == versioned_thumbnail_url assert payload.snippet_player_poster == versioned_thumbnail_url + + private_video = + video + |> Ecto.Changeset.change(playback_visibility: :private, playback_status: :private) + |> Repo.update!() + + private_payload = Embeds.get_video_dashboard_payload(space, private_video) + uri = URI.parse(private_payload.preview_poster) + query = URI.decode_query(uri.query) + + assert private_payload.preview_poster == private_payload.thumbnail + assert String.ends_with?(uri.path, "/#{video.hash}/thumbnail.jpg") + assert query["X-Amz-Signature"] + assert query["X-Amz-Expires"] == "86400" + assert query["response-cache-control"] == "private, max-age=300" + assert query["e"] == to_string(DateTime.to_unix(inserted_at, :microsecond)) + assert length(URI.query_decoder(uri.query) |> Enum.filter(fn {key, _} -> key == "e" end)) == 1 + refute private_payload.snippet_player_poster =~ "X-Amz-Signature" + assert private_video.settings.external_poster == thumbnail_url end test "dashboard video rows cache-bust completed custom thumbnail URLs" do diff --git a/test/mave_core/media/storage_test.exs b/test/mave_core/media/storage_test.exs index 4d438b2..2db16a4 100644 --- a/test/mave_core/media/storage_test.exs +++ b/test/mave_core/media/storage_test.exs @@ -9,6 +9,42 @@ defmodule MaveCore.Media.StorageTest do setup {Req.Test, :verify_on_exit!} + test "list_keys follows object-store pagination without guessing version prefixes" do + Req.default_options(plug: {Req.Test, __MODULE__}) + + profile = [ + endpoint: "https://storage.example.test", + access_key_id: "key", + secret_access_key: "secret" + ] + + Req.Test.expect(__MODULE__, fn conn -> + conn = fetch_query_params(conn) + assert conn.query_params["prefix"] == "embed/" + refute conn.query_params["continuation-token"] + + send_resp( + conn, + 200, + "embed/manifest.jsontruenext-page" + ) + end) + + Req.Test.expect(__MODULE__, fn conn -> + conn = fetch_query_params(conn) + assert conn.query_params["continuation-token"] == "next-page" + + send_resp( + conn, + 200, + "embed/v8/player.htmlfalse" + ) + end) + + assert {:ok, ["embed/manifest.json", "embed/v8/player.html"]} = + Storage.list_keys("bucket", "embed/", profile) + end + test "signed reads support a full dashboard day while remaining bounded" do profile = [ endpoint: "https://storage.example.test", diff --git a/test/mave_core_web/live/dashboard/videos/show_test.exs b/test/mave_core_web/live/dashboard/videos/show_test.exs index bcc68e0..d6c0a01 100644 --- a/test/mave_core_web/live/dashboard/videos/show_test.exs +++ b/test/mave_core_web/live/dashboard/videos/show_test.exs @@ -252,6 +252,37 @@ defmodule MaveCoreWeb.Live.Dashboard.Videos.ShowTest do assert html =~ versioned_thumbnail_url end + @tag :private_playback_ui + test "private custom thumbnails use signed URLs in the player and upload preview", %{conn: conn} do + {conn, space} = authenticated_conn(conn) + video = video_embed_fixture(space, %{name: "Private custom thumbnail"}) + + thumbnail_url = + SettingsSerializer.storage_object_url("space-#{space.hash}", "#{video.hash}/thumbnail.jpg") + + {:ok, video} = + MaveCore.Embeds.update_settings(video, %{poster: :upload, external_poster: thumbnail_url}) + + video + |> Ecto.Changeset.change(playback_visibility: :private, playback_status: :private) + |> Repo.update!() + + {:ok, view, _html} = live(conn, "/videos/#{video.id}") + + assert has_element?( + view, + ~s(mave-player[poster*="/#{video.hash}/thumbnail.jpg?"][poster*="X-Amz-Signature="]) + ) + + assert has_element?( + view, + ~s(#droparea[style*="/#{video.hash}/thumbnail.jpg?"][style*="X-Amz-Signature="]) + ) + + refute has_element?(view, ~s(mave-player[poster="#{thumbnail_url}"])) + refute has_element?(view, "#snippet-code", "X-Amz-Signature") + end + @tag :poster_upload test "poster processing remains visible until a delayed conversion completes", %{conn: conn} do {conn, space} = authenticated_conn(conn)