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

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

# `role`

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

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

# `stats`

```elixir
@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`).

# `add_member`

```elixir
@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?`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

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

# `delete_library`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@spec list_members(PhoenixKit.Modules.Storage.Library.t()) :: [
  PhoenixKit.Modules.Storage.LibraryMember.t()
]
```

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

# `list_system_libraries`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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?`

```elixir
@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?`

```elixir
@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?`

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

Whether `uuid` is Media's.

# `media_uuid`

```elixir
@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?`

```elixir
@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`

```elixir
@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?`

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

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

# `purge_library`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@spec rename_library(PhoenixKit.Modules.Storage.Library.t(), String.t()) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()} | {:error, Ecto.Changeset.t()}
```

Renames a library.

# `rename_user_library`

```elixir
@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`

```elixir
@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`

```elixir
@spec role(PhoenixKit.Modules.Storage.Library.t(), term()) :: role()
```

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

# `set_default_library`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@spec url_id(PhoenixKit.Modules.Storage.Library.t(), term()) :: String.t()
```

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?`

```elixir
@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`

```elixir
@spec user_library_limit() :: non_neg_integer()
```

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

---

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