├── .prettierignore ├── .gitignore ├── .github └── FUNDING.yml ├── __tests__ ├── nodejs-512.png └── implementation.spec.ts ├── index.d.ts ├── index.js ├── dist ├── types │ ├── get-filename.d.ts │ ├── get-filename.d.ts.map │ ├── get-sharp-options.d.ts │ ├── transformer.d.ts │ ├── get-sharp-options.d.ts.map │ ├── transformer.d.ts.map │ ├── main.d.ts.map │ ├── main.d.ts │ └── types.d.ts ├── get-filename.js ├── get-filename.js.map ├── get-sharp-options.js.map ├── get-sharp-options.js ├── transformer.js.map ├── transformer.js ├── main.js.map └── main.js ├── .travis.yml ├── src ├── get-filename.ts ├── get-sharp-options.ts ├── transformer.ts ├── types.d.ts └── main.ts ├── wallaby.js ├── prettier.config.js ├── scripts └── copy.js ├── tsconfig.json ├── .editorconfig ├── tslint.json ├── CHANGELOG.md ├── LICENSE ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | coverage 4 | .env 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: ikhsaan 2 | custom: ["https://www.paypal.com/paypalme/abdulikhsan"] 3 | -------------------------------------------------------------------------------- /__tests__/nodejs-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikhsanalatsary/multer-sharp-s3/HEAD/__tests__/nodejs-512.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import s3Storage from './dist/types/main' 2 | 3 | export = s3Storage 4 | export as namespace s3Storage; 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const s3Storage = require('./dist/main').default 4 | 5 | module.exports = s3Storage 6 | -------------------------------------------------------------------------------- /dist/types/get-filename.d.ts: -------------------------------------------------------------------------------- 1 | declare function getFileName(req: any, file: any, cb: any): void; 2 | export default getFileName; 3 | //# sourceMappingURL=get-filename.d.ts.map -------------------------------------------------------------------------------- /dist/types/get-filename.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"get-filename.d.ts","sourceRoot":"src/","sources":["get-filename.ts"],"names":[],"mappings":"AAEA,iBAAS,WAAW,CAAC,GAAG,KAAA,EAAE,IAAI,KAAA,EAAE,EAAE,KAAA,QAIjC;AAED,eAAe,WAAW,CAAC"} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | jobs: 3 | include: 4 | - os: linux 5 | dist: bionic 6 | node_js: "12" 7 | install: 8 | - npm install 9 | script: 10 | - npm test 11 | after_success: 12 | - npm run report-coverage 13 | -------------------------------------------------------------------------------- /dist/types/get-sharp-options.d.ts: -------------------------------------------------------------------------------- 1 | import { S3StorageOptions, SharpOptions } from './types'; 2 | declare function getSharpOptions(options: S3StorageOptions): SharpOptions; 3 | export default getSharpOptions; 4 | //# sourceMappingURL=get-sharp-options.d.ts.map -------------------------------------------------------------------------------- /src/get-filename.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | 3 | function getFileName(req, file, cb) { 4 | crypto.pseudoRandomBytes(16, function (err, raw) { 5 | cb(err, err ? undefined : raw.toString('hex')); 6 | }); 7 | }; 8 | 9 | export default getFileName; 10 | -------------------------------------------------------------------------------- /dist/types/transformer.d.ts: -------------------------------------------------------------------------------- 1 | import * as sharp from 'sharp'; 2 | import { ResizeOption, SharpOptions } from './types'; 3 | export default transformer; 4 | declare function transformer(options: SharpOptions, size: ResizeOption): sharp.Sharp; 5 | //# sourceMappingURL=transformer.d.ts.map -------------------------------------------------------------------------------- /dist/types/get-sharp-options.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"get-sharp-options.d.ts","sourceRoot":"src/","sources":["get-sharp-options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAExD,iBAAS,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,CAoChE;AAED,eAAe,eAAe,CAAA"} -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | files: ['tsconfig.json', 'src/**/*.ts'], 4 | 5 | tests: ['__tests__/*.ts'], 6 | 7 | env: { 8 | type: 'node', 9 | runner: 'node', 10 | }, 11 | 12 | testFramework: 'jest', 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('./types').PrettierConfig} 3 | */ 4 | const config = { 5 | singleQuote: true, 6 | arrowParens: 'always', 7 | semi: false, 8 | bracketSpacing: true, 9 | trailingComma: 'es5', 10 | printWidth: 80, 11 | } 12 | 13 | module.exports = config 14 | -------------------------------------------------------------------------------- /dist/types/transformer.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"transformer.d.ts","sourceRoot":"src/","sources":["transformer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAEpD,eAAe,WAAW,CAAA;AAQ1B,iBAAS,WAAW,CAClB,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,YAAY,GACjB,KAAK,CAAC,KAAK,CAQb"} -------------------------------------------------------------------------------- /scripts/copy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { copyFileSync } = require('fs') 3 | const { resolve } = require('path') 4 | ;(function main() { 5 | const rootDir = resolve(__dirname, '..') 6 | const srcDir = resolve(rootDir, 'src') 7 | const distDir = resolve(rootDir, 'dist') 8 | copyFileSync( 9 | resolve(srcDir, 'types.d.ts'), 10 | resolve(distDir, 'types/types.d.ts') 11 | ) 12 | })() 13 | -------------------------------------------------------------------------------- /dist/get-filename.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const crypto = require("crypto"); 4 | function getFileName(req, file, cb) { 5 | crypto.pseudoRandomBytes(16, function (err, raw) { 6 | cb(err, err ? undefined : raw.toString('hex')); 7 | }); 8 | } 9 | ; 10 | exports.default = getFileName; 11 | //# sourceMappingURL=get-filename.js.map -------------------------------------------------------------------------------- /dist/get-filename.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"get-filename.js","sourceRoot":"src/","sources":["get-filename.ts"],"names":[],"mappings":";;AAAA,iCAAiC;AAEjC,SAAS,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;IAChC,MAAM,CAAC,iBAAiB,CAAC,EAAE,EAAE,UAAU,GAAG,EAAE,GAAG;QAC7C,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC;AAAA,CAAC;AAEF,kBAAe,WAAW,CAAC"} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceRoot": "src", 7 | "outDir": "dist", 8 | "declaration": true, 9 | "declarationMap": true, 10 | "declarationDir": "dist/types", 11 | "watch": true, 12 | "types": [ 13 | "node", 14 | "jest", 15 | "core-js" 16 | ], 17 | "sourceMap": true, 18 | "lib": [ 19 | "es2015", 20 | "es2017" 21 | ] 22 | }, 23 | "compileOnSave": true, 24 | "exclude": [ 25 | "**/*.spec.ts", 26 | "examples/*" 27 | ], 28 | "include": [ 29 | "src/*.ts", 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | max_line_length = 80 14 | 15 | # We recommend you to keep these unchanged 16 | end_of_line = lf 17 | charset = utf-8 18 | trim_trailing_whitespace = true 19 | insert_final_newline = true 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.{js,ts}] 25 | quote_type = single 26 | curly_bracket_next_line = false 27 | spaces_around_brackets = inside 28 | indent_brace_style = BSD KNF 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "jsRules": {}, 6 | "rules": { 7 | "semicolon": [ 8 | false 9 | ], 10 | "quotemark": [ 11 | false 12 | ], 13 | "trailing-comma": [ 14 | false 15 | ], 16 | "no-console": [ 17 | false 18 | ], 19 | "no-shadowed-variable": false, 20 | "only-arrow-functions": false, 21 | "interface-name" : [true, "never-prefix"], 22 | "object-literal-sort-keys": false, 23 | "ordered-imports": false, 24 | "max-line-length": [true, 500], 25 | "no-unused-variable": true 26 | }, 27 | "rulesDirectory": [] 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.2.5](https://github.com/ikhsanalatsary/multer-sharp-s3/compare/v0.2.4...v0.2.5) (2022-02-16) 6 | 7 | ### [0.2.4](https://github.com/ikhsanalatsary/multer-sharp-s3/compare/v0.2.3...v0.2.4) (2022-02-15) 8 | 9 | 10 | ### Bug Fixes 11 | 12 | * Key with custom directory ([6d693fc](https://github.com/ikhsanalatsary/multer-sharp-s3/commit/6d693fc97853025ff87762a2d2aa27300b0d71b0)) 13 | 14 | ### [0.2.3](https://github.com/ikhsanalatsary/multer-sharp-s3/compare/v0.2.2...v0.2.3) (2022-02-15) 15 | 16 | ### [0.2.2](https://github.com/ikhsanalatsary/multer-sharp-s3/compare/v0.2.1...v0.2.2) (2022-02-15) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * package.json to reduce vulnerabilities ([ef21012](https://github.com/ikhsanalatsary/multer-sharp-s3/commit/ef21012efecff36857376cdf342d22077e8dc724)) 22 | 23 | ### [0.2.1](https://github.com/ikhsanalatsary/multer-sharp-s3/compare/v0.2.0...v0.2.1) (2020-05-26) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Abdul Fattah Ikhsan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/get-sharp-options.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"get-sharp-options.js","sourceRoot":"src/","sources":["get-sharp-options.ts"],"names":[],"mappings":";;AAEA,SAAS,eAAe,CAAC,OAAyB;IAChD,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,WAAW,EAAE,OAAO,CAAC,WAAW;KACjC,CAAA;AACH,CAAC;AAED,kBAAe,eAAe,CAAA"} -------------------------------------------------------------------------------- /dist/types/main.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.d.ts","sourceRoot":"src/","sources":["main.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAG5B,OAAO,UAAU,MAAM,gBAAgB,CAAA;AACvC,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAExD,oBAAY,OAAO,GAAG;IACpB,MAAM,EAAE,MAAM,CAAC,cAAc,GAAG,KAAK,CAAC,KAAK,CAAA;CAC5C,CAAA;AACD,oBAAY,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,GACrC,OAAO,GACP,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;AACpC,oBAAY,IAAI,GAAG,OAAO,CACxB,OAAO,CAAC,MAAM,CAAC,IAAI,GACjB,aAAa,CAAC,QAAQ,GACtB,EAAE,CAAC,KAAK,CAAC,gBAAgB,GACzB,KAAK,CAAC,UAAU,CACnB,CAAA;AACD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,gBAAgB,CAAA;IACtB,SAAS,EAAE,YAAY,CAAA;CACxB;AACD,qBAAa,SAAU,YAAW,aAAa;IAC7C,SAAS,CAAC,MAAM,CAAC,cAAc;;;;;MAK9B;gBAEW,OAAO,EAAE,gBAAgB;IAsB9B,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,IAAI,KAAK,IAAI;IA2C7E,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI;IAIvE,OAAO,CAAC,cAAc;IA0JtB,OAAO,CAAC,eAAe;CA6BxB;AAED,iBAAS,SAAS,CAAC,OAAO,EAAE,gBAAgB,aAE3C;AAED,eAAe,SAAS,CAAA"} -------------------------------------------------------------------------------- /src/get-sharp-options.ts: -------------------------------------------------------------------------------- 1 | import { S3StorageOptions, SharpOptions } from './types' 2 | 3 | function getSharpOptions(options: S3StorageOptions): SharpOptions { 4 | return { 5 | resize: options.resize, 6 | composite: options.composite, 7 | modulate: options.modulate, 8 | toFormat: options.toFormat, 9 | extract: options.extract, 10 | trim: options.trim, 11 | flatten: options.flatten, 12 | extend: options.extend, 13 | negate: options.negate, 14 | rotate: options.rotate, 15 | flip: options.flip, 16 | flop: options.flop, 17 | blur: options.blur, 18 | sharpen: options.sharpen, 19 | gamma: options.gamma, 20 | grayscale: options.grayscale, 21 | greyscale: options.greyscale, 22 | normalize: options.normalize, 23 | normalise: options.normalise, 24 | convolve: options.convolve, 25 | threshold: options.threshold, 26 | toColourspace: options.toColourspace, 27 | toColorspace: options.toColorspace, 28 | withMetadata: options.withMetadata, 29 | linear: options.linear, 30 | median: options.median, 31 | tint: options.tint, 32 | removeAlpha: options.removeAlpha, 33 | bandbool: options.bandbool, 34 | boolean: options.boolean, 35 | joinChannel: options.joinChannel, 36 | extractChannel: options.extractChannel, 37 | ensureAlpha: options.ensureAlpha, 38 | } 39 | } 40 | 41 | export default getSharpOptions 42 | -------------------------------------------------------------------------------- /dist/types/main.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as sharp from 'sharp'; 3 | import { ManagedUpload } from 'aws-sdk/lib/s3/managed_upload'; 4 | import { StorageEngine } from 'multer'; 5 | import { Request } from 'express'; 6 | import { S3 } from 'aws-sdk'; 7 | import defaultKey from './get-filename'; 8 | import { S3StorageOptions, SharpOptions } from './types'; 9 | export declare type EStream = { 10 | stream: NodeJS.ReadableStream & sharp.Sharp; 11 | }; 12 | export declare type EFile = Express.Multer.File & EStream & Partial; 13 | export declare type Info = Partial; 14 | export interface S3Storage { 15 | opts: S3StorageOptions; 16 | sharpOpts: SharpOptions; 17 | } 18 | export declare class S3Storage implements StorageEngine { 19 | protected static defaultOptions: { 20 | ACL: string; 21 | Bucket: string; 22 | Key: typeof defaultKey; 23 | multiple: boolean; 24 | }; 25 | constructor(options: S3StorageOptions); 26 | _handleFile(req: Request, file: EFile, cb: (error?: any, info?: Info) => void): void; 27 | _removeFile(req: Request, file: Info, cb: (error: Error) => void): void; 28 | private _uploadProcess; 29 | private _uploadNonImage; 30 | } 31 | declare function s3Storage(options: S3StorageOptions): S3Storage; 32 | export default s3Storage; 33 | //# sourceMappingURL=main.d.ts.map -------------------------------------------------------------------------------- /dist/get-sharp-options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function getSharpOptions(options) { 4 | return { 5 | resize: options.resize, 6 | composite: options.composite, 7 | modulate: options.modulate, 8 | toFormat: options.toFormat, 9 | extract: options.extract, 10 | trim: options.trim, 11 | flatten: options.flatten, 12 | extend: options.extend, 13 | negate: options.negate, 14 | rotate: options.rotate, 15 | flip: options.flip, 16 | flop: options.flop, 17 | blur: options.blur, 18 | sharpen: options.sharpen, 19 | gamma: options.gamma, 20 | grayscale: options.grayscale, 21 | greyscale: options.greyscale, 22 | normalize: options.normalize, 23 | normalise: options.normalise, 24 | convolve: options.convolve, 25 | threshold: options.threshold, 26 | toColourspace: options.toColourspace, 27 | toColorspace: options.toColorspace, 28 | withMetadata: options.withMetadata, 29 | linear: options.linear, 30 | median: options.median, 31 | tint: options.tint, 32 | removeAlpha: options.removeAlpha, 33 | bandbool: options.bandbool, 34 | boolean: options.boolean, 35 | joinChannel: options.joinChannel, 36 | extractChannel: options.extractChannel, 37 | ensureAlpha: options.ensureAlpha, 38 | }; 39 | } 40 | exports.default = getSharpOptions; 41 | //# sourceMappingURL=get-sharp-options.js.map -------------------------------------------------------------------------------- /dist/transformer.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"transformer.js","sourceRoot":"src/","sources":["transformer.ts"],"names":[],"mappings":";;AAAA,+BAA8B;AAG9B,kBAAe,WAAW,CAAA;AAC1B,IAAI,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAChC,CAAC,OAAO,EAAE,OAAO,CAAC;IAClB,CAAC,QAAQ,EAAE,QAAQ,CAAC;IACpB,CAAC,QAAQ,EAAE,QAAQ,CAAC;IACpB,CAAC,MAAM,EAAE,MAAM,CAAC;CACjB,CAAC,CAAA;AAEF,SAAS,WAAW,CAClB,OAAqB,EACrB,IAAkB;IAElB,IAAI,WAAW,GAAG,KAAK,EAAE,CAAA;IACzB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;QAClD,IAAI,KAAK,EAAE;YACT,WAAW,GAAG,kBAAkB,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,CAAC,CAAA;SAChE;KACF;IACD,OAAO,WAAW,CAAA;AACpB,CAAC;AAED,MAAM,oBAAoB,GAAG,CAAC,MAAc,EAAE,IAAY,EAAE,EAAE,CAC5D,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;AACpD,MAAM,OAAO,GAAG,CAAC,KAAa,EAAE,EAAE,CAChC,OAAO,KAAK,KAAK,QAAQ,IAAI,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;AAClE,MAAM,cAAc,GAAG,CAAC,KAAU,EAAE,EAAE;IACpC,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE;QAClB,OAAO,KAAK,CAAC,IAAI,CAAA;KAClB;IACD,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AACD,MAAM,0BAA0B,GAAG,CAAC,GAAW,EAAE,KAAU,EAAE,EAAE;IAC7D,IAAI,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;QAChC,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE;YAC9B,IAAI,KAAK,EAAE;gBACT,OAAO,SAAS,CAAA;aACjB;SACF;KACF;IACD,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AACD,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAAE,KAAU,EAAE,IAAkB,EAAE,WAAwB,EAAE,EAAE;IACnG,IAAI,GAAG,KAAK,QAAQ,EAAE;QACpB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACxB,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;SACxE;KACF;SAAM,IAAI,GAAG,KAAK,UAAU,EAAE;QAC7B,WAAW,GAAG,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;KACzE;SAAM,IAAI,GAAG,KAAK,QAAQ,EAAE;QAC3B,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE;YAC9B,WAAW,GAAG,WAAW,CAAC,MAAM,EAAE,CAAA;SACnC;aAAM;YACL,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAA;SAC3C;KACF;SAAM,IAAI,GAAG,KAAK,aAAa,EAAE;QAChC,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;KACnE;SAAM;QACL,MAAM,UAAU,GAAG,0BAA0B,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QACzD,WAAW,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAA;KAC3C;IACD,OAAO,WAAW,CAAA;AACpB,CAAC,CAAA"} -------------------------------------------------------------------------------- /src/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as sharp from 'sharp' 2 | import { ResizeOption, SharpOptions } from './types' 3 | 4 | export default transformer 5 | let dynamicParamMethods = new Map([ 6 | ['gamma', 'gamma'], 7 | ['median', 'median'], 8 | ['rotate', 'rotate'], 9 | ['trim', 'trim'], 10 | ]) 11 | 12 | function transformer( 13 | options: SharpOptions, 14 | size: ResizeOption 15 | ): sharp.Sharp { 16 | let imageStream = sharp() 17 | for (const [key, value] of Object.entries(options)) { 18 | if (value) { 19 | imageStream = resolveImageStream(key, value, size, imageStream) 20 | } 21 | } 22 | return imageStream 23 | } 24 | 25 | const objectHasOwnProperty = (source: Object, prop: string) => 26 | Object.prototype.hasOwnProperty.call(source, prop) 27 | const hasProp = (value: Object) => 28 | typeof value === 'object' && objectHasOwnProperty(value, 'type') 29 | const validateFormat = (value: any) => { 30 | if (hasProp(value)) { 31 | return value.type 32 | } 33 | return value 34 | } 35 | const validateValueForRelatedKey = (key: string, value: any) => { 36 | if (dynamicParamMethods.has(key)) { 37 | if (typeof value === 'boolean') { 38 | if (value) { 39 | return undefined 40 | } 41 | } 42 | } 43 | return value 44 | } 45 | const resolveImageStream = (key: string, value: any, size: ResizeOption, imageStream: sharp.Sharp) => { 46 | if (key === 'resize') { 47 | if (!Array.isArray(size)) { 48 | imageStream = imageStream.resize(size.width, size.height, size.options) 49 | } 50 | } else if (key === 'toFormat') { 51 | imageStream = imageStream.toFormat(validateFormat(value), value.options) 52 | } else if (key === 'linear') { 53 | if (typeof value === 'boolean') { 54 | imageStream = imageStream.linear() 55 | } else { 56 | imageStream = imageStream.linear(...value) 57 | } 58 | } else if (key === 'joinChannel') { 59 | imageStream = imageStream.joinChannel(value.images, value.options) 60 | } else { 61 | const validValue = validateValueForRelatedKey(key, value) 62 | imageStream = imageStream[key](validValue) 63 | } 64 | return imageStream 65 | } 66 | -------------------------------------------------------------------------------- /dist/transformer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const sharp = require("sharp"); 4 | exports.default = transformer; 5 | let dynamicParamMethods = new Map([ 6 | ['gamma', 'gamma'], 7 | ['median', 'median'], 8 | ['rotate', 'rotate'], 9 | ['trim', 'trim'], 10 | ]); 11 | function transformer(options, size) { 12 | let imageStream = sharp(); 13 | for (const [key, value] of Object.entries(options)) { 14 | if (value) { 15 | imageStream = resolveImageStream(key, value, size, imageStream); 16 | } 17 | } 18 | return imageStream; 19 | } 20 | const objectHasOwnProperty = (source, prop) => Object.prototype.hasOwnProperty.call(source, prop); 21 | const hasProp = (value) => typeof value === 'object' && objectHasOwnProperty(value, 'type'); 22 | const validateFormat = (value) => { 23 | if (hasProp(value)) { 24 | return value.type; 25 | } 26 | return value; 27 | }; 28 | const validateValueForRelatedKey = (key, value) => { 29 | if (dynamicParamMethods.has(key)) { 30 | if (typeof value === 'boolean') { 31 | if (value) { 32 | return undefined; 33 | } 34 | } 35 | } 36 | return value; 37 | }; 38 | const resolveImageStream = (key, value, size, imageStream) => { 39 | if (key === 'resize') { 40 | if (!Array.isArray(size)) { 41 | imageStream = imageStream.resize(size.width, size.height, size.options); 42 | } 43 | } 44 | else if (key === 'toFormat') { 45 | imageStream = imageStream.toFormat(validateFormat(value), value.options); 46 | } 47 | else if (key === 'linear') { 48 | if (typeof value === 'boolean') { 49 | imageStream = imageStream.linear(); 50 | } 51 | else { 52 | imageStream = imageStream.linear(...value); 53 | } 54 | } 55 | else if (key === 'joinChannel') { 56 | imageStream = imageStream.joinChannel(value.images, value.options); 57 | } 58 | else { 59 | const validValue = validateValueForRelatedKey(key, value); 60 | imageStream = imageStream[key](validValue); 61 | } 62 | return imageStream; 63 | }; 64 | //# sourceMappingURL=transformer.js.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multer-sharp-s3", 3 | "version": "0.2.5", 4 | "description": "A plugin multer to transform image and upload to AWS S3", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "tslint -c tslint.json 'src/**/*.ts' '__tests__/**/*.ts'", 8 | "pretest": "npm run lint", 9 | "test": "jest --detectOpenHandles --runInBand --ci --coverage", 10 | "prebuild": "shx rm -rf dist", 11 | "build": "tsc -w false", 12 | "postbuild": "node scripts/copy.js", 13 | "format": "prettier --config prettier.config.js \"**/*.{ts,tsx,js,jsx,md}\"", 14 | "report-coverage": "cat ./coverage/lcov.info | ./node_modules/.bin/codecov", 15 | "release": "npm run build && git add dist/ && standard-version -a", 16 | "postrelease": "npm run release:github && npm run release:npm", 17 | "release:github": "git push --no-verify --follow-tags origin master", 18 | "release:npm": "npm publish", 19 | "dry": "npm run build && standard-version -a --dry-run" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/ikhsanalatsary/multer-sharp-s3.git" 24 | }, 25 | "author": "Abdul Fattah Ikhsan (http://ikhsannetwork.com/)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/ikhsanalatsary/multer-sharp-s3/issues" 29 | }, 30 | "homepage": "https://github.com/ikhsanalatsary/multer-sharp-s3#readme", 31 | "files": [ 32 | "LICENSE", 33 | "README.md", 34 | "dist/", 35 | "index.js", 36 | "index.d.ts", 37 | "yarn.lock" 38 | ], 39 | "keywords": [ 40 | "multer", 41 | "sharp", 42 | "image", 43 | "resize", 44 | "imageprocessing", 45 | "aws", 46 | "aws-sdk", 47 | "aws-s3", 48 | "s3", 49 | "storage engine", 50 | "storage", 51 | "form", 52 | "post", 53 | "multipart", 54 | "form-data", 55 | "formdata", 56 | "express", 57 | "middleware" 58 | ], 59 | "devDependencies": { 60 | "@types/core-js": "^2.5.0", 61 | "@types/express": "^4.16.1", 62 | "@types/jest": "^24.0.12", 63 | "@types/mime-types": "^2.1.0", 64 | "@types/multer": "^1.4.3", 65 | "@types/sharp": "^0.29.5", 66 | "@types/supertest": "^2.0.7", 67 | "aws-sdk": "^2.445.0", 68 | "codecov": "^3.7.0", 69 | "express": "^4.16.4", 70 | "jest": "^24.0.12", 71 | "multer": "^1.4.1", 72 | "prettier": "^1.17.0", 73 | "shx": "0.3.4", 74 | "standard-version": "^8.0.0", 75 | "supertest": "^4.0.2", 76 | "ts-jest": "^24.0.2", 77 | "ts-lint": "^4.5.1", 78 | "typescript": "3.4.5" 79 | }, 80 | "dependencies": { 81 | "mime-types": "2.1.24", 82 | "rxjs": "6.5.1", 83 | "sharp": "^0.30.0" 84 | }, 85 | "jest": { 86 | "moduleFileExtensions": [ 87 | "ts", 88 | "tsx", 89 | "js" 90 | ], 91 | "transform": { 92 | "\\.(ts|tsx)$": "/node_modules/ts-jest/preprocessor.js" 93 | }, 94 | "testRegex": "/__tests__/.*\\.(ts|tsx|js)$", 95 | "verbose": true, 96 | "notify": false, 97 | "bail": false 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResizeOptions, 3 | // RGBA, 4 | Region, 5 | ExtendOptions, 6 | ThresholdOptions, 7 | AvailableFormatInfo, 8 | OutputOptions, 9 | JpegOptions, 10 | PngOptions, 11 | WriteableMetadata, 12 | Kernel, 13 | Sharp, 14 | OverlayOptions, 15 | Color, 16 | FlattenOptions, 17 | Raw, 18 | SharpOptions as SharpOptionsCore 19 | } from 'sharp' 20 | import { S3 } from 'aws-sdk' 21 | 22 | export declare interface Size { 23 | width?: number 24 | height?: number 25 | options?: ResizeOptions 26 | } 27 | 28 | export declare interface Sharpen { 29 | sigma?: number 30 | flat?: number 31 | jagged?: number 32 | } 33 | 34 | export declare interface Bool { 35 | operand: string | Buffer 36 | operator: string 37 | options?: { raw: Raw } 38 | } 39 | 40 | export declare interface JoinChannel { 41 | images: string | Buffer | ArrayLike 42 | options?: SharpOptionsCore 43 | } 44 | 45 | export declare interface Modulate { 46 | brightness?: number 47 | saturation?: number 48 | hue?: number 49 | } 50 | 51 | export declare interface Threshold { 52 | threshold?: number 53 | options?: ThresholdOptions 54 | } 55 | 56 | export declare interface Format { 57 | type: string | AvailableFormatInfo 58 | options?: OutputOptions | JpegOptions | PngOptions 59 | } 60 | 61 | export declare interface ExtendSize extends Size { 62 | suffix: string, 63 | directory?: string, 64 | Body?: NodeJS.ReadableStream & Sharp 65 | } 66 | 67 | export declare type SharpOption = T 68 | 69 | export declare type ResizeOption = 70 | | SharpOption 71 | | Array> 72 | 73 | export declare type MaybeA = T | undefined | null 74 | 75 | export declare interface SharpOptions { 76 | _resize?: any 77 | resize?: ResizeOption 78 | // MARK: deprecated since sharp v0.22.0 79 | // crop?: SharpOption 80 | // background?: SharpOption 81 | // embed?: boolean 82 | // max?: boolean 83 | // min?: boolean 84 | // withoutEnlargement?: boolean 85 | // ignoreAspectRatio?: boolean 86 | modulate?: SharpOption 87 | composite?: SharpOption 88 | extract?: SharpOption 89 | trim?: SharpOption 90 | flatten?: SharpOption 91 | extend?: SharpOption 92 | negate?: SharpOption 93 | rotate?: SharpOption 94 | flip?: SharpOption 95 | flop?: SharpOption 96 | blur?: SharpOption 97 | sharpen?: SharpOption 98 | gamma?: SharpOption 99 | grayscale?: SharpOption 100 | greyscale?: SharpOption 101 | normalize?: SharpOption 102 | normalise?: SharpOption 103 | withMetadata?: SharpOption 104 | convolve?: SharpOption 105 | threshold?: SharpOption 106 | toColourspace?: SharpOption 107 | toColorspace?: SharpOption 108 | toFormat?: SharpOption 109 | linear?: SharpOption, MaybeA]> 110 | median?: SharpOption 111 | tint?: SharpOption 112 | removeAlpha?: SharpOption 113 | bandbool?: SharpOption 114 | boolean?: SharpOption 115 | joinChannel?: SharpOption 116 | extractChannel?: SharpOption 117 | ensureAlpha?: SharpOption 118 | } 119 | 120 | export declare interface CloudStorageOptions 121 | extends Partial { 122 | Key?: any 123 | multiple?: boolean 124 | s3: S3 125 | } 126 | 127 | export declare type S3StorageOptions = CloudStorageOptions & SharpOptions 128 | -------------------------------------------------------------------------------- /dist/types/types.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResizeOptions, 3 | // RGBA, 4 | Region, 5 | ExtendOptions, 6 | ThresholdOptions, 7 | AvailableFormatInfo, 8 | OutputOptions, 9 | JpegOptions, 10 | PngOptions, 11 | WriteableMetadata, 12 | Kernel, 13 | Sharp, 14 | OverlayOptions, 15 | Color, 16 | FlattenOptions, 17 | Raw, 18 | SharpOptions as SharpOptionsCore 19 | } from 'sharp' 20 | import { S3 } from 'aws-sdk' 21 | 22 | export declare interface Size { 23 | width?: number 24 | height?: number 25 | options?: ResizeOptions 26 | } 27 | 28 | export declare interface Sharpen { 29 | sigma?: number 30 | flat?: number 31 | jagged?: number 32 | } 33 | 34 | export declare interface Bool { 35 | operand: string | Buffer 36 | operator: string 37 | options?: { raw: Raw } 38 | } 39 | 40 | export declare interface JoinChannel { 41 | images: string | Buffer | ArrayLike 42 | options?: SharpOptionsCore 43 | } 44 | 45 | export declare interface Modulate { 46 | brightness?: number 47 | saturation?: number 48 | hue?: number 49 | } 50 | 51 | export declare interface Threshold { 52 | threshold?: number 53 | options?: ThresholdOptions 54 | } 55 | 56 | export declare interface Format { 57 | type: string | AvailableFormatInfo 58 | options?: OutputOptions | JpegOptions | PngOptions 59 | } 60 | 61 | export declare interface ExtendSize extends Size { 62 | suffix: string, 63 | directory?: string, 64 | Body?: NodeJS.ReadableStream & Sharp 65 | } 66 | 67 | export declare type SharpOption = T 68 | 69 | export declare type ResizeOption = 70 | | SharpOption 71 | | Array> 72 | 73 | export declare type MaybeA = T | undefined | null 74 | 75 | export declare interface SharpOptions { 76 | _resize?: any 77 | resize?: ResizeOption 78 | // MARK: deprecated since sharp v0.22.0 79 | // crop?: SharpOption 80 | // background?: SharpOption 81 | // embed?: boolean 82 | // max?: boolean 83 | // min?: boolean 84 | // withoutEnlargement?: boolean 85 | // ignoreAspectRatio?: boolean 86 | modulate?: SharpOption 87 | composite?: SharpOption 88 | extract?: SharpOption 89 | trim?: SharpOption 90 | flatten?: SharpOption 91 | extend?: SharpOption 92 | negate?: SharpOption 93 | rotate?: SharpOption 94 | flip?: SharpOption 95 | flop?: SharpOption 96 | blur?: SharpOption 97 | sharpen?: SharpOption 98 | gamma?: SharpOption 99 | grayscale?: SharpOption 100 | greyscale?: SharpOption 101 | normalize?: SharpOption 102 | normalise?: SharpOption 103 | withMetadata?: SharpOption 104 | convolve?: SharpOption 105 | threshold?: SharpOption 106 | toColourspace?: SharpOption 107 | toColorspace?: SharpOption 108 | toFormat?: SharpOption 109 | linear?: SharpOption, MaybeA]> 110 | median?: SharpOption 111 | tint?: SharpOption 112 | removeAlpha?: SharpOption 113 | bandbool?: SharpOption 114 | boolean?: SharpOption 115 | joinChannel?: SharpOption 116 | extractChannel?: SharpOption 117 | ensureAlpha?: SharpOption 118 | } 119 | 120 | export declare interface CloudStorageOptions 121 | extends Partial { 122 | Key?: any 123 | multiple?: boolean 124 | s3: S3 125 | } 126 | 127 | export declare type S3StorageOptions = CloudStorageOptions & SharpOptions 128 | -------------------------------------------------------------------------------- /dist/main.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.js","sourceRoot":"src/","sources":["main.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,+BAA2B;AAC3B,8CAAuD;AACvD,+BAA8B;AAC9B,2CAAmC;AAKnC,2DAAiD;AACjD,+CAAuC;AACvC,iDAAuC;AAmBvC,MAAa,SAAS;IAQpB,YAAY,OAAyB;QACnC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE;YACf,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;SAC9D;QAED,IAAI,CAAC,IAAI,qBAAQ,SAAS,CAAC,cAAc,EAAK,OAAO,CAAE,CAAA;QACvD,IAAI,CAAC,SAAS,GAAG,2BAAe,CAAC,OAAO,CAAC,CAAA;QAEzC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YACrB,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAA;SAClE;QAED,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,QAAQ,EAAE;YACrC,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,UAAU,EAAE;gBACvC,MAAM,IAAI,SAAS,CACjB,+DAA+D,OAAO,IAAI;qBACvE,IAAI,CAAC,GAAG,EAAE,CACd,CAAA;aACF;SACF;IACH,CAAC;IAEM,WAAW,CAAC,GAAY,EAAE,IAAW,EAAE,EAAsC;QAClF,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;QAChC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;QACjC,MAAM,MAAM,GAAG;YACb,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,oBAAoB,EAAE,IAAI,CAAC,oBAAoB;YAC/C,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,IAAI,EAAE,MAAM;YACZ,GAAG,EAAE,IAAI,CAAC,GAAG;SACd,CAAA;QAED,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,UAAU,EAAE;YACpC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;SACxC;QAED,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,UAAU,EAAE;YAClC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;gBACnC,IAAI,OAAO,EAAE;oBACX,EAAE,CAAC,OAAO,CAAC,CAAA;oBACX,OAAM;iBACP;gBACD,MAAM,CAAC,GAAG,GAAG,GAAG,CAAA;gBAEhB,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE;oBAC9B,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;iBACtC;qBAAM;oBACL,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;iBACvC;YACH,CAAC,CAAC,CAAA;SACH;aAAM;YACL,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE;gBAC9B,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;aACtC;iBAAM;gBACL,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;aACvC;SACF;IACH,CAAC;IAEM,WAAW,CAAC,GAAY,EAAE,IAAU,EAAE,EAA0B;QACrE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,CAAA;IACvE,CAAC;IAEO,cAAc,CACpB,MAAiC,EACjC,IAAW,EACX,EAAsC;QAEtC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;QAChC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;QAC/B,MAAM,EACJ,GAAG,EACH,kBAAkB,EAClB,WAAW,EAAE,eAAe,EAC5B,YAAY,EACZ,oBAAoB,EACpB,QAAQ,GACT,GAAG,IAAI,CAAA;QACR,IAAI,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE;YACzE,MAAM,KAAK,GAAG,WAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAC/B,KAAK;iBACF,IAAI,CACH,eAAG,CAAC,CAAC,IAAI,EAAE,EAAE;gBACX,MAAM,aAAa,GAAG,qBAAW,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;gBAClD,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU,EAAE;oBAC9B,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;iBACjC;qBAAM;oBACL,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;iBACvC;gBACD,OAAO,IAAI,CAAA;YACb,CAAC,CAAC,EACF,oBAAQ,CAAC,CAAC,IAAI,EAAE,EAAE;gBAChB,MAAM,IAAI,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,CAAA;gBAClC,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;oBAC5C,iBAAiB,EAAE,IAAI;iBACxB,CAAC,CAAA;gBACF,OAAO,WAAI,CACT,gBAAgB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;oBAC/B,yBACK,IAAI,EACJ,MAAM,CAAC,IAAI,IACd,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,EAC/B,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,IAC9B;gBACH,CAAC,CAAC,CACH,CAAA;YACH,CAAC,CAAC,EACF,oBAAQ,CAAC,CAAC,IAAI,EAAE,EAAE;gBAChB,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAI,CAAA;gBAClC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;gBACpC,IAAI,GAAG,GAAG,GAAG,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,CAAA;gBACxC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE;oBACrB,MAAM,CAAC,GAAG,EAAE,CAAA;oBACZ,GAAG,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAA;iBACnF;gBAED,IAAI,SAAS,qBACR,MAAM,IACT,IAAI;oBACJ,WAAW,EACX,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,GACvD,CAAA;gBAED,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;gBACxC,IAAI,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAA;gBACtC,MAAM,CAAC,EAAE,CAAC,oBAAoB,EAAE,UAAS,EAAE;oBACzC,IAAI,EAAE,CAAC,KAAK,EAAE;wBACZ,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAA;qBACpC;gBACH,CAAC,CAAC,CAAA;gBACF,MAAM,OAAO,GAAG,WAAI,CAClB,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;oBAC/B,2BAA2B;oBAC3B,MAAM,EAAE,IAAI,KAAc,IAAI,EAAhB,6BAAgB,CAAA;oBAC9B,yBACK,MAAM,EACN,IAAI,IACP,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAC1D;gBACH,CAAC,CAAC,CACH,CAAA;gBACD,OAAO,OAAO,CAAA;YAChB,CAAC,CAAC,EACF,mBAAO,EAAE,CACV;iBACA,SAAS,CAAC,CAAC,GAAG,EAAE,EAAE;gBACjB,MAAM,gBAAgB,GAAyB,GAAG,CAAC,MAAM,CACvD,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;oBACZ,2BAA2B;oBAC3B,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,KAAc,IAAI,EAAhB,sGAAgB,CAAA;oBAC3F,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,mBACd,GAAG;wBACH,kBAAkB;wBAClB,YAAY;wBACZ,oBAAoB;wBACpB,QAAQ,IACL,IAAI,IACP,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,eAAe,IAAI,WAAW,GAC5C,CAAA;oBACD,QAAQ,GAAG,mBAAM,CAAC,WAAW,CAAC,IAAI,SAAS,WAAW,EAAE,CAAA;oBACxD,OAAO,GAAG,CAAA;gBACZ,CAAC,EAAE,EAAE,CAAC,CAAA;gBAER,gBAAgB,CAAC,QAAQ,GAAG,QAAQ,CAAA;gBACpC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAA;YACxD,CAAC,EAAE,EAAE,CAAC,CAAA;SACT;aAAM;YACL,IAAI,WAAW,GAAG,CAAC,CAAA;YACnB,MAAM,aAAa,GAAG,qBAAW,CAAC,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;YAC9D,IAAI,SAAS,qBAAQ,MAAM,IAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,GAAE,CAAA;YAC/D,MAAM,IAAI,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,CAAA;YACvC,MAAM,KAAK,GAAG,WAAI,CAChB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;gBACnB,iBAAiB,EAAE,IAAI;aACxB,CAAC,CACH,CAAA;YACD,KAAK;iBACF,IAAI,CACH,eAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;gBACf,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAA;gBAChE,OAAO,QAAQ,CAAA;YACjB,CAAC,CAAC,EACF,oBAAQ,CAAC,CAAC,QAAQ,EAAE,EAAE;gBACpB,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;gBACxC,MAAM,CAAC,EAAE,CAAC,oBAAoB,EAAE,UAAS,EAAE;oBACzC,IAAI,EAAE,CAAC,KAAK,EAAE;wBACZ,WAAW,GAAG,EAAE,CAAC,KAAK,CAAA;qBACvB;gBACH,CAAC,CAAC,CAAA;gBACF,MAAM,OAAO,GAAG,WAAI,CAClB,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;oBAC5B,yBAAY,GAAG,EAAK,QAAQ,CAAC,IAAI,EAAE;gBACrC,CAAC,CAAC,CACH,CAAA;gBACD,OAAO,OAAO,CAAA;YAChB,CAAC,CAAC,CACH;iBACA,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE;gBACpB,2BAA2B;gBAC3B,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,KAAc,MAAM,EAAlB,qDAAkB,CAAA;gBAClD,MAAM,MAAM,mBACV,GAAG;oBACH,kBAAkB;oBAClB,YAAY;oBACZ,oBAAoB;oBACpB,QAAQ,IACL,IAAI,IACP,IAAI,EAAE,WAAW,IAAI,IAAI,EACzB,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,MAAM,EACvC,QAAQ,EAAE,mBAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,GAC5D,CAAA;gBACD,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YAC9C,CAAC,EAAE,EAAE,CAAC,CAAA;SACT;IACH,CAAC;IAEO,eAAe,CACrB,MAAiC,EACjC,IAAW,EACX,EAAsC;QAEtC,MAAM,EAAE,IAAI,EAAE,GAAG,IAAI,CAAA;QACrB,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;QACzB,IAAI,WAAW,GAAG,CAAC,CAAA;QACnB,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,QAAQ,CAAA;QACnD,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACrC,MAAM,CAAC,EAAE,CAAC,oBAAoB,EAAE,UAAS,EAAE;YACzC,IAAI,EAAE,CAAC,KAAK,EAAE;gBACZ,WAAW,GAAG,EAAE,CAAC,KAAK,CAAA;aACvB;QACH,CAAC,CAAC,CAAA;QACF,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YAC/B,MAAM,MAAM,mBACV,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,IAAI,CAAC,GAAG,EACb,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,QAAQ,EACzC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB,EAC3C,YAAY,EAAE,IAAI,CAAC,YAAY,EAC/B,oBAAoB,EAAE,IAAI,CAAC,oBAAoB,EAC/C,QAAQ,EAAE,IAAI,CAAC,QAAQ,IACpB,MAAM,CACV,CAAA;YACD,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAC9C,CAAC,EAAE,EAAE,CAAC,CAAA;IACR,CAAC;;AAlQgB,wBAAc,GAAG;IAChC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,aAAa;IACzC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI;IACtC,GAAG,EAAE,sBAAU;IACf,QAAQ,EAAE,KAAK;CAChB,CAAA;AANH,8BAoQC;AAED,SAAS,SAAS,CAAC,OAAyB;IAC1C,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,CAAA;AAC/B,CAAC;AAED,kBAAe,SAAS,CAAA"} -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __rest = (this && this.__rest) || function (s, e) { 3 | var t = {}; 4 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 5 | t[p] = s[p]; 6 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 7 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0) 8 | t[p[i]] = s[p[i]]; 9 | return t; 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const rxjs_1 = require("rxjs"); 13 | const operators_1 = require("rxjs/operators"); 14 | const sharp = require("sharp"); 15 | const mime_types_1 = require("mime-types"); 16 | const get_sharp_options_1 = require("./get-sharp-options"); 17 | const transformer_1 = require("./transformer"); 18 | const get_filename_1 = require("./get-filename"); 19 | class S3Storage { 20 | constructor(options) { 21 | if (!options.s3) { 22 | throw new Error('You have to specify s3 for AWS S3 to work.'); 23 | } 24 | this.opts = Object.assign({}, S3Storage.defaultOptions, options); 25 | this.sharpOpts = get_sharp_options_1.default(options); 26 | if (!this.opts.Bucket) { 27 | throw new Error('You have to specify Bucket for AWS S3 to work.'); 28 | } 29 | if (typeof this.opts.Key !== 'string') { 30 | if (typeof this.opts.Key !== 'function') { 31 | throw new TypeError(`Key must be a "string" or "function" or "undefined" but got ${typeof this 32 | .opts.Key}`); 33 | } 34 | } 35 | } 36 | _handleFile(req, file, cb) { 37 | const { opts, sharpOpts } = this; 38 | const { mimetype, stream } = file; 39 | const params = { 40 | Bucket: opts.Bucket, 41 | ACL: opts.ACL, 42 | CacheControl: opts.CacheControl, 43 | ContentType: opts.ContentType, 44 | Metadata: opts.Metadata, 45 | StorageClass: opts.StorageClass, 46 | ServerSideEncryption: opts.ServerSideEncryption, 47 | SSEKMSKeyId: opts.SSEKMSKeyId, 48 | Body: stream, 49 | Key: opts.Key, 50 | }; 51 | if (typeof opts._resize === 'function') { 52 | this.opts.resize = opts._resize(req); 53 | } 54 | if (typeof opts.Key === 'function') { 55 | opts.Key(req, file, (fileErr, Key) => { 56 | if (fileErr) { 57 | cb(fileErr); 58 | return; 59 | } 60 | params.Key = Key; 61 | if (mimetype.includes('image')) { 62 | this._uploadProcess(params, file, cb); 63 | } 64 | else { 65 | this._uploadNonImage(params, file, cb); 66 | } 67 | }); 68 | } 69 | else { 70 | if (mimetype.includes('image')) { 71 | this._uploadProcess(params, file, cb); 72 | } 73 | else { 74 | this._uploadNonImage(params, file, cb); 75 | } 76 | } 77 | } 78 | _removeFile(req, file, cb) { 79 | this.opts.s3.deleteObject({ Bucket: file.Bucket, Key: file.Key }, cb); 80 | } 81 | _uploadProcess(params, file, cb) { 82 | const { opts, sharpOpts } = this; 83 | let { stream, mimetype } = file; 84 | const { ACL, ContentDisposition, ContentType: optsContentType, StorageClass, ServerSideEncryption, Metadata, } = opts; 85 | if (opts.multiple && Array.isArray(opts.resize) && opts.resize.length > 0) { 86 | const sizes = rxjs_1.from(opts.resize); 87 | sizes 88 | .pipe(operators_1.map((size) => { 89 | const resizerStream = transformer_1.default(sharpOpts, size); 90 | if (size.suffix === 'original') { 91 | size.Body = stream.pipe(sharp()); 92 | } 93 | else { 94 | size.Body = stream.pipe(resizerStream); 95 | } 96 | return size; 97 | }), operators_1.mergeMap((size) => { 98 | const meta = { stream: size.Body }; 99 | const getMetaFromSharp = meta.stream.toBuffer({ 100 | resolveWithObject: true, 101 | }); 102 | return rxjs_1.from(getMetaFromSharp.then((result) => { 103 | return Object.assign({}, size, result.info, { ContentType: result.info.format, currentSize: result.info.size }); 104 | })); 105 | }), operators_1.mergeMap((size) => { 106 | const { Body, ContentType } = size; 107 | const keyDot = params.Key.split('.'); 108 | let key = `${params.Key}-${size.suffix}`; 109 | if (keyDot.length > 1) { 110 | keyDot.pop(); 111 | key = `${keyDot.join('.')}-${size.suffix}.${params.Key.split('.')[keyDot.length]}`; 112 | } 113 | let newParams = Object.assign({}, params, { Body, 114 | ContentType, Key: size.directory ? `${size.directory}/${key}` : key }); 115 | const upload = opts.s3.upload(newParams); 116 | let currentSize = { [size.suffix]: 0 }; 117 | upload.on('httpUploadProgress', function (ev) { 118 | if (ev.total) { 119 | currentSize[size.suffix] = ev.total; 120 | } 121 | }); 122 | const upload$ = rxjs_1.from(upload.promise().then((result) => { 123 | // tslint:disable-next-line 124 | const { Body } = size, rest = __rest(size, ["Body"]); 125 | return Object.assign({}, result, rest, { currentSize: size.currentSize || currentSize[size.suffix] }); 126 | })); 127 | return upload$; 128 | }), operators_1.toArray()) 129 | .subscribe((res) => { 130 | const mapArrayToObject = res.reduce((acc, curr) => { 131 | // tslint:disable-next-line 132 | const { suffix, ContentType, size, format, channels, options, currentSize } = curr, rest = __rest(curr, ["suffix", "ContentType", "size", "format", "channels", "options", "currentSize"]); 133 | acc[curr.suffix] = Object.assign({ ACL, 134 | ContentDisposition, 135 | StorageClass, 136 | ServerSideEncryption, 137 | Metadata }, rest, { size: currentSize, ContentType: optsContentType || ContentType }); 138 | mimetype = mime_types_1.lookup(ContentType) || `image/${ContentType}`; 139 | return acc; 140 | }, {}); 141 | mapArrayToObject.mimetype = mimetype; 142 | cb(null, JSON.parse(JSON.stringify(mapArrayToObject))); 143 | }, cb); 144 | } 145 | else { 146 | let currentSize = 0; 147 | const resizerStream = transformer_1.default(sharpOpts, sharpOpts.resize); 148 | let newParams = Object.assign({}, params, { Body: stream.pipe(resizerStream) }); 149 | const meta = { stream: newParams.Body }; 150 | const meta$ = rxjs_1.from(meta.stream.toBuffer({ 151 | resolveWithObject: true, 152 | })); 153 | meta$ 154 | .pipe(operators_1.map((metadata) => { 155 | newParams.ContentType = opts.ContentType || metadata.info.format; 156 | return metadata; 157 | }), operators_1.mergeMap((metadata) => { 158 | const upload = opts.s3.upload(newParams); 159 | upload.on('httpUploadProgress', function (ev) { 160 | if (ev.total) { 161 | currentSize = ev.total; 162 | } 163 | }); 164 | const upload$ = rxjs_1.from(upload.promise().then((res) => { 165 | return Object.assign({}, res, metadata.info); 166 | })); 167 | return upload$; 168 | })) 169 | .subscribe((result) => { 170 | // tslint:disable-next-line 171 | const { size, format, channels } = result, rest = __rest(result, ["size", "format", "channels"]); 172 | const endRes = Object.assign({ ACL, 173 | ContentDisposition, 174 | StorageClass, 175 | ServerSideEncryption, 176 | Metadata }, rest, { size: currentSize || size, ContentType: opts.ContentType || format, mimetype: mime_types_1.lookup(result.format) || `image/${result.format}` }); 177 | cb(null, JSON.parse(JSON.stringify(endRes))); 178 | }, cb); 179 | } 180 | } 181 | _uploadNonImage(params, file, cb) { 182 | const { opts } = this; 183 | const { mimetype } = file; 184 | let currentSize = 0; 185 | params.ContentType = params.ContentType || mimetype; 186 | const upload = opts.s3.upload(params); 187 | upload.on('httpUploadProgress', function (ev) { 188 | if (ev.total) { 189 | currentSize = ev.total; 190 | } 191 | }); 192 | upload.promise().then((result) => { 193 | const endRes = Object.assign({ size: currentSize, ACL: opts.ACL, ContentType: opts.ContentType || mimetype, ContentDisposition: opts.ContentDisposition, StorageClass: opts.StorageClass, ServerSideEncryption: opts.ServerSideEncryption, Metadata: opts.Metadata }, result); 194 | cb(null, JSON.parse(JSON.stringify(endRes))); 195 | }, cb); 196 | } 197 | } 198 | S3Storage.defaultOptions = { 199 | ACL: process.env.AWS_ACL || 'public-read', 200 | Bucket: process.env.AWS_BUCKET || null, 201 | Key: get_filename_1.default, 202 | multiple: false, 203 | }; 204 | exports.S3Storage = S3Storage; 205 | function s3Storage(options) { 206 | return new S3Storage(options); 207 | } 208 | exports.default = s3Storage; 209 | //# sourceMappingURL=main.js.map -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { from } from 'rxjs' 2 | import { map, mergeMap, toArray } from 'rxjs/operators' 3 | import * as sharp from 'sharp' 4 | import { lookup } from 'mime-types' 5 | import { ManagedUpload } from 'aws-sdk/lib/s3/managed_upload' 6 | import { StorageEngine } from 'multer' 7 | import { Request } from 'express' 8 | import { S3 } from 'aws-sdk' 9 | import getSharpOptions from './get-sharp-options' 10 | import transformer from './transformer' 11 | import defaultKey from './get-filename' 12 | import { S3StorageOptions, SharpOptions } from './types' 13 | 14 | export type EStream = { 15 | stream: NodeJS.ReadableStream & sharp.Sharp 16 | } 17 | export type EFile = Express.Multer.File & 18 | EStream & 19 | Partial 20 | export type Info = Partial< 21 | Express.Multer.File & 22 | ManagedUpload.SendData & 23 | S3.Types.PutObjectRequest & 24 | sharp.OutputInfo 25 | > 26 | export interface S3Storage { 27 | opts: S3StorageOptions 28 | sharpOpts: SharpOptions 29 | } 30 | export class S3Storage implements StorageEngine { 31 | protected static defaultOptions = { 32 | ACL: process.env.AWS_ACL || 'public-read', 33 | Bucket: process.env.AWS_BUCKET || null, 34 | Key: defaultKey, 35 | multiple: false, 36 | } 37 | 38 | constructor(options: S3StorageOptions) { 39 | if (!options.s3) { 40 | throw new Error('You have to specify s3 for AWS S3 to work.') 41 | } 42 | 43 | this.opts = { ...S3Storage.defaultOptions, ...options } 44 | this.sharpOpts = getSharpOptions(options) 45 | 46 | if (!this.opts.Bucket) { 47 | throw new Error('You have to specify Bucket for AWS S3 to work.') 48 | } 49 | 50 | if (typeof this.opts.Key !== 'string') { 51 | if (typeof this.opts.Key !== 'function') { 52 | throw new TypeError( 53 | `Key must be a "string" or "function" or "undefined" but got ${typeof this 54 | .opts.Key}` 55 | ) 56 | } 57 | } 58 | } 59 | 60 | public _handleFile(req: Request, file: EFile, cb: (error?: any, info?: Info) => void) { 61 | const { opts, sharpOpts } = this 62 | const { mimetype, stream } = file 63 | const params = { 64 | Bucket: opts.Bucket, 65 | ACL: opts.ACL, 66 | CacheControl: opts.CacheControl, 67 | ContentType: opts.ContentType, 68 | Metadata: opts.Metadata, 69 | StorageClass: opts.StorageClass, 70 | ServerSideEncryption: opts.ServerSideEncryption, 71 | SSEKMSKeyId: opts.SSEKMSKeyId, 72 | Body: stream, 73 | Key: opts.Key, 74 | } 75 | 76 | if (typeof opts._resize === 'function') { 77 | this.opts.resize = opts._resize(req); 78 | } 79 | 80 | if (typeof opts.Key === 'function') { 81 | opts.Key(req, file, (fileErr, Key) => { 82 | if (fileErr) { 83 | cb(fileErr) 84 | return 85 | } 86 | params.Key = Key 87 | 88 | if (mimetype.includes('image')) { 89 | this._uploadProcess(params, file, cb) 90 | } else { 91 | this._uploadNonImage(params, file, cb) 92 | } 93 | }) 94 | } else { 95 | if (mimetype.includes('image')) { 96 | this._uploadProcess(params, file, cb) 97 | } else { 98 | this._uploadNonImage(params, file, cb) 99 | } 100 | } 101 | } 102 | 103 | public _removeFile(req: Request, file: Info, cb: (error: Error) => void) { 104 | this.opts.s3.deleteObject({ Bucket: file.Bucket, Key: file.Key }, cb) 105 | } 106 | 107 | private _uploadProcess( 108 | params: S3.Types.PutObjectRequest, 109 | file: EFile, 110 | cb: (error?: any, info?: Info) => void 111 | ) { 112 | const { opts, sharpOpts } = this 113 | let { stream, mimetype } = file 114 | const { 115 | ACL, 116 | ContentDisposition, 117 | ContentType: optsContentType, 118 | StorageClass, 119 | ServerSideEncryption, 120 | Metadata, 121 | } = opts 122 | if (opts.multiple && Array.isArray(opts.resize) && opts.resize.length > 0) { 123 | const sizes = from(opts.resize) 124 | sizes 125 | .pipe( 126 | map((size) => { 127 | const resizerStream = transformer(sharpOpts, size) 128 | if (size.suffix === 'original') { 129 | size.Body = stream.pipe(sharp()) 130 | } else { 131 | size.Body = stream.pipe(resizerStream) 132 | } 133 | return size 134 | }), 135 | mergeMap((size) => { 136 | const meta = { stream: size.Body } 137 | const getMetaFromSharp = meta.stream.toBuffer({ 138 | resolveWithObject: true, 139 | }) 140 | return from( 141 | getMetaFromSharp.then((result) => { 142 | return { 143 | ...size, 144 | ...result.info, 145 | ContentType: result.info.format, 146 | currentSize: result.info.size, 147 | } 148 | }) 149 | ) 150 | }), 151 | mergeMap((size) => { 152 | const { Body, ContentType } = size 153 | const keyDot = params.Key.split('.') 154 | let key = `${params.Key}-${size.suffix}` 155 | if (keyDot.length > 1) { 156 | keyDot.pop() 157 | key = `${keyDot.join('.')}-${size.suffix}.${params.Key.split('.')[keyDot.length]}` 158 | } 159 | 160 | let newParams = { 161 | ...params, 162 | Body, 163 | ContentType, 164 | Key: size.directory ? `${size.directory}/${key}` : key, 165 | } 166 | 167 | const upload = opts.s3.upload(newParams) 168 | let currentSize = { [size.suffix]: 0 } 169 | upload.on('httpUploadProgress', function(ev) { 170 | if (ev.total) { 171 | currentSize[size.suffix] = ev.total 172 | } 173 | }) 174 | const upload$ = from( 175 | upload.promise().then((result) => { 176 | // tslint:disable-next-line 177 | const { Body, ...rest } = size 178 | return { 179 | ...result, 180 | ...rest, 181 | currentSize: size.currentSize || currentSize[size.suffix], 182 | } 183 | }) 184 | ) 185 | return upload$ 186 | }), 187 | toArray() 188 | ) 189 | .subscribe((res) => { 190 | const mapArrayToObject: { [k: string]: any } = res.reduce( 191 | (acc, curr) => { 192 | // tslint:disable-next-line 193 | const { suffix, ContentType, size, format, channels, options, currentSize, ...rest } = curr 194 | acc[curr.suffix] = { 195 | ACL, 196 | ContentDisposition, 197 | StorageClass, 198 | ServerSideEncryption, 199 | Metadata, 200 | ...rest, 201 | size: currentSize, 202 | ContentType: optsContentType || ContentType 203 | } 204 | mimetype = lookup(ContentType) || `image/${ContentType}` 205 | return acc 206 | }, {}) 207 | 208 | mapArrayToObject.mimetype = mimetype 209 | cb(null, JSON.parse(JSON.stringify(mapArrayToObject))) 210 | }, cb) 211 | } else { 212 | let currentSize = 0 213 | const resizerStream = transformer(sharpOpts, sharpOpts.resize) 214 | let newParams = { ...params, Body: stream.pipe(resizerStream) } 215 | const meta = { stream: newParams.Body } 216 | const meta$ = from( 217 | meta.stream.toBuffer({ 218 | resolveWithObject: true, 219 | }) 220 | ) 221 | meta$ 222 | .pipe( 223 | map((metadata) => { 224 | newParams.ContentType = opts.ContentType || metadata.info.format 225 | return metadata 226 | }), 227 | mergeMap((metadata) => { 228 | const upload = opts.s3.upload(newParams) 229 | upload.on('httpUploadProgress', function(ev) { 230 | if (ev.total) { 231 | currentSize = ev.total 232 | } 233 | }) 234 | const upload$ = from( 235 | upload.promise().then((res) => { 236 | return { ...res, ...metadata.info } 237 | }) 238 | ) 239 | return upload$ 240 | }) 241 | ) 242 | .subscribe((result) => { 243 | // tslint:disable-next-line 244 | const { size, format, channels, ...rest } = result 245 | const endRes = { 246 | ACL, 247 | ContentDisposition, 248 | StorageClass, 249 | ServerSideEncryption, 250 | Metadata, 251 | ...rest, 252 | size: currentSize || size, 253 | ContentType: opts.ContentType || format, 254 | mimetype: lookup(result.format) || `image/${result.format}` 255 | } 256 | cb(null, JSON.parse(JSON.stringify(endRes))) 257 | }, cb) 258 | } 259 | } 260 | 261 | private _uploadNonImage( 262 | params: S3.Types.PutObjectRequest, 263 | file: EFile, 264 | cb: (error?: any, info?: Info) => void 265 | ) { 266 | const { opts } = this 267 | const { mimetype } = file 268 | let currentSize = 0 269 | params.ContentType = params.ContentType || mimetype 270 | const upload = opts.s3.upload(params) 271 | upload.on('httpUploadProgress', function(ev) { 272 | if (ev.total) { 273 | currentSize = ev.total 274 | } 275 | }) 276 | upload.promise().then((result) => { 277 | const endRes = { 278 | size: currentSize, 279 | ACL: opts.ACL, 280 | ContentType: opts.ContentType || mimetype, 281 | ContentDisposition: opts.ContentDisposition, 282 | StorageClass: opts.StorageClass, 283 | ServerSideEncryption: opts.ServerSideEncryption, 284 | Metadata: opts.Metadata, 285 | ...result, 286 | } 287 | cb(null, JSON.parse(JSON.stringify(endRes))) 288 | }, cb) 289 | } 290 | } 291 | 292 | function s3Storage(options: S3StorageOptions) { 293 | return new S3Storage(options) 294 | } 295 | 296 | export default s3Storage 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multer-sharp-s3 2 | 3 | [![Build Status](https://travis-ci.org/ikhsanalatsary/multer-sharp-s3.svg?branch=master)](https://travis-ci.org/ikhsanalatsary/multer-sharp-s3) 4 | [![codecov.io](https://codecov.io/gh/ikhsanalatsary/multer-sharp-s3/coverage.svg?branch=master)](https://codecov.io/gh/ikhsanalatsary/multer-sharp?branch=master) [![Depedencies Status](https://david-dm.org/ikhsanalatsary/multer-sharp-s3.svg)](https://david-dm.org/ikhsanalatsary/multer-sharp-s3) [![devDepedencies Status](https://david-dm.org/ikhsanalatsary/multer-sharp-s3/dev-status.svg)](https://david-dm.org/ikhsanalatsary/multer-sharp-s3?type=dev) 5 | [![npm](https://img.shields.io/npm/dm/multer-sharp-s3.svg)](http://npm.im/multer-sharp-s3) 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/ikhsanalatsary/multer-sharp-s3.svg)](https://greenkeeper.io/) 7 | 8 | [![DigitalOcean Referral Badge](https://web-platforms.sfo2.digitaloceanspaces.com/WWW/Badge%203.svg)](https://www.digitalocean.com/?refcode=dec9802955b6&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) 9 | 10 | *** 11 | 12 | Multer Sharp S3 is streaming multer storage engine permit to transform / resize the image and upload to AWS S3. 13 | 14 | This project is mostly an integration piece for existing code samples from Multer's [storage engine documentation](https://github.com/expressjs/multer/blob/master/StorageEngine.md). With add-ons include [AWS S3](https://github.com/aws/aws-sdk-js) and [sharp](https://github.com/lovell/sharp) 15 | 16 | # Minimum Requirement: 17 | 18 | Node v12.13.0, npm v6+ 19 | 20 | # Installation 21 | 22 | npm: 23 | 24 | npm install --save aws-sdk multer-sharp-s3 25 | 26 | yarn: 27 | 28 | yarn add aws-sdk multer-sharp-s3 29 | 30 | # Tests 31 | Change aws configuration in your local. 32 | 33 | ``` 34 | yarn test 35 | ``` 36 | 37 | # Importing 38 | ### NodeJS 39 | 40 | ```javascript 41 | const s3Storage = require('multer-sharp-s3'); 42 | 43 | const storage = s3Storage(options); 44 | ``` 45 | 46 | ### TypeScript 47 | 48 | ```typescript 49 | import * as s3Storage from 'multer-sharp-s3'; 50 | 51 | const storage = s3Storage(options); 52 | ``` 53 | 54 | # Usage 55 | 56 | ```javascript 57 | const express = require('express'); 58 | const multer = require('multer'); 59 | const s3Storage = require('multer-sharp-s3'); 60 | const aws = require('aws-sdk'); 61 | 62 | aws.config.update({ 63 | secretAccessKey: config.uploads.aws.secretAccessKey, // Not working key, Your SECRET ACCESS KEY from AWS should go here, never share it!!! 64 | accessKeyId: config.uploads.aws.accessKeyId, // Not working key, Your ACCESS KEY ID from AWS should go here, never share it!!! 65 | region: config.uploads.aws.region, // region of your bucket 66 | }) 67 | 68 | const s3 = new aws.S3() 69 | const app = express(); 70 | 71 | // without resize image 72 | const storage = s3Storage({ 73 | s3, 74 | Bucket: config.uploads.aws.Bucket, 75 | Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myImage`, 76 | ACL: config.uploads.aws.ACL, 77 | }) 78 | const upload = multer({ storage: storage }) 79 | 80 | app.post('/upload', upload.single('myPic'), (req, res) => { 81 | console.log(req.file); // Print upload details 82 | res.send('Successfully uploaded!'); 83 | }); 84 | 85 | // or 86 | 87 | // single resize without Key 88 | const storage2 = gcsSharp({ 89 | s3, 90 | Bucket: config.uploads.aws.Bucket, 91 | ACL: config.uploads.aws.ACL, 92 | resize: { 93 | width: 400, 94 | height: 400 95 | }, 96 | max: true 97 | }); 98 | const upload2 = multer({ storage: storage2 }); 99 | 100 | app.post('/uploadandresize', upload2.single('myPic'), (req, res, next) => { 101 | console.log(req.file); // Print upload details 102 | res.send('Successfully uploaded!'); 103 | }); 104 | 105 | /* If you need generate image with specific size 106 | * simply to adding `multiple: true` property and 107 | * resize must be an `array` and must be include `suffix` property 108 | * and suffix has a special value that is 'original' 109 | * it will no transform image, just upload the image as is 110 | * example below with `Key` as callback function 111 | */ 112 | const storage = s3Storage({ 113 | Key: (req, file, cb) => { 114 | crypto.pseudoRandomBytes(16, (err, raw) => { 115 | cb(err, err ? undefined : raw.toString('hex')) 116 | }) 117 | }, 118 | s3, 119 | Bucket: config.uploads.aws.Bucket, 120 | multiple: true, 121 | resize: [ 122 | { suffix: 'xlg', width: 1200, height: 1200 }, 123 | { suffix: 'lg', width: 800, height: 800 }, 124 | { suffix: 'md', width: 500, height: 500 }, 125 | { suffix: 'sm', width: 300, height: 300 }, 126 | { suffix: 'xs', width: 100 }, 127 | { suffix: 'original' } 128 | ], 129 | }); 130 | const upload = multer({ storage }); 131 | 132 | app.post('/uploadmultiplesize', upload.single('myPic'), (req, res, next) => { 133 | console.log(req.file); // print output 134 | res.send('Successfully uploaded!'); 135 | }); 136 | 137 | /* 138 | * If the directory property exists, 139 | * the suffix property is ignored and 140 | * inserted separated by Bucket's directory. 141 | */ 142 | const storage3 = s3Storage({ 143 | Key: (req, file, cb) => { 144 | crypto.pseudoRandomBytes(16, (err, raw) => { 145 | cb(err, err ? undefined : raw.toString('hex')) 146 | }) 147 | }, 148 | s3, 149 | Bucket: config.uploads.aws.Bucket, 150 | multiple: true, 151 | resize: [ 152 | { suffix: 'lg', directory: 'large', width: 800, height: 800 }, // insert BUCKET/large/filename 153 | { suffix: 'md', directory: 'medium', width: 500, height: 500 }, // insert BUCKET/medium/filename 154 | { suffix: 'sm', directory: 'small', width: 300, height: 300 }, // insert BUCKET/small/filename 155 | ], 156 | }); 157 | const upload3 = multer({ storage3 }); 158 | 159 | app.post('/uploadmultiplesize', upload3.single('myPic'), (req, res, next) => { 160 | console.log(req.file); // print output 161 | res.send('Successfully uploaded!'); 162 | }); 163 | 164 | // also can upload any file (non image type) 165 | const storage = s3Storage({ 166 | s3, 167 | Bucket: config.uploads.aws.Bucket, 168 | Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myFile`, 169 | ACL: config.uploads.aws.ACL, 170 | // resize or any sharp options will ignore when uploading non image file type 171 | resize: { 172 | width: 400, 173 | height: 400, 174 | }, 175 | }) 176 | const upload = multer({ storage }) 177 | 178 | app.post('/uploadfile', upload.single('myFile'), (req, res, next) => { 179 | console.log(req.file); // print output 180 | res.send('Successfully uploaded!'); 181 | }); 182 | 183 | ``` 184 | 185 | for more example you can see [here](https://github.com/ikhsanalatsary/multer-sharp-s3/blob/master/__tests__/implementation.spec.ts) 186 | 187 | 188 | #### Multer-Sharp-S3 options 189 | multer sharp s3 is inherit from s3 upload property [putObjectRequest](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property). 190 | Below are special / custom options from this package 191 | 192 | | option | default | value | role | 193 | | ------ | ------- | ----- | ---- | 194 | | S3 | no | `object` | instance from AWS.S3 class. it mus be specify | 195 | | Key | randomString | `string` or `function` | your s3 Key | 196 | | Bucket | no | `string` | Required your bucket name on AWS S3 to upload. Environment variable - AWS_BUCKET | 197 | | ACL | 'public-read' | `string` | Required acl credentials file for AWS S3 | 198 | | multiple | false | `boolean` | for multiple resize to work | 199 | | resize | no | `object` or `Array` when multiple is true. **Note:** suffix must be specify when using resize as `Array` | size specification | 200 | 201 | #### Sharp options 202 | Please visit this **[sharp](http://sharp.pixelplumbing.com/)** for detailed overview of specific option. 203 | 204 | multer sharp s3 embraces sharp options, as table below: 205 | 206 | | option | default | value | role | 207 | | ------ | ------- | ----- | ---- | 208 | | resize | `undefined` | `object` for output image, as follow: `{ width?: 300, height?: 200, options?: {...resizeOptions} }`. doc: [sharpResizeOptions](https://sharp.pixelplumbing.com/api-resize#resize) | size specification | 209 | | crop | `undefined` | | crop image | 210 | | background | `undefined` | | set the background for the embed, flatten and extend operations. | 211 | | embed | `undefined` | | embed on canvas | 212 | | max | `undefined` | | set maximum output dimension | 213 | | min | `undefined` | | set minimum output dimension | 214 | | withoutEnlargement | `undefined` | | do not enlarge small images | 215 | | ignoreAspectRatio | `undefined` | | ignore aspect ration while resizing images | 216 | | extract | `undefined` | | extract specific part of image | 217 | | trim | `undefined` | | Trim **boring** pixels from all edges | 218 | | flatten | `undefined` | | Merge alpha transparency channel, if any, with background. | 219 | | extend | `undefined` | | Extends/pads the edges of the image with background. | 220 | | negate | `undefined` | | Produces the **negative** of the image. | 221 | | rotate | `undefined` | | Rotate the output image by either an explicit angle | 222 | | flip | `undefined` | | Flip the image about the vertical Y axis. | 223 | | flop | `undefined` | | Flop the image about the horizontal X axis. | 224 | | blur | `undefined` | | Mild blur of the output image | 225 | | sharpen | `undefined` | | Mild sharpen of the output image | 226 | | gamma | `undefined` | | Apply a gamma correction. | 227 | | grayscale *or* greyscale | `undefined` | | Convert to 8-bit greyscale; 256 shades of grey. | 228 | | normalize *or* normalise | `undefined` | | Enhance output image contrast by stretching its luminance to cover the full dynamic range. | 229 | | withMetadata | `undefined` | | Include all metadata (EXIF, XMP, IPTC) from the input image in the output image. 230 | | convolve | `undefined` | | Convolve the image with the specified kernel. 231 | | threshold | `undefined` | | Any pixel value greather than or equal to the threshold value will be set to 255, otherwise it will be set to 0 232 | | toColourspace *or* toColorspace | `undefined` | | Set the output colourspace. By default output image will be web-friendly sRGB, with additional channels interpreted as alpha channels. 233 | | toFormat | `undefined` | `'jpeg'`, `'png'`, `'magick'`, `'webp'`, `'tiff'`, `'openslide'`, `'dz'`, `'ppm'`, `'fits'`, `'gif'`, `'svg'`, `'pdf'`, `'v'`, `'raw'` or `object`. if `object` specify as follow: `{ type: 'png', options: { ...toFormatOptions } }` doc: [sharpToFormat](https://sharp.pixelplumbing.com/api-output#toformat) | type of output file to produce.| 234 | 235 | 236 | **NOTE** Some of the contents in the above table maybe is not be updated, you can check more [here](https://github.com/ikhsanalatsary/multer-sharp-s3/blob/c40c1d2ed9ace33df51f1ff079d1fb894c521db3/src/get-sharp-options.ts#L3) 237 | *** 238 | 239 | ## Why 240 | Because We need to transform an image using sharp and upload it to AWS S3 using multer middleware at once. Build on top with [TypeScript](https://www.typescriptlang.org/) and reactive using [RxJS](https://rxjs-dev.firebaseapp.com/) as helper library in this package. 241 | 242 | ![Mantra](http://i.imgur.com/AIimQ8C.jpg) 243 | 244 | refer to: [intro rx](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754#modelling-the-3-suggestions-with-streams) 245 | 246 | ## License 247 | [MIT](http://opensource.org/licenses/MIT) 248 | Copyright (c) 2017 - forever [Abdul Fattah Ikhsan](https://twitter.com/abdfattahikhsan) 249 | 250 | -------------------------------------------------------------------------------- /__tests__/implementation.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-disable no-console */ 4 | 5 | import * as express from 'express' 6 | import * as supertest from 'supertest' 7 | import * as multer from 'multer' 8 | import * as aws from 'aws-sdk' 9 | import * as crypto from 'crypto' 10 | import * as sharp from 'sharp' 11 | 12 | import multerSharp from '../src/main' 13 | const config = { 14 | uploads: { 15 | aws: { 16 | Bucket: process.env.AWS_BUCKET, 17 | ACL: 'private', 18 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 19 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 20 | region: process.env.AWS_REGION, 21 | }, 22 | }, 23 | } 24 | 25 | aws.config.update({ 26 | secretAccessKey: config.uploads.aws.secretAccessKey, // Not working key, Your SECRET ACCESS KEY from AWS should go here, never share it!!! 27 | accessKeyId: config.uploads.aws.accessKeyId, // Not working key, Your ACCESS KEY ID from AWS should go here, never share it!!! 28 | region: config.uploads.aws.region, // region of your bucket 29 | }) 30 | 31 | const s3 = new aws.S3() 32 | 33 | const app = express() 34 | // const wrongConfig = { 35 | // uploads: { 36 | // gcsUpload: { 37 | // bucket: 'multer.appspot.com', // Required : bucket name to upload 38 | // projectId: 'multer', // Required : Google project ID 39 | // keyFilename: 'test/firebase.auth.json', // Required : JSON credentials file for Google Cloud Storage 40 | // destination: 'public', // Optional : destination folder to store your file for Google Cloud Storage, default: '' 41 | // acl: 'publicRead' // Required : acl credentials file for Google Cloud Storage, publicrRead or private, default: private 42 | // } 43 | // } 44 | // }; 45 | 46 | let lastRes = null 47 | let lastReq = lastRes 48 | 49 | const storage = multerSharp({ 50 | s3, 51 | Bucket: config.uploads.aws.Bucket, 52 | // Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myPic`, 53 | ACL: config.uploads.aws.ACL, 54 | resize: { 55 | width: 400, 56 | height: 400, 57 | options: { 58 | kernel: sharp.kernel.lanczos2, 59 | }, 60 | }, 61 | }) 62 | const upload = multer({ storage }) 63 | const storage2 = multerSharp({ 64 | Key: (req, file, cb) => { 65 | crypto.pseudoRandomBytes(16, (err, raw) => { 66 | err = Error('Something wrong') 67 | Error.captureStackTrace(this, this.Key) 68 | cb(err, err ? undefined : raw.toString('hex')) 69 | }) 70 | }, 71 | s3, 72 | Bucket: config.uploads.aws.Bucket, 73 | resize: { 74 | width: 400, 75 | height: 400, 76 | }, 77 | }) 78 | const upload2 = multer({ storage: storage2 }) 79 | 80 | const storage3 = multerSharp({ 81 | s3, 82 | Bucket: config.uploads.aws.Bucket, 83 | Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myFile`, 84 | ACL: config.uploads.aws.ACL, 85 | // resize (sharp options) will ignore when uploading non image file type 86 | resize: { 87 | width: 400, 88 | height: 400, 89 | }, 90 | }) 91 | const upload3 = multer({ storage: storage3 }) 92 | 93 | const storage4 = multerSharp({ 94 | s3, 95 | Bucket: config.uploads.aws.Bucket, 96 | Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myPic`, 97 | ACL: config.uploads.aws.ACL, 98 | resize: { width: 200 }, 99 | trim: true, 100 | flatten: true, 101 | extend: { top: 10, bottom: 20, left: 10, right: 10 }, 102 | negate: true, 103 | rotate: true, 104 | flip: true, 105 | flop: true, 106 | blur: true, 107 | sharpen: true, 108 | gamma: false, 109 | greyscale: true, 110 | normalize: true, 111 | linear: [1.0, 1.0], 112 | median: true, 113 | withMetadata: true, 114 | composite: [ 115 | { 116 | input: { 117 | create: { 118 | width: 100, 119 | height: 100, 120 | channels: 3, 121 | background: 'rgb(255, 255, 255)', 122 | }, 123 | }, 124 | premultiplied: true, 125 | }, 126 | ], 127 | removeAlpha: true, 128 | bandbool: 'and', 129 | tint: 'rgb(255, 255, 255)', 130 | toFormat: { 131 | type: 'jpeg', 132 | options: { 133 | progressive: true, 134 | quality: 90, 135 | }, 136 | }, 137 | }) 138 | const upload4 = multer({ storage: storage4 }) 139 | 140 | const storage5 = multerSharp({ 141 | s3, 142 | Bucket: config.uploads.aws.Bucket, 143 | Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myPic`, 144 | ACL: config.uploads.aws.ACL, 145 | resize: { width: 400, height: 400 }, 146 | extract: { left: 0, top: 2, width: 50, height: 100 }, 147 | trim: 50, 148 | flatten: true, 149 | extend: { top: 10, bottom: 20, left: 10, right: 10 }, 150 | negate: true, 151 | rotate: 90, 152 | flip: true, 153 | flop: true, 154 | blur: true, 155 | sharpen: true, 156 | gamma: 2.5, 157 | grayscale: true, 158 | normalise: true, 159 | toFormat: 'jpeg', 160 | withMetadata: { 161 | orientation: 4, 162 | }, 163 | convolve: { 164 | width: 3, 165 | height: 3, 166 | kernel: [-1, 0, 1, -2, 0, 2, -1, 0, 1], 167 | }, 168 | threshold: 129, 169 | // extractChannel: 'green', 170 | toColorspace: 'b-w', 171 | }) 172 | const upload5 = multer({ storage: storage5 }) 173 | 174 | const storage6 = multerSharp({ 175 | s3, 176 | Bucket: config.uploads.aws.Bucket, 177 | Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myPic`, 178 | ACL: config.uploads.aws.ACL, 179 | resize: { 180 | width: 400, 181 | height: 400, 182 | }, 183 | extract: { left: 0, top: 2, width: 400, height: 400 }, 184 | }) 185 | const upload6 = multer({ storage: storage6 }) 186 | 187 | const storage8 = multerSharp({ 188 | s3, 189 | Bucket: config.uploads.aws.Bucket, 190 | Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myPic`, 191 | ACL: config.uploads.aws.ACL, 192 | multiple: true, 193 | resize: [ 194 | // { suffix: 'xlg', width: 1200, height: 1200 }, 195 | { suffix: 'original' }, 196 | { suffix: 'md', width: 500, height: 500 }, 197 | { suffix: 'sm', width: 300, height: 300 }, 198 | { suffix: 'xs', width: 100, height: 100 }, 199 | ], 200 | toFormat: 'jpeg', 201 | }) 202 | const upload8 = multer({ storage: storage8 }) 203 | 204 | // const storage9 = multerSharp({ 205 | // bucket: wrongConfig.uploads.gcsUpload.bucket, 206 | // projectId: wrongConfig.uploads.gcsUpload.projectId, 207 | // keyFilename: wrongConfig.uploads.gcsUpload.keyFilename, 208 | // sizes: [ 209 | // // { suffix: 'xlg', width: 1200, height: 1200 }, 210 | // // { suffix: 'lg', width: 800, height: 800 }, 211 | // { suffix: 'md', width: 500, height: 500 }, 212 | // { suffix: 'sm', width: 300, height: 300 }, 213 | // { suffix: 'xs', width: 100, height: 100 } 214 | // ] 215 | // }); 216 | // const upload9 = multer({ storage: storage9 }); 217 | 218 | // const storage10 = multerSharp({ 219 | // bucket: config.uploads.gcsUpload.bucket, 220 | // projectId: config.uploads.gcsUpload.projectId, 221 | // keyFilename: config.uploads.gcsUpload.keyFilename, 222 | // acl: config.uploads.gcsUpload.acl, 223 | // sizes: [ 224 | // // { suffix: 'xlg', width: 1200, height: 1200 }, 225 | // // { suffix: 'lg', width: 800, height: 800 }, 226 | // { suffix: 'md', width: 500, height: 500 }, 227 | // { suffix: 'sm', width: 300, height: 300 }, 228 | // { suffix: 'xs', width: 100, height: 100 } 229 | // ], 230 | // extract: { left: 0, top: 2, width: 400, height: 400 } 231 | // }); 232 | // const upload10 = multer({ storage: storage10 }); 233 | 234 | const storage11 = multerSharp({ 235 | s3, 236 | Bucket: config.uploads.aws.Bucket, 237 | // Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myFile`, 238 | ACL: config.uploads.aws.ACL, 239 | joinChannel: { 240 | images: 'ss' 241 | }, 242 | }) 243 | const upload11 = multer({ storage: storage11 }) 244 | 245 | // express setup 246 | app.get('/book', (req, res) => { 247 | res.sendStatus(200) 248 | }) 249 | 250 | // express setup 251 | app.post('/upload', (req, res, next) => { 252 | upload.single('myPic')(req, res, (err) => { 253 | lastReq = req 254 | lastRes = res 255 | res.sendStatus(200) 256 | next() 257 | }) 258 | }) 259 | 260 | // express setup 261 | app.post('/uploadwitherrorkey', (req, res, next) => { 262 | upload2.single('myPic')(req, res, (errorFile) => { 263 | lastReq = req 264 | lastRes = res 265 | // console.log(errorFile.stack); 266 | res.status(400).json({ message: errorFile.message }) 267 | 268 | next() 269 | }) 270 | }) 271 | 272 | // express setup 273 | app.post('/uploadfile', upload3.single('myFile'), (req, res, next) => { 274 | lastReq = req 275 | lastRes = res 276 | res.sendStatus(200) 277 | next() 278 | }) 279 | 280 | // express setup 281 | app.post( 282 | '/uploadwithsharpsetting', 283 | upload4.single('myPic'), 284 | (req, res, next) => { 285 | lastReq = req 286 | lastRes = res 287 | res.sendStatus(200) 288 | next() 289 | } 290 | ) 291 | 292 | // // express setup 293 | app.post('/uploadanddelete', (req, res, next) => { 294 | upload5.single('myPic')(req, res, (err) => { 295 | if (err) { 296 | next(err) 297 | } 298 | storage5._removeFile(req, req.file, (err) => { 299 | // eslint-disable-line no-underscore-dangle 300 | if (err) { 301 | next(err) 302 | } 303 | res.sendStatus(200) 304 | next() 305 | }) 306 | }) 307 | }) 308 | 309 | app.post('/uploadwithtransformerror', (req, res) => { 310 | const uploadAndError = upload6.single('myPic') 311 | uploadAndError(req, res, (uploadError) => { 312 | if (uploadError) { 313 | res.status(400).json({ message: 'Something went wrong when resize' }) 314 | } 315 | }) 316 | }) 317 | 318 | app.post('/uploadwitherror', (req, res) => { 319 | aws.config.update({ 320 | secretAccessKey: 'sllll', // Not working key, Your SECRET ACCESS KEY from AWS should go here, never share it!!! 321 | accessKeyId: config.uploads.aws.accessKeyId, // Not working key, Your ACCESS KEY ID from AWS should go here, never share it!!! 322 | region: config.uploads.aws.region, // region of your bucket 323 | }) 324 | const s3 = new aws.S3() 325 | const storage7 = multerSharp({ 326 | s3, 327 | Bucket: config.uploads.aws.Bucket, 328 | Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myPic`, 329 | }) 330 | const upload7 = multer({ storage: storage7 }) 331 | const uploadAndError = upload7.single('myPic') 332 | uploadAndError(req, res, (uploadError) => { 333 | if (uploadError) { 334 | res.status(uploadError.statusCode).json({ message: uploadError.message }) 335 | } 336 | }) 337 | }) 338 | 339 | app.post( 340 | '/uploadfilewithdefaultkey', 341 | upload11.single('myFile'), 342 | (req, res, next) => { 343 | lastReq = req 344 | lastRes = res 345 | res.sendStatus(200) 346 | next() 347 | } 348 | ) 349 | 350 | // express setup 351 | app.post('/uploadwithmultiplesize', (req, res, next) => { 352 | upload8.single('myPic')(req, res, (err) => { 353 | lastReq = req 354 | lastRes = res 355 | if (err) { 356 | throw err 357 | } 358 | res.sendStatus(200) 359 | next() 360 | }) 361 | // lastReq = req 362 | // lastRes = res 363 | // console.log('req ', req.file) 364 | 365 | // if (lastReq && lastReq.file) { 366 | // res.sendStatus(200) 367 | // } 368 | // next() 369 | }) 370 | 371 | // app.post('/uploadwithmultiplesizetransformerror', (req, res) => { 372 | // const uploadAndError = upload9.single('myPic'); 373 | // uploadAndError(req, res, (uploadError) => { 374 | // if (uploadError) { 375 | // res.status(400).json({ message: 'Something went wrong when resize' }); 376 | // } 377 | // }); 378 | // }); 379 | // app.post('/uploadwithmultiplesizegcerror', upload10.single('myPic'), (req, res) => { 380 | // lastReq = req; 381 | // lastRes = res; 382 | // }); 383 | 384 | // Run Test 385 | describe('S3Storage', () => { 386 | it('should throw an error, cause s3 is not specify', (done) => { 387 | expect( 388 | multerSharp.bind(multerSharp, { 389 | // s3, 390 | Bucket: config.uploads.aws.Bucket, 391 | Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myPic`, 392 | }) 393 | ).toThrow('You have to specify s3 for AWS S3 to work.') 394 | done() 395 | }) 396 | it('should throw an error, cause Bucket is not specify', (done) => { 397 | expect( 398 | multerSharp.bind(multerSharp, { 399 | s3, 400 | Bucket: undefined, 401 | // Key: `${config.uploads.aws.Bucket}/test/${Date.now()}-myPic`, 402 | }) 403 | ).toThrow('You have to specify Bucket for AWS S3 to work.') 404 | done() 405 | }) 406 | it('should work without Key', (done) => { 407 | expect( 408 | multerSharp.bind(multerSharp, { 409 | s3, 410 | Bucket: config.uploads.aws.Bucket, 411 | }) 412 | ).not.toThrow('anything') 413 | done() 414 | }) 415 | it('should throw an error if Key is not string or function', (done) => { 416 | const opts = { s3, Bucket: config.uploads.aws.Bucket, Key: true } 417 | expect(multerSharp.bind(multerSharp, opts)).toThrow( 418 | `Key must be a "string" or "function" or "undefined" but got ${typeof opts.Key}` 419 | ) 420 | done() 421 | }) 422 | }) 423 | 424 | describe('Upload test', () => { 425 | jest.setTimeout(15000) 426 | it('initial server', (done) => { 427 | supertest(app) 428 | .get('/book') 429 | .expect(200, done) 430 | }) 431 | it('successfully uploads a file', (done) => { 432 | supertest(app) 433 | .post('/upload') 434 | .attach('myPic', '__tests__/nodejs-512.png') 435 | .end((err) => { 436 | const file = lastReq.file 437 | // console.log('filee ', file); 438 | expect(file).toBeDefined() 439 | expect(file).toHaveProperty('Location') 440 | expect(file).toHaveProperty('fieldname') 441 | expect(file).toHaveProperty('encoding') 442 | expect(file).toHaveProperty('mimetype') 443 | expect(file).toHaveProperty('originalname') 444 | expect(file).toHaveProperty('Key') 445 | done() 446 | }) 447 | // .expect(200, done); 448 | }) 449 | it('return a req.file with multiple sizes', (done) => { 450 | // jest.setTimeout(done, 1000); 451 | supertest(app) 452 | .post('/uploadwithmultiplesize') 453 | .attach('myPic', '__tests__/nodejs-512.png') 454 | .end(() => { 455 | const file = lastReq.file 456 | expect(file).toHaveProperty('original') 457 | expect(file).toHaveProperty('md') 458 | expect(file).toHaveProperty('sm') 459 | expect(file).toHaveProperty('xs') 460 | expect(file).toHaveProperty('fieldname') 461 | expect(file).toHaveProperty('encoding') 462 | expect(file).toHaveProperty('mimetype') 463 | expect(file).toHaveProperty('originalname') 464 | expect(file.xs).toHaveProperty('Key') 465 | expect(file.md).toHaveProperty('Key') 466 | expect(file.sm).toHaveProperty('Key') 467 | expect(file.original).toHaveProperty('Key') 468 | expect(file.xs).toHaveProperty('Location') 469 | expect(file.md).toHaveProperty('Location') 470 | expect(file.sm).toHaveProperty('Location') 471 | expect(file.original).toHaveProperty('Location') 472 | done() 473 | }) 474 | }) 475 | 476 | it('upload file without Key', (done) => { 477 | supertest(app) 478 | .post('/uploadfilewithdefaultkey') 479 | .attach('myFile', '.travis.yml') 480 | .end((err, res) => { 481 | const { file } = lastReq 482 | expect(file).toHaveProperty('Key') 483 | expect(file).toHaveProperty('fieldname') 484 | expect(file).toHaveProperty('encoding') 485 | expect(file).toHaveProperty('mimetype') 486 | expect(file).toHaveProperty('originalname') 487 | expect(file.mimetype).toMatch('text/yaml') 488 | expect(file.fieldname).toMatch('myFile') 489 | expect(file.Location).toMatch('aws') 490 | done() 491 | }) 492 | }) 493 | 494 | // it('returns a req.file with the Google Cloud Storage filename and path', (done) => { 495 | // supertest(app) 496 | // .post('/upload') 497 | // .attach('myPic', 'test/nodejs-512.png') 498 | // .end(() => { 499 | // const file = lastReq.file; 500 | // console.log(file); 501 | // expect(file).toHaveProperty('path'); 502 | // expect(file).toHaveProperty('filename'); 503 | // expect(file).toHaveProperty('fieldname'); 504 | // expect(file).toHaveProperty('encoding'); 505 | // expect(file).toHaveProperty('mimetype'); 506 | // expect(file).toHaveProperty('originalname'); 507 | // expect(file.fieldname).toMatch('myPic'); 508 | // expect(file.path).toMatch('googleapis'); 509 | // done(); 510 | // }); 511 | // }); 512 | it('return a req.file with type application/javascript ', (done) => { 513 | supertest(app) 514 | .post('/uploadfile') 515 | .attach('myFile', 'wallaby.js') 516 | .end(() => { 517 | const file = lastReq.file 518 | expect(file).toHaveProperty('Key') 519 | expect(file).toHaveProperty('fieldname') 520 | expect(file).toHaveProperty('encoding') 521 | expect(file).toHaveProperty('mimetype') 522 | expect(file).toHaveProperty('originalname') 523 | expect(file.mimetype).toMatch('application/javascript') 524 | expect(file.fieldname).toMatch('myFile') 525 | expect(file.Location).toMatch('aws') 526 | done() 527 | }) 528 | }) 529 | it('return an error when creating random key', (done) => { 530 | supertest(app) 531 | .post('/uploadwitherrorkey') 532 | .attach('myPic', '__tests__/nodejs-512.png') 533 | .end((err, res) => { 534 | expect(res.status).toEqual(400) 535 | expect(res.body.message).toEqual('Something wrong') 536 | done() 537 | }) 538 | }) 539 | it('return a req.file with mimetype image/jpeg', (done) => { 540 | supertest(app) 541 | .post('/uploadwithsharpsetting') 542 | .attach('myPic', '__tests__/nodejs-512.png') 543 | .end(() => { 544 | const file = lastReq.file 545 | expect(file).toHaveProperty('Key') 546 | expect(file).toHaveProperty('fieldname') 547 | expect(file).toHaveProperty('encoding') 548 | expect(file).toHaveProperty('mimetype') 549 | expect(file).toHaveProperty('originalname') 550 | expect(file.fieldname).toMatch('myPic') 551 | expect(file.mimetype).toMatch('image/jpeg') 552 | expect(file.Location).toMatch('amazonaws') 553 | done() 554 | }) 555 | }) 556 | it('upload and delete after', (done) => { 557 | supertest(app) 558 | .post('/uploadanddelete') 559 | .attach('myPic', '__tests__/nodejs-512.png') 560 | .expect(200, done) 561 | }) 562 | it('upload and return error, cause transform/resize error', (done) => { 563 | supertest(app) 564 | .post('/uploadwithtransformerror') 565 | .attach('myPic', '__tests__/nodejs-512.png') 566 | .end((err, res) => { 567 | expect(res.status).toEqual(400) 568 | expect(res.body.message).toEqual('Something went wrong when resize') 569 | done() 570 | }) 571 | }) 572 | it('upload and return error, cause wrong configuration', (done) => { 573 | supertest(app) 574 | .post('/uploadwitherror') 575 | .attach('myPic', '__tests__/nodejs-512.png') 576 | .end((err, res) => { 577 | expect(res.status).toEqual(403) 578 | expect(res.body.message).toEqual( 579 | 'The request signature we calculated does not match the signature you provided. Check your key and signing method.' 580 | ) 581 | // expect(true).toBe(true) 582 | done() 583 | }) 584 | }) 585 | // it('upload multisize and return error, cause transform/resize error', (done) => { 586 | // supertest(app) 587 | // .post('/uploadwithmultiplesizetransformerror') 588 | // .attach('myPic', 'test/nodejs-512.png') 589 | // .end((err, res) => { 590 | // expect(res.status).toEqual(400); 591 | // expect(res.body.message).toEqual('Something went wrong when resize'); 592 | // done(); 593 | // }); 594 | // }); 595 | // it('upload multisize and return error, cause google cloud error', (done) => { 596 | // supertest(app) 597 | // .post('/uploadwithmultiplesizegcerror') 598 | // .attach('myPic', 'test/nodejs-512.png') 599 | // .end((err, res) => { 600 | // expect(res.status).toEqual(500); 601 | // done(); 602 | // }); 603 | // }); 604 | }) 605 | --------------------------------------------------------------------------------