Skip to content
Attachment
Language:en Author:AtomForge

Upload Middleware

Not every upload should be accepted as it is. You may want to cap the size, allow only images, convert everything to WebP or read an MP3's duration. Middlewares do exactly this. They are small functions that run one after another before a file is stored, and each can reject the upload or change the file and its metadata. This chapter lists the built-in ones in three groups: validators, transformers and name helpers.

Middlewares run in order before add() and replace(). Throw AttachmentValidationError to abort the upload.

type UploadMiddleware<TMeta = unknown> = (
  file:  File,
  meta:  TMeta,
  files: AttachmentData<TMeta>[],   // existing files in the category
) => Promise<{ file: File; meta: TMeta }>;

Middlewares run in the order you list them. The diagram shows a typical chain: the cheap checks come first and can stop the upload early, then the transformers reshape the file.

flowchart LR
  F[File + meta] --> V1[count] --> V2[mime] --> T1[toWebP] --> T2[imgstat] --> R[Stored file + meta]
  V1 -. throws .-> X[AttachmentValidationError]
  V2 -. throws .-> X

Validators

count(max)

Rejects the upload when the category already has max or more files.

count(1)   // at most one file (avatar use case)
count(10)  // at most ten

size(maxBytes)

Rejects files larger than maxBytes.

size(2 * 1024 * 1024)   // 2 MB

sumSize(maxBytes)

Rejects when total size of all existing files + the new file exceeds maxBytes.

sumSize(50 * 1024 * 1024)   // 50 MB total for the category

mime(pattern)

Allows only files whose MIME type matches. Supports wildcard subtypes.

mime('image/*')
mime(['image/jpeg', 'image/png', 'image/webp'])

ext(extensions)

Allows only files with a matching extension (case-insensitive, leading dot required).

ext(['.jpg', '.jpeg', '.png', '.webp'])

Transformers

Require peer dependencies (sharp, music-metadata).

toWebP(lossy?, width?, height?)

Converts the uploaded image to WebP.

Param Type Default Description
lossy false | number false false = lossless; 0–100 = lossy quality
width number — Max width. One dimension → proportional resize (no enlarge). Both → contain.
height number — Max height.
toWebP()              // lossless, no resize
toWebP(80)            // lossy q80
toWebP(false, 1280)   // lossless, max-width 1280
toWebP(80, 800, 600)  // lossy q80, contain in 800×600

Peer dependency: sharp

imgstat(cropMode?, focusHash?)

Reads image dimensions, dominant color, animated flag, and crop mode. Injects an ImgStat object into meta.img.

Param Type Default Description
cropMode 'e' | 'a' | 'c' | Focus 'e' Crop strategy stored for thumbnail URL building
focusHash FocusHashFn — Required to store meta.img.ch for manual focus
imgstat()        // entropy crop (default)
imgstat('a')     // attention crop
imgstat('c')     // centre crop
imgstat({ x: 500, y: 300, s: { x: 0, y: 0, w: 1000, h: 1000 } })  // manual focus

The injected ImgStat:

interface ImgStat {
  w:   number;                    // width px
  h:   number;                    // height px
  d:   string;                    // dominant color (#rrggbb)
  a:   boolean;                   // animated (GIF / animated WebP)
  c:   'e' | 'a' | 'c' | Focus;  // stored crop mode
  ch?: string;                    // HMAC hash of focus12 (manual focus only)
}
const meta = attachment.meta as { img: ImgStat };

Peer dependency: sharp

getMp3Duration(metaKey?)

Reads the duration of an MP3 and injects it into meta[metaKey] (seconds, number). Default metaKey: 'duration'.

getMp3Duration()                  // → meta.duration
getMp3Duration('durationSeconds') // → meta.durationSeconds

Peer dependency: music-metadata


Name helpers

randname()

Replaces the uploaded filename with a random UUID-based name, keeping the extension.

randname()
// 'photo.jpg' → 'f47ac10b-58cc-4372-a567-0e02b2c3d479.jpg'

No extra dependency — uses crypto.randomUUID().