diff --git a/example/largeImage.jpg b/example/largeImage.jpg new file mode 100644 index 0000000..6c5d403 Binary files /dev/null and b/example/largeImage.jpg differ diff --git a/lib/storage-handler.js b/lib/storage-handler.js index af964b7..b5bfcdf 100644 --- a/lib/storage-handler.js +++ b/lib/storage-handler.js @@ -1,6 +1,10 @@ var IncomingForm = require('formidable'); var StringDecoder = require('string_decoder').StringDecoder; +var defaultOptions = { + maxFileSize: 10 * 1024 * 1024 // 10 MB +}; + /** * Handle multipart/form-data upload to the storage service * @param {Object} provider The storage service provider @@ -16,6 +20,10 @@ exports.upload = function (provider, req, res, options, cb) { options = {}; } + if (!options.maxFileSize) { + options.maxFileSize = defaultOptions.maxFileSize; + } + var form = new IncomingForm(this.options); container = options.container || req.params.container; var fields = {}, files = {}; @@ -56,18 +64,55 @@ exports.upload = function (provider, req, res, options, cb) { type: part.mime }; + // Options for this file + + // Build a filename if ('function' === typeof options.getFilename) { file.name = options.getFilename(file, req, res); } - self.emit('fileBegin', part.name, file); - - var headers = {}; - if ('content-type' in part.headers) { - headers['content-type'] = part.headers['content-type']; + // Get allowed mime types + if (options.allowedContentTypes) { + var allowedContentTypes; + if ('function' === typeof options.allowedContentTypes) { + allowedContentTypes = options.allowedContentTypes(file, req, res); + } else { + allowedContentTypes = options.allowedContentTypes; + } + if (Array.isArray(allowedContentTypes) && allowedContentTypes.length !== 0) { + if (allowedContentTypes.indexOf(file.type) === -1) { + self._error(new Error('contentType "' + file.type + '" is not allowed (Must be in [' + allowedContentTypes.join(', ') + '])')); + return; + } + } } - var writer = provider.upload({container: container, remote: file.name}); + // Get max file size + var maxFileSize; + if (options.maxFileSize) { + if ('function' === typeof options.maxFileSize) { + maxFileSize = options.maxFileSize(file, req, res); + } else { + maxFileSize = options.maxFileSize; + } + } + + // Get access control list + if (options.acl) { + if ('function' === typeof options.acl) { + file.acl = options.acl(file, req, res); + } else { + file.acl = options.acl; + } + } + + self.emit('fileBegin', part.name, file); + + var uploadParams = {container: container, remote: file.name, contentType: file.type}; + if (file.acl) { + uploadParams.acl = file.acl; + } + var writer = provider.upload(uploadParams); var endFunc = function () { self._flushing--; @@ -82,21 +127,19 @@ exports.upload = function (provider, req, res, options, cb) { self._maybeEnd(); }; - /* - part.on('data', function (buffer) { - self.pause(); - writer.write(buffer, function () { - // pkgcloud stream doesn't make callbacks - }); - self.resume(); - }); - - part.on('end', function () { - - writer.end(); // pkgcloud stream doesn't make callbacks - endFunc(); - }); - */ + var fileSize = 0; + if (maxFileSize) { + part.on('data', function (buffer) { + fileSize += buffer.length; + if (fileSize > maxFileSize) { + // We are missing some way to tell the provider to cancel upload/multipart upload of the current file. + // - s3-upload-stream doesn't provide a way to do this in it's public interface + // - We could call provider.delete file but it would not delete multipart data + self._error(new Error('maxFileSize exceeded, received ' + fileSize + ' bytes of field data (max is ' + maxFileSize + ')')); + return; + } + }); + } part.pipe(writer, { end: false }); part.on("end", function () { diff --git a/lib/storage-service.js b/lib/storage-service.js index 79d96cd..8483729 100644 --- a/lib/storage-service.js +++ b/lib/storage-service.js @@ -27,9 +27,20 @@ function StorageService(options) { } this.provider = options.provider; this.client = factory.createClient(options); + if ('function' === typeof options.getFilename) { this.getFilename = options.getFilename; } + if (options.acl) { + this.acl = options.acl; + } + if (options.allowedContentTypes) { + this.allowedContentTypes = options.allowedContentTypes; + } + if (options.maxFileSize) { + this.maxFileSize = options.maxFileSize; + } + } function map(obj) { @@ -218,6 +229,15 @@ StorageService.prototype.upload = function(req, res, options, cb) { if (this.getFilename && !options.getFilename) { options.getFilename = this.getFilename; } + if (this.acl && !options.acl) { + options.acl = this.acl; + } + if (this.allowedContentTypes && !options.allowedContentTypes) { + options.allowedContentTypes = this.allowedContentTypes; + } + if (this.maxFileSize && !options.maxFileSize) { + options.maxFileSize = this.maxFileSize; + } return handler.upload(this.client, req, res, options, cb); }; diff --git a/test/images/album1/.gitignore b/test/images/album1/.gitignore index ac4f9b5..9df892a 100644 --- a/test/images/album1/.gitignore +++ b/test/images/album1/.gitignore @@ -1,2 +1,2 @@ test.jpg -image-test.jpg +image-*.jpg diff --git a/test/upload-download.test.js b/test/upload-download.test.js index 1e198e4..ef276d0 100644 --- a/test/upload-download.test.js +++ b/test/upload-download.test.js @@ -15,7 +15,10 @@ var dsImage = loopback.createDataSource({ getFilename: function(fileInfo) { return 'image-' + fileInfo.name; - } + }, + acl: 'public-read', + allowedContentTypes: ['image/png', 'image/jpeg'], + maxFileSize: 5 * 1024 * 1024 }); var ImageContainer = dsImage.createModel('imageContainer'); @@ -175,12 +178,40 @@ describe('storage service', function () { .expect('Content-Type', /json/) .expect(200, function (err, res) { assert.deepEqual(res.body, {"result": {"files": {"image": [ - {"container": "album1", "name": "image-test.jpg", "type": "image/jpeg"} + {"container": "album1", "name": "image-test.jpg", "type": "image/jpeg", "acl":"public-read"} ]}, "fields": {}}}); done(); }); }); + it('uploads file wrong content type', function (done) { + + request('http://localhost:3000') + .post('/imageContainers/album1/upload') + .attach('image', path.join(__dirname, '../example/app.js')) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert(err); + assert(res.body.error.message.indexOf('is not allowed') !== -1); + done(); + }); + }); + + it('uploads file too large', function (done) { + + request('http://localhost:3000') + .post('/imageContainers/album1/upload') + .attach('image', path.join(__dirname, '../example/largeImage.jpg')) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200, function (err, res) { + assert(err); + assert(res.body.error.message.indexOf('maxFileSize exceeded') !== -1); + done(); + }); + }); + it('should get file by name', function (done) { request('http://localhost:3000')