diff --git a/config/prod.exs b/config/prod.exs index 9c2f673..6d1269e 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -19,8 +19,9 @@ config :logger, level: :info # Structured JSON logs for production (machine-parseable by fly logs, journalctl, Loki, etc.) config :logger, :default_handler, formatter: {LoggerJSON.Formatters.Basic, []} -# Persistent image cache on the Fly volume (survives deploys) +# Persistent image cache and mockup variants on the Fly volume (survives deploys) config :berrypod, :image_cache_dir, "/data/image_cache" +config :berrypod, :mockup_dir, "/data/mockups" # Runtime production configuration, including reading # of environment variables, is done on config/runtime.exs. diff --git a/lib/berrypod/images/variant_cache.ex b/lib/berrypod/images/variant_cache.ex index f319385..1a70626 100644 --- a/lib/berrypod/images/variant_cache.ex +++ b/lib/berrypod/images/variant_cache.ex @@ -19,7 +19,13 @@ defmodule Berrypod.Images.VariantCache do alias Berrypod.Sync.ImageDownloadWorker import Ecto.Query - defp mockup_dir, do: Application.app_dir(:berrypod, "priv/static/mockups") + # Source mockups bundled in the release + defp release_mockup_dir, do: Application.app_dir(:berrypod, "priv/static/mockups") + + # Where mockup variants live — persistent volume in prod, release dir in dev + defp mockup_dir do + Application.get_env(:berrypod, :mockup_dir) || release_mockup_dir() + end def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) @@ -84,9 +90,25 @@ defmodule Berrypod.Images.VariantCache do end defp ensure_mockup_variants do - if File.dir?(mockup_dir()) do + output = mockup_dir() + release = release_mockup_dir() + + # When using a persistent volume, copy source WebPs from the release + # so variants can be generated there (and survive deploys). + if output != release and File.dir?(release) do + File.mkdir_p!(output) + + Path.wildcard(Path.join(release, "*.webp")) + |> Enum.reject(&is_variant?/1) + |> Enum.each(fn src -> + dest = Path.join(output, Path.basename(src)) + unless File.exists?(dest), do: File.cp!(src, dest) + end) + end + + if File.dir?(output) do sources = - Path.wildcard(Path.join(mockup_dir(), "*.webp")) + Path.wildcard(Path.join(output, "*.webp")) |> Enum.reject(&is_variant?/1) missing = Enum.reject(sources, &mockup_variants_exist?/1) diff --git a/lib/berrypod_web/endpoint.ex b/lib/berrypod_web/endpoint.ex index 4f5199d..856bd27 100644 --- a/lib/berrypod_web/endpoint.ex +++ b/lib/berrypod_web/endpoint.ex @@ -16,7 +16,7 @@ defmodule BerrypodWeb.Endpoint do websocket: [connect_info: [session: @session_options]], longpoll: [connect_info: [session: @session_options]] - # In prod, image variants live on the persistent volume (/data/image_cache) + # In prod, image variants and mockups live on the persistent volume # rather than inside the ephemeral release directory. if image_cache_dir = Application.compile_env(:berrypod, :image_cache_dir) do plug Plug.Static, @@ -25,6 +25,13 @@ defmodule BerrypodWeb.Endpoint do cache_control_for_etags: "public, max-age=31536000, immutable" end + if mockup_dir = Application.compile_env(:berrypod, :mockup_dir) do + plug Plug.Static, + at: "/mockups", + from: mockup_dir, + cache_control_for_etags: "public, max-age=31536000, immutable" + end + # Serve at "/" the static files from "priv/static" directory. # gzip only in prod — avoids stale .gz files from mix assets.deploy # shadowing freshly-built dev assets. diff --git a/lib/berrypod_web/plugs/broken_url_tracker.ex b/lib/berrypod_web/plugs/broken_url_tracker.ex index ae65b97..6084a31 100644 --- a/lib/berrypod_web/plugs/broken_url_tracker.ex +++ b/lib/berrypod_web/plugs/broken_url_tracker.ex @@ -35,6 +35,7 @@ defmodule BerrypodWeb.Plugs.BrokenUrlTracker do String.starts_with?(path, "/assets/") or String.starts_with?(path, "/images/") or String.starts_with?(path, "/image_cache/") or + String.starts_with?(path, "/mockups/") or String.starts_with?(path, "/favicon") end end