Attached.Originals (Attached v0.1.1)

Copy Markdown View Source

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):

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

count(opts \\ [])

Counts originals matching the given options.

Accepts the same :query hook as list/1.

count_orphans()

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.

count_orphans(owner_table, owner_field)

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.

create_from_file!(path, opts)

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 to Path.basename(path)
  • :content_type — defaults to "application/octet-stream" (refined by Attached.Originals.ContentType unless disabled)

Variants go through Attached.Variants.process/3, not here.

create_from_stream!(stream, opts)

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.

create_from_upload!(original, opts)

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.

extract_metadata_later(original_id)

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).

get(id, opts \\ [])

Fetches an original by id. Returns nil if not found.

Options

  • :preload — associations to preload
  • :query — 1-arity function for additional composition

get!(id, opts \\ [])

Fetches an original by id. Raises Ecto.NoResultsError if not found.

Takes the same options as get/2.

get_by_key(key, opts \\ [])

Fetches an original by its storage key. Returns nil if not found.

Options

  • :preload — associations to preload
  • :query — 1-arity function for additional composition

get_owner(original)

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).

list(opts \\ [])

Returns originals matching the given options.

Options

  • :preload — associations to preload
  • :order_by — passed to Ecto.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 (implies select, distinct, and order_by on the field). Useful for filter-dropdown lookups.
  • :exclude_nil — when true together with :distinct, filters out rows where the distinct field is nil.
  • :query — 1-arity function for additional composition, e.g. &Scopes.orphans(&1, "users", "avatar_attached_original_id")

list_orphan_groups()

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.

list_orphans(owner_table, owner_field, limit \\ nil, offset \\ nil)

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.

list_owner_groups()

Returns the distinct %{owner_table, owner_field} pairs across all originals.

Use this to drive per-group orphan sweeps (see Scopes.orphans/3).

paginate(opts \\ [])

Paginates originals with 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 a map %{entries: [...], total: n, page: p, per_page: pp}.

purge!(original_id)

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.

purge_by_owner_group(owner_table, owner_field)

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")

purge_later(original_id)

Enqueues a job to purge an original asynchronously. Accepts a %Original{} or its id.

purge_orphans_later()

Enqueues a scan-and-purge pass over all orphaned originals.

update_metadata!(original, metadata)

Merges metadata into original.metadata and persists it.