# `PhoenixKit.Modules.Storage`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v1.7.106/lib/modules/storage/storage.ex#L1)

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.

# `ancestor_of?`

Returns true if `folder_uuid` is an ancestor of `target_uuid`.

# `build_folder_tree`

Builds a folder tree structure from a flat list of folders.

# `calculate_bucket_free_space`

Calculates free space for a bucket.

For local storage, checks actual disk space.
For cloud storage, returns the configured max_size_mb minus usage.

# `calculate_bucket_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.

# `calculate_user_file_checksum`

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"

# `change_bucket`

Returns an `%Ecto.Changeset{}` for tracking bucket changes.

# `change_dimension`

Returns an `%Ecto.Changeset{}` for tracking dimension changes.

# `change_file`

Returns an `%Ecto.Changeset{}` for tracking file changes.

# `change_file_instance`

Returns an `%Ecto.Changeset{}` for tracking file instance changes.

# `count_folder_contents`

Counts files in a folder (home files + linked files).

# `count_orphaned_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.

# `count_trashed_files`

Returns the count of trashed files, optionally scoped.

# `create_bucket`

Creates a new bucket.

## Examples

    iex> create_bucket(%{name: "Local Storage", provider: "local"})
    {:ok, %Bucket{}}

    iex> create_bucket(%{name: nil})
    {:error, %Ecto.Changeset{}}

# `create_dimension`

Creates a new dimension.

## Examples

    iex> create_dimension(%{name: "thumbnail", width: 150, height: 150})
    {:ok, %Dimension{}}

    iex> create_dimension(%{name: nil})
    {:error, %Ecto.Changeset{}}

# `create_directory`

Creates a directory if it doesn't exist.

# `create_file`

Creates a new file record.

This only creates the database record. Use `store_file/4` to actually
store the file data in storage buckets.

# `create_file_instance`

Creates a new file instance.

# `create_file_locations_for_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 instance
  * `bucket_uuids` - List of bucket UUIDs to create locations for
  * `file_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}]}

# `create_folder`

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

# `create_folder_link`

Creates a link (shortcut) of a file in a folder.

# `delete_bucket`

Deletes a bucket.

## Examples

    iex> delete_bucket(bucket)
    {:ok, %Bucket{}}

    iex> delete_bucket(bucket)
    {:error, %Ecto.Changeset{}}

# `delete_dimension`

Deletes a dimension.

## Examples

    iex> delete_dimension(dimension)
    {:ok, %Dimension{}}

    iex> delete_dimension(dimension)
    {:error, %Ecto.Changeset{}}

# `delete_file`

Deletes a file.

This only removes the database record. Use `delete_file_data/1` to
remove the actual file data from storage buckets.

# `delete_file_completely`

Deletes a file completely - physical data from all storage buckets and database record.

## Examples

    iex> delete_file_completely(file)
    {:ok, %File{}}

# `delete_file_data`

Deletes file data from all storage buckets for all variants.

# `delete_file_instance`

Deletes a file instance.

# `delete_folder`

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.

# `delete_folder_link`

Removes a folder link.

# `empty_trash`

Permanently deletes all trashed files, optionally scoped.

# `ensure_default_bucket_exists`

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

# `file_exists?`

Checks if a file exists in storage.

# `file_orphaned?`

Returns true if the given file UUID is not referenced by any known entity.

# `find_orphaned_files`

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

# `folder_breadcrumbs`

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

# `get_absolute_path`

Gets the absolute path for local storage.

# `get_auto_generate_variants`

# `get_bucket`

Gets a single bucket by ID.

Returns `nil` if bucket does not exist.

# `get_bucket_by_name`

Gets a bucket by name.

# `get_config`

Gets the current storage configuration.

# `get_default_path`

Gets the default storage path for local file uploads (relative path).

Returns the configured relative path or the default "priv/uploads" if not set.

# `get_dimension`

Gets a single dimension by ID.

# `get_dimension_by_name`

Gets a dimension by name.

# `get_file`

Gets a single file by ID.

# `get_file_by_checksum`

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.

# `get_file_by_user_checksum`

Gets a file by its user-specific checksum.

This checks for duplicates for a specific user.

# `get_file_instance`

Gets a single file instance by ID.

# `get_file_instance_bucket_uuids`

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.

# `get_file_instance_by_name`

Gets a file instance by file UUID and variant name.

# `get_folder`

Gets a single folder by UUID.

# `get_health_report`

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 files
- `healthy` — files where all instances meet the redundancy target
- `under_replicated` — list of files with at least one under-replicated instance
- `health_percentage` — percentage of healthy files

# `get_public_url`

Gets a public URL for a file.

# `get_public_url_by_uuid`

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

# `get_public_url_by_uuid`

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"

# `get_public_url_by_variant`

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"

# `list_all_folders`

Returns all folders as a flat list ordered by name, for building a tree.

# `list_buckets`

Returns a list of all storage buckets, ordered by priority.

# `list_dimensions`

Returns a list of all dimensions, ordered by size (width x height).

# `list_dimensions_for_type`

Returns enabled dimensions for a specific file type.

# `list_enabled_buckets`

Gets enabled buckets, ordered by priority.

# `list_file_instances`

Returns a list of file instances for a given file.

# `list_files`

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]`)

