UNPKG

multer-s3-transform

Version:

Streaming multer storage engine for AWS S3

434 lines (388 loc) 11.7 kB
var crypto = require("crypto"); var stream = require("stream"); var fileType = require("file-type"); var isSvg = require("is-svg"); var parallel = require("run-parallel"); function staticValue(value) { return function (req, file, cb) { cb(null, value); }; } var defaultAcl = staticValue("private"); var defaultContentType = staticValue("application/octet-stream"); var defaultMetadata = staticValue(null); var defaultCacheControl = staticValue(null); var defaultShouldTransform = staticValue(false); var defaultTransforms = []; var defaultContentDisposition = staticValue(null); var defaultStorageClass = staticValue("STANDARD"); var defaultSSE = staticValue(null); var defaultSSEKMS = staticValue(null); function defaultKey(req, file, cb) { crypto.randomBytes(16, function (err, raw) { cb(err, err ? undefined : raw.toString("hex")); }); } function autoContentType(req, file, cb) { file.stream.once("data", function (firstChunk) { var type = fileType(firstChunk); var mime; if (type) { mime = type.mime; } else if (isSvg(firstChunk)) { mime = "image/svg+xml"; } else { mime = "application/octet-stream"; } var outStream = new stream.PassThrough(); outStream.write(firstChunk); file.stream.pipe(outStream); cb(null, mime, outStream); }); } function collect(storage, req, file, cb) { parallel( [ storage.getBucket.bind(storage, req, file), storage.getKey.bind(storage, req, file), storage.getAcl.bind(storage, req, file), storage.getMetadata.bind(storage, req, file), storage.getCacheControl.bind(storage, req, file), storage.getShouldTransform.bind(storage, req, file), storage.getContentDisposition.bind(storage, req, file), storage.getStorageClass.bind(storage, req, file), storage.getSSE.bind(storage, req, file), storage.getSSEKMS.bind(storage, req, file), ], function (err, values) { if (err) return cb(err); storage.getContentType(req, file, function ( err, contentType, replacementStream ) { if (err) return cb(err); cb.call(storage, null, { bucket: values[0], key: values[1], acl: values[2], metadata: values[3], cacheControl: values[4], shouldTransform: values[5], contentDisposition: values[6], storageClass: values[7], contentType: contentType, replacementStream: replacementStream, serverSideEncryption: values[8], sseKmsKeyId: values[9], }); }); } ); } function S3Storage(opts) { switch (typeof opts.s3) { case "object": this.s3 = opts.s3; break; default: throw new TypeError("Expected opts.s3 to be object"); } switch (typeof opts.bucket) { case "function": this.getBucket = opts.bucket; break; case "string": this.getBucket = staticValue(opts.bucket); break; case "undefined": throw new Error("bucket is required"); default: throw new TypeError( "Expected opts.bucket to be undefined, string or function" ); } switch (typeof opts.key) { case "function": this.getKey = opts.key; break; case "undefined": this.getKey = defaultKey; break; default: throw new TypeError("Expected opts.key to be undefined or function"); } switch (typeof opts.acl) { case "function": this.getAcl = opts.acl; break; case "string": this.getAcl = staticValue(opts.acl); break; case "undefined": this.getAcl = defaultAcl; break; default: throw new TypeError( "Expected opts.acl to be undefined, string or function" ); } switch (typeof opts.contentType) { case "function": this.getContentType = opts.contentType; break; case "undefined": this.getContentType = defaultContentType; break; default: throw new TypeError( "Expected opts.contentType to be undefined or function" ); } switch (typeof opts.metadata) { case "function": this.getMetadata = opts.metadata; break; case "undefined": this.getMetadata = defaultMetadata; break; default: throw new TypeError("Expected opts.metadata to be undefined or function"); } switch (typeof opts.cacheControl) { case "function": this.getCacheControl = opts.cacheControl; break; case "string": this.getCacheControl = staticValue(opts.cacheControl); break; case "undefined": this.getCacheControl = defaultCacheControl; break; default: throw new TypeError( "Expected opts.cacheControl to be undefined, string or function" ); } 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.transforms) { case "object": this.getTransforms = opts.transforms; break; case "undefined": this.getTransforms = defaultTransforms; break; default: throw new TypeError("Expected opts.transforms to be undefined or object"); } this.getTransforms.map(function (transform, i) { switch (typeof transform.key) { case "function": break; case "string": transform.key = staticValue(transform.key); break; case "undefined": transform.key = defaultKey(); break; default: throw new TypeError( "Expected opts.transform[].key to be unedefined, string or function" ); } switch (typeof transform.transform) { case "function": break; default: throw new TypeError( "Expected opts.transform[].transform to be function" ); } return transform; }); switch (typeof opts.contentDisposition) { case "function": this.getContentDisposition = opts.contentDisposition; break; case "string": this.getContentDisposition = staticValue(opts.contentDisposition); break; case "undefined": this.getContentDisposition = defaultContentDisposition; break; default: throw new TypeError( "Expected opts.contentDisposition to be undefined, string or function" ); } switch (typeof opts.storageClass) { case "function": this.getStorageClass = opts.storageClass; break; case "string": this.getStorageClass = staticValue(opts.storageClass); break; case "undefined": this.getStorageClass = defaultStorageClass; break; default: throw new TypeError( "Expected opts.storageClass to be undefined, string or function" ); } switch (typeof opts.serverSideEncryption) { case "function": this.getSSE = opts.serverSideEncryption; break; case "string": this.getSSE = staticValue(opts.serverSideEncryption); break; case "undefined": this.getSSE = defaultSSE; break; default: throw new TypeError( "Expected opts.serverSideEncryption to be undefined, string or function" ); } switch (typeof opts.sseKmsKeyId) { case "function": this.getSSEKMS = opts.sseKmsKeyId; break; case "string": this.getSSEKMS = staticValue(opts.sseKmsKeyId); break; 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); var storage = this; if (!opts.shouldTransform) { storage.directUpload(opts, file, cb); } else { storage.transformUpload(opts, req, file, cb); } }); }; 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.replacementStream || file.stream, }; if (opts.contentDisposition) { params.ContentDisposition = opts.contentDisposition; } var upload = this.s3.upload(params); upload.on("httpUploadProgress", function (ev) { if (ev.total) currentSize = ev.total; }); upload.send(function (err, result) { if (err) return cb(err); cb(null, { size: currentSize, bucket: opts.bucket, key: opts.key, acl: opts.acl, contentType: opts.contentType, contentDisposition: opts.contentDisposition, 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 results = []; parallel( storage.getTransforms.map(function (transform) { return transform.key.bind(storage, req, file); }), function (err, keys) { if (err) return cb(err); keys.forEach(function (key, i) { var currentSize = 0; storage.getTransforms[i].transform(req, file, function (err, piper) { if (err) return cb(err); var upload = storage.s3.upload({ Bucket: opts.bucket, Key: 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).pipe(piper), }); upload.on("httpUploadProgress", function (ev) { if (ev.total) currentSize = ev.total; }); upload.send(function (err, result) { if (err) return cb(err); results.push({ id: storage.getTransforms[i].id || i, size: currentSize, bucket: opts.bucket, key: key, acl: opts.acl, contentType: opts.contentType, contentDisposition: opts.contentDisposition, storageClass: opts.storageClass, serverSideEncryption: opts.serverSideEncryption, metadata: opts.metadata, location: result.Location, etag: result.ETag, }); if (results.length === keys.length) { return cb(null, { transforms: results }); } }); }); }); } ); }; S3Storage.prototype._removeFile = function (req, file, cb) { this.s3.deleteObject({ Bucket: file.bucket, Key: file.key }, cb); }; module.exports = function (opts) { return new S3Storage(opts); }; module.exports.AUTO_CONTENT_TYPE = autoContentType; module.exports.DEFAULT_CONTENT_TYPE = defaultContentType;