PhxMediaLibrary (PhxMediaLibrary v0.6.1)

Copy Markdown View Source

A robust, Ecto-backed media management library for Elixir and Phoenix.

PhxMediaLibrary provides a simple, fluent API for:

  • Associating files with Ecto schemas
  • Organizing media into collections
  • Storing files across different storage backends (local, S3)
  • Generating image conversions (thumbnails, etc.)
  • Creating responsive images for optimal loading

Quick Start

  1. Add use PhxMediaLibrary.HasMedia to your Ecto schema
  2. Run mix phx_media_library.install to generate the migration
  3. Start associating media with your models!

Example

# In your schema
defmodule MyApp.Post do
  use Ecto.Schema
  use PhxMediaLibrary.HasMedia

  schema "posts" do
    field :title, :string
    has_media()
    timestamps()
  end

  def media_collections do
    [
      collection(:images),
      collection(:documents, accepts: ~w(application/pdf)),
      collection(:avatar, single_file: true)
    ]
  end

  def media_conversions do
    [
      conversion(:thumb, width: 150, height: 150, fit: :cover),
      conversion(:preview, width: 800)
    ]
  end
end

# Adding media
post
|> PhxMediaLibrary.add("/path/to/image.jpg")
|> PhxMediaLibrary.to_collection(:images)

# Retrieving media
PhxMediaLibrary.get_first_media_url(post, :images)
PhxMediaLibrary.get_first_media_url(post, :images, :thumb)

Summary

Functions

Start adding media to a model from a file path or upload.

Add media from a remote URL.

Get the BlurHash string for a media item, or nil if not generated.

Get a CDN-friendly URL with a cache-busting fingerprint query parameter.

Delete all media in a collection for a model.

Delete all media for a model.

Complete a direct (presigned) upload by creating the Media record.

Delete a media item and its files from storage.

Get a download URL that triggers Content-Disposition: attachment.

Get the first media item for a model in a collection.

Get the URL for the first media item in a collection.

Get all media for a model, optionally filtered by collection.

Get all soft-deleted media for a model, optionally filtered by collection.

Returns an Ecto.Query for all media belonging to a model, optionally filtered by collection.

Move a single media item to a specific position within its collection.

Get the filesystem path for a media item (local storage only).

Permanently delete a media item and its files from storage.

Generate a presigned URL for direct client-to-storage uploads.

Permanently delete all trashed media for a model that was soft-deleted before the given DateTime. Removes files from storage and database records.

Reorder media items in a collection by a list of IDs.

Restore a soft-deleted media item by clearing its deleted_at timestamp.

Get a signed, time-limited URL for a media item.

Soft-delete a media item by setting its deleted_at timestamp.

Get the srcset attribute value for responsive images.

Finalize adding media to a collection.

Check whether a media item has been soft-deleted.

Get the URL for a media item, optionally for a specific conversion.

Set a custom filename for the media.

Verify the integrity of a stored media file by comparing its stored checksum against a freshly computed one.

Attach custom properties (metadata) to the media.

Enable responsive image generation for this media.

Disable automatic metadata extraction for this media.

Functions

add(model, source)

Start adding media to a model from a file path or upload.

Returns a MediaAdder struct that can be piped through configuration functions before finalizing with to_collection/2.

Examples

post
|> PhxMediaLibrary.add("/path/to/file.jpg")
|> PhxMediaLibrary.to_collection(:images)

add_from_url(model, url, opts \\ [])

@spec add_from_url(Ecto.Schema.t(), String.t(), keyword()) ::
  PhxMediaLibrary.MediaAdder.t()

Add media from a remote URL.

The file will be downloaded and stored locally before processing. URL validation ensures only http and https schemes are accepted.

Options

  • :headers — custom request headers (e.g. [{"Authorization", "Bearer token"}])
  • :timeout — download timeout in milliseconds

Telemetry

Downloads emit [:phx_media_library, :download, :start | :stop | :exception] events with URL, size, and MIME type metadata.

Examples

post
|> PhxMediaLibrary.add_from_url("https://example.com/image.jpg")
|> PhxMediaLibrary.to_collection(:images)

# With authentication
post
|> PhxMediaLibrary.add_from_url("https://api.example.com/file.pdf",
     headers: [{"Authorization", "Bearer my-token"}])
|> PhxMediaLibrary.to_collection(:documents)

blurhash(media)

@spec blurhash(PhxMediaLibrary.Media.t()) :: String.t() | nil

Get the BlurHash string for a media item, or nil if not generated.

Blurhash must be opted in via config :phx_media_library, responsive_images: [blurhash: true] and the :image library must be available.

Use the <PhxMediaLibrary.Components.blurhash> component to render the decoded placeholder client-side via a <canvas> element.

Examples

