Collections and sharing
A collection is the unit users browse: a named node in a tree that holds files and child collections. Public collections make up the catalogue and are managed by admins; private collections belong to one user. Either kind can be synchronized with a folder of the assets tree, or assembled by hand.
The collection model
Section titled “The collection model”Collections live in collections, a materialized-path tree (mpath) with a unique (parent_id, name) pair.
| Column | Meaning |
|---|---|
public |
Part of the catalogue; public collections have no owner |
draft |
Hidden from members through the public clause |
owner_id |
The user who owns a private collection; null for public ones |
asset_folder_id |
When set, the collection mirrors that folder and is “synchronized” |
limited_to_group_ids, can_edit_limited_to_group_ids |
Group restriction; see Groups and regions |
sample_file_ids |
Up to 4 file ids used as the folder preview, maintained by triggers |
number_of_files |
Count of files in the collection and its descendants, maintained by triggers |
has_thumbnail |
A custom thumbnail exists under collections/{id}-thumbnail in the main bucket |
Files are rows of collection_files (collection_id, asset_file_id, unique together).
The admin screen
Section titled “The admin screen”/admin/collections (admin only) shows the public tree returned by collection.treeAdmin, with a page, draft or thumbnail badge per node and Edit / Delete actions. Add new collection opens the creation dialog. The delete confirmation explains that no file is removed from cloud storage.
Create a collection
Section titled “Create a collection”| Procedure | Who | Result |
|---|---|---|
collection.create |
approved users | Manual collection. Non-admins always get public = false, draft = false, owner = themselves. Admins can pass public/draft; a child inherits both from its parent. |
collection.createFromAsset |
admin | Synchronized collection named after the folder; pushes collection/synchronization |
collection.createUserCollection |
approved users | Private collection (owner = the user), optionally under one of their own collections |
collection.addItems |
owner or admin | Copies files or whole collections into a manual collection; refused on synchronized ones (Synchronized folders cannot be changed.) |
Synchronization mirrors a folder
Section titled “Synchronization mirrors a folder”The collection/synchronization queue (one job at a time) runs synchronizeCollection in server/src/services/collection.ts inside a transaction:
- Renames the collection to the folder’s name and refreshes its menu items.
- Upserts one
collection_filesrow per file of the folder and deletes rows that no longer match. - Upserts one child collection per subfolder (matching on
parent_idandname), copyingpublic,draftandowner_idfrom the parent, and deletes children that no longer have a subfolder. - Pushes one synchronization job per child, so the whole subtree converges.
Jobs are triggered by createFromAsset and by the asset updater whenever a folder is created, renamed or moved (upsertFolder). Independently, server/src/index.ts runs this after every sync pass:
INSERT INTO collection_files (asset_file_id, collection_id)SELECT asset_files.id, collections.id FROM asset_filesINNER JOIN collections ON collections.asset_folder_id = asset_files.folder_idON CONFLICT DO NOTHINGIt back-fills any file that reached a linked folder between two synchronizations.
Triggers keep counts and previews fresh
Section titled “Triggers keep counts and previews fresh”number_of_filesis incremented or decremented on every insert or delete incollection_files, for the collection and all its ancestors read frommpath(initial migration).sample_file_idsis recomputed by the functionrefresh_collection_sample_files_from_asset_files, fired after insert and update onasset_files(migrations1751012487660-add-trigger-to-sample-files.tsand1751187976556-update-asset-file-trigger.ts). For the affected collection and its ancestors it selects up to 4collection_fileswhose asset has a thumbnail, ordered by depth then creation date. The dailysystem/integrity-checkjob recomputes the same field for every collection.
Edit and delete
Section titled “Edit and delete”collection.update requires canEdit (admin or owner) and changes name, description, draft, hasThumbnail and, when allowed, limitedToGroupIds. The public flag cannot change (Cannot change the public status of a collection.). Draft, owner and group limits are copied to every descendant, menu items are re-synced, and the thumbnail object is removed when hasThumbnail is false. collection.presignedThumbnailUploadUrl returns a 24-hour PUT URL for collections/{id}-thumbnail in the main bucket.
collection.removeFiles refuses files of synchronized collections; collection.remove refuses a collection whose parent is synchronized (Synchronized collections cannot be deleted.). Deleting a collection removes its thumbnail object and cascades to children, files, invitations, menu items and its page.
Private collections
Section titled “Private collections”collection.ListPrivateCollections returns the tree of collections where public is false and owner_id is the caller. This is the “my collections” area where members copy files with addItems.
Share a collection with a guest
Section titled “Share a collection with a guest”The Share Collection dialog (CollectionDialogShare.vue) is available to whoever can edit the collection. It asks for a Guest Email Address and an Expiry Date (defaults to 30 days ahead, must be after today) and offers two buttons:
Send Invitecreates the invitation withsendEmail: true. A job onmailer/invitationsends a link to/collections/{id}?dam_token=<jwt>; the client stores that token in thedam_tokencookie, so the guest is logged in on arrival.Copy Linkcreates the invitation silently and copies a URL carryingauth_params(base64 of the email,magicLink: trueand the collection). Opening it pre-fills the login form and sends a login email.
On the server, collection.invitation.create looks up a user with that email. If none exists, createGuestUser creates one with name and company set to NA, role guest, approved and emailVerified true, the inviter’s region and that region’s default group. The invitation stores collection_id, email, user_id and expires_at.
The dialog lists invitations with their expiry date, a Copy Link and a Remove button; collection.invitation.remove deletes the row. collection.invitation.getUserInvitations feeds the member links dialog with invitations on the caller’s collections (and, for admins, on all public ones). Deleting a user deletes the invitations sent to their email.
Who can see a collection
Section titled “Who can see a collection”userCollectionsQuery grants access when any of these holds:
| Rule | Condition |
|---|---|
| Owner | owner_id is the user |
| Admin | user is admin and the collection is public (drafts included) |
| Public | user is member or manager, collection is public and not draft, has no group limit, and the folder license allows the user’s region today |
| Group | one of the user’s groups is in limited_to_group_ids |
| Invitation | an unexpired invitation for the user exists on the collection or on any ancestor in mpath |
Files follow the same rules through userCollectionFilesQuery, with the license read from the file. See Licenses for the license clause and Menu and pages for how public collections appear in the navigation.