formidable
Advanced tools
Comparing version 2.0.0-dev.20200131.2 to 2.0.0
@@ -0,19 +1,24 @@ | ||
# Changelog | ||
### Unreleased (`canary` & `dev` dist-tags) | ||
* Test only on Node.js >= v10. Support only Node LTS and latest ([#515](https://github.com/node-formidable/node-formidable/pull/515)) | ||
* stop using deprecated features ([#516](https://github.com/node-formidable/node-formidable/pull/516), [#472](https://github.com/node-formidable/node-formidable/issues/472), [#406](https://github.com/node-formidable/node-formidable/issues/406)) | ||
* throw error during data parsing ([#513](https://github.com/node-formidable/node-formidable/pull/513)) | ||
* Array support for fields and files ([#380](https://github.com/node-formidable/node-formidable/pull/380), [#340](https://github.com/node-formidable/node-formidable/pull/340), [#367](https://github.com/node-formidable/node-formidable/pull/367), [#33](https://github.com/node-formidable/node-formidable/issues/33), [#498](https://github.com/node-formidable/node-formidable/issues/498), [#280](https://github.com/node-formidable/node-formidable/issues/280), [#483](https://github.com/node-formidable/node-formidable/issues/483)) | ||
* feat: add options.filter ([#716](https://github.com/node-formidable/formidable/pull/716)) | ||
* feat: add code and httpCode to most errors ([#686](https://github.com/node-formidable/formidable/pull/686)) | ||
* rename: option.hash into option.hashAlgorithm ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* rename: file.path into file.filepath ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* rename: file.type into file.mimetype ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* refactor: split file.name into file.newFilename and file.originalFilename ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* feat: prevent directory traversal attacks by default ([#689](https://github.com/node-formidable/formidable/pull/689)) | ||
* meta: stop including test files in npm ([7003c](https://github.com/node-formidable/formidable/commit/7003cd6133f90c384081accb51743688d5e1f4be)) | ||
* fix: handle invalid filenames ([d0a34](https://github.com/node-formidable/formidable/commit/d0a3484b048b8c177e62d66aecb03f5928f7a857)) | ||
* feat: add fileWriteStreamHandler option | ||
* feat: add allowEmptyFiles and minFileSize options | ||
* feat: Array support for fields and files ([#380](https://github.com/node-formidable/node-formidable/pull/380), [#340](https://github.com/node-formidable/node-formidable/pull/340), [#367](https://github.com/node-formidable/node-formidable/pull/367), [#33](https://github.com/node-formidable/node-formidable/issues/33), [#498](https://github.com/node-formidable/node-formidable/issues/498), [#280](https://github.com/node-formidable/node-formidable/issues/280), [#483](https://github.com/node-formidable/node-formidable/issues/483)) | ||
* possible partial fix of [#386](https://github.com/node-formidable/node-formidable/pull/386) with #380 (need tests and better implementation) | ||
* use hasOwnProperty in check against files/fields ([#522](https://github.com/node-formidable/node-formidable/pull/522)) | ||
* do not promote `IncomingForm` and add `exports.default` ([#529](https://github.com/node-formidable/node-formidable/pull/529)) | ||
* Improve examples and tests ([#523](https://github.com/node-formidable/node-formidable/pull/523)) | ||
* First step of Code quality improvements ([#525](https://github.com/node-formidable/node-formidable/pull/525)) | ||
* refactor: use hasOwnProperty in check against files/fields ([#522](https://github.com/node-formidable/node-formidable/pull/522)) | ||
* meta: do not promote `IncomingForm` and add `exports.default` ([#529](https://github.com/node-formidable/node-formidable/pull/529)) | ||
* meta: Improve examples and tests ([#523](https://github.com/node-formidable/node-formidable/pull/523)) | ||
* refactor: First step of Code quality improvements ([#525](https://github.com/node-formidable/node-formidable/pull/525)) | ||
* chore(funding): remove patreon & add npm funding field ([#525](https://github.com/node-formidable/node-formidable/pull/532) | ||
* feat: use Modern Streams API ([#531](https://github.com/node-formidable/node-formidable/pull/531)) | ||
* fix: remove gently hijack and tests ([#539](https://github.com/node-formidable/node-formidable/pull/539)) | ||
* docs: Clarify supported hash algorithms ([#537](https://github.com/node-formidable/node-formidable/pull/537)) | ||
* feat: better tests, add Airbnb + Prettier ([#542](https://github.com/node-formidable/node-formidable/pull/542)) | ||
* fix(incomingForm): better detection of fields vs files | ||
* fix: resolves [#128](https://github.com/node-formidable/node-formidable/pull/128) | ||
* fix: urlencoded parsing to emit end [#543](https://github.com/node-formidable/node-formidable/pull/543), introduced in [#531](https://github.com/node-formidable/node-formidable/pull/531) | ||
@@ -24,5 +29,7 @@ * fix(tests): include multipart and qs parser unit tests, part of [#415](https://github.com/node-formidable/node-formidable/issues/415) | ||
* feat: introduce Plugins API, fix silent failing tests ([#545](https://github.com/node-formidable/node-formidable/pull/545), [#391](https://github.com/node-formidable/node-formidable/pull/391), [#407](https://github.com/node-formidable/node-formidable/pull/407), [#386](https://github.com/node-formidable/node-formidable/pull/386), [#374](https://github.com/node-formidable/node-formidable/pull/374), [#521](https://github.com/node-formidable/node-formidable/pull/521), [#267](https://github.com/node-formidable/node-formidable/pull/267)) | ||
* respect form hash option on incoming octect/stream requests ([#407](https://github.com/node-formidable/node-formidable/pull/407)) | ||
* fix: exposing file writable stream errors ([#520](https://github.com/node-formidable/node-formidable/pull/520), [#316](https://github.com/node-formidable/node-formidable/pull/316), [#469](https://github.com/node-formidable/node-formidable/pull/469), [#470](https://github.com/node-formidable/node-formidable/pull/470)) | ||
* feat: custom file (re)naming, thru options.filename ([#591](https://github.com/node-formidable/node-formidable/pull/591), [#84](https://github.com/node-formidable/node-formidable/issues/84), [#86](https://github.com/node-formidable/node-formidable/issues/86), [#94](https://github.com/node-formidable/node-formidable/issues/94), [#154](https://github.com/node-formidable/node-formidable/issues/154), [#158](https://github.com/node-formidable/node-formidable/issues/158), [#488](https://github.com/node-formidable/node-formidable/issues/488), [#595](https://github.com/node-formidable/node-formidable/issues/595)) | ||
### v1.2.1 (2018-03-20) | ||
@@ -83,1 +90,5 @@ | ||
* Fix file handle leak on error (OrangeDog) | ||
--- | ||
[First commit, #3270eb4b1f8b (May 4th, 2010)](https://github.com/node-formidable/formidable/commit/3270eb4b1f8bb667b8c12f64c36a4e7b854216d8) |
{ | ||
"name": "formidable", | ||
"version": "2.0.0-dev.20200131.2", | ||
"version": "2.0.0", | ||
"license": "MIT", | ||
"description": "A node.js module for parsing form data, especially file uploads.", | ||
"homepage": "https://github.com/node-formidable/node-formidable", | ||
"homepage": "https://github.com/node-formidable/formidable", | ||
"funding": "https://ko-fi.com/tunnckoCore/commissions", | ||
"repository": "node-formidable/node-formidable", | ||
"repository": "node-formidable/formidable", | ||
"main": "./src/index.js", | ||
"files": [ | ||
"src", | ||
"test" | ||
"CHANGELOG.md", | ||
"LICENSE", | ||
"README.md", | ||
"VERSION_NOTES.md" | ||
], | ||
"publishConfig": { | ||
"access": "public", | ||
"tag": "dev" | ||
"tag": "latest" | ||
}, | ||
@@ -24,37 +27,46 @@ "scripts": { | ||
"lint:prepare": "eslint --cache --fix --quiet --format codeframe", | ||
"reinstall": "rm -rf node_modules yarn.lock && yarn", | ||
"pretest": "rm -rf test/tmp && mkdir test/tmp", | ||
"test": "node test/run.js", | ||
"pretest:ci": "yarn pretest", | ||
"test:ci": "nyc node test/run.js" | ||
"reinstall": "del-cli ./node_modules ./yarn.lock", | ||
"postreinstall": "yarn setup", | ||
"setup": "yarn", | ||
"pretest": "del-cli ./test/tmp && make-dir ./test/tmp", | ||
"test": "jest --coverage", | ||
"pretest:ci": "yarn run pretest", | ||
"test:ci": "nyc jest --coverage", | ||
"test:jest": "jest --coverage" | ||
}, | ||
"dependencies": { | ||
"dezalgo": "^1.0.3", | ||
"once": "^1.4.0" | ||
"dezalgo": "1.0.3", | ||
"hexoid": "1.0.0", | ||
"once": "1.4.0", | ||
"qs": "6.9.3" | ||
}, | ||
"devDependencies": { | ||
"@commitlint/cli": "^8.3.5", | ||
"@commitlint/config-conventional": "^8.3.4", | ||
"@tunnckocore/prettier-config": "^1.2.0", | ||
"eslint": "^6.8.0", | ||
"eslint-config-airbnb-base": "^14.0.0", | ||
"eslint-config-prettier": "^6.10.0", | ||
"eslint-plugin-import": "^2.20.0", | ||
"eslint-plugin-prettier": "^3.1.2", | ||
"husky": "^4.2.1", | ||
"jest": "^25.1.0", | ||
"koa": "^2.11.0", | ||
"lint-staged": "^10.0.6", | ||
"nyc": "^15.0.0", | ||
"prettier": "^1.19.1", | ||
"prettier-plugin-pkgjson": "^0.2.0", | ||
"request": "^2.88.0", | ||
"supertest": "^4.0.2", | ||
"urun": "^0.0.8", | ||
"utest": "^0.0.8" | ||
"@commitlint/cli": "8.3.5", | ||
"@commitlint/config-conventional": "8.3.4", | ||
"@tunnckocore/prettier-config": "1.3.8", | ||
"del-cli": "3.0.0", | ||
"eslint": "6.8.0", | ||
"eslint-config-airbnb-base": "14.1.0", | ||
"eslint-config-prettier": "6.11.0", | ||
"eslint-plugin-import": "2.20.2", | ||
"eslint-plugin-prettier": "3.1.3", | ||
"express": "4.17.1", | ||
"husky": "4.2.5", | ||
"jest": "25.4.0", | ||
"koa": "2.11.0", | ||
"lint-staged": "10.2.7", | ||
"make-dir-cli": "2.0.0", | ||
"nyc": "15.0.1", | ||
"prettier": "2.0.5", | ||
"prettier-plugin-pkgjson": "0.2.8", | ||
"request": "2.88.2", | ||
"supertest": "4.0.2" | ||
}, | ||
"jest": { | ||
"verbose": true | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "git status --porcelain && lint-staged", | ||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" | ||
"pre-commit": "git status --porcelain && yarn lint-staged", | ||
"commit-msg": "yarn commitlint -E HUSKY_GIT_PARAMS" | ||
} | ||
@@ -74,3 +86,19 @@ }, | ||
] | ||
} | ||
}, | ||
"renovate": { | ||
"extends": [ | ||
"@tunnckocore", | ||
":pinAllExceptPeerDependencies" | ||
] | ||
}, | ||
"keywords": [ | ||
"multipart", | ||
"form", | ||
"data", | ||
"querystring", | ||
"www", | ||
"json", | ||
"ulpoad", | ||
"file" | ||
] | ||
} |
461
README.md
<p align="center"> | ||
<img alt="node formidable logo" src="./logo.png" /> | ||
<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] | ||
# 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] | ||
@@ -10,10 +10,38 @@ > A Node.js module for parsing form data, especially file uploads. | ||
[![Code style][codestyle-img]][codestyle-url] | ||
[![build status][build-img]][build-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] | ||
If you have any _how-to_ kind of questions, please read the [Contributing | ||
Guide][contributing-url] and [Code of Conduct][code_of_conduct-url] | ||
documents.<br /> For bugs reports and feature requests, [please create an | ||
issue][open-issue-url] or ping [@tunnckoCore / @3a1FcBx0](https://twitter.com/3a1FcBx0) | ||
at Twitter. | ||
[![Conventional Commits][ccommits-img]][ccommits-url] | ||
[![Minimum Required Nodejs][nodejs-img]][npmv-url] | ||
[![Tidelift Subcsription][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] | ||
[![Twitter][twitter-img]][twitter-url] | ||
## Status: Maintained [![npm version][npmv-canary-img]][npmv-url] [![npm version][npmv-dev-img]][npmv-url] | ||
This project is [semantically versioned](https://semver.org) and available as | ||
part of the [Tidelift Subscription][tidelift-url] for professional grade | ||
assurances, enhanced support and security. | ||
[Learn more.](https://tidelift.com/subscription/pkg/npm-formidable?utm_source=npm-formidable&utm_medium=referral&utm_campaign=enterprise) | ||
_The maintainers of `formidable` and thousands of other packages are working | ||
with Tidelift to deliver commercial support and maintenance for the Open Source | ||
dependencies you use to build your applications. Save time, reduce risk, and | ||
improve code health, while paying the maintainers of the exact dependencies you | ||
use._ | ||
[![][npm-weekly-img]][npmv-url] [![][npm-monthly-img]][npmv-url] | ||
[![][npm-yearly-img]][npmv-url] [![][npm-alltime-img]][npmv-url] | ||
## Project Status: Maintained | ||
_Check [VERSION NOTES](https://github.com/node-formidable/formidable/blob/master/VERSION_NOTES.md) for more information on v1, v2, and v3 plans, NPM dist-tags and branches._ | ||
This module was initially developed by | ||
@@ -28,21 +56,12 @@ [**@felixge**](https://github.com/felixge) for | ||
are always welcome! :heart: Jump on | ||
[issue #412](https://github.com/felixge/node-formidable/issues/412) if you are | ||
interested. | ||
[issue #412](https://github.com/felixge/node-formidable/issues/412) which is | ||
closed, but if you are interested we can discuss it and add you after strict | ||
rules, like enabling Two-Factor Auth in your npm and GitHub accounts. | ||
_**Note:** Master is a "canary" branch - try it with `npm i formidable@canary`. | ||
Do not expect (for now) things from it to be inside the`latest`"dist-tag" in the | ||
Npm. The`formidable@latest`is the`v1.2.1` version and probably it will be the | ||
last`v1` release!_ | ||
_**Note: v2 is coming soon!**_ | ||
You can try the | ||
[Plugins API](https://github.com/felixge/node-formidable/tree/plugins-api) | ||
([#545](https://github.com/felixge/node-formidable/pull/545)), which is | ||
available through `formidable@dev`. | ||
## Highlights | ||
- Fast (~900-2500 mb/sec), streaming multipart parser | ||
- Automatically writing file uploads to disk | ||
- [Fast (~900-2500 mb/sec)](#benchmarks) & streaming multipart parser | ||
- Automatically writing file uploads to disk (optional, see | ||
[`options.fileWriteStreamHandler`](#options)) | ||
- [Plugins API](#useplugin-plugin) - allowing custom parsers and plugins | ||
- Low memory footprint | ||
@@ -54,65 +73,182 @@ - Graceful error handling | ||
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._ | ||
This is a low-level package, and if you're using a high-level framework it _may_ | ||
already be included. Check the examples below and the [examples/](https://github.com/node-formidable/formidable/tree/master/examples) folder. | ||
```sh | ||
# v2 | ||
npm install formidable | ||
# or the canary version | ||
npm install formidable@canary | ||
npm install formidable@latest | ||
npm install formidable@v2 | ||
# or v3 | ||
npm install formidable@v3 | ||
``` | ||
or with Yarn v1/v2 | ||
_**Note:** In near future v3 will be published on the `latest` NPM dist-tag. Future not ready releases will continue to be published on `canary` dist-tag._ | ||
```sh | ||
yarn add formidable | ||
# or the canary version | ||
yarn add formidable@canary | ||
``` | ||
This is a low-level package, and if you're using a high-level framework it may | ||
already be included. | ||
## Examples | ||
However, [Express v4](http://expressjs.com) does not include any multipart | ||
handling, nor does [body-parser](https://github.com/expressjs/body-parser). | ||
For more examples look at the `examples/` directory. | ||
For `koa` there is [koa-better-body](https://ghub.now.sh/koa-better-body) which | ||
can handle ANY type of body / form-data - JSON, urlencoded, multpart and so on. | ||
A new major release is coming there too. | ||
### with Node.js http module | ||
## Example | ||
Parse an incoming file upload, with the | ||
[Node.js's built-in `http` module](https://nodejs.org/api/http.html). | ||
Parse an incoming file upload. | ||
```js | ||
const http = require('http'); | ||
const util = require('util'); | ||
const formidable = require('formidable'); | ||
http | ||
.createServer((req, res) => { | ||
if (req.url === '/upload' && req.method.toLowerCase() === 'post') { | ||
// parse a file upload | ||
const form = formidable(); | ||
const server = http.createServer((req, res) => { | ||
if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') { | ||
// parse a file upload | ||
const form = formidable({ multiples: true }); | ||
form.parse(req, (err, fields, files) => { | ||
res.writeHead(200, { 'content-type': 'text/plain' }); | ||
res.write('received upload:\n\n'); | ||
res.end(util.inspect({ fields: fields, files: files })); | ||
}); | ||
form.parse(req, (err, fields, files) => { | ||
if (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; | ||
} | ||
// 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="multipleFiles" multiple="multiple" /></div> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`); | ||
}); | ||
server.listen(8080, () => { | ||
console.log('Server listening on http://localhost:8080/ ...'); | ||
}); | ||
``` | ||
### with Express.js | ||
There are multiple variants to do this, but Formidable just need Node.js Request | ||
stream, so something like the following example should work just fine, without | ||
any third-party [Express.js](https://ghub.now.sh/express) middleware. | ||
Or try the | ||
[examples/with-express.js](https://github.com/node-formidable/formidable/blob/master/examples/with-express.js) | ||
```js | ||
const express = require('express'); | ||
const formidable = require('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({ multiples: true }); | ||
form.parse(req, (err, fields, files) => { | ||
if (err) { | ||
next(err); | ||
return; | ||
} | ||
res.json({ fields, files }); | ||
}); | ||
}); | ||
// show a file upload form | ||
res.writeHead(200, { 'content-type': 'text/html' }); | ||
res.end(` | ||
<form action="/upload" enctype="multipart/form-data" method="post"> | ||
<input type="text" name="title" /><br/> | ||
<input type="file" name="upload" multiple="multiple" /><br/> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`); | ||
}) | ||
.listen(8080, () => { | ||
console.log('Server listening on http://localhost:8080/ ...'); | ||
}); | ||
app.listen(3000, () => { | ||
console.log('Server listening on http://localhost:3000 ...'); | ||
}); | ||
``` | ||
### with Koa and Formidable | ||
Of course, with [Koa v1, v2 or future v3](https://ghub.now.sh/koa) the things | ||
are very similar. You can use `formidable` manually as shown below or through | ||
the [koa-better-body](https://ghub.now.sh/koa-better-body) package which is | ||
using `formidable` under the hood and support more features and different | ||
request bodies, check its documentation for more info. | ||
_Note: this example is assuming Koa v2. Be aware that you should pass `ctx.req` | ||
which is Node.js's Request, and **NOT** the `ctx.request` which is Koa's Request | ||
object - there is a difference._ | ||
```js | ||
const Koa = require('koa'); | ||
const formidable = require('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({ multiples: true }); | ||
// not very elegant, but that's for now if you don't want to use `koa-better-body` | ||
// or other 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; | ||
} | ||
// show a file upload form | ||
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 | ||
@@ -158,4 +294,4 @@ | ||
_Please pass [`options`](#options) to the function/constructor, not by passing | ||
assigning them to the instance `form`_ | ||
_Please pass [`options`](#options) to the function/constructor, not by assigning | ||
them to the instance `form`_ | ||
@@ -181,4 +317,4 @@ ```js | ||
See it's defaults in [src/Formidable.js](./src/Formidable.js#L14-L22) (the | ||
`DEFAULT_OPTIONS` constant). | ||
See it's defaults in [src/Formidable.js DEFAULT_OPTIONS](./src/Formidable.js) | ||
(the `DEFAULT_OPTIONS` constant). | ||
@@ -188,16 +324,29 @@ - `options.encoding` **{string}** - default `'utf-8'`; sets encoding for | ||
- `options.uploadDir` **{string}** - default `os.tmpdir()`; the directory for | ||
placing file uploads in. You can move them later by using `fs.rename()` | ||
placing file uploads in. You can move them later by using `fs.rename()`. | ||
- `options.keepExtensions` **{boolean}** - default `false`; to include the | ||
extensions of the original files or not | ||
- `options.allowEmptyFiles` **{boolean}** - default `true`; allow upload empty | ||
files | ||
- `options.minFileSize` **{number}** - default `1` (1byte); the minium size of | ||
uploaded file. | ||
- `options.maxFileSize` **{number}** - default `200 * 1024 * 1024` (200mb); | ||
limit the size of uploaded file. | ||
- `options.maxFields` **{number}** - default `1000`; limit the number of fields | ||
that the Querystring parser will decode, set 0 for unlimited | ||
- `options.maxFields` **{number}** - default `1000`; limit the number of fields, set 0 for unlimited | ||
- `options.maxFieldsSize` **{number}** - default `20 * 1024 * 1024` (20mb); | ||
limit the amount of memory all fields together (except files) can allocate in | ||
bytes. | ||
- `options.hash` **{boolean}** - default `false`; include checksums calculated | ||
- `options.hashAlgorithm` **{string | false}** - default `false`; include checksums calculated | ||
for incoming files, set this to some hash algorithm, see | ||
[crypto.createHash](https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm_options) | ||
for available algorithms | ||
- `options.fileWriteStreamHandler` **{function}** - default `null`, which by | ||
default writes to host machine file system every file parsed; The function | ||
should return an instance of a | ||
[Writable stream](https://nodejs.org/api/stream.html#stream_class_stream_writable) | ||
that will receive the uploaded file data. With this option, you can have any | ||
custom behavior regarding where the uploaded file data will be streamed for. | ||
If you are looking to write the file uploaded in other types of cloud storages | ||
(AWS S3, Azure blob storage, Google cloud storage) or private file storage, | ||
this is the option you're looking for. When this option is defined the default | ||
behavior of writing the file in the host machine file system is lost. | ||
- `options.multiples` **{boolean}** - default `false`; when you call the | ||
@@ -208,5 +357,14 @@ `.parse` method, the `files` argument (of the callback) will contain arrays of | ||
fields that have names ending with '[]'. | ||
- `options.filename` **{function}** - default `undefined` Use it to control | ||
newFilename. Must return a string. Will be joined with options.uploadDir. | ||
_**Note:** If this value is exceeded, an `'error'` event is emitted._ | ||
- `options.filter` **{function}** - default function that always returns true. | ||
Use it to filter files before they are uploaded. Must return a boolean. | ||
#### `options.filename` **{function}** function (name, ext, part, form) -> string | ||
_**Note:** If this size of combined fields, or size of some file is exceeded, an | ||
`'error'` event is fired._ | ||
```js | ||
@@ -222,2 +380,16 @@ // The amount of bytes received for this form so far. | ||
#### `options.filter` **{function}** function ({name, originalFilename, mimetype}) -> boolean | ||
**Note:** use an outside variable to cancel all uploads upon the first error | ||
```js | ||
const options = { | ||
filter: function ({name, originalFilename, mimetype}) { | ||
// keep only images | ||
return mimetype && mimetype.includes("image"); | ||
} | ||
}; | ||
``` | ||
### .parse(request, callback) | ||
@@ -244,2 +416,37 @@ | ||
About `uploadDir`, given the following directory structure | ||
``` | ||
project-name | ||
├── src | ||
│ └── server.js | ||
│ | ||
└── uploads | ||
└── image.jpg | ||
``` | ||
`__dirname` would be the same directory as the source file itself (src) | ||
```js | ||
`${__dirname}/../uploads` | ||
``` | ||
to put files in uploads. | ||
Omitting `__dirname` would make the path relative to the current working directory. This would be the same if server.js is launched from src but not project-name. | ||
`null` will use default which is `os.tmpdir()` | ||
Note: If the directory does not exist, the uploaded files are __silently discarded__. To make sure it exists: | ||
```js | ||
import {createNecessaryDirectoriesSync} from "filesac"; | ||
const uploadPath = `${__dirname}/../uploads`; | ||
createNecessaryDirectoriesSync(`${uploadPath}/x`); | ||
``` | ||
In the example below, we listen on couple of events and direct them to the | ||
@@ -256,8 +463,8 @@ `data` listener, so you can do whatever you choose there, based on whether its | ||
form.on('fileBegin', (filename, file) => { | ||
form.emit('data', { name: 'fileBegin', filename, value: file }); | ||
form.on('fileBegin', (formname, file) => { | ||
form.emit('data', { name: 'fileBegin', formname, value: file }); | ||
}); | ||
form.on('file', (filename, file) => { | ||
form.emit('data', { name: 'file', key: filename, value: file }); | ||
form.on('file', (formname, file) => { | ||
form.emit('data', { name: 'file', formname, value: file }); | ||
}); | ||
@@ -274,3 +481,3 @@ | ||
// If you want to customize whatever you want... | ||
form.on('data', ({ name, key, value, buffer, start, end, ...more }) => { | ||
form.on('data', ({ name, key, value, buffer, start, end, formname, ...more }) => { | ||
if (name === 'partBegin') { | ||
@@ -293,6 +500,6 @@ } | ||
if (name === 'file') { | ||
console.log('file:', key, value); | ||
console.log('file:', formname, value); | ||
} | ||
if (name === 'fileBegin') { | ||
console.log('fileBegin:', key, value); | ||
console.log('fileBegin:', formname, value); | ||
} | ||
@@ -352,3 +559,3 @@ }); | ||
const form = new Formidable({ | ||
hash: 'sha1', | ||
hashAlgorithm: 'sha1', | ||
enabledPlugins: ['octetstream', 'querystring', 'json'], | ||
@@ -376,3 +583,3 @@ }); | ||
form.onPart = (part) => { | ||
part.on('data', (buffer) { | ||
part.on('data', (buffer) => { | ||
// do whatever you want here | ||
@@ -389,7 +596,7 @@ }); | ||
form.onPart = function(part) { | ||
form.onPart = function (part) { | ||
// let formidable handle only non-file parts | ||
if (part.filename === '' || !part.mime) { | ||
if (part.originalFilename === '' || !part.mimetype) { | ||
// used internally, please do not override! | ||
form.handlePart(part); | ||
form._handlePart(part); | ||
} | ||
@@ -410,16 +617,20 @@ }; | ||
// case you are unhappy with the way formidable generates a temporary path for your files. | ||
file.path: string; | ||
file.filepath: string; | ||
// The name this file had according to the uploading client. | ||
file.name: string | null; | ||
file.originalFilename: string | null; | ||
// calculated based on options provided | ||
file.newFilename: string | null; | ||
// The mime type of this file, according to the uploading client. | ||
file.type: string | null; | ||
file.mimetype: string | null; | ||
// A Date object (or `null`) containing the time this file was last written to. | ||
// Mostly here for compatibility with the [W3C File API Draft](http://dev.w3.org/2006/webapi/FileAPI/). | ||
file.lastModifiedDate: Date | null; | ||
file.mtime: Date | null; | ||
// If `options.hash` calculation was set, you can read the hex digest out of this var. | ||
file.hash: string | 'sha1' | 'md5' | 'sha256' | null; | ||
file.hashAlgorithm: false | |'sha1' | 'md5' | 'sha256' | ||
// If `options.hashAlgorithm` calculation was set, you can read the hex digest out of this var (at the end it will be a string) | ||
file.hash: string | object | null; | ||
} | ||
@@ -460,3 +671,10 @@ ``` | ||
```js | ||
form.on('fileBegin', (name, file) => {}); | ||
form.on('fileBegin', (formName, file) => { | ||
// accessible here | ||
// formName the name in the form (<input name="thisname" type="file">) 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 = CUSTOM_PATH // to change the final path | ||
}); | ||
``` | ||
@@ -470,3 +688,7 @@ | ||
```js | ||
form.on('file', (name, file) => {}); | ||
form.on('file', (formname, file) => { | ||
// same as fileBegin, except | ||
// it is too late to change file.filepath | ||
// file.hash is available if options.hash was used | ||
}); | ||
``` | ||
@@ -480,2 +702,4 @@ | ||
May have `error.httpCode` and `error.code` attached. | ||
```js | ||
@@ -505,2 +729,6 @@ form.on('error', (err) => {}); | ||
## Changelog | ||
[./CHANGELOG.md](./CHANGELOG.md) | ||
## Ports & Credits | ||
@@ -518,4 +746,4 @@ | ||
button (pencil icon) and suggest a correction. If you would like to help us fix | ||
a bug or add a new feature, please check our | ||
[Contributing Guide](./CONTRIBUTING.md). Pull requests are welcome! | ||
a bug or add a new feature, please check our [Contributing | ||
Guide][contributing-url]. Pull requests are welcome! | ||
@@ -550,2 +778,3 @@ Thanks goes to these wonderful people | ||
<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> | ||
@@ -559,2 +788,9 @@ </table> | ||
From a [Felix blog post](https://felixge.de/2013/03/11/the-pull-request-hack/): | ||
- [Sven Lito](https://github.com/svnlto) for fixing bugs and merging patches | ||
- [egirshov](https://github.com/egirshov) for contributing many improvements to the node-formidable multipart parser | ||
- [Andrew Kelley](https://github.com/superjoe30) for also helping with fixing bugs and making improvements | ||
- [Mike Frey](https://github.com/mikefrey) for contributing JSON support | ||
## License | ||
@@ -569,6 +805,4 @@ | ||
[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/node-formidable | ||
[codecov-img]: https://badgen.net/codecov/c/github/node-formidable/node-formidable/master?icon=codecov | ||
[build-img]: https://badgen.net/github/checks/node-formidable/node-formidable?label=build&icon=github | ||
[build-url]: https://github.com/node-formidable/node-formidable/actions?query=workflow%3Anodejs | ||
[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 | ||
@@ -579,3 +813,3 @@ [npmv-dev-img]: https://badgen.net/npm/v/formidable/dev?icon=npm | ||
[license-img]: https://badgen.net/npm/license/formidable | ||
[license-url]: https://github.com/node-formidable/node-formidable/blob/master/LICENSE | ||
[license-url]: https://github.com/node-formidable/formidable/blob/master/LICENSE | ||
[chat-img]: https://badgen.net/badge/chat/on%20gitter/46BC99?icon=gitter | ||
@@ -589,5 +823,30 @@ [chat-url]: https://gitter.im/node-formidable/Lobby | ||
[prs-welcome-url]: http://makeapullrequest.com | ||
[twitter-url]: https://twitter.com/tunnckoCore | ||
[twitter-img]: https://badgen.net/twitter/follow/tunnckoCore?icon=twitter&color=1da1f2&cache=300 | ||
[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 --> |
@@ -7,5 +7,4 @@ /* eslint-disable class-methods-use-this */ | ||
const os = require('os'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const crypto = require('crypto'); | ||
const hexoid = require('hexoid'); | ||
const once = require('once'); | ||
@@ -15,3 +14,5 @@ const dezalgo = require('dezalgo'); | ||
const { StringDecoder } = require('string_decoder'); | ||
const qs = require('qs'); | ||
const toHexoId = hexoid(25); | ||
const DEFAULT_OPTIONS = { | ||
@@ -21,13 +22,25 @@ maxFields: 1000, | ||
maxFileSize: 200 * 1024 * 1024, | ||
minFileSize: 1, | ||
allowEmptyFiles: true, | ||
keepExtensions: false, | ||
encoding: 'utf-8', | ||
hash: false, | ||
hashAlgorithm: false, | ||
uploadDir: os.tmpdir(), | ||
multiples: false, | ||
enabledPlugins: ['octetstream', 'querystring', 'multipart', 'json'], | ||
fileWriteStreamHandler: null, | ||
defaultInvalidName: 'invalid-name', | ||
filter: function () { | ||
return true; | ||
}, | ||
}; | ||
const File = require('./File'); | ||
const PersistentFile = require('./PersistentFile'); | ||
const VolatileFile = require('./VolatileFile'); | ||
const DummyParser = require('./parsers/Dummy'); | ||
const MultipartParser = require('./parsers/Multipart'); | ||
const errors = require('./FormidableError.js'); | ||
const { FormidableError } = errors; | ||
function hasOwnProp(obj, key) { | ||
@@ -40,15 +53,26 @@ return Object.prototype.hasOwnProperty.call(obj, key); | ||
super(); | ||
this.error = null; | ||
this.ended = false; | ||
this.options = { ...DEFAULT_OPTIONS, ...options }; | ||
this.uploadDir = this.uploadDir || os.tmpdir(); | ||
this.headers = null; | ||
this.type = null; | ||
const dir = path.resolve( | ||
this.options.uploadDir || this.options.uploaddir || os.tmpdir(), | ||
); | ||
this.bytesReceived = null; | ||
this.bytesExpected = null; | ||
this.uploaddir = dir; | ||
this.uploadDir = dir; | ||
this._parser = null; | ||
// initialize with null | ||
[ | ||
'error', | ||
'headers', | ||
'type', | ||
'bytesExpected', | ||
'bytesReceived', | ||
'_parser', | ||
].forEach((key) => { | ||
this[key] = null; | ||
}); | ||
this._setUpRename(); | ||
this._flushing = 0; | ||
@@ -60,9 +84,10 @@ this._fieldsSize = 0; | ||
const enabledPlugins = [] | ||
this.options.enabledPlugins = [] | ||
.concat(this.options.enabledPlugins) | ||
.filter(Boolean); | ||
if (enabledPlugins.length === 0) { | ||
throw new Error( | ||
if (this.options.enabledPlugins.length === 0) { | ||
throw new FormidableError( | ||
'expect at least 1 enabled builtin plugin, see options.enabledPlugins', | ||
errors.missingPlugin, | ||
); | ||
@@ -76,2 +101,4 @@ } | ||
}); | ||
this._setUpMaxFields(); | ||
} | ||
@@ -81,3 +108,6 @@ | ||
if (typeof plugin !== 'function') { | ||
throw new Error('.use: expect `plugin` to be a function'); | ||
throw new FormidableError( | ||
'.use: expect `plugin` to be a function', | ||
errors.pluginFunction, | ||
); | ||
} | ||
@@ -122,23 +152,17 @@ this._plugins.push(plugin.bind(this)); | ||
const fields = {}; | ||
let mockFields = ''; | ||
const files = {}; | ||
this.on('field', (name, value) => { | ||
// TODO: too much nesting | ||
if (this.options.multiples && name.slice(-2) === '[]') { | ||
const realName = name.slice(0, name.length - 2); | ||
if (hasOwnProp(fields, realName)) { | ||
if (!Array.isArray(fields[realName])) { | ||
fields[realName] = [fields[realName]]; | ||
} | ||
} else { | ||
fields[realName] = []; | ||
} | ||
fields[realName].push(value); | ||
if ( | ||
this.options.multiples && | ||
(this.type === 'multipart' || this.type === 'urlencoded') | ||
) { | ||
const mObj = { [name]: value }; | ||
mockFields = mockFields | ||
? `${mockFields}&${qs.stringify(mObj)}` | ||
: `${qs.stringify(mObj)}`; | ||
} else { | ||
fields[name] = value; | ||
} | ||
// if (name === 'simple') { | ||
// console.log('fields name!!', name); | ||
// console.log('fields value!!', value); | ||
// } | ||
}); | ||
@@ -159,7 +183,2 @@ this.on('file', (name, file) => { | ||
} | ||
// console.log('files!!', files); | ||
// if (name === 'simple') { | ||
// console.log('files name!!', name); | ||
// console.log('files value!!', file); | ||
// } | ||
}); | ||
@@ -170,2 +189,5 @@ this.on('error', (err) => { | ||
this.on('end', () => { | ||
if (this.options.multiples) { | ||
Object.assign(fields, qs.parse(mockFields)); | ||
} | ||
callback(null, fields, files); | ||
@@ -185,3 +207,3 @@ }); | ||
this.emit('aborted'); | ||
this._error(new Error('Request aborted')); | ||
this._error(new FormidableError('Request aborted', errors.aborted)); | ||
}) | ||
@@ -214,3 +236,9 @@ .on('data', (buffer) => { | ||
if (!this._parser) { | ||
this._error(new Error('not parser found')); | ||
this._error( | ||
new FormidableError( | ||
'no parser found', | ||
errors.noParser, | ||
415, // Unsupported Media Type | ||
), | ||
); | ||
return; | ||
@@ -229,3 +257,5 @@ } | ||
if (!this._parser) { | ||
this._error(new Error('uninitialized parser')); | ||
this._error( | ||
new FormidableError('uninitialized parser', errors.uninitializedParser), | ||
); | ||
return null; | ||
@@ -258,8 +288,13 @@ } | ||
_handlePart(part) { | ||
if (part.filename && typeof part.filename !== 'string') { | ||
this._error(new Error(`the part.filename should be string when exists`)); | ||
if (part.originalFilename && typeof part.originalFilename !== 'string') { | ||
this._error( | ||
new FormidableError( | ||
`the part.originalFilename should be string when it exists`, | ||
errors.filenameNotString, | ||
), | ||
); | ||
return; | ||
} | ||
// This MUST check exactly for undefined. You can not change it to !part.filename. | ||
// This MUST check exactly for undefined. You can not change it to !part.originalFilename. | ||
@@ -272,5 +307,5 @@ // todo: uncomment when switch tests to Jest | ||
// and such thing because code style | ||
// ? NOTE(@tunnckocore): or even better, if there is no mime, then it's for sure a field | ||
// ? NOTE(@tunnckocore): filename is an empty string when a field? | ||
if (!part.mime) { | ||
// ? NOTE(@tunnckocore): or even better, if there is no mimetype, then it's for sure a field | ||
// ? NOTE(@tunnckocore): originalFilename is an empty string when a field? | ||
if (!part.mimetype) { | ||
let value = ''; | ||
@@ -285,4 +320,6 @@ const decoder = new StringDecoder( | ||
this._error( | ||
new Error( | ||
`options.maxFieldsSize exceeded, received ${this._fieldsSize} bytes of field data`, | ||
new FormidableError( | ||
`options.maxFieldsSize (${this.options.maxFieldsSize} bytes) exceeded, received ${this._fieldsSize} bytes of field data`, | ||
errors.maxFieldsSizeExceeded, | ||
413, // Payload Too Large | ||
), | ||
@@ -301,9 +338,15 @@ ); | ||
if (!this.options.filter(part)) { | ||
return; | ||
} | ||
this._flushing += 1; | ||
const file = new File({ | ||
path: this._uploadPath(part.filename), | ||
name: part.filename, | ||
type: part.mime, | ||
hash: this.options.hash, | ||
const newFilename = this._getNewName(part); | ||
const filepath = this._joinDirectoryName(newFilename); | ||
const file = this._newFile({ | ||
newFilename, | ||
filepath, | ||
originalFilename: part.originalFilename, | ||
mimetype: part.mimetype, | ||
}); | ||
@@ -320,6 +363,18 @@ file.on('error', (err) => { | ||
this._fileSize += buffer.length; | ||
if (this._fileSize < this.options.minFileSize) { | ||
this._error( | ||
new FormidableError( | ||
`options.minFileSize (${this.options.minFileSize} bytes) inferior, received ${this._fileSize} bytes of file data`, | ||
errors.smallerThanMinFileSize, | ||
400, | ||
), | ||
); | ||
return; | ||
} | ||
if (this._fileSize > this.options.maxFileSize) { | ||
this._error( | ||
new Error( | ||
`options.maxFileSize exceeded, received ${this._fileSize} bytes of file data`, | ||
new FormidableError( | ||
`options.maxFileSize (${this.options.maxFileSize} bytes) exceeded, received ${this._fileSize} bytes of file data`, | ||
errors.biggerThanMaxFileSize, | ||
413, | ||
), | ||
@@ -339,2 +394,13 @@ ); | ||
part.on('end', () => { | ||
if (!this.options.allowEmptyFiles && this._fileSize === 0) { | ||
this._error( | ||
new FormidableError( | ||
`options.allowEmptyFiles is false, file size should be greather than 0`, | ||
errors.noEmptyFiles, | ||
400, | ||
), | ||
); | ||
return; | ||
} | ||
file.end(() => { | ||
@@ -356,3 +422,9 @@ this._flushing -= 1; | ||
if (!this.headers['content-type']) { | ||
this._error(new Error('bad content-type header, no content-type')); | ||
this._error( | ||
new FormidableError( | ||
'bad content-type header, no content-type', | ||
errors.missingContentType, | ||
400, | ||
), | ||
); | ||
return; | ||
@@ -375,4 +447,6 @@ } | ||
// there is no other better way, except a handle through options | ||
const error = new Error( | ||
const error = new FormidableError( | ||
`plugin on index ${idx} failed with: ${err.message}`, | ||
errors.pluginFailed, | ||
500, | ||
); | ||
@@ -416,4 +490,3 @@ error.idx = idx; | ||
this.openedFiles.forEach((file) => { | ||
file._writeStream.destroy(); | ||
setTimeout(fs.unlink, 0, file.path, () => {}); | ||
file.destroy(); | ||
}); | ||
@@ -440,7 +513,25 @@ } | ||
_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, | ||
}); | ||
} | ||
_getFileName(headerValue) { | ||
// matches either a quoted-string or a token (RFC 2616 section 19.5.1) | ||
const m = headerValue.match( | ||
// eslint-disable-next-line no-useless-escape | ||
/\bfilename=("(.*?)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))($|;\s)/i, | ||
/\bfilename=("(.*?)"|([^()<>{}[\]@,;:"?=\s/\t]+))($|;\s)/i, | ||
); | ||
@@ -450,24 +541,88 @@ if (!m) return null; | ||
const match = m[2] || m[3] || ''; | ||
let filename = match.substr(match.lastIndexOf('\\') + 1); | ||
filename = filename.replace(/%22/g, '"'); | ||
filename = filename.replace(/&#([\d]{4});/g, (_, code) => | ||
let originalFilename = match.substr(match.lastIndexOf('\\') + 1); | ||
originalFilename = originalFilename.replace(/%22/g, '"'); | ||
originalFilename = originalFilename.replace(/&#([\d]{4});/g, (_, code) => | ||
String.fromCharCode(code), | ||
); | ||
return filename; | ||
return originalFilename; | ||
} | ||
_uploadPath(filename) { | ||
const buf = crypto.randomBytes(16); | ||
let name = `upload_${buf.toString('hex')}`; | ||
_getExtension(str) { | ||
if (!str) { | ||
return ''; | ||
} | ||
if (this.options.keepExtensions) { | ||
let ext = path.extname(filename); | ||
ext = ext.replace(/(\.[a-z0-9]+).*/i, '$1'); | ||
const basename = path.basename(str); | ||
const firstDot = basename.indexOf('.'); | ||
const lastDot = basename.lastIndexOf('.'); | ||
const extname = path.extname(basename).replace(/(\.[a-z0-9]+).*/i, '$1'); | ||
name += ext; | ||
if (firstDot === lastDot) { | ||
return extname; | ||
} | ||
return path.join(this.uploadDir, name); | ||
return basename.slice(firstDot, lastDot) + extname; | ||
} | ||
_joinDirectoryName(name) { | ||
const newPath = path.join(this.uploadDir, name); | ||
// prevent directory traversal attacks | ||
if (!newPath.startsWith(this.uploadDir)) { | ||
return path.join(this.uploadDir, this.options.defaultInvalidName); | ||
} | ||
return newPath; | ||
} | ||
_setUpRename() { | ||
const hasRename = typeof this.options.filename === 'function'; | ||
if (hasRename) { | ||
this._getNewName = (part) => { | ||
let ext = ''; | ||
let name = this.options.defaultInvalidName; | ||
if (part.originalFilename) { | ||
// can be null | ||
({ ext, name } = path.parse(part.originalFilename)); | ||
if (this.options.keepExtensions !== true) { | ||
ext = ''; | ||
} | ||
} | ||
return this.options.filename.call(this, name, ext, part, this); | ||
}; | ||
} else { | ||
this._getNewName = (part) => { | ||
const name = toHexoId(); | ||
if (part && this.options.keepExtensions) { | ||
const originalFilename = typeof part === 'string' ? part : part.originalFilename; | ||
return `${name}${this._getExtension(originalFilename)}`; | ||
} | ||
return name; | ||
} | ||
} | ||
} | ||
_setUpMaxFields() { | ||
if (this.options.maxFields !== 0) { | ||
let fieldsCount = 0; | ||
this.on('field', () => { | ||
fieldsCount += 1; | ||
if (fieldsCount > this.options.maxFields) { | ||
this._error( | ||
new FormidableError( | ||
`options.maxFields (${this.options.maxFields}) exceeded`, | ||
errors.maxFieldsExceeded, | ||
413, | ||
), | ||
); | ||
} | ||
}); | ||
} | ||
} | ||
_maybeEnd() { | ||
@@ -474,0 +629,0 @@ // console.log('ended', this.ended); |
'use strict'; | ||
const File = require('./File'); | ||
const PersistentFile = require('./PersistentFile'); | ||
const VolatileFile = require('./VolatileFile'); | ||
const Formidable = require('./Formidable'); | ||
const FormidableError = require('./FormidableError'); | ||
@@ -14,3 +16,6 @@ const plugins = require('./plugins/index'); | ||
module.exports = Object.assign(formidable, { | ||
File, | ||
errors: FormidableError, | ||
File: PersistentFile, | ||
PersistentFile, | ||
VolatileFile, | ||
Formidable, | ||
@@ -17,0 +22,0 @@ formidable, |
@@ -0,0 +0,0 @@ /* eslint-disable no-underscore-dangle */ |
@@ -0,0 +0,0 @@ 'use strict'; |
@@ -0,0 +0,0 @@ /* eslint-disable no-underscore-dangle */ |
@@ -9,3 +9,6 @@ /* eslint-disable no-fallthrough */ | ||
const { Transform } = require('stream'); | ||
const errors = require('../FormidableError.js'); | ||
const { FormidableError } = errors; | ||
let s = 0; | ||
@@ -63,3 +66,3 @@ const STATE = { | ||
_final(done) { | ||
_flush(done) { | ||
if ( | ||
@@ -74,4 +77,6 @@ (this.state === STATE.HEADER_FIELD_START && this.index === 0) || | ||
done( | ||
new Error( | ||
new FormidableError( | ||
`MultipartParser.end(): stream ended unexpectedly: ${this.explain()}`, | ||
errors.malformedMultipart, | ||
400, | ||
), | ||
@@ -114,3 +119,3 @@ ); | ||
const setMark = (name, idx) => { | ||
this[`${name}Mark`] = idx || i; | ||
this[`${name}Mark`] = typeof idx === 'number' ? idx : i; | ||
}; | ||
@@ -130,6 +135,6 @@ | ||
this._handleCallback(name, buffer, this[markSymbol], buffer.length); | ||
setMark(markSymbol, 0); | ||
setMark(name, 0); | ||
} else { | ||
this._handleCallback(name, buffer, this[markSymbol], i); | ||
clearMarkSymbol(markSymbol); | ||
clearMarkSymbol(name); | ||
} | ||
@@ -136,0 +141,0 @@ }; |
@@ -0,0 +0,0 @@ 'use strict'; |
@@ -14,3 +14,2 @@ /* eslint-disable no-underscore-dangle */ | ||
this.globalOptions = { ...options }; | ||
this.maxKeys = this.globalOptions.maxFields; | ||
this.buffer = ''; | ||
@@ -27,5 +26,3 @@ this.bufferLength = 0; | ||
_flush(callback) { | ||
const fields = querystring.parse(this.buffer, '&', '=', { | ||
maxKeys: this.maxKeys, | ||
}); | ||
const fields = querystring.parse(this.buffer, '&', '='); | ||
// eslint-disable-next-line no-restricted-syntax, guard-for-in | ||
@@ -32,0 +29,0 @@ for (const key in fields) { |
@@ -0,0 +0,0 @@ 'use strict'; |
@@ -0,0 +0,0 @@ /* eslint-disable no-underscore-dangle */ |
@@ -7,3 +7,6 @@ /* eslint-disable no-underscore-dangle */ | ||
const MultipartParser = require('../parsers/Multipart'); | ||
const errors = require('../FormidableError.js'); | ||
const { FormidableError } = errors; | ||
// the `options` is also available through the `options` / `formidable.options` | ||
@@ -17,3 +20,6 @@ module.exports = function plugin(formidable, options) { | ||
if (/multipart\/form-data/i.test(self.headers['content-type'])) { | ||
// NOTE: we (currently) support both multipart/form-data and multipart/related | ||
const multipart = /multipart/i.test(self.headers['content-type']); | ||
if (multipart) { | ||
const m = self.headers['content-type'].match( | ||
@@ -24,5 +30,9 @@ /boundary=(?:"([^"]+)"|([^;]+))/i, | ||
const initMultipart = createInitMultipart(m[1] || m[2]); | ||
initMultipart.call(self, self, options); | ||
initMultipart.call(self, self, options); // lgtm [js/superfluous-trailing-arguments] | ||
} else { | ||
const err = new Error('bad content-type header, no multipart boundary'); | ||
const err = new FormidableError( | ||
'bad content-type header, no multipart boundary', | ||
errors.missingMultipartBoundary, | ||
400, | ||
); | ||
self._error(err); | ||
@@ -54,6 +64,6 @@ } | ||
part.name = null; | ||
part.filename = null; | ||
part.mime = null; | ||
part.originalFilename = null; | ||
part.mimetype = null; | ||
part.transferEncoding = 'binary'; | ||
part.transferEncoding = this.options.encoding; | ||
part.transferBuffer = ''; | ||
@@ -81,5 +91,5 @@ | ||
part.filename = this._getFileName(headerValue); | ||
part.originalFilename = this._getFileName(headerValue); | ||
} else if (headerField === 'content-type') { | ||
part.mime = headerValue; | ||
part.mimetype = headerValue; | ||
} else if (headerField === 'content-transfer-encoding') { | ||
@@ -95,3 +105,4 @@ part.transferEncoding = headerValue.toLowerCase(); | ||
case '7bit': | ||
case '8bit': { | ||
case '8bit': | ||
case 'utf-8': { | ||
const dataPropagation = (ctx) => { | ||
@@ -150,3 +161,9 @@ if (ctx.name === 'partData') { | ||
default: | ||
return this._error(new Error('unknown transfer-encoding')); | ||
return this._error( | ||
new FormidableError( | ||
'unknown transfer-encoding', | ||
errors.unknownTransferEncoding, | ||
501, | ||
), | ||
); | ||
} | ||
@@ -153,0 +170,0 @@ |
@@ -5,3 +5,2 @@ /* eslint-disable no-underscore-dangle */ | ||
const File = require('../File'); | ||
const OctetStreamParser = require('../parsers/OctetStream'); | ||
@@ -29,13 +28,19 @@ | ||
this.type = 'octet-stream'; | ||
const filename = this.headers['x-file-name']; | ||
const mime = this.headers['content-type']; | ||
const originalFilename = this.headers['x-file-name']; | ||
const mimetype = this.headers['content-type']; | ||
const file = new File({ | ||
path: this._uploadPath(filename), | ||
name: filename, | ||
type: mime, | ||
hash: this.options.hash, | ||
const thisPart = { | ||
originalFilename, | ||
mimetype, | ||
}; | ||
const newFilename = this._getNewName(thisPart); | ||
const filepath = this._joinDirectoryName(newFilename); | ||
const file = this._newFile({ | ||
newFilename, | ||
filepath, | ||
originalFilename, | ||
mimetype, | ||
}); | ||
this.emit('fileBegin', filename, file); | ||
this.emit('fileBegin', originalFilename, file); | ||
file.open(); | ||
@@ -42,0 +47,0 @@ this.openedFiles.push(file); |
@@ -0,0 +0,0 @@ /* eslint-disable no-underscore-dangle */ |
Sorry, the diff of this file is not supported yet
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
0
0
827
2
0
99959
4
20
22
1547
+ Addedhexoid@1.0.0
+ Addedqs@6.9.3
+ Addeddezalgo@1.0.3(transitive)
+ Addedhexoid@1.0.0(transitive)
+ Addedqs@6.9.3(transitive)
- Removeddezalgo@1.0.4(transitive)
Updateddezalgo@1.0.3
Updatedonce@1.4.0