PhoenixKit.Modules.Storage.Libraries (phoenix_kit v2.40.1)

Copy Markdown View Source

Storage libraries: partitions of the file store (V202).

Every stored file, media folder and folder link belongs to exactly one library (library_uuid). V202 put everything that existed into one system library, Media, under a fixed uuid (media_uuid/0), and made Media the column default — so a writer that names no library, in core or in any module, keeps landing there, and an install with a single library behaves exactly as it did before libraries existed.

More system libraries can be created by an admin (create_system_library/1). Each keeps its own folders (folder names are unique per library and parent) and gives the files stored in it an object-key prefix of its own (key_prefix).

User libraries (V203)

When the install turns them on (user_libraries_enabled?/0), a user with the "storage" permission uses the libraries they own or are a member of (list_user_libraries/1), and one with "storage.create_library" creates them (create_user_library/2, up to user_library_limit/0). A trashed one can be restored by its owner until it is purged (restore_library/2). A user library is private and has an owner and members (PhoenixKit.Modules.Storage.LibraryMember: manager, contributor, viewer; allows?/2 says who does what). Trashing one frees its name at once and purges it, bytes included, after the trash retention period; deleting a user trashes the libraries they own first (trash_owned_libraries/1).

Dedup is one copy per uploader per library: the same person may keep the same bytes in Media and in a library of their own (Storage.calculate_user_file_checksum/3).

Per-library storage (profiles, variant sets, user-owned buckets) is a later phase of dev_docs/plans/2026-09-22-storage-libraries.md.

Summary

Types

What someone is to a library: its owner, a member's role, or nothing.

A library with what it holds (list_system_libraries_with_stats/0).

Functions

Adds the user with email to a user library as role, for its owner or a manager. :no_such_user when nobody has that email, :owner when it is the owner's own.

Whether role may do action in a user library

Whether scope may do action to file. One predicate for every per-file check, so they stop drifting apart

Creates a system library. attrs takes a "name" (or :name). The URL slug comes from the name ("Brand Assets" → "brand-assets", then "brand-assets-2" … while one is taken) and the object-key prefix is generated, unless either is given.

Creates a user library owned by scope's user. attrs takes a "name". The first live library a user has becomes their default. Refused with :not_allowed without may_create_library?/1, and :limit_reached at user_library_limit/0 live libraries.

The user's default library, or nil (they have none yet).

Deletes a library that holds nothing. The default library is never deleted, and a library that still has a file or a folder — trashed ones included — is refused (:not_empty); the database refuses it too.

Drops rows whose library is private.

A library by uuid, or nil (also for anything that is not a uuid).

The live system library with this uuid, or nil — what a URL-supplied library id is checked against before anything is listed or stored in it.

The live system library whose URL slug is slug, or nil. The default library has no slug (it is the bare /admin/media).

A live user library scope may see, by its uuid or — for one of their own — its slug, with what they are to it. Nil otherwise, whatever the reason (missing, trashed, not theirs), so a URL gives nothing away.

A user library's members with their users, by email.

The live system libraries, the default (Media) first, then by name.

list_system_libraries/0 with what each one holds: files (live, visible files — not trashed, not system-managed tiles or edit backups), bytes (their total size), folders (live folders) and holds (any file or folder at all, trash and system-managed rows included — what delete_library/1 refuses). The live counts and holds are separate queries, whatever the number of libraries.

The user libraries user_uuid owns that are in the trash and not purged yet, newest first.

The live user libraries user_uuid owns or is a member of, with what they are to each: the ones they own first (their default first), then the rest, by name.

Every user library, trashed ones included, with its owner, member count and what it holds — what /admin/storage/libraries lists. Metadata only: nothing here opens a library's files.

Whether scope may create user libraries: may_use_libraries?/1, and the "storage.create_library" permission.

Whether scope may take part in user libraries at all — use the ones they own or are a member of, at /admin/libraries: user libraries are on, and the scope holds the "storage" permission.

Whether uuid is Media's.

The uuid of Media, the default system library every existing file, folder and link was put into by V202, and the column default for new ones. Fixed on every install.

Whether files in a library are private (visibility: "private", every user library): their URLs carry a time-window token and are never answered with a redirect to a public object URL. Media is answered without a query; anything that is not a library is not private.