PhxMediaLibrary.blurhash(media)
#=> "LKO2?V%2Tw=w]~RBVZRi};RPxuwH"

cdn_url(media, conversion \\ nil)

@spec cdn_url(PhxMediaLibrary.Media.t(), atom() | nil) :: String.t()

Get a CDN-friendly URL with a cache-busting fingerprint query parameter.

Appends ?v={checksum[0..7]} when the media item has a stored checksum so CDN edges pick up the new file whenever it is replaced. Falls back to a plain URL when no checksum is stored.

Equivalent to PhxMediaLibrary.url(media, conversion, cache_bust: true).

Examples

PhxMediaLibrary.cdn_url(media)
#=> "/uploads/images/1/uuid/photo.jpg?v=a1b2c3d4"

clear_collection(model, collection_name)

@spec clear_collection(Ecto.Schema.t(), atom()) ::
  {:ok, non_neg_integer()} | {:error, term()}

Delete all media in a collection for a model.

Deletes files from storage for each item, then removes all matching database records in a single query. This is significantly more efficient than deleting one-by-one for large collections.

Examples

PhxMediaLibrary.clear_collection(post, :images)

clear_media(model)

@spec clear_media(Ecto.Schema.t()) :: {:ok, non_neg_integer()} | {:error, term()}

Delete all media for a model.

Deletes files from storage for each item, then removes all matching database records in a single query.

Examples

PhxMediaLibrary.clear_media(post)

complete_external_upload(model, collection_name, storage_path, opts)

@spec complete_external_upload(Ecto.Schema.t(), atom(), String.t(), keyword()) ::
  {:ok, PhxMediaLibrary.Media.t()} | {:error, term()}

Complete a direct (presigned) upload by creating the Media record.

Call this after the client has finished uploading directly to storage. The file is already stored — this function only creates the database record and optionally triggers conversions.

Required options

  • :filename — original filename
  • :content_type — MIME type of the uploaded file
  • :size — file size in bytes

Optional options

  • :disk — storage disk (must match the one used for presigned_upload_url/3)
  • :custom_properties — user-defined metadata map
  • :checksum — pre-computed checksum (e.g. from client-side hashing)
  • :checksum_algorithm — algorithm used (default: "sha256")

Examples

{:ok, media} =
  PhxMediaLibrary.complete_external_upload(post, :images, upload_key,
    filename: "photo.jpg",
    content_type: "image/jpeg",
    size: 45_000
  )

delete(media)

@spec delete(PhxMediaLibrary.Media.t()) ::
  :ok | {:ok, PhxMediaLibrary.Media.t()} | {:error, term()}

Delete a media item and its files from storage.

download_url(media, conversion \\ nil, opts \\ [])

@spec download_url(PhxMediaLibrary.Media.t(), atom() | nil, keyword()) :: String.t()

Get a download URL that triggers Content-Disposition: attachment.

For S3 storage this generates a presigned GET URL with the response-content-disposition query parameter included in the AWS Signature V4 canonical request.

For local disk storage the URL routes through PhxMediaLibrary.Plug.MediaDownload, which serves the file with the proper header. The plug must be mounted in the app's router.

Options

  • :filename — override the filename in the header (default: media.file_name).
  • :expires_in — presigned URL expiry in seconds for S3 (default: 3600).

Examples

# In a template
<a href={PhxMediaLibrary.download_url(@media)} download>Download</a>

get_first_media(model, collection_name)

@spec get_first_media(Ecto.Schema.t(), atom()) :: PhxMediaLibrary.Media.t() | nil

Get the first media item for a model in a collection.

get_first_media_url(model, collection_name, conversion_or_opts \\ [])

@spec get_first_media_url(Ecto.Schema.t(), atom(), atom() | keyword()) ::
  String.t() | nil

Get the URL for the first media item in a collection.

Examples

PhxMediaLibrary.get_first_media_url(post, :images)
PhxMediaLibrary.get_first_media_url(post, :images, :thumb)
PhxMediaLibrary.get_first_media_url(post, :avatar, fallback: "/default.jpg")

get_first_media_url(model, collection_name, conversion, opts)

@spec get_first_media_url(Ecto.Schema.t(), atom(), atom() | nil, keyword()) ::
  String.t() | nil

get_media(model, collection_name \\ nil)

@spec get_media(Ecto.Schema.t(), atom() | nil) :: [PhxMediaLibrary.Media.t()]

Get all media for a model, optionally filtered by collection.

Examples

PhxMediaLibrary.get_media(post)
PhxMediaLibrary.get_media(post, :images)

get_trashed_media(model, collection_name \\ nil)

@spec get_trashed_media(Ecto.Schema.t(), atom() | nil) :: [PhxMediaLibrary.Media.t()]

Get all soft-deleted media for a model, optionally filtered by collection.

