Storage context for managing files, buckets, and dimensions.
Provides a distributed file storage system with support for multiple storage providers (local filesystem, AWS S3, Backblaze B2, Cloudflare R2) with automatic redundancy and failover capabilities.
Features
- Multi-location storage with configurable redundancy (1-5 copies)
- Support for local, S3, B2, and R2 storage providers
- Automatic variant generation for images and videos
- Priority-based storage selection
- Built-in usage tracking and statistics
- PostgreSQL-backed file registry
Module Status
This module is always enabled and cannot be disabled. It provides core functionality for file management across PhoenixKit.
Summary
Functions
Returns true if folder_uuid is an ancestor of target_uuid.
Builds a folder tree structure from a flat list of folders.
Calculates free space for a bucket.
Calculates storage usage for a bucket in MB.
Calculates user-specific file checksum (salted with user_uuid).
Returns an %Ecto.Changeset{} for tracking bucket changes.
Returns an %Ecto.Changeset{} for tracking dimension changes.
Returns an %Ecto.Changeset{} for tracking file changes.
Returns an %Ecto.Changeset{} for tracking file instance changes.
Counts files in a folder (home files + linked files).
Returns the count of orphaned files.
Returns the count of trashed files, optionally scoped.
Counts trashed folders (with optional scope).
Creates a new bucket.
Creates a new dimension.
Creates a directory if it doesn't exist.
Creates a new file record.
Creates a new file instance.
Creates file locations for a file instance across specified buckets.
Creates a new folder.
Creates a link (shortcut) of a file in a folder.
Deletes a bucket.
Deletes a dimension.
Deletes a file.
Deletes a file completely - physical data from all storage buckets and database record.
Deletes file data from all storage buckets for all variants.
Deletes a file instance.
Deletes a folder.
Permanently deletes a (presumably trashed) folder and everything
underneath it. Files are removed via delete_file_completely/1 so
backend storage cleanup runs; folders are deleted from the DB in
bottom-up order so foreign-key constraints stay happy.
Removes a folder link.
Permanently deletes all trashed files, optionally scoped.
Ensures at least one default bucket exists.
Checks if a file exists in storage.
Returns true if the given file UUID is not referenced by any known entity.
Returns a list of orphaned files (files not referenced by any known entity).
Returns the ancestor chain from root to the given folder (for breadcrumbs).
Gets the absolute path for local storage.
Gets a single bucket by ID.
Gets a bucket by name.
Gets the current storage configuration.
Gets the default storage path for local file uploads (relative path).
Gets a single dimension by ID.
Gets a dimension by name.
Gets a single file by ID.
Gets a file by its original content checksum (file_checksum).
Gets a file by its user-specific checksum.
Gets a single file instance by ID.
Gets the bucket UUIDs where a file instance is stored.
Gets a file instance by file UUID and variant name.
Fetches multiple files by UUID in a single query and returns them ordered to
match uuids. Missing UUIDs are silently omitted from the result.
Gets a single folder by UUID.
Returns a health report comparing file location counts against the redundancy target.
Gets a public URL for a file.
Gets a public URL for a file by file ID.
Gets a public URL for a specific file variant by file ID.
Gets a public URL for a specific file variant.
Returns all non-trashed folders as a flat list ordered by name, for
building a tree. Trashed folders are excluded — they live in the
trash bucket alongside trashed files and are read via
list_trashed_folders/2.
Returns a list of all storage buckets, ordered by priority.
Returns a list of all dimensions, ordered by size (width x height).
Returns enabled dimensions for a specific file type.
Gets enabled buckets, ordered by priority.
Returns a list of file instances for a given file.
Returns a list of files, optionally filtered by bucket.
Lists files within the given scope with optional folder filter, search, and pagination.
Returns folder tree rooted at scope_folder_id (exclusive of scope itself). For nil scope, returns the real-root tree.
Lists folders within a parent folder (nil = root).
Returns variant data for building an <.image_set> <picture> element.
Bulk version of list_image_set_variants/1 for multiple files.
Returns trashed files ordered by trashed_at descending, with pagination and optional scope.
Returns trashed folders ordered by trashed_at descending, with optional scope.
Checks if the Storage module is enabled.
Moves a file's home folder.
Permanently deletes trashed files older than the given number of days.
Queues a list of file UUIDs for orphan cleanup via Oban.
Repairs the storage module by resetting configuration to defaults.
Resets all dimensions to default seeded values. Deletes all current dimensions and recreates the 8 default ones.
Resets storage settings to their default values.
Restores a trashed file back to active status.
Restores a previously trashed folder and everything underneath it.
Reverses trash_folder/2 — clears trashed_at on all subtree folders
and resets status: "active", trashed_at: nil on all files in the
subtree.
Retrieves a file from storage by file UUID.
Retrieves a file by its hash.
Stores a file in the storage system.
Stores a file in buckets with hierarchical path structure.
Persists a system-managed chunk (e.g. a Tessera DZI tile or manifest)
into every configured bucket and into the storage DB as a File row
with a single "original" FileInstance.
Syncs under-replicated files to meet the redundancy target.
Syncs under-replicated files with progress reporting via callback.
Tests connectivity for a bucket configuration.
Moves a file to trash (soft-delete). Sets status to 'trashed' and records timestamp.
Soft-deletes a folder and everything underneath it (descendant folders +
files in the subtree). All affected rows get trashed_at = now; files
also get status = "trashed" to match the existing file-trash convention.
Restore via restore_folder/2, permanent delete via
delete_folder_completely/2.
Returns the configured trash retention period in days (default 30).
Updates a bucket.
Updates the default storage path.
Updates a dimension.
Updates a file.
Updates a file instance.
Updates a folder (rename, color change, move).
Updates a file instance's processing status.
Updates a file instance with file information after processing.
Validates and normalizes a storage path.
Returns true if folder_uuid is within the given scope.
Functions
Returns true if folder_uuid is an ancestor of target_uuid.
Builds a folder tree structure from a flat list of folders.
Calculates free space for a bucket.
For local storage, checks actual disk space. For cloud storage, returns the configured max_size_mb minus usage.
Calculates storage usage for a bucket in MB.
Returns total size of all files stored in this bucket by summing up all file instances that have locations in this bucket.
Calculates user-specific file checksum (salted with user_uuid).
This creates a unique checksum per user+file combination for duplicate detection, while preserving the original file checksum for popularity queries.
Parameters
- user_uuid: The user UUID
- file_checksum: The SHA256 checksum of the file content
Returns
String representing the SHA256 checksum of "user_uuid + file_checksum"
Returns an %Ecto.Changeset{} for tracking bucket changes.
Returns an %Ecto.Changeset{} for tracking dimension changes.
Returns an %Ecto.Changeset{} for tracking file changes.
Returns an %Ecto.Changeset{} for tracking file instance changes.
Counts files in a folder (home files + linked files).
Returns the count of orphaned files.
When scope_folder_id is set, returns 0 because orphaned files (folder_uuid IS NULL) are always outside any non-nil scope.
Returns the count of trashed files, optionally scoped.
Counts trashed folders (with optional scope).
Creates a new bucket.
Examples
iex> create_bucket(%{name: "Local Storage", provider: "local"})
{:ok, %Bucket{}}
iex> create_bucket(%{name: nil})
{:error, %Ecto.Changeset{}}
Creates a new dimension.
Examples
iex> create_dimension(%{name: "thumbnail", width: 150, height: 150})
{:ok, %Dimension{}}
iex> create_dimension(%{name: nil})
{:error, %Ecto.Changeset{}}
Creates a directory if it doesn't exist.
Creates a new file record.
This only creates the database record. Use store_file/4 to actually
store the file data in storage buckets.
Creates a new file instance.
Creates file locations for a file instance across specified buckets.
Returns {:ok, locations} on success or {:error, :file_locations_failed, errors} if any insertions fail.
Parameters
file_instance_uuid- The UUID of the file instancebucket_uuids- List of bucket UUIDs to create locations forfile_path- The storage path for the file
Examples
iex> create_file_locations_for_instance(instance_uuid, [bucket_uuid], "path/to/file")
{:ok, [%FileLocation{}]}
iex> create_file_locations_for_instance(instance_uuid, [invalid_bucket], "path")
{:error, :file_locations_failed, [{bucket_uuid, changeset}]}
Creates a new folder.
When scope_folder_id is set:
- If attrs.parent_uuid is outside scope, returns
{:error, :out_of_scope}. - If attrs.parent_uuid is nil, rewrites to scope_folder_id (new folder at scope root).
Creates a link (shortcut) of a file in a folder.
Deletes a bucket.
Examples
iex> delete_bucket(bucket)
{:ok, %Bucket{}}
iex> delete_bucket(bucket)
{:error, %Ecto.Changeset{}}
Deletes a dimension.
Examples
iex> delete_dimension(dimension)
{:ok, %Dimension{}}
iex> delete_dimension(dimension)
{:error, %Ecto.Changeset{}}
Deletes a file.
This only removes the database record. Use delete_file_data/1 to
remove the actual file data from storage buckets.
Deletes a file completely - physical data from all storage buckets and database record.
Examples
iex> delete_file_completely(file)
{:ok, %File{}}
Deletes file data from all storage buckets for all variants.
Deletes a file instance.
Deletes a folder.
Moves child folders and home files to the deleted folder's parent.
Folder links are cascade-deleted by the database FK.
Returns {:error, :out_of_scope} if the folder is outside scope.
Permanently deletes a (presumably trashed) folder and everything
underneath it. Files are removed via delete_file_completely/1 so
backend storage cleanup runs; folders are deleted from the DB in
bottom-up order so foreign-key constraints stay happy.
Removes a folder link.
Permanently deletes all trashed files, optionally scoped.
Ensures at least one default bucket exists.
If no buckets exist, creates a default local storage bucket.
Returns
{:created, bucket}- If a new bucket was created:exists- If buckets already exist
Examples
iex> ensure_default_bucket_exists()
{:created, %Bucket{name: "Local Storage"}}
iex> ensure_default_bucket_exists()
:exists
Checks if a file exists in storage.
Returns true if the given file UUID is not referenced by any known entity.
Returns a list of orphaned files (files not referenced by any known entity).
Options
:limit- Maximum number of results:offset- Number of results to skip
Returns the ancestor chain from root to the given folder (for breadcrumbs).
When scope_folder_id is set, the chain stops before scope (scope itself not included — it is the virtual root).
Gets the absolute path for local storage.
Gets a single bucket by ID.
Returns nil if bucket does not exist.
Gets a bucket by name.
Gets the current storage configuration.
Gets the default storage path for local file uploads (relative path).
Returns the configured relative path or the default "priv/uploads" if not set.
Gets a single dimension by ID.
Gets a dimension by name.
Gets a single file by ID.
Gets a file by its original content checksum (file_checksum).
This can find files uploaded by any user with the same content. Useful for popularity queries.
Gets a file by its user-specific checksum.
This checks for duplicates for a specific user.
Gets a single file instance by ID.
Gets the bucket UUIDs where a file instance is stored.
Returns a list of bucket UUIDs from the file_locations for the given file instance.
Gets a file instance by file UUID and variant name.
Fetches multiple files by UUID in a single query and returns them ordered to
match uuids. Missing UUIDs are silently omitted from the result.
Gets a single folder by UUID.
Returns a health report comparing file location counts against the redundancy target.
Groups by file (not instance) — a file is "under-replicated" if any of its instances have fewer active locations than the redundancy target.
Returns a map with:
total— total fileshealthy— files where all instances meet the redundancy targetunder_replicated— list of files with at least one under-replicated instancehealth_percentage— percentage of healthy files
Gets a public URL for a file.
Gets a public URL for a file by file ID.
Convenience function that fetches the file and returns its URL.
Examples
iex> get_public_url_by_uuid("018e3c4a-9f6b-7890-abcd-ef1234567890")
"https://cdn.example.com/12/a1/a1b2c3d4e5f6/a1b2c3d4e5f6_original.jpg"
iex> get_public_url_by_uuid("invalid-uuid")
nil
Gets a public URL for a specific file variant by file ID.
Examples
iex> get_public_url_by_uuid("018e3c4a-9f6b-7890-abcd-ef1234567890", "thumbnail")
"https://cdn.example.com/12/a1/a1b2c3d4e5f6/a1b2c3d4e5f6_thumbnail.jpg"
Gets a public URL for a specific file variant.
Variants
For images: "original", "thumbnail", "small", "medium", "large" For videos: "original", "360p", "720p", "1080p", "video_thumbnail"
Examples
iex> get_public_url_by_variant(file, "thumbnail")
"https://cdn.example.com/12/a1/a1b2c3d4e5f6/a1b2c3d4e5f6_thumbnail.jpg"
iex> get_public_url_by_variant(file, "medium")
"https://cdn.example.com/12/a1/a1b2c3d4e5f6/a1b2c3d4e5f6_medium.jpg"
Returns all non-trashed folders as a flat list ordered by name, for
building a tree. Trashed folders are excluded — they live in the
trash bucket alongside trashed files and are read via
list_trashed_folders/2.
Returns a list of all storage buckets, ordered by priority.
Returns a list of all dimensions, ordered by size (width x height).
Returns enabled dimensions for a specific file type.
Gets enabled buckets, ordered by priority.
Returns a list of file instances for a given file.
Returns a list of files, optionally filtered by bucket.
Options
:bucket_uuid- Filter by bucket UUID:limit- Maximum number of results:offset- Number of results to skip:order_by- Ordering (default:[desc: :inserted_at])
Lists files within the given scope with optional folder filter, search, and pagination.
Options
:folder_uuid— specific folder within scope; returns{:error, :out_of_scope}if outside.:search— ilike search on original_file_name; restricted to scope descendants when scope set.:include_orphaned— boolean (default false); only meaningful when scope is nil. When true, returns only files with folder_uuid IS NULL.include_orphaned: trueis ignored whenscope_folder_idis non-nil (orphans are always outside any scope).:page— page number (default 1).:per_page— page size (default 20).
Returns
{files, total_count} or {:error, :out_of_scope}.
When scope_folder_id == nil and no folder_uuid is specified, ALL files are returned (not
just orphans). To fetch orphans only at real root, pass include_orphaned: true AND use a
dedicated orphan-only branch. Task 4 callers must preserve the current /admin/media behavior
(list orphans only when filter_orphaned is on) via a separate code path.
Returns folder tree rooted at scope_folder_id (exclusive of scope itself). For nil scope, returns the real-root tree.
Lists folders within a parent folder (nil = root).
When parent_uuid is nil and scope_folder_id is set, returns children of scope_folder_id instead of real root.
Returns variant data for building an <.image_set> <picture> element.
Returns a list of maps with :variant_name, :mime_type, :width, and :url
for all completed image instances of the given file.
Bulk version of list_image_set_variants/1 for multiple files.
Returns a map of %{file_uuid => [variant_maps]}. Uses a single DB query.
Returns trashed files ordered by trashed_at descending, with pagination and optional scope.
Returns trashed folders ordered by trashed_at descending, with optional scope.
Checks if the Storage module is enabled.
This module is always enabled and cannot be disabled.
Examples
iex> PhoenixKit.Modules.Storage.module_enabled?()
true
Moves a file's home folder.
Permanently deletes trashed files older than the given number of days.
Queues a list of file UUIDs for orphan cleanup via Oban.
Each file is scheduled for deletion after a 60-second delay to protect against race conditions (another entity may reference the file). Only files that are still orphaned at job execution time will be deleted.
Repairs the storage module by resetting configuration to defaults.
This is a safe, non-destructive operation that:
- Creates a default local bucket if no buckets exist
- Resets dimensions to 8 defaults (4 image + 4 video)
- Resets storage settings to recommended defaults
All existing files are preserved.
Returns
{:ok, repairs}- List of repairs performed{:error, reason}- If repair failed
Examples
iex> repair_storage_module()
{:ok, [{:bucket_created, "Local Storage"}, {:dimensions_reset, 8}, {:settings_reset, 3}]}
Resets all dimensions to default seeded values. Deletes all current dimensions and recreates the 8 default ones.
Resets storage settings to their default values.
Resets:
storage_redundancy_copiesto "1"storage_auto_generate_variantsto "true"storage_default_bucket_uuidto nil
Returns
:ok
Restores a trashed file back to active status.
Restores a previously trashed folder and everything underneath it.
Reverses trash_folder/2 — clears trashed_at on all subtree folders
and resets status: "active", trashed_at: nil on all files in the
subtree.
Retrieves a file from storage by file UUID.
Will try buckets in priority order until the file is found.
Retrieves a file by its hash.
Stores a file in the storage system.
This will:
- Store the file in multiple buckets based on redundancy settings
- Generate variants if enabled
- Create database records for the file and its variants
Options
:filename- Original filename (required):content_type- MIME type (required):size_bytes- File size in bytes (required):user_uuid- User UUID who owns the file:metadata- Additional metadata map
Stores a file in buckets with hierarchical path structure.
Path Structure
Files are stored using the pattern:
{user_uuid[0..1]}/{hash[0..1]}/{full_hash}/{full_hash}_{variant}.{format}
Examples
User ID: "12345678" File hash: "a1b2c3d4e5f6..." Original: "12/a1/a1b2c3d4e5f6/a1b2c3d4e5f6_original.jpg" Thumbnail: "12/a1/a1b2c3d4e5f6/a1b2c3d4e5f6_thumbnail.jpg"
Persists a system-managed chunk (e.g. a Tessera DZI tile or manifest)
into every configured bucket and into the storage DB as a File row
with a single "original" FileInstance.
System-managed Files:
- have
system_managed: true - carry a
parent_file_uuidpointing at the source File that this chunk was derived from — used for cascade cleanup (the FK in V112 isON DELETE :delete_all) - have no
user_uuid(the changeset'svalidate_system_managed_invariantsrequires the parent instead) - skip the variant pipeline (see
VariantGenerator.should_generate_variants?/1) - are excluded from MediaBrowser listings
Required opts
:parent_file_uuid— the source image's UUID:mime_type— content type:size— content size in bytes (taken from disk if omitted)
Optional opts
:file_type— defaults to"tile":width/:height— dimensions, if known:metadata— JSONB payload (e.g. tile coords)
Returns {:ok, %{file: file, instance: instance}} or {:error, reason}.
Syncs under-replicated files to meet the redundancy target.
For each under-replicated file, retrieves it from an existing bucket and replicates it to the missing buckets. Returns a summary of results.
Syncs under-replicated files with progress reporting via callback.
The callback receives a map with :done, :total, :synced, :failed,
and :status (:in_progress or :complete) after each file is processed.
Tests connectivity for a bucket configuration.
Builds a temporary Bucket struct from the given params and delegates
to the appropriate provider's test_connection/1 callback.
Returns :ok or {:error, reason}.
Moves a file to trash (soft-delete). Sets status to 'trashed' and records timestamp.
Soft-deletes a folder and everything underneath it (descendant folders +
files in the subtree). All affected rows get trashed_at = now; files
also get status = "trashed" to match the existing file-trash convention.
Restore via restore_folder/2, permanent delete via
delete_folder_completely/2.
Scope-guarded: returns {:error, :out_of_scope} if the folder is outside
the provided scope.
Returns the configured trash retention period in days (default 30).
Updates a bucket.
Examples
iex> update_bucket(bucket, %{name: "New Name"})
{:ok, %Bucket{}}
iex> update_bucket(bucket, %{name: nil})
{:error, %Ecto.Changeset{}}
Updates the default storage path.
Updates a dimension.
Examples
iex> update_dimension(dimension, %{name: "New Name"})
{:ok, %Dimension{}}
iex> update_dimension(dimension, %{name: nil})
{:error, %Ecto.Changeset{}}
Updates a file.
Updates a file instance.
Updates a folder (rename, color change, move).
Returns {:error, :cycle} if the move would create a circular reference.
Returns {:error, :out_of_scope} if the folder or new parent is outside scope.
parent_uuid semantics under scope: omit :parent_uuid from attrs
for rename/recolor (no move is attempted). Pass an explicit value to
move — including nil, which means "move to the system's true root."
Under a non-nil scope, an explicit parent_uuid: nil fails with
:out_of_scope because the system root is outside the scope subtree.
Updates a file instance's processing status.
Updates a file instance with file information after processing.
Validates and normalizes a storage path.
Returns {:ok, relative_path} if valid, or error tuple if invalid.
Returns true if folder_uuid is within the given scope.
- When scope_folder_id is nil, always returns true (no scope restriction).
- When folder_uuid equals scope_folder_id, returns true (scope is the virtual root).
- When scope_folder_id is an ancestor of folder_uuid, returns true (folder is a descendant).
- Returns false otherwise, including when folder_uuid is nil and scope is set (real root is outside any non-nil scope).