Load preset definitions from a database or another service while ImagePipe
serves a request, so editors can add or change presets without a restart.
This assumes ImagePipe.Plug or ImagePipe.run/4 in your Elixir
application. image_pipe_server has no preset lookup. Its presets come only
from its configuration file. Presets explains how stored presets
combine with the ones in the configuration.
Implement the lookup
Write a module that implements ImagePipe.PresetLookup. fetch/2 receives
the names a request needs and returns the definition of each name it has:
# lib/my_app/presets.ex
defmodule MyApp.Presets do
@behaviour ImagePipe.PresetLookup
import Ecto.Query
@impl true
def validate_options(options) do
case Keyword.fetch(options, :repo) do
{:ok, _repo} -> {:ok, options}
:error -> {:error, :missing_repo}
end
end
@impl true
def fetch(names, options) do
repo = Keyword.fetch!(options, :repo)
query = from p in "presets", where: p.name in ^names, select: {p.name, p.fragment}
{:ok, Map.new(repo.all(query))}
end
endEach definition is a fragment of URL options, the same as a preset in the
configuration, such as "w=400/h=300/fit=cover". Leave out names the
database doesn't have, and the request answers 400 with
unknown preset. When the database is unreachable, the request answers
503, whether fetch/2 returns {:error, reason} or, as repo.all/1 does
here, raises.
Cache the definitions
ImagePipe calls fetch/2 on every request that names a preset missing from
the configuration, and doesn't keep the results. Cache them in your module,
for example in ETS, :persistent_term, or Cachex, and set a short timeout on
the database or HTTP client it uses, since the request waits for the call.
Configure the lookup
Pass the module and its options as :preset_lookup, next to any presets in
the configuration:
# lib/my_app/application.ex
{ImagePipe,
name: MyApp.Images,
presets: %{"card" => "w=400/h=300/fit=cover"},
preset_lookup: {MyApp.Presets, repo: MyApp.Repo},
sources: [...]}ImagePipe calls validate_options/1 once, when it builds the configuration,
and raises ArgumentError when it returns {:error, reason}.
A name in :presets always wins, and requests that use only those names
never call the lookup. Stored presets may name presets from :presets or
other stored presets, and ImagePipe fetches the names they reference in
further calls. :max_preset_lookups (default 32) caps how many distinct
names one request may look up. A request fails with 400 when its URL names
more stored presets than the cap. It fails with 500 when the presets that
stored definitions reference push the total over the cap.
Build URLs for stored presets
The URL configuration from ImagePipe.url_config/2 has
preset_lookup: true in its :validate_against, so the builder accepts
names missing from :presets and leaves them to the server.
ImagePipe.validate/2 runs the lookup, for a full check before you hand out
a URL:
builder =
ImagePipe.URL.new(ImagePipe.url_config(MyApp.Images))
|> ImagePipe.URL.group(presets: ["spring-sale"])
{:ok, _warnings} = ImagePipe.validate(MyApp.Images, builder)A builder in another application passes preset_lookup: true in its
:validate_against.
Retire a stored preset
Return "" for a retired name instead of deleting its row. Its URLs keep
working, without the preset's options. A preset that added a watermark, a
crop that hides part of the image, or a size cap should usually stay
instead, since its URLs would quietly lose it.
Check the lookup
Request a stored preset. Each request that calls the lookup emits the
[:image_pipe, :preset, :lookup] span, whose stop metadata counts the
definitions fetched and the calls to fetch/2 (see
telemetry events). The default Logger logs failed
lookups as warnings. ImagePipe.PresetLookup lists the status each
lookup result gives.