This is the inverse of get_media/2 — it returns only trashed items.

Examples

PhxMediaLibrary.get_trashed_media(post)
PhxMediaLibrary.get_trashed_media(post, :images)

media_query(model, collection_name \\ nil)

@spec media_query(Ecto.Schema.t(), atom() | nil) :: Ecto.Query.t()

Returns an Ecto.Query for all media belonging to a model, optionally filtered by collection.

This is useful for composing queries with Ecto — you can add further filters, selects, limits, or use it with Repo.all/1, Repo.one/1, etc.

Examples

# All media for a post
PhxMediaLibrary.media_query(post)
|> Repo.all()

# Only images
PhxMediaLibrary.media_query(post, :images)
|> Repo.all()

# Compose with further constraints
PhxMediaLibrary.media_query(post, :images)
|> where([m], m.mime_type == "image/png")
|> Repo.all()

move_to(media, position)

@spec move_to(PhxMediaLibrary.Media.t(), pos_integer()) ::
  {:ok, PhxMediaLibrary.Media.t()} | {:error, term()}

Move a single media item to a specific position within its collection.

Shifts other items' order_column values to make room, then sets the target item's position. Position is 1-based.

Examples

PhxMediaLibrary.move_to(media, 1)   # move to first position
PhxMediaLibrary.move_to(media, 3)   # move to third position

path(media, conversion \\ nil)

@spec path(PhxMediaLibrary.Media.t(), atom() | nil) :: String.t() | nil

Get the filesystem path for a media item (local storage only).

permanently_delete(media)

@spec permanently_delete(PhxMediaLibrary.Media.t()) :: :ok | {:error, term()}

Permanently delete a media item and its files from storage.

This always performs a hard delete regardless of whether soft deletes are enabled. Use this for force-deleting or cleaning up trashed items.

presigned_upload_url(model, collection_name, opts \\ [])

@spec presigned_upload_url(Ecto.Schema.t(), atom(), keyword()) ::
  {:ok, String.t(), map(), String.t()} | {:error, term()}

Generate a presigned URL for direct client-to-storage uploads.

This allows the client (browser) to upload files directly to a remote storage backend (e.g. S3) without proxying through the server. The server only generates the signed URL and, after the upload completes, creates the Media record via complete_external_upload/4.

Returns {:ok, url, fields, upload_key} on success, where:

  • url — the presigned upload endpoint
  • fields — a map of form fields for POST-based uploads (empty for PUT)
  • upload_key — the storage path to pass back to complete_external_upload/4

Returns {:error, :not_supported} if the storage adapter doesn't support presigned URLs (e.g. local disk storage).

Options

  • :disk — storage disk to use (default: collection or global default)
  • :filename — the filename for the upload (required)
  • :content_type — expected MIME type of the upload
  • :expires_in — URL expiration in seconds (default: 3600)
  • :max_size — maximum upload size in bytes

Examples

{:ok, url, fields, key} =
  PhxMediaLibrary.presigned_upload_url(post, :images,
    filename: "photo.jpg",
    content_type: "image/jpeg",
    expires_in: 600
  )

# Client uploads directly to `url` with `fields`
# Then server completes:
{:ok, media} =
  PhxMediaLibrary.complete_external_upload(post, :images, key,
    filename: "photo.jpg",
    content_type: "image/jpeg",
    size: 123_456
  )

purge_trashed(model, opts \\ [])

@spec purge_trashed(
  Ecto.Schema.t(),
  keyword()
) :: {:ok, non_neg_integer()} | {:error, term()}

Permanently delete all trashed media for a model that was soft-deleted before the given DateTime. Removes files from storage and database records.

When called without a cutoff, permanently deletes all trashed media for the model.

Examples

# Delete everything trashed more than 30 days ago
cutoff = DateTime.add(DateTime.utc_now(), -30, :day)
{:ok, count} = PhxMediaLibrary.purge_trashed(post, before: cutoff)

# Delete all trashed media for the model
{:ok, count} = PhxMediaLibrary.purge_trashed(post)

reorder(model, collection_name, ordered_ids)

@spec reorder(Ecto.Schema.t(), atom(), [String.t()]) ::
  {:ok, non_neg_integer()} | {:error, term()}

Reorder media items in a collection by a list of IDs.

Sets the order_column for each media record according to its position in the given ID list. Uses a single database transaction with individual updates for correctness.

IDs not present in the collection are silently ignored. Media items in the collection whose IDs are not in the list keep their current order but are shifted after the explicitly ordered items.

Examples

# Set explicit order: id3 first, id1 second, id2 third
PhxMediaLibrary.reorder(post, :images, [id3, id1, id2])

restore(media)

@spec restore(PhxMediaLibrary.Media.t()) ::
  {:ok, PhxMediaLibrary.Media.t()} | {:error, Ecto.Changeset.t()}

