Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(deps): update dependency formidable to v3.5.2 #40

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

renovate[bot]
Copy link
Contributor

@renovate renovate bot commented Dec 25, 2024

This PR contains the following updates:

Package Change Age Adoption Passing Confidence
formidable 3.2.4 -> 3.5.2 age adoption passing confidence

Release Notes

node-formidable/formidable (formidable)

v3.5.2

Compare Source

  • fix: (#​982) make it easier to import hexoid with webpack

v3.5.1

Compare Source

  • fix: (#​945) multipart parser fix: flush or fail always (don't hang)

v3.5.0

Compare Source

  • feature: (#​944) Dual package: Can be imported as ES module and required as commonjs module

v3.4.0

Compare Source

  • feature: (#​940) form.parse returns a promise if no callback is provided
  • it resolves with an array [fields, files]

v3.3.2

Compare Source

  • feature: (#​855) add options.createDirsFromUploads, see README for usage
  • form.parse is an async function (ignore the promise)
  • benchmarks: add e2e becnhmark with as many request as possible per second
    • npm run to display all the commands
  • mark as latest on npm

v3.2.5

Compare Source

  • fix: (#​881) fail earlier when maxFiles is exceeded

Configuration

📅 Schedule: Branch creation - "* 0-4 * * 3" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

Copy link

[puLL-Merge] - node-formidable/formidable@v3.2.4..v3.5.2

Diff
diff --git .eslintrc.cjs .eslintrc.cjs
index 3b6d4df6..fd6e9583 100644
--- .eslintrc.cjs
+++ .eslintrc.cjs
@@ -18,7 +18,7 @@ const ignoredProps = bestPractices.rules[
   'cfg',
 );
 
-// Additional rules that are specific and overiding previous
+// Additional rules that are specific and overriding previous
 const additionalChanges = {
   strict: 'off',
 
diff --git .github/workflows/nodejs.yml .github/workflows/nodejs.yml
index ff747ad1..2dda02de 100644
--- .github/workflows/nodejs.yml
+++ .github/workflows/nodejs.yml
@@ -28,7 +28,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        node: [14.x]
+        node: [20.x]
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
@@ -56,7 +56,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        node: [12.x, 14.x]
+        node: [18.x, 20.x]
     runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v2
@@ -77,5 +77,5 @@ jobs:
       - name: Testing
         run: yarn test:ci
       - name: Sending test coverage to CodeCov
-        if: matrix.os == 'ubuntu-latest' && matrix.node == '14.x'
+        if: matrix.os == 'ubuntu-latest' && matrix.node == '20.x'
         run: echo ${{ matrix.node }} && bash <(curl -s https://codecov.io/bash)
diff --git .gitignore .gitignore
index d20308f1..7c7ccb2c 100644
--- .gitignore
+++ .gitignore
@@ -24,6 +24,8 @@
 
 !**/test
 !**/test/**
+!**/test-node
+!**/test-node/**
 
 !**/*tests*
 !**/*tests*/**
@@ -125,3 +127,4 @@ test/tmp
 
 # # next.js build output
 # .next
+benchmark/testuploads/
diff --git CHANGELOG.md CHANGELOG.md
index fff528d0..a8b552ec 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,5 +1,45 @@
 # Changelog
 
+### 3.5.2
+
+ * fix: ([#982](https://github.com/node-formidable/formidable/pull/982)) make it easier to import hexoid with webpack
+
+### 3.5.1
+
+ * fix: ([#945](https://github.com/node-formidable/formidable/pull/945)) multipart parser fix: flush or fail always (don't hang)
+
+
+### 3.5.0
+
+ * feature: ([#944](https://github.com/node-formidable/formidable/pull/944)) Dual package: Can be imported as ES module and required as commonjs module
+
+
+### 3.4.0
+
+ * feature: ([#940](https://github.com/node-formidable/formidable/pull/940)) form.parse returns a promise if no callback is provided
+ * it resolves with an array `[fields, files]`
+
+
+### 3.3.2
+
+ * feature: ([#855](https://github.com/node-formidable/formidable/pull/855)) add options.createDirsFromUploads, see README for usage
+ * form.parse is an async function (ignore the promise)
+ * benchmarks: add e2e becnhmark with as many request as possible per second
+    * npm run to display all the commands
+ * mark as latest on npm
+
+### 3.2.5
+
+ * fix: ([#881](https://github.com/node-formidable/formidable/pull/881)) fail earlier when maxFiles is exceeded
+
+### 3.2.4
+
+ * fix: ([#857](https://github.com/node-formidable/formidable/pull/857)) improve keep extension
+ * The code from before 3.2.4 already removed some characters from the file extension. But not always. So it was inconsistent.
+ * The new code cuts the file extension at the first invalid character (invalid in a file extension).
+ * The characters that are considered invalid inside a file extension are all except the . numbers and a-Z.
+ * This change only has an effect if filename option is not used and keepextension option is used
+
 
 ### 3.2.3
 
@@ -46,12 +86,12 @@
 
 ### 3.0.0
 
- * feat: remove options.multiples ([730](https://github.com/node-formidable/formidable/pull/730))
+ * feat: remove options.multiples ([#730](https://github.com/node-formidable/formidable/pull/730))
  * use modern URLSearchParams https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams internally
  * files and fields values are always arrays
  * fields with [] in the name do not receive special treatment
  * remove unused qs and querystring dependency
- * feat: Use ES modules ([727](https://github.com/node-formidable/formidable/pull/727))
+ * feat: Use ES modules ([#727](https://github.com/node-formidable/formidable/pull/727))
  * options.enabledPlugins must contain the plugin themselves instead of the plugins names 
 
 
diff --git README.md README.md
index 6bf71dbe..b1f12934 100644
--- README.md
+++ README.md
@@ -20,7 +20,7 @@ at Twitter.
 
 [![Conventional Commits][ccommits-img]][ccommits-url]
 [![Minimum Required Nodejs][nodejs-img]][npmv-url]
-[![Tidelift Subcsription][tidelift-img]][tidelift-url]
+[![Tidelift Subscription][tidelift-img]][tidelift-url]
 [![Buy me a Kofi][kofi-img]][kofi-url]
 [![Renovate App Status][renovateapp-img]][renovateapp-url]
 [![Make A Pull Request][prs-welcome-img]][prs-welcome-url]
@@ -68,6 +68,8 @@ rules, like enabling Two-Factor Auth in your npm and GitHub accounts.
 
 ## Install
 
+This package is a dual ESM/commonjs package.
+
 This project requires `Node.js >= 10.13`. Install it using
 [yarn](https://yarnpkg.com) or [npm](https://npmjs.com).<br /> _We highly
 recommend to use Yarn when you think to contribute to this project._
@@ -77,14 +79,14 @@ already be included. Check the examples below and the [examples/](https://github
 
 \`\`\`
 # v2
-npm install formidable
 npm install formidable@v2
 
 # v3
+npm install formidable
 npm install formidable@v3

-Note: In the near future v3 will be published on the latest NPM dist-tag. Future not ready releases will be published on *-next dist-tags for the corresponding version.
+Note: Future not ready releases will be published on *-next dist-tags for the corresponding version.

Examples

@@ -98,23 +100,28 @@ Parse an incoming file upload, with the

import http from 'node:http';
-import formidable from 'formidable';
+import formidable, {errors as formidableErrors} from 'formidable';

-const server = http.createServer((req, res) => {
+const server = http.createServer(async (req, res) => {
  if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
    // parse a file upload
    const form = formidable({});
+    let fields;
+    let files;
+    try {
+        [fields, files] = await form.parse(req);
+    } catch (err) {
+        // example to check for a very specific error
+        if (err.code === formidableErrors.maxFieldsExceeded) {

-    form.parse(req, (err, fields, files) => {
-      if (err) {
+        }
+        console.error(err);
        res.writeHead(err.httpCode || 400, { 'Content-Type': 'text/plain' });
        res.end(String(err));
        return;
-      }
-      res.writeHead(200, { 'Content-Type': 'application/json' });
-      res.end(JSON.stringify({ fields, files }, null, 2));
-    });
-
+    }
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    res.end(JSON.stringify({ fields, files }, null, 2));
    return;
  }

@@ -338,7 +345,9 @@ See it's defaults in [src/Formidable.js DEFAULT_OPTIONS](./src/Formidable.js)
  newFilename. Must return a string. Will be joined with options.uploadDir.

- `options.filter` **{function}** - default function that always returns true.
-  Use it to filter files before they are uploaded. Must return a boolean.
+  Use it to filter files before they are uploaded. Must return a boolean. Will not make the form.parse error
+
+- `options.createDirsFromUploads` **{boolean}** - default false. If true, makes direct folder uploads possible. Use `<input type="file" name="folders" webkitdirectory directory multiple>` to create a form to upload folders. Has to be used with the options `options.uploadDir` and `options.filename` where `options.filename` has to return a string with the character `/` for folders to be created. The base will be `options.uploadDir`.


#### `options.filename`  **{function}** function (name, ext, part, form) -> string
@@ -364,7 +373,7 @@ form.bytesExpected;

#### `options.filter`  **{function}** function ({name, originalFilename, mimetype}) -> boolean

-**Note:** use an outside variable to cancel all uploads upon the first error 
+Behaves like Array.filter: Returning false will simply ignore the file and go to the next.

```js
const options = {
@@ -375,11 +384,29 @@ const options = {
};

+Note: use an outside variable to cancel all uploads upon the first error

-### .parse(request, callback)
+Note: use form.emit('error') to make form.parse error

-Parses an incoming Node.js request containing form data. If callback is
-provided, all fields and files are collected and passed to the callback.
+```js
+let cancelUploads = false;// create variable at the same scope as form
+const options = {

  • filter: function ({name, originalFilename, mimetype}) {
  • // keep only images
  • const valid = mimetype && mimetype.includes("image");
  • if (!valid) {
  •  form.emit('error', new formidableErrors.default('invalid type', 0, 400)); // optional make form.parse error
    
  •  cancelUploads = true; //variable to make filter return false after the first problem
    
  • }
  • return valid && !cancelUploads;
  • }
    +};
    +```

+### .parse(request, ?callback)
+
+Parses an incoming Node.js request containing form data. If callback is not provided a promise is returned.

const form = formidable({ uploadDir: __dirname });
@@ -388,6 +415,9 @@ form.parse(req, (err, fields, files) => {
  console.log('fields:', fields);
  console.log('files:', files);
});
+
+// with Promise
+const [fields, files] = await form.parse(req);

You may overwrite this method if you are interested in directly accessing the
@@ -487,7 +517,7 @@ form.on('data', ({ name, key, value, buffer, start, end, formname, ...more }) =>

.use(plugin: Plugin)

A method that allows you to extend the Formidable library. By default we include
-4 plugins, which esentially are adapters to plug the different built-in parsers.
+4 plugins, which essentially are adapters to plug the different built-in parsers.

The plugins added by this method are always enabled.

@@ -644,7 +674,7 @@ form.on('fileBegin', (formName, file) => {
// formName the name in the form () or http filename for octetstream
// file.originalFilename http filename or null if there was a parsing error
// file.newFilename generated hexoid or what options.filename returned

  • // file.filepath default pathnme as per options.uploadDir and options.filename
  • // file.filepath default pathname as per options.uploadDir and options.filename
    // file.filepath = CUSTOM_PATH // to change the final path
    });
diff --git a/README_pt_BR.md b/README_pt_BR.md
new file mode 100644
index 00000000..ce5b453a
--- /dev/null
+++ README_pt_BR.md
@@ -0,0 +1,841 @@
+<p align="center">
+  <img alt="npm formidable package logo" src="https://raw.githubusercontent.com/node-formidable/formidable/master/logo.png" />
+</p>
+
+# formidable [![npm version][npmv-img]][npmv-url] [![MIT license][license-img]][license-url] [![Libera Manifesto][libera-manifesto-img]][libera-manifesto-url] [![Twitter][twitter-img]][twitter-url]
+
+> A Node.js module for parsing form data, especially file uploads.
+
+[![Code style][codestyle-img]][codestyle-url]
+[![codecoverage][codecov-img]][codecov-url]
+[![linux build status][linux-build-img]][build-url]
+[![windows build status][windows-build-img]][build-url]
+[![macos build status][macos-build-img]][build-url]
+
+Se você tiver qualquer tipo de pergunta sobre _como_ fazer, por favor leia o [Contributing
+Guia][contributing-url] e [Código de Conduta][code_of_conduct-url]
+documentos.<br /> Para relatórios de bugs e solicitações de recursos, [crie uma
+issue][open-issue-url] ou ping [@tunnckoCore / @3a1FcBx0](https://twitter.com/3a1FcBx0)
+no Twitter.
+
+[![Conventional Commits][ccommits-img]][ccommits-url]
+[![Minimum Required Nodejs][nodejs-img]][npmv-url]
+[![Tidelift Subscription][tidelift-img]][tidelift-url]
+[![Buy me a Kofi][kofi-img]][kofi-url]
+[![Renovate App Status][renovateapp-img]][renovateapp-url]
+[![Make A Pull Request][prs-welcome-img]][prs-welcome-url]
+
+Este projeto é [semanticamente versionado](https://semver.org) e está disponível como
+parte da [Assinatura Tidelift][tidelift-url] para nível profissional
+garantias, suporte aprimorado e segurança.
+[Saiba mais.](https://tidelift.com/subscription/pkg/npm-formidable?utm_source=npm-formidable&utm_medium=referral&utm_campaign=enterprise)
+
+_Os mantenedores do `formidable` e milhares de outros pacotes estão trabalhando
+com Tidelift para fornecer suporte comercial e manutenção para o Open Source
+dependências que você usa para construir seus aplicativos. Economize tempo, reduza riscos e
+melhorar a integridade do código, enquanto paga aos mantenedores das dependências exatas que você
+usar._
+
+[![][npm-weekly-img]][npmv-url] [![][npm-monthly-img]][npmv-url]
+[![][npm-yearly-img]][npmv-url] [![][npm-alltime-img]][npmv-url]
+
+## Status do Projeto: Mantido
+
+_Verifique [VERSION NOTES](https://github.com/node-formidable/formidable/blob/master/VERSION_NOTES.md) para obter mais informações sobre os planos v1, v2 e v3, NPM dist-tags e branches._
+
+Este módulo foi inicialmente desenvolvido por
+[**@felixge**](https://github.com/felixge) para
+[Transloadit](http://transloadit.com/), um serviço focado em upload e
+codificação de imagens e vídeos. Foi testado em batalha contra centenas de GBs de
+uploads de arquivos de uma grande variedade de clientes e é considerado pronto para produção
+e é usado na produção por anos.
+
+Atualmente, somos poucos mantenedores tentando lidar com isso. :) Mais contribuidores
+são sempre bem-vindos! ❤️ Pule
+[issue #412](https://github.com/felixge/node-formidable/issues/412) que está
+fechado, mas se você estiver interessado, podemos discuti-lo e adicioná-lo após regras estritas, como ativar o Two-Factor Auth em suas contas npm e GitHub.
+
+## Destaques
+
+- [Rápido (~ 900-2500 mb/seg)](#benchmarks) e analisador multiparte de streaming
+- Gravar uploads de arquivos automaticamente no disco (opcional, consulte
+   [`options.fileWriteStreamHandler`](#options))
+- [API de plug-ins](#useplugin-plugin) - permitindo analisadores e plug-ins personalizados
+- Baixo consumo de memória
+- Tratamento de erros gracioso
+- Cobertura de teste muito alta
+
+## Instalar
+
+Este projeto requer `Node.js >= 10.13`. Instale-o usando
+[yarn](https://yarnpkg.com) ou [npm](https://npmjs.com).<br /> _Nós altamente
+recomendamos usar o Yarn quando pensar em contribuir para este projeto._
+
+Este é um pacote de baixo nível e, se você estiver usando uma estrutura de alto nível, _pode_ já estar incluído. Verifique os exemplos 
+abaixo e a pasta [examples/](https://github.com/node-formidable/formidable/tree/master/examples).
+
+```
+# v2
+npm install formidable
+npm install formidable@v2
+
+# v3
+npm install formidable@v3
+```
+
+_**Nota:** Em um futuro próximo, a v3 será publicada na dist-tag `latest` do NPM. 
+Versões futuras não prontas serão publicadas nas dist-tags `*-next` para a versão correspondente._
+
+
+## Exemplos
+
+Para mais exemplos veja o diretório `examples/`.
+
+### com módulo http Node.js
+
+Analisar um upload de arquivo de entrada, com o
+[Módulo `http` integrado do Node.js](https://nodejs.org/api/http.html).
+
+```js
+import http from 'node:http';
+import formidable, {errors as formidableErrors} from 'formidable';
+
+const server = http.createServer((req, res) => {
+  if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
+    // analisar um upload de arquivo
+    const form = formidable({});
+
+    form.parse(req, (err, fields, files) => {
+      if (err) {
+        // exemplo para verificar um erro muito específico
+        if (err.code === formidableErrors.maxFieldsExceeded) {
+
+        }
+        res.writeHead(err.httpCode || 400, { 'Content-Type': 'text/plain' });
+        res.end(String(err));
+        return;
+      }
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify({ fields, files }, null, 2));
+    });
+
+    return;
+  }
+
+  // mostrar um formulário de upload de arquivo
+  res.writeHead(200, { 'Content-Type': 'text/html' });
+  res.end(`
+    <h2>With Node.js <code>"http"</code> module</h2>
+    <form action="/api/upload" enctype="multipart/form-data" method="post">
+      <div>Text field title: <input type="text" name="title" /></div>
+      <div>File: <input type="file" name="multipleFiles" multiple="multiple" /></div>
+      <input type="submit" value="Upload" />
+    </form>
+  `);
+});
+
+server.listen(8080, () => {
+  console.log('Server listening on http://localhost:8080/ ...');
+});
+```
+
+### com Express.js
+
+Existem várias variantes para fazer isso, mas o Formidable só precisa do Node.js Request
+stream, então algo como o exemplo a seguir deve funcionar bem, sem nenhum middleware [Express.js](https://ghub.now.sh/express) de terceiros.
+
+Ou tente o
+[examples/with-express.js](https://github.com/node-formidable/formidable/blob/master/examples/with-express.js)
+
+```js
+import express from 'express';
+import formidable from 'formidable';
+
+const app = express();
+
+app.get('/', (req, res) => {
+  res.send(`
+    <h2>With <code>"express"</code> npm package</h2>
+    <form action="/api/upload" enctype="multipart/form-data" method="post">
+      <div>Text field title: <input type="text" name="title" /></div>
+      <div>File: <input type="file" name="someExpressFiles" multiple="multiple" /></div>
+      <input type="submit" value="Upload" />
+    </form>
+  `);
+});
+
+app.post('/api/upload', (req, res, next) => {
+  const form = formidable({});
+
+  form.parse(req, (err, fields, files) => {
+    if (err) {
+      next(err);
+      return;
+    }
+    res.json({ fields, files });
+  });
+});
+
+app.listen(3000, () => {
+  console.log('Server listening on http://localhost:3000 ...');
+});
+```
+
+### com Koa e Formidable
+
+Claro, com [Koa v1, v2 ou future v3](https://ghub.now.sh/koa) as coisas
+sao muito parecidas. Você pode usar `formidable` manualmente como mostrado abaixo ou através
+do pacote [koa-better-body](https://ghub.now.sh/koa-better-body) que é
+usando `formidable` sob o capô e suporte a mais recursos e diferentes
+corpos de solicitação, verifique sua documentação para mais informações.
+
+_Nota: este exemplo está assumindo Koa v2. Esteja ciente de que você deve passar `ctx.req`
+que é a solicitação do Node.js e **NÃO** o `ctx.request` que é a solicitação do Koa
+objeto - há uma diferença._
+
+```js
+import Koa from 'Koa';
+import formidable from 'formidable';
+
+const app = new Koa();
+
+app.on('error', (err) => {
+  console.error('server error', err);
+});
+
+app.use(async (ctx, next) => {
+  if (ctx.url === '/api/upload' && ctx.method.toLowerCase() === 'post') {
+    const form = formidable({});
+    
+     // não muito elegante, mas é por enquanto se você não quiser usar `koa-better-body`
+     // ou outros middlewares.
+    await new Promise((resolve, reject) => {
+      form.parse(ctx.req, (err, fields, files) => {
+        if (err) {
+          reject(err);
+          return;
+        }
+
+        ctx.set('Content-Type', 'application/json');
+        ctx.status = 200;
+        ctx.state = { fields, files };
+        ctx.body = JSON.stringify(ctx.state, null, 2);
+        resolve();
+      });
+    });
+    await next();
+    return;
+  }
+
+  // mostrar um formulário de upload de arquivo
+  ctx.set('Content-Type', 'text/html');
+  ctx.status = 200;
+  ctx.body = `
+    <h2>With <code>"koa"</code> npm package</h2>
+    <form action="/api/upload" enctype="multipart/form-data" method="post">
+    <div>Text field title: <input type="text" name="title" /></div>
+    <div>File: <input type="file" name="koaFiles" multiple="multiple" /></div>
+    <input type="submit" value="Upload" />
+    </form>
+  `;
+});
+
+app.use((ctx) => {
+  console.log('The next middleware is called');
+  console.log('Results:', ctx.state);
+});
+
+app.listen(3000, () => {
+  console.log('Server listening on http://localhost:3000 ...');
+});
+```
+
+## Benchmarks
+
+O benchmark é bastante antigo, da antiga base de código. Mas talvez seja bem verdade.
+Anteriormente, os números giravam em torno de ~ 500 mb/s. Atualmente com a mudança para o novo
+Node.js Streams API, é mais rápido. Você pode ver claramente as diferenças entre as
+versões do Node.
+
+_Observação: um benchmarking muito melhor pode e deve ser feito no futuro._
+
+Benchmark realizado em 8 GB de RAM, Xeon X3440 (2,53 GHz, 4 núcleos, 8 threads)
+
+```
+~/github/node-formidable master
+❯ nve --parallel 8 10 12 13 node benchmark/bench-multipart-parser.js
+
+ ⬢  Node 8
+
+1261.08 mb/sec
+
+ ⬢  Node 10
+
+1113.04 mb/sec
+
+ ⬢  Node 12
+
+2107.00 mb/sec
+
+ ⬢  Node 13
+
+2566.42 mb/sec
+```
+
+![benchmark 29 de janeiro de 2020](./benchmark/2020-01-29_xeon-x3440.png)
+
+## API
+
+### Formidable / IncomingForm
+
+Todos os mostrados são equivalentes.
+
+_Por favor, passe [`options`](#options) para a função/construtor, não atribuindo
+eles para a instância `form`_
+
+```js
+import formidable from 'formidable';
+const form = formidable(options);
+```
+
+### Opções
+
+Veja seus padrões em [src/Formidable.js DEFAULT_OPTIONS](./src/Formidable.js)
+(a constante `DEFAULT_OPTIONS`).
+
+- `options.encoding` **{string}** - padrão `'utf-8'`; define a codificação para campos de formulário de entrada,
+- `options.uploadDir` **{string}** - padrão `os.tmpdir()`; o diretório para colocar os uploads de arquivos. Você pode movê-los mais tarde usando `fs.rename()`.
+- `options.keepExtensions` **{boolean}** - padrão `false`; incluir as extensões dos arquivos originais ou não
+- `options.allowEmptyFiles` **{boolean}** - padrão `false`; permitir upload de arquivos vazios
+- `options.minFileSize` **{number}** - padrão `1` (1byte); o tamanho mínimo do arquivo carregado.
+- `options.maxFiles` **{number}** - padrão `Infinity`;
+  limitar a quantidade de arquivos carregados, defina Infinity para ilimitado
+- `options.maxFileSize` **{number}** - padrão `200 * 1024 * 1024` (200mb);
+  limitar o tamanho de cada arquivo carregado.
+- `options.maxTotalFileSize` **{number}** - padrão `options.maxFileSize`;
+  limitar o tamanho do lote de arquivos carregados.
+- `options.maxFields` **{number}** - padrão `1000`; limite o número de campos, defina Infinity para ilimitado
+- `options.maxFieldsSize` **{number}** - padrão `20 * 1024 * 1024` (20mb);
+  limitar a quantidade de memória que todos os campos juntos (exceto arquivos) podem alocar em
+  bytes.
+- `options.hashAlgorithm` **{string | false}** - padrão `false`; incluir checksums calculados
+  para arquivos recebidos, defina isso para algum algoritmo de hash, consulte
+  [crypto.createHash](https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm_options)
+  para algoritmos disponíveis
+- `options.fileWriteStreamHandler` **{function}** - padrão `null`, que por padrão grava no sistema de arquivos da máquina host cada arquivo analisado; A função
+  deve retornar uma instância de um
+  [fluxo gravável](https://nodejs.org/api/stream.html#stream_class_stream_writable)
+  que receberá os dados do arquivo carregado. Com esta opção, você pode ter qualquer
+  comportamento personalizado em relação a onde os dados do arquivo carregado serão transmitidos.
+  Se você deseja gravar o arquivo carregado em outros tipos de armazenamento em nuvem
+  (AWS S3, armazenamento de blob do Azure, armazenamento em nuvem do Google) ou armazenamento de arquivo privado,
+  esta é a opção que você está procurando. Quando esta opção é definida, o comportamento padrão de gravar o arquivo no sistema de arquivos da máquina host é perdido.
+- `options.filename` **{function}** - padrão `undefined` Use-o para controlar newFilename. Deve retornar uma string. Será associado a options.uploadDir.
+
+- `options.filter` **{function}** - função padrão que sempre retorna verdadeiro.
+  Use-o para filtrar arquivos antes de serem carregados. Deve retornar um booleano.
+
+
+#### `options.filename`  **{function}** function (name, ext, part, form) -> string
+
+onde a parte pode ser decomposta como
+
+```js
+const { originalFilename, mimetype} = part;
+```
+
+_**Observação:** Se este tamanho de campos combinados, ou tamanho de algum arquivo for excedido, um
+O evento `'error'` é disparado._
+
+```js
+// A quantidade de bytes recebidos para este formulário até agora.
+form.bytesReceived;
+```
+
+```js
+// O número esperado de bytes neste formulário.
+form.bytesExpected;
+```
+
+#### `options.filter`  **{function}** function ({name, originalFilename, mimetype}) -> boolean
+
+**Observação:** use uma variável externa para cancelar todos os uploads no primeiro erro 
+
+```js
+const options = {
+  filter: function ({name, originalFilename, mimetype}) {
+    // manter apenas imagens
+    return mimetype && mimetype.includes("image");
+  }
+};
+```
+
+
+### .parse(request, callback)
+
+Analisa uma `request` do Node.js recebida contendo dados de formulário. Se `callback` for
+fornecido, todos os campos e arquivos são coletados e passados para o retorno de chamada.
+
+```js
+const form = formidable({ uploadDir: __dirname });
+
+form.parse(req, (err, fields, files) => {
+  console.log('fields:', fields);
+  console.log('files:', files);
+});
+```
+
+Você pode substituir esse método se estiver interessado em acessar diretamente o 
+fluxo de várias partes. Fazer isso desativará qualquer processamento de eventos `'field'` / `'file'` 
+que ocorreria de outra forma, tornando você totalmente responsável por lidar com o processamento.
+
+Sobre `uploadDir`, dada a seguinte estrutura de diretório
+```
+project-name
+├── src
+│   └── server.js
+│       
+└── uploads
+    └── image.jpg
+```
+
+`__dirname` seria o mesmo diretório que o próprio arquivo de origem (src)
+
+
+```js
+ `${__dirname}/../uploads`
+```
+
+para colocar arquivos em uploads.
+
+Omitir `__dirname` tornaria o caminho relativo ao diretório de trabalho atual. Isso seria o mesmo se server.js fosse iniciado a partir de src, mas não de project-name.
+
+
+`null` usará o padrão que é `os.tmpdir()`
+
+Nota: Se o diretório não existir, os arquivos carregados são __silenciosamente descartados__. Para ter certeza de que existe:
+
+```js
+import {createNecessaryDirectoriesSync} from "filesac";
+
+
+const uploadPath = `${__dirname}/../uploads`;
+createNecessaryDirectoriesSync(`${uploadPath}/x`);
+```
+
+
+No exemplo abaixo, escutamos alguns eventos e os direcionamos para o ouvinte `data`, para 
+que você possa fazer o que quiser lá, com base em se é antes do arquivo ser emitido, o valor do
+cabeçalho, o nome do cabeçalho, no campo , em arquivo e etc.
+
+Ou a outra maneira poderia ser apenas substituir o `form.onPart` como é mostrado um pouco
+mais tarde.
+
+```js
+form.once('error', console.error);
+
+form.on('fileBegin', (formname, file) => {
+  form.emit('data', { name: 'fileBegin', formname, value: file });
+});
+
+form.on('file', (formname, file) => {
+  form.emit('data', { name: 'file', formname, value: file });
+});
+
+form.on('field', (fieldName, fieldValue) => {
+  form.emit('data', { name: 'field', key: fieldName, value: fieldValue });
+});
+
+form.once('end', () => {
+  console.log('Done!');
+});
+
+// Se você quiser personalizar o que quiser...
+form.on('data', ({ name, key, value, buffer, start, end, formname, ...more }) => {
+  if (name === 'partBegin') {
+  }
+  if (name === 'partData') {
+  }
+  if (name === 'headerField') {
+  }
+  if (name === 'headerValue') {
+  }
+  if (name === 'headerEnd') {
+  }
+  if (name === 'headersEnd') {
+  }
+  if (name === 'field') {
+    console.log('field name:', key);
+    console.log('field value:', value);
+  }
+  if (name === 'file') {
+    console.log('file:', formname, value);
+  }
+  if (name === 'fileBegin') {
+    console.log('fileBegin:', formname, value);
+  }
+});
+```
+
+### .use(plugin: Plugin)
+
+Um método que permite estender a biblioteca Formidable. Por padrão, incluímos
+4 plug-ins, que são essencialmente adaptadores para conectar os diferentes analisadores integrados.
+
+**Os plugins adicionados por este método estão sempre ativados.**
+
+_Consulte [src/plugins/](./src/plugins/) para uma visão mais detalhada dos plug-ins padrão._
+
+O parâmetro `plugin` tem essa assinatura:
+
+```typescript
+function(formidable: Formidable, options: Options): void;
+```
+
+A arquitetura é simples. O `plugin` é uma função que é passada com a instância Formidable (o `form` nos exemplos README) e as opções.
+
+**Observação:** o contexto `this` da função do plug-in também é a mesma instância.
+
+```js
+const form = formidable({ keepExtensions: true });
+
+form.use((self, options) => {
+  // self === this === form
+  console.log('woohoo, custom plugin');
+  // faça suas coisas; verifique `src/plugins` para inspiração
+});
+
+form.parse(req, (error, fields, files) => {
+  console.log('done!');
+});
+```
+**Importante observar**, é que dentro do plugin `this.options`, `self.options` e
+`options` PODEM ou NÃO ser iguais. A melhor prática geral é sempre usar o
+`this`, para que você possa testar seu plugin mais tarde de forma independente e mais fácil.
+
+Se você quiser desabilitar alguns recursos de análise do Formidable, você pode desabilitar
+o plugin que corresponde ao analisador. Por exemplo, se você deseja desabilitar a análise de 
+várias partes (para que o [src/parsers/Multipart.js](./src/parsers/Multipart.js)
+que é usado em [src/plugins/multipart.js](./src/plugins/multipart.js)), então
+você pode removê-lo do `options.enabledPlugins`, assim
+
+```js
+import formidable, {octetstream, querystring, json} from "formidable";
+const form = formidable({
+  hashAlgorithm: 'sha1',
+  enabledPlugins: [octetstream, querystring, json],
+});
+```
+
+**Esteja ciente** de que a ordem _PODE_ ser importante também. Os nomes correspondem 1:1 a
+arquivos na pasta [src/plugins/](./src/plugins).
+
+Solicitações pull para novos plug-ins integrados PODEM ser aceitas - por exemplo, analisador de 
+querystring mais avançado. Adicione seu plugin como um novo arquivo na pasta `src/plugins/` (em letras minúsculas) e 
+siga como os outros plugins são feitos.
+
+### form.onPart
+
+Se você quiser usar Formidable para manipular apenas algumas partes para você, você pode fazer
+alguma coisa similar. ou ver
+[#387](https://github.com/node-formidable/node-formidable/issues/387) para
+inspiração, você pode, por exemplo, validar o tipo mime.
+
+```js
+const form = formidable();
+
+form.onPart = (part) => {
+  part.on('data', (buffer) => {
+    // faça o que quiser aqui
+  });
+};
+```
+
+Por exemplo, force Formidable a ser usado apenas em "partes" que não sejam de arquivo (ou seja, html
+Campos)
+
+```js
+const form = formidable();
+
+form.onPart = function (part) {
+  // deixe formidável lidar apenas com partes não arquivadas
+  if (part.originalFilename === '' || !part.mimetype) {
+    // usado internamente, por favor, não substitua!
+    form._handlePart(part);
+  }
+};
+```
+
+### Arquivo
+
+```ts
+export interface File {
+   // O tamanho do arquivo enviado em bytes.
+   // Se o arquivo ainda estiver sendo carregado (veja o evento `'fileBegin'`),
+   // esta propriedade diz quantos bytes do arquivo já foram gravados no disco.
+  file.size: number;
+
+   // O caminho em que este arquivo está sendo gravado. Você pode modificar isso no evento `'fileBegin'`
+   // caso você esteja insatisfeito com a forma como o formidable gera um caminho temporário para seus arquivos.
+  file.filepath: string;
+
+  // O nome que este arquivo tinha de acordo com o cliente de upload.
+  file.originalFilename: string | null;
+  
+  // calculado com base nas opções fornecidas.
+  file.newFilename: string | null;
+
+  // O tipo mime deste arquivo, de acordo com o cliente de upload.
+  file.mimetype: string | null;
+
+  // Um objeto Date (ou `null`) contendo a hora em que este arquivo foi gravado pela última vez.
+  // Principalmente aqui para compatibilidade com o [W3C File API Draft](http://dev.w3.org/2006/webapi/FileAPI/).
+  file.mtime: Date | null;
+
+  file.hashAlgorithm: false | |'sha1' | 'md5' | 'sha256'
+  // Se o cálculo `options.hashAlgorithm` foi definido, você pode ler o resumo hexadecimal desta var (no final, será uma string)
+  file.hash: string | object | null;
+}
+```
+
+#### file.toJSON()
+
+Este método retorna uma representação JSON do arquivo, permitindo que você `JSON.stringify()` 
+o arquivo que é útil para registrar e responder a solicitações.
+
+### Eventos
+
+#### `'progress'`
+Emitido após cada bloco de entrada de dados que foi analisado. Pode ser usado para rolar sua própria barra de progresso. **Aviso** Use isso
+apenas para a barra de progresso do lado do servidor. No lado do cliente, é melhor usar `XMLHttpRequest` com `xhr.upload.onprogress =`
+
+```js
+form.on('progress', (bytesReceived, bytesExpected) => {});
+```
+
+#### `'field'`
+
+Emitido sempre que um par campo/valor é recebido.
+
+```js
+form.on('field', (name, value) => {});
+```
+
+#### `'fileBegin'`
+
+Emitido sempre que um novo arquivo é detectado no fluxo de upload. 
+Use este evento se desejar transmitir o arquivo para outro lugar enquanto armazena o upload no sistema de arquivos.
+
+```js
+form.on('fileBegin', (formName, file) => {
+     // acessível aqui
+     // formName o nome no formulário (<input name="thisname" type="file">) ou http filename para octetstream
+     // file.originalFilename http filename ou null se houver um erro de análise
+     // file.newFilename gerou hexoid ou o que options.filename retornou
+     // file.filepath nome do caminho padrão de acordo com options.uploadDir e options.filename
+     // file.filepath = CUSTOM_PATH // para alterar o caminho final
+});
+```
+
+#### `'file'`
+
+Emitido sempre que um par campo/arquivo é recebido. `file` é uma instância de
+`File`.
+
+```js
+form.on('file', (formname, file) => {
+     // o mesmo que fileBegin, exceto
+     // é muito tarde para alterar file.filepath
+     // file.hash está disponível se options.hash foi usado
+});
+```
+
+#### `'error'`
+
+Emitido quando há um erro no processamento do formulário recebido. Uma solicitação que 
+apresenta um erro é pausada automaticamente, você terá que chamar manualmente
+`request.resume()` se você quiser que a requisição continue disparando eventos `'data'`.
+
+Pode ter `error.httpCode` e `error.code` anexados.
+
+```js
+form.on('error', (err) => {});
+```
+
+#### `'aborted'`
+
+Emitido quando a requisição foi abortada pelo usuário. Agora isso pode ser devido a um
+evento 'timeout' ou 'close' no soquete. Após este evento ser emitido, um
+O evento `error` seguirá. No futuro, haverá um 'timeout' separado
+evento (precisa de uma mudança no núcleo do nó).
+
+```js
+form.on('aborted', () => {});
+```
+
+#### `'end'`
+
+Emitido quando toda a solicitação foi recebida e todos os arquivos contidos foram 
+liberados para o disco. Este é um ótimo lugar para você enviar sua resposta.
+
+```js
+form.on('end', () => {});
+```
+
+
+### Helpers
+
+#### firstValues
+
+Obtém os primeiros valores dos campos, como pré 3.0.0 sem passar múltiplos em uma
+lista de exceções opcionais onde arrays de strings ainda são desejados (`<select multiple>` por exemplo)
+
+```js
+import { firstValues } from 'formidable/src/helpers/firstValues.js';
+
+// ...
+form.parse(request, async (error, fieldsMultiple, files) => {
+    if (error) {
+        //...
+    }
+    const exceptions = ['thisshouldbeanarray'];
+    const fieldsSingle = firstValues(form, fieldsMultiple, exceptions);
+    // ...
+```
+
+#### readBooleans
+
+Html form input type="checkbox" envia apenas o valor "on" se marcado,
+converta-o em booleanos para cada entrada que deve ser enviada como uma caixa de seleção, use somente após a chamada de firstValues ou similar.
+
+```js
+import { firstValues } from 'formidable/src/helpers/firstValues.js';
+import { readBooleans } from 'formidable/src/helpers/readBooleans.js';
+
+// ...
+form.parse(request, async (error, fieldsMultiple, files) => {
+    if (error) {
+        //...
+    }
+    const fieldsSingle = firstValues(form, fieldsMultiple);
+    
+    const expectedBooleans = ['checkbox1', 'wantsNewsLetter', 'hasACar'];
+    const fieldsWithBooleans = readBooleans(fieldsSingle, expectedBooleans);
+    // ...
+```
+
+## Changelog
+
+[./CHANGELOG.md](./CHANGELOG.md)
+
+## Ports & Créditos
+
+- [multipart-parser](http://github.com/FooBarWidget/multipart-parser): um analisador C++ baseado em formidável
+- [Ryan Dahl](http://twitter.com/ryah) por seu trabalho em
+  [http-parser](http://github.com/ry/http-parser) que inspirou fortemente o `multipart_parser.js` inicial.
+
+## Contribuindo
+
+Se a documentação não estiver clara ou tiver um erro de digitação, clique no botão `Edit` da página (ícone de lápis) e sugira uma correção. 
+Se você gostaria de nos ajudar a corrigir
+um bug ou adicionar um novo recurso, verifique nosso [Contributing
+Guide][contribuindo-url]. Pull requests são bem-vindos!
+
+Agradecimentos vão para essas pessoas maravilhosas
+([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+
+<!-- ALL-CONTRIBUTORS-LIST:START -->
+<!-- prettier-ignore-start -->
+<!-- markdownlint-disable -->
+<table>
+  <tr>
+    <td align="center"><a href="https://twitter.com/felixge"><img src="https://avatars3.githubusercontent.com/u/15000?s=460&v=4" width="100px;" alt=""/><br /><sub><b>Felix Geisendörfer</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/commits?author=felixge" title="Code">💻</a> <a href="#design-felixge" title="Design">🎨</a> <a href="#ideas-felixge" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=felixge" title="Documentation">📖</a></td>
+    <td align="center"><a href="https://tunnckoCore.com"><img src="https://avatars3.githubusercontent.com/u/5038030?v=4" width="100px;" alt=""/><br /><sub><b>Charlike Mike Reagent</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/issues?q=author%3AtunnckoCore" title="Bug reports">🐛</a> <a href="#infra-tunnckoCore" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#design-tunnckoCore" title="Design">🎨</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=tunnckoCore" title="Code">💻</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=tunnckoCore" title="Documentation">📖</a> <a href="#example-tunnckoCore" title="Examples">💡</a> <a href="#ideas-tunnckoCore" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-tunnckoCore" title="Maintenance">🚧</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=tunnckoCore" title="Tests">⚠️</a></td>
+    <td align="center"><a href="https://github.com/kedarv"><img src="https://avatars1.githubusercontent.com/u/1365665?v=4" width="100px;" alt=""/><br /><sub><b>Kedar</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/commits?author=kedarv" title="Code">💻</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=kedarv" title="Tests">⚠️</a> <a href="#question-kedarv" title="Answering Questions">💬</a> <a href="https://github.com/node-formidable/node-formidable/issues?q=author%3Akedarv" title="Bug reports">🐛</a></td>
+    <td align="center"><a href="https://github.com/GrosSacASac"><img src="https://avatars0.githubusercontent.com/u/5721194?v=4" width="100px;" alt=""/><br /><sub><b>Walle Cyril</b></sub></a><br /><a href="#question-GrosSacASac" title="Answering Questions">💬</a> <a href="https://github.com/node-formidable/node-formidable/issues?q=author%3AGrosSacASac" title="Bug reports">🐛</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=GrosSacASac" title="Code">💻</a> <a href="#financial-GrosSacASac" title="Financial">💵</a> <a href="#ideas-GrosSacASac" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-GrosSacASac" title="Maintenance">🚧</a></td>
+    <td align="center"><a href="https://github.com/xarguments"><img src="https://avatars2.githubusercontent.com/u/40522463?v=4" width="100px;" alt=""/><br /><sub><b>Xargs</b></sub></a><br /><a href="#question-xarguments" title="Answering Questions">💬</a> <a href="https://github.com/node-formidable/node-formidable/issues?q=author%3Axarguments" title="Bug reports">🐛</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=xarguments" title="Code">💻</a> <a href="#maintenance-xarguments" title="Maintenance">🚧</a></td>
+    <td align="center"><a href="https://github.com/Amit-A"><img src="https://avatars1.githubusercontent.com/u/7987238?v=4" width="100px;" alt=""/><br /><sub><b>Amit-A</b></sub></a><br /><a href="#question-Amit-A" title="Answering Questions">💬</a> <a href="https://github.com/node-formidable/node-formidable/issues?q=author%3AAmit-A" title="Bug reports">🐛</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=Amit-A" title="Code">💻</a></td>
+  </tr>
+  <tr>
+    <td align="center"><a href="https://charmander.me/"><img src="https://avatars1.githubusercontent.com/u/1889843?v=4" width="100px;" alt=""/><br /><sub><b>Charmander</b></sub></a><br /><a href="#question-charmander" title="Answering Questions">💬</a> <a href="https://github.com/node-formidable/node-formidable/issues?q=author%3Acharmander" title="Bug reports">🐛</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=charmander" title="Code">💻</a> <a href="#ideas-charmander" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-charmander" title="Maintenance">🚧</a></td>
+    <td align="center"><a href="https://twitter.com/dylan_piercey"><img src="https://avatars2.githubusercontent.com/u/4985201?v=4" width="100px;" alt=""/><br /><sub><b>Dylan Piercey</b></sub></a><br /><a href="#ideas-DylanPiercey" title="Ideas, Planning, & Feedback">🤔</a></td>
+    <td align="center"><a href="http://ochrona.jawne.info.pl"><img src="https://avatars1.githubusercontent.com/u/3618479?v=4" width="100px;" alt=""/><br /><sub><b>Adam Dobrawy</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/issues?q=author%3Aad-m" title="Bug reports">🐛</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=ad-m" title="Documentation">📖</a></td>
+    <td align="center"><a href="https://github.com/amitrohatgi"><img src="https://avatars3.githubusercontent.com/u/12177021?v=4" width="100px;" alt=""/><br /><sub><b>amitrohatgi</b></sub></a><br /><a href="#ideas-amitrohatgi" title="Ideas, Planning, & Feedback">🤔</a></td>
+    <td align="center"><a href="https://github.com/fengxinming"><img src="https://avatars2.githubusercontent.com/u/6262382?v=4" width="100px;" alt=""/><br /><sub><b>Jesse Feng</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/issues?q=author%3Afengxinming" title="Bug reports">🐛</a></td>
+    <td align="center"><a href="https://qtmsheep.com"><img src="https://avatars1.githubusercontent.com/u/7271496?v=4" width="100px;" alt=""/><br /><sub><b>Nathanael Demacon</b></sub></a><br /><a href="#question-quantumsheep" title="Answering Questions">💬</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=quantumsheep" title="Code">💻</a> <a href="https://github.com/node-formidable/node-formidable/pulls?q=is%3Apr+reviewed-by%3Aquantumsheep" title="Reviewed Pull Requests">👀</a></td>
+  </tr>
+  <tr>
+    <td align="center"><a href="https://github.com/MunMunMiao"><img src="https://avatars1.githubusercontent.com/u/18216142?v=4" width="100px;" alt=""/><br /><sub><b>MunMunMiao</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/issues?q=author%3AMunMunMiao" title="Bug reports">🐛</a></td>
+    <td align="center"><a href="https://github.com/gabipetrovay"><img src="https://avatars0.githubusercontent.com/u/1170398?v=4" width="100px;" alt=""/><br /><sub><b>Gabriel Petrovay</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/issues?q=author%3Agabipetrovay" title="Bug reports">🐛</a> <a href="https://github.com/node-formidable/node-formidable/commits?author=gabipetrovay" title="Code">💻</a></td>
+    <td align="center"><a href="https://github.com/Elzair"><img src="https://avatars0.githubusercontent.com/u/2352818?v=4" width="100px;" alt=""/><br /><sub><b>Philip Woods</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/commits?author=Elzair" title="Code">💻</a> <a href="#ideas-Elzair" title="Ideas, Planning, & Feedback">🤔</a></td>
+    <td align="center"><a href="https://github.com/dmolim"><img src="https://avatars2.githubusercontent.com/u/7090374?v=4" width="100px;" alt=""/><br /><sub><b>Dmitry Ivonin</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/commits?author=dmolim" title="Documentation">📖</a></td>
+    <td align="center"><a href="https://audiobox.fm"><img src="https://avatars1.githubusercontent.com/u/12844?v=4" width="100px;" alt=""/><br /><sub><b>Claudio Poli</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/commits?author=masterkain" title="Code">💻</a></td>
+  </tr>
+</table>
+
+<!-- markdownlint-enable -->
+<!-- prettier-ignore-end -->
+
+<!-- ALL-CONTRIBUTORS-LIST:END -->
+
+De uma [postagem do blog Felix](https://felixge.de/2013/03/11/the-pull-request-hack/):
+
+- [Sven Lito](https://github.com/svnlto) por corrigir bugs e mesclar patches
+- [egirshov](https://github.com/egirshov) por contribuir com muitas melhorias para o analisador multipartes formidável de nós
+- [Andrew Kelley](https://github.com/superjoe30) por também ajudar a corrigir bugs e fazer melhorias
+- [Mike Frey](https://github.com/mikefrey) por contribuir com suporte JSON
+
+## Licença
+
+Formidable é licenciado sob a [MIT License][license-url].
+
+<!-- badges -->
+<!-- prettier-ignore-start -->
+
+[codestyle-url]: https://github.com/airbnb/javascript
+[codestyle-img]: https://badgen.net/badge/code%20style/airbnb%20%2B%20prettier/ff5a5f?icon=airbnb&cache=300
+[codecov-url]: https://codecov.io/gh/node-formidable/formidable
+[codecov-img]: https://badgen.net/codecov/c/github/node-formidable/formidable/master?icon=codecov
+[npmv-canary-img]: https://badgen.net/npm/v/formidable/canary?icon=npm
+[npmv-dev-img]: https://badgen.net/npm/v/formidable/dev?icon=npm
+[npmv-img]: https://badgen.net/npm/v/formidable?icon=npm
+[npmv-url]: https://npmjs.com/package/formidable
+[license-img]: https://badgen.net/npm/license/formidable
+[license-url]: https://github.com/node-formidable/formidable/blob/master/LICENSE
+[chat-img]: https://badgen.net/badge/chat/on%20gitter/46BC99?icon=gitter
+[chat-url]: https://gitter.im/node-formidable/Lobby
+[libera-manifesto-url]: https://liberamanifesto.com
+[libera-manifesto-img]: https://badgen.net/badge/libera/manifesto/grey
+[renovateapp-url]: https://renovatebot.com
+[renovateapp-img]: https://badgen.net/badge/renovate/enabled/green?cache=300
+[prs-welcome-img]: https://badgen.net/badge/PRs/welcome/green?cache=300
+[prs-welcome-url]: http://makeapullrequest.com
+[twitter-url]: https://twitter.com/3a1fcBx0
+[twitter-img]: https://badgen.net/twitter/follow/3a1fcBx0?icon=twitter&color=1da1f2&cache=300
+
+[npm-weekly-img]: https://badgen.net/npm/dw/formidable?icon=npm&cache=300
+[npm-monthly-img]: https://badgen.net/npm/dm/formidable?icon=npm&cache=300
+[npm-yearly-img]: https://badgen.net/npm/dy/formidable?icon=npm&cache=300
+[npm-alltime-img]: https://badgen.net/npm/dt/formidable?icon=npm&cache=300&label=total%20downloads
+
+[nodejs-img]: https://badgen.net/badge/node/>=%2010.13/green?cache=300
+
+[ccommits-url]: https://conventionalcommits.org/
+[ccommits-img]: https://badgen.net/badge/conventional%20commits/v1.0.0/green?cache=300
+
+[contributing-url]: https://github.com/node-formidable/.github/blob/master/CONTRIBUTING.md
+[code_of_conduct-url]: https://github.com/node-formidable/.github/blob/master/CODE_OF_CONDUCT.md
+
+[open-issue-url]: https://github.com/node-formidable/formidable/issues/new
+
+[tidelift-url]: https://tidelift.com/subscription/pkg/npm-formidable?utm_source=npm-formidable&utm_medium=referral&utm_campaign=enterprise
+[tidelift-img]: https://badgen.net/badge/tidelift/subscription/4B5168?labelColor=F6914D
+
+[kofi-url]: https://ko-fi.com/tunnckoCore/commissions
+[kofi-img]: https://badgen.net/badge/ko-fi/support/29abe0c2?cache=300&icon=https://rawcdn.githack.com/tunnckoCore/badgen-icons/f8264c6414e0bec449dd86f2241d50a9b89a1203/icons/kofi.svg
+
+[linux-build-img]: https://badgen.net/github/checks/node-formidable/formidable/master/ubuntu?cache=300&label=linux%20build&icon=github
+[macos-build-img]: https://badgen.net/github/checks/node-formidable/formidable/master/macos?cache=300&label=macos%20build&icon=github
+[windows-build-img]: https://badgen.net/github/checks/node-formidable/formidable/master/windows?cache=300&label=windows%20build&icon=github
+[build-url]: https://github.com/node-formidable/formidable/actions?query=workflow%3Anodejs
+<!-- prettier-ignore-end -->
diff --git VERSION_NOTES.md VERSION_NOTES.md
index 43cf254b..3645f38e 100644
--- VERSION_NOTES.md
+++ VERSION_NOTES.md
@@ -14,22 +14,25 @@ We highly recommend to use `v2` or `v3`. Both are already in use by many, especi
- Please move to at least **v2**! 
- Try with installing `formidable@v2` and if still have the problem - report!

-## v2 is the new `latest`
-The `v2` will be simultaneously on two places for some time - `formidable@latest` and `formidable@v2`.
-The source code be available **only** on [v2 branch][v2branch].
-If you want to use v2, it's recommended to lock and use the v2 dist-tag `formidable@v2`. 
+## v2 is going to be deprecated
+
+The `v2` is available as `formidable@v2`.
+The source code is available **only** on [v2 branch][v2branch].
+If you want to use v2, it's recommended to lock and use the v2 dist-tag `formidable@v2-latest`. 

**Main Differences from v1:**
+
- Better organization and modernized code, requiring newer Node.js versions (>= v10).
- A lot of bugfixes, closed issues, merged or closed PRs.
- Better docs, new features (plugins, parsers, options) and optimizations.

## v3 - ESModules, Monorepo structure
-We recommend to use `formidable@v3` to install, as it uses more modern Node.js Streams, has support for more stuff.
+
+We recommend to use `formidable@latest` to install, as it uses more modern Node.js Streams, has support for more stuff.
You can see more info and track some ideas on [issue#635](https://github.com/node-formidable/formidable/issues/635).

-- The source code can be found on the [master branch](https://github.com/node-formidable/formidable) on GitHub.
-- It will be published on `formidable@latest` after some time.
+- The source code can be found on the [master branch][v3branch] on GitHub.
+- It is published as `formidable@latest`
- Dropping older Node.js versions, requiring higher than v12-v14.
- Dropping v1 compatibility.
- Rewritten to ESModules, more optimizations.
@@ -37,4 +40,4 @@ You can see more info and track some ideas on [issue#635](https://github.com/nod

[v1branch]: https://github.com/node-formidable/formidable/tree/v1-legacy
[v2branch]: https://github.com/node-formidable/formidable/tree/v2-latest
-[v3branch]: https://github.com/node-formidable/formidable/tree/v3
+[v3branch]: https://github.com/node-formidable/formidable/tree/master
diff --git a/benchmark/2022-11-30-i5-9600k.txt b/benchmark/2022-11-30-i5-9600k.txt
new file mode 100644
index 00000000..4d614444
--- /dev/null
+++ benchmark/2022-11-30-i5-9600k.txt
@@ -0,0 +1,30 @@
+npm run bench
+
+> formidable@3.2.5 bench
+> node benchmark
+
+4132.23 mb/sec
+PS C:\files\formidable> node -v      
+v18.0.0
+
+npm run bench
+
+> formidable@3.2.5 bench
+> node benchmark
+
+3952.57 mb/sec
+
+PS C:\files\formidable> node -v      
+v19.2.0
+
+C:\files\formidable> bombardier --body-file="./README.md" --method=POST 
+--duration=10s --connections=100 http://localhost:3000/api/upload 
+Bombarding http://localhost:3000/api/upload for 10s using 100 connection(s)[====================================================================] 10s 
+Done!
+Statistics        Avg      Stdev        Max
+  Reqs/sec      2824.09    1512.74    6881.85
+  Latency       35.51ms    37.38ms      0.98s
+  HTTP codes:
+    1xx - 0, 2xx - 28163, 3xx - 0, 4xx - 0, 5xx - 0
+    others - 0
+  Throughput:   102.08MB/s
diff --git a/benchmark/e2e.txt b/benchmark/e2e.txt
new file mode 100644
index 00000000..5802a0a0
--- /dev/null
+++ benchmark/e2e.txt
@@ -0,0 +1,6 @@
+node ./benchmark/server.js
+bombardier --body-file="./README.md" --method=POST --duration=10s --connections=100 http://localhost:3000/api/upload 
+
+
+
+
diff --git benchmark/index.js benchmark/index.js
index 136bf123..9dbb4c46 100644
--- benchmark/index.js
+++ benchmark/index.js
@@ -1,8 +1,6 @@
-'use strict';
+import assert from "node:assert";
+import MultipartParser from '../src/parsers/Multipart.js';

-const assert = require('assert');
-
-const MultipartParser = require('../src/parsers/Multipart');

const parser = new MultipartParser();
const customBoundary = '-----------------------------168072824752491622650073';
@@ -20,7 +18,7 @@ const calls = {
  end: 0,
};

-const start = Date.now();
+const start = performance.now();

parser.initWithBoundary(customBoundary);
parser.on('data', ({ name }) => {
@@ -29,7 +27,7 @@ parser.on('data', ({ name }) => {

parser.write(buf);

-const duration = Date.now() - start;
+const duration = performance.now() - start;
const mbPerSec = (mb / (duration / 1000)).toFixed(2);

console.log(`${mbPerSec} mb/sec`);
@@ -51,10 +49,10 @@ process.on('exit', () => {
  assert.deepStrictEqual(calls, {
    partBegin: 1,
    headerField: 1,
-    headerValue: 2,
+    headerValue: 1,
    headerEnd: 1,
    headersEnd: 1,
-    partData: 2,
+    partData: 1,
    partEnd: 1,
    end: 1,
  });
diff --git a/benchmark/server.js b/benchmark/server.js
new file mode 100644
index 00000000..f99f176b
--- /dev/null
+++ benchmark/server.js
@@ -0,0 +1,40 @@
+// inital copy of with-http.js
+// made a copy so that examples can be changed without impacting tests
+import http from 'node:http';
+import slugify from '@sindresorhus/slugify';
+import formidable, {errors as formidableErrors} from '../src/index.js';
+
+const server = http.createServer((req, res) => {
+  // handle common internet errors
+  // to avoid server crash
+  req.on('error', console.error);
+  res.on('error', console.error);
+
+
+  if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
+    const form = formidable({
+      uploadDir: `benchmark/testuploads`,
+      keepExtensions: true,
+    });
+
+    form.parse(req, (err, fields, files) => {
+      if (err) {
+        console.error(err);
+        res.writeHead(err.httpCode || 400, { 'Content-Type': 'text/plain' });
+        res.end(String(err));
+        return;
+      }
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify({ fields, files }, null, 2));
+    });
+
+    return;
+  }
+
+  // else not used in tests
+
+});
+
+server.listen(3000, () => {
+  console.log('Server listening on http://localhost:3000 ...');
+});
diff --git a/benchmark/testuploads/note.txt b/benchmark/testuploads/note.txt
new file mode 100644
index 00000000..449f599f
--- /dev/null
+++ benchmark/testuploads/note.txt
@@ -0,0 +1 @@
+File to force directory existence in git 
diff --git a/examples/forceBuffer.js b/examples/forceBuffer.js
new file mode 100644
index 00000000..47411499
--- /dev/null
+++ examples/forceBuffer.js
@@ -0,0 +1,69 @@
+// warning: forcing file into a Buffer elminates the benefits of using streams and may cause memory overflow
+import http from 'node:http';
+import { Buffer } from 'node:buffer'
+import { Writable } from 'node:stream';
+import formidable from '../src/index.js';
+
+
+const server = http.createServer((req, res) => {
+  if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
+    // parse a file upload
+    const endBuffers = {};
+    const form = formidable({
+      fileWriteStreamHandler: (file) => {
+        const chunks = [];
+        
+        const writable = new Writable({
+          write (chunk, enc, next) {
+            chunks.push(chunk);            
+            next();
+          },
+          destroy() {
+            endBuffers = {};
+          },
+          final(cb) {
+            const buffer = Buffer.concat(chunks);
+            // if filename option is not provided file.newFilename will be a random string
+            endBuffers[file.newFilename] = buffer;
+            cb();
+          },
+        })
+        return writable;
+      },
+    });
+
+    form.parse(req, (err, fields, files) => {
+      // available here endBuffers
+      if (err) {
+        console.error(err);
+        res.writeHead(err.httpCode || 400, { 'Content-Type': 'text/plain' });
+        res.end(String(err));
+        return;
+      }
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify({ fields, files }, null, 2));
+
+      Object.entries(endBuffers).map(([key, value]) => {
+        console.log(key);
+        console.log(value.toString("utf8"));
+      });
+    });
+
+    return;
+  }
+
+  // show a file upload form
+  res.writeHead(200, { 'Content-Type': 'text/html' });
+  res.end(`
+    <h2>With Node.js <code>"http"</code> module</h2>
+    <form action="/api/upload" enctype="multipart/form-data" method="post">
+      <div>Text field title: <input type="text" name="title"></div>
+      <div>File: <input type="file" name="myfile"></div>
+      <button>Upload</button>
+    </form>
+  `);
+});
+
+server.listen(3000, () => {
+  console.log('Server listening on http://localhost:3000 ...');
+});
diff --git examples/json.js examples/json.js
index f37bd4d8..dc872fb4 100644
--- examples/json.js
+++ examples/json.js
@@ -34,8 +34,8 @@ const server = http.createServer((req, res) => {
});

server.listen(PORT, () => {
-  const choosenPort = server.address().port;
-  console.log(`Listening on http://localhost:${choosenPort}/`);
+  const chosenPort = server.address().port;
+  console.log(`Listening on http://localhost:${chosenPort}/`);

  const body = JSON.stringify({
    numbers: [1, 2, 3, 4, 5],
@@ -46,7 +46,7 @@ server.listen(PORT, () => {
    {
      host: 'localhost',
      path: '/',
-      port: choosenPort,
+      port: chosenPort,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
diff --git examples/multiples.js examples/multiples.js
index 1cb378cf..03158492 100644
--- examples/multiples.js
+++ examples/multiples.js
@@ -20,8 +20,8 @@ const server = http.createServer((req, res) => {
        <label>file html array0<input type="file" name="filearray[]" /></label><br />
        <label>file html array1<input type="file" name="filearray[]" /></label><br />

-        <label>file html array and mulitple0<input type="file" name="filearray_with_multiple[]" multiple /></label><br />
-        <label>file html array and mulitple1<input type="file" name="filearray_with_multiple[]" multiple /></label><br />
+        <label>file html array and multiple0<input type="file" name="filearray_with_multiple[]" multiple /></label><br />
+        <label>file html array and multiple1<input type="file" name="filearray_with_multiple[]" multiple /></label><br />
        <br />
        <button>Upload</button>
      </form>
diff --git examples/with-http.js examples/with-http.js
index b19b0712..91d61a3b 100644
--- examples/with-http.js
+++ examples/with-http.js
@@ -1,7 +1,6 @@
import http from 'node:http';
import slugify from '@sindresorhus/slugify';
-import formidable from '../src/index.js';
-
+import formidable, {errors as formidableErrors} from '../src/index.js';

const server = http.createServer((req, res) => {
  // handle common internet errors
@@ -13,22 +12,35 @@ const server = http.createServer((req, res) => {
  if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
    // parse a file upload
    const form = formidable({
-      // uploadDir: `uploads`,
+      defaultInvalidName: 'invalid',
+      uploadDir: `uploads`,
      keepExtensions: true,
+      createDirsFromUploads: true,
+      allowEmptyFiles: true,
+      minFileSize: 0,
      filename(name, ext, part, form) {
        /* name basename of the http originalFilename
          ext with the dot ".txt" only if keepExtensions is true
         */
-        // slugify to avoid invalid filenames
-        // substr to define a maximum 
-        return `${slugify(name)}.${slugify(ext, {separator: ''})}`.substr(0, 100);
+        // originalFilename will have slashes with relative path if a
+        // directory was uploaded
+        const {originalFilename} = part;
+        if (!originalFilename) {
+          return 'invalid';
+        }
+        
        // return 'yo.txt'; // or completly different name
        // return 'z/yo.txt'; // subdirectory
+        return originalFilename.split("/").map((subdir) => {
+          return slugify(subdir, {separator: ''});  // slugify to avoid invalid filenames
+        }).join("/").substr(0, 100); // substr to define a maximum 
      },
-      // filter: function ({name, originalFilename, mimetype}) {
-      //   // keep only images
-      //   return mimetype && mimetype.includes("image");
-      // }
+      filter: function ({name, originalFilename, mimetype}) {
+        return Boolean(originalFilename);
+        // keep only images
+        // return mimetype?.includes("image");
+      }
+
      // maxTotalFileSize: 4000,
      // maxFileSize: 1000,

@@ -54,7 +66,8 @@ const server = http.createServer((req, res) => {
    <h2>With Node.js <code>"http"</code> module</h2>
    <form action="/api/upload" enctype="multipart/form-data" method="post">
      <div>Text field title: <input type="text" name="title" /></div>
-      <div>File: <input type="file" name="multipleFiles" multiple="multiple" /></div>
+      <div>File: <input type="file" name="multipleFiles" multiple /></div>
+      <div>Folders: <input type="file" name="folders" webkitdirectory directory multiple /></div>
      <input type="submit" value="Upload" />
    </form>

diff --git package.json package.json
index 32df036b..1e8b148f 100644
--- package.json
+++ package.json
@@ -1,22 +1,57 @@
{
  "name": "formidable",
-  "version": "3.2.4",
+  "version": "3.5.2",
  "license": "MIT",
  "description": "A node.js module for parsing form data, especially file uploads.",
  "homepage": "https://github.com/node-formidable/formidable",
  "funding": "https://ko-fi.com/tunnckoCore/commissions",
  "repository": "node-formidable/formidable",
  "type": "module",
-  "main": "./src/index.js",
+  "main": "./dist/index.cjs",
+  "exports": {
+    ".": {
+      "import": {
+        "default": "./src/index.js"
+      },
+      "require": {
+        "default": "./dist/index.cjs"
+      },
+      "default": "./dist/index.cjs"
+    },
+    "./src/helpers/*.js": {
+      "import": {
+        "default": "./src/helpers/*.js"
+      },
+      "require": {
+        "default": "./dist/helpers/*.cjs"
+      }
+    },
+    "./src/parsers/*.js": {
+      "import": {
+        "default": "./src/parsers/*.js"
+      },
+      "require": {
+        "default": "./dist/index.cjs"
+      }
+    }
+  },
  "files": [
-    "src"
+    "src",
+    "./dist",
+    "./CHANGELOG",
+    "./README.md",
+    "./README_pt_BR.md"
  ],
  "publishConfig": {
    "access": "public",
-    "tag": "v3"
+    "tag": "latest"
  },
  "scripts": {
+    "build-package": "rollup --config ./tool/rollup.config.js",
+    "prepublishOnly": "npm run build-package",
    "bench": "node benchmark",
+    "bench2prep": "node benchmark/server.js",
+    "bench2": "bombardier --body-file=\"./README.md\" --method=POST --duration=10s --connections=100 http://localhost:3000/api/upload",
    "fmt": "yarn run fmt:prepare '**/*'",
    "fmt:prepare": "prettier --write",
    "lint": "yarn run lint:prepare .",
@@ -25,18 +60,23 @@
    "postreinstall": "yarn setup",
    "setup": "yarn",
    "pretest": "del-cli ./test/tmp && make-dir ./test/tmp",
-    "test": "node  --experimental-vm-modules ./node_modules/jest/bin/jest.js --coverage",
+    "test-specific": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --testPathPattern=test/standalone/keep-alive-error.test.js",
+    "test": "npm run test-jest && npm run test-node",
+    "test-jest": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --testPathPattern=test/ --coverage",
+    "test-node": "node --test test-node/",
    "pretest:ci": "yarn run pretest",
-    "test:ci": "node --experimental-vm-modules node_modules/.bin/nyc jest --coverage"
+    "test:ci": "node --experimental-vm-modules node_modules/.bin/nyc jest --testPathPattern=test/ --coverage && node --experimental-vm-modules node_modules/.bin/nyc node --test test-node/"
  },
  "dependencies": {
-    "dezalgo": "1.0.3",
,-    "hexoid": "1.0.0",
-    "once": "1.4.0"
+    "dezalgo": "^1.0.4",
+    "hexoid": "^2.0.0",
+    "once": "^1.4.0"
  },
  "devDependencies": {
    "@commitlint/cli": "8.3.5",
    "@commitlint/config-conventional": "8.3.4",
+    "@rollup/plugin-commonjs": "^25.0.2",
+    "@rollup/plugin-node-resolve": "^15.1.0",
    "@sindresorhus/slugify": "^2.1.0",
    "@tunnckocore/prettier-config": "1.3.8",
    "del-cli": "3.0.0",
@@ -45,7 +85,7 @@
    "eslint-config-prettier": "6.11.0",
    "eslint-plugin-import": "2.20.2",
    "eslint-plugin-prettier": "3.1.3",
-    "express": "4.17.1",
+    "express": "^4.21.1",
    "formdata-polyfill": "^4.0.10",
    "husky": "4.2.5",
    "jest": "27.2.4",
@@ -55,6 +95,7 @@
    "nyc": "15.1.0",
    "prettier": "2.0.5",
    "prettier-plugin-pkgjson": "0.2.8",
+    "rollup": "^3.25.3",
    "supertest": "6.1.6"
  },
  "jest": {
diff --git src/Formidable.js src/Formidable.js
index 11aeda3c..7cc722bf 100644
--- src/Formidable.js
+++ src/Formidable.js
@@ -3,9 +3,10 @@

import os from 'node:os';
import path from 'node:path';
+import fsPromises from 'node:fs/promises';
import { EventEmitter } from 'node:events';
import { StringDecoder } from 'node:string_decoder';
-import hexoid from 'hexoid';
+import { hexoid } from 'hexoid';
import once from 'once';
import dezalgo from 'dezalgo';
import { octetstream, querystring, multipart, json } from './plugins/index.js';
@@ -25,6 +26,7 @@ const DEFAULT_OPTIONS = {
  maxTotalFileSize: undefined,
  minFileSize: 1,
  allowEmptyFiles: false,
+  createDirsFromUploads: false,
  keepExtensions: false,
  encoding: 'utf-8',
  hashAlgorithm: false,
@@ -42,6 +44,32 @@ function hasOwnProp(obj, key) {
  return Object.prototype.hasOwnProperty.call(obj, key);
}

+
+const decorateForceSequential = function (promiseCreator) {
+  /* forces a function that returns a promise to be sequential
+  useful for fs  for example */
+  let lastPromise = Promise.resolve();
+  return async function (...x) {
+      const promiseWeAreWaitingFor = lastPromise;
+      let currentPromise;
+      let callback;
+      // we need to change lastPromise before await anything,
+      // otherwise 2 calls might wait the same thing
+      lastPromise = new Promise(function (resolve) {
+          callback = resolve;
+      });
+      await promiseWeAreWaitingFor;
+      currentPromise = promiseCreator(...x);
+      currentPromise.then(callback).catch(callback);
+      return currentPromise;
+  };
+};
+
+const createNecessaryDirectoriesAsync = decorateForceSequential(function (filePath) {
+  const directoryname = path.dirname(filePath);
+  return fsPromises.mkdir(directoryname, { recursive: true });
+});
+
const invalidExtensionChar = (c) => {
  const code = c.charCodeAt(0);
  return !(
@@ -150,43 +178,58 @@ class IncomingForm extends EventEmitter {
    return true;
  }

-  parse(req, cb) {
+  // returns a promise if no callback is provided
+  async parse(req, cb) {
    this.req = req;
+    let promise;

    // Setup callback first, so we don't miss anything from data events emitted immediately.
-    if (cb) {
-      const callback = once(dezalgo(cb));
-      this.fields = {};
-      const files = {};
-
-      this.on('field', (name, value) => {
-        if (this.type === 'multipart' || this.type === 'urlencoded') {
-          if (!hasOwnProp(this.fields, name)) {
-            this.fields[name] = [value];
-          } else {
-            this.fields[name].push(value);
-          }
-        } else {
-          this.fields[name] = value;
-        }
+    if (!cb) {
+      let resolveRef;
+      let rejectRef;
+      promise = new Promise((resolve, reject) => {
+        resolveRef = resolve;
+        rejectRef = reject;
      });
-      this.on('file', (name, file) => {
-        if (!hasOwnProp(files, name)) {
-          files[name] = [file];
+      cb = (err, fields, files) => {
+        if (err) {
+          rejectRef(err);
        } else {
-          files[name].push(file);
+          resolveRef([fields, files]);
        }
-      });
-      this.on('error', (err) => {
-        callback(err, this.fields, files);
-      });
-      this.on('end', () => {
-        callback(null, this.fields, files);
-      });
+      }
    }
+    const callback = once(dezalgo(cb));
+    this.fields = {};
+    const files = {};
+
+    this.on('field', (name, value) => {
+      if (this.type === 'multipart' || this.type === 'urlencoded') {
+        if (!hasOwnProp(this.fields, name)) {
+          this.fields[name] = [value];
+        } else {
+          this.fields[name].push(value);
+        }
+      } else {
+        this.fields[name] = value;
+      }
+    });
+    this.on('file', (name, file) => {
+      if (!hasOwnProp(files, name)) {
+        files[name] = [file];
+      } else {
+        files[name].push(file);
+      }
+    });
+    this.on('error', (err) => {
+      callback(err, this.fields, files);
+    });
+    this.on('end', () => {
+      callback(null, this.fields, files);
+    });

    // Parse headers and setup the parser, ready to start listening for data.
-    this.writeHeaders(req.headers);
+    await this.writeHeaders(req.headers);

    // Start listening for data.
    req
@@ -212,14 +255,16 @@ class IncomingForm extends EventEmitter {
          this._parser.end();
        }
      });
-
+    if (promise) {
+      return promise;
+    }
    return this;
  }

-  writeHeaders(headers) {
+  async writeHeaders(headers) {
    this.headers = headers;
    this._parseContentLength();
-    this._parseContentType();
+    await this._parseContentType();

    if (!this._parser) {
      this._error(
@@ -258,10 +303,10 @@ class IncomingForm extends EventEmitter {

  onPart(part) {
    // this method can be overwritten by the user
-    this._handlePart(part);
+    return this._handlePart(part);
  }

-  _handlePart(part) {
+  async _handlePart(part) {
    if (part.originalFilename && typeof part.originalFilename !== 'string') {
      this._error(
        new FormidableError(
@@ -318,7 +363,7 @@ class IncomingForm extends EventEmitter {
    let fileSize = 0;
    const newFilename = this._getNewName(part);
    const filepath = this._joinDirectoryName(newFilename);
-    const file = this._newFile({
+    const file = await this._newFile({
      newFilename,
      filepath,
      originalFilename: part.originalFilename,
@@ -335,7 +380,7 @@ class IncomingForm extends EventEmitter {
    part.on('data', (buffer) => {
      this._totalFileSize += buffer.length;
      fileSize += buffer.length;
-      
+
      if (this._totalFileSize > this.options.maxTotalFileSize) {
        this._error(
          new FormidableError(
@@ -359,7 +404,7 @@ class IncomingForm extends EventEmitter {
      if (!this.options.allowEmptyFiles && fileSize === 0) {
        this._error(
          new FormidableError(
-            `options.allowEmptyFiles is false, file size should be greather than 0`,
+            `options.allowEmptyFiles is false, file size should be greater than 0`,
            errors.noEmptyFiles,
            400,
          ),
@@ -396,7 +441,7 @@ class IncomingForm extends EventEmitter {
  }

  // eslint-disable-next-line max-statements
-  _parseContentType() {
+  async _parseContentType() {
    if (this.bytesExpected === 0) {
      this._parser = new DummyParser(this, this.options);
      return;
@@ -417,10 +462,10 @@ class IncomingForm extends EventEmitter {
    new DummyParser(this, this.options);

    const results = [];
-    this._plugins.forEach((plugin, idx) => {
+    await Promise.all(this._plugins.map(async (plugin, idx) => {
      let pluginReturn = null;
      try {
-        pluginReturn = plugin(this, this.options) || this;
+        pluginReturn = await plugin(this, this.options) || this;
      } catch (err) {
        // directly throw from the `form.parse` method;
        // there is no other better way, except a handle through options
@@ -436,7 +481,7 @@ class IncomingForm extends EventEmitter {

      // todo: use Set/Map and pass plugin name instead of the `idx` index
      this.emit('plugin', idx, pluginReturn);
-    });
+    }));
    this.emit('pluginsResults', results);
  }

@@ -471,23 +516,35 @@ class IncomingForm extends EventEmitter {
    return new MultipartParser(this.options);
  }

-  _newFile({ filepath, originalFilename, mimetype, newFilename }) {
-    return this.options.fileWriteStreamHandler
-      ? new VolatileFile({
-          newFilename,
-          filepath,
-          originalFilename,
-          mimetype,
-          createFileWriteStream: this.options.fileWriteStreamHandler,
-          hashAlgorithm: this.options.hashAlgorithm,
-        })
-      : new PersistentFile({
-          newFilename,
-          filepath,
-          originalFilename,
-          mimetype,
-          hashAlgorithm: this.options.hashAlgorithm,
-        });
+  async _newFile({ filepath, originalFilename, mimetype, newFilename }) {
+    if (this.options.fileWriteStreamHandler) {
+      return new VolatileFile({
+        newFilename,
+        filepath,
+        originalFilename,
+        mimetype,
+        createFileWriteStream: this.options.fileWriteStreamHandler,
+        hashAlgorithm: this.options.hashAlgorithm,
+      });
+    }
+    if (this.options.createDirsFromUploads) {
+      try {
+        await createNecessaryDirectoriesAsync(filepath);
+      } catch (errorCreatingDir) {
+        this._error(new FormidableError(
+          `cannot create directory`,
+          errors.cannotCreateDir,
+          409,
+        ));
+      }
+    }
+    return new PersistentFile({
+      newFilename,
+      filepath,
+      originalFilename,
+      mimetype,
+      hashAlgorithm: this.options.hashAlgorithm,
+    });
  }

  _getFileName(headerValue) {
@@ -599,7 +656,7 @@ class IncomingForm extends EventEmitter {
  _setUpMaxFiles() {
    if (this.options.maxFiles !== Infinity) {
      let fileCount = 0;
-      this.on('file', () => {
+      this.on('fileBegin', () => {
        fileCount += 1;
        if (fileCount > this.options.maxFiles) {
          this._error(
diff --git src/FormidableError.js src/FormidableError.js
index baae4ebf..b49e7fd4 100644
--- src/FormidableError.js
+++ src/FormidableError.js
@@ -16,6 +16,7 @@ const unknownTransferEncoding = 1014;
const maxFilesExceeded = 1015;
const biggerThanMaxFileSize = 1016;
const pluginFailed = 1017;
+const cannotCreateDir = 1018;

const FormidableError = class extends Error {
  constructor(message, internalCode, httpCode = 500) {
@@ -44,6 +45,7 @@ export {
  unknownTransferEncoding,
  biggerThanTotalMaxFileSize,
  pluginFailed,
+  cannotCreateDir,
};

export default FormidableError;
diff --git src/parsers/Multipart.js src/parsers/Multipart.js
index 361a9cc4..9d7e4a55 100644
--- src/parsers/Multipart.js
+++ src/parsers/Multipart.js
@@ -59,6 +59,14 @@ class MultipartParser extends Transform {
    this.flags = 0;
  }

+  _endUnexpected() {
+    return new FormidableError(
+      `MultipartParser.end(): stream ended unexpectedly: ${this.explain()}`,
+      errors.malformedMultipart,
+      400,
+    );
+  }
+
  _flush(done) {
    if (
      (this.state === STATE.HEADER_FIELD_START && this.index === 0) ||
@@ -68,13 +76,9 @@ class MultipartParser extends Transform {
      this._handleCallback('end');
      done();
    } else if (this.state !== STATE.END) {
-      done(
-        new FormidableError(
-          `MultipartParser.end(): stream ended unexpectedly: ${this.explain()}`,
-          errors.malformedMultipart,
-          400,
-        ),
-      );
+      done(this._endUnexpected());
+    } else {
+      done();
    }
  }

@@ -136,7 +140,8 @@ class MultipartParser extends Transform {
      c = buffer[i];
      switch (state) {
        case STATE.PARSER_UNINITIALIZED:
-          return i;
+          done(this._endUnexpected());
+          return;
        case STATE.START:
          index = 0;
          state = STATE.START_BOUNDARY;
@@ -145,7 +150,8 @@ class MultipartParser extends Transform {
            if (c === HYPHEN) {
              flags |= FBOUNDARY.LAST_BOUNDARY;
            } else if (c !== CR) {
-              return i;
+              done(this._endUnexpected());
+              return;
            }
            index++;
            break;
@@ -159,7 +165,8 @@ class MultipartParser extends Transform {
              this._handleCallback('partBegin');
              state = STATE.HEADER_FIELD_START;
            } else {
-              return i;
+              done(this._endUnexpected());
+              return;
            }
            break;
          }
@@ -190,7 +197,8 @@ class MultipartParser extends Transform {
          if (c === COLON) {
            if (index === 1) {
              // empty header field
-              return i;
+              done(this._endUnexpected());
+              return;
            }
            dataCallback('headerField', true);
            state = STATE.HEADER_VALUE_START;
@@ -199,7 +207,8 @@ class MultipartParser extends Transform {

          cl = lower(c);
          if (cl < A || cl > Z) {
-            return i;
+            done(this._endUnexpected());
+            return;
          }
          break;
        case STATE.HEADER_VALUE_START:
@@ -218,13 +227,15 @@ class MultipartParser extends Transform {
          break;
        case STATE.HEADER_VALUE_ALMOST_DONE:
          if (c !== LF) {
-            return i;
+            done(this._endUnexpected());
+return;
          }
          state = STATE.HEADER_FIELD_START;
          break;
        case STATE.HEADERS_ALMOST_DONE:
          if (c !== LF) {
-            return i;
+            done(this._endUnexpected());
+            return;
          }

          this._handleCallback('headersEnd');
@@ -237,7 +248,7 @@ class MultipartParser extends Transform {
          prevIndex = index;

          if (index === 0) {
-            // boyer-moore derrived algorithm to safely skip non-boundary data
+            // boyer-moore derived algorithm to safely skip non-boundary data
            i += boundaryEnd;
            while (i < this.bufferLength && !(buffer[i] in boundaryChars)) {
              i += boundaryLength;
@@ -311,7 +322,8 @@ class MultipartParser extends Transform {
        case STATE.END:
          break;
        default:
-          return i;
+          done(this._endUnexpected());
+          return;
      }
    }

diff --git src/plugins/multipart.js src/plugins/multipart.js
index dfb54651..eebd96a7 100644
--- src/plugins/multipart.js
+++ src/plugins/multipart.js
@@ -51,7 +51,7 @@ function createInitMultipart(boundary) {
    parser.initWithBoundary(boundary);

    // eslint-disable-next-line max-statements, consistent-return
-    parser.on('data', ({ name, buffer, start, end }) => {
+    parser.on('data', async ({ name, buffer, start, end }) => {
      if (name === 'partBegin') {
        part = new Stream();
        part.readable = true;
@@ -124,7 +124,7 @@ function createInitMultipart(boundary) {
                /*
                  four bytes (chars) in base64 converts to three bytes in binary
                  encoding. So we should always work with a number of bytes that
-                  can be divided by 4, it will result in a number of buytes that
+                  can be divided by 4, it will result in a number of bytes that
                  can be divided vy 3.
                  */
                const offset = parseInt(part.transferBuffer.length / 4, 10) * 4;
@@ -159,8 +159,9 @@ function createInitMultipart(boundary) {
              ),
            );
        }
-
-        this.onPart(part);
+        this._parser.pause();
+        await this.onPart(part);
+        this._parser.resume();
      } else if (name === 'end') {
        this.ended = true;
        this._maybeEnd();
diff --git src/plugins/octetstream.js src/plugins/octetstream.js
index 33c23291..0bc5c67e 100644
--- src/plugins/octetstream.js
+++ src/plugins/octetstream.js
@@ -4,7 +4,7 @@ import OctetStreamParser from '../parsers/OctetStream.js';

export const octetStreamType = 'octet-stream';
// the `options` is also available through the `options` / `formidable.options`
-export default function plugin(formidable, options) {
+export default async function plugin(formidable, options) {
  // the `this` context is always formidable, as the first argument of a plugin
  // but this allows us to customize/test each plugin

@@ -12,7 +12,7 @@ export default function plugin(formidable, options) {
  const self = this || formidable;

  if (/octet-stream/i.test(self.headers['content-type'])) {
-    init.call(self, self, options);
+    await init.call(self, self, options);
  }
  return self;
}
@@ -20,7 +20,7 @@ export default function plugin(formidable, options) {
// Note that it's a good practice (but it's up to you) to use the `this.options` instead
// of the passed `options` (second) param, because when you decide
// to test the plugin you can pass custom `this` context to it (and so `this.options`)
-function init(_self, _opts) {
+async function init(_self, _opts) {
  this.type = octetStreamType;
  const originalFilename = this.headers['x-file-name'];
  const mimetype = this.headers['content-type'];
@@ -31,7 +31,7 @@ function init(_self, _opts) {
  };
  const newFilename = this._getNewName(thisPart);
  const filepath = this._joinDirectoryName(newFilename);
-  const file = this._newFile({
+  const file = await this._newFile({
    newFilename,
    filepath,
    originalFilename,
diff --git a/test-node/standalone/createDirsFromUploads.test.js b/test-node/standalone/createDirsFromUploads.test.js
new file mode 100644
index 00000000..400dbbfb
--- /dev/null
+++ test-node/standalone/createDirsFromUploads.test.js
@@ -0,0 +1,65 @@
+import {strictEqual, deepEqual} from 'node:assert';
+import { createServer, request } from 'node:http';
+import formidable from '../../src/index.js';
+import test from 'node:test';
+import fs from 'node:fs';
+
+const PORT = 13539;
+const uploads = './uploads';
+
+test('folder created', (t,done) => {
+  const server = createServer((req, res) => {
+    const form = formidable({
+      createDirsFromUploads: true,
+      uploadDir: uploads,
+      filename: (x) => {
+        return 'x/y/z.txt'
+      }
+    });
+
+    form.parse(req, () => {
+      res.writeHead(200);
+      res.end("ok")
+    });
+  });
+
+  server.listen(PORT, () => {
+    const chosenPort = server.address().port;
+    const  body = `----13068458571765726332503797717\r
+Content-Disposition: form-data; name="title"\r
+\r
+a\r
+----13068458571765726332503797717\r
+Content-Disposition: form-data; name="multipleFiles"; filename="x.txt"\r
+Content-Type: application/x-javascript\r
+\r
+\r
+\r
+a\r
+b\r
+c\r
+d\r
+\r
+----13068458571765726332503797717--\r
+`;
+    fetch(String(new URL(`http:localhost:${chosenPort}/`)), {
+      method: 'POST',
+      
+      headers: {
+        'Content-Length': body.length,
+        Host: `localhost:${chosenPort}`,
+        'Content-Type': 'multipart/form-data; boundary=--13068458571765726332503797717',
+      },
+      body
+    }).then(res => {
+      //may also contain tests from other tests
+      deepEqual(fs.readdirSync(uploads).includes('x'), true);
+      deepEqual(fs.readdirSync(`${uploads}/x`), ['y']);
+      deepEqual(fs.readdirSync(`${uploads}/x/y`), ['z.txt']);
+      strictEqual(res.status, 200);
+      server.close();
+      done();
+    });
+   
+  });
+});
diff --git a/test-node/standalone/end-event-emitted-twice.test.js b/test-node/standalone/end-event-emitted-twice.test.js
new file mode 100644
index 00000000..9cc03e7b
--- /dev/null
+++ test-node/standalone/end-event-emitted-twice.test.js
@@ -0,0 +1,63 @@
+import {strictEqual} from 'node:assert';
+import { createServer, request } from 'node:http';
+import formidable from '../../src/index.js';
+import test from 'node:test';
+
+const PORT = 13539;
+
+test('end event emitted twice', (t,done) => {
+  const server = createServer((req, res) => {
+    const form = formidable();
+
+    let i = 0;
+    form.on('end', () => {
+      i += 1;
+      strictEqual(i, 1, 'end should be emitted once  (on end)');
+    });
+    form.parse(req, () => {
+      try {
+        strictEqual(i, 1, 'end should be emitted once (callback)');
+      } catch (e) {
+        done(e);
+      }
+      res.writeHead(200);
+      res.end("ok")
+    });
+  });
+
+  server.listen(PORT, () => {
+    const chosenPort = server.address().port;
+    const  body = `----13068458571765726332503797717\r
+Content-Disposition: form-data; name="title"\r
+\r
+a\r
+----13068458571765726332503797717\r
+Content-Disposition: form-data; name="multipleFiles"; filename="x.txt"\r
+Content-Type: application/x-javascript\r
+\r
+\r
+\r
+a\r
+b\r
+c\r
+d\r
+\r
+----13068458571765726332503797717--\r
+`;
+    fetch(String(new URL(`http:localhost:${chosenPort}/`)), {
+      method: 'POST',
+      
+      headers: {
+        'Content-Length': body.length,
+        Host: `localhost:${chosenPort}`,
+        'Content-Type': 'multipart/form-data; boundary=--13068458571765726332503797717',
+      },
+      body
+    }).then(res => {
+      strictEqual(res.status, 200);
+        server.close();
+        done();
+    });
+   
+  });
+});
diff --git a/test-node/standalone/multipart_parser.test.js b/test-node/standalone/multipart_parser.test.js
new file mode 100644
index 00000000..df56b7c4
--- /dev/null
+++ test-node/standalone/multipart_parser.test.js
@@ -0,0 +1,27 @@
+import {Readable} from 'node:stream';
+import MultipartParser from '../../src/parsers/Multipart.js';
+import {malformedMultipart} from '../../src/FormidableError.js';
+
+import test from 'node:test';
+import assert, { deepEqual } from 'node:assert';
+
+
+
+test('MultipartParser does not hang', async (t) => {
+    const mime = `--_\r\n--_--\r\n`;
+    const parser = new MultipartParser();
+    parser.initWithBoundary('_');
+    try {
+        for await (const {name, buffer, start, end} of Readable.from(mime).pipe(parser)) {
+            console.log(name, buffer ? buffer.subarray(start, end).toString() : '');
+        }
+
+    } catch (e) {
+        deepEqual(e.code, malformedMultipart)
+        return;
+        // console.error('error');
+        // console.error(e);
+
+    }
+    assert(false, 'should catch error');
+});
\ No newline at end of file
diff --git a/test-node/standalone/promise.test.js b/test-node/standalone/promise.test.js
new file mode 100644
index 00000000..7026dd99
--- /dev/null
+++ test-node/standalone/promise.test.js
@@ -0,0 +1,115 @@
+import {strictEqual, ok} from 'node:assert';
+import { createServer, request } from 'node:http';
+import formidable, {errors} from '../../src/index.js';
+import test from 'node:test';
+
+const PORT = 13539;
+
+const isPromise = (x) => {
+    return x && typeof x === `object` && typeof x.then === `function`; 
+};
+
+test('parse returns promise if no callback is provided', (t,done) => {
+  const server = createServer((req, res) => {
+    const form = formidable();
+
+    const promise = form.parse(req);
+    strictEqual(isPromise(promise), true);
+    promise.then(([fields, files]) => {
+      ok(typeof fields === 'object');
+      ok(typeof files === 'object');
+      res.writeHead(200);
+      res.end("ok")
+    }).catch(e => {
+      done(e)
+    })
+  });
+
+  server.listen(PORT, () => {
+    const chosenPort = server.address().port;
+    const  body = `----13068458571765726332503797717\r
+Content-Disposition: form-data; name="title"\r
+\r
+a\r
+----13068458571765726332503797717\r
+Content-Disposition: form-data; name="multipleFiles"; filename="x.txt"\r
+Content-Type: application/x-javascript\r
+\r
+\r
+\r
+a\r
+b\r
+c\r
+d\r
+\r
+----13068458571765726332503797717--\r
+`;
+    fetch(String(new URL(`http:localhost:${chosenPort}/`)), {
+      method: 'POST',
+      
+      headers: {
+        'Content-Length': body.length,
+        Host: `localhost:${chosenPort}`,
+        'Content-Type': 'multipart/form-data; boundary=--13068458571765726332503797717',
+      },
+      body
+    }).then(res => {
+        strictEqual(res.status, 200);
+        server.close();
+        done();
+    });
+   
+  });
+});
+
+test('parse rejects with promise if it fails', (t,done) => {
+  const server = createServer((req, res) => {
+    const form = formidable({minFileSize: 10 ** 6}); // create condition to fail
+
+    const promise = form.parse(req);
+    strictEqual(isPromise(promise), true);
+    promise.then(() => {
+      done('should have failed')
+    }).catch(e => {
+      res.writeHead(e.httpCode);
+      strictEqual(e.code, errors.smallerThanMinFileSize);
+      res.end(String(e))
+    })
+  });
+
+  server.listen(PORT, () => {
+    const chosenPort = server.address().port;
+    const  body = `----13068458571765726332503797717\r
+Content-Disposition: form-data; name="title"\r
+\r
+a\r
+----13068458571765726332503797717\r
+Content-Disposition: form-data; name="multipleFiles"; filename="x.txt"\r
+Content-Type: application/x-javascript\r
+\r
+\r
+\r
+a\r
+b\r
+c\r
+d\r
+\r
+----13068458571765726332503797717--\r
+`;
+    fetch(String(new URL(`http:localhost:${chosenPort}/`)), {
+      method: 'POST',
+      
+      headers: {
+        'Content-Length': body.length,
+        Host: `localhost:${chosenPort}`,
+        'Content-Type': 'multipart/form-data; boundary=--13068458571765726332503797717',
+      },
+      body
+    }).then(res => {
+        strictEqual(res.status, 400);
+        server.close();
+        done();
+    });
+   
+  });
+});
diff --git test/fixture/file/plain.txt test/fixture/file/plain.txt
index 7b4c80da..a162a7e8 100644
--- test/fixture/file/plain.txt
+++ test/fixture/file/plain.txt
@@ -1 +1 @@
-I am a simple plain text file!
+I am a simple plain text file
diff --git test/fixture/http/workarounds/missing-hyphens1.http test/fixture/http/workarounds/missing-hyphens1.http
index 28f29a51..996a48bc 100644
--- test/fixture/http/workarounds/missing-hyphens1.http
+++ test/fixture/http/workarounds/missing-hyphens1.http
@@ -7,6 +7,6 @@ Content-Length: 189
Content-Disposition: form-data; name="upload"; filename="plain.txt"
Content-Type: text/plain

-I am a simple plain text file!
+I am a simple plain text file

------TLV0SrKD4z1TRxRhAPUvZ
diff --git test/fixture/js/no-filename.js test/fixture/js/no-filename.js
index c39355f7..21320026 100644
--- test/fixture/js/no-filename.js
+++ test/fixture/js/no-filename.js
@@ -14,7 +14,7 @@ const filename_name_http = [
    name: 'upload',
    originalFilename: 'plain.txt',
    fixture: 'filename-name',
-    sha1: 'b31d07bac24ac32734de88b3687dddb10e976872',
+    sha1: 'a47f7a8a7959f36c3f151ba8b0bd28f2d6b606e2',
  },
];

diff --git test/fixture/js/special-chars-in-filename.js test/fixture/js/special-chars-in-filename.js
index 1aa9d991..3940cd13 100644
--- test/fixture/js/special-chars-in-filename.js
+++ test/fixture/js/special-chars-in-filename.js
@@ -18,7 +18,7 @@ function expect(originalFilename, fixtureName) {
}

const osx_chrome_13_http = expect(' ? % * | " < > . ? ; \' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt', 'osx-chrome-13');
-const osx_firefox_3_6_http = expect(' ? % * | " < > . ? ; \' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt', 'osx-firefox-3.6');
+const osx_firefox_3_6_http = expect(' ? % * | " < > . ☃ ; \' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt', 'osx-firefox-3.6');

const xp_ie_7_http = expect(' ? % * | " < > . ☃ ; \' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt', 'xp-ie-7');
const xp_ie_8_http = expect(' ? % * | " < > . ☃ ; \' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt', 'xp-ie-8');
diff --git test/integration/fixtures.test.js test/integration/fixtures.test.js
index 0b6c81b3..a37840c0 100644
--- test/integration/fixtures.test.js
+++ test/integration/fixtures.test.js
@@ -24,9 +24,9 @@ const fixtures= {
  encoding,
  misc,
  [`no-filename`]: noFilename,
-  preamble, 
+  preamble,
  [`special-chars-in-filename`]: specialCharsInFilename,
-  workarounds,
+  // workarounds, // todo uncomment this and make it work
};

test('fixtures', (done) => {
@@ -70,13 +70,14 @@ test('fixtures', (done) => {

        if (parsedPart.type === 'file') {
          const file = parsedPart.value;
-          strictEqual(file.originalFilename, expectedPart.originalFilename);
+          strictEqual(file.originalFilename, expectedPart.originalFilename,
+            `${JSON.stringify([expectedPart, file])}`);

          if (expectedPart.sha1) {
            strictEqual(
              file.hash,
              expectedPart.sha1,
-              `SHA1 error ${file.name} on ${file.filepath}`,
+              `SHA1 error ${file.originalFilename} on ${file.filepath} ${JSON.stringify([expectedPart, file])}`,
            );
          }
        }
@@ -97,7 +98,7 @@ test('fixtures', (done) => {
      function callback(...args) {
        const realCallback = cb;
        // eslint-disable-next-line no-param-reassign
-        cb = function calbackFn() {};
+        cb = function callbackFn() {};

        realCallback(...args);
      }
diff --git test/standalone/connection-aborted.test.js test/standalone/connection-aborted.test.js
index b7181424..014a1230 100644
--- test/standalone/connection-aborted.test.js
+++ test/standalone/connection-aborted.test.js
@@ -3,7 +3,7 @@ import { createServer } from 'node:http';
import { connect } from 'node:net';
import formidable from '../../src/index.js';

-const PORT = 13539;
+const PORT = 13540;

test('connection aborted', (done) => {
  const server = createServer((req) => {
@@ -15,7 +15,6 @@ test('connection aborted', (done) => {
    });
    form.on('error', () => {
      assert(abortedReceived, 'Error event should follow aborted');
-      server.close();
    });
    form.on('end', () => {
      throw new Error('Unexpected "end" event');
@@ -26,15 +25,16 @@ test('connection aborted', (done) => {
        'from .parse() callback: Error event should follow aborted',
      );

-      server.close();
-      done();
+      server.close(() => {
+        done();
+      });
    });
  });

  server.listen(PORT, 'localhost', () => {
-    const choosenPort = server.address().port;
+    const chosenPort = server.address().port;

-    const client = connect(choosenPort);
+    const client = connect(chosenPort);

    client.write(
      'POST / HTTP/1.1\r\n' +
diff --git test/standalone/content-transfer-encoding.test.js test/standalone/content-transfer-encoding.test.js
index 4db89ea9..faf67ff2 100644
--- test/standalone/content-transfer-encoding.test.js
+++ test/standalone/content-transfer-encoding.test.js
@@ -9,9 +9,10 @@ const UPLOAD_DIR = join(process.cwd(), 'test', 'tmp');
// OS choosing port
const PORT = 13530;
test('content transfer encoding', (done) => {
-  const server = createServer((req, res) => {
-    const form = formidable();
-    form.uploadDir = UPLOAD_DIR;
+  const server = createServer(async (req, res) => {
+    const form = formidable({
+      uploadDir: UPLOAD_DIR
+    });
    form.on('end', () => {
      throw new Error('Unexpected "end" event');
    });
@@ -19,11 +20,14 @@ test('content transfer encoding', (done) => {
      res.writeHead(500);
      res.end(e.message);
    });
-    form.parse(req);
+    try {
+      await form.parse(req);
+    } catch (formidableError) {
+    }
  });

  server.listen(PORT, () => {
-    const choosenPort = server.address().port;
+    const chosenPort = server.address().port;

    const body =
      '--foo\r\n' +
@@ -39,7 +43,7 @@ test('content transfer encoding', (done) => {

    const req = request({
      method: 'POST',
-      port: choosenPort,
+      port: chosenPort,
      headers: {
        'Content-Length': body.length,
        'Content-Type': 'multipart/form-data; boundary=foo',
diff --git test/standalone/end-event-emitted-twice.test.js test/standalone/end-event-emitted-twice.test.js
deleted file mode 100644
index 650f431b..00000000
--- test/standalone/end-event-emitted-twice.test.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import {strictEqual} from 'node:assert';
-import { createServer } from 'node:http';
-import { connect } from 'node:net';
-import formidable from '../../src/index.js';
-
-const PORT = 13539;
-
-test('end event emitted twice', (done) => {
-  const server = createServer((req) => {
-    const form = formidable();
-
-    let i = 0;
-    form.on('end', () => {
-      i += 1;
-      strictEqual(i, 1, 'end should be emitted once');
-    });
-    form.parse(req, () => {
-
-      server.close();
-      strictEqual(i, 1, 'end should be emitted once');
-      done();
-    });
-  });
-
-  server.listen(PORT, 'localhost', () => {
-    const choosenPort = server.address().port;
-
-    const client = connect(choosenPort);
-
-    client.write(
-`POST /api/upload HTTP/1.1
-Host: localhost:${choosenPort}
-User-Agent: N
-Content-Type: multipart/form-data; boundary=---------------------------13068458571765726332503797717
-
-
------------------------------13068458571765726332503797717
-Content-Disposition: form-data; name="title"
-
-a
------------------------------13068458571765726332503797717
-Content-Disposition: form-data; name="multipleFiles"; filename="x.txt"
-Content-Type: application/x-javascript
-
-
-
-a
-b
-c
-d
-
------------------------------13068458571765726332503797717--
-`,
-    );
-    client.end();
-  });
-});
diff --git test/standalone/issue-46.test.js test/standalone/issue-46.test.js
index 6707f247..3a24d892 100644
--- test/standalone/issue-46.test.js
+++ test/standalone/issue-46.test.js
@@ -10,20 +10,20 @@ const body = "LS1hN2E2NWI5OS04YTYxLTRlMmMtYjE0OS1mNzNhM2IzNWY5MjMNCmNvbnRlbnQtZG
const buffer = Buffer.from(body, 'base64url');

test("issue 46", (done) => {
-  const server = createServer((req, res) => {
+  const server = createServer(async (req, res) => {
    // Parse form and write results to response.
    const form = formidable();
-    form.parse(req, (err, fields, files) => {
-      ok(fields.foo, 'should have fields.foo === barry');
-      strictEqual(fields.foo[0], 'barry');
-      server.close();
+    const [fields] = await form.parse(req);
+    ok(fields.foo, 'should have fields.foo === barry');
+    strictEqual(fields.foo[0], 'barry');
+    server.close(() => {
      done();
    });
  });

  server.listen(PORT, () => {
-    const choosenPort = server.address().port;
-    const url = `http://localhost:${choosenPort}`;
+    const chosenPort = server.address().port;
+    const url = `http://localhost:${chosenPort}`;

    const req = request(url, {
      method: "POST",
@@ -35,5 +35,6 @@ test("issue 46", (done) => {

    req.write(buffer);
    req.end();
+    
  });
});
diff --git test/standalone/keep-alive-error.test.js test/standalone/keep-alive-error.test.js
index 18ddab48..f1bc5ccf 100644
--- test/standalone/keep-alive-error.test.js
+++ test/standalone/keep-alive-error.test.js
@@ -8,10 +8,10 @@ import formidable from '../../src/index.js';
let ok = 0;
let errors = 0;

-const PORT = 0;
+const PORT = 89;

test('keep alive error', (done) => {
-  const server = createServer((req, res) => {
+  const server = createServer(async (req, res) => {
    const form = formidable();
    form.on('error', () => {
      errors += 1;
@@ -23,13 +23,36 @@ test('keep alive error', (done) => {
      res.writeHead(200);
      res.end();
    });
-    form.parse(req);
+    try {
+      await form.parse(req);
+      // for client two
+      strictEqual(ok, 1, `should "ok" count === 1, has: ${ok}`);
+
+      server.close(() => {
+        done();
+      });
+    } catch (formidableError) {
+      strictEqual(errors, 1, `should "errors" === 1, has: ${errors}`);
+
+      const clientTwo = createConnection(PORT);
+
+      // correct post upload (with hyphens)
+      clientTwo.write(
+        'POST /upload-test HTTP/1.1\r\n' +
+          'Host: localhost\r\n' +
+          'Connection: keep-alive\r\n' +
+          'Content-Type: multipart/form-data; boundary=----aaa\r\n' +
+          'Content-Length: 13\r\n\r\n' +
+          '------aaa--\r\n',
+      );
+      clientTwo.end();
+
+    }
  });

  server.listen(PORT, () => {
-    const choosenPort = server.address().port;

-    const client = createConnection(choosenPort);
+    const client = createConnection(PORT);

    // first send malformed (boundary / hyphens) post upload
    client.write(
@@ -47,29 +70,6 @@ test('keep alive error', (done) => {
      client.write(buf);
      client.end();

-      setTimeout(() => {
-        strictEqual(errors, 1, `should "errors" === 1, has: ${errors}`);
-
-        const clientTwo = createConnection(choosenPort);
-
-        // correct post upload (with hyphens)
-        clientTwo.write(
-          'POST /upload-test HTTP/1.1\r\n' +
-            'Host: localhost\r\n' +
-            'Connection: keep-alive\r\n' +
-            'Content-Type: multipart/form-data; boundary=----aaa\r\n' +
-            'Content-Length: 13\r\n\r\n' +
-            '------aaa--\r\n',
-        );
-        clientTwo.end();
-
-        setTimeout(() => {
-          strictEqual(ok, 1, `should "ok" count === 1, has: ${ok}`);
-
-          server.close();
-          done();
-        }, 300);
-      }, 200);
    }, 150);
  });
});
diff --git test/unit/custom-plugins.test.js test/unit/custom-plugins.test.js
index 6ae00ef1..ca15534d 100644
--- test/unit/custom-plugins.test.js
+++ test/unit/custom-plugins.test.js
@@ -5,7 +5,7 @@ import { join } from 'node:path';
import Koa from 'koa';
import request from 'supertest';

-import { formidable, json, octetstream, multipart } from '../../src/index.js';
+import { formidable, json, octetstream, multipart, errors } from '../../src/index.js';

function createServer(options, handler) {
  const app = new Koa();
@@ -25,14 +25,14 @@ function fromFixtures(...args) {

// function makeRequest(server, options) {
//   server.listen(0, () => {
-//     const choosenPort = server.address().port;
-//     const url = `http://localhost:${choosenPort}`;
+//     const chosenPort = server.address().port;
+//     const url = `http://localhost:${chosenPort}`;

//     const method = 'POST';

//     const opts = {
//       ...options,
-//       port: choosenPort,
+//       port: chosenPort,
//       url,
//       method,
//     };
@@ -85,7 +85,6 @@ test('should call 3 custom and 1 builtin plugins, when .parse() is called', asyn
      expect(fields.qux).toBe('zaz');
      expect(fields.a).toBe('bbb');
      expect(ctx.__pluginsCount).toBe(4);
-      expect(ctx.__pluginsResults).toBe(true);
    });
  });

@@ -101,7 +100,7 @@ test('should call 3 custom and 1 builtin plugins, when .parse() is called', asyn
test('.parse throw error when some plugin fail', async () => {
  const server = createServer(
    { enabledPlugins: [octetstream, json] },
-    (ctx, form) => {
+    async (ctx, form) => {
      // const failedIsOkay = false;
      // ! not emitted?
      // form.on('file', () => {
@@ -124,10 +123,10 @@ test('.parse throw error when some plugin fail', async () => {

      let res = null;
      try {
-        form.parse(ctx.req);
+        await form.parse(ctx.req);
      } catch (err) {
-        expect(err.message).toMatch(/custom plugin err/);
-        expect(err.message).toMatch(/plugin on index 2 failed/);
+        expect(err.code).toBe(errors.pluginFailed);
+        expect(err.httpCode).toBe(500);

        expect(form._plugins.length).toBe(3);
        expect(ctx.__pluginsCount).toBe(2);
@@ -144,12 +143,18 @@ test('.parse throw error when some plugin fail', async () => {
    },
  );

-  await new Promise((resolve, reject) => {
+  return new Promise((resolve, reject) => {
    request(server.callback())
      .post('/')
      .type('application/octet-stream')
      .attach('bin', fromFixtures('file', 'binaryfile.tar.gz'))
-      .end((err) => (err ? reject(err) : resolve()));
+      .end((err) => {
+        if (err.code === 'ECONNRESET') {
+          resolve();
+        } else {
+          reject(err);
+        }
+      });
  });
});

diff --git test/unit/formidable.test.js test/unit/formidable.test.js
index ab6de22a..8d0769ee 100644
--- test/unit/formidable.test.js
+++ test/unit/formidable.test.js
@@ -9,6 +9,14 @@ import path from 'node:path';
import formidable from '../../src/index.js';
import * as mod from '../../src/index.js';

+
+function requestStub() {
+  return Object.assign(new Stream (), {
+    pause() {},
+    resume() {},
+  });
+}
+
function getForm(name, opts) {
  return name === 'formidable' ? formidable(opts) : new mod[name](opts);
}
@@ -188,18 +196,21 @@ function makeHeader(originalFilename) {
          const form = getForm(name, {
            allowEmptyFiles: false,
          });
+          form.req = requestStub();

          const part = new Stream();
          part.mimetype = 'text/plain';
          // eslint-disable-next-line max-nested-callbacks
          form.on('error', (error) => {
            expect(error.message).toBe(
-              'options.allowEmptyFiles is false, file size should be greather than 0',
+              'options.allowEmptyFiles is false, file size should be greater than 0',
            );
            done();
          });
-          form.onPart(part);
-          part.emit('end');
+          form.onPart(part).then (function () {
+            part.emit('end');
+            form.emit('end');
+          });
        });
      });

@@ -214,6 +225,8 @@ function makeHeader(originalFilename) {
          part.mimetype = 'text/plain';
          form.onPart(part);
          part.emit('data', Buffer.alloc(1));
+          part.emit('end');
+          form.emit('end');
          expect(formEmitSpy).not.toBeCalledWith('error');
        });
      });
@@ -228,6 +241,7 @@ function makeHeader(originalFilename) {
        part.mimetype = 'text/plain';
        form.onPart(part);
        part.emit('end');
+        form.emit('end');
        expect(formEmitSpy).not.toBeCalledWith('error');
      });
    });
@@ -237,6 +251,7 @@ function makeHeader(originalFilename) {
        const form = getForm(name, { minFileSize: 5 });

        const part = new Stream();
+        const req = requestStub();
        part.mimetype = 'text/plain';
        form.on('error', (error) => {
          expect(error.message).toBe(
@@ -244,8 +259,13 @@ function makeHeader(originalFilename) {
          );
          done();
        });
-        form.onPart(part);
-        part.emit('data', Buffer.alloc(4));
+        form.req = req;
+        form.onPart(part).then(function () {
+          part.emit('data', Buffer.alloc(4));
+          part.emit('end');
+          form.emit('end');
+        });
+
      });
    });

@@ -258,6 +278,8 @@ function makeHeader(originalFilename) {
        part.mimetype = 'text/plain';
        form.onPart(part);
        part.emit('data', Buffer.alloc(11));
+        part.emit('end');
+        form.emit('end');
        expect(formEmitSpy).not.toBeCalledWith('error');
      });
    });
diff --git test/unit/persistent-file.test.js test/unit/persistent-file.disabled-test.js
similarity index 87%
rename from test/unit/persistent-file.test.js
rename to test/unit/persistent-file.disabled-test.js
index cb6f39f5..99142245 100644
--- test/unit/persistent-file.test.js
+++ test/unit/persistent-file.disabled-test.js
@@ -1,6 +1,8 @@
import {jest} from '@jest/globals';
+import fs from 'node:fs';
import PersistentFile from '../../src/PersistentFile.js';

+const mockFs = fs;
const now = new Date();
const file = new PersistentFile({
  size: 1024,
@@ -13,12 +15,11 @@ const file = new PersistentFile({
  mimetype: 'image/png',
});

-
+const mockFn = jest.fn();
jest.mock('fs', () => {
-  const fs = jest.requireActual('fs');
  return {
-    ...fs,
-    unlink: jest.fn(),
+    ...mockFs,
+    unlink: mockFn,
  };
});

@@ -41,6 +42,6 @@ describe('PersistentFile', () => {
    file.open();
    file.destroy();
    // eslint-disable-next-line global-require
-    expect(require('fs').unlink).toBeCalled();
+    expect(mockFn).toBeCalled();
  });
});
diff --git a/tool/rollup.config.js b/tool/rollup.config.js
new file mode 100644
index 00000000..d95a2a40
--- /dev/null
+++ tool/rollup.config.js
@@ -0,0 +1,105 @@
+/* eslint-disable */
+import cjs from '@rollup/plugin-commonjs';
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+import packageJson from '../package.json' with { type: "json" };
+
+const {dependencies} = packageJson;
+const plugins = [nodeResolve(), cjs()];
+const cjsOptions = {
+  format: `cjs`,
+  exports: `named`,
+}
+
+const external = [...Object.keys(dependencies)];
+
+export default [
+  {
+    input: `src/index.js`,
+    output: [
+      {
+        file: `dist/index.cjs`,
+        ...cjsOptions,
+      },
+    ],
+    external,
+    plugins,
+  },
+  {
+    input: `src/helpers/firstValues.js`,
+    output: [
+      {
+        file: `dist/helpers/firstValues.cjs`,
+        ...cjsOptions,
+      },
+    ],
+    external,
+    plugins,
+  },
+  {
+    input: `src/helpers/readBooleans.js`,
+    output: [
+      {
+        file: `dist/helpers/readBooleans.cjs`,
+        ...cjsOptions,
+      },
+    ],
+    external,
+    plugins,
+  },
+  {
+    input: `src/parsers/JSON.js`,
+    output: [
+      {
+        file: `dist/parsers/JSON.cjs`,
+        ...cjsOptions,
+      },
+    ],
+    external,
+    plugins,
+  },
+  {
+    input: `src/parsers/Multipart.js`,
+    output: [
+      {
+        file: `dist/parsers/Multipart.cjs`,
+        ...cjsOptions,
+      },
+    ],
+    external,
+    plugins,
+  },
+  {
+    input: `src/parsers/Querystring.js`,
+    output: [
+      {
+        file: `dist/parsers/Querystring.cjs`,
+        ...cjsOptions,
+      },
+    ],
+    external,
+    plugins,
+  },
+  {
+    input: `src/parsers/OctetStream.js`,
+    output: [
+      {
+        file: `dist/parsers/OctetStream.cjs`,
+        ...cjsOptions,
+      },
+    ],
+    external,
+    plugins,
+  },
+  {
+    input: `src/parsers/StreamingQuerystring.js`,
+    output: [
+      {
+        file: `dist/parsers/StreamingQuerystring.cjs`,
+        ...cjsOptions,
+      },
+    ],
+    external,
+    plugins,
+  },
+];
+


Description

This PR updates the formidable package, introducing several improvements and changes to the codebase. The main motivations for these changes include enhancing the package's functionality, improving performance, and addressing various issues.

Possible Issues

  1. The removal of the workarounds fixture in the integration tests might lead to reduced test coverage for certain edge cases.
  2. The changes to the MultipartParser class might introduce unexpected behavior in certain scenarios if not thoroughly tested.

Security Hotspots

  1. The implementation of createDirsFromUploads option in Formidable.js could potentially be exploited if not properly sanitized, allowing arbitrary directory creation.
Changes

Changes

  1. .eslintrc.cjs:

    • Fixed a typo in a comment.
  2. .github/workflows/nodejs.yml:

    • Updated Node.js versions for CI/CD.
  3. .gitignore:

    • Added new directories and files to ignore.
  4. CHANGELOG.md:

    • Added entries for versions 3.5.2, 3.5.1, 3.5.0, 3.4.0, 3.3.2, 3.2.5, and 3.2.4.
  5. README.md:

    • Updated installation instructions and examples.
    • Added information about the new createDirsFromUploads option.
    • Updated API documentation.
  6. VERSION_NOTES.md:

    • Updated information about v2 and v3 versions.
  7. benchmark/:

    • Added new benchmark files and results.
  8. examples/:

    • Added new examples and updated existing ones.
  9. package.json:

    • Updated version to 3.5.2.
    • Added new scripts and dependencies.
    • Updated export configurations.
  10. src/Formidable.js:

    • Added support for createDirsFromUploads option.
    • Implemented promise-based parsing.
    • Various improvements and bug fixes.
  11. src/FormidableError.js:

    • Added new error code for directory creation failures.
  12. src/parsers/Multipart.js:

    • Improved error handling and flushing behavior.
  13. src/plugins/multipart.js:

    • Updated to use async/await for better flow control.
  14. test/:

    • Added new tests and updated existing ones.
    • Moved some tests to the new test-node/ directory.
  15. tool/rollup.config.js:

    • Added new configuration for building the package.
sequenceDiagram
    participant Client
    participant Formidable
    participant Parser
    participant FileSystem

    Client->>Formidable: Send form data
    Formidable->>Parser: Initialize parser
    Parser->>Formidable: Parse data
    Formidable->>FileSystem: Create directories (if enabled)
    FileSystem-->>Formidable: Directories created
    Formidable->>FileSystem: Write files
    FileSystem-->>Formidable: Files written
    Formidable-->>Client: Return parsed data
Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0 participants