FoalTS / foal

Full-featured Node.js framework, with no complexity. 🚀 Simple and easy to use, TypeScript-based and well-documented.
https://foalts.org/
MIT License
1.89k stars 139 forks source link

A hook for validating 'multipart/formdata' request (file uploads). #560

Closed kingdun3284 closed 4 years ago

kingdun3284 commented 4 years ago

As the title said.

LoicPoullain commented 4 years ago

Thank for submitting this issue @kingdun3284 .

I guess that it would be in the case that we need to upload files? Or do you have also another use case?

kingdun3284 commented 4 years ago

Yes, I need to upload file, but not just file and some data field with it. Although it seems ajv can't validate file type and size, I think it should be able to validate those field value. So a parser hook to parse incoming stream and validation hook to validate those fields and emit the remaining file stream to the controller function would be great.

LoicPoullain commented 4 years ago

This a definitely a must have. I'm moving this to the To Do column.

In the meantime, if you use @foal/formidable, it's possible to validate the fields manually:

const form = new IncomingForm();
form.uploadDir = 'uploaded';
form.keepExtensions = true;
const { fields, files } = await parseForm(form, ctx);

// Use Ajv to validate `fields`.

This example only addresses one aspect of the problem though.

kingdun3284 commented 4 years ago

This a definitely a must have. I'm moving this to the To Do column.

In the meantime, if you use @foal/formidable, it's possible to validate the fields manually:

const form = new IncomingForm();
form.uploadDir = 'uploaded';
form.keepExtensions = true;
const { fields, files } = await parseForm(form, ctx);

// Use Ajv to validate `fields`.

This example only addresses one aspect of the problem though.

Yes, I have tried this. And I think it is better not to use formidable, because It has no way to cancel the upload process manually, the file will upload no matter it is valid or not. It is better to use busboy or multar that support the function mention above. So that it won't waste resource and time when the upload file is not valid.

LoicPoullain commented 4 years ago

Overall remarks on multipart/formdata requests

So a parser hook to parse incoming stream and validation hook to validate those fields and emit the remaining file stream to the controller function would be great.

It is technically not possible to create such a hook (that emits remaining file stream to the controller) due to how multipart/formdata requests work. There is no guarantee that the fields reach the server before the file. We cannot make assumptions about the order of arrival of files and fields. We can have, for example, a file, then a field, then another field, then another file.

So there are two solutions IMO:

The below proposal offers the two possibilities (using FoalTS filesystem for the second).

Proposal

Example 1: one field, one file (buffer)

import { ValidateMultipartFormDataBody } from '@foal/storage';

export class ApiController {
  @ValidateMultipartFormDataBody({
    fields: {
      name: { type: 'string' },
    },
    files: [ 'profile' ],
  })
  upload(ctx) {
    ctx.request.body.name // string
    ctx.request.body.profile // Buffer
  }
}

Example 2: multiple files (buffer)

<input type="file" id="profile" name="profile">
<input type="file" id="album" name="album" multiple>
import { ValidateMultipartFormDataBody } from '@foal/storage';

export class ApiController {
  @ValidateMultipartFormDataBody({
    fields: {
      name: { type: 'string' },
    },
    files: [ 'profile', ['album'] ],
  })
  upload(ctx) {
    ctx.request.body.name // string
    ctx.request.body.profile // Buffer
    ctx.request.body.album // Buffer[]
  }
}

Example 3: file saved with FoalTS filesystem (it uses streams under the hood)

import { ValidateMultipartFormDataBody } from '@foal/storage';

export class ApiController {
  @ValidateMultipartFormDataBody({
    fields: {
      name: { type: 'string' },
    },
    files: [ 'profile' ],
    directory: 'avatars/new'
  })
  upload(ctx) {
    ctx.request.body.name // string
    ctx.request.body.profile.path // string. Ex: avatars/new/6xOOeNeytTJLMume3iNzK0PRF68hfh0agLbx2TDqkgg=.png
  }
}

Example 4: file saved with FoalTS filesystem (multiple directories)

import { ValidateMultipartFormDataBody } from '@foal/storage';

export class ApiController {
  @ValidateMultipartFormDataBody({
    fields: {
      name: { type: 'string' },
    },
    files: [ 'profile', [ 'album'] ],
    directory: [ 'avatars', 'album' ]
  })
  upload(ctx) {
    ctx.request.body.name // string
    ctx.request.body.profile.path // string
    ctx.request.body.album[0].path // string
  }
}
LoicPoullain commented 4 years ago

Version 1.7 published!