├── .all-contributorsrc ├── .circleci └── config.yml ├── .env-example ├── .github ├── lock.yml └── stale.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── jest.config.js ├── lib ├── constants.ts ├── index.ts ├── interceptors │ ├── amazon-s3-file.interceptor.ts │ └── index.ts ├── interfaces │ ├── index.ts │ ├── multer-extended-options.interface.ts │ ├── multer-extended-s3-async-options.interface.ts │ ├── multer-extended-s3-options-factory.interface.ts │ └── multer-extended-s3-options.interface.ts ├── multer-config.loader.ts ├── multer-extended.module.ts ├── multer-extended.providers.ts └── multer-sharp │ ├── amazon-s3-storage.ts │ ├── enums │ ├── extended-options.enum.ts │ ├── image-file-extensions.enum.ts │ ├── index.ts │ └── multer-exceptions.enum.ts │ ├── index.ts │ ├── interfaces │ ├── amazon-s3-upload-options.interface.ts │ ├── s3-storage.interface.ts │ └── sharp-options.interface.ts │ ├── multer-sharp.ts │ └── multer-sharp.utils.ts ├── media ├── header-dark-theme.png └── header-light-theme.png ├── nest-cli.json ├── package-lock.json ├── package.json ├── renovate.json ├── tests ├── fixtures │ ├── base-path.constants.ts │ └── uid.ts ├── jest-e2e.json ├── src │ ├── app.e2e-spec.ts │ ├── app.module.ts │ ├── config │ │ └── aws.ts │ ├── data │ │ ├── Readme.md │ │ ├── cat.jpg │ │ ├── crying.jpg │ │ ├── go.jpeg │ │ └── smile.jpg │ ├── image-upload │ │ ├── image-upload.controller.ts │ │ └── image-upload.module.ts │ └── user-profile-image-upload │ │ ├── user-profile-image-upload.controller.ts │ │ └── user-profile-upload.module.ts └── unit │ └── multer-sharp │ └── multer-sharp.utils.spec.ts ├── tsconfig.build.json ├── tsconfig.json └── tslint.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "jeffminsungkim", 10 | "name": "Minsung Kim", 11 | "avatar_url": "https://avatars1.githubusercontent.com/u/6092023?v=4", 12 | "profile": "https://jeffminsungkim.com", 13 | "contributions": [ 14 | "code", 15 | "maintenance", 16 | "doc", 17 | "infra", 18 | "ideas", 19 | "test" 20 | ] 21 | }, 22 | { 23 | "login": "jmcdo29", 24 | "name": "Jay McDoniel", 25 | "avatar_url": "https://avatars3.githubusercontent.com/u/28268680?v=4", 26 | "profile": "https://github.com/jmcdo29", 27 | "contributions": [ 28 | "ideas", 29 | "tool", 30 | "review", 31 | "code" 32 | ] 33 | }, 34 | { 35 | "login": "semin3276", 36 | "name": "semin3276", 37 | "avatar_url": "https://avatars1.githubusercontent.com/u/60590414?v=4", 38 | "profile": "https://github.com/semin3276", 39 | "contributions": [ 40 | "design" 41 | ] 42 | }, 43 | { 44 | "login": "volbrene", 45 | "name": "René Volbach", 46 | "avatar_url": "https://avatars0.githubusercontent.com/u/18092644?v=4", 47 | "profile": "https://www.rene-volbach.de", 48 | "contributions": [ 49 | "code", 50 | "test" 51 | ] 52 | }, 53 | { 54 | "login": "gimyboya", 55 | "name": "gimyboya", 56 | "avatar_url": "https://avatars0.githubusercontent.com/u/14952013?v=4", 57 | "profile": "http://grainer.io", 58 | "contributions": [ 59 | "code" 60 | ] 61 | }, 62 | { 63 | "login": "dineshsalunke", 64 | "name": "dineshsalunke", 65 | "avatar_url": "https://avatars2.githubusercontent.com/u/52815633?v=4", 66 | "profile": "https://github.com/dineshsalunke", 67 | "contributions": [ 68 | "code", 69 | "doc" 70 | ] 71 | }, 72 | { 73 | "login": "michaelwolz", 74 | "name": "Michael Wolz", 75 | "avatar_url": "https://avatars3.githubusercontent.com/u/7479806?v=4", 76 | "profile": "http://michaelwolz.de", 77 | "contributions": [ 78 | "code", 79 | "doc" 80 | ] 81 | }, 82 | { 83 | "login": "visurel", 84 | "name": "visurel", 85 | "avatar_url": "https://avatars3.githubusercontent.com/u/7209649?v=4", 86 | "profile": "https://visurel.com", 87 | "contributions": [ 88 | "code", 89 | "doc" 90 | ] 91 | }, 92 | { 93 | "login": "newbish", 94 | "name": "Keith Kikta", 95 | "avatar_url": "https://avatars.githubusercontent.com/u/1214744?v=4", 96 | "profile": "http://tevpro.com", 97 | "contributions": [ 98 | "maintenance", 99 | "code" 100 | ] 101 | } 102 | ], 103 | "contributorsPerLine": 7, 104 | "projectName": "nestjs-multer-extended", 105 | "projectOwner": "jeffminsungkim", 106 | "repoType": "github", 107 | "repoHost": "https://github.com", 108 | "skipCi": true 109 | } 110 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | aliases: 4 | - &restore-cache 5 | restore_cache: 6 | key: dependency-cache-{{ checksum "package.json" }} 7 | - &install-deps 8 | run: 9 | name: Install dependencies 10 | command: npm ci 11 | - &build-packages 12 | run: 13 | name: Build 14 | command: npm run build 15 | 16 | jobs: 17 | build: 18 | working_directory: ~/nestjs-multer-extended 19 | docker: 20 | - image: circleci/node:12 21 | steps: 22 | - checkout 23 | - run: 24 | name: Update NPM version 25 | command: 'sudo npm install -g npm@latest' 26 | - restore_cache: 27 | key: dependency-cache-{{ checksum "package.json" }} 28 | - run: 29 | name: Install dependencies 30 | command: npm ci 31 | - save_cache: 32 | key: dependency-cache-{{ checksum "package.json" }} 33 | paths: 34 | - ./node_modules 35 | - run: 36 | name: Code formatting 37 | command: npm run format 38 | - run: 39 | name: Code Linting 40 | command: npm run lint:fix 41 | - run: 42 | name: Build 43 | command: npm run build 44 | 45 | unit_tests: 46 | working_directory: ~/nestjs-multer-extended 47 | docker: 48 | - image: circleci/node:12 49 | steps: 50 | - checkout 51 | - *restore-cache 52 | - *install-deps 53 | - run: 54 | name: Unit tests 55 | command: npm run test 56 | 57 | integration_tests: 58 | working_directory: ~/nestjs-multer-extended 59 | docker: 60 | - image: circleci/node:12 61 | steps: 62 | - checkout 63 | - *restore-cache 64 | - *install-deps 65 | - run: 66 | name: Integration tests 67 | command: npm run test:integration 68 | - run: 69 | name: Collect coverage 70 | command: npm run coverage 71 | - store_artifacts: 72 | path: coverage 73 | 74 | workflows: 75 | version: 2 76 | build-and-test: 77 | jobs: 78 | - build 79 | - unit_tests: 80 | requires: 81 | - build 82 | - integration_tests: 83 | requires: 84 | - build 85 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | # AWS Configurations 2 | AWS_ACCESS_KEY_ID= 3 | AWS_SECRET_ACCESS_KEY= 4 | AWS_S3_REGION= 5 | AWS_S3_BUCKET_NAME= 6 | AWS_S3_BASE_PATH= 7 | AWS_S3_MAX_IMAGE_SIZE= -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before a closed issue or pull request is locked 2 | daysUntilLock: 90 3 | 4 | # Skip issues and pull requests created before a given timestamp. Timestamp must 5 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 6 | skipCreatedBefore: false 7 | 8 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 9 | exemptLabels: [] 10 | 11 | # Label to add before locking, such as `outdated`. Set to `false` to disable 12 | lockLabel: false 13 | 14 | # Comment to post before locking. Set to `false` to disable 15 | lockComment: > 16 | This thread has been automatically locked since there has not been 17 | any recent activity after it was closed. Please open a new issue for 18 | related bugs. 19 | 20 | # Assign `resolved` as the reason for locking. Set to `false` to disable 21 | setLockReason: true 22 | 23 | # Limit to only `issues` or `pulls` 24 | # only: issues 25 | 26 | # Optionally, specify configuration settings just for `issues` or `pulls` 27 | # issues: 28 | # exemptLabels: 29 | # - help-wanted 30 | # lockLabel: outdated 31 | 32 | # pulls: 33 | # daysUntilLock: 30 34 | 35 | # Repository to extend settings from 36 | # _extends: repo -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 28 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 15 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - good first issue 👍 8 | - "status: wip ⚡️" 9 | - "type: bug 🐛" 10 | - "type: discussion 🔥" 11 | - "type: future plan 🧐" 12 | - "type: help wanted 🙂" 13 | - "type: potential issue 🥶" 14 | # Label to use when marking an issue as stale 15 | staleLabel: "type: stale" 16 | # Comment to post when marking an issue as stale. Set to `false` to disable 17 | markComment: > 18 | Hello 👋, to help manage issues we automatically close stale issues. 19 | 20 | This issue has been automatically marked as stale because it has not had activity for quite some time. 21 | Has this issue been fixed, or does it still require the community's attention? 22 | 23 | > This issue will be closed in 15 days if no further activity occurs. 24 | 25 | Thank you for your contributions. 26 | # Comment to post when closing a stale issue. Set to `false` to disable 27 | closeComment: > 28 | Closing this issue after a prolonged period of inactivity. If this is still present in the latest release, please feel free to create a new issue with up-to-date information. 29 | only: issues -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.awcache 7 | /.vscode 8 | 9 | # misc 10 | npm-debug.log 11 | .DS_Store 12 | 13 | # tests 14 | /test 15 | /coverage 16 | /tests/coverage 17 | /.nyc_output 18 | 19 | # dist 20 | dist 21 | 22 | # Environment variables 23 | .env 24 | .env.* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | message="chore(release): %s :tada:" -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "proseWrap": "always" 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.4.2](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/1.4.1...1.4.2) (2021-05-03) 2 | 3 | 4 | 5 | 6 | 7 | ## [1.4.1](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/1.3.1...1.4.1) (2020-10-12) 8 | 9 | - Resolve missing dist folder issue [#255](https://github.com/jeffminsungkim/nestjs-multer-extended/issues/255) 10 | 11 | 12 | # [1.4.0](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/1.3.1...1.4.0) (2020-10-11) 13 | 14 | ### Features 15 | 16 | - **config:** allow to pass all config options for AWS and S3 instances 17 | ([3a34c55](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/3a34c55e1440117df8d423608fa0794a7cd03570)) 18 | - **config:** update README, correct AWS Instance option typings 19 | ([c7548ea](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/c7548ea1f865012e4a2b08a6e8339c95979f3ff7)) 20 | 21 | 22 | 23 | ## [1.3.1](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/1.3.0...1.3.1) (2020-09-10) 24 | 25 | ### Bug Fixes 26 | 27 | - **multer-sharp:** Allow request params without setting dynamic path 28 | ([5674a05](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/5674a05)) 29 | 30 | 31 | 32 | # [1.3.0](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/1.2.1...1.3.0) (2020-06-30) 33 | 34 | ### Features 35 | 36 | - **multer-sharp:** path parameters can be used as a dynamic key path 37 | ([98329e8](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/98329e8)) 38 | 39 | 40 | 41 | ## [1.2.1](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/1.2.0...1.2.1) (2020-06-13) 42 | 43 | ### Bug Fixes 44 | 45 | - **multer-sharp:** fix for sharpjs for samsung galaxy images 46 | ([bc6c832](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/bc6c832)) 47 | 48 | 49 | 50 | # [1.2.0](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/1.1.1...1.2.0) (2020-06-08) 51 | 52 | ### Features 53 | 54 | - **s3options and configloader:** support digitalocean spaces 55 | ([09adacb](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/09adacb)) 56 | 57 | 58 | 59 | ## [1.1.1](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/1.1.0...1.1.1) (2020-05-22) 60 | 61 | ### Bug Fixes 62 | 63 | - **deps:** update dependency sharp to ^0.25.0 64 | ([054d334](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/054d334)) 65 | 66 | 67 | 68 | # [1.1.0](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/v1.0.2...v1.1.0) (2020-03-06) 69 | 70 | ### Features 71 | 72 | - Add random filename, ability to use random filenames on upload 73 | ([#61](https://github.com/jeffminsungkim/nestjs-multer-extended/issues/61)) 74 | ([8c4110d](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/8c4110d)) 75 | - Add random filename, ability to use random filenames on upload 76 | ([#61](https://github.com/jeffminsungkim/nestjs-multer-extended/issues/61)) 77 | ([b7ebea0](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/b7ebea0)) 78 | 79 | ### Reverts 80 | 81 | - revert changes 82 | ([bca70f2](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/bca70f2)) 83 | 84 | 85 | 86 | ## [1.0.2](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/v1.0.1...v1.0.2) (2020-02-04) 87 | 88 | 89 | 90 | ## [1.0.1](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/v1.0.0...v1.0.1) (2020-02-03) 91 | 92 | 93 | 94 | # [1.0.0](https://github.com/jeffminsungkim/nestjs-multer-extended/compare/e1962ff...v1.0.0) (2020-02-03) 95 | 96 | ### Features 97 | 98 | - add feature to generate a single image in different sizes 99 | ([c29b48d](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/c29b48d)) 100 | - **multer-sharp:** remove unneeded dependencies and add image resizing 101 | ([5eed8ba](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/5eed8ba)) 102 | - **options:** update options to allow for any logger 103 | ([e1962ff](https://github.com/jeffminsungkim/nestjs-multer-extended/commit/e1962ff)) 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2020 Minsung Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 | NestJS Multer Extended Logo 5 | 6 |
7 |

