Attached.Variants (Attached v0.1.0)

Copy Markdown View Source

Context module for cached variant derivations of Attached.Originals.Original.

A variant is produced on demand from a parent original and a named transform declared on the parent schema, then cached as an Attached.Variants.Variant row pointing back at its parent original via original_id.

Custom transformations

Use fn: to bypass the built-in transformer entirely:

attached :report, variants: %{
  text: [fn: &__MODULE__.to_text/3, mime_type: "text/plain"]
}

def to_text(input_path, _transforms, output_path) do
  System.cmd("pdftotext", [input_path, output_path])
  :ok
end

The function receives (input_path, transforms, output_path) and must return :ok or {:error, reason}. Use named function captures only — anonymous functions produce non-deterministic variant digests.

mime_type: sets the content type of the stored variant. Defaults to "image/png" when omitted.

quality: (integer 1–100) sets the encoder quality for the output file. Honored by jpeg, webp, and gif; ignored for png. Different quality values produce distinct cached variants.

attached :header_image, variants: %{
  medium: [resize_to_limit: {700, 700}, mime_type: "image/webp", quality: 80]
}

Querying

This module exposes the standard CRUD shape against attached_variants: list/1, get/2, get!/2, count/1, paginate/1. See Attached.Ecto.CRUD for the supported option set.

Attached.Variants.list(order_by: [desc: :inserted_at], limit: 50)

Summary

Functions

Counts variants matching the given options.

Deletes all variants belonging to original — both their storage files and DB rows.

Fetches a variant by id. Returns nil if not found.

Fetches a variant by id. Raises Ecto.NoResultsError if not found.

Reverse of path_for/2 — given a storage path, returns the matching variant (or nil if the path doesn't refer to one).

Fetches the variant for original with transform_digest, or nil if none exists yet.

Returns variants matching the given options.

Paginates variants.

Returns the storage path for variant (a %Variant{} or {name, transform_digest} pair) belonging to parent.

Returns {:ok, url} for a cached preview image of original, generating the variant on first call. Returns {:error, reason} when the original is not previewable or the transform fails.

Returns true when a preview image can be produced for original.

Returns the cached %Variant{} for original with transform_digest, generating and storing it if it doesn't exist yet.

Deletes a variant and its storage file synchronously.

Returns the deterministic digest for transforms, used as the cache key in attached_variants.transform_digest.

Returns the transforms configured for variant_name on field of record.

Functions

count(opts \\ [])

Counts variants matching the given options.

Accepts the same :query hook as list/1.

delete_for!(original)

Deletes all variants belonging to original — both their storage files and DB rows.

Idempotent. Called explicitly from Attached.Originals.purge!/1 before the parent delete so storage gets cleaned (FK cascade would handle the DB rows but not the files).

get(id, opts \\ [])

Fetches a variant by id. Returns nil if not found.

Supports :preload and :query.

get!(id, opts \\ [])

Fetches a variant by id. Raises Ecto.NoResultsError if not found.

Supports :preload and :query.

get_by_path(path)

Reverse of path_for/2 — given a storage path, returns the matching variant (or nil if the path doesn't refer to one).

Used by Attached.Web.Plug to resolve the variant's content type when serving a variant URL. Falls back to a LIKE match on the 4-char transform-digest prefix encoded in the path.

get_for(original, transform_digest)

Fetches the variant for original with transform_digest, or nil if none exists yet.

list(opts \\ [])

Returns variants matching the given options.

See Attached.Ecto.CRUD for the supported option set (:preload, :order_by, :limit, :offset, :select, :distinct, :exclude_nil, :query).

paginate(opts \\ [])

Paginates variants.

Accepts the same :query/:order_by/:preload/:select options as list/1, plus:

  • :page — 1-based page number (default 1)
  • :per_page — items per page (default 25)

Returns %{entries: [...], total: n, page: p, per_page: pp}.

path_for(parent, variant)

Returns the storage path for variant (a %Variant{} or {name, transform_digest} pair) belonging to parent.

Variants live under a _variants/ namespace separate from the original files, so listings and backups can treat derived files independently. Prefix-based cleanup therefore requires two calls: one for the parent key and one for the _variants/<parent.key> prefix.

path_for(original, name, transform_digest)

preview_url(original)

Returns {:ok, url} for a cached preview image of original, generating the variant on first call. Returns {:error, reason} when the original is not previewable or the transform fails.

The preview is a small image/png (≤400×400), cached as a variant so subsequent calls are cheap.

previewable?(arg1)

Returns true when a preview image can be produced for original.

Any image/* original is previewable. For other content types an image previewer must accept the type (which includes a runtime availability check — e.g. ffmpeg installed for video, pdftoppm for PDF).

process(original, transform_digest, transforms)

Returns the cached %Variant{} for original with transform_digest, generating and storing it if it doesn't exist yet.

Idempotent — safe to call concurrently; a unique index on (original_id, transform_digest) prevents duplicate rows.

purge!(variant_id)

Deletes a variant and its storage file synchronously.

Accepts a %Variant{} struct (with :original preloaded) or a variant id.

transform_digest(transforms)

Returns the deterministic digest for transforms, used as the cache key in attached_variants.transform_digest.

The :variant_name key is excluded from the hash so renaming a variant does not invalidate its cached original.

transforms_for(record, field, variant_name)

Returns the transforms configured for variant_name on field of record.

Raises ArgumentError if the variant is not declared.