Which of library_uuids are private, in one query — for a page that builds URLs for many files at once.

Whether file (anything with a library_uuid) is in a private library.

Purges a trashed library: every file in it (trashed and system-managed ones included) through the normal delete path, which deletes the bytes no other file still names; then its folders; then the library row (its members go with it). A live library is refused.

Queues the purge of every user library trashed more than days ago, and of every one whose owner is gone. Run by the daily trash prune.

Removes a member, for the library's owner or a manager, or the member themselves (leaving).

Renames a library.

Renames a user library, for its owner or a manager.

Takes a trashed user library out of the trash, for its owner, until it is purged. It gets a URL slug again (the old one may have been taken), and becomes the default when the owner has none. A live library of the owner that has taken its name meanwhile refuses it ({:error, changeset}).

What user_uuid is to library: :owner, a member's role, or nil. A trashed library has no one.

Makes a user library its owner's default, for the owner.

Whether a library is a system library. Media always is; anything else is read. A missing library is not.

Trashes a user library, for its owner. Its members lose it at once; its files are purged, bytes included, after the trash retention period (Storage.trash_retention_days/0). Its name and URL are free again right away. When it was the default, the owner's next library by name becomes the default.

Trashes every live library user_uuid owns and queues their purge — the step Auth.delete_user/2 takes before deleting the user. The database refuses to delete a user whose live library still names them (V203), so this cannot be skipped. Returns how many were trashed.

Changes a member's role, for the library's owner or a manager.

How a user library is named in a URL for user_uuid: its slug when it is theirs, its uuid when it is shared with them (slugs are only unique among one owner's libraries).

Whether user libraries are on for this install (the storage_user_libraries_enabled setting, off by default). Existing sites do not start offering a feature they never planned for.

How many live libraries one user may own (storage_user_library_limit, default 10).

Types

role()

@type role() :: :owner | :manager | :contributor | :viewer | nil

What someone is to a library: its owner, a member's role, or nothing.

stats()

@type stats() :: %{
  library: PhoenixKit.Modules.Storage.Library.t(),
  files: non_neg_integer(),
  bytes: non_neg_integer(),
  folders: non_neg_integer(),
  holds: boolean()
}

A library with what it holds (list_system_libraries_with_stats/0).

Functions

add_member(scope, library, email, role)

@spec add_member(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKit.Modules.Storage.Library.t(),
  String.t(),
  String.t()
) ::
  {:ok, PhoenixKit.Modules.Storage.LibraryMember.t()}
  | {:error, :not_allowed | :no_such_user | :owner | Ecto.Changeset.t()}

Adds the user with email to a user library as role, for its owner or a manager. :no_such_user when nobody has that email, :owner when it is the owner's own.

allows?(role, action)

@spec allows?(role(), atom()) :: boolean()

Whether role may do action in a user library:

  • :read — see its files: everyone
  • :upload — add files: owner, manager, contributor
  • :edit_any — change or trash anyone's files: owner, manager
  • :members — add, change and remove members: owner, manager
  • :rename — owner, manager
  • :own — trash the library, make it the default: owner only

can?(scope, file, action)

@spec can?(PhoenixKit.Users.Auth.Scope.t() | nil, map(), :read | :edit) :: boolean()

Whether scope may do action to file. One predicate for every per-file check, so they stop drifting apart:

  • :read — the file's info and signed URLs: its uploader, an Owner/Admin (Scope.system_role?/1), or — for a file in a user library — that library's owner or any of its members. Deliberately NOT the "media" permission: a single permission must not open every other user's file metadata (issue #687).
  • :edit — change the picture (image editing, annotation burn-in, the unedited original): the uploader, an Owner/Admin, a holder of the "media" permission when the file is in a system library, or the owner or a manager of the user library it is in.

Anything else, and a scope without a user, is refused.

create_system_library(attrs)

@spec create_system_library(map()) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()} | {:error, Ecto.Changeset.t()}

Creates a system library. attrs takes a "name" (or :name). The URL slug comes from the name ("Brand Assets" → "brand-assets", then "brand-assets-2" … while one is taken) and the object-key prefix is generated, unless either is given.

create_user_library(scope, attrs)

