PhoenixKit.Modules.Storage (phoenix_kit v1.7.118)

Copy Markdown View Source

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

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

Walks the folder tree from root_uuid down and returns every descendant folder uuid, including the root itself.

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.

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.

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.

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

ancestor_of?(folder_uuid, target_uuid)

Returns true if folder_uuid is an ancestor of target_uuid.

build_folder_tree(folders)

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

calculate_bucket_free_space(bucket)

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(bucket_uuid)

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(user_uuid, 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(bucket, attrs \\ %{})

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

change_dimension(dimension, attrs \\ %{})

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

change_file(file, attrs \\ %{})

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

change_file_instance(instance, attrs \\ %{})

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

count_folder_contents(folder_uuid)

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

count_orphaned_files(scope_folder_id \\ nil)

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(scope \\ nil)

Returns the count of trashed files, optionally scoped.

count_trashed_folders(scope_folder_id \\ nil)

Counts trashed folders (with optional scope).

create_bucket(attrs \\ %{})

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(attrs \\ %{})

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(path)

Creates a directory if it doesn't exist.

create_file(attrs \\ %{})

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(attrs \\ %{})

Creates a new file instance.

create_file_locations_for_instance(file_instance_uuid, bucket_uuids, file_path)

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(attrs, scope_folder_id \\ nil)

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(folder_uuid, file_uuid, scope_folder_id \\ nil)

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

delete_bucket(bucket)

Deletes a bucket.

Examples

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

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

delete_dimension(dimension)

Deletes a dimension.

Examples

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

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

delete_file(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(file)

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

Examples

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

delete_file_data(file)

Deletes file data from all storage buckets for all variants.

delete_file_instance(instance)

Deletes a file instance.

delete_folder(folder, scope_folder_id \\ nil)

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_completely(folder, scope_folder_id \\ nil)

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.

delete_folder_link(link_uuid)

Removes a folder link.

empty_trash(scope \\ nil)

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?(file)

Checks if a file exists in storage.

file_orphaned?(file_uuid)

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

find_orphaned_files(opts \\ [])

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(folder_uuid, scope_folder_id \\ nil)

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

folder_subtree_uuids(root_uuid)

Walks the folder tree from root_uuid down and returns every descendant folder uuid, including the root itself.

Used by trash, restore, and permanent-delete to determine the affected subtree in one pass, and by scoped media listings that should include files in nested folders.

get_absolute_path()

Gets the absolute path for local storage.

get_auto_generate_variants()

get_bucket(id)

Gets a single bucket by ID.

Returns nil if bucket does not exist.

get_bucket_by_name(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(id)

Gets a single dimension by ID.

get_dimension_by_name(name)

Gets a dimension by name.

get_file(id)

Gets a single file by ID.

get_file_by_checksum(file_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(user_file_checksum)

Gets a file by its user-specific checksum.

This checks for duplicates for a specific user.

get_file_instance(id)

Gets a single file instance by ID.

get_file_instance_bucket_uuids(file_instance_uuid)

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(file_uuid, variant_name)

Gets a file instance by file UUID and variant name.

get_files(uuids)

Fetches multiple files by UUID in a single query and returns them ordered to match uuids. Missing UUIDs are silently omitted from the result.

get_folder(uuid)

Gets a single folder by UUID.

get_health_report(redundancy_target)

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(file)

Gets a public URL for a file.

get_public_url_by_uuid(file_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(file_uuid, variant_name)

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(file, variant_name)

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

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(file_type)

Returns enabled dimensions for a specific file type.

list_enabled_buckets()

Gets enabled buckets, ordered by priority.

list_file_instances(file_uuid)

Returns a list of file instances for a given file.

list_files(opts \\ [])

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(scope_folder_id, opts \\ [])

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(scope_folder_id \\ nil)

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

list_folders(parent_uuid \\ nil, scope_folder_id \\ nil)

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(file_uuid)

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(file_uuids)

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(scope \\ nil, opts \\ [])

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

list_trashed_folders(scope_folder_id \\ nil, opts \\ [])

Returns trashed folders ordered by trashed_at descending, with 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(file_uuid, target_folder_uuid, scope_folder_id \\ nil)

Moves a file's home folder.

prune_trash(days)

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

queue_file_cleanup(file_uuids)

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(file)

Restores a trashed file back to active status.

restore_folder(folder, scope_folder_id \\ nil)

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.

retrieve_file(file_uuid)

Retrieves a file from storage by file UUID.

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

retrieve_file_by_hash(hash)

Retrieves a file by its hash.

store_file(source_path, opts \\ [])

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(source_path, file_type, user_uuid, file_checksum, ext, original_filename \\ nil)

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"

store_system_file(content_path, key, opts)

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_uuid pointing at the source File that this chunk was derived from — used for cascade cleanup (the FK in V112 is ON DELETE :delete_all)
  • have no user_uuid (the changeset's validate_system_managed_invariants requires 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}.

sync_under_replicated(redundancy_target)

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(redundancy_target, callback, opts \\ [])

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(bucket_params)

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(file)

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

trash_folder(folder, scope_folder_id \\ nil)

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.

trash_retention_days()

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

update_bucket(bucket, attrs)

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(relative_path)

Updates the default storage path.

update_dimension(dimension, attrs)

Updates a dimension.

Examples

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

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

update_file(file, attrs)

Updates a file.

update_file_instance(instance, attrs)

Updates a file instance.

update_folder(folder, attrs, scope_folder_id \\ nil)

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.

update_instance_status(instance, status)

Updates a file instance's processing status.

update_instance_with_file_info(instance, file_path, dimensions \\ nil)

Updates a file instance with file information after processing.

validate_and_normalize_path(path)

Validates and normalizes a storage path.

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

within_scope?(folder_uuid, scope_folder_id)

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