8 | 9 |
10 | 11 | NPM Version 12 | 13 | 14 | license 15 | 16 | 17 | CircleCI 18 | 19 | 20 | Coverage Status 21 | 22 | 23 | Built with NestJS 24 | 25 | 26 | Built with @nestjsplus/dyn-schematics 27 | 28 | 29 | Commitizen 30 | 31 | 32 | Awesome Nest 33 | 34 | PRs welcome 35 |
36 | 37 | ## Features 38 | 39 | - Single file upload to an Amazon S3 bucket 40 | - Support for dynamic paths, upload files wherever you want! 41 | - Generate thumbnail image along with the original 42 | - Resize single image or even make it into different sizes 43 | - Load AWS S3 configuration at runtime 44 | 45 | ## Installation 46 | 47 | **NPM** 48 | ```bash 49 | $ npm i -s nestjs-multer-extended 50 | ``` 51 | 52 | **Yarn** 53 | ```bash 54 | $ yarn add nestjs-multer-extended 55 | ``` 56 | 57 | ## Getting started 58 | 59 | Once the installation process is complete, we can import the module either synchronously or asynchronosly into the root `AppModule`. 60 | 61 |   62 | 63 | ### Synchronous configuration 64 | 65 | ```typescript 66 | import { Module } from '@nestjs/common'; 67 | import { MulterExtendedModule } from 'nestjs-multer-extended'; 68 | 69 | @Module({ 70 | imports: [ 71 | MulterExtendedModule.register({ 72 | awsConfig: { 73 | accessKeyId: 'YOUR_AWS_ACCESS_KEY_ID', 74 | secretAccessKey: 'YOUR_AWS_ACCESS_KEY_ID', 75 | region: 'AWS_REGION_NEAR_TO_YOU', 76 | // ... any options you want to pass to the AWS instance 77 | }, 78 | bucket: 'YOUR_S3_BUCKET_NAME', 79 | basePath: 'ROOT_DIR_OF_ASSETS', 80 | fileSize: 1 * 1024 * 1024, 81 | }), 82 | ], 83 | }) 84 | export class AppModule {} 85 | ``` 86 | 87 | ### Asynchronous configuration 88 | 89 | In this example, the module integrates with the awesome [nestjs-config](https://github.com/nestjsx/nestjs-config) package. 90 | 91 | `useFactory` should return an object with [MulterExtendedS3Options interface](#MulterExtendedS3Options) or undefined. 92 | 93 | ```typescript 94 | import { Module } from '@nestjs/common'; 95 | import { MulterExtendedModule } from 'nestjs-multer-extended'; 96 | import { ConfigService } from 'nestjs-config'; 97 | 98 | @Module({ 99 | imports: [ 100 | MulterExtendedModule.registerAsync({ 101 | useFactory: (config: ConfigService) => config.get('s3'), 102 | inject: [ConfigService], 103 | }), 104 | ], 105 | }) 106 | export class AppModule {} 107 | ``` 108 | 109 | > **Note**: You can import this module from not only the root module of your app but also from other feature modules where you want to use it. 110 | 111 |   112 | 113 | To upload a single file, simply tie the `AmazonS3FileInterceptor()` interceptor to the route handler and extract `file` from the request using the `@UploadedFile()` decorator. 114 | 115 | ```javascript 116 | import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; 117 | import { AmazonS3FileInterceptor } from 'nestjs-multer-extended'; 118 | 119 | @Controller() 120 | export class AppController { 121 | 122 | @Post('upload') 123 | @UseInterceptors(AmazonS3FileInterceptor('file')) 124 | uploadFile(@UploadedFile() file) { 125 | console.log(file); 126 | } 127 | } 128 | ``` 129 | 130 | In this example, `uploadFile()` method will upload a file under the base path you have configured earlrier. 131 | 132 | The `AmazonS3FileInterceptor()` decorator takes two arguments: 133 | 134 | - `fieldName`: string that supplies the name of the field from the HTML form that holds a file. 135 | - `options`: optional object of type `MulterExtendedOptions`. (mode details [**here**](#MulterExtendedOptions)) 136 | 137 | What if you wanted to upload a file in a different location under the base path? Thankfully, `AmazonS3FileInterceptor()` decorator accepts `dynamicPath` property as a second argument option. Pass the string path as shown below: 138 | 139 | ```javascript 140 | @Post('upload') 141 | @UseInterceptors( 142 | AmazonS3FileInterceptor('file', { 143 | dynamicPath: 'aec16138-a75a-4961-b8c1-8e803b6bf2cf' 144 | }), 145 | ) 146 | uploadFile(@UploadedFile() file) { 147 | console.log(file); 148 | } 149 | ``` 150 | 151 | In this example, `uploadFile()` method will upload a file in `${basePath}/aec16138-a75a-4961-b8c1-8e803b6bf2cf/${originalname}`. 152 | 153 | Route parameters can also be used as a key. For example, if you have the route `/user/:name`, then pass the `name` into the `dynamicPath` property as a value. 154 | 155 | ```javascript 156 | @Post('user/:name') 157 | @UseInterceptors( 158 | AmazonS3FileInterceptor('file', { 159 | dynamicPath: 'name' 160 | }), 161 | ) 162 | uploadFile(@UploadedFile() file) { 163 | // POST /user/jeffminsungkim 164 | console.log(file); 165 | // => YOUR-BASE-PATH/jeffminsungkim/filename.png 166 | } 167 | 168 | @Post('user/:name/team/:no') 169 | @UseInterceptors( 170 | AmazonS3FileInterceptor('file', { 171 | dynamicPath: 'no' 172 | }), 173 | ) 174 | uploadFile(@UploadedFile() file) { 175 | // POST /user/jeffminsungkim/team/8987 176 | console.log(file); 177 | // => YOUR-BASE-PATH/8987/filename.png 178 | } 179 | 180 | @Post('user/:name/team/:no') 181 | @UseInterceptors( 182 | AmazonS3FileInterceptor('file', { 183 | dynamicPath: ['name', 'no'] 184 | }), 185 | ) 186 | uploadFile(@UploadedFile() file) { 187 | // POST /user/jeffminsungkim/team/8987 188 | console.log(file); 189 | // => YOUR-BASE-PATH/jeffminsungkim/8987/filename.png 190 | } 191 | ``` 192 | 193 |   194 | 195 | You may want to store the file with an arbitrary name instead of the original file name. You can do this by passing the `randomFilename` property attribute set to `true` as follows: 196 | 197 | ```javascript 198 | @Post('upload') 199 | @UseInterceptors( 200 | AmazonS3FileInterceptor('file', { 201 | randomFilename: true 202 | }), 203 | ) 204 | uploadFile(@UploadedFile() file) { 205 | console.log(file); 206 | } 207 | ``` 208 | 209 | If you want to resize the file before the upload, you can pass on the `resize` property as follows: 210 | 211 | ```javascript 212 | @Post('upload') 213 | @UseInterceptors( 214 | AmazonS3FileInterceptor('file', { 215 | resize: { width: 500, height: 400 }, 216 | }), 217 | ) 218 | uploadFile(@UploadedFile() file) { 219 | console.log(file); 220 | } 221 | ``` 222 | 223 | You can pass an array of size options to resize a single image into different sizes as follows: 224 | 225 | ```javascript 226 | @Post('upload') 227 | @UseInterceptors( 228 | AmazonS3FileInterceptor('file', { 229 | resizeMultiple: [ 230 | { suffix: 'sm', width: 200, height: 200 }, 231 | { suffix: 'md', width: 300, height: 300 }, 232 | { suffix: 'lg', width: 400, height: 400 }, 233 | ], 234 | } 235 | ) 236 | uploadFile(@UploadedFile() file) { 237 | console.log(file); 238 | } 239 | ``` 240 | 241 | Not only creating a thumbnail image but also willing to change the file size limit, you can pass the properties as follows: 242 | 243 | ```javascript 244 | @Post('upload') 245 | @UseInterceptors( 246 | AmazonS3FileInterceptor('file', { 247 | thumbnail: { suffix: 'thumb', width: 200, height: 200 }, 248 | limits: { fileSize: 7 * 1024 * 1024 }, 249 | }), 250 | ) 251 | uploadFile(@UploadedFile() file) { 252 | console.log(file); 253 | } 254 | ``` 255 | 256 | In this example, `uploadFile()` method will upload both thumbnail and original images. 257 | 258 |   259 | 260 | ### MulterExtendedS3Options 261 | 262 | `MulterExtendedModule` requires an object with the following interface: 263 | 264 | ```typescript 265 | interface MulterExtendedS3Options { 266 | /** 267 | * AWS Access Key ID 268 | * @deprecated v2 use awsConfig instead 269 | */ 270 | readonly accessKeyId?: string; 271 | /** 272 | * AWS Secret Access Key 273 | * @deprecated v2 use awsConfig instead 274 | */ 275 | readonly secretAccessKey?: string; 276 | /** 277 | * Default region name 278 | * default: us-west-2 279 | * @deprecated v2 use awsConfig instead 280 | */ 281 | readonly region?: string; 282 | /** 283 | * AWS Config 284 | */ 285 | readonly awsConfig?: ConfigurationOptions & ConfigurationServicePlaceholders & APIVersions & {[key: string]: any}; 286 | /** 287 | * S3 Config 288 | */ 289 | readonly s3Config?: AWS.S3.Types.ClientConfiguration; 290 | /** 291 | * The name of Amazon S3 bucket 292 | */ 293 | readonly bucket: string; 294 | /** 295 | * The base path where you want to store files in 296 | */ 297 | readonly basePath: string; 298 | /** 299 | * Optional parameter for Access control level for the file 300 | * default: public-read 301 | * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl 302 | */ 303 | readonly acl?: string; 304 | /** 305 | * AWS Endpoint 306 | * @deprecated v2 use s3Config instead 307 | */ 308 | readonly endpoint?: string; 309 | /** 310 | * Optional parameter for the file size 311 | * default: 3MB 312 | */ 313 | readonly fileSize?: number | string; 314 | /** 315 | * Optional parameter for a custom logger 316 | * default: NestJS built-in text-based logger 317 | * @see https://docs.nestjs.com/techniques/logger 318 | */ 319 | readonly logger?: LoggerService; 320 | } 321 | ``` 322 | 323 | ### MulterExtendedOptions 324 | 325 | Key | Default | Description | Example 326 | --- | --- | --- | --- 327 | `dynamicPath` | undefined | The name that you assign to an S3 object | "aec16138-a75a-4961-b8c1-8e803b6bf2cf/random/dir" 328 | `randomFilename` | undefined | If this property sets to true, a random file name will be generated | "aec16138-a75a-4961-b8c1-8e803b6bf2cf" 329 | `fileFilter` | Accepts JPEG, PNG types only | Function to control which files are accepted 330 | `limits` | 3MB | Limits of the uploaded data | 5242880 (in bytes) 331 | `resize` | undefined | Resize a single file | { width: 300, height: 350 } 332 | `resizeMultiple` | undefined | Resize a single file into different sizes (`Array`) | [{ suffix: 'md', width: 300, height: 350 }, { suffix: 'sm', width: 200, height: 200 }] 333 | `thumbnail` | undefined | Create a thumbnail image (`object`) | { suffix: 'thumbnail', width: 200, height: 200 } 334 | 335 | ## Support 336 | 337 | 338 | Buymeacoffee 339 | 340 | 341 | You could help me out for some coffees 🥤 or give us a star ⭐️ 342 | 343 | ## Maintainers 344 | 345 | - [Minsung Kim](https://github.com/jeffminsungkim) 346 | 347 | ## Contributors ✨ 348 | 349 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 |

