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>[],
) => 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)
count(10)
size(maxBytes)
Rejects files larger than maxBytes.
size(2 * 1024 * 1024)
sumSize(maxBytes)
Rejects when total size of all existing files + the new file exceeds maxBytes.
sumSize(50 * 1024 * 1024)
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'])
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()
toWebP(80)
toWebP(false, 1280)
toWebP(80, 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()
imgstat('a')
imgstat('c')
imgstat({ x: 500, y: 300, s: { x: 0, y: 0, w: 1000, h: 1000 } })
The injected ImgStat:
interface ImgStat {
w: number;
h: number;
d: string;
a: boolean;
c: 'e' | 'a' | 'c' | Focus;
ch?: string;
}
const meta = attachment.meta as { img: ImgStat };
Peer dependency: sharp
Reads the duration of an MP3 and injects it into meta[metaKey] (seconds, number). Default metaKey: 'duration'.
getMp3Duration()
getMp3Duration('durationSeconds')
Peer dependency: music-metadata
Name helpers
randname()
Replaces the uploaded filename with a random UUID-based name, keeping the extension.
randname()
No extra dependency — uses crypto.randomUUID().
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()andreplace(). ThrowAttachmentValidationErrorto 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.
Validators
count(max)Rejects the upload when the category already has
maxor more files.count(1) // at most one file (avatar use case) count(10) // at most tensize(maxBytes)Rejects files larger than
maxBytes.size(2 * 1024 * 1024) // 2 MBsumSize(maxBytes)Rejects when total size of all existing files + the new file exceeds
maxBytes.sumSize(50 * 1024 * 1024) // 50 MB total for the categorymime(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.
lossyfalse | numberfalsefalse= lossless;0–100= lossy qualitywidthnumberheightnumbertoWebP() // lossless, no resize toWebP(80) // lossy q80 toWebP(false, 1280) // lossless, max-width 1280 toWebP(80, 800, 600) // lossy q80, contain in 800×600Peer dependency:
sharpimgstat(cropMode?, focusHash?)Reads image dimensions, dominant color, animated flag, and crop mode. Injects an
ImgStatobject intometa.img.cropMode'e' | 'a' | 'c' | Focus'e'focusHashFocusHashFnmeta.img.chfor manual focusimgstat() // 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 focusThe 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:
sharpgetMp3Duration(metaKey?)Reads the duration of an MP3 and injects it into
meta[metaKey](seconds, number). DefaultmetaKey:'duration'.getMp3Duration() // → meta.duration getMp3Duration('durationSeconds') // → meta.durationSecondsPeer dependency:
music-metadataName 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().