PhxMediaLibrary abstracts file storage behind behaviours, so you can swap backends without changing application code. Out of the box it ships with local disk, Amazon S3, and an in-memory adapter for tests.
Configuring Storage
Storage is configured via disks — named backend configurations. Set a default disk and define one or more named disks:
# config/config.exs
config :phx_media_library,
default_disk: :local,
disks: [
local: [
adapter: PhxMediaLibrary.Storage.Disk,
root: "priv/static/uploads",
base_url: "/uploads"
]
]Local Disk
The default adapter. Files are stored on the local filesystem.
| Option | Required | Description |
|---|---|---|
:root | yes | Filesystem directory for stored files |
:base_url | yes | URL prefix for generating public URLs |
disks: [
local: [
adapter: PhxMediaLibrary.Storage.Disk,
root: "priv/static/uploads",
base_url: "/uploads"
]
]Amazon S3
Requires the optional :ex_aws, :ex_aws_s3, and :sweet_xml dependencies.
| Option | Required | Description |
|---|---|---|
:bucket | yes | S3 bucket name |
:region | yes | AWS region (e.g. "us-east-1") |
disks: [
s3: [
adapter: PhxMediaLibrary.Storage.S3,
bucket: "my-media-bucket",
region: "us-east-1"
]
]
# ExAws credentials (never hard-code these)
config :ex_aws,
access_key_id: {:system, "AWS_ACCESS_KEY_ID"},
secret_access_key: {:system, "AWS_SECRET_ACCESS_KEY"}In-Memory (Testing)
Stores files in a process-backed map. Perfect for fast, isolated tests with no filesystem side effects.
# config/test.exs
config :phx_media_library,
repo: MyApp.Repo,
disks: [
local: [
adapter: PhxMediaLibrary.Storage.Memory
]
]Start the memory agent in your test_helper.exs:
{:ok, _} = PhxMediaLibrary.Storage.Memory.start_link()Per-Collection Disk Override
Individual collections can use a different disk than the default:
media_collections do
collection :images # uses default_disk
collection :backups, disk: :s3 # uses the :s3 disk
collection :temp_files, disk: :local # explicit local
endMultiple Disks
You can define as many disks as you need — for example, a local disk for development and S3 for production:
# config/config.exs
config :phx_media_library,
default_disk: :local,
disks: [
local: [
adapter: PhxMediaLibrary.Storage.Disk,
root: "priv/static/uploads",
base_url: "/uploads"
],
s3: [
adapter: PhxMediaLibrary.Storage.S3,
bucket: "my-bucket",
region: "us-east-1"
]
]
# config/prod.exs — override the default in production
config :phx_media_library,
default_disk: :s3Custom Storage Adapters
Implement the PhxMediaLibrary.Storage behaviour to add your own backend
(e.g. Google Cloud Storage, Azure Blob, SFTP):
defmodule MyApp.Storage.CustomAdapter do
@behaviour PhxMediaLibrary.Storage
@impl true
def put(path, content, opts) do
# Store binary content at path
# Return :ok or {:error, reason}
:ok
end
@impl true
def get(path, opts) do
# Return {:ok, binary} or {:error, reason}
{:ok, <<>>}
end
@impl true
def delete(path, opts) do
# Return :ok or {:error, reason}
:ok
end
@impl true
def exists?(path, opts) do
# Return boolean
true
end
@impl true
def url(path, opts) do
# Return a public URL string
"https://my-cdn.com/#{path}"
end
endThen register it as a disk:
config :phx_media_library,
disks: [
custom: [
adapter: MyApp.Storage.CustomAdapter,
# Any adapter-specific options go here and are passed as `opts`
]
]Behaviour Callbacks
| Callback | Signature | Returns | Description |
|---|---|---|---|
put/3 | (path, content, opts) | :ok | {:error, reason} | Store binary content at path |
get/2 | (path, opts) | {:ok, binary} | {:error, reason} | Retrieve file content |
delete/2 | (path, opts) | :ok | {:error, reason} | Remove a file |
exists?/2 | (path, opts) | boolean() | Check if a file exists |
url/2 | (path, opts) | String.t() | Generate a public URL |
The opts map contains the disk configuration options (e.g. :root,
:base_url, :bucket) so your adapter can read them at runtime without
relying on global config.
Path Structure
PhxMediaLibrary stores files using a predictable path convention:
{collection}/{mediable_id}/{filename}
{collection}/{mediable_id}/conversions/{conversion_name}/{filename}
{collection}/{mediable_id}/responsive/{filename}-{width}.{ext}For example:
images/42/photo.jpg
images/42/conversions/thumb/photo.jpg
images/42/responsive/photo-320.jpgOriginal filenames provided by users are sanitized for safety. The original
name is preserved in the database file_name field for display purposes.
Telemetry
All storage operations emit telemetry events under the
[:phx_media_library, :storage, ...] prefix. See the
Telemetry guide for details on attaching handlers to monitor
storage performance and errors.