Downloads Printify CDN images via ImageDownloadWorker, processes through Media pipeline (WebP conversion, AVIF/WebP variant generation), and links to ProductImage via new image_id FK. - Add image_id to product_images table - ImageDownloadWorker downloads and processes external images - sync_product_images preserves image_id when URL unchanged - PreviewData uses local images for responsive <picture> elements - VariantCache enqueues pending downloads on startup - mix simpleshop.download_images backfill task Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
118 lines
3.3 KiB
Elixir
118 lines
3.3 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
|
|
|
|
@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())
|
|
|
|
ensure_database_image_variants()
|
|
ensure_mockup_variants()
|
|
ensure_product_image_downloads()
|
|
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
|