Storing presets in a database

Copy Markdown View Source

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
end

Each 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. A definition that doesn't parse or names a watermark the configuration doesn't define answers 500.

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.

Build URLs for stored presets

The URL configuration from ImagePipe.url_config/1 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.