berrypod/lib/simpleshop_theme/images/variant_cache.ex
jamey bb358f890b consolidate image serving and clean up pipeline
Move all image URL logic into ProductImage.url/2 and thumbnail_url/1,
remove dead on-demand generation code from Optimizer, strip controller
routes down to SVG recolor only, fix mockup startup check to verify all
variant formats, and isolate test image cache directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 17:47:41 +00:00

152 lines
4.6 KiB
Elixir

defmodule SimpleshopTheme.Images.VariantCache do
@moduledoc """
Ensures all image variants exist on startup.
This GenServer runs at startup and checks for:
1. Database images with incomplete variants_status or missing disk files
2. Mockup source files missing their generated variants
For any images missing variants, it enqueues Oban jobs to regenerate them.
"""
use GenServer
require Logger
alias SimpleshopTheme.Repo
alias SimpleshopTheme.Media.Image, as: ImageSchema
alias SimpleshopTheme.Images.{Optimizer, OptimizeWorker}
alias SimpleshopTheme.Products
alias SimpleshopTheme.Sync.ImageDownloadWorker
import Ecto.Query
defp mockup_dir, do: Application.app_dir(:simpleshop_theme, "priv/static/mockups")
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
Task.start(fn -> ensure_all_variants() end)
{:ok, %{}}
end
defp ensure_all_variants do
Logger.info("[VariantCache] Checking image variant cache...")
File.mkdir_p!(Optimizer.cache_dir())
reset_stale_sync_status()
ensure_database_image_variants()
ensure_mockup_variants()
ensure_product_image_downloads()
end
# Reset any provider connections stuck in "syncing" status from interrupted syncs
defp reset_stale_sync_status do
{count, _} = Products.reset_stale_sync_status()
if count > 0 do
Logger.info("[VariantCache] Reset #{count} stale sync status(es) to idle")
end
end
defp ensure_database_image_variants do
# Only load IDs and source_width for the disk check — avoids loading BLOBs
incomplete_ids =
ImageSchema
|> where([i], i.variants_status != "complete" or is_nil(i.variants_status))
|> where([i], i.is_svg == false)
|> select([i], {i.id, i.source_width})
|> Repo.all()
complete_missing_ids =
ImageSchema
|> where([i], i.variants_status == "complete")
|> where([i], i.is_svg == false)
|> where([i], not is_nil(i.source_width))
|> select([i], {i.id, i.source_width})
|> Repo.all()
|> Enum.reject(fn {id, source_width} ->
Optimizer.disk_variants_exist?(id, source_width)
end)
to_process = incomplete_ids ++ complete_missing_ids
if to_process == [] do
Logger.info("[VariantCache] All database image variants up to date")
else
Logger.info(
"[VariantCache] Processing #{length(to_process)} images with missing variants..."
)
# Process directly instead of round-tripping through Oban — more reliable at startup
to_process
|> Task.async_stream(
fn {id, _source_width} ->
case Optimizer.process_for_image(id) do
{:ok, _} ->
:ok
{:error, reason} ->
Logger.warning("[VariantCache] Failed to process #{id}: #{inspect(reason)}")
end
end,
max_concurrency: 4,
timeout: :timer.seconds(30),
on_timeout: :kill_task
)
|> Enum.count(fn
{:ok, :ok} -> true
_ -> false
end)
|> then(fn count ->
Logger.info("[VariantCache] Processed #{count}/#{length(to_process)} image variants")
end)
end
end
defp ensure_mockup_variants do
if File.dir?(mockup_dir()) do
sources =
Path.wildcard(Path.join(mockup_dir(), "*.webp"))
|> Enum.reject(&is_variant?/1)
missing = Enum.reject(sources, &mockup_variants_exist?/1)
if missing == [] do
Logger.info("[VariantCache] All mockup variants up to date")
else
Logger.info("[VariantCache] Enqueueing #{length(missing)} mockups for processing")
Enum.each(missing, &OptimizeWorker.enqueue_mockup/1)
end
end
end
defp is_variant?(path) do
basename = Path.basename(path) |> Path.rootname()
String.match?(basename, ~r/-(400|800|1200|thumb)$/)
end
defp mockup_variants_exist?(source_path) do
basename = Path.basename(source_path) |> Path.rootname()
dir = Path.dirname(source_path)
expected =
["#{basename}-thumb.jpg"] ++
for w <- [400, 800, 1200], ext <- ["avif", "webp", "jpg"], do: "#{basename}-#{w}.#{ext}"
Enum.all?(expected, &File.exists?(Path.join(dir, &1)))
end
defp ensure_product_image_downloads do
pending = Products.list_pending_downloads(limit: 500)
if pending == [] do
Logger.info("[VariantCache] All product images downloaded")
else
Logger.info("[VariantCache] Enqueueing #{length(pending)} product images for download")
Enum.each(pending, fn image -> ImageDownloadWorker.enqueue(image.id) end)
end
end
end