Skip to content

Commit beb36d1

Browse files
feat: add option to store files or not
1 parent 76ad4ae commit beb36d1

File tree

9 files changed

+221
-22
lines changed

9 files changed

+221
-22
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ See it's defaults in [src/Formidable.js DEFAULT_OPTIONS](./src/Formidable.js) (t
334334
for incoming files, set this to some hash algorithm, see
335335
[crypto.createHash](https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm_options)
336336
for available algorithms
337+
- `options.storeFiles` **{boolean}** - default `true`; to store uploaded file(s)
338+
in `uploadDir` on host machine or not and only parse the file(s).
337339
- `options.multiples` **{boolean}** - default `false`; when you call the
338340
`.parse` method, the `files` argument (of the callback) will contain arrays of
339341
files for inputs which submit multiple files using the HTML5 `multiple`

src/Formidable.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ const DEFAULT_OPTIONS = {
2424
encoding: 'utf-8',
2525
hash: false,
2626
uploadDir: os.tmpdir(),
27+
storeFiles: true,
2728
multiples: false,
2829
enabledPlugins: ['octetstream', 'querystring', 'multipart', 'json'],
2930
};
3031

31-
const File = require('./File');
32+
const PersistentFile = require('./PersistentFile');
33+
const VolatileFile = require('./VolatileFile');
3234
const DummyParser = require('./parsers/Dummy');
3335
const MultipartParser = require('./parsers/Multipart');
3436

