Rindle (Rindle v0.1.5)

Copy Markdown View Source

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

storage_result()

@type storage_result() :: {:ok, term()} | {:error, term()}

Tagged storage result shape: {:ok, result} | {:error, reason}

Functions

attach(asset_or_id, owner, slot, opts \\ [])

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

attach!(asset_or_id, owner, slot, opts \\ [])

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.

attachment_for(owner, slot, opts \\ [])

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

cancel_processing(asset_or_id)

@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}

cancel_resumable_session(session_id, opts \\ [])

@spec cancel_resumable_session(
  binary(),
  keyword()
) :: Rindle.Upload.Broker.cancel_resumable_result()

Cancels a broker-owned resumable upload session.

complete_multipart_upload(session_id, parts, opts \\ [])

@spec complete_multipart_upload(binary(), [map()], keyword()) ::
  Rindle.Upload.Broker.verify_result()

Completes a multipart upload through the broker and reuses upload verification.

delete(profile, key, opts \\ [])

@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

detach(owner, slot, opts \\ [])

@spec detach(struct(), String.t(), keyword()) :: :ok | {:error, term()}

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

detach!(owner, slot, opts \\ [])

@spec detach!(struct(), String.t(), keyword()) :: :ok

Same as detach/3 but raises Rindle.Error on failure.

download(profile, key, destination_path, opts \\ [])

@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

head(profile, key, opts \\ [])

@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

initiate_multipart_upload(profile, opts \\ [])

@spec initiate_multipart_upload(
  module(),
  keyword()
) :: Rindle.Upload.Broker.initiate_multipart_result()

Initiates a multipart direct upload session through the broker.

initiate_resumable_session(profile, opts \\ [])

@spec initiate_resumable_session(
  module(),
  keyword()
) :: Rindle.Upload.Broker.initiate_resumable_result()

Initiates a resumable upload session through the broker.

initiate_upload(profile, opts \\ [])

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

presigned_put(profile, key, expires_in, opts \\ [])

@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

ready_variants_for(asset_or_id)

@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

reprobe(asset_or_id)

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

requeue_variants(asset_or_id, opts \\ [])

@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

resumable_session_status(session_id, opts \\ [])

@spec resumable_session_status(
  binary(),
  keyword()
) :: Rindle.Upload.Broker.resumable_status_result()

Polls the broker-owned resumable upload session without changing completion trust.

runtime_status(opts \\ [])

@spec runtime_status(keyword() | map()) :: {:ok, map()} | {:error, term()}

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

sign_multipart_part(session_id, part_number, opts \\ [])

@spec sign_multipart_part(binary(), pos_integer(), keyword()) ::
  Rindle.Upload.Broker.sign_part_result()

Signs a single multipart upload part through the broker.

storage_adapter_for(profile)

@spec storage_adapter_for(module()) :: module()

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

store(profile, key, source_path, opts \\ [])

@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

store_variant(profile, key, source_path, opts \\ [])

@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

upload(profile_module, upload, opts \\ [])

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

upload!(profile_module, upload, opts \\ [])

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

url(profile, key, opts \\ [])

@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

url!(profile, key, opts \\ [])

@spec url!(module(), String.t(), keyword()) :: String.t()

Same as url/3 but raises Rindle.Error on failure or re-raises the original exception for storage adapter exceptions.

variant_url(profile, asset, variant, opts \\ [])

@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

variant_url!(profile, asset, variant, opts \\ [])

@spec variant_url!(module(), map(), map(), keyword()) :: String.t()

Same as variant_url/4 but raises Rindle.Error on failure or re-raises the original exception for storage adapter exceptions.

verify_completion(session_id, opts \\ [])

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

verify_upload(session_id, opts \\ [])

This function is deprecated. Use verify_completion/2.
@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"

version()

@spec version() :: String.t()

Returns the current version of Rindle.

Examples

iex> is_binary(Rindle.version())
true