@spec create_user_library(PhoenixKit.Users.Auth.Scope.t() | nil, map()) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()}
  | {:error, :not_allowed | :limit_reached | Ecto.Changeset.t()}

Creates a user library owned by scope's user. attrs takes a "name". The first live library a user has becomes their default. Refused with :not_allowed without may_create_library?/1, and :limit_reached at user_library_limit/0 live libraries.

default_user_library(user_uuid)

@spec default_user_library(term()) :: PhoenixKit.Modules.Storage.Library.t() | nil

The user's default library, or nil (they have none yet).

delete_library(library)

@spec delete_library(PhoenixKit.Modules.Storage.Library.t()) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()}
  | {:error, :default | :not_empty}

Deletes a library that holds nothing. The default library is never deleted, and a library that still has a file or a folder — trashed ones included — is refused (:not_empty); the database refuses it too.

exclude_private(query)

@spec exclude_private(Ecto.Query.t()) :: Ecto.Query.t()

Drops rows whose library is private.

The file or folder is the query's first binding. Site listings (/admin/media, the media pickers, orphan cleanup) use this so a user library is not mixed into the site's media; pass that library's uuid to read it. A private library's files are not orphans: nothing in the site references them, and treating them as unreferenced would delete them.

get_library(uuid)

@spec get_library(term()) :: PhoenixKit.Modules.Storage.Library.t() | nil

A library by uuid, or nil (also for anything that is not a uuid).

get_system_library(uuid)

@spec get_system_library(term()) :: PhoenixKit.Modules.Storage.Library.t() | nil

The live system library with this uuid, or nil — what a URL-supplied library id is checked against before anything is listed or stored in it.

get_system_library_by_slug(slug)

@spec get_system_library_by_slug(term()) ::
  PhoenixKit.Modules.Storage.Library.t() | nil

The live system library whose URL slug is slug, or nil. The default library has no slug (it is the bare /admin/media).

get_user_library(scope, id)

@spec get_user_library(PhoenixKit.Users.Auth.Scope.t() | nil, term()) ::
  %{library: PhoenixKit.Modules.Storage.Library.t(), role: role()} | nil

A live user library scope may see, by its uuid or — for one of their own — its slug, with what they are to it. Nil otherwise, whatever the reason (missing, trashed, not theirs), so a URL gives nothing away.

list_members(library)

A user library's members with their users, by email.

list_system_libraries()

@spec list_system_libraries() :: [PhoenixKit.Modules.Storage.Library.t()]

The live system libraries, the default (Media) first, then by name.

list_system_libraries_with_stats()

@spec list_system_libraries_with_stats() :: [stats()]

list_system_libraries/0 with what each one holds: files (live, visible files — not trashed, not system-managed tiles or edit backups), bytes (their total size), folders (live folders) and holds (any file or folder at all, trash and system-managed rows included — what delete_library/1 refuses). The live counts and holds are separate queries, whatever the number of libraries.

list_trashed_user_libraries(user_uuid)

@spec list_trashed_user_libraries(term()) :: [PhoenixKit.Modules.Storage.Library.t()]

The user libraries user_uuid owns that are in the trash and not purged yet, newest first.

list_user_libraries(user_uuid)

@spec list_user_libraries(term()) :: [
  %{library: PhoenixKit.Modules.Storage.Library.t(), role: role()}
]

The live user libraries user_uuid owns or is a member of, with what they are to each: the ones they own first (their default first), then the rest, by name.

list_user_libraries_for_admin()

@spec list_user_libraries_for_admin() :: [map()]

Every user library, trashed ones included, with its owner, member count and what it holds — what /admin/storage/libraries lists. Metadata only: nothing here opens a library's files.

may_create_library?(scope)

@spec may_create_library?(PhoenixKit.Users.Auth.Scope.t() | nil) :: boolean()

Whether scope may create user libraries: may_use_libraries?/1, and the "storage.create_library" permission.

may_use_libraries?(scope)

@spec may_use_libraries?(PhoenixKit.Users.Auth.Scope.t() | nil) :: boolean()

Whether scope may take part in user libraries at all — use the ones they own or are a member of, at /admin/libraries: user libraries are on, and the scope holds the "storage" permission.

media?(uuid)

@spec media?(term()) :: boolean()

Whether uuid is Media's.

media_uuid()