@@ -138,11 +140,11 @@ class IncomingForm extends EventEmitter {
138140
const fields = {};
139141
let mockFields = '';
140142
const files = {};
141-
143+
142144
this.on('field', (name, value) => {
143145
if (this.options.multiples) {
144-
let mObj = { [name] : value };
145-
mockFields = mockFields + '&' + qs.stringify(mObj);
146+
const mObj = { [name]: value };
147+
mockFields = `${mockFields}&${qs.stringify(mObj)}`;
146148
} else {
147149
fields[name] = value;
148150
}
@@ -295,11 +297,10 @@ class IncomingForm extends EventEmitter {
295297

296298
this._flushing += 1;
297299

298-
const file = new File({
300+
const file = this._newFile({
299301
path: this._rename(part),
300-
name: part.filename,
301-
type: part.mime,
302-
hash: this.options.hash,
302+
filename: part.filename,
303+
mime: part.mime,
303304
});
304305
file.on('error', (err) => {
305306
this._error(err);
@@ -420,7 +421,7 @@ class IncomingForm extends EventEmitter {
420421

421422
if (Array.isArray(this.openedFiles)) {
422423
this.openedFiles.forEach((file) => {
423-
file._writeStream.destroy();
424+
file.destroy();
424425
setTimeout(fs.unlink, 0, file.path, () => {});
425426
});
426427
}
@@ -443,6 +444,21 @@ class IncomingForm extends EventEmitter {
443444
return new MultipartParser(this.options);
444445
}
445446

447+
_newFile({ path: filePath, filename: name, mime: type }) {
448+
return this.options.storeFiles
449+
? new PersistentFile({
450+
path: filePath,
451+
name,
452+
type,
453+
hash: this.options.hash,
454+
})
455+
: new VolatileFile({
456+
name,
457+
type,
458+
hash: this.options.hash,
459+
});
460+
}
461+
446462
_getFileName(headerValue) {
447463
// matches either a quoted-string or a token (RFC 2616 section 19.5.1)
448464
const m = headerValue.match(

src/File.js renamed to src/PersistentFile.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const fs = require('fs');
66
const crypto = require('crypto');
77
const { EventEmitter } = require('events');
88

9-
class File extends EventEmitter {
9+
class PersistentFile extends EventEmitter {
1010
constructor(properties) {
1111
super();
1212

@@ -56,7 +56,7 @@ class File extends EventEmitter {
5656
}
5757

5858
toString() {
59-
return `File: ${this.name}, Path: ${this.path}`;
59+
return `PersistentFile: ${this.name}, Path: ${this.path}`;
6060
}
6161

6262
write(buffer, cb) {
@@ -86,6 +86,10 @@ class File extends EventEmitter {
8686
cb();
8787
});
8888
}
89+
90+
destroy() {
91+
this._writeStream.destroy();
92+
}
8993
}
9094

91-
module.exports = File;
95+
module.exports = PersistentFile;

src/VolatileFile.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* eslint-disable no-underscore-dangle */
2+
3+
'use strict';
4+
5+
const crypto = require('crypto');
6+
const { EventEmitter } = require('events');
7+
8+
class VolatileFile extends EventEmitter {
9+
constructor(properties) {
10+
super();
11+
12+
this.size = 0;
13+
this.name = null;
14+
this.type = null;
15+
this.hash = null;
16+
17+
// eslint-disable-next-line guard-for-in, no-restricted-syntax
18+
for (const key in properties) {
19+
this[key] = properties[key];
20+
}
21+
22+
if (typeof this.hash === 'string') {
23+
this.hash = crypto.createHash(properties.hash);
24+
} else {
25+
this.hash = null;
26+
}
27+
}
28+
29+
// eslint-disable-next-line class-methods-use-this
30+
open() {}
31+
32+
// eslint-disable-next-line class-methods-use-this
33+
destroy() {}
34+
35+
toJSON() {
36+
const json = {
37+
size: this.size,
38+
name: this.name,
39+
type: this.type,
40+
length: this.length,
41+
filename: this.filename,
42+
mime: this.mime,
43+
};
44+
if (this.hash && this.hash !== '') {
45+
json.hash = this.hash;
46+
}
47+
return json;
48+
}
49+
50+
toString() {
51+
return `VolatileFile: ${this.name}`;
52+
}
53+
54+
write(buffer, cb) {
55+
if (this.hash) {
56+
this.hash.update(buffer);
57+
}
58+
59+
this.size += buffer.length;
60+
this.emit('progress', this.size);
61+
cb();
62+
}
63+
64+
end(cb) {
65+
if (this.hash) {
66+
this.hash = this.hash.digest('hex');
67+
}
68+
this.emit('end');
69+
cb();
70+
}
71+
}
72+
73+
module.exports = VolatileFile;

src/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

3-
const File = require('./File');
3+
const PersistentFile = require('./PersistentFile');
4+
const VolatileFile = require('./VolatileFile');
45
const Formidable = require('./Formidable');
56

67
const plugins = require('./plugins/index');
@@ -11,7 +12,9 @@ const parsers = require('./parsers/index');
1112
const formidable = (...args) => new Formidable(...args);
1213

1314
module.exports = Object.assign(formidable, {
14-
File,
15+
File: PersistentFile,
16+
PersistentFile,
17+
VolatileFile,
1518
Formidable,
1619
formidable,
1720

src/plugins/octetstream.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
'use strict';
44

5-
const File = require('../File');
65
const OctetStreamParser = require('../parsers/OctetStream');
76

87
// the `options` is also available through the `options` / `formidable.options`
@@ -28,11 +27,10 @@ function init(_self, _opts) {
2827
const filename = this.headers['x-file-name'];
2928
const mime = this.headers['content-type'];
3029

31-
const file = new File({
30+
const file = this._newFile({
3231
path: this._uploadPath(filename),
33-
name: filename,
34-
type: mime,
35-
hash: this.options.hash,
32+
filename,
33+
mime,
3634
});
3735

3836
this.emit('fileBegin', filename, file);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const os = require('os');
5+
const http = require('http');
6+
const path = require('path');
7+
const assert = require('assert');
8+
9+
const formidable = require('../../src/index');
10+
11+
const PORT = 13532;
12+
const UPLOAD_DIR = path.join(os.tmpdir(), 'test-store-files-option');
13+
const testFilePath = path.join(
14+
path.dirname(__dirname),
15+
'fixture',
16+
'file',
17+
'binaryfile.tar.gz',
18+
);
19+
20+
const server = http.createServer((req, res) => {
21+
if (!fs.existsSync(UPLOAD_DIR)) {
22+
fs.mkdirSync(UPLOAD_DIR);
23+
}
24+
const form = formidable({ storeFiles: false, uploadDir: UPLOAD_DIR });
25+
26+
form.parse(req, (err, fields, files) => {
27+
assert.strictEqual(Object.keys(files).length, 1);
28+
const { file } = files;
29+
30+
assert.strictEqual(file.size, 301);
31+
assert.ok(file.path === undefined);
32+
33+
const dirFiles = fs.readdirSync(UPLOAD_DIR);
34+
assert.ok(dirFiles.length === 0);
35+
36+
res.end();
37+
server.close();
38+
});
39+
});
40+
41+
server.listen(PORT, (err) => {
42+
const choosenPort = server.address().port;
43+
const url = `http://localhost:${choosenPort}`;
44+
console.log('Server up and running at:', url);
45+
46+
assert(!err, 'should not have error, but be falsey');
47+
48+
const request = http.request({
49+
port: PORT,
50+
method: 'POST',
51+
headers: {
52+
'Content-Type': 'application/octet-stream',
53+
},
54+
});
55+
56+
fs.createReadStream(testFilePath).pipe(request);
57+
});

test/unit/file.test.js renamed to test/unit/persistent-file.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use strict';
22

3-
const { File } = require('../../src/index');
3+
const PersistentFile = require('../../src/PersistentFile');
44

55
const now = new Date();
6-
const file = new File({
6+
const file = new PersistentFile({
77
size: 1024,
88
path: '/tmp/cat.png',
99
name: 'cat.png',
@@ -13,7 +13,7 @@ const file = new File({
1313
mime: 'image/png',
1414
});
1515

16-
test('File#toJSON()', () => {
16+
test('PersistentFile#toJSON()', () => {
1717
const obj = file.toJSON();
1818
const len = Object.keys(obj).length;
1919

test/unit/volatile-file.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict';
2+
3+
const VolatileFile = require('../../src/VolatileFile');
4+
5+
const file = new VolatileFile({
6+
size: 1024,
7+
name: 'cat.png',
8+
type: 'image/png',
9+
filename: 'cat.png',
10+
mime: 'image/png',
11+
});
12+
13+
test('VolatileFile#toJSON()', () => {
14+
const obj = file.toJSON();
15+
const len = Object.keys(obj).length;
16+
17+
expect(obj.size).toBe(1024);
18+
expect(obj.name).toBe('cat.png');
19+
expect(obj.type).toBe('image/png');
20+
expect(obj.mime).toBe('image/png');
21+
expect(obj.filename).toBe('cat.png');
22+
expect(6).toBe(len);
23+
});
24+
25+
test('VolatileFile#toString()', () => {
26+
expect(file.toString()).toBe('VolatileFile: cat.png');
27+
});
28+
29+
test('VolatileFile#write()', (done) => {
30+
file.on('progress', (size) => {
31+
expect(size).toBe(1029);
32+
});
33+
34+
const buffer = Buffer.alloc(5);
35+
file.write(buffer, () => {
36+
done();
37+
});
38+
});
39+
40+
test('VolatileFile#end()', (done) => {
41+
const fileEmitSpy = jest.spyOn(file, 'emit');
42+
43+
file.end(() => done());
44+
45+
expect(fileEmitSpy).toBeCalledWith('end');
46+
});

0 commit comments

Comments
 (0)