Skip to content

Configuration

Everything lives in config/attachment.ts, built with defineConfig. The only required field is storage.

ts
// 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)

ts
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:

ts
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:

ts
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  >  defaults

Set 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).

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

OptionAccepted valueFallback when unsetScope
diskStorage disk nameStorage adapter defaultConfig defaults, decorator, manager
folderRelative path or async callbackStorage rootConfig defaults, decorator, manager
renameBoolean or async filename callbacktrueConfig defaults, decorator, manager
normalizeFileNameBooleantrueConfig defaults, decorator, manager
metaBooleanExtraction not requestedConfig defaults, decorator, manager
preComputeUrlBooleanfalseSame priority; consumed on Lucid relation reads
variantsArray of configured keysNo automatic variantsSame priority; automatic scheduling with Lucid
originalName, mimeTypeStringDerived from the sourceManager options only
metadataMetadata objectNo supplied metadataManager options only
maxBytesPositive integerNo size ceilingsources.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.

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

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

ts
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:

sh
npm install sharp exifreader
SourceDefault extractionRequirement
Supported raster imagesSharp technical metadata, then EXIF/GPS where supportedsharp and exifreader
SVGSharp technical metadata, including dimensions; never EXIFsharp only
Audio/videoDuration, codecs, and video dimensionsffprobe executable
PDFDimensions, pages, version, and creation datepdfinfo executable
DOCX and other unsupported formatsNo extracted metadata; the original can still be storedNone 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.

ts
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:

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

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

ts
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:

ts
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:

ts
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:

tableNamelink table
adonis_attachmentsadonis_attachment_links
media_attachmentsmedia_attachment_links
ts
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:

ts
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 enables GET /attachments/:id/:name? with another persistence layer. See Serving files.
  • route - false to disable the built-in route, or { prefix: '/media' } to move it.
  • queue - controls where jobs run. Omit it for an in-memory queue, or declare queue.connections and a typed queue.default to select a connection, optionally by environment. The generated configuration starts with the memory connection. 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 or jobHandler. See Background processing.

Next: Creating attachments.