Configuration
Everything lives in config/attachment.ts, built with defineConfig. The only required field is storage.
// config/attachment.ts - generated by `node ace configure @jrmc/adonis-attachment`
import { defineConfig, LocalFileStorage } from '@jrmc/adonis-attachment'
export default defineConfig({
storage: LocalFileStorage.fromApp,
})Storage
Storage decides where the bytes go. Pick one.
Local filesystem (default)
storage: LocalFileStorage.fromApp,LocalFileStorage writes under storage/attachments, on the fs disk. It creates parent directories, writes atomically (temp file + rename), and refuses paths that escape its root. Pass baseUrl when those files are exposed publicly and you need public URLs:
storage: new LocalFileStorage({
location: app.makePath('storage/attachments'),
baseUrl: 'https://app.example.test/uploads',
})Adonis Drive (S3, GCS, ...)
Install @adonisjs/drive, configure config/drive.ts, then:
import { AdonisDriveStorage, defineConfig } from '@jrmc/adonis-attachment'
export default defineConfig({
storage: AdonisDriveStorage.fromApp, // uses your default Drive disk
})Your own backend
Any object implementing AttachmentStorage (write, read, remove) works. The fallback disk name is fs unless your adapter exposes defaultDisk or you set defaultDisk explicitly.
Default persistence options
defaults sets the lowest-priority file options. They're resolved per setting, from most to least specific:
createFrom*(...) options > decorator options > defaultsSet an option to null at a higher level to clear an inherited value. This restores the package fallback; it is not equivalent to false (for example, rename: null restores generated names, while rename: false uses the client filename).
export default defineConfig({
storage: LocalFileStorage.fromApp,
defaults: {
folder: 'uploads',
rename: true, // store under a generated name instead of the client name
normalizeFileName: true, // make supplied filenames portable across storage backends
variants: ['thumbnail'],
},
})Available options: disk, folder, rename, normalizeFileName, meta, preComputeUrl, variants.
| Option | Accepted value | Fallback when unset | Scope |
|---|---|---|---|
disk | Storage disk name | Storage adapter default | Config defaults, decorator, manager |
folder | Relative path or async callback | Storage root | Config defaults, decorator, manager |
rename | Boolean or async filename callback | true | Config defaults, decorator, manager |
normalizeFileName | Boolean | true | Config defaults, decorator, manager |
meta | Boolean | Extraction not requested | Config defaults, decorator, manager |
preComputeUrl | Boolean | false | Same priority; consumed on Lucid relation reads |
variants | Array of configured keys | No automatic variants | Same priority; automatic scheduling with Lucid |
originalName, mimeType | String | Derived from the source | Manager options only |
metadata | Metadata object | No supplied metadata | Manager options only |
maxBytes | Positive integer | No size ceiling | sources.maxBytes and manager options |
meta requests extraction, whereas metadata supplies already known values. The media policy controls whether extraction is synchronous, deferred, or disabled. Outside Lucid, the application owns database persistence and post-commit job scheduling.
meta activates the default metadata extractors during persist(). With Lucid relations, preComputeUrl calculates the public URL when the relation is read and keeps it only in memory. With Lucid relations, variants schedules the listed keys after the blob and its link are committed. It is resolved with the same priority as the other persistence options.
Folder and rename defaults
folder accepts a relative path or an async callback. rename accepts true, false, or a callback that returns the stored filename. normalizeFileName defaults to true and makes names supplied by rename: false or a callback portable across storage backends. Defaults may use V5-style :attribute parameters, which are resolved only when a model context exists, such as a Lucid decorator or relation:
Set normalizeFileName: false only when the selected storage backend accepts the supplied filename unchanged.
defaults: {
folder: 'uploads/:name',
rename: () => ':name-file.jpg',
}String model attributes are lowercased, HTML-escaped, and slugified. A parameter that is unknown or not a string remains unchanged. See Creating attachments for callbacks and standalone behavior.
URLs
Use attachmentService to resolve URLs from any persisted attachment. Public URLs can be pre-calculated on Lucid relation reads with preComputeUrl: true; signed URLs are always generated on demand and are never cached or stored.
import { attachmentService } from '@jrmc/adonis-attachment'
const url = await attachmentService.getUrl(attachment)
const signedUrl = await attachmentService.getSignedUrl(attachment, {
expiresIn: '15m',
})Storage adapters without public or signed URL support return undefined. AdonisDriveStorage delegates to the selected Drive disk. LocalFileStorage can generate public URLs only when configured with baseUrl.
Media binaries
Declare external media binaries once under media.binaries. The autodetected video, PDF, and office-document converters use these values. A command or timeout declared directly on a converter remains more specific and takes precedence.
const binaries = {
ffmpeg: { command: '/opt/media/bin/ffmpeg', timeout: 15_000 },
ffprobe: { command: '/opt/media/bin/ffprobe', timeout: 5_000 },
pdftoppm: { command: '/opt/media/bin/pdftoppm', timeout: 10_000 },
pdfinfo: { command: '/opt/media/bin/pdfinfo', timeout: 5_000 },
soffice: { command: '/opt/media/bin/libreoffice', timeout: 20_000 },
}
export default defineConfig({
storage: LocalFileStorage.fromApp,
media: {
binaries,
},
})The default metadata extractors reuse the ffprobe and pdfinfo declarations automatically. Converter-level values remain more specific and take precedence over shared binary values.
Media metadata
Set meta: true on an attachment or in defaults to enable the built-in profile. It extracts the historical metadata shape: dimension, orientation, date, host, gps, duration, codecs, PDF pages, and PDF version. No media.metadata entry is required.
For the default image profile, install the optional dependencies:
npm install sharp exifreader| Source | Default extraction | Requirement |
|---|---|---|
| Supported raster images | Sharp technical metadata, then EXIF/GPS where supported | sharp and exifreader |
| SVG | Sharp technical metadata, including dimensions; never EXIF | sharp only |
| Audio/video | Duration, codecs, and video dimensions | ffprobe executable |
| Dimensions, pages, version, and creation date | pdfinfo executable | |
| DOCX and other unsupported formats | No extracted metadata; the original can still be stored | None for metadata |
Sharp adds format, density, hasAlpha, and available page/frame information. EXIF enriches that result with fields such as orientation description, date, software, and GPS. Expanded ExifReader coordinates are normalized to gps.latitude, gps.longitude, and gps.altitude automatically; no application reader wrapper or type cast is needed.
Image readers are loaded only when a matching extraction runs, not when configuration is created. Missing required packages raise E_MISSING_PACKAGE; corrupt supported files and binary failures still raise errors. Unsupported formats are skipped rather than sent to an incompatible parser. In particular, meta: true does not imply that DOCX metadata is supported. The existing binary path configuration applies to ffprobe and pdfinfo unchanged.
Advanced: override the extractors
media.metadata completely replaces the default profile. Extractors receive the finalized attachment information and source bytes. Their results are merged in declaration order; metadata supplied by the application wins on key conflicts.
import { defineConfig, LocalFileStorage } from '@jrmc/adonis-attachment'
import { createDefaultMetadataExtractors } from '@jrmc/adonis-attachment/media/metadata'
export default defineConfig({
storage: LocalFileStorage.fromApp,
media: {
metadata: createDefaultMetadataExtractors({
ffprobe: { command: '/opt/media/bin/ffprobe', timeout: 5_000 },
pdfinfo: { command: '/opt/media/bin/pdfinfo', timeout: 5_000 },
}),
},
defaults: { meta: true },
})command accepts either an executable available on PATH or an absolute path. Set sharp, exif, ffprobe, or pdfinfo to false to disable an extractor. Use metadata: [] to disable all default extraction even when an attachment requests meta: true.
For example, createDefaultMetadataExtractors({ sharp: false }) keeps EXIF and the binary extractors but disables Sharp technical metadata, including SVG metadata. To customize Sharp, pass a metadata factory as sharp; the actual imported sharp function is accepted.
Performance policy
Metadata runs synchronously by default, preserving the v5 behavior: persist() waits for the extraction. Set command timeouts for external binaries to prevent a stalled process from holding a request indefinitely:
media: {
binaries: {
ffprobe: { timeout: 5_000 },
pdfinfo: { timeout: 5_000 },
},
}For video or PDF-heavy applications, defer extraction to the queue after the attachment row is committed. Lucid configures its metadata persister automatically; another ORM must provide metadataPersister with a persistMetadata(attachment, metadata) method.
media: {
metadataPolicy: {
mode: 'deferred',
variants: false,
},
}variants defaults to true for v5 compatibility. Set it to false when thumbnails do not need their own metadata. This disables both synchronous extraction and deferred metadata jobs for variants, even with defaults.meta: true. An explicit meta: false on a generation request also overrides the global metadata default. Originals retain their own metadata settings. In deferred mode, configure the metadata processor shown in Background processing so workers handle extract-metadata jobs.
With custom persistence, call AttachmentService.scheduleMetadataExtraction(attachment) only after the record that owns the attachment has committed. This prevents a worker from updating a record that does not exist yet. See the complete custom-persistence example.
import { defineConfig, LocalFileStorage, type MediaMetadataExtractor } from '@jrmc/adonis-attachment'
const imageMetadata: MediaMetadataExtractor = {
supports: ({ attachment }) => attachment.mimeType.startsWith('image/'),
async extract({ body }) {
return { sourceBytes: body.byteLength }
},
}
export default defineConfig({
storage: LocalFileStorage.fromApp,
media: { metadata: [imageMetadata] },
})The default profile already includes Sharp. To explicitly replace the profile with only Sharp image metadata, use the optional adapter instead of writing the extractor yourself:
import sharp from 'sharp'
import { createSharpMetadataExtractor } from '@jrmc/adonis-attachment/media/sharp'
media: { metadata: [createSharpMetadataExtractor(sharp)] }For audio and video, createFfprobeMetadataExtractor() from @jrmc/adonis-attachment/media/binaries reads duration, codecs and video dimensions. It accepts the same command and timeout options when the v5 profile is not used.
Generated variants use the same extraction pipeline when their original was scheduled with meta: true. Converter-provided metadata still takes precedence over extracted values.
Source limits
sources configures the attachment manager. The package enforces no size policy by default - validate uploads in your app. You may set a technical byte ceiling:
export default defineConfig({
storage: LocalFileStorage.fromApp,
sources: { maxBytes: 10 * 1024 * 1024 }, // 10 MB, per source
})Lucid table names
By default the Lucid integration uses adonis_attachments (blobs) and adonis_attachment_links (links). The link table name is always derived from the blob table name with string.singular:
tableName | link table |
|---|---|
adonis_attachments | adonis_attachment_links |
media_attachments | media_attachment_links |
export default defineConfig({
storage: LocalFileStorage.fromApp,
integrations: {
lucid: { tableName: 'media_attachments' },
},
})Lucid is detected automatically through the lucid.db container binding. When present, the package configures the default Lucid repository, enables the read route, and can persist deferred metadata without additional wiring. Use integrations.lucid only to customize table names or to disable this automatic integration:
integrations: {
lucid: false,
}WARNING
Use the same tableName when you generate the migration and at runtime - the models are pointed at these names at boot. Changing configuration does not rename existing tables. To keep an existing attachments and attachment_links schema, explicitly set integrations.lucid.tableName: 'attachments'.
Read route & background processing
repository- overrides the automatically detected Lucid repository, or enablesGET /attachments/:id/:name?with another persistence layer. See Serving files.route-falseto disable the built-in route, or{ prefix: '/media' }to move it.queue- controls where jobs run. Omit it for an in-memory queue, or declarequeue.connectionsand a typedqueue.defaultto select a connection, optionally by environment. The generated configuration starts with thememoryconnection. The direct driver, instance, and factory forms remain supported.processor- overrides the processor automatically created for an in-memory queue when Lucid is detected. Custom persistence must provide a processor orjobHandler. See Background processing.
Next: Creating attachments.