- Alpine multi-stage Dockerfile (131 MB image) - Release overlays (bin/server, bin/migrate), env.sh, Release module - Health check endpoint at GET /health - Fly.io config with SQLite volume mount - Fix hardcoded paths in optimizer.ex and variant_cache.ex to use Application.app_dir/2 (breaks in releases where Plug.Static serves from a different directory than CWD) - strip_beams: true in release config - Optimised .dockerignore and .gitignore for mockup variants Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
3.7 KiB
Elixir
128 lines
3.7 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
|
|
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
|