Phoenix/Ecto-native media lifecycle library.
Rindle manages the full post-upload lifecycle: upload sessions, staged object verification, asset modeling, attachment associations, variant/derivative generation, background processing, secure delivery, observability, and day-2 operations.
Summary
Types
Tagged storage result shape: {:ok, result} | {:error, reason}
Functions
Attaches a MediaAsset to an owner at a specific slot.
Same as attach/4 but raises Rindle.Error on failure or re-raises the original exception for storage adapter exceptions. Database constraint failures (e.g., foreign-key violations) surface as Rindle.Error with the underlying changeset as the reason.
Fetches the most recent MediaAttachment for an (owner, slot) pair.
Cancels active variant processing for an asset.
Cancels a broker-owned resumable upload session.
Completes a multipart upload through the broker and reuses upload verification.
Deletes an object through the profile-specific storage adapter.
Detaches any MediaAsset from an owner at a specific slot and triggers a purge.
Same as detach/3 but raises Rindle.Error on failure.
Downloads an object through the profile-specific storage adapter.
Checks for object existence through the profile-specific storage adapter.
Initiates a multipart direct upload session through the broker.
Initiates a resumable upload session through the broker.
Initiates a direct upload session through the broker.
Generates a presigned PUT payload through the profile-specific storage adapter.
Lists MediaVariant rows in the "ready" state for a given asset.
Reruns probe detection for an asset and persists only probe-derived fields.
Requeues failed or cancelled variants for a single asset.
Polls the broker-owned resumable upload session without changing completion trust.
Returns a bounded runtime diagnostics report for operators.
Signs a single multipart upload part through the broker.
Resolves the storage adapter module for a given profile.
Stores an object through the profile-specific storage adapter.
Executes variant storage and logs failures with required context metadata.
Uploads a file directly through the server (proxied upload).
Same as upload/3 but raises Rindle.Error on failure, Ecto.InvalidChangesetError for changeset failures, or re-raises the original exception for storage adapter exceptions.
Generates a delivery URL through the profile-specific storage adapter.
Same as url/3 but raises Rindle.Error on failure or re-raises the original exception for storage adapter exceptions.
Generates a delivery URL for a variant, falling back when needed.
Same as variant_url/4 but raises Rindle.Error on failure or re-raises the original exception for storage adapter exceptions.
Verifies a direct upload completion through the broker.
Verifies a direct upload completion through the broker.
Returns the current version of Rindle.
Types
Functions
@spec attach(Rindle.Domain.MediaAsset.t() | binary(), struct(), String.t(), keyword()) :: {:ok, Rindle.Domain.MediaAttachment.t()} | {:error, term()}
Attaches a MediaAsset to an owner at a specific slot.
If an attachment already exists in that slot, it is replaced and the old
asset is purged asynchronously via PurgeStorage.
Examples
# Requires a configured Rindle repo + an existing MediaAsset and owner record.
iex> {:ok, attachment} = Rindle.attach(asset_id, %MyApp.User{id: user_id}, "avatar")
iex> attachment.slot
"avatar"
@spec attach!( Rindle.Domain.MediaAsset.t() | binary(), struct(), String.t(), keyword() ) :: Rindle.Domain.MediaAttachment.t()
Same as attach/4 but raises Rindle.Error on failure or re-raises the original exception for storage adapter exceptions. Database constraint failures (e.g., foreign-key violations) surface as Rindle.Error with the underlying changeset as the reason.
@spec attachment_for(struct(), String.t(), keyword()) :: Rindle.Domain.MediaAttachment.t() | nil
Fetches the most recent MediaAttachment for an (owner, slot) pair.
Returns the attachment row with :asset preloaded by default, or nil
when no attachment exists at the slot. Pass preload: <list> to override
the default preload (the override replaces [:asset] rather than
merging — pass preload: [asset: :variants] to extend, preload: [] to
disable preloading entirely).
When multiple attachment rows exist for the same (owner, slot) (possible
because the join schema enforces uniqueness only at the application level
via attach/4's last-write-wins replacement), the most-recent row by
:inserted_at is returned.
This helper does not issue a write or a side-effect query; it is safe to call in render paths.
Examples
# Requires a configured Rindle repo + an existing owner record.
iex> attachment = Rindle.attachment_for(%MyApp.User{id: user_id}, "avatar")
iex> attachment && attachment.slot
"avatar"
@spec cancel_processing(Rindle.Domain.MediaAsset.t() | binary()) :: :ok | {:error, :not_processing}
Cancels active variant processing for an asset.
Returns :ok when the asset has queued or executing variant work that can be
cancelled. Returns {:error, :not_processing} when the asset has no queued
or executing variant work.
The public surface remains asset-scoped; callers do not need to know variant ids, job ids, or Oban internals to stop in-flight processing.
Examples
iex> Rindle.cancel_processing(asset_id)
:ok
iex> Rindle.cancel_processing("missing-or-idle-asset")
{:error, :not_processing}
@spec cancel_resumable_session( binary(), keyword() ) :: Rindle.Upload.Broker.cancel_resumable_result()
Cancels a broker-owned resumable upload session.
@spec complete_multipart_upload(binary(), [map()], keyword()) :: Rindle.Upload.Broker.verify_result()
Completes a multipart upload through the broker and reuses upload verification.
@spec delete(module(), String.t(), keyword()) :: storage_result()
Deletes an object through the profile-specific storage adapter.
Examples
# Requires a configured storage adapter.
iex> {:ok, _} = Rindle.delete(MyApp.MediaProfile, "uploads/abc.png")
iex> :ok
:ok
Detaches any MediaAsset from an owner at a specific slot and triggers a purge.
Idempotent: returns :ok even when no attachment exists at the slot.
Examples
# Requires a configured Rindle repo + an existing attachment at owner+slot.
iex> :ok = Rindle.detach(%MyApp.User{id: user_id}, "avatar")
iex> :ok
:ok
Same as detach/3 but raises Rindle.Error on failure.
@spec download(module(), String.t(), Path.t(), keyword()) :: storage_result()
Downloads an object through the profile-specific storage adapter.
Examples
# Requires a configured storage adapter and an existing object.
iex> {:ok, _meta} = Rindle.download(MyApp.MediaProfile, "uploads/abc.png", "/tmp/abc.png")
iex> :ok
:ok
@spec head(module(), String.t(), keyword()) :: storage_result()
Checks for object existence through the profile-specific storage adapter.
Examples
# Requires a configured storage adapter.
iex> {:ok, _meta} = Rindle.head(MyApp.MediaProfile, "uploads/abc.png")
iex> :ok
:ok
@spec initiate_multipart_upload( module(), keyword() ) :: Rindle.Upload.Broker.initiate_multipart_result()
Initiates a multipart direct upload session through the broker.
@spec initiate_resumable_session( module(), keyword() ) :: Rindle.Upload.Broker.initiate_resumable_result()
Initiates a resumable upload session through the broker.
@spec initiate_upload( module(), keyword() ) :: {:ok, Rindle.Domain.MediaUploadSession.t()} | {:error, term()}
Initiates a direct upload session through the broker.
Delegates to Broker.initiate_session/2. Returns
{:ok, %MediaUploadSession{}} on success.
Examples
# Requires `config :rindle, :repo, MyApp.Repo` and a configured profile module.
iex> {:ok, session} = Rindle.initiate_upload(MyApp.MediaProfile, filename: "photo.png")
iex> session.state
"initialized"
@spec presigned_put(module(), String.t(), pos_integer(), keyword()) :: storage_result()
Generates a presigned PUT payload through the profile-specific storage adapter.
Examples
# Requires an S3-compatible storage adapter with :presigned_put capability.
iex> {:ok, %{url: url}} = Rindle.presigned_put(MyApp.MediaProfile, "uploads/abc.png", 3600)
iex> is_binary(url)
true
@spec ready_variants_for(Rindle.Domain.MediaAsset.t() | binary()) :: [ Rindle.Domain.MediaVariant.t() ]
Lists MediaVariant rows in the "ready" state for a given asset.
Accepts either a %MediaAsset{} struct or a binary asset id. Returns a
list of variants ordered by :name ascending; returns [] when no
variants are ready. The unique constraint on (asset_id, name) makes the
ordering deterministic.
Only variants in the "ready" state are returned — variants in
"planned", "queued", "processing", "stale", "missing",
"failed", or "purged" are excluded. Adopters wanting fallback
behavior should call variant_url/4, which already orchestrates the
stale-policy fallback.
Examples
# Requires a configured Rindle repo + at least one ready variant row.
iex> variants = Rindle.ready_variants_for(asset)
iex> Enum.all?(variants, &(&1.state == "ready"))
true
@spec reprobe(Rindle.Domain.MediaAsset.t() | binary()) :: {:ok, Rindle.Ops.LifecycleRepair.reprobe_report()} | {:error, term()}
Reruns probe detection for an asset and persists only probe-derived fields.
Accepts either a %MediaAsset{} struct or a binary asset id. Reprobe is
asset-scoped and refreshes only content_type, kind, width, height,
duration_ms, has_video_track, and has_audio_track; fields that no
longer apply are cleared explicitly, while unrelated lifecycle state and
ownership data stay untouched.
Returns {:ok, report} on a completed probe refresh and {:error, reason}
when the run could not be completed.
Examples
iex> {:ok, report} = Rindle.reprobe(asset_id)
iex> report.kind
"image"
@spec requeue_variants(Rindle.Domain.MediaAsset.t() | binary(), keyword() | map()) :: {:ok, Rindle.Ops.LifecycleRepair.requeue_report()} | {:error, term()}
Requeues failed or cancelled variants for a single asset.
Accepts either a %MediaAsset{} struct or a binary asset id. By default,
only this asset's variants currently in failed or cancelled state are
targeted. Pass variant_names: [...] to narrow the repair to explicit
variant names; unknown names fail loudly, and already-ready siblings stay
untouched.
Returns {:ok, report} after the enqueue attempt finishes, including
deterministic counters for selected, enqueued, skipped, and errored
variants. Equivalent in-flight jobs are counted as skipped through Oban
uniqueness rather than double-enqueued.
Examples
iex> {:ok, report} = Rindle.requeue_variants(asset_id)
iex> report.enqueued
1
iex> {:ok, report} = Rindle.requeue_variants(asset_id, variant_names: ["thumb"])
iex> report.selected
1
@spec resumable_session_status( binary(), keyword() ) :: Rindle.Upload.Broker.resumable_status_result()
Polls the broker-owned resumable upload session without changing completion trust.
Returns a bounded runtime diagnostics report for operators.
The report is read-only and groups lifecycle drift, stuck work, and upload
residue into a stable map shape with counts, oldest age, and bounded
examples. Supported filters are intentionally narrow: :profile,
:older_than, :limit, and :format.
Examples
iex> {:ok, report} = Rindle.runtime_status(limit: 3)
iex> is_map(report.variants)
true
@spec sign_multipart_part(binary(), pos_integer(), keyword()) :: Rindle.Upload.Broker.sign_part_result()
Signs a single multipart upload part through the broker.
Resolves the storage adapter module for a given profile.
Examples
# Requires a profile module that defines `storage_adapter/0`.
iex> Rindle.storage_adapter_for(MyApp.MediaProfile)
Rindle.Storage.Local
@spec store(module(), String.t(), Path.t(), keyword()) :: storage_result()
Stores an object through the profile-specific storage adapter.
Examples
# Requires a configured storage adapter and a readable source file.
iex> {:ok, _meta} = Rindle.store(MyApp.MediaProfile, "uploads/abc.png", "/tmp/abc.png")
iex> :ok
:ok
@spec store_variant(module(), String.t(), Path.t(), keyword()) :: storage_result()
Executes variant storage and logs failures with required context metadata.
Wraps store/4 with structured failure logging that captures the
asset_id and variant_name for observability dashboards.
Examples
# Requires a configured storage adapter.
iex> {:ok, _meta} = Rindle.store_variant(MyApp.MediaProfile, "variants/abc-thumb.png", "/tmp/abc-thumb.png", asset_id: asset_id, variant_name: "thumb")
iex> :ok
:ok
@spec upload(module(), map() | Plug.Upload.t(), keyword()) :: {:ok, Rindle.Domain.MediaAsset.t()} | {:error, term()}
Uploads a file directly through the server (proxied upload).
Accepts a profile module and an upload (map or %Plug.Upload{}).
The file is validated against the profile's upload_policy/0, stored
via the profile's storage adapter, and a MediaAsset row is inserted
in the analyzing state.
Examples
# Requires a configured Rindle repo + a configured storage adapter + a Plug.Upload.
iex> {:ok, asset} = Rindle.upload(MyApp.MediaProfile, %Plug.Upload{path: "/tmp/x.png", filename: "x.png"})
iex> asset.state
"analyzing"
@spec upload!(module(), map() | Plug.Upload.t(), keyword()) :: Rindle.Domain.MediaAsset.t()
Same as upload/3 but raises Rindle.Error on failure, Ecto.InvalidChangesetError for changeset failures, or re-raises the original exception for storage adapter exceptions.
@spec url(module(), String.t(), keyword()) :: storage_result()
Generates a delivery URL through the profile-specific storage adapter.
Delegates to Rindle.Delivery.url/3 so policy (public vs. signed) is honored.
Examples
# Requires a configured storage adapter and a key that exists in storage.
iex> {:ok, url} = Rindle.url(MyApp.MediaProfile, "uploads/abc.png")
iex> is_binary(url)
true
Same as url/3 but raises Rindle.Error on failure or re-raises the original exception for storage adapter exceptions.
@spec variant_url(module(), map(), map(), keyword()) :: storage_result()
Generates a delivery URL for a variant, falling back when needed.
Delegates to Rindle.Delivery.variant_url/4. Stale or non-ready variants
fall back to the original asset URL per the configured stale-serving policy.
Examples
# Requires a configured storage adapter and ready/stale variant rows.
iex> {:ok, url} = Rindle.variant_url(MyApp.MediaProfile, asset, variant)
iex> is_binary(url)
true
Same as variant_url/4 but raises Rindle.Error on failure or re-raises the original exception for storage adapter exceptions.
@spec verify_completion( binary(), keyword() ) :: Rindle.Upload.Broker.verify_result()
Verifies a direct upload completion through the broker.
Delegates to Broker.verify_completion/2. Promotes the
session to completed and the asset to validating.
Examples
# Requires a configured Rindle repo + the upload object to exist in storage.
iex> {:ok, %{session: session, asset: asset}} = Rindle.verify_completion(session_id)
iex> session.state
"completed"
iex> asset.state
"validating"
@spec verify_upload( binary(), keyword() ) :: Rindle.Upload.Broker.verify_result()
Verifies a direct upload completion through the broker.
Legacy compatibility shim for 0.1.x. Delegates to
verify_completion/2 while the older name remains supported.
Examples
# Requires a configured Rindle repo + the upload object to exist in storage.
iex> {:ok, %{session: session, asset: asset}} = Rindle.verify_upload(session_id)
iex> session.state
"completed"
iex> asset.state
"validating"
@spec version() :: String.t()
Returns the current version of Rindle.
Examples
iex> is_binary(Rindle.version())
true