diff --git a/README.md b/README.md index c9e6dd2..51170db 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,72 @@ var upload = multer({ ``` You may also use a function as the `contentEncoding`, which should be of the form `function(req, file, cb)`. +## Transforming Files Before Upload + +The optional `shouldTransform` option tells multer whether it should transform the file before it is uploaded. By default, it is set to `false`. If set to `true`, `transformers` option must be added, which tells how to transform the file. `transformers` option should be an `Array`, containing objects with can have properties `id`, `key` and `transform`. + +```javascript +var upload = multer({ + storage: multerS3({ + s3: s3, + bucket: 'some-bucket', + shouldTransform: function (req, file, cb) { + cb(null, /^image/i.test(file.mimetype)) + }, + transformers: [{ + id: 'original', + key: function (req, file, cb) { + cb(null, 'image-original.jpg') + }, + transform: function (req, file, cb) { + cb(null, sharp().jpeg()) + } + }, { + id: 'thumbnail', + key: function (req, file, cb) { + cb(null, 'image-thumbnail.jpg') + }, + transform: function (req, file, cb) { + cb(null, sharp().resize(100, 100).jpeg()) + } + }] + }) +}) +``` +If this option is used, each file passed to your router request will have a `transformations` object, with every transform you defined. +```json +{ + "fieldname":"image", + "originalname":"image.jpg", + "encoding":"7bit", + "mimetype":"image/jpg", + "transformations":{ + "original":{ + "id":"original", + "size":18006, + "bucket":"some-bucket", + "key":"image-original.jpg", + "acl":"public-read", + "contentType":"image/jpg", + "metadata":null, + "location":"https://some-bucket.s3.us-east-1.amazonaws.com/image-original.jpg", + "etag":"76c09df7bdd752a749f91b9663838fb2" + }, + "thumbnail":{ + "id":"thumbnail", + "size":18006, + "bucket":"some-bucket", + "key":"image-thumbnail.jpg", + "acl":"public-read", + "contentType":"image/jpg", + "metadata":null, + "location":"https://some-bucket.s3.us-east-1.amazonaws.com/image-thumbnail.jpg", + "etag":"9d554e03e37c79bff7ce31d375900db6" + } + } +} +``` + ## Testing The tests mock all access to S3 and can be run completely offline. diff --git a/index.js b/index.js index f64fa00..052489c 100644 --- a/index.js +++ b/index.js @@ -1,21 +1,12 @@ var crypto = require('crypto') var stream = require('stream') var fileType = require('file-type') -var htmlCommentRegex = require('html-comment-regex') -var parallel = require('run-parallel') var Upload = require('@aws-sdk/lib-storage').Upload var DeleteObjectCommand = require('@aws-sdk/client-s3').DeleteObjectCommand var util = require('util') -function staticValue (value) { - return function (req, file, cb) { - cb(null, value) - } -} - var defaultAcl = staticValue('private') var defaultContentType = staticValue('application/octet-stream') - var defaultMetadata = staticValue(undefined) var defaultCacheControl = staticValue(null) var defaultContentDisposition = staticValue(null) @@ -23,21 +14,8 @@ var defaultContentEncoding = staticValue(null) var defaultStorageClass = staticValue('STANDARD') var defaultSSE = staticValue(null) var defaultSSEKMS = staticValue(null) - -// Regular expression to detect svg file content, inspired by: https://github.com/sindresorhus/is-svg/blob/master/index.js -// It is not always possible to check for an end tag if a file is very big. The firstChunk, see below, might not be the entire file. -var svgRegex = /^\s*(?:<\?xml[^>]*>\s*)?(?:]*>\s*)?]*>/i - -function isSvg (svg) { - // Remove DTD entities - svg = svg.replace(/\s*/img, '') - // Remove DTD markup declarations - svg = svg.replace(/\[?(?:\s*]*>\s*)*\]?/g, '') - // Remove HTML comments - svg = svg.replace(htmlCommentRegex, '') - - return svgRegex.test(svg) -} +var defaultShouldTransform = staticValue(false) +var defaultTransformers = [] function defaultKey (req, file, cb) { crypto.randomBytes(16, function (err, raw) { @@ -45,29 +23,66 @@ function defaultKey (req, file, cb) { }) } +function staticValue (value) { + return function (req, file, cb) { + cb(null, value) + } +} + +function waterfall (funcs, callback) { + var index = 0 + var values = [] + + function next (err, value) { + if (err) return callback(err) + values.push(value) + if (index >= funcs.length) return callback(null, values) + funcs[index++](next) + } + + funcs[index++](next) +} + function autoContentType (req, file, cb) { - file.stream.once('data', function (firstChunk) { - var type = fileType(firstChunk) - var mime = 'application/octet-stream' // default type - - // Make sure to check xml-extension for svg files. - if ((!type || type.ext === 'xml') && isSvg(firstChunk.toString())) { - mime = 'image/svg+xml' - } else if (type) { - mime = type.mime - } + // Regular expression to detect svg file content, inspired by: https://github.com/sindresorhus/is-svg/blob/master/index.js + // It is not always possible to check for an end tag if a file is very big. The firstChunk, see below, might not be the entire file. + var svgRegex = /^\s*(?:<\?xml[^>]*>\s*)?(?:]*>\s*)?]*>/i + + function isSvg (svg) { + // Remove DTD entities + svg = svg.replace(/\s*/img, '') + // Remove DTD markup declarations + svg = svg.replace(/\[?(?:\s*]*>\s*)*\]?/g, '') + // Remove HTML comments + svg = svg.replace(//g, '') + + return svgRegex.test(svg) + } + + var outStream = new stream.PassThrough() + + stream.pipeline(file.stream, outStream, function (err) { + if (err) return cb(err) + }) - var outStream = new stream.PassThrough() + file.stream.once('data', function (firstChunk) { + fileType.fromBuffer(firstChunk).then(function (type) { + var mime = 'application/octet-stream' // default type - outStream.write(firstChunk) - file.stream.pipe(outStream) + // Make sure to check xml-extension for svg files. + if ((!type || type.ext === 'xml') && isSvg(firstChunk.toString())) { + mime = 'image/svg+xml' + } else if (type) { + mime = type.mime + } - cb(null, mime, outStream) + cb(null, mime, outStream) + }) }) } function collect (storage, req, file, cb) { - parallel([ + waterfall([ storage.getBucket.bind(storage, req, file), storage.getKey.bind(storage, req, file), storage.getAcl.bind(storage, req, file), @@ -77,7 +92,8 @@ function collect (storage, req, file, cb) { storage.getStorageClass.bind(storage, req, file), storage.getSSE.bind(storage, req, file), storage.getSSEKMS.bind(storage, req, file), - storage.getContentEncoding.bind(storage, req, file) + storage.getContentEncoding.bind(storage, req, file), + storage.getShouldTransform.bind(storage, req, file) ], function (err, values) { if (err) return cb(err) @@ -96,7 +112,8 @@ function collect (storage, req, file, cb) { replacementStream: replacementStream, serverSideEncryption: values[7], sseKmsKeyId: values[8], - contentEncoding: values[9] + contentEncoding: values[9], + shouldTransform: values[10] }) }) }) @@ -117,6 +134,7 @@ function S3Storage (opts) { switch (typeof opts.key) { case 'function': this.getKey = opts.key; break + case 'string': this.getKey = staticValue(opts.key); break case 'undefined': this.getKey = defaultKey; break default: throw new TypeError('Expected opts.key to be undefined or function') } @@ -130,8 +148,9 @@ function S3Storage (opts) { switch (typeof opts.contentType) { case 'function': this.getContentType = opts.contentType; break + case 'string': this.getContentType = staticValue(opts.contentType); break case 'undefined': this.getContentType = defaultContentType; break - default: throw new TypeError('Expected opts.contentType to be undefined or function') + default: throw new TypeError('Expected opts.contentType to be undefined, string or function') } switch (typeof opts.metadata) { @@ -181,63 +200,154 @@ function S3Storage (opts) { case 'undefined': this.getSSEKMS = defaultSSEKMS; break default: throw new TypeError('Expected opts.sseKmsKeyId to be undefined, string, or function') } -} -S3Storage.prototype._handleFile = function (req, file, cb) { - collect(this, req, file, function (err, opts) { - if (err) return cb(err) + switch (typeof opts.shouldTransform) { + case 'function': this.getShouldTransform = opts.shouldTransform; break + case 'boolean': this.getShouldTransform = staticValue(opts.shouldTransform); break + case 'undefined': this.getShouldTransform = defaultShouldTransform; break + default: throw new TypeError('Expected opts.shouldTransform to be undefined, boolean or function') + } + + switch (typeof opts.transformers) { + case 'object': this.transformers = opts.transformers; break + case 'undefined': this.transformers = defaultTransformers; break + default: throw new TypeError('Expected opts.transforms to be undefined or object') + } - var currentSize = 0 - - var params = { - Bucket: opts.bucket, - Key: opts.key, - ACL: opts.acl, - CacheControl: opts.cacheControl, - ContentType: opts.contentType, - Metadata: opts.metadata, - StorageClass: opts.storageClass, - ServerSideEncryption: opts.serverSideEncryption, - SSEKMSKeyId: opts.sseKmsKeyId, - Body: (opts.replacementStream || file.stream) + this.transformers.map(function (transformer) { + switch (typeof transformer.id) { + case 'string': break + default: throw new TypeError('Expected opts.transformer[].id to be string') } - if (opts.contentDisposition) { - params.ContentDisposition = opts.contentDisposition + switch (typeof transformer.key) { + case 'function': break + case 'string': transformer.key = staticValue(transformer.key); break + case 'undefined': transformer.key = defaultKey(); break + default: throw new TypeError('Expected opts.transformer[].key to be unedefined, string or function') } - if (opts.contentEncoding) { - params.ContentEncoding = opts.contentEncoding + switch (typeof transformer.transform) { + case 'function': break + default: throw new TypeError('Expected opts.transformer[].transform to be function') } - var upload = new Upload({ - client: this.s3, - params: params - }) + switch (typeof transformer.contentType) { + case 'function': transformer.getContentType = transformer.contentType; break + case 'string': transformer.getContentType = staticValue(transformer.contentType); break + case 'undefined': transformer.getContentType = staticValue(undefined); break + default: throw new TypeError('Expected opts.transformer[].contentType to be undefined, string or function') + } + + return transformer + }) +} + +S3Storage.prototype.directUpload = function (opts, file, cb) { + var currentSize = 0 + + var params = { + Bucket: opts.bucket, + Key: opts.key, + ACL: opts.acl, + CacheControl: opts.cacheControl, + ContentType: opts.contentType, + Metadata: opts.metadata, + StorageClass: opts.storageClass, + ServerSideEncryption: opts.serverSideEncryption, + SSEKMSKeyId: opts.sseKmsKeyId, + Body: opts.piper ? (opts.replacementStream || file.stream).pipe(opts.piper) : (opts.replacementStream || file.stream) + } + + if (opts.contentDisposition) { + params.ContentDisposition = opts.contentDisposition + } - upload.on('httpUploadProgress', function (ev) { - if (ev.total) currentSize = ev.total + if (opts.contentEncoding) { + params.ContentEncoding = opts.contentEncoding + } + + var upload = new Upload({ + client: this.s3, + params: params + }) + + upload.on('httpUploadProgress', function (ev) { + if (ev.total) currentSize = ev.total + }) + + util.callbackify(upload.done.bind(upload))(function (err, result) { + if (err) return cb(err) + + cb(null, { + id: opts.id, + size: currentSize, + bucket: opts.bucket, + key: opts.key, + acl: opts.acl, + contentType: opts.contentType, + contentDisposition: opts.contentDisposition, + contentEncoding: opts.contentEncoding, + storageClass: opts.storageClass, + serverSideEncryption: opts.serverSideEncryption, + metadata: opts.metadata, + location: result.Location, + etag: result.ETag, + versionId: result.VersionId }) + }) +} + +S3Storage.prototype.transformUpload = function (opts, req, file, cb) { + var storage = this + var transformations = {} + var pending = storage.transformers.length - util.callbackify(upload.done.bind(upload))(function (err, result) { + waterfall( + storage.transformers.map(function (transform) { + return transform.key.bind(storage, req, file) + }), + function (err, keys) { if (err) return cb(err) - cb(null, { - size: currentSize, - bucket: opts.bucket, - key: opts.key, - acl: opts.acl, - contentType: opts.contentType, - contentDisposition: opts.contentDisposition, - contentEncoding: opts.contentEncoding, - storageClass: opts.storageClass, - serverSideEncryption: opts.serverSideEncryption, - metadata: opts.metadata, - location: result.Location, - etag: result.ETag, - versionId: result.VersionId + keys.forEach(function (key, i) { + var transform = storage.transformers[i].transform.bind(storage, req, file) + var getContentType = storage.transformers[i].getContentType.bind(storage, req, file) + + getContentType(function (err, contentType) { + if (err) return cb(err) + + transform(function (err, piper) { + if (err) return cb(err) + + var transformerOpts = Object.assign({}, opts) + transformerOpts.id = storage.transformers[i].id || i + transformerOpts.key = key + transformerOpts.contentType = contentType || transformerOpts.contentType + transformerOpts.piper = piper + + storage.directUpload(transformerOpts, file, function (err, result) { + if (err) return cb(err) + + transformations[transformerOpts.id] = result + if (--pending === 0) cb(null, { transformations: transformations }) + }) + }) + }) }) - }) + } + ) +} + +S3Storage.prototype._handleFile = function (req, file, cb) { + collect(this, req, file, function (err, opts) { + if (err) return cb(err) + + if (opts.shouldTransform) { + this.transformUpload(opts, req, file, cb) + } else { + this.directUpload(opts, file, cb) + } }) } diff --git a/package-lock.json b/package-lock.json index 6d8f601..e30e90a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,7 @@ "license": "MIT", "dependencies": { "@aws-sdk/lib-storage": "^3.46.0", - "file-type": "^3.3.0", - "html-comment-regex": "^1.1.2", - "run-parallel": "^1.1.6" + "file-type": "^16.5.4" }, "devDependencies": { "express": "^4.13.1", @@ -1326,6 +1324,11 @@ "node": ">= 12.0.0" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -3255,11 +3258,19 @@ } }, "node_modules/file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, "node_modules/finalhandler": { @@ -3535,11 +3546,6 @@ "node": ">=0.10.0" } }, - "node_modules/html-comment-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", - "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==" - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4730,6 +4736,18 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", "dev": true }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/pkg-config": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pkg-config/-/pkg-config-1.1.1.tgz", @@ -4834,6 +4852,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -4885,6 +4904,42 @@ "string_decoder": "~0.10.x" } }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", + "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/readline2": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", @@ -5204,6 +5259,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -5637,6 +5693,22 @@ "node": ">=0.8.0" } }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/supports-color": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", @@ -5722,6 +5794,22 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", @@ -7056,6 +7144,11 @@ "tslib": "^2.3.1" } }, + "@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -8691,9 +8784,14 @@ } }, "file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==" + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "requires": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + } }, "finalhandler": { "version": "1.2.0", @@ -8913,11 +9011,6 @@ } } }, - "html-comment-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", - "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==" - }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -9913,6 +10006,11 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", "dev": true }, + "peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==" + }, "pkg-config": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pkg-config/-/pkg-config-1.1.1.tgz", @@ -9996,7 +10094,8 @@ "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true }, "range-parser": { "version": "1.2.1", @@ -10028,6 +10127,34 @@ "string_decoder": "~0.10.x" } }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "requires": { + "readable-stream": "^3.6.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", + "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, "readline2": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", @@ -10291,6 +10418,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -10641,6 +10769,15 @@ "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", "dev": true }, + "strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "requires": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + } + }, "supports-color": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", @@ -10701,6 +10838,15 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "requires": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + } + }, "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", diff --git a/package.json b/package.json index e1a31a1..7cfcceb 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,7 @@ "homepage": "https://github.com/badunk/multer-s3#readme", "dependencies": { "@aws-sdk/lib-storage": "^3.46.0", - "file-type": "^3.3.0", - "html-comment-regex": "^1.1.2", - "run-parallel": "^1.1.6" + "file-type": "^16.5.4" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.0.0" diff --git a/test/basic.js b/test/basic.js index 9b7dc2f..a6f3ae6 100644 --- a/test/basic.js +++ b/test/basic.js @@ -64,7 +64,7 @@ describe('Multer S3', function () { it('upload files', function (done) { var s3 = mockS3() var form = new FormData() - var storage = multerS3({ s3: s3, bucket: 'test' }) + var storage = multerS3({ s3: s3, bucket: 'test', key: 'mock-location' }) var upload = multer({ storage: storage }) var parser = upload.single('image') var image = fs.createReadStream(path.join(__dirname, 'files', 'ffffff.png')) @@ -82,7 +82,7 @@ describe('Multer S3', function () { assert.equal(req.file.size, 68) assert.equal(req.file.bucket, 'test') assert.equal(req.file.etag, 'mock-etag') - assert.equal(req.file.location, 'mock-location') + assert.equal(req.file.location, 'https//test.hostname/mock-location') done() }) @@ -91,7 +91,7 @@ describe('Multer S3', function () { it('uploads file with AES256 server-side encryption', function (done) { var s3 = mockS3() var form = new FormData() - var storage = multerS3({ s3: s3, bucket: 'test', serverSideEncryption: 'AES256' }) + var storage = multerS3({ s3: s3, bucket: 'test', key: 'mock-location', serverSideEncryption: 'AES256' }) var upload = multer({ storage: storage }) var parser = upload.single('image') var image = fs.createReadStream(path.join(__dirname, 'files', 'ffffff.png')) @@ -109,7 +109,7 @@ describe('Multer S3', function () { assert.equal(req.file.size, 68) assert.equal(req.file.bucket, 'test') assert.equal(req.file.etag, 'mock-etag') - assert.equal(req.file.location, 'mock-location') + assert.equal(req.file.location, 'https//test.hostname/mock-location') assert.equal(req.file.serverSideEncryption, 'AES256') done() @@ -119,7 +119,7 @@ describe('Multer S3', function () { it('uploads file with AWS KMS-managed server-side encryption', function (done) { var s3 = mockS3() var form = new FormData() - var storage = multerS3({ s3: s3, bucket: 'test', serverSideEncryption: 'aws:kms' }) + var storage = multerS3({ s3: s3, bucket: 'test', key: 'mock-location', serverSideEncryption: 'aws:kms' }) var upload = multer({ storage: storage }) var parser = upload.single('image') var image = fs.createReadStream(path.join(__dirname, 'files', 'ffffff.png')) @@ -137,7 +137,7 @@ describe('Multer S3', function () { assert.equal(req.file.size, 68) assert.equal(req.file.bucket, 'test') assert.equal(req.file.etag, 'mock-etag') - assert.equal(req.file.location, 'mock-location') + assert.equal(req.file.location, 'https//test.hostname/mock-location') assert.equal(req.file.serverSideEncryption, 'aws:kms') done() @@ -147,7 +147,7 @@ describe('Multer S3', function () { it('uploads PNG file with correct content-type', function (done) { var s3 = mockS3() var form = new FormData() - var storage = multerS3({ s3: s3, bucket: 'test', serverSideEncryption: 'aws:kms', contentType: multerS3.AUTO_CONTENT_TYPE }) + var storage = multerS3({ s3: s3, bucket: 'test', key: 'mock-location', serverSideEncryption: 'aws:kms', contentType: multerS3.AUTO_CONTENT_TYPE }) var upload = multer({ storage: storage }) var parser = upload.single('image') var image = fs.createReadStream(path.join(__dirname, 'files', 'ffffff.png')) @@ -166,7 +166,7 @@ describe('Multer S3', function () { assert.equal(req.file.size, 68) assert.equal(req.file.bucket, 'test') assert.equal(req.file.etag, 'mock-etag') - assert.equal(req.file.location, 'mock-location') + assert.equal(req.file.location, 'https//test.hostname/mock-location') assert.equal(req.file.serverSideEncryption, 'aws:kms') done() @@ -176,7 +176,7 @@ describe('Multer S3', function () { it('uploads pure SVG file with correct content-type', function (done) { var s3 = mockS3() var form = new FormData() - var storage = multerS3({ s3: s3, bucket: 'test', serverSideEncryption: 'aws:kms', contentType: multerS3.AUTO_CONTENT_TYPE }) + var storage = multerS3({ s3: s3, bucket: 'test', key: 'mock-location', serverSideEncryption: 'aws:kms', contentType: multerS3.AUTO_CONTENT_TYPE }) var upload = multer({ storage: storage }) var parser = upload.single('image') var image = fs.createReadStream(path.join(__dirname, 'files', 'test.svg')) @@ -195,7 +195,7 @@ describe('Multer S3', function () { assert.equal(req.file.size, 100) assert.equal(req.file.bucket, 'test') assert.equal(req.file.etag, 'mock-etag') - assert.equal(req.file.location, 'mock-location') + assert.equal(req.file.location, 'https//test.hostname/mock-location') assert.equal(req.file.serverSideEncryption, 'aws:kms') done() @@ -205,7 +205,7 @@ describe('Multer S3', function () { it('uploads common SVG file with correct content-type', function (done) { var s3 = mockS3() var form = new FormData() - var storage = multerS3({ s3: s3, bucket: 'test', serverSideEncryption: 'aws:kms', contentType: multerS3.AUTO_CONTENT_TYPE }) + var storage = multerS3({ s3: s3, bucket: 'test', key: 'mock-location', serverSideEncryption: 'aws:kms', contentType: multerS3.AUTO_CONTENT_TYPE }) var upload = multer({ storage: storage }) var parser = upload.single('image') var image = fs.createReadStream(path.join(__dirname, 'files', 'test2.svg')) @@ -224,7 +224,7 @@ describe('Multer S3', function () { assert.equal(req.file.size, 285) assert.equal(req.file.bucket, 'test') assert.equal(req.file.etag, 'mock-etag') - assert.equal(req.file.location, 'mock-location') + assert.equal(req.file.location, 'https//test.hostname/mock-location') assert.equal(req.file.serverSideEncryption, 'aws:kms') done() @@ -236,7 +236,7 @@ describe('Multer S3', function () { var s3 = mockS3() var form = new FormData() - var storage = multerS3({ s3: s3, bucket: 'test', serverSideEncryption: 'aws:kms', contentType: multerS3.AUTO_CONTENT_TYPE }) + var storage = multerS3({ s3: s3, bucket: 'test', key: 'mock-location', serverSideEncryption: 'aws:kms', contentType: multerS3.AUTO_CONTENT_TYPE }) var upload = multer({ storage: storage }) var parser = upload.single('image') fs.writeFileSync(path.join(__dirname, 'files', 'test_generated.svg'), ' ({ protocol: 'https', hostname: 'hostname' }) } } } module.exports = createMockS3