Перейти к основному содержимому

Декораторы загрузки файлов

Express-Cargo привязывает файлы из запроса multipart/form-data прямо в ваш DTO вместе с обычными полями. Две формы, которые может принимать загрузка, покрываются двумя декораторами:

  • @UploadedFile() — один файл для поля.
  • @UploadedFiles() — все файлы, разделяющие имя поля, в виде массива.

Express-Cargo не разбирает тело multipart самостоятельно. Промежуточное ПО-парсер (например, multer) должно выполняться до bindingCargo и прикреплять разобранные файлы к запросу. Затем Express-Cargo находит эти файлы и привязывает их как есть — объекты файлов никогда не изменяются.

@UploadedFile(key?: string)​

Привязывает один загруженный файл. Если несколько файлов разделяют имя поля, привязывается первый.

  • key: Имя поля формы. По умолчанию — имя свойства.
class UploadAvatarRequest {
@UploadedFile()
avatar!: Express.Multer.File
}

@UploadedFiles(key?: string)​

Привязывает все загруженные файлы, разделяющие имя поля, в виде массива.

  • key: Имя поля формы. По умолчанию — имя свойства.
class UploadGalleryRequest {
@UploadedFiles('photos')
photos!: Express.Multer.File[]
}

Пример использования​

Настройте парсер multipart, запустите его как промежуточное ПО маршрута перед bindingCargo, затем прочитайте привязанные файлы из DTO. Обычные поля, такие как @Body, привязываются из того же запроса обычным образом.

import express, { Request, Response } from 'express'
import multer from 'multer'
import { bindingCargo, getCargo, Body, UploadedFile, UploadedFiles } from 'express-cargo'

const upload = multer({ storage: multer.memoryStorage() })

// Текстовое поле и один файл в одном запросе
class UploadProfileRequest {
@Body('bio')
bio!: string

@UploadedFile('avatar')
avatar!: Express.Multer.File

@UploadedFiles('gallery')
gallery!: Express.Multer.File[]
}

const app = express()

// Промежуточное ПО-парсер выполняется первым и объявляет ожидаемые поля,
// затем bindingCargo сопоставляет разобранные файлы с DTO.
app.post(
'/upload/profile',
upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery' }]),
bindingCargo(UploadProfileRequest),
(req: Request, res: Response) => {
const cargo = getCargo<UploadProfileRequest>(req)
res.json({
bio: cargo.bio,
avatar: cargo.avatar.originalname,
gallery: cargo.gallery.map(file => file.originalname),
})
},
)

Промежуточное ПО-парсер определяет, какие поля принимаются:

Вызов multerСоответствующий декоратор
upload.single('avatar')@UploadedFile('avatar')
upload.array('photos')@UploadedFiles('photos')
upload.fields([{ name: 'avatar' }, ... ])по одному декоратору на именованное поле
upload.any()любой из вышеперечисленных, по полю

Отсутствующие файлы​

Поле загрузки следует тем же правилам отсутствующих значений, что и любой другой источник. Когда для поля не приходит файл:

  • Без обоих декораторов привязка завершается ошибкой <field> is required.
  • С @Optional() поле привязывается к null, а его валидаторы пропускаются.
  • С @Default(value) используется запасное значение.
class UploadRequest {
@UploadedFile()
@Optional()
avatar?: Express.Multer.File
}

@Transform не допускается​

Файлы привязываются точно так, как их создал парсер, поэтому пользовательскому @Transform на поле загрузки нечего преобразовывать. Их сочетание вызывает ошибку схемы при запуске:

class UploadRequest {
// ❌ Недопустимо: файлы привязываются как есть и не могут быть преобразованы
@UploadedFile()
@Transform((file: Express.Multer.File) => file)
avatar!: Express.Multer.File
}

Использование другого парсера​

По умолчанию Express-Cargo читает файлы из req.file и req.files multer. Чтобы интегрировать парсер с другой структурой запроса — например, formidable, другой широко используемый парсер multipart — зарегистрируйте один раз при запуске приложения пользовательский локатор файлов через setCargoFileLocator. Он применяется к каждому маршруту, поэтому локатор адаптирует парсер в целом, а не знает о каком-либо конкретном поле.

Локатор принимает Express Request и возвращает загруженные файлы, сгруппированные по имени поля формы — Record<string, File[]>, индексированный по имени поля. Поскольку formidable не является промежуточным ПО, разберите запрос самостоятельно и прикрепите результат; затем локатор передаёт его в Express-Cargo:

import express, { Request, Response, NextFunction } from 'express'
import formidable, { File } from 'formidable'
import { setCargoFileLocator } from 'express-cargo'

// 1. formidable не является промежуточным ПО — разберите запрос самостоятельно и прикрепите результат.
// В v3 `files` уже индексирован по имени поля с массивом на каждое поле.
async function parseMultipart(req: Request, _res: Response, next: NextFunction) {
const [, files] = await formidable().parse(req)
;(req as any).files = files // Record<string, File[]>
next()
}

// 2. Зарегистрируйте локатор один раз при запуске, чтобы Express-Cargo читал эти файлы.
setCargoFileLocator((req: Request): Record<string, File[]> => (req as any).files ?? {})

Каждое поле сопоставляется с массивом, даже то, что несёт один файл: @UploadedFile() берёт первый элемент массива своего поля, а @UploadedFiles() берёт весь массив. Только логика поиска специфична для парсера; сами объекты файлов по-прежнему привязываются как есть.

Поскольку файлы привязываются как есть, их тип и имена свойств происходят от парсера: multer предоставляет originalname и buffer, а formidable предоставляет originalFilename и filepath. Типизируйте каждое поле DTO типом файла используемого вами парсера и читайте свойства, которые предоставляет этот парсер.