Minsung Kim

💻 🚧 📖 🚇 🤔 ⚠️

Jay McDoniel

🤔 🔧 👀 💻

semin3276

🎨

René Volbach

💻 ⚠️

gimyboya

💻

dineshsalunke

💻 📖

Michael Wolz

💻 📖

visurel

💻 📖

Keith Kikta

🚧 💻
369 | 370 | 371 | 372 | 373 | 374 | 375 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 376 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ['jest-extended'], 3 | moduleFileExtensions: ['js', 'json', 'ts'], 4 | rootDir: '.', 5 | verbose: true, 6 | testRegex: '.spec.ts$', 7 | transform: { '^.+\\.(t|j)s$': 'ts-jest' }, 8 | testEnvironment: 'node', 9 | }; 10 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const MULTER_MODULE_OPTIONS = 'MULTER_MODULE_OPTIONS'; 2 | export const MULTER_EXTENDED_S3_OPTIONS = 'MULTER_EXTENDED_S3_OPTIONS'; 3 | export const MULTER_EXTENDED_S3_MODULE_ID = 'MULTER_EXTENDED_S3_MODULE_ID'; 4 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './multer-config.loader'; 2 | export * from './multer-extended.module'; 3 | export * from './interceptors'; 4 | export * from './interfaces'; 5 | export * from './constants'; 6 | -------------------------------------------------------------------------------- /lib/interceptors/amazon-s3-file.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Type, 3 | NestInterceptor, 4 | Optional, 5 | Inject, 6 | ExecutionContext, 7 | CallHandler, 8 | mixin, 9 | } from '@nestjs/common'; 10 | import multer from 'multer'; 11 | import { Observable } from 'rxjs'; 12 | import { MulterModuleOptions } from '@nestjs/platform-express'; 13 | import { MULTER_MODULE_OPTIONS } from '@nestjs/platform-express/multer/files.constants'; 14 | import { MulterExtendedOptions } from '../interfaces'; 15 | import { AmazonS3Storage, ExtendedOptions, transformException } from '../multer-sharp'; 16 | import { S3StorageOptions } from '../multer-sharp/interfaces/s3-storage.interface'; 17 | 18 | type MulterInstance = any; 19 | 20 | export function AmazonS3FileInterceptor( 21 | fieldName: string, 22 | localOptions?: MulterExtendedOptions, 23 | ): Type { 24 | class MixinInterceptor implements NestInterceptor { 25 | protected multer: MulterInstance; 26 | private localOptions: MulterExtendedOptions; 27 | private options: MulterModuleOptions; 28 | 29 | constructor( 30 | @Optional() 31 | @Inject(MULTER_MODULE_OPTIONS) 32 | options: MulterModuleOptions = {}, 33 | ) { 34 | this.localOptions = localOptions; 35 | this.options = options; 36 | 37 | this.multer = (multer as any)({ 38 | ...this.options, 39 | ...this.localOptions, 40 | }); 41 | } 42 | 43 | async intercept(context: ExecutionContext, next: CallHandler): Promise> { 44 | const ctx = context.switchToHttp(); 45 | 46 | if (this.localOptions) { 47 | this.multer.storage = this.pickStorageOptions(); 48 | } 49 | 50 | await new Promise((resolve, reject) => 51 | this.multer.single(fieldName)(ctx.getRequest(), ctx.getResponse(), (err: any) => { 52 | if (err) { 53 | const error = transformException(err); 54 | return reject(error); 55 | } 56 | resolve(); 57 | }), 58 | ); 59 | 60 | return next.handle(); 61 | } 62 | 63 | private pickStorageOptions() { 64 | let storageOptions: S3StorageOptions; 65 | const extendedOptionProperty = Object.keys(this.localOptions)[0]; 66 | 67 | switch (extendedOptionProperty) { 68 | case ExtendedOptions.CREATE_THUMBNAIL: 69 | storageOptions = { 70 | ...this.options.storage.storageOpts, 71 | resize: [this.localOptions[extendedOptionProperty], { suffix: 'original' }], 72 | ignoreAspectRatio: true, 73 | dynamicPath: this.localOptions.dynamicPath, 74 | }; 75 | return AmazonS3Storage(storageOptions); 76 | case ExtendedOptions.RESIZE_IMAGE: 77 | storageOptions = { 78 | ...this.options.storage.storageOpts, 79 | resize: this.localOptions[extendedOptionProperty], 80 | dynamicPath: this.localOptions.dynamicPath, 81 | }; 82 | return AmazonS3Storage(storageOptions); 83 | case ExtendedOptions.RESIZE_IMAGE_MULTIPLE_SIZES: 84 | storageOptions = { 85 | ...this.options.storage.storageOpts, 86 | resizeMultiple: this.localOptions[extendedOptionProperty], 87 | ignoreAspectRatio: true, 88 | dynamicPath: this.localOptions.dynamicPath, 89 | }; 90 | return AmazonS3Storage(storageOptions); 91 | default: 92 | return AmazonS3Storage({ ...this.options.storage.storageOpts, ...this.localOptions }); 93 | } 94 | } 95 | } 96 | const Interceptor = mixin(MixinInterceptor); 97 | return Interceptor as Type; 98 | } 99 | -------------------------------------------------------------------------------- /lib/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export { AmazonS3FileInterceptor } from './amazon-s3-file.interceptor'; 2 | -------------------------------------------------------------------------------- /lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './multer-extended-s3-options.interface'; 2 | export * from './multer-extended-s3-async-options.interface'; 3 | export * from './multer-extended-s3-options-factory.interface'; 4 | export * from './multer-extended-options.interface'; 5 | -------------------------------------------------------------------------------- /lib/interfaces/multer-extended-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; 2 | 3 | export interface ResizeOptions { 4 | width: number; 5 | height: number; 6 | } 7 | 8 | export interface MultipleSizeOptions { 9 | suffix: string; 10 | width?: number; 11 | height?: number; 12 | } 13 | 14 | export interface MulterExtendedOptions extends Pick { 15 | dynamicPath?: string | string[]; 16 | randomFilename?: boolean; 17 | resize?: ResizeOptions; 18 | resizeMultiple?: MultipleSizeOptions[]; 19 | thumbnail?: MultipleSizeOptions; 20 | } 21 | -------------------------------------------------------------------------------- /lib/interfaces/multer-extended-s3-async-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata, Type } from '@nestjs/common/interfaces'; 2 | import { MulterExtendedS3Options } from './multer-extended-s3-options.interface'; 3 | import { MulterExtendedS3OptionsFactory } from './multer-extended-s3-options-factory.interface'; 4 | 5 | export interface MulterExtendedS3AsyncOptions extends Pick { 6 | inject?: any[]; 7 | useExisting?: Type; 8 | useClass?: Type; 9 | useFactory?: (...args: any[]) => Promise | MulterExtendedS3Options; 10 | } 11 | -------------------------------------------------------------------------------- /lib/interfaces/multer-extended-s3-options-factory.interface.ts: -------------------------------------------------------------------------------- 1 | import { MulterExtendedS3Options } from './multer-extended-s3-options.interface'; 2 | 3 | export interface MulterExtendedS3OptionsFactory { 4 | createMulterExtendedS3Options(): Promise | MulterExtendedS3Options; 5 | } 6 | -------------------------------------------------------------------------------- /lib/interfaces/multer-extended-s3-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from '@nestjs/common'; 2 | import { APIVersions } from 'aws-sdk/lib/config'; 3 | import { ConfigurationOptions } from 'aws-sdk/lib/config-base'; 4 | import { ConfigurationServicePlaceholders } from 'aws-sdk/lib/config_service_placeholders'; 5 | import AWS from 'aws-sdk'; 6 | 7 | export interface MulterExtendedS3Options { 8 | /** 9 | * AWS Access Key ID 10 | * @deprecated v2 use awsConfig instead 11 | */ 12 | readonly accessKeyId?: string; 13 | /** 14 | * AWS Secret Access Key 15 | * @deprecated v2 use awsConfig instead 16 | */ 17 | readonly secretAccessKey?: string; 18 | /** 19 | * Default region name 20 | * default: us-west-2 21 | * @deprecated v2 use awsConfig instead 22 | */ 23 | readonly region?: string; 24 | /** 25 | * AWS Config 26 | */ 27 | readonly awsConfig?: ConfigurationOptions & 28 | ConfigurationServicePlaceholders & 29 | APIVersions & { [key: string]: any }; 30 | /** 31 | * S3 Config 32 | */ 33 | readonly s3Config?: AWS.S3.Types.ClientConfiguration; 34 | /** 35 | * The name of Amazon S3 bucket 36 | */ 37 | readonly bucket: string; 38 | /** 39 | * The base path where you want to store files in 40 | */ 41 | readonly basePath: string; 42 | /** 43 | * Optional parameter for Access control level for the file 44 | * default: public-read 45 | * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl 46 | */ 47 | readonly acl?: string; 48 | /** 49 | * AWS Endpoint 50 | * @deprecated v2 use s3Config instead 51 | */ 52 | readonly endpoint?: string; 53 | /** 54 | * Optional parameter for the file size 55 | * default: 3MB 56 | */ 57 | readonly fileSize?: number | string; 58 | /** 59 | * Optional parameter for a custom logger 60 | * default: NestJS built-in text-based logger 61 | * @see https://docs.nestjs.com/techniques/logger 62 | */ 63 | readonly logger?: LoggerService; 64 | } 65 | -------------------------------------------------------------------------------- /lib/multer-config.loader.ts: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import { AmazonS3Storage, ImageFileExtensions, MulterExceptions } from './multer-sharp'; 3 | import { BadRequestException, Inject, Injectable, Logger, LoggerService } from '@nestjs/common'; 4 | import { MulterModuleOptions, MulterOptionsFactory } from '@nestjs/platform-express'; 5 | import { MULTER_EXTENDED_S3_OPTIONS } from './constants'; 6 | import { MulterExtendedS3Options } from './interfaces'; 7 | 8 | interface MulterS3ConfigService extends MulterOptionsFactory { 9 | filterImageFileExtension(req, file, cb): any; 10 | } 11 | 12 | @Injectable() 13 | export class MulterConfigLoader implements MulterS3ConfigService { 14 | static DEFAULT_ACL = 'public-read'; 15 | static DEFAULT_REGION = 'us-west-2'; 16 | static DEFAULT_MAX_FILESIZE = 3145728; 17 | private readonly S3: AWS.S3; 18 | private readonly logger: LoggerService; 19 | 20 | constructor(@Inject(MULTER_EXTENDED_S3_OPTIONS) private s3Options: MulterExtendedS3Options) { 21 | AWS.config.update({ 22 | accessKeyId: s3Options.accessKeyId, 23 | secretAccessKey: s3Options.secretAccessKey, 24 | region: s3Options.region || MulterConfigLoader.DEFAULT_REGION, 25 | ...s3Options.awsConfig, 26 | }); 27 | 28 | this.S3 = new AWS.S3({ 29 | endpoint: s3Options.endpoint, 30 | ...s3Options.s3Config, 31 | }); 32 | this.logger = s3Options.logger || new Logger(MulterConfigLoader.name); 33 | this.logger.log(JSON.stringify(s3Options)); 34 | } 35 | 36 | createMulterOptions(): MulterModuleOptions | Promise { 37 | const storage = AmazonS3Storage({ 38 | Key: (req, file, cb) => { 39 | const basePath = `${this.s3Options.basePath}`; 40 | 41 | cb(null, basePath); 42 | }, 43 | s3: this.S3, 44 | Bucket: this.s3Options.bucket, 45 | ACL: this.s3Options.acl || MulterConfigLoader.DEFAULT_ACL, 46 | }); 47 | 48 | return { 49 | storage, 50 | fileFilter: this.filterImageFileExtension, 51 | limits: { 52 | fileSize: +this.s3Options.fileSize || MulterConfigLoader.DEFAULT_MAX_FILESIZE, 53 | }, 54 | }; 55 | } 56 | 57 | filterImageFileExtension(req, file, cb) { 58 | const { mimetype } = file; 59 | const extension = mimetype.substring(mimetype.lastIndexOf('/') + 1); 60 | const mimetypeIsNotImage = (ext: ImageFileExtensions): boolean => 61 | !Object.values(ImageFileExtensions).includes(ext); 62 | 63 | if (mimetypeIsNotImage(extension)) { 64 | req.fileValidationError = MulterExceptions.INVALID_IMAGE_FILE_TYPE; 65 | return cb(new BadRequestException(MulterExceptions.INVALID_IMAGE_FILE_TYPE), false); 66 | } 67 | 68 | return cb(null, true); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/multer-extended.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, DynamicModule, Provider } from '@nestjs/common'; 2 | import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; 3 | import { MulterConfigLoader } from './multer-config.loader'; 4 | import { 5 | createMulterExtendedProviders, 6 | createMulterOptionsFactory, 7 | } from './multer-extended.providers'; 8 | import { 9 | MULTER_EXTENDED_S3_OPTIONS, 10 | MULTER_EXTENDED_S3_MODULE_ID, 11 | MULTER_MODULE_OPTIONS, 12 | } from './constants'; 13 | import { 14 | MulterExtendedS3Options, 15 | MulterExtendedS3AsyncOptions, 16 | MulterExtendedS3OptionsFactory, 17 | } from './interfaces'; 18 | 19 | @Module({ 20 | providers: [MulterConfigLoader], 21 | exports: [MulterConfigLoader], 22 | }) 23 | export class MulterExtendedModule { 24 | public static register(options: MulterExtendedS3Options): DynamicModule { 25 | return { 26 | module: MulterExtendedModule, 27 | providers: createMulterExtendedProviders(options), 28 | exports: [MULTER_EXTENDED_S3_OPTIONS, MULTER_MODULE_OPTIONS], 29 | }; 30 | } 31 | 32 | public static registerAsync(options: MulterExtendedS3AsyncOptions): DynamicModule { 33 | return { 34 | module: MulterExtendedModule, 35 | imports: options.imports, 36 | providers: [ 37 | ...this.createProviders(options), 38 | { 39 | provide: MULTER_EXTENDED_S3_MODULE_ID, 40 | useValue: randomStringGenerator(), 41 | }, 42 | createMulterOptionsFactory, 43 | ], 44 | exports: [MULTER_EXTENDED_S3_OPTIONS, MULTER_MODULE_OPTIONS], 45 | }; 46 | } 47 | 48 | private static createProviders(options: MulterExtendedS3AsyncOptions): Provider[] { 49 | if (options.useExisting || options.useFactory) { 50 | return [this.createOptionsProvider(options)]; 51 | } 52 | 53 | return [ 54 | this.createOptionsProvider(options), 55 | { 56 | provide: options.useClass, 57 | useClass: options.useClass, 58 | }, 59 | ]; 60 | } 61 | 62 | private static createOptionsProvider(options: MulterExtendedS3AsyncOptions): Provider { 63 | if (options.useFactory) { 64 | return { 65 | provide: MULTER_EXTENDED_S3_OPTIONS, 66 | useFactory: options.useFactory, 67 | inject: options.inject || [], 68 | }; 69 | } 70 | 71 | return { 72 | provide: MULTER_EXTENDED_S3_OPTIONS, 73 | useFactory: async (optionsFactory: MulterExtendedS3OptionsFactory) => 74 | optionsFactory.createMulterExtendedS3Options(), 75 | inject: [options.useExisting || options.useClass], 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/multer-extended.providers.ts: -------------------------------------------------------------------------------- 1 | import { MulterExtendedS3Options } from './interfaces'; 2 | import { 3 | MULTER_EXTENDED_S3_OPTIONS, 4 | MULTER_EXTENDED_S3_MODULE_ID, 5 | MULTER_MODULE_OPTIONS, 6 | } from './constants'; 7 | import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; 8 | import { MulterConfigLoader } from './multer-config.loader'; 9 | 10 | export const createMulterOptionsFactory = { 11 | provide: MULTER_MODULE_OPTIONS, 12 | useFactory: async (loader: MulterConfigLoader) => loader.createMulterOptions(), 13 | inject: [MulterConfigLoader], 14 | }; 15 | 16 | export function createMulterExtendedProviders(options: MulterExtendedS3Options) { 17 | return [ 18 | { 19 | provide: MULTER_EXTENDED_S3_OPTIONS, 20 | useValue: options, 21 | }, 22 | { 23 | provide: MULTER_EXTENDED_S3_MODULE_ID, 24 | useValue: randomStringGenerator(), 25 | }, 26 | createMulterOptionsFactory, 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /lib/multer-sharp/amazon-s3-storage.ts: -------------------------------------------------------------------------------- 1 | import { S3StorageOptions } from './interfaces/s3-storage.interface'; 2 | import { MulterSharp } from './multer-sharp'; 3 | 4 | export const AmazonS3Storage = (options: S3StorageOptions) => new MulterSharp(options); 5 | -------------------------------------------------------------------------------- /lib/multer-sharp/enums/extended-options.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ExtendedOptions { 2 | CREATE_THUMBNAIL = 'thumbnail', 3 | RESIZE_IMAGE = 'resize', 4 | RESIZE_IMAGE_MULTIPLE_SIZES = 'resizeMultiple', 5 | } 6 | -------------------------------------------------------------------------------- /lib/multer-sharp/enums/image-file-extensions.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ImageFileExtensions { 2 | PNG = 'png', 3 | JPG = 'jpg', 4 | JPEG = 'jpeg', 5 | } 6 | -------------------------------------------------------------------------------- /lib/multer-sharp/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './extended-options.enum'; 2 | export * from './image-file-extensions.enum'; 3 | export * from './multer-exceptions.enum'; 4 | -------------------------------------------------------------------------------- /lib/multer-sharp/enums/multer-exceptions.enum.ts: -------------------------------------------------------------------------------- 1 | export enum MulterExceptions { 2 | LIMIT_PART_COUNT = 'Too many parts', 3 | LIMIT_FILE_SIZE = 'File too large', 4 | LIMIT_FILE_COUNT = 'Too many files', 5 | LIMIT_FIELD_KEY = 'Field name too long', 6 | LIMIT_FIELD_VALUE = 'Field value too long', 7 | LIMIT_FIELD_COUNT = 'Too many fields', 8 | LIMIT_UNEXPECTED_FILE = 'Unexpected field', 9 | INVALID_IMAGE_FILE_TYPE = 'Invalid Image File Type', 10 | } 11 | -------------------------------------------------------------------------------- /lib/multer-sharp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './amazon-s3-storage'; 2 | export * from './enums'; 3 | export * from './multer-sharp.utils'; 4 | -------------------------------------------------------------------------------- /lib/multer-sharp/interfaces/amazon-s3-upload-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { S3 } from 'aws-sdk'; 2 | 3 | export interface AmazonS3UploadOptions extends Partial { 4 | s3: S3; 5 | Key?: any; 6 | dynamicPath?: string | string[]; 7 | randomFilename?: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /lib/multer-sharp/interfaces/s3-storage.interface.ts: -------------------------------------------------------------------------------- 1 | import { AmazonS3UploadOptions } from './amazon-s3-upload-options.interface'; 2 | import { SharpOptions } from './sharp-options.interface'; 3 | 4 | export type S3StorageOptions = AmazonS3UploadOptions & SharpOptions; 5 | 6 | export interface S3Storage { 7 | storageOpts: S3StorageOptions; 8 | sharpOpts: SharpOptions; 9 | } 10 | -------------------------------------------------------------------------------- /lib/multer-sharp/interfaces/sharp-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ResizeOptions, Sharp } from 'sharp'; 2 | 3 | type SharpOption = T; 4 | 5 | export type ResizeOption = SharpOption | SharpOption[]; 6 | 7 | export interface Size { 8 | width?: number; 9 | height?: number; 10 | options?: ResizeOptions; 11 | } 12 | 13 | export interface ExtendSize { 14 | suffix: string; 15 | Body?: NodeJS.ReadableStream & Sharp; 16 | } 17 | 18 | export interface SharpOptions { 19 | resize?: ResizeOption; 20 | resizeMultiple?: ResizeOption; 21 | ignoreAspectRatio?: boolean; 22 | } 23 | -------------------------------------------------------------------------------- /lib/multer-sharp/multer-sharp.ts: -------------------------------------------------------------------------------- 1 | import sharp, { Sharp } from 'sharp'; 2 | import { StorageEngine } from 'multer'; 3 | import { S3 } from 'aws-sdk'; 4 | import { ManagedUpload } from 'aws-sdk/lib/s3/managed_upload'; 5 | import { isFunction, isString } from '@nestjs/common/utils/shared.utils'; 6 | import { Request } from 'express'; 7 | import { from, Observable } from 'rxjs'; 8 | import { map, mergeMap, toArray, first } from 'rxjs/operators'; 9 | import { lookup, extension } from 'mime-types'; 10 | import { S3StorageOptions, S3Storage } from './interfaces/s3-storage.interface'; 11 | import { SharpOptions, Size, ExtendSize } from './interfaces/sharp-options.interface'; 12 | import { 13 | getSharpOptions, 14 | getSharpOptionProps, 15 | transformImage, 16 | isOriginalSuffix, 17 | } from './multer-sharp.utils'; 18 | import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; 19 | 20 | export interface EventStream { 21 | stream: NodeJS.ReadableStream & Sharp; 22 | } 23 | export type File = Express.Multer.File & EventStream & Partial; 24 | export type Info = Partial< 25 | Express.Multer.File & ManagedUpload.SendData & S3.Types.PutObjectRequest & sharp.OutputInfo 26 | >; 27 | 28 | export class MulterSharp implements StorageEngine, S3Storage { 29 | storageOpts: S3StorageOptions; 30 | sharpOpts: SharpOptions; 31 | 32 | constructor(options: S3StorageOptions) { 33 | if (!options.s3) { 34 | throw new Error('You have to specify s3 object.'); 35 | } 36 | 37 | this.storageOpts = options; 38 | this.sharpOpts = getSharpOptions(options); 39 | 40 | if (!this.storageOpts.Bucket) { 41 | throw new Error('You have to specify Bucket property.'); 42 | } 43 | 44 | if (!isFunction(this.storageOpts.Key) && !isString(this.storageOpts.Key)) { 45 | throw new TypeError(`Key must be a "string", "function" or undefined`); 46 | } 47 | } 48 | 49 | public _removeFile(req: Request, file: Info, cb: (error: Error) => void) { 50 | this.storageOpts.s3.deleteObject({ Bucket: file.Bucket, Key: file.Key }, cb); 51 | } 52 | 53 | public _handleFile( 54 | req: Request, 55 | file: File, 56 | callback: (error?: any, info?: Partial) => void, 57 | ): void { 58 | const { storageOpts } = this; 59 | const { mimetype, stream } = file; 60 | const params = { 61 | Bucket: storageOpts.Bucket, 62 | ACL: storageOpts.ACL, 63 | CacheControl: storageOpts.CacheControl, 64 | ContentType: storageOpts.ContentType, 65 | Metadata: storageOpts.Metadata, 66 | StorageClass: storageOpts.StorageClass, 67 | ServerSideEncryption: storageOpts.ServerSideEncryption, 68 | SSEKMSKeyId: storageOpts.SSEKMSKeyId, 69 | Body: stream, 70 | Key: storageOpts.Key, 71 | }; 72 | 73 | if (isFunction(storageOpts.Key)) { 74 | storageOpts.Key(req, file, (err, Key) => { 75 | if (err) { 76 | callback(err); 77 | return; 78 | } 79 | 80 | let { originalname } = file; 81 | 82 | if (storageOpts.randomFilename) { 83 | originalname = `${randomStringGenerator()}.${extension(mimetype)}`; 84 | } 85 | 86 | const routeParams = Object.keys(req.params); 87 | 88 | if (routeParams.length > 0 && storageOpts.dynamicPath) { 89 | if (typeof storageOpts.dynamicPath === 'string') { 90 | params.Key = routeParams.includes(storageOpts.dynamicPath) 91 | ? `${Key}/${req.params[storageOpts.dynamicPath]}/${originalname}` 92 | : `${Key}/${storageOpts.dynamicPath}/${originalname}`; 93 | } else { 94 | const paramDir = []; 95 | storageOpts.dynamicPath.forEach((pathSegment) => { 96 | paramDir.push(routeParams.includes(pathSegment) ? req.params[pathSegment] : pathSegment); 97 | }); 98 | params.Key = `${Key}/${paramDir.join('/')}/${originalname}`; 99 | } 100 | } else { 101 | params.Key = storageOpts.dynamicPath 102 | ? `${Key}/${storageOpts.dynamicPath}/${originalname}` 103 | : `${Key}/${originalname}`; 104 | } 105 | 106 | mimetype.includes('image') 107 | ? this.uploadImageFileToS3(params, file, callback) 108 | : this.uploadFileToS3(params, file, callback); 109 | }); 110 | } 111 | } 112 | 113 | private uploadImageFileToS3( 114 | params: S3.Types.PutObjectRequest, 115 | file: File, 116 | callback: (error?: any, info?: Info) => void, 117 | ) { 118 | const { storageOpts, sharpOpts } = this; 119 | const { stream } = file; 120 | const { 121 | ACL, 122 | ContentDisposition, 123 | ContentType: optsContentType, 124 | StorageClass, 125 | ServerSideEncryption, 126 | Metadata, 127 | } = storageOpts; 128 | 129 | const resizeBucket = getSharpOptionProps(storageOpts); 130 | 131 | if (Array.isArray(resizeBucket) && resizeBucket.length > 0) { 132 | const sizes$ = from(resizeBucket) as Observable; 133 | 134 | sizes$ 135 | .pipe( 136 | map((size) => { 137 | const resizedStream = transformImage(sharpOpts, size); 138 | 139 | if (isOriginalSuffix(size.suffix)) { 140 | size.Body = stream.pipe(sharp({ failOnError: false })); 141 | } else { 142 | size.Body = stream.pipe(resizedStream); 143 | } 144 | return size; 145 | }), 146 | mergeMap((size) => { 147 | const sharpStream = size.Body; 148 | const sharpPromise = sharpStream.toBuffer({ resolveWithObject: true }); 149 | 150 | return from( 151 | sharpPromise.then((result) => { 152 | return { 153 | ...size, 154 | ...result.info, 155 | ContentType: result.info.format, 156 | currentSize: result.info.size, 157 | }; 158 | }), 159 | ); 160 | }), 161 | mergeMap((size) => { 162 | const { Body, ContentType } = size; 163 | const newParams = { 164 | ...params, 165 | Body, 166 | ContentType, 167 | Key: `${params.Key}-${size.suffix}`, 168 | }; 169 | const upload = storageOpts.s3.upload(newParams); 170 | const currentSize = { [size.suffix]: 0 }; 171 | 172 | upload.on('httpUploadProgress', (event) => { 173 | if (event.total) { 174 | currentSize[size.suffix] = event.total; 175 | } 176 | }); 177 | 178 | const upload$ = from( 179 | upload.promise().then((result) => { 180 | // tslint:disable-next-line 181 | const { Body, ...rest } = size; 182 | return { 183 | ...result, 184 | ...rest, 185 | currentSize: size.currentSize || currentSize[size.suffix], 186 | }; 187 | }), 188 | ); 189 | return upload$; 190 | }), 191 | toArray(), 192 | first(), 193 | ) 194 | .subscribe((response) => { 195 | const multipleUploadedFiles = response.reduce((acc, uploadedFile) => { 196 | // tslint:disable-next-line 197 | const { suffix, ContentType, currentSize, ...details } = uploadedFile; 198 | acc[uploadedFile.suffix] = { 199 | ACL, 200 | ContentDisposition, 201 | StorageClass, 202 | ServerSideEncryption, 203 | Metadata, 204 | ...details, 205 | size: currentSize, 206 | ContentType: optsContentType || ContentType, 207 | mimetype: lookup(ContentType) || `image/${ContentType}`, 208 | }; 209 | return acc; 210 | }, {}); 211 | callback(null, JSON.parse(JSON.stringify(multipleUploadedFiles))); 212 | }, callback); 213 | } else { 214 | let currentSize = 0; 215 | const resizedStream = transformImage(sharpOpts, sharpOpts.resize); 216 | const newParams = { ...params, Body: stream.pipe(resizedStream) }; 217 | const meta$ = from(newParams.Body.toBuffer({ resolveWithObject: true })); 218 | 219 | meta$ 220 | .pipe( 221 | first(), 222 | map((metadata) => { 223 | newParams.ContentType = storageOpts.ContentType || metadata.info.format; 224 | return metadata; 225 | }), 226 | mergeMap((metadata) => { 227 | const upload = storageOpts.s3.upload(newParams); 228 | 229 | upload.on('httpUploadProgress', (eventProgress) => { 230 | if (eventProgress.total) { 231 | currentSize = eventProgress.total; 232 | } 233 | }); 234 | 235 | const data = upload 236 | .promise() 237 | .then((uploadedData) => ({ ...uploadedData, ...metadata.info })); 238 | const upload$ = from(data); 239 | return upload$; 240 | }), 241 | ) 242 | .subscribe((response) => { 243 | const { size, format, channels, ...details } = response; 244 | const data = { 245 | ACL, 246 | ContentDisposition, 247 | StorageClass, 248 | ServerSideEncryption, 249 | Metadata, 250 | ...details, 251 | size: currentSize || size, 252 | ContentType: storageOpts.ContentType || format, 253 | mimetype: lookup(format) || `image/${format}`, 254 | }; 255 | callback(null, JSON.parse(JSON.stringify(data))); 256 | }, callback); 257 | } 258 | } 259 | 260 | private uploadFileToS3( 261 | params: S3.Types.PutObjectRequest, 262 | file: File, 263 | callback: (error?: any, info?: Info) => void, 264 | ) { 265 | const { storageOpts } = this; 266 | const { mimetype } = file; 267 | 268 | params.ContentType = params.ContentType || mimetype; 269 | 270 | const upload = storageOpts.s3.upload(params); 271 | let currentSize = 0; 272 | 273 | upload.on('httpUploadProgress', (event) => { 274 | if (event.total) { 275 | currentSize = event.total; 276 | } 277 | }); 278 | 279 | upload.promise().then((uploadedData) => { 280 | const data = { 281 | size: currentSize, 282 | ACL: storageOpts.ACL, 283 | ContentType: storageOpts.ContentType || mimetype, 284 | ContentDisposition: storageOpts.ContentDisposition, 285 | StorageClass: storageOpts.StorageClass, 286 | ServerSideEncryption: storageOpts.ServerSideEncryption, 287 | Metadata: storageOpts.Metadata, 288 | ...uploadedData, 289 | }; 290 | callback(null, JSON.parse(JSON.stringify(data))); 291 | }, callback); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /lib/multer-sharp/multer-sharp.utils.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, HttpException, PayloadTooLargeException } from '@nestjs/common'; 2 | import sharp, { Sharp } from 'sharp'; 3 | import { SharpOptions, ResizeOption } from './interfaces/sharp-options.interface'; 4 | import { S3StorageOptions } from './interfaces/s3-storage.interface'; 5 | import { ExtendedOptions, MulterExceptions } from './enums'; 6 | 7 | export const transformException = (error: Error | undefined) => { 8 | if (!error || error instanceof HttpException) { 9 | return error; 10 | } 11 | switch (error.message) { 12 | case MulterExceptions.LIMIT_FILE_SIZE: 13 | return new PayloadTooLargeException(error.message); 14 | case MulterExceptions.LIMIT_FILE_COUNT: 15 | case MulterExceptions.LIMIT_FIELD_KEY: 16 | case MulterExceptions.LIMIT_FIELD_VALUE: 17 | case MulterExceptions.LIMIT_FIELD_COUNT: 18 | case MulterExceptions.LIMIT_UNEXPECTED_FILE: 19 | case MulterExceptions.LIMIT_PART_COUNT: 20 | case MulterExceptions.INVALID_IMAGE_FILE_TYPE: 21 | return new BadRequestException(error.message); 22 | } 23 | return error; 24 | }; 25 | 26 | export const transformImage = (options: SharpOptions, size: ResizeOption): Sharp => { 27 | let imageStream = sharp({ failOnError: false }); 28 | 29 | for (const [key, value] of Object.entries(options)) { 30 | if (value) { 31 | imageStream = resolveImageStream(key, value, size, imageStream); 32 | } 33 | } 34 | return imageStream; 35 | }; 36 | 37 | export const getSharpOptionProps = (storageOpts: S3StorageOptions) => { 38 | const prop = Object.keys(storageOpts).filter(p => p === 'resize' || p === 'resizeMultiple')[0]; 39 | return storageOpts[prop]; 40 | }; 41 | 42 | export const isOriginalSuffix = (suffix: string) => suffix === 'original'; 43 | const isObject = obj => typeof obj === 'object' && obj !== null; 44 | 45 | const resolveImageStream = (key: string, value, size, imageStream: Sharp) => { 46 | switch (key) { 47 | case ExtendedOptions.RESIZE_IMAGE: 48 | case ExtendedOptions.RESIZE_IMAGE_MULTIPLE_SIZES: 49 | if (isObject(size)) { 50 | imageStream = imageStream.resize(size.width, size.height, size.options); 51 | } 52 | break; 53 | } 54 | 55 | return imageStream; 56 | }; 57 | 58 | export const getSharpOptions = (options: SharpOptions): SharpOptions => { 59 | return { 60 | resize: options.resize, 61 | resizeMultiple: options.resizeMultiple, 62 | ignoreAspectRatio: options.ignoreAspectRatio, 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /media/header-dark-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/media/header-dark-theme.png -------------------------------------------------------------------------------- /media/header-light-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/media/header-light-theme.png -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-multer-extended", 3 | "version": "1.4.2", 4 | "description": "Extended MulterModule for NestJS", 5 | "author": "Minsung Kim ", 6 | "license": "MIT", 7 | "url": "https://github.com/jeffminsungkim/nestjs-multer-extended#readme", 8 | "main": "dist/index.js", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "commit": "git-cz", 14 | "coverage": "jest -c ./tests/jest-e2e.json --runInBand --coverage --coverageReporters=text-lcov | coveralls", 15 | "test": "jest --runInBand --coverage", 16 | "test:integration": "jest --config ./tests/jest-e2e.json --runInBand --coverage", 17 | "format": "prettier --write \"lib/**/*.ts\"", 18 | "lint": "tslint -p tsconfig.json -c tslint.json", 19 | "lint:fix": "tslint --fix -c tslint.json 'lib/**/*{.ts,.tsx}'", 20 | "build": "rimraf -rf dist && tsc -p tsconfig.json", 21 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md", 22 | "prepublish:npm": "npm run build", 23 | "publish:npm": "npm publish --access public", 24 | "prepublish:next": "npm run build", 25 | "publish:next": "npm publish --access public --tag next" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/jeffminsungkim/nestjs-multer-extended" 30 | }, 31 | "keywords": [ 32 | "nestjs", 33 | "nest", 34 | "multer", 35 | "multer-sharp", 36 | "sharp", 37 | "file upload", 38 | "file interceptor", 39 | "extend", 40 | "extended", 41 | "aws", 42 | "s3" 43 | ], 44 | "publishConfig": { 45 | "access": "public" 46 | }, 47 | "peerDependencies": { 48 | "@nestjs/common": "^6.11.5 || ^7.0.0", 49 | "@nestjs/platform-express": "^6.11.5 || ^7.0.0" 50 | }, 51 | "dependencies": { 52 | "aws-sdk": "^2.802.0", 53 | "mime-types": "^2.1.27", 54 | "sharp": "^0.26.0" 55 | }, 56 | "devDependencies": { 57 | "@commitlint/cli": "13.1.0", 58 | "@commitlint/config-conventional": "13.1.0", 59 | "@nestjs/common": "7.6.18", 60 | "@nestjs/core": "7.6.18", 61 | "@nestjs/platform-express": "7.6.18", 62 | "@nestjs/testing": "7.6.18", 63 | "@types/express": "4.17.13", 64 | "@types/jest": "27.0.2", 65 | "@types/jest-when": "2.7.3", 66 | "@types/mime-types": "2.1.1", 67 | "@types/multer": "1.4.7", 68 | "@types/node": "14.17.18", 69 | "@types/sharp": "0.26.1", 70 | "@types/sinon": "10.0.3", 71 | "@types/supertest": "2.0.11", 72 | "commitizen": "4.2.4", 73 | "conventional-changelog-cli": "2.1.1", 74 | "coveralls": "3.1.1", 75 | "cz-conventional-changelog": "3.3.0", 76 | "husky": "7.0.2", 77 | "jest": "27.2.1", 78 | "jest-extended": "0.11.5", 79 | "jest-when": "3.4.0", 80 | "lint-staged": "11.1.2", 81 | "nestjs-config": "1.4.8", 82 | "prettier": "2.3.2", 83 | "pretty-quick": "3.1.1", 84 | "reflect-metadata": "0.1.13", 85 | "rxjs": "6.6.7", 86 | "sinon": "11.1.2", 87 | "supertest": "6.1.6", 88 | "ts-jest": "27.0.5", 89 | "ts-node": "10.2.1", 90 | "tsc-watch": "4.5.0", 91 | "tsconfig-paths": "3.11.0", 92 | "tslint": "6.1.3", 93 | "tslint-config-prettier": "1.18.0", 94 | "typescript": "4.0.5" 95 | }, 96 | "husky": { 97 | "hooks": { 98 | "pre-commit": "lint-staged", 99 | "commit-message": "commitlint -E HUSKY_GIT_PARAMS" 100 | } 101 | }, 102 | "config": { 103 | "commitizen": { 104 | "path": "./node_modules/cz-conventional-changelog" 105 | } 106 | }, 107 | "lint-staged": { 108 | "*.ts": [ 109 | "pretty-quick", 110 | "tslint -p tsconfig.json" 111 | ], 112 | "*.{js,json}": "pretty-quick" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "semanticCommits": true, 4 | "packageRules": [ 5 | { 6 | "depTypeList": ["devDependencies"], 7 | "automerge": true 8 | } 9 | ], 10 | "schedule": ["every weekday"] 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/base-path.constants.ts: -------------------------------------------------------------------------------- 1 | export const IMAGE_UPLOAD_MODULE_BASE_PATH = 'image-upload-module'; 2 | export const USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH = 'user-profile-image-upload-module'; 3 | -------------------------------------------------------------------------------- /tests/fixtures/uid.ts: -------------------------------------------------------------------------------- 1 | export const uid = 'aec16138-a75a-4961-b8c1-8e803b6bf2cf'; 2 | -------------------------------------------------------------------------------- /tests/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "verbose": true, 5 | "testEnvironment": "node", 6 | "testRegex": "(.*(e2e-spec))\\.ts$", 7 | "transform": { 8 | "^.+\\.(t|j)s$": "ts-jest" 9 | }, 10 | "coverageDirectory": "coverage" 11 | } 12 | -------------------------------------------------------------------------------- /tests/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import { AppModule } from './app.module'; 4 | import { MulterExceptions } from '../../lib/multer-sharp/enums'; 5 | import { uid } from '../fixtures/uid'; 6 | import { 7 | IMAGE_UPLOAD_MODULE_BASE_PATH, 8 | USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH, 9 | } from '../fixtures/base-path.constants'; 10 | 11 | import path from 'path'; 12 | import request from 'supertest'; 13 | 14 | describe('AppModule', () => { 15 | let app: INestApplication; 16 | 17 | beforeEach(async () => { 18 | const module: TestingModule = await Test.createTestingModule({ 19 | imports: [AppModule], 20 | }).compile(); 21 | 22 | app = module.createNestApplication(); 23 | await app.init(); 24 | }); 25 | 26 | afterAll(async () => { 27 | await app.close(); 28 | }); 29 | 30 | describe('ImageUploadController /POST', () => { 31 | const basePath = `${IMAGE_UPLOAD_MODULE_BASE_PATH}`; 32 | const dynamicPath = `${IMAGE_UPLOAD_MODULE_BASE_PATH}/${uid}`; 33 | 34 | it(`should upload an image under the base path "${basePath}"`, async () => { 35 | const res = await request(app.getHttpServer()) 36 | .post(`/image-upload/without-dynamic-key-path`) 37 | .set('Content-Type', 'multipart/form-data') 38 | .attach('file', path.resolve(__dirname, 'data/smile.jpg')); 39 | 40 | expect(res.status).toEqual(201); 41 | expect(res.body.key).toEqual(`${basePath}/smile.jpg`); 42 | }); 43 | 44 | it(`should upload an image under the dynamic path "${dynamicPath}"`, async () => { 45 | const res = await request(app.getHttpServer()) 46 | .post(`/image-upload/with-dynamic-key-path`) 47 | .set('Content-Type', 'multipart/form-data') 48 | .attach('file', path.resolve(__dirname, 'data/smile.jpg')); 49 | 50 | expect(res.status).toEqual(201); 51 | expect(res.body.key).toEqual(`${dynamicPath}/smile.jpg`); 52 | }); 53 | 54 | it(`should upload an image with random filename`, async () => { 55 | const res = await request(app.getHttpServer()) 56 | .post(`/image-upload/with-random-filename`) 57 | .set('Content-Type', 'multipart/form-data') 58 | .attach('file', path.resolve(__dirname, 'data/smile.jpg')); 59 | 60 | expect(res.status).toEqual(201); 61 | }); 62 | 63 | it(`should not upload non image format`, async () => { 64 | const res = await request(app.getHttpServer()) 65 | .post(`/image-upload/non-image-file`) 66 | .set('Content-Type', 'multipart/form-data') 67 | .attach('file', path.resolve(__dirname, 'data/Readme.md')); 68 | 69 | expect(res.status).toEqual(400); 70 | expect(res.body.message).toEqual(MulterExceptions.INVALID_IMAGE_FILE_TYPE); 71 | }); 72 | 73 | it(`should upload non image format if the file filter is missing`, async () => { 74 | const res = await request(app.getHttpServer()) 75 | .post(`/image-upload/non-image-file-no-filter`) 76 | .set('Content-Type', 'multipart/form-data') 77 | .attach('file', path.resolve(__dirname, 'data/Readme.md')); 78 | 79 | expect(res.status).toEqual(201); 80 | expect(res.body.key).toEqual(`${basePath}/Readme.md`); 81 | }); 82 | 83 | it(`should not upload an image when its size exceed the limit`, async () => { 84 | const res = await request(app.getHttpServer()) 85 | .post(`/image-upload/big-size-file`) 86 | .set('Content-Type', 'multipart/form-data') 87 | .attach('file', path.resolve(__dirname, 'data/cat.jpg')); 88 | 89 | expect(res.status).toEqual(413); 90 | expect(res.body.message).toEqual(MulterExceptions.LIMIT_FILE_SIZE); 91 | }); 92 | 93 | it(`should upload an image when the size limit option sets higher than the file size`, async () => { 94 | const res = await request(app.getHttpServer()) 95 | .post(`/image-upload/big-size-file-higher-limit`) 96 | .set('Content-Type', 'multipart/form-data') 97 | .attach('file', path.resolve(__dirname, 'data/cat.jpg')); 98 | 99 | expect(res.status).toEqual(201); 100 | expect(res.body.key).toEqual(`${basePath}/cat.jpg`); 101 | }); 102 | }); 103 | 104 | describe('UserProfileImageUploadController /POST', () => { 105 | const basePath = `${USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH}`; 106 | const dynamicPath = `${USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH}/${uid}`; 107 | 108 | it(`should upload an image under the base path "${basePath}"`, async () => { 109 | const res = await request(app.getHttpServer()) 110 | .post(`/user-profile-image-upload/without-dynamic-key-path`) 111 | .set('Content-Type', 'multipart/form-data') 112 | .attach('file', path.resolve(__dirname, 'data/crying.jpg')); 113 | 114 | expect(res.status).toEqual(201); 115 | expect(res.body.key).toEqual(`${basePath}/crying.jpg`); 116 | }); 117 | 118 | it(`should upload an image under the dynamic path "${dynamicPath}"`, async () => { 119 | const res = await request(app.getHttpServer()) 120 | .post(`/user-profile-image-upload/with-dynamic-key-path`) 121 | .set('Content-Type', 'multipart/form-data') 122 | .attach('file', path.resolve(__dirname, 'data/crying.jpg')); 123 | 124 | expect(res.status).toEqual(201); 125 | expect(res.body.key).toEqual(`${dynamicPath}/crying.jpg`); 126 | }); 127 | 128 | it(`should upload an image under the first path parameter :key(abcd1234)`, async () => { 129 | const res = await request(app.getHttpServer()) 130 | .post(`/user-profile-image-upload/use-path-param-as-a-key/abcd1234`) 131 | .set('Content-Type', 'multipart/form-data') 132 | .attach('file', path.resolve(__dirname, 'data/crying.jpg')); 133 | 134 | expect(res.status).toEqual(201); 135 | expect(res.body.key).toEqual(`user-profile-image-upload-module/abcd1234/crying.jpg`); 136 | }); 137 | 138 | it(`should upload an image under :key(abcd1234)/:id(msk)`, async () => { 139 | const res = await request(app.getHttpServer()) 140 | .post(`/user-profile-image-upload/use-path-param-as-a-key/abcd1234/user/msk`) 141 | .set('Content-Type', 'multipart/form-data') 142 | .attach('file', path.resolve(__dirname, 'data/crying.jpg')); 143 | 144 | expect(res.status).toEqual(201); 145 | expect(res.body.key).toEqual(`user-profile-image-upload-module/abcd1234/msk/crying.jpg`); 146 | }); 147 | 148 | it(`should upload both an original and a thumbnail image`, async () => { 149 | const res = await request(app.getHttpServer()) 150 | .post(`/user-profile-image-upload/create-thumbnail-with-custom-options`) 151 | .set('Content-Type', 'multipart/form-data') 152 | .attach('file', path.resolve(__dirname, 'data/crying.jpg')); 153 | const { thumbnail, original } = res.body; 154 | 155 | expect(res.status).toEqual(201); 156 | expect(thumbnail.width).toEqual(250); 157 | expect(thumbnail.height).toEqual(250); 158 | expect(thumbnail.key).toEqual(`${basePath}/crying.jpg-thumbnail`); 159 | expect(original.key).toEqual(`${basePath}/crying.jpg-original`); 160 | }); 161 | 162 | it(`should upload thumb and original under the dynamic path "${dynamicPath}/test"`, async () => { 163 | const res = await request(app.getHttpServer()) 164 | .post(`/user-profile-image-upload/create-thumbnail-with-dynamic-key`) 165 | .set('Content-Type', 'multipart/form-data') 166 | .attach('file', path.resolve(__dirname, 'data/crying.jpg')); 167 | const { thumb, original } = res.body; 168 | 169 | expect(res.status).toEqual(201); 170 | expect(thumb.width).toEqual(200); 171 | expect(thumb.height).toEqual(200); 172 | expect(thumb.key).toEqual(`${dynamicPath}/test/crying.jpg-thumb`); 173 | expect(original.key).toEqual(`${dynamicPath}/test/crying.jpg-original`); 174 | }); 175 | 176 | it(`should upload resized image`, async () => { 177 | const res = await request(app.getHttpServer()) 178 | .post(`/user-profile-image-upload/resized`) 179 | .set('Content-Type', 'multipart/form-data') 180 | .attach('file', path.resolve(__dirname, 'data/go.jpeg')); 181 | 182 | expect(res.status).toEqual(201); 183 | expect(res.body.width).toEqual(500); 184 | expect(res.body.height).toEqual(450); 185 | expect(res.body.key).toEqual(`${basePath}/go.jpeg`); 186 | }); 187 | 188 | it(`should upload images in different sizes`, async () => { 189 | const res = await request(app.getHttpServer()) 190 | .post(`/user-profile-image-upload/different-sizes`) 191 | .set('Content-Type', 'multipart/form-data') 192 | .attach('file', path.resolve(__dirname, 'data/cat.jpg')); 193 | const { sm, md, lg } = res.body; 194 | 195 | expect(res.status).toEqual(201); 196 | expect(sm.width).toEqual(200); 197 | expect(sm.height).toEqual(200); 198 | expect(md.height).toEqual(300); 199 | expect(md.height).toEqual(300); 200 | expect(lg.height).toEqual(400); 201 | expect(lg.height).toEqual(400); 202 | expect(sm.key).toEqual(`${dynamicPath}/different-sizes/cat.jpg-sm`); 203 | expect(md.key).toEqual(`${dynamicPath}/different-sizes/cat.jpg-md`); 204 | expect(lg.key).toEqual(`${dynamicPath}/different-sizes/cat.jpg-lg`); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /tests/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from 'nestjs-config'; 3 | import { UserProfileImageUploadModule } from './user-profile-image-upload/user-profile-upload.module'; 4 | import { ImageUploadModule } from './image-upload/image-upload.module'; 5 | import path from 'path'; 6 | 7 | @Module({ 8 | imports: [ 9 | ConfigModule.load(path.resolve(__dirname, 'config', '**', '!(*.d).{ts,js}')), 10 | UserProfileImageUploadModule, 11 | ImageUploadModule, 12 | ], 13 | }) 14 | export class AppModule {} 15 | -------------------------------------------------------------------------------- /tests/src/config/aws.ts: -------------------------------------------------------------------------------- 1 | import { MulterExtendedS3Options } from '../../../lib/interfaces'; 2 | import { 3 | IMAGE_UPLOAD_MODULE_BASE_PATH, 4 | USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH, 5 | } from '../../fixtures/base-path.constants'; 6 | import { Logger } from '@nestjs/common'; 7 | 8 | export default { 9 | optionA: { 10 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 11 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 12 | region: process.env.AWS_S3_REGION, 13 | bucket: process.env.AWS_S3_BUCKET_NAME, 14 | basePath: USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH, 15 | fileSize: process.env.AWS_S3_MAX_IMAGE_SIZE, 16 | } as MulterExtendedS3Options, 17 | optionB: { 18 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 19 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 20 | region: process.env.AWS_S3_REGION, 21 | bucket: process.env.AWS_S3_BUCKET_NAME, 22 | basePath: IMAGE_UPLOAD_MODULE_BASE_PATH, 23 | fileSize: 1 * 1024 * 1024, 24 | acl: 'private', 25 | logger: new Logger('Test Logger'), 26 | } as MulterExtendedS3Options, 27 | }; 28 | -------------------------------------------------------------------------------- /tests/src/data/Readme.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | Nest is a framework for building efficient, scalable 28 | Node.js server-side applications. It uses modern 29 | JavaScript, is built with TypeScript 30 | (preserves compatibility with pure JavaScript) and combines elements of OOP (Object Oriented 31 | Programming), FP (Functional Programming), and FRP (Functional Reactive Programming). 32 | 33 |

Under the hood, Nest makes use of Express, but also, provides compatibility with a wide range of other libraries, like e.g. Fastify, allowing for easy use of the myriad third-party plugins which are available.

34 | 35 | ## Philosophy 36 | 37 |

In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like Angular, React and Vue which improve developer productivity and enable the construction of fast, testable, extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers and tools for Node, none of them effectively solve the main problem - the architecture.

38 |

Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, loosely coupled and easily maintainable applications.

39 | 40 | ## Getting started 41 | 42 | - To check out the [guide](https://docs.nestjs.com), visit 43 | [docs.nestjs.com](https://docs.nestjs.com). :books: 44 | - 要查看中文 [指南](readme_zh.md), 请访问 [docs.nestjs.cn](https://docs.nestjs.cn). :books: 45 | 46 | ## Consulting 47 | 48 | With official support, you can get expert help straight from Nest core team. We provide dedicated 49 | technical support, migration strategies, advice on best practices (and design decisions), PR 50 | reviews, and team augmentation. Read more about [support here](https://enterprise.nestjs.com). 51 | 52 | ## Support 53 | 54 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the 55 | amazing backers. If you'd like to join them, please 56 | [read more here](https://docs.nestjs.com/support). 57 | 58 | #### Principal Sponsor 59 | 60 | 61 | 62 | #### Silver Sponsors 63 | 64 | 65 |   66 | 67 |   68 | 69 |   70 | 71 | 72 | #### Sponsors 73 | 74 | 75 |   76 | 77 |   78 | 79 |   80 | 81 | 82 |   83 | 84 |   85 | 86 |   87 | 88 |   89 | 90 |   91 | 92 |   93 | 94 |   95 | 96 |   97 | 98 |     99 | 100 |   101 | 102 |   103 | 104 |   105 | 106 |   107 | 108 |   109 | 110 |   111 | 112 | 113 | ## Backers 114 | 115 | 116 | 117 | ## Stay in touch 118 | 119 | - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) 120 | - Website - [https://nestjs.com](https://nestjs.com/) 121 | - Twitter - [@nestframework](https://twitter.com/nestframework) 122 | 123 | ## License 124 | 125 | Nest is [MIT licensed](LICENSE). 126 | -------------------------------------------------------------------------------- /tests/src/data/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/tests/src/data/cat.jpg -------------------------------------------------------------------------------- /tests/src/data/crying.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/tests/src/data/crying.jpg -------------------------------------------------------------------------------- /tests/src/data/go.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/tests/src/data/go.jpeg -------------------------------------------------------------------------------- /tests/src/data/smile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/tests/src/data/smile.jpg -------------------------------------------------------------------------------- /tests/src/image-upload/image-upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; 2 | import { AmazonS3FileInterceptor } from '../../../lib/interceptors'; 3 | import { uid } from '../../fixtures/uid'; 4 | 5 | @Controller('image-upload') 6 | export class ImageUploadController { 7 | @Post('without-dynamic-key-path') 8 | @UseInterceptors(AmazonS3FileInterceptor('file')) 9 | async uploadImageWithoutKeyOption(@UploadedFile() file: any): Promise { 10 | return file; 11 | } 12 | 13 | @Post('with-dynamic-key-path') 14 | @UseInterceptors(AmazonS3FileInterceptor('file', { dynamicPath: uid })) 15 | async uploadImageWithKeyOption(@UploadedFile() file: any): Promise { 16 | return file; 17 | } 18 | 19 | @Post('with-random-filename') 20 | @UseInterceptors(AmazonS3FileInterceptor('file', { randomFilename: true })) 21 | async uploadImageWithRandomFilenameKeyOption(@UploadedFile() file: any): Promise { 22 | return file; 23 | } 24 | 25 | @Post('non-image-file') 26 | @UseInterceptors(AmazonS3FileInterceptor('file')) 27 | async uploadNonImageFile(@UploadedFile() file: any): Promise {} 28 | 29 | @Post('non-image-file-no-filter') 30 | @UseInterceptors(AmazonS3FileInterceptor('file', { fileFilter: undefined })) 31 | async uploadNonImageFileWithoutFilter(@UploadedFile() file: any): Promise { 32 | return file; 33 | } 34 | 35 | @Post('big-size-file') 36 | @UseInterceptors(AmazonS3FileInterceptor('file')) 37 | async uploadBigImage(@UploadedFile() file: any): Promise {} 38 | 39 | @Post('big-size-file-higher-limit') 40 | @UseInterceptors(AmazonS3FileInterceptor('file', { limits: { fileSize: 3 * 1024 * 1024 } })) 41 | async uploadBigImageUsingCustomLimitOption(@UploadedFile() file: any): Promise { 42 | return file; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/src/image-upload/image-upload.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ImageUploadController } from './image-upload.controller'; 3 | import { MulterExtendedModule } from '../../../lib/multer-extended.module'; 4 | import { ConfigService } from 'nestjs-config'; 5 | 6 | @Module({ 7 | imports: [ 8 | MulterExtendedModule.registerAsync({ 9 | useFactory: (configService: ConfigService) => configService.get('aws.optionB'), 10 | inject: [ConfigService], 11 | }), 12 | ], 13 | controllers: [ImageUploadController], 14 | }) 15 | export class ImageUploadModule {} 16 | -------------------------------------------------------------------------------- /tests/src/user-profile-image-upload/user-profile-image-upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; 2 | import { AmazonS3FileInterceptor } from '../../../lib/interceptors'; 3 | import { uid } from '../../fixtures/uid'; 4 | 5 | @Controller('user-profile-image-upload') 6 | export class UserProfileImageUploadController { 7 | @Post('without-dynamic-key-path') 8 | @UseInterceptors(AmazonS3FileInterceptor('file')) 9 | async uploadImageWithoutKeyOption(@UploadedFile() file: any): Promise { 10 | return file; 11 | } 12 | 13 | @Post('with-dynamic-key-path') 14 | @UseInterceptors(AmazonS3FileInterceptor('file', { dynamicPath: uid })) 15 | async uploadImageWithKeyOption(@UploadedFile() file: any): Promise { 16 | return file; 17 | } 18 | 19 | @Post('use-path-param-as-a-key/:key') 20 | @UseInterceptors(AmazonS3FileInterceptor('file', { dynamicPath: 'key' })) 21 | async uploadImageWithPathParamKey(@UploadedFile() file: any): Promise { 22 | return file; 23 | } 24 | 25 | @Post('use-path-param-as-a-key/:key/user/:id') 26 | @UseInterceptors(AmazonS3FileInterceptor('file', { dynamicPath: ['key', 'id'] })) 27 | async uploadImageWithMultiplePathParamKeys(@UploadedFile() file: any): Promise { 28 | return file; 29 | } 30 | 31 | @Post('create-thumbnail-with-custom-options') 32 | @UseInterceptors( 33 | AmazonS3FileInterceptor('file', { 34 | thumbnail: { suffix: 'thumbnail', width: 250, height: 250 }, 35 | limits: { fileSize: 7 * 1024 * 1024 }, 36 | }), 37 | ) 38 | async uploadImageWithThumbnail(@UploadedFile() file: any): Promise { 39 | return file; 40 | } 41 | 42 | @Post('create-thumbnail-with-dynamic-key') 43 | @UseInterceptors( 44 | AmazonS3FileInterceptor('file', { 45 | thumbnail: { suffix: 'thumb', width: 200, height: 200 }, 46 | limits: { fileSize: 2 * 1024 * 1024 }, 47 | dynamicPath: `${uid}/test`, 48 | }), 49 | ) 50 | async uploadImageWithDynamicKey(@UploadedFile() file: any): Promise { 51 | return file; 52 | } 53 | 54 | @Post('resized') 55 | @UseInterceptors( 56 | AmazonS3FileInterceptor('file', { 57 | resize: { width: 500, height: 450 }, 58 | }), 59 | ) 60 | async uploadResizedImage(@UploadedFile() file: any): Promise { 61 | return file; 62 | } 63 | 64 | @Post('different-sizes') 65 | @UseInterceptors( 66 | AmazonS3FileInterceptor('file', { 67 | resizeMultiple: [ 68 | { suffix: 'sm', width: 200, height: 200 }, 69 | { suffix: 'md', width: 300, height: 300 }, 70 | { suffix: 'lg', width: 400, height: 400 }, 71 | ], 72 | dynamicPath: `${uid}/different-sizes`, 73 | }), 74 | ) 75 | async uploadImageWithDifferentSizes(@UploadedFile() file: any): Promise { 76 | return file; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/src/user-profile-image-upload/user-profile-upload.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserProfileImageUploadController } from './user-profile-image-upload.controller'; 3 | import { MulterExtendedModule } from '../../../lib/multer-extended.module'; 4 | import { ConfigService } from 'nestjs-config'; 5 | 6 | @Module({ 7 | imports: [ 8 | MulterExtendedModule.registerAsync({ 9 | useFactory: (configService: ConfigService) => configService.get('aws.optionA'), 10 | inject: [ConfigService], 11 | }), 12 | ], 13 | controllers: [UserProfileImageUploadController], 14 | }) 15 | export class UserProfileImageUploadModule {} 16 | -------------------------------------------------------------------------------- /tests/unit/multer-sharp/multer-sharp.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jest-extended'; 2 | import { HttpException, PayloadTooLargeException, BadRequestException } from '@nestjs/common'; 3 | import AWS from 'aws-sdk'; 4 | import { 5 | transformImage, 6 | isOriginalSuffix, 7 | getSharpOptionProps, 8 | getSharpOptions, 9 | transformException, 10 | } from '../../../lib/multer-sharp/multer-sharp.utils'; 11 | import { S3StorageOptions } from '../../../lib/multer-sharp/interfaces/s3-storage.interface'; 12 | import { SharpOptions } from '../../../lib/multer-sharp/interfaces/sharp-options.interface'; 13 | import { MulterExceptions } from '../../../lib/multer-sharp/enums'; 14 | 15 | describe('Shared Multer Sharp Utils', () => { 16 | describe('transformException', () => { 17 | describe('if error does not exist', () => { 18 | it('behave as identity', () => { 19 | const err = undefined; 20 | expect(transformException(err)).toEqual(err); 21 | }); 22 | }); 23 | describe('if error is instance of HttpException', () => { 24 | it('behave as identity', () => { 25 | const err = new HttpException('response', 500); 26 | expect(transformException(err)).toEqual(err); 27 | }); 28 | }); 29 | describe('if error exists and is not instance of HttpException', () => { 30 | describe('and is LIMIT_FILE_SIZE exception', () => { 31 | it('should return "PayloadTooLargeException"', () => { 32 | const err = { message: MulterExceptions.LIMIT_FILE_SIZE }; 33 | expect(transformException(err as any)).toBeInstanceOf(PayloadTooLargeException); 34 | }); 35 | }); 36 | describe('and is multer exception but not a LIMIT_FILE_SIZE', () => { 37 | it('should return "BadRequestException"', () => { 38 | const err = { message: MulterExceptions.INVALID_IMAGE_FILE_TYPE }; 39 | expect(transformException(err as any)).toBeInstanceOf(BadRequestException); 40 | }); 41 | }); 42 | }); 43 | }); 44 | 45 | describe('transformImage', () => { 46 | it('should return resolved image stream when the property is resize', () => { 47 | const option: SharpOptions = { 48 | resize: { width: 300, height: 350 }, 49 | }; 50 | expect(transformImage(option, option.resize)).toBeObject(); 51 | }); 52 | }); 53 | 54 | describe('isOriginalSuffix', () => { 55 | it('should return true when the suffix is original', () => { 56 | expect(isOriginalSuffix('original')).toBeTruthy(); 57 | }); 58 | 59 | it('should return false when the suffix is not original', () => { 60 | expect(isOriginalSuffix('thumbnail')).toBeFalsy(); 61 | }); 62 | }); 63 | 64 | describe('getSharpOptionProps', () => { 65 | let storageOpts: S3StorageOptions; 66 | 67 | beforeEach(() => { 68 | storageOpts = { 69 | s3: new AWS.S3(), 70 | resizeMultiple: [ 71 | { suffix: 'xs', width: 100, height: 100 }, 72 | { suffix: 'sm', width: 200, height: 200 }, 73 | { suffix: 'md', width: 300, height: 300 }, 74 | { suffix: 'lg', width: 400, height: 400 }, 75 | ], 76 | resize: { width: 500, height: 450 }, 77 | }; 78 | }); 79 | 80 | describe('The first set of property serves first', () => { 81 | it('should return an array of storage options when the resizeMultiple property set before the resize property', () => { 82 | expect(getSharpOptionProps(storageOpts)).toEqual([ 83 | { suffix: 'xs', width: 100, height: 100 }, 84 | { suffix: 'sm', width: 200, height: 200 }, 85 | { suffix: 'md', width: 300, height: 300 }, 86 | { suffix: 'lg', width: 400, height: 400 }, 87 | ]); 88 | }); 89 | 90 | it('should return an object value of resize property when the resizeMultiple property is missing', () => { 91 | delete storageOpts.resizeMultiple; 92 | 93 | expect(getSharpOptionProps(storageOpts)).toEqual({ width: 500, height: 450 }); 94 | }); 95 | }); 96 | }); 97 | 98 | describe('getSharpOptions', () => { 99 | let options: SharpOptions; 100 | 101 | beforeEach(() => { 102 | options = { 103 | resize: { width: 500, height: 450 }, 104 | ignoreAspectRatio: true, 105 | }; 106 | }); 107 | 108 | it('should return SharpOptions', () => { 109 | expect(getSharpOptions(options)).toContainAllEntries([ 110 | ['resizeMultiple', undefined], 111 | ['resize', { width: 500, height: 450 }], 112 | ['ignoreAspectRatio', true], 113 | ]); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "tests", "media", "dist"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "esModuleInterop": true, 9 | "target": "es6", 10 | "sourceMap": false, 11 | "outDir": "./dist", 12 | "rootDir": "./lib", 13 | "baseUrl": "./", 14 | "noLib": false, 15 | "noImplicitAny": false, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["lib/**/*.ts"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "callable-types": true, 6 | "class-name": true, 7 | "comment-format": [true, "check-space"], 8 | "curly": true, 9 | "eofline": true, 10 | "forin": true, 11 | "import-spacing": true, 12 | "indent": [true, "spaces"], 13 | "interface-over-type-literal": true, 14 | "label-position": true, 15 | "max-line-length": [true, 140], 16 | "max-union-size": false, 17 | "member-access": false, 18 | "member-ordering": [ 19 | true, 20 | { 21 | "order": [ 22 | "static-field", 23 | "instance-field", 24 | "static-method", 25 | "instance-method" 26 | ] 27 | } 28 | ], 29 | "no-arg": true, 30 | "no-bitwise": true, 31 | "no-console": true, 32 | "no-construct": true, 33 | "no-debugger": true, 34 | "no-duplicate-super": true, 35 | "no-empty": false, 36 | "no-empty-interface": true, 37 | "no-eval": true, 38 | "no-inferrable-types": [true, "ignore-params"], 39 | "no-misused-new": true, 40 | "no-non-null-assertion": true, 41 | "no-shadowed-variable": true, 42 | "no-string-literal": false, 43 | "no-string-throw": true, 44 | "no-switch-case-fall-through": true, 45 | "no-trailing-whitespace": true, 46 | "no-unnecessary-initializer": true, 47 | "no-unused-expression": true, 48 | "no-var-keyword": true, 49 | "object-literal-sort-keys": false, 50 | "one-line": [ 51 | true, 52 | "check-open-brace", 53 | "check-catch", 54 | "check-else", 55 | "check-whitespace" 56 | ], 57 | "prefer-const": true, 58 | "quotemark": [true, "single", "warn"], 59 | "radix": false, 60 | "semicolon": [true, "always", "ignore-interfaces"], 61 | "trailing-comma": true, 62 | "triple-equals": [true, "allow-null-check"], 63 | "typedef-whitespace": [ 64 | true, 65 | { 66 | "call-signature": "nospace", 67 | "index-signature": "nospace", 68 | "parameter": "nospace", 69 | "property-declaration": "nospace", 70 | "variable-declaration": "nospace" 71 | } 72 | ], 73 | "unified-signatures": true, 74 | "variable-name": false, 75 | "whitespace": [ 76 | true, 77 | "check-branch", 78 | "check-decl", 79 | "check-operator", 80 | "check-separator", 81 | "check-type" 82 | ], 83 | "no-implicit-dependencies": false, 84 | "no-submodule-imports": false, 85 | "interface-name": false 86 | } 87 | } 88 | --------------------------------------------------------------------------------