@spec media_uuid() :: String.t()

The uuid of Media, the default system library every existing file, folder and link was put into by V202, and the column default for new ones. Fixed on every install.

private?(uuid)

@spec private?(term()) :: boolean()

Whether files in a library are private (visibility: "private", every user library): their URLs carry a time-window token and are never answered with a redirect to a public object URL. Media is answered without a query; anything that is not a library is not private.

private_among(library_uuids)

@spec private_among([term()]) :: [String.t()]

Which of library_uuids are private, in one query — for a page that builds URLs for many files at once.

private_file?(arg1)

@spec private_file?(map()) :: boolean()

Whether file (anything with a library_uuid) is in a private library.

purge_library(library)

@spec purge_library(PhoenixKit.Modules.Storage.Library.t() | term()) ::
  :ok | {:error, :not_trashed | :not_found}

Purges a trashed library: every file in it (trashed and system-managed ones included) through the normal delete path, which deletes the bytes no other file still names; then its folders; then the library row (its members go with it). A live library is refused.

queue_expired_purges(days)

@spec queue_expired_purges(non_neg_integer()) :: non_neg_integer()

Queues the purge of every user library trashed more than days ago, and of every one whose owner is gone. Run by the daily trash prune.

remove_member(scope, library, user_uuid)

@spec remove_member(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKit.Modules.Storage.Library.t(),
  term()
) :: :ok | {:error, :not_allowed | :not_found}

Removes a member, for the library's owner or a manager, or the member themselves (leaving).

rename_library(library, name)

Renames a library.

rename_user_library(scope, library, name)

@spec rename_user_library(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKit.Modules.Storage.Library.t(),
  String.t()
) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()}
  | {:error, :not_allowed | Ecto.Changeset.t()}

Renames a user library, for its owner or a manager.

restore_library(scope, library)

@spec restore_library(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKit.Modules.Storage.Library.t()
) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()}
  | {:error, :not_allowed | :limit_reached | Ecto.Changeset.t()}

Takes a trashed user library out of the trash, for its owner, until it is purged. It gets a URL slug again (the old one may have been taken), and becomes the default when the owner has none. A live library of the owner that has taken its name meanwhile refuses it ({:error, changeset}).

role(library, user_uuid)

What user_uuid is to library: :owner, a member's role, or nil. A trashed library has no one.

set_default_library(scope, library)

@spec set_default_library(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKit.Modules.Storage.Library.t()
) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()} | {:error, :not_allowed}

Makes a user library its owner's default, for the owner.

system_library?(uuid)

@spec system_library?(term()) :: boolean()

Whether a library is a system library. Media always is; anything else is read. A missing library is not.

trash_library(scope, library)

@spec trash_library(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKit.Modules.Storage.Library.t()
) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()} | {:error, :not_allowed}

Trashes a user library, for its owner. Its members lose it at once; its files are purged, bytes included, after the trash retention period (Storage.trash_retention_days/0). Its name and URL are free again right away. When it was the default, the owner's next library by name becomes the default.

trash_owned_libraries(user_uuid)

@spec trash_owned_libraries(term()) :: {:ok, non_neg_integer()}

Trashes every live library user_uuid owns and queues their purge — the step Auth.delete_user/2 takes before deleting the user. The database refuses to delete a user whose live library still names them (V203), so this cannot be skipped. Returns how many were trashed.

update_member_role(scope, library, user_uuid, role)

@spec update_member_role(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKit.Modules.Storage.Library.t(),
  term(),
  String.t()
) ::
  {:ok, PhoenixKit.Modules.Storage.LibraryMember.t()}
  | {:error, :not_allowed | :not_found | Ecto.Changeset.t()}

Changes a member's role, for the library's owner or a manager.

url_id(library, user_uuid)

How a user library is named in a URL for user_uuid: its slug when it is theirs, its uuid when it is shared with them (slugs are only unique among one owner's libraries).

user_libraries_enabled?()

@spec user_libraries_enabled?() :: boolean()

Whether user libraries are on for this install (the storage_user_libraries_enabled setting, off by default). Existing sites do not start offering a feature they never planned for.

user_library_limit()

@spec user_library_limit() :: non_neg_integer()

How many live libraries one user may own (storage_user_library_limit, default 10).