Restore a soft-deleted media item by clearing its deleted_at timestamp.

Examples

{:ok, restored} = PhxMediaLibrary.restore(media)
restored.deleted_at  #=> nil

signed_url(media, conversion \\ nil, opts \\ [])

@spec signed_url(PhxMediaLibrary.Media.t(), atom() | nil, keyword()) :: String.t()

Get a signed, time-limited URL for a media item.

For S3 this is an AWS Signature V4 presigned GET URL. For local disk this is an HMAC-SHA256–signed URL verified by PhxMediaLibrary.Plug.MediaDownload. A secret_key_base must be present in the disk config and the plug must be mounted.

Options

  • :expires_in — seconds until the URL expires (default: 3600).
  • :download — also add a Content-Disposition: attachment header.

Examples

PhxMediaLibrary.signed_url(media)
PhxMediaLibrary.signed_url(media, nil, expires_in: 300)
PhxMediaLibrary.signed_url(media, nil, download: true, expires_in: 600)

soft_delete(media)

@spec soft_delete(PhxMediaLibrary.Media.t()) ::
  {:ok, PhxMediaLibrary.Media.t()} | {:error, Ecto.Changeset.t()}

Soft-delete a media item by setting its deleted_at timestamp.

The record remains in the database but is excluded from all queries by default. Files are not removed from storage. Call permanently_delete/1 to remove files and the database record.

Examples

{:ok, trashed} = PhxMediaLibrary.soft_delete(media)
trashed.deleted_at  #=> ~U[2026-02-27 17:00:00Z]

srcset(media, conversion \\ nil)

@spec srcset(PhxMediaLibrary.Media.t(), atom() | nil) :: String.t() | nil

Get the srcset attribute value for responsive images.

to_collection(adder, collection_name, opts \\ [])

@spec to_collection(PhxMediaLibrary.MediaAdder.t(), atom(), keyword()) ::
  {:ok, PhxMediaLibrary.Media.t()} | {:error, term()}

Finalize adding media to a collection.

Options

  • :disk - Override the storage disk for this media

Examples

post
|> PhxMediaLibrary.add(upload)
|> PhxMediaLibrary.to_collection(:images)

post
|> PhxMediaLibrary.add(upload)
|> PhxMediaLibrary.to_collection(:images, disk: :s3)

to_collection!(adder, collection_name, opts \\ [])

Same as to_collection/3 but raises on error.

Raises PhxMediaLibrary.Error if the operation fails.

trashed?(media)

@spec trashed?(PhxMediaLibrary.Media.t()) :: boolean()

Check whether a media item has been soft-deleted.

Examples

PhxMediaLibrary.trashed?(media)  #=> false
{:ok, trashed} = PhxMediaLibrary.soft_delete(media)
PhxMediaLibrary.trashed?(trashed)  #=> true

url(media, conversion \\ nil, opts \\ [])

@spec url(PhxMediaLibrary.Media.t(), atom() | nil, keyword()) :: String.t()

Get the URL for a media item, optionally for a specific conversion.

Accepts the same options as PhxMediaLibrary.UrlGenerator.url/3, including cache_bust: true, signed: true, download: true, and expires_in.

using_filename(adder, filename)

Set a custom filename for the media.

verify_integrity(media)

@spec verify_integrity(PhxMediaLibrary.Media.t()) ::
  :ok | {:error, :checksum_mismatch | :no_checksum | term()}

Verify the integrity of a stored media file by comparing its stored checksum against a freshly computed one.

Returns :ok if the checksums match, {:error, :checksum_mismatch} if they differ, or {:error, :no_checksum} if no checksum was stored.

Examples

case PhxMediaLibrary.verify_integrity(media) do
  :ok -> IO.puts("File is intact")
  {:error, :checksum_mismatch} -> IO.puts("File has been corrupted!")
  {:error, :no_checksum} -> IO.puts("No checksum stored for this media")
end

with_custom_properties(adder, properties)

@spec with_custom_properties(PhxMediaLibrary.MediaAdder.t(), map()) ::
  PhxMediaLibrary.MediaAdder.t()

Attach custom properties (metadata) to the media.

with_responsive_images(adder)

@spec with_responsive_images(PhxMediaLibrary.MediaAdder.t()) ::
  PhxMediaLibrary.MediaAdder.t()

Enable responsive image generation for this media.

without_metadata(adder)

Disable automatic metadata extraction for this media.

By default, PhxMediaLibrary extracts metadata (dimensions, EXIF, etc.) from uploaded files. Use this to skip extraction for a specific upload.

Examples

post
|> PhxMediaLibrary.add(upload)
|> PhxMediaLibrary.without_metadata()
|> PhxMediaLibrary.to_collection(:images)