Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 36e3f1b

Browse files
committed
feat(file): allow file instance to take validation options
validation options allows to define file size and extension
1 parent 3003d9b commit 36e3f1b

File tree

4 files changed

+223
-24
lines changed

4 files changed

+223
-24
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
},
3939
"dependencies": {
4040
"bcryptjs": "^2.3.0",
41+
"bytes": "^2.4.0",
4142
"cat-log": "^1.0.2",
4243
"co": "^4.6.0",
4344
"dotenv": "^2.0.0",

src/File/index.js

Lines changed: 131 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
const path = require('path')
1313
const fs = require('fs')
14+
const bytes = require('bytes')
1415

1516
/**
1617
* Used by request object internally to manage file uploads.
@@ -21,11 +22,123 @@ const fs = require('fs')
2122
*/
2223
class File {
2324

24-
constructor (formidableObject) {
25+
constructor (formidableObject, options) {
26+
options = options || {}
2527
this.file = formidableObject
2628
this.file.error = null
27-
this.file.filename = ''
28-
this.file.filepath = ''
29+
this.file.fileName = ''
30+
this.file.maxSize = options.maxSize ? bytes(options.maxSize) : null
31+
this.file.allowedExtensions = options.allowedExtensions || []
32+
this.file.filePath = ''
33+
}
34+
35+
/**
36+
* sets error on the file instance and clears
37+
* the file name and path
38+
*
39+
* @param {String} error
40+
*
41+
* @private
42+
*/
43+
_setError (error) {
44+
this.file.error = error
45+
this.file.fileName = ''
46+
this.file.filePath = ''
47+
}
48+
49+
/**
50+
* sets filePath and name after the move
51+
* and clears the error.
52+
*
53+
* @param {String} fileName
54+
* @param {String} filePath
55+
*
56+
* @private
57+
*/
58+
_setUploadedFile (fileName, filePath) {
59+
this.file.error = null
60+
this.file.fileName = fileName
61+
this.file.filePath = filePath
62+
}
63+
64+
/**
65+
* sets file size exceeds error
66+
*
67+
* @private
68+
*/
69+
_setFileSizeExceedsError () {
70+
this._setError(`Uploaded file size ${bytes(this.clientSize())} exceeds the limit of ${bytes(this.file.maxSize)}`)
71+
}
72+
73+
/**
74+
* sets file size extension error
75+
*
76+
* @private
77+
*/
78+
_setFileExtensionError () {
79+
this._setError(`Uploaded file extension ${this.extension()} is not valid`)
80+
}
81+
82+
/**
83+
* validates the file size
84+
*
85+
* @return {Boolean}
86+
*
87+
* @private
88+
*/
89+
_underAllowedSize () {
90+
return !this.file.maxSize || (this.clientSize() <= this.file.maxSize)
91+
}
92+
93+
/**
94+
* returns whether file has one of the defined extension
95+
* or not.
96+
*
97+
* @return {Boolean} [description]
98+
*
99+
* @private
100+
*/
101+
_hasValidExtension () {
102+
return !this.file.allowedExtensions.length || this.file.allowedExtensions.indexOf(this.extension()) > -1
103+
}
104+
105+
/**
106+
* a method to validate a given file.
107+
*
108+
* @return {Boolean}
109+
*/
110+
validate () {
111+
if (!this._hasValidExtension()) {
112+
this._setFileExtensionError()
113+
return false
114+
} else if (!this._underAllowedSize()) {
115+
this._setFileSizeExceedsError()
116+
return false
117+
}
118+
return true
119+
}
120+
121+
/**
122+
* validates the file size and move it to the destination
123+
*
124+
* @param {String} fileName
125+
* @param {String} completePath
126+
*
127+
* @return {Promise}
128+
*
129+
* @private
130+
*/
131+
_validateAndMove (fileName, completePath) {
132+
return new Promise((resolve) => {
133+
if (!this.validate()) {
134+
resolve()
135+
return
136+
}
137+
fs.rename(this.tmpPath(), completePath, (error) => {
138+
error ? this._setError(error) : this._setUploadedFile(fileName, completePath)
139+
resolve()
140+
})
141+
})
29142
}
30143

31144
/**
@@ -43,20 +156,7 @@ class File {
43156
move (toPath, name) {
44157
name = name || this.clientName()
45158
const uploadingFileName = `${toPath}/${name}`
46-
return new Promise((resolve) => {
47-
fs.rename(this.tmpPath(), uploadingFileName, (err) => {
48-
if (err) {
49-
this.file.error = err
50-
this.file.filename = ''
51-
this.file.filepath = ''
52-
} else {
53-
this.file.error = null
54-
this.file.filename = name
55-
this.file.filepath = uploadingFileName
56-
}
57-
resolve()
58-
})
59-
})
159+
return this._validateAndMove(name, uploadingFileName)
60160
}
61161

62162
/**
@@ -122,7 +222,7 @@ class File {
122222
* @public
123223
*/
124224
uploadName () {
125-
return this.file.filename
225+
return this.file.fileName
126226
}
127227

128228
/**
@@ -133,7 +233,7 @@ class File {
133233
* @public
134234
*/
135235
uploadPath () {
136-
return this.file.filepath
236+
return this.file.filePath
137237
}
138238

139239
/**
@@ -169,6 +269,18 @@ class File {
169269
return this.file.error
170270
}
171271

272+
/**
273+
* returns the JSON representation of the
274+
* file instance.
275+
*
276+
* @return {Object}
277+
*
278+
* @public
279+
*/
280+
toJSON () {
281+
return this.file
282+
}
283+
172284
}
173285

174286
module.exports = File

src/Request/index.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -463,12 +463,13 @@ class Request {
463463
* if already is not an instance
464464
*
465465
* @param {Object} file
466+
* @param {Object} [options]
466467
* @return {Object}
467468
* @private
468469
*/
469-
_toFileInstance (file) {
470+
_toFileInstance (file, options) {
470471
if (!(file instanceof File)) {
471-
file = new File(file)
472+
file = new File(file, options)
472473
}
473474
return file
474475
}
@@ -478,13 +479,14 @@ class Request {
478479
* @instance Request.file
479480
*
480481
* @param {String} key
482+
* @param {Objecr} [options]
481483
* @return {Object}
482484
*
483485
* @example
484486
* request.file('avatar')
485487
* @public
486488
*/
487-
file (key) {
489+
file (key, options) {
488490
/**
489491
* if requested file was not uploaded return an
490492
* empty instance of file object.
@@ -504,9 +506,9 @@ class Request {
504506
* file instance
505507
*/
506508
if (_.isArray(fileToReturn)) {
507-
return _.map(fileToReturn, (file) => this._toFileInstance(file.toJSON()))
509+
return _.map(fileToReturn, (file) => this._toFileInstance(file.toJSON(), options))
508510
}
509-
return this._toFileInstance(fileToReturn.toJSON())
511+
return this._toFileInstance(fileToReturn.toJSON(), options)
510512
}
511513

512514
/**

test/unit/request.spec.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,90 @@ describe('Request', function () {
705705
expect(res.body.logo2).to.equal(true)
706706
})
707707

708+
it('should be able to define max size for a given file', function * () {
709+
const server = http.createServer(function (req, res) {
710+
var form = new formidable.IncomingForm({multiples: true});
711+
const request = new Request(req, res, Config)
712+
form.parse(req, function(err, fields, files) {
713+
request._files = files
714+
const logo = request.file('logo', {maxSize: '1kb'})
715+
res.writeHead(200, {"Content-type":"application/json"})
716+
res.end(JSON.stringify({logo: logo.toJSON()}), 'utf8')
717+
})
718+
})
719+
720+
const res = yield supertest(server).get("/")
721+
.attach('logo',__dirname+'/uploads/npm-logo.svg')
722+
.expect(200)
723+
.end()
724+
expect(res.body.logo.maxSize).to.equal(1024)
725+
})
726+
727+
it('should be able to define allowed extensions for a given file', function * () {
728+
const server = http.createServer(function (req, res) {
729+
var form = new formidable.IncomingForm({multiples: true});
730+
const request = new Request(req, res, Config)
731+
form.parse(req, function(err, fields, files) {
732+
request._files = files
733+
const logo = request.file('logo', {allowedExtensions: ['jpg']})
734+
res.writeHead(200, {"Content-type":"application/json"})
735+
res.end(JSON.stringify({logo: logo.toJSON()}), 'utf8')
736+
})
737+
})
738+
739+
const res = yield supertest(server).get("/")
740+
.attach('logo',__dirname+'/uploads/npm-logo.svg')
741+
.expect(200)
742+
.end()
743+
expect(res.body.logo.allowedExtensions).deep.equal(['jpg'])
744+
})
745+
746+
it('should return error when trying to move a file of larger size', function * () {
747+
const server = http.createServer(function (req, res) {
748+
var form = new formidable.IncomingForm({multiples: true});
749+
const request = new Request(req, res, Config)
750+
form.parse(req, function(err, fields, files) {
751+
request._files = files
752+
const logo = request.file('logo', {maxSize: '100b'})
753+
logo
754+
.move()
755+
.then(() => {
756+
res.writeHead(200, {"Content-type":"application/json"})
757+
res.end(JSON.stringify({logo: logo.toJSON()}), 'utf8')
758+
})
759+
})
760+
})
761+
762+
const res = yield supertest(server).get("/")
763+
.attach('logo',__dirname+'/uploads/npm-logo.svg')
764+
.expect(200)
765+
.end()
766+
expect(res.body.logo.error).to.equal('Uploaded file size 235B exceeds the limit of 100B')
767+
})
768+
769+
it('should return error when trying to move a file of invalid extension', function * () {
770+
const server = http.createServer(function (req, res) {
771+
var form = new formidable.IncomingForm({multiples: true});
772+
const request = new Request(req, res, Config)
773+
form.parse(req, function(err, fields, files) {
774+
request._files = files
775+
const logo = request.file('logo', {allowedExtensions: ['jpg']})
776+
logo
777+
.move()
778+
.then(() => {
779+
res.writeHead(200, {"Content-type":"application/json"})
780+
res.end(JSON.stringify({logo: logo.toJSON()}), 'utf8')
781+
})
782+
})
783+
})
784+
785+
const res = yield supertest(server).get("/")
786+
.attach('logo',__dirname+'/uploads/npm-logo.svg')
787+
.expect(200)
788+
.end()
789+
expect(res.body.logo.error).to.equal('Uploaded file extension svg is not valid')
790+
})
791+
708792
it('should return true when a pattern matches the current route url', function * () {
709793
const server = http.createServer(function (req, res) {
710794
const request = new Request(req, res, Config)

0 commit comments

Comments
 (0)