Context module for originals.
Original-entity-level operations. The top-level Attached module exposes a
record-oriented API (put_attached/3, Attached.purge/2) that works with
user schemas and fields; this module exposes the original-id-oriented
counterparts for dashboards, scripts, and custom tooling.
All worker enqueues go through this module — the workers themselves
(ExtractMetadataWorker, PurgeWorker, PurgeOrphansWorker) should
not be called directly.
Ingestion
Original ingestion has three entry points, all funneling into the same pipeline (store, stat, checksum, insert, enqueue analysis):
create_from_upload!/2— duck-typed uploads with a:pathkey (e.g.%Plug.Upload{}, plain maps from Oban jobs).create_from_file!/2— a local path on disk.create_from_stream!/2— the primitive. AnyEnumerableof binary chunks. Use this when the bytes come from somewhere else (HTTP download, S3 copy, binary DB column, in-memory buffer).
Variant ingestion lives in Attached.Variants and writes to the
attached_variants table — see Attached.Variants.process/3.
Querying
list/1 and count/1 accept the same composable option set as
ecto_context-generated functions:
# All originals
Attached.Originals.list()Orphan detection happens per (owner_table, owner_field) group, since
SQL identifiers can't be bound per row. Use list_owner_groups/0 to iterate
the distinct groups and orphans/3 as the filter building block:
Attached.Originals.list_owner_groups()
|> Enum.flat_map(fn %{owner_table: table, owner_field: field} ->
Attached.Originals.list(query: &Scopes.orphans(&1, table, field))
end)
Summary
Functions
Counts originals matching the given options.
Total count of orphaned originals across every (owner_table, owner_field) group.
Counts orphaned originals within a single (owner_table, owner_field) group.
Ingests an original file at path into storage and inserts the original row.
Ingests an Enumerable of binary chunks.
Ingests a duck-typed upload (%Plug.Upload{}, plain map with :path).
Enqueues a job to extract metadata from an original asynchronously.
Fetches an original by id. Returns nil if not found.
Fetches an original by id. Raises Ecto.NoResultsError if not found.
Fetches an original by its storage key. Returns nil if not found.
Looks up the owner row that references this original.
Returns originals matching the given options.
Returns orphan summary per group as [%{owner_table, owner_field, orphan_count, total_bytes}].
Lists orphaned originals within a single (owner_table, owner_field) group,
ordered by inserted_at descending.
Returns the distinct %{owner_table, owner_field} pairs across all originals.
Paginates originals with the same :query/:order_by/:preload/:select
options as list/1, plus
Synchronously deletes an original, its variants, and all associated storage files.
Enqueues purge jobs for all orphaned originals in a specific (owner_table, owner_field) group.
Enqueues a job to purge an original asynchronously. Accepts a %Original{} or its id.
Enqueues a scan-and-purge pass over all orphaned originals.
Merges metadata into original.metadata and persists it.
Functions
Counts originals matching the given options.
Accepts the same :query hook as list/1.
Total count of orphaned originals across every (owner_table, owner_field) group.
Groups whose owner_field is not a real column on owner_table are skipped
(with a Logger.warning/1) rather than raising.
Counts orphaned originals within a single (owner_table, owner_field) group.
Returns 0 and logs a warning if owner_field is not a real column on
owner_table.
Ingests an original file at path into storage and inserts the original row.
This is the original-ingest primitive — create_from_upload!/2 and
create_from_stream!/2 both funnel into it (an upload already has a path
on disk; a stream gets materialized to a tmp file first).
Options:
:owner_table(required):owner_field(required, coerced to string):filename— defaults toPath.basename(path):content_type— defaults to"application/octet-stream"(refined byAttached.Originals.ContentTypeunless disabled)
Variants go through Attached.Variants.process/3, not here.
Ingests an Enumerable of binary chunks.
Writes the stream to a tmp file, then delegates to create_from_file!/2
(storage backends need a path for efficient upload). Use this when bytes
come from a source the other helpers don't cover — HTTP downloads, S3
copies, in-memory buffers.
:filename is required since there's no path to derive it from.
Ingests a duck-typed upload (%Plug.Upload{}, plain map with :path).
Short-circuits when upload is already a %Attached.Originals.Original{} — returns
it unchanged, so callers can accept either fresh uploads or pre-existing
originals (e.g. re-attaching an original to a new record).
Pulls :filename and :content_type from the struct (unless explicitly
passed) and delegates to create_from_file!/2.
Enqueues a job to extract metadata from an original asynchronously.
Runs the first accepting Attached.Processors.MetadataExtractors module
against the original and merges the extracted fields into original.metadata —
e.g. width/height for images, duration/bit_rate for audio,
width/height/duration/angle/aspect_ratio/audio/video for
video. The MIME type is not touched (that's set at ingest time by
Attached.Originals.ContentType).
Called automatically from ingest!/4 after insert; safe to re-enqueue
by hand (e.g. after adding a new extractor).
Fetches an original by id. Returns nil if not found.
Options
:preload— associations to preload:query— 1-arity function for additional composition
Fetches an original by id. Raises Ecto.NoResultsError if not found.
Takes the same options as get/2.
Fetches an original by its storage key. Returns nil if not found.
Options
:preload— associations to preload:query— 1-arity function for additional composition
Looks up the owner row that references this original.
Queries original.owner_table for a row whose original.owner_field equals
original.id and returns it as a plain map. Returns nil if no such row
exists, or if owner_field is not a real column on owner_table
(e.g. legacy rows pointing at a removed field).
Returns originals matching the given options.
Options
:preload— associations to preload:order_by— passed toEcto.Query.order_by/2:limit— max results:offset— number of rows to skip (for pagination with:limit):select— list of fields to select:distinct— field atom; returns distinct values of that field (impliesselect,distinct, andorder_byon the field). Useful for filter-dropdown lookups.:exclude_nil— whentruetogether with:distinct, filters out rows where the distinct field isnil.:query— 1-arity function for additional composition, e.g.&Scopes.orphans(&1, "users", "avatar_attached_original_id")
Returns orphan summary per group as [%{owner_table, owner_field, orphan_count, total_bytes}].
Groups with zero orphans are omitted. Groups whose owner_field is not a real
column on owner_table are skipped with a Logger.warning/1.
Lists orphaned originals within a single (owner_table, owner_field) group,
ordered by inserted_at descending.
Returns [] and logs a warning if owner_field is not a real column on
owner_table.
Returns the distinct %{owner_table, owner_field} pairs across all originals.
Use this to drive per-group orphan sweeps (see Scopes.orphans/3).
Paginates originals with the same :query/:order_by/:preload/:select
options as list/1, plus:
:page— 1-based page number (default1):per_page— items per page (default25)
Returns a map %{entries: [...], total: n, page: p, per_page: pp}.
Synchronously deletes an original, its variants, and all associated storage files.
Cascades to Attached.Variants.delete_for!/1 for the variant cleanup,
then deletes the original row and its storage object.
Accepts either a %Original{} struct or an original id — the id form loads the
original first and is a no-op if it no longer exists.
Enqueues purge jobs for all orphaned originals in a specific (owner_table, owner_field) group.
Useful when you want to clean up a single group rather than all orphans at once:
Attached.Originals.purge_by_owner_group("users", "avatar_attached_original_id")
Enqueues a job to purge an original asynchronously. Accepts a %Original{} or its id.
Enqueues a scan-and-purge pass over all orphaned originals.
Merges metadata into original.metadata and persists it.