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 @mockup_dir "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 incomplete = ImageSchema |> where([i], i.variants_status != "complete" or is_nil(i.variants_status)) |> where([i], i.is_svg == false) |> Repo.all() complete_missing = ImageSchema |> where([i], i.variants_status == "complete") |> where([i], i.is_svg == false) |> where([i], not is_nil(i.source_width)) |> Repo.all() |> Enum.reject(fn img -> Optimizer.disk_variants_exist?(img.id, img.source_width) end) to_process = incomplete ++ complete_missing if to_process == [] do Logger.info("[VariantCache] All database image variants up to date") else Logger.info( "[VariantCache] Enqueueing #{length(to_process)} database images for processing" ) Enum.each(to_process, fn image -> image |> ImageSchema.changeset(%{variants_status: "pending"}) |> Repo.update!() OptimizeWorker.enqueue(image.id) 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) File.exists?(Path.join(dir, "#{basename}-800.webp")) 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