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.

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.