# `list_files_in_scope`

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: true` is ignored when `scope_folder_id` is 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.

# `list_folder_tree`

Returns folder tree rooted at scope_folder_id (exclusive of scope itself).
For nil scope, returns the real-root tree.

# `list_folders`

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.

# `list_image_set_variants`

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.

# `list_image_set_variants_for_files`

Bulk version of `list_image_set_variants/1` for multiple files.

Returns a map of `%{file_uuid => [variant_maps]}`. Uses a single DB query.

# `list_trashed_files`

Returns trashed files ordered by trashed_at descending, with pagination and optional scope.

# `module_enabled?`

Checks if the Storage module is enabled.

This module is always enabled and cannot be disabled.

## Examples

    iex> PhoenixKit.Modules.Storage.module_enabled?()
    true

# `move_file_to_folder`

Moves a file's home folder.

# `prune_trash`

Permanently deletes trashed files older than the given number of days.

# `queue_file_cleanup`

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.

# `repair_storage_module`

Repairs the storage module by resetting configuration to defaults.

This is a safe, non-destructive operation that:
1. Creates a default local bucket if no buckets exist
2. Resets dimensions to 8 defaults (4 image + 4 video)
3. 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}]}

# `reset_dimensions_to_defaults`

Resets all dimensions to default seeded values.
Deletes all current dimensions and recreates the 8 default ones.

# `reset_settings_to_defaults`

Resets storage settings to their default values.

Resets:
- `storage_redundancy_copies` to "1"
- `storage_auto_generate_variants` to "true"
- `storage_default_bucket_uuid` to nil

## Returns

- `:ok`

# `restore_file`

Restores a trashed file back to active status.

# `retrieve_file`

Retrieves a file from storage by file UUID.

Will try buckets in priority order until the file is found.

# `retrieve_file_by_hash`

Retrieves a file by its hash.

# `store_file`

Stores a file in the storage system.

This will:
1. Store the file in multiple buckets based on redundancy settings
2. Generate variants if enabled
3. 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

# `store_file_in_buckets`

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"

# `sync_under_replicated`

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.

# `sync_under_replicated_with_progress`

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.

# `test_connection`

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}`.

# `trash_file`

Moves a file to trash (soft-delete). Sets status to 'trashed' and records timestamp.

# `trash_retention_days`

Returns the configured trash retention period in days (default 30).

# `update_bucket`

Updates a bucket.

## Examples

    iex> update_bucket(bucket, %{name: "New Name"})
    {:ok, %Bucket{}}

    iex> update_bucket(bucket, %{name: nil})
    {:error, %Ecto.Changeset{}}

# `update_default_path`

Updates the default storage path.

# `update_dimension`

Updates a dimension.

## Examples

    iex> update_dimension(dimension, %{name: "New Name"})
    {:ok, %Dimension{}}

    iex> update_dimension(dimension, %{name: nil})
    {:error, %Ecto.Changeset{}}

# `update_file`

Updates a file.

# `update_file_instance`

Updates a file instance.

# `update_folder`

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.

# `update_instance_status`

Updates a file instance's processing status.

# `update_instance_with_file_info`

Updates a file instance with file information after processing.

# `validate_and_normalize_path`

Validates and normalizes a storage path.

Returns `{:ok, relative_path}` if valid, or error tuple if invalid.

# `within_scope?`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
