Single-File Store
@adonis-agora/media/single-file — a tiny seam for avatar-style uploads, so another package can delegate 'replace this owner's one file and give me a URL' to media without taking a hard dependency on it.
Some callers need one narrow thing from a media library: replace this owner's single file and hand me back a URL. An avatar. A workspace logo. A signature image.
Going through the full MediaLibrary for that means knowing about collections, conversions, records and disks — a lot of surface for one field. Worse, it means depending on media in a package that would otherwise work fine without it.
@adonis-agora/media/single-file is the seam that solves both. It is a handful of functions with no types of their own beyond plain inputs, and a feature-detection helper so a caller can degrade gracefully when media isn't installed at all. Other Agora packages use it exactly this way.
import {
isSingleFileStoreAvailable,
removeSingleFile,
storeSingleFile,
} from '@adonis-agora/media/single-file'Storing and removing
const { url, thumbUrl } = await storeSingleFile({
ownerType: 'User',
ownerId: String(user.id),
collection: 'avatar',
fileName: upload.clientName,
mimeType: upload.type!,
contents: await readFile(upload.tmpPath!), // a Buffer
})
user.avatarUrl = url
user.avatarThumbUrl = thumbUrl // string | nullstoreSingleFile attaches the file and resolves two URLs: the original, and the thumb conversion. thumbUrl is null — not an error — when that conversion isn't configured on the collection or couldn't be generated, because a missing thumbnail should never fail the store.
await removeSingleFile({ ownerType: 'User', ownerId: String(user.id), collection: 'avatar' })removeSingleFile empties the owner's collection, deleting every record it holds (which for a single-file collection is at most one) along with each one's disk object and conversions.
`single: true` lives in the config, not in the call
Nothing about these functions makes a collection single-slot. Replacement is a property of the
collection, so the collection you name here must be declared single: true in
config/media.ts. Without that, each call appends another file and "the avatar" quietly becomes
a growing list.
collections: [
{ name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] },
]Feature detection
if (await isSingleFileStoreAvailable()) {
const { url } = await storeSingleFile({ … })
} else {
await storeAvatarSomewhereElse(file)
}isSingleFileStoreAvailable() reports whether the MediaManager is bound in the app container. That is what lets a package offer media-backed uploads as an enhancement: import the subpath unconditionally, check before using it, and behave sensibly in an app that never installed media.
Explicit-manager variants
storeSingleFileWith(manager, input) and removeSingleFileWith(manager, input) are the same functions driven by a MediaManager you pass in, instead of one resolved from the container. Two uses:
- Tests — exercise the behaviour against an in-memory manager without booting an app. See Testing.
- Multiple managers — an app that builds more than one media manager and needs to say which.
import { storeSingleFileWith } from '@adonis-agora/media/single-file'
const { url } = await storeSingleFileWith(testManager, {
ownerType: 'User', ownerId: '1', collection: 'avatar',
fileName: 'me.png', mimeType: 'image/png', contents: pngBytes,
})The container-reading variants resolve the manager through the provider-captured booted app rather than @adonisjs/core/services/app — the same defence the rest of the package uses against a duplicated @adonisjs/core tree in a pnpm workspace.
Next steps
- Collections & Conversions — declaring the
single: truecollection this needs - Attachments — the other single-file shape: a file on a model column
- Testing — driving the
*Withvariants against an in-memory manager
Attachments
Column attachments — the adonis-attachment style, where a file (and its image variants) lives directly on a model column as a JSON value, over the same Drive disks and image processor as the media library.
Upload Modes
Two upload strategies over a multipart-capable disk — proxy (bytes stream through your app) and direct (the browser uploads straight to S3 via presigned multipart part URLs). MediaManager.uploads and the opt-in provider routes under /media/uploads.