# `PhoenixKit.Modules.Storage.Libraries`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.55.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.

# `setting`

```elixir
@type setting() :: :annotated_thumbnails | :deep_zoom
```

A per-library setting.

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

# `browse_in_media?`

```elixir
@spec browse_in_media?(PhoenixKit.Users.Auth.Scope.t() | nil) :: boolean()
```

Whether Media (`/admin/media`) is where `scope` browses their user libraries:
they hold `media` and are not an Owner/Admin. Media's switcher then lists their
own and shared libraries beside the site's (`/admin/media/my/<id>`); everyone
else (only `storage`, or an Owner/Admin who also opens other people's libraries,
audit-logged) uses `/admin/libraries`.

# `browse_index_path`

```elixir
@spec browse_index_path(PhoenixKit.Users.Auth.Scope.t() | nil) :: String.t()
```

The path of the page that lists `scope`'s libraries (see `browse_path/2`).

# `browse_path`

```elixir
@spec browse_path(
  PhoenixKit.Users.Auth.Scope.t() | nil,
  PhoenixKit.Modules.Storage.Library.t()
) ::
  String.t()
```

The (canonical, unprefixed) path of the page that browses `library` for
`scope`: Media's for a holder of `media`, `/admin/libraries` otherwise.

# `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 with `"media.view_all"` 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(), keyword()) ::
  {: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(), keyword()) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()}
  | {:error,
     :not_allowed
     | :limit_reached
     | :no_site_storage
     | {:storage, String.t() | Ecto.Changeset.t()}
     | Ecto.Changeset.t()}
```

Creates a user library owned by `scope`'s user. `attrs` takes a `"name"` and
optionally a `"storage"` (below). 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.

## Where the library keeps its files

By default on the site's storage (the Default profile). With `"storage"` a map
the user chooses their own instead, and **the choice is final**: the library's
profile can never be changed afterwards (`Profiles.set_library_profile/2`
refuses it).

    %{"name" => "Photos",
      "storage" => %{
        "mode" => "only",               # or "backup"
        "integration_uuid" => "…",      # one of the user's own connections
        "provider" => "s3",             # s3 | b2 | r2 | tigris
        "bucket_name" => "my-photos",
        "region" => "eu-central-1",     # and/or
        "endpoint" => "https://…"}}

`"only"` keeps everything in their bucket; `"backup"` keeps the site's
storage as it is and copies the originals to theirs (see
`Profiles.create_user_profile/3`). Needs `may_use_own_storage?/1`
(`{:error, :not_allowed}` otherwise).

The bucket is probed first, reading, writing and deleting a small object
(`:probe`, a function of the bucket params, `Storage.test_connection/1` by
default): a library that could not store anything is not created, and the
reason is `{:error, {:storage, message}}`. A bucket, profile or library that
cannot be created undoes the rest. Problems with the bucket fields are
`{:error, {:storage, %Ecto.Changeset{}}}`.

# `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(), keyword()) ::
  {: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_system_library_with_stats`

```elixir
@spec get_system_library_with_stats(term()) :: stats() | nil
```

`list_system_libraries_with_stats/0` for the one live system library with this
uuid (what a URL-supplied id is checked against), or nil.

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

# `may_use_own_storage?`

```elixir
@spec may_use_own_storage?(PhoenixKit.Users.Auth.Scope.t() | nil) :: boolean()
```

Whether `scope` may put a new library on their own storage:
`may_create_library?/1`, the site allowing it (`user_buckets_enabled?/0`), the
`"storage.own_storage"` permission, and the `"integrations"` permission: the
bucket's keys are a personal connection, which only a holder of it can make.

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

# `probe_own_storage`

```elixir
@spec probe_own_storage(PhoenixKit.Users.Auth.Scope.t() | nil, map(), keyword()) ::
  :ok | {:error, :not_allowed | String.t() | Ecto.Changeset.t()}
```

Checks a bucket for `scope`'s user to put a library on (what the wizard's "Test
the bucket" asks, and what creating the library asks again): they may use their
own storage at all, the fields pass `Bucket.owned_changeset/4` (an S3-protocol
provider, their own connection, a host the server may reach), and a small
object can be written, read back and deleted.

Returns `:ok`, `{:error, :not_allowed}`, `{:error, message}` from the bucket, or
`{:error, %Ecto.Changeset{}}` for the fields. `:probe` replaces the network
check (a function of the bucket params), for tests.

# `purge_library`

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

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.

A library on a user's own storage (V206) is purged more carefully: each file's
objects are deleted BEFORE its rows, and a file whose objects could not be
deleted is left as it was, along with the library, its profile and the bucket
row (the record of where the objects are and how to reach them). The purge then
returns `{:error, :objects_remain}`, so the job is retried, and nothing is
forgotten that could not be cleaned up. If the user's credentials are gone
(their account was deleted, taking their connections with it) nothing can be
deleted from their bucket, and the purge goes on without waiting for it.

# `put_setting`

```elixir
@spec put_setting(
  PhoenixKit.Modules.Storage.Library.t(),
  setting(),
  term(),
  keyword()
) ::
  {:ok, PhoenixKit.Modules.Storage.Library.t()}
  | {:error, :unknown_setting | :invalid_value | :not_found}
```

Sets a library's own value of `key`; nil removes it, so the library follows
its default again (the site setting for annotated thumbnails, the rendition
set for deep zoom). Only the key changes (a concurrent change to
another one is not lost), and a value of the wrong type 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(), keyword()) ::
  {: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.

# `setting`

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

A library's own value of `key`, or nil when it has none and follows the
site-wide default. Takes a library or a library uuid (nil is Media).

# `setting_among`

```elixir
@spec setting_among([term()], setting()) :: %{required(String.t()) =&gt; term()}
```

The own value of `key` of each library in `library_uuids`, in one query — for
a page that decides for many files at once. A library with none is missing
from the map; nil stands for Media.

# `show_libraries_entry?`

```elixir
@spec show_libraries_entry?(PhoenixKit.Users.Auth.Scope.t() | nil) :: boolean()
```

Whether the sidebar lists a separate "Libraries" entry for `scope`: user
libraries are on for them, and they do not already have them in Media.

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

```elixir
@spec user_buckets_enabled?() :: boolean()
```

Whether the site lets users keep a library on their own bucket (the
`storage_user_buckets_enabled` setting, off by default: the site answers for
nothing that lands outside its own storage until it says so).

# `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*
