├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── node.js.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ava.config.js ├── conf.json ├── lib ├── cache.d.ts ├── cache.js ├── cache.js.map ├── gridfs.d.ts ├── gridfs.js ├── gridfs.js.map ├── index.d.ts ├── index.js ├── index.js.map ├── types │ ├── cache-index.d.ts │ ├── cache-index.js │ ├── cache-index.js.map │ ├── cache-value.d.ts │ ├── cache-value.js │ ├── cache-value.js.map │ ├── comparator-result.d.ts │ ├── comparator-result.js │ ├── comparator-result.js.map │ ├── connection-result.d.ts │ ├── connection-result.js │ ├── connection-result.js.map │ ├── db-storage-options.d.ts │ ├── db-storage-options.js │ ├── db-storage-options.js.map │ ├── db-types.d.ts │ ├── db-types.js │ ├── db-types.js.map │ ├── grid-file.d.ts │ ├── grid-file.js │ ├── grid-file.js.map │ ├── index.d.ts │ ├── index.js │ ├── index.js.map │ ├── multer-gfs-options.d.ts │ ├── multer-gfs-options.js │ ├── multer-gfs-options.js.map │ ├── node-callback.d.ts │ ├── node-callback.js │ ├── node-callback.js.map │ ├── url-storage-options.d.ts │ ├── url-storage-options.js │ └── url-storage-options.js.map ├── utils.d.ts ├── utils.js └── utils.js.map ├── package-lock.json ├── package.json ├── src ├── cache.ts ├── gridfs.ts ├── index.ts ├── types │ ├── cache-index.ts │ ├── cache-value.ts │ ├── comparator-result.ts │ ├── connection-result.ts │ ├── db-storage-options.ts │ ├── db-types.ts │ ├── grid-file.ts │ ├── index.ts │ ├── multer-gfs-options.ts │ ├── node-callback.ts │ └── url-storage-options.ts └── utils.ts ├── test ├── attachments │ ├── sample1.jpg │ └── sample2.jpg ├── cache-class.spec.ts ├── cache-errors.spec.ts ├── cache-handling.spec.ts ├── connection-options.spec.ts ├── connection-ready.spec.ts ├── default-generator.spec.ts ├── edge-cases.spec.ts ├── error-handling.spec.ts ├── file-concurrency.spec.ts ├── file-function.spec.ts ├── generator-promises.spec.ts ├── handling-names.spec.ts ├── incomplete-generators.spec.ts ├── md5-hash.spec.ts ├── storage-constructor.spec.ts ├── tsconfig.json ├── types │ ├── cache-class-context.ts │ ├── cache-errors-context.ts │ ├── cache-handling-context.ts │ ├── connection-options-context.ts │ ├── connection-ready-context.ts │ ├── default-generator-context.ts │ ├── edge-cases-context.ts │ ├── error-handling-context.ts │ ├── file-concurrency-context.ts │ ├── file-function-context.ts │ ├── generator-promises-context.ts │ ├── handling-names-context.ts │ ├── incomplete-generators-context.ts │ ├── md5-hash-context.ts │ ├── storage-constructor-context.ts │ ├── uploaded-file-context.ts │ ├── utility-functions-context.ts │ └── utility-methods-context.ts ├── uploaded-file.spec.ts ├── utility-functions.spec.ts ├── utility-methods.spec.ts └── utils │ ├── macros.ts │ ├── settings.ts │ └── testutils.ts ├── tsconfig.json └── wiki └── Generators.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/**/* 2 | docs/**/* 3 | examples/** 4 | lib/** 5 | 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multer-gridfs-storage", 3 | "eslintConfig": { 4 | "extends": [ 5 | "xo", 6 | "xo-typescript" 7 | ] 8 | }, 9 | "rules": { 10 | "max-len": ["error", { 11 | "code": 180 12 | }] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Help fix a problem you found with this project 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | **Describe the bug** 15 | 16 | 17 | **Environment** 18 | 19 | 20 | 21 | - I'm using `multer-gridfs-storage` version *(major.minor.patch)* 22 | 23 | - My installed MongoDb version is *(major.minor.patch)* 24 | 25 | - I have Multer *(major.minor.patch)* installed to upload files 26 | 27 | - The Node version used to run the code is *(major.minor.patch)* 28 | 29 | 30 | 31 | - I'm *(using/not using)* Mongoose connection objects to create storage instances. The Mongoose version installed is *(major.minor.patch)* 32 | 33 | **To Reproduce** 34 | 35 | 36 | 37 | **Expected behavior** 38 | 39 | 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other issue 3 | about: Contribute with this project in any other way 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | **Describe your problem** 15 | 16 | 17 | **Environment** 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | **Additional context** 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | **Is your feature request related to a problem? Please describe.** 15 | 16 | 17 | **Describe the solution you'd like** 18 | 19 | 20 | **Describe alternatives you've considered** 21 | 22 | 23 | **Additional context** 24 | 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### PR Checklist 2 | 3 | Verify that you did the following: 4 | 5 | - [ ] Opened an issue to track and discuss the problem you are trying to solve. 6 | - [ ] Submitted the changes using a new branch in your fork of this repo. 7 | - [ ] Added test for the changes and checked that code coverage didn't diminished if possible. 8 | - [ ] Docs were updated using jsdoc style comments when necessary. 9 | 10 | #### Related issue 11 | 12 | 13 | Issue: XX 14 | 15 | #### Describe what you did 16 | 17 | 18 | 19 | #### Is this a breaking change? 20 | 21 | 22 | 23 | - [ ] Yes 24 | - [ ] No 25 | 26 | #### Other information 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | target-branch: develop 9 | ignore: 10 | - dependency-name: eslint 11 | versions: 12 | - 7.19.0 13 | - 7.20.0 14 | - 7.22.0 15 | - 7.24.0 16 | - dependency-name: "@types/mongodb" 17 | versions: 18 | - 3.6.11 19 | - 3.6.6 20 | - 3.6.7 21 | - 3.6.9 22 | - dependency-name: mongoose 23 | versions: 24 | - 5.11.13 25 | - 5.11.19 26 | - 5.12.0 27 | - 5.12.1 28 | - 5.12.2 29 | - 5.12.3 30 | - dependency-name: typescript 31 | versions: 32 | - 4.1.3 33 | - 4.1.5 34 | - 4.2.3 35 | - dependency-name: xo 36 | versions: 37 | - 0.37.1 38 | - 0.38.1 39 | - dependency-name: supertest 40 | versions: 41 | - 6.1.2 42 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ '**' ] 6 | pull_request: 7 | branches: [ '**' ] 8 | 9 | jobs: 10 | default: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [ 14.x, 16.x, 18.x ] 15 | mongodb-version: [ '6.3' ] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - name: Start MongoDB 25 | uses: supercharge/mongodb-github-action@1.6.0 26 | with: 27 | mongodb-version: ${{ matrix.mongodb-version }} 28 | - run: npm ci 29 | - run: npm install multer --no-save 30 | - run: npm install mongodb --no-save 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | coverage/ 4 | .coveralls.yml 5 | docs/ 6 | .nyc_output/ 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 5.0.2 2 | 3 | * Fixed: Solved bug when not using the client parameter and the topology is not present in the db object #377 4 | * Update: Updated dependencies 5 | 6 | # 5.0.1 7 | 8 | * Fixed: Updated ObjectID reference to ObjectId to allow compatibility with mongodb4. 9 | 10 | # 5.0.0 11 | 12 | * Feature: Module rewritten in Typescript. Separate definition files are no longer required. 13 | * Fixed: If using the `fromStream` method the readable source emits an error the promise is rejected. #205 14 | * Fixed: Attached events to `MongoClient` or `Db` object depending on the installed mongo version. 15 | * Fixed: Replaced mongoose reference with mongoose like object to avoid version conflicts. 16 | * Update: Updated dependencies. 17 | 18 | # 4.2.0 19 | 20 | * Feature: Added the `fromFile` and `fromStream` public methods 21 | * Update: Documented the `generateBytes` method 22 | * Update: Updated dependencies 23 | 24 | # 4.1.0 25 | 26 | * Breaking change: Removed Node 8 support 27 | * Update: Updated dependencies 28 | 29 | # 4.0.3 30 | 31 | * Update: Updated dependencies 32 | 33 | # 4.0.2 34 | 35 | * Update: Updated dependencies 36 | 37 | # 4.0.1 38 | 39 | * Fix: Moved multer from dependencies to peerDependencies 40 | * Fix: Removed xo from dependencies 41 | * Update: Updated `pump` dependency 42 | 43 | # 4.0.0 44 | 45 | * Feature: Added the `client` option to the constructor 46 | * Feature: Supported `client` as a promise 47 | * Update: Removed the `connectionOpts` setting 48 | * Breaking change: Removed Node 6 support 49 | * Breaking change: The `ready` method and the `connection` event now produces an object with the `db` and the `client` 50 | 51 | # 3.3.0 52 | 53 | * Update: Removed compatibility with Node 4 54 | 55 | # 3.2.3 56 | 57 | * Fix: Solved bug in mongodb@2 and mongoose compatibility 58 | 59 | # 3.2.2 60 | 61 | * Fix: Removed multer extra dependency from `package.json` 62 | 63 | # 3.2.1 64 | 65 | * Feature: Added `aliases` and `disableMD5` properties to file naming configuration 66 | 67 | # 3.2.0 68 | 69 | * Feature: Support for Mongoose connections 70 | * Feature: Ready method to wait for the MongoDb connection 71 | * Breaking change: Deprecated "connectionOpts" in favor of "options" 72 | 73 | # 3.1.0 74 | 75 | * Feature: Added caching feature 76 | * Fix: Updated dependencies 77 | * Fix: Moved multer to peer dependencies 78 | * Breaking change: Dropped support for node 0.x 79 | * Breaking change: Removed es6-promise dependency 80 | * Breaking change: Added lodash.isplainobject dependency 81 | 82 | # 3.0.1 83 | 84 | * Fix: Changed mongodb dependency version from 3 to >=2 85 | 86 | # 3.0.0 87 | 88 | * Feature: Added support for mongodb version 3 in url connection string 89 | * Feature: Added `client` property to storage object 90 | 91 | # 2.1.0 92 | 93 | * Feature: Allowed strings, numbers and null values as file configuration 94 | * Fix: Added examples to the readme 95 | 96 | # 2.0.0 97 | 98 | * Breaking change: Removed gridfs-stream dependency 99 | * Breaking change: Removed all old file configuration options 100 | * Breaking change: Removed logging functions 101 | * Breaking change: The grid property in the file object was removed and its properties merged directly with the file object 102 | * Feature: Simplified api by adding a new option `file` to control file configuration 103 | * Feature: Added delayed file storage after successful connection instead of failing with an error 104 | 105 | # 1.3.0 106 | 107 | * Fix: Renamed 'error' event to 'streamError' to prevent a bug where the the user does not set any listener for that event and emitting it causes the program to crash. 108 | 109 | 110 | # 1.2.2 111 | 112 | * Feature: Added 'dbError' event 113 | * Fix: Call log function in 'error' event 114 | 115 | # 1.2.1 116 | 117 | * Feature: Added 'error' event 118 | 119 | # 1.2.0 120 | 121 | * Feature: Added generator function support 122 | * Feature: Allow to use promises in configuration options instead of callbacks 123 | 124 | # 1.1.1 125 | 126 | * Fix: Fixed UnhandledPromiseRejection error 127 | 128 | 129 | # 1.1.0 130 | 131 | * Feature: Added support for connection promises 132 | * Feature: Added file size information 133 | * Feature: Allow the api to be called with the `new` operator 134 | * Feature: Added Typescript support 135 | 136 | # 1.0.3 137 | 138 | * Fix: Fixed code coverage 139 | 140 | # 1.0.2 141 | 142 | * Feature: Changed log option to accept a function 143 | 144 | # 1.0.1 145 | 146 | * Fix: Added validation for options 147 | 148 | # 1.0.0 149 | 150 | * Initial stable release 151 | 152 | # 0.0.5 153 | 154 | * Feature: Added support for changing the default collection with the root option 155 | 156 | # 0.0.4 157 | 158 | * Feature: Added support for changing the chunk size 159 | 160 | # 0.0.3 161 | 162 | * First release 163 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at devconcept@outlook.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Do you like this library and want to get involved? We would love for you to contribute and help make it even better. As a contributor, here are the guidelines we would like you to follow. 4 | 5 | ## Code of Conduct 6 | 7 | This library is open and inclusive. Please read and follow our [Code of Conduct][coc]. 8 | 9 | ## Questions 10 | 11 | First make sure your question is entirely related to this library and not to MongoDb or Multer. You can find answers for those in StackOverflow and GitHub. 12 | 13 | Search the issues, even the closed ones using keywords for your problem. There is a possibility that someone had the same problem before and there is already a fix available. Also try StackOverflow, you will find thousands of developers willing to help there and the solutions might help other people with the same problem as you. 14 | 15 | [coc]: https://github.com/devconcept/ng-shopping-cart/blob/master/CODE_OF_CONDUCT.md 16 | 17 | ## Bugs and features 18 | 19 | If you found a bug you are welcome to report it submitting an issue. You can also open a pull request if you are confident you can fix it but make sure to open the issue first and discuss the problem. The same applies for new features big or small. This helps coordinate our efforts and prevent duplication of work. 20 | 21 | Try not to pollute your changes trying to address several issues at once. Keep them simple and focused on one single problem. You can open a new PR or issue to solve the others. 22 | 23 | Provide a demo via a GitHub repo or RunKit with the problem you found. We need to confirm it actually exist before proceeding to fix it. Saving us time will serve to fix more bugs and help more people. 24 | 25 | ## Pull request 26 | 27 | Make sure you follow all of the steps mentioned here as they ensure your changes are accepted and merged quickly: 28 | 29 | Click the Fork button to create your personal fork of this repository 30 | 31 | Clone your copy using git 32 | 33 | Run `npm install` to download and install dependencies. 34 | 35 | Create a new branch for your changes 36 | 37 | ```shell 38 | git checkout -b my-fix-branch master 39 | ``` 40 | 41 | Fix the bug or add the feature you want 42 | 43 | Add the required tests to make sure your code works and run the test suite ensuring all tests pass. 44 | 45 | ```shell 46 | ng test 47 | ``` 48 | 49 | If possible make sure code coverage didn't diminished. There are tasks available for that too. Execute `npm run coverage` and serve the `coverage` folder with your preferred web server for static assets. 50 | 51 | Lint the code using the task `npm run lint` and fix the issues that cannot be solved automatically. 52 | 53 | Commit your changes using a descriptive commit message that give us an idea of what you did 54 | 55 | Push your branch to GitHub 56 | 57 | ```shell 58 | git push origin my-fix-branch 59 | ``` 60 | 61 | Open GitHub and send a pull request to the `master` branch. You could also sent it to `develop` if is a work in progress. 62 | 63 | Check the results from the Travis CI tests 64 | 65 | Keep adding commits with more changes if needed. 66 | 67 | After the pull request is merged, you can delete your branch and update your local copy from the upstream repository. 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Rafael Alberto Barata Gámez 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 | SOFTWARE. -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | require: ['ts-node/register/transpile-only'], 3 | files: ['test/**/*.spec.ts'], 4 | cache: true, 5 | concurrency: 10, 6 | verbose: true, 7 | tap: false, 8 | failFast: true, 9 | typescript: { 10 | rewritePaths: { 11 | 'src/': 'lib/', 12 | }, 13 | compile: false, 14 | }, 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": [ 5 | "jsdoc", 6 | "closure" 7 | ] 8 | }, 9 | "source": { 10 | "include": [ 11 | "index.js", 12 | "lib/gridfs.js", 13 | "lib/cache.js", 14 | "lib/utils.js" 15 | ], 16 | "includePattern": ".+\\.js(doc|x)?$", 17 | "excludePattern": "(^|\\/|\\\\)_" 18 | }, 19 | "plugins": [ 20 | "plugins/markdown" 21 | ], 22 | "templates": { 23 | "cleverLinks": true, 24 | "monospaceLinks": true 25 | }, 26 | "opts": { 27 | "destination": "./docs/" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/cache.d.ts: -------------------------------------------------------------------------------- 1 | import { Db, MongoClient } from 'mongodb'; 2 | import { CacheIndex, CacheValue } from './types'; 3 | /** 4 | * Plugin cached connection handling class. 5 | * @version 3.1.0 6 | */ 7 | export declare class Cache { 8 | private store; 9 | private readonly emitter; 10 | constructor(); 11 | /** 12 | * Handles creating a new connection from an url and caching if necessary 13 | * @param {object} options - Options to initialize the cache 14 | * @param {string} options.url - The url to cache 15 | * @param {string} options.cacheName - The name of the cache to use 16 | * @param {any} options.init - The connection options provided 17 | **/ 18 | initialize(options: any): CacheIndex; 19 | /** 20 | * Search the cache for a space stored under an equivalent url. 21 | * 22 | * Just swapping parameters can cause two url to be deemed different when in fact they are not. 23 | * This method finds an url in the cache where another url could be stored even when they are not strictly equal 24 | * @param cacheName The name of the cache to search 25 | * @param url The mongodb url to compare 26 | * @return The similar url already in the cache 27 | */ 28 | findUri(cacheName: string, url: string): string; 29 | /** 30 | * Returns true if the cache has an entry matching the given index 31 | * @param cacheIndex The index to look for 32 | * @return Returns if the cache was found 33 | */ 34 | has(cacheIndex: CacheIndex): boolean; 35 | /** 36 | * Returns the contents of the cache in a given index 37 | * @param cacheIndex {object} The index to look for 38 | * @return {object} The cache contents or null if was not found 39 | */ 40 | get(cacheIndex: CacheIndex): CacheValue; 41 | /** 42 | * Sets the contents of the cache in a given index 43 | * @param cacheIndex The index to look for 44 | * @param value The value to set 45 | */ 46 | set(cacheIndex: CacheIndex, value: CacheValue): void; 47 | /** 48 | * Returns true if a given cache is resolving its associated connection 49 | * @param cacheIndex {object} The index to look for 50 | * @return Return true if the connection is not found yet 51 | */ 52 | isPending(cacheIndex: CacheIndex): boolean; 53 | /** 54 | * Return true if a given cache started resolving a connection for itself 55 | * @param cacheIndex {object} The index to look for 56 | * @return Return true if no instances have started creating a connection for this cache 57 | */ 58 | isOpening(cacheIndex: CacheIndex): boolean; 59 | /** 60 | * Sets the database and client for a given cache and resolves all instances waiting for it 61 | * @param cacheIndex {object} The index to look for 62 | * @param db The database used to store files 63 | * @param [client] The client used to open the connection or null if none is provided 64 | */ 65 | resolve(cacheIndex: CacheIndex, db: Db, client?: MongoClient): void; 66 | /** 67 | * Rejects all instances waiting for this connections 68 | * @param cacheIndex The index to look for 69 | * @param err The error thrown by the driver 70 | */ 71 | reject(cacheIndex: CacheIndex, error: any): void; 72 | /** 73 | * Allows waiting for a connection associated to a given cache 74 | * @param cacheIndex The index to look for 75 | * @return A promise that will resolve when the connection for this cache is created 76 | */ 77 | waitFor(cacheIndex: CacheIndex): Promise; 78 | /** 79 | * Gives the number of connections created by all cache instances 80 | * @return {number} The number of created connections 81 | */ 82 | connections(): number; 83 | /** 84 | * Removes a cache entry. 85 | * 86 | * > If the cache hasn't resolved yet it will be rejected. 87 | * @param cacheIndex The index to look for 88 | */ 89 | remove(cacheIndex: CacheIndex): void; 90 | /** 91 | * Removes all entries in the cache and all listeners 92 | */ 93 | clear(): void; 94 | } 95 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.Cache = void 0; 16 | /** 17 | * Storage cache 18 | * @module multer-gridfs-storage/cache 19 | */ 20 | const node_events_1 = require("node:events"); 21 | const mongodb_uri_1 = __importDefault(require("mongodb-uri")); 22 | const utils_1 = require("./utils"); 23 | /** 24 | * Plugin cached connection handling class. 25 | * @version 3.1.0 26 | */ 27 | class Cache { 28 | constructor() { 29 | this.store = new Map(); 30 | this.emitter = new node_events_1.EventEmitter(); 31 | this.emitter.setMaxListeners(0); 32 | } 33 | /** 34 | * Handles creating a new connection from an url and caching if necessary 35 | * @param {object} options - Options to initialize the cache 36 | * @param {string} options.url - The url to cache 37 | * @param {string} options.cacheName - The name of the cache to use 38 | * @param {any} options.init - The connection options provided 39 | **/ 40 | initialize(options) { 41 | let { url, cacheName: name } = options; 42 | // If the option is a falsey value or empty object use null as initial value 43 | const init = (0, utils_1.compare)(options.init, null) ? null : options.init; 44 | // If a cache under that name does not exist create one 45 | if (!this.store.has(name)) { 46 | this.store.set(name, new Map()); 47 | } 48 | // Check if the url has been used for that cache before 49 | let cached = this.store.get(name).get(url); 50 | if (!this.store.get(name).has(url)) { 51 | // If the url matches any equivalent url used before use that connection instead 52 | const eqUrl = this.findUri(name, url); 53 | if (!eqUrl) { 54 | const store = new Map(); 55 | store.set(0, { 56 | db: null, 57 | client: null, 58 | pending: true, 59 | opening: false, 60 | init, 61 | }); 62 | this.store.get(name).set(url, store); 63 | return { 64 | url, 65 | name, 66 | index: 0, 67 | }; 68 | } 69 | url = eqUrl; 70 | cached = this.store.get(name).get(url); 71 | } 72 | // Compare connection options to create more only if they are semantically different 73 | for (const [index, value] of cached) { 74 | if ((0, utils_1.compare)(value.init, options.init)) { 75 | return { 76 | url, 77 | name, 78 | index, 79 | }; 80 | } 81 | } 82 | cached.set(cached.size, { 83 | db: null, 84 | client: null, 85 | pending: true, 86 | opening: false, 87 | init, 88 | }); 89 | return { 90 | url, 91 | name, 92 | index: cached.size - 1, 93 | }; 94 | } 95 | /** 96 | * Search the cache for a space stored under an equivalent url. 97 | * 98 | * Just swapping parameters can cause two url to be deemed different when in fact they are not. 99 | * This method finds an url in the cache where another url could be stored even when they are not strictly equal 100 | * @param cacheName The name of the cache to search 101 | * @param url The mongodb url to compare 102 | * @return The similar url already in the cache 103 | */ 104 | findUri(cacheName, url) { 105 | for (const [storedUrl] of this.store.get(cacheName)) { 106 | const parsedUri = mongodb_uri_1.default.parse(storedUrl); 107 | const parsedCache = mongodb_uri_1.default.parse(url); 108 | if ((0, utils_1.compareUris)(parsedUri, parsedCache)) { 109 | return storedUrl; 110 | } 111 | } 112 | } 113 | /** 114 | * Returns true if the cache has an entry matching the given index 115 | * @param cacheIndex The index to look for 116 | * @return Returns if the cache was found 117 | */ 118 | has(cacheIndex) { 119 | return Boolean(this.get(cacheIndex)); 120 | } 121 | /** 122 | * Returns the contents of the cache in a given index 123 | * @param cacheIndex {object} The index to look for 124 | * @return {object} The cache contents or null if was not found 125 | */ 126 | get(cacheIndex) { 127 | const { name, url, index } = cacheIndex; 128 | if (!this.store.has(name)) { 129 | return null; 130 | } 131 | if (!this.store.get(name).has(url)) { 132 | return null; 133 | } 134 | if (!this.store.get(name).get(url).has(index)) { 135 | return null; 136 | } 137 | return this.store.get(name).get(url).get(index); 138 | } 139 | /** 140 | * Sets the contents of the cache in a given index 141 | * @param cacheIndex The index to look for 142 | * @param value The value to set 143 | */ 144 | set(cacheIndex, value) { 145 | const { name, url, index } = cacheIndex; 146 | this.store.get(name).get(url).set(index, value); 147 | } 148 | /** 149 | * Returns true if a given cache is resolving its associated connection 150 | * @param cacheIndex {object} The index to look for 151 | * @return Return true if the connection is not found yet 152 | */ 153 | isPending(cacheIndex) { 154 | const cached = this.get(cacheIndex); 155 | return Boolean(cached) && cached.pending; 156 | } 157 | /** 158 | * Return true if a given cache started resolving a connection for itself 159 | * @param cacheIndex {object} The index to look for 160 | * @return Return true if no instances have started creating a connection for this cache 161 | */ 162 | isOpening(cacheIndex) { 163 | const cached = this.get(cacheIndex); 164 | return Boolean(cached === null || cached === void 0 ? void 0 : cached.opening); 165 | } 166 | /** 167 | * Sets the database and client for a given cache and resolves all instances waiting for it 168 | * @param cacheIndex {object} The index to look for 169 | * @param db The database used to store files 170 | * @param [client] The client used to open the connection or null if none is provided 171 | */ 172 | resolve(cacheIndex, db, client) { 173 | const cached = this.get(cacheIndex); 174 | cached.db = db; 175 | cached.client = client; 176 | cached.pending = false; 177 | cached.opening = false; 178 | this.emitter.emit('resolve', cacheIndex); 179 | } 180 | /** 181 | * Rejects all instances waiting for this connections 182 | * @param cacheIndex The index to look for 183 | * @param err The error thrown by the driver 184 | */ 185 | reject(cacheIndex, error) { 186 | const cached = this.get(cacheIndex); 187 | cached.pending = false; 188 | this.emitter.emit('reject', cacheIndex, error); 189 | this.remove(cacheIndex); 190 | } 191 | /** 192 | * Allows waiting for a connection associated to a given cache 193 | * @param cacheIndex The index to look for 194 | * @return A promise that will resolve when the connection for this cache is created 195 | */ 196 | waitFor(cacheIndex) { 197 | return __awaiter(this, void 0, void 0, function* () { 198 | if (!this.isPending(cacheIndex) && !this.isOpening(cacheIndex)) { 199 | return this.get(cacheIndex); 200 | } 201 | return new Promise((resolve, reject) => { 202 | const _resolve = (index) => { 203 | if ((0, utils_1.compare)(cacheIndex, index)) { 204 | this.emitter.removeListener('resolve', _resolve); 205 | this.emitter.removeListener('reject', _reject); 206 | resolve(this.get(cacheIndex)); 207 | } 208 | }; 209 | const _reject = (index, error) => { 210 | if ((0, utils_1.compare)(cacheIndex, index)) { 211 | this.emitter.removeListener('resolve', _resolve); 212 | this.emitter.removeListener('reject', _reject); 213 | reject(error); 214 | } 215 | }; 216 | this.emitter.on('resolve', _resolve); 217 | this.emitter.on('reject', _reject); 218 | }); 219 | }); 220 | } 221 | /** 222 | * Gives the number of connections created by all cache instances 223 | * @return {number} The number of created connections 224 | */ 225 | connections() { 226 | let total = 0; 227 | for (const urlStore of this.store.values()) { 228 | for (const store of urlStore.values()) { 229 | total += store.size; 230 | } 231 | } 232 | return total; 233 | } 234 | /** 235 | * Removes a cache entry. 236 | * 237 | * > If the cache hasn't resolved yet it will be rejected. 238 | * @param cacheIndex The index to look for 239 | */ 240 | remove(cacheIndex) { 241 | if (this.has(cacheIndex)) { 242 | if (this.isPending(cacheIndex)) { 243 | this.emitter.emit('reject', cacheIndex, new Error('The cache entry was deleted')); 244 | } 245 | const { name, url, index } = cacheIndex; 246 | this.store.get(name).get(url).delete(index); 247 | } 248 | } 249 | /** 250 | * Removes all entries in the cache and all listeners 251 | */ 252 | clear() { 253 | this.store = new Map(); 254 | this.emitter.removeAllListeners(); 255 | } 256 | } 257 | exports.Cache = Cache; 258 | //# sourceMappingURL=cache.js.map -------------------------------------------------------------------------------- /lib/cache.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA;;;GAGG;AACH,6CAAyC;AACzC,8DAAmC;AAEnC,mCAA6C;AAG7C;;;GAGG;AACH,MAAa,KAAK;IAIjB;QAHQ,UAAK,GAAsD,IAAI,GAAG,EAAE,CAAC;QAC5D,YAAO,GAAG,IAAI,0BAAY,EAAE,CAAC;QAG7C,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC;IAED;;;;;;QAMI;IACJ,UAAU,CAAC,OAAO;QACjB,IAAI,EAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAC,GAAG,OAAO,CAAC;QACrC,4EAA4E;QAC5E,MAAM,IAAI,GAAG,IAAA,eAAO,EAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;QAE/D,uDAAuD;QACvD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;YAC1B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;SAChC;QAED,uDAAuD;QACvD,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;YACnC,gFAAgF;YAChF,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACtC,IAAI,CAAC,KAAK,EAAE;gBACX,MAAM,KAAK,GAAG,IAAI,GAAG,EAAE,CAAC;gBACxB,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE;oBACZ,EAAE,EAAE,IAAI;oBACR,MAAM,EAAE,IAAI;oBACZ,OAAO,EAAE,IAAI;oBACb,OAAO,EAAE,KAAK;oBACd,IAAI;iBACJ,CAAC,CAAC;gBACH,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBAErC,OAAO;oBACN,GAAG;oBACH,IAAI;oBACJ,KAAK,EAAE,CAAC;iBACR,CAAC;aACF;YAED,GAAG,GAAG,KAAK,CAAC;YACZ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;SACvC;QAED,oFAAoF;QACpF,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE;YACpC,IAAI,IAAA,eAAO,EAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE;gBACtC,OAAO;oBACN,GAAG;oBACH,IAAI;oBACJ,KAAK;iBACL,CAAC;aACF;SACD;QAED,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;YACvB,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,KAAK;YACd,IAAI;SACJ,CAAC,CAAC;QAEH,OAAO;YACN,GAAG;YACH,IAAI;YACJ,KAAK,EAAE,MAAM,CAAC,IAAI,GAAG,CAAC;SACtB,CAAC;IACH,CAAC;IAED;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAiB,EAAE,GAAW;QACrC,KAAK,MAAM,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE;YACpD,MAAM,SAAS,GAAG,qBAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YAC5C,MAAM,WAAW,GAAG,qBAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACxC,IAAI,IAAA,mBAAW,EAAC,SAAS,EAAE,WAAW,CAAC,EAAE;gBACxC,OAAO,SAAS,CAAC;aACjB;SACD;IACF,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAC,UAAsB;QACzB,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;IACtC,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAC,UAAsB;QACzB,MAAM,EAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAC,GAAG,UAAU,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;YAC1B,OAAO,IAAI,CAAC;SACZ;QAED,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;YACnC,OAAO,IAAI,CAAC;SACZ;QAED,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE;YAC9C,OAAO,IAAI,CAAC;SACZ;QAED,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAC,UAAsB,EAAE,KAAiB;QAC5C,MAAM,EAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAC,GAAG,UAAU,CAAC;QACtC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,UAAsB;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACpC,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC;IAC1C,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,UAAsB;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACpC,OAAO,OAAO,CAAC,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IAED;;;;;OAKG;IACH,OAAO,CAAC,UAAsB,EAAE,EAAM,EAAE,MAAoB;QAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACpC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;QACf,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;QACvB,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAC1C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,UAAsB,EAAE,KAAU;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACpC,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACzB,CAAC;IAED;;;;OAIG;IACG,OAAO,CAAC,UAAsB;;YACnC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE;gBAC/D,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;aAC5B;YAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACtC,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,EAAE;oBAC1B,IAAI,IAAA,eAAO,EAAC,UAAU,EAAE,KAAK,CAAC,EAAE;wBAC/B,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;wBACjD,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;wBAC/C,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;qBAC9B;gBACF,CAAC,CAAC;gBAEF,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;oBAChC,IAAI,IAAA,eAAO,EAAC,UAAU,EAAE,KAAK,CAAC,EAAE;wBAC/B,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;wBACjD,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;wBAC/C,MAAM,CAAC,KAAK,CAAC,CAAC;qBACd;gBACF,CAAC,CAAC;gBAEF,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;gBACrC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACpC,CAAC,CAAC,CAAC;QACJ,CAAC;KAAA;IAED;;;OAGG;IACH,WAAW;QACV,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE;YAC3C,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,MAAM,EAAE,EAAE;gBACtC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC;aACpB;SACD;QAED,OAAO,KAAK,CAAC;IACd,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,UAAsB;QAC5B,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE;YACzB,IAAI,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE;gBAC/B,IAAI,CAAC,OAAO,CAAC,IAAI,CAChB,QAAQ,EACR,UAAU,EACV,IAAI,KAAK,CAAC,6BAA6B,CAAC,CACxC,CAAC;aACF;YAED,MAAM,EAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAC,GAAG,UAAU,CAAC;YACtC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;SAC5C;IACF,CAAC;IAED;;OAEG;IACH,KAAK;QACJ,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC;IACnC,CAAC;CACD;AApQD,sBAoQC"} -------------------------------------------------------------------------------- /lib/gridfs.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | import { EventEmitter } from 'node:events'; 4 | import { Db, GridFSBucketWriteStream, MongoClient, MongoClientOptions } from 'mongodb'; 5 | import { StorageEngine } from 'multer'; 6 | import { Cache } from './cache'; 7 | import { GridFile, ConnectionResult, NodeCallback, UrlStorageOptions, DbStorageOptions } from './types'; 8 | /** 9 | * Multer GridFS Storage Engine class definition. 10 | * @extends EventEmitter 11 | * @param {object} configuration 12 | * @param {string} [configuration.url] - The url pointing to a MongoDb database 13 | * @param {object} [configuration.options] - Options to use when connection with an url. 14 | * @param {object} [configuration.connectionOpts] - DEPRECATED: Use options instead. 15 | * @param {boolean | string} [configuration.cache] - Store this connection in the internal cache. 16 | * @param {Db | Promise} [configuration.db] - The MongoDb database instance to use or a promise that resolves with it 17 | * @param {Function} [configuration.file] - A function to control the file naming in the database 18 | * @fires GridFsStorage#connection 19 | * @fires GridFsStorage#connectionFailed 20 | * @fires GridFsStorage#file 21 | * @fires GridFsStorage#streamError 22 | * @fires GridFsStorage#dbError 23 | * @version 0.0.3 24 | */ 25 | export declare class GridFsStorage extends EventEmitter implements StorageEngine { 26 | static cache: Cache; 27 | db: Db; 28 | client: MongoClient; 29 | configuration: DbStorageOptions | UrlStorageOptions; 30 | connected: boolean; 31 | connecting: boolean; 32 | caching: boolean; 33 | error: any; 34 | private _file; 35 | private readonly _options; 36 | private readonly cacheName; 37 | private readonly cacheIndex; 38 | constructor(configuration: UrlStorageOptions | DbStorageOptions); 39 | /** 40 | * Generates 16 bytes long strings in hexadecimal format 41 | */ 42 | static generateBytes(): Promise<{ 43 | filename: string; 44 | }>; 45 | /** 46 | * Merge the properties received in the file function with default values 47 | * @param extra Extra properties like contentType 48 | * @param fileSettings Properties received in the file function 49 | * @return An object with the merged properties wrapped in a promise 50 | */ 51 | private static _mergeProps; 52 | /** 53 | * Handles generator function and promise results 54 | * @param result - Can be a promise or a generator yielded value 55 | * @param isGen - True if is a yielded value 56 | **/ 57 | private static _handleResult; 58 | /** 59 | * Storage interface method to handle incoming files 60 | * @param {Request} request - The request that trigger the upload 61 | * @param {File} file - The uploaded file stream 62 | * @param cb - A standard node callback to signal the end of the upload or an error 63 | **/ 64 | _handleFile(request: any, file: any, cb: NodeCallback): void; 65 | /** 66 | * Storage interface method to delete files in case an error turns the request invalid 67 | * @param request - The request that trigger the upload 68 | * @param {File} file - The uploaded file stream 69 | * @param cb - A standard node callback to signal the end of the upload or an error 70 | **/ 71 | _removeFile(request: any, file: any, cb: NodeCallback): void; 72 | /** 73 | * Waits for the MongoDb connection associated to the storage to succeed or fail 74 | */ 75 | ready(): Promise; 76 | /** 77 | * Pipes the file stream to the MongoDb database. The file requires a property named `file` which is a readable stream 78 | * @param request - The http request where the file was uploaded 79 | * @param {File} file - The file stream to pipe 80 | * @return {Promise} Resolves with the uploaded file 81 | */ 82 | fromFile(request: any, file: any): Promise; 83 | /** 84 | * Pipes the file stream to the MongoDb database. The request and file parameters are optional and used for file generation only 85 | * @param readStream - The http request where the file was uploaded 86 | * @param [request] - The http request where the file was uploaded 87 | * @param {File} [file] - The file stream to pipe 88 | * @return Resolves with the uploaded file 89 | */ 90 | fromStream(readStream: NodeJS.ReadableStream, request: any, file: any): Promise; 91 | protected _openConnection(url: string, options: MongoClientOptions): Promise; 92 | /** 93 | * Create a writable stream with backwards compatibility with GridStore 94 | * @param {object} options - The stream options 95 | */ 96 | protected createStream(options: any): GridFSBucketWriteStream; 97 | private fromMulterStream; 98 | /** 99 | * Determines if a new connection should be created, a explicit connection is provided or a cached instance is required. 100 | */ 101 | private _connect; 102 | /** 103 | * Returns a promise that will resolve to the db and client from the cache or a new connection depending on the provided configuration 104 | */ 105 | private _resolveConnection; 106 | /** 107 | * Handles creating a new connection from an url and storing it in the cache if necessary*}>} 108 | */ 109 | private _createConnection; 110 | /** 111 | * Updates the connection status based on the internal db or client object 112 | **/ 113 | private _updateConnectionStatus; 114 | /** 115 | * Sets the database connection and emit the connection event 116 | * @param db - Database instance or Mongoose instance to set 117 | * @param [client] - Optional Mongo client for MongoDb v3 118 | **/ 119 | private _setDb; 120 | /** 121 | * Removes the database reference and emit the connectionFailed event 122 | * @param err - The error received while trying to connect 123 | **/ 124 | private _fail; 125 | /** 126 | * Tests for generator functions or plain functions and delegates to the appropriate method 127 | * @param request - The request that trigger the upload as received in _handleFile 128 | * @param {File} file - The uploaded file stream as received in _handleFile 129 | * @return A promise with the value generated by the file function 130 | **/ 131 | private _generate; 132 | } 133 | /** 134 | * Event emitted when the MongoDb connection is ready to use 135 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#connection 136 | * @param {{db: Db, client: MongoClient}} result - An object containing the mongodb database and client 137 | * @version 0.0.3 138 | */ 139 | /** 140 | * Event emitted when the MongoDb connection fails to open 141 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#connectionFailed 142 | * @param {Error} err - The error received when attempting to connect 143 | * @version 2.0.0 144 | */ 145 | /** 146 | * Event emitted when a new file is uploaded 147 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#file 148 | * @param {File} file - The uploaded file 149 | * @version 0.0.3 150 | */ 151 | /** 152 | * Event emitted when an error occurs streaming to MongoDb 153 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#streamError 154 | * @param {Error} error - The error thrown by the stream 155 | * @param {Object} conf - The failed file configuration 156 | * @version 1.3 157 | */ 158 | /** 159 | * Event emitted when the internal database connection emits an error 160 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#dbError 161 | * @param {Error} error - The error thrown by the database connection 162 | * @version 1.2.2 163 | **/ 164 | export declare const GridFsStorageCtr: typeof GridFsStorage; 165 | -------------------------------------------------------------------------------- /lib/gridfs.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"gridfs.js","sourceRoot":"","sources":["../src/gridfs.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Module entry point 3 | * @module multer-gridfs-storage 4 | */ 5 | export * from './cache'; 6 | export * from './types'; 7 | export { GridFsStorageCtr as GridFsStorage } from './gridfs'; 8 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Module entry point 4 | * @module multer-gridfs-storage 5 | */ 6 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 7 | if (k2 === undefined) k2 = k; 8 | var desc = Object.getOwnPropertyDescriptor(m, k); 9 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 10 | desc = { enumerable: true, get: function() { return m[k]; } }; 11 | } 12 | Object.defineProperty(o, k2, desc); 13 | }) : (function(o, m, k, k2) { 14 | if (k2 === undefined) k2 = k; 15 | o[k2] = m[k]; 16 | })); 17 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 18 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 19 | }; 20 | Object.defineProperty(exports, "__esModule", { value: true }); 21 | exports.GridFsStorage = void 0; 22 | __exportStar(require("./cache"), exports); 23 | __exportStar(require("./types"), exports); 24 | var gridfs_1 = require("./gridfs"); 25 | Object.defineProperty(exports, "GridFsStorage", { enumerable: true, get: function () { return gridfs_1.GridFsStorageCtr; } }); 26 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;;AAEH,0CAAwB;AACxB,0CAAwB;AAExB,mCAA2D;AAAnD,uGAAA,gBAAgB,OAAiB"} -------------------------------------------------------------------------------- /lib/types/cache-index.d.ts: -------------------------------------------------------------------------------- 1 | export interface CacheIndex { 2 | name: string; 3 | url: string; 4 | index: number; 5 | } 6 | -------------------------------------------------------------------------------- /lib/types/cache-index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=cache-index.js.map -------------------------------------------------------------------------------- /lib/types/cache-index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"cache-index.js","sourceRoot":"","sources":["../../src/types/cache-index.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/types/cache-value.d.ts: -------------------------------------------------------------------------------- 1 | export interface CacheValue { 2 | db: any; 3 | client: any; 4 | pending: boolean; 5 | opening: boolean; 6 | init: any; 7 | } 8 | -------------------------------------------------------------------------------- /lib/types/cache-value.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=cache-value.js.map -------------------------------------------------------------------------------- /lib/types/cache-value.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"cache-value.js","sourceRoot":"","sources":["../../src/types/cache-value.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/types/comparator-result.d.ts: -------------------------------------------------------------------------------- 1 | export declare type ComparatorResult = 'object' | 'array' | 'buffer' | 'identity'; 2 | -------------------------------------------------------------------------------- /lib/types/comparator-result.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=comparator-result.js.map -------------------------------------------------------------------------------- /lib/types/comparator-result.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"comparator-result.js","sourceRoot":"","sources":["../../src/types/comparator-result.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/types/connection-result.d.ts: -------------------------------------------------------------------------------- 1 | import { Db, MongoClient } from 'mongodb'; 2 | export interface ConnectionResult { 3 | db: Db; 4 | client?: MongoClient; 5 | } 6 | -------------------------------------------------------------------------------- /lib/types/connection-result.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=connection-result.js.map -------------------------------------------------------------------------------- /lib/types/connection-result.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"connection-result.js","sourceRoot":"","sources":["../../src/types/connection-result.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/types/db-storage-options.d.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | import { MulterGfsOptions } from './multer-gfs-options'; 3 | import { DbTypes } from './db-types'; 4 | export interface DbStorageOptions extends MulterGfsOptions { 5 | db: T | Promise; 6 | client?: MongoClient; 7 | } 8 | -------------------------------------------------------------------------------- /lib/types/db-storage-options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=db-storage-options.js.map -------------------------------------------------------------------------------- /lib/types/db-storage-options.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"db-storage-options.js","sourceRoot":"","sources":["../../src/types/db-storage-options.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/types/db-types.d.ts: -------------------------------------------------------------------------------- 1 | import { Db } from 'mongodb'; 2 | export interface MongooseConnectionInstance { 3 | db: Db; 4 | } 5 | export interface MongooseInstance { 6 | connection: MongooseConnectionInstance; 7 | } 8 | export declare type DbTypes = MongooseInstance | MongooseConnectionInstance | Db; 9 | -------------------------------------------------------------------------------- /lib/types/db-types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=db-types.js.map -------------------------------------------------------------------------------- /lib/types/db-types.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"db-types.js","sourceRoot":"","sources":["../../src/types/db-types.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/types/grid-file.d.ts: -------------------------------------------------------------------------------- 1 | export interface GridFile { 2 | id: any; 3 | filename: string; 4 | metadata: any; 5 | contentType: string; 6 | chunkSize: number; 7 | bucketName: string; 8 | uploadDate: Date; 9 | md5: string; 10 | size: number; 11 | } 12 | -------------------------------------------------------------------------------- /lib/types/grid-file.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=grid-file.js.map -------------------------------------------------------------------------------- /lib/types/grid-file.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"grid-file.js","sourceRoot":"","sources":["../../src/types/grid-file.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './cache-index'; 2 | export * from './cache-value'; 3 | export * from './comparator-result'; 4 | export * from './connection-result'; 5 | export * from './db-storage-options'; 6 | export * from './db-types'; 7 | export * from './grid-file'; 8 | export * from './multer-gfs-options'; 9 | export * from './node-callback'; 10 | export * from './url-storage-options'; 11 | -------------------------------------------------------------------------------- /lib/types/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 14 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 15 | }; 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | __exportStar(require("./cache-index"), exports); 18 | __exportStar(require("./cache-value"), exports); 19 | __exportStar(require("./comparator-result"), exports); 20 | __exportStar(require("./connection-result"), exports); 21 | __exportStar(require("./db-storage-options"), exports); 22 | __exportStar(require("./db-types"), exports); 23 | __exportStar(require("./grid-file"), exports); 24 | __exportStar(require("./multer-gfs-options"), exports); 25 | __exportStar(require("./node-callback"), exports); 26 | __exportStar(require("./url-storage-options"), exports); 27 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /lib/types/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,gDAA8B;AAC9B,gDAA8B;AAC9B,sDAAoC;AACpC,sDAAoC;AACpC,uDAAqC;AACrC,6CAA2B;AAC3B,8CAA4B;AAC5B,uDAAqC;AACrC,kDAAgC;AAChC,wDAAsC"} -------------------------------------------------------------------------------- /lib/types/multer-gfs-options.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | export interface MulterGfsOptions { 3 | file?: (request: Request, file: any) => any; 4 | } 5 | -------------------------------------------------------------------------------- /lib/types/multer-gfs-options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=multer-gfs-options.js.map -------------------------------------------------------------------------------- /lib/types/multer-gfs-options.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"multer-gfs-options.js","sourceRoot":"","sources":["../../src/types/multer-gfs-options.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/types/node-callback.d.ts: -------------------------------------------------------------------------------- 1 | export interface NodeCallback { 2 | (error: E, result?: undefined | undefined): void; 3 | (error: undefined | undefined, result: T): void; 4 | } 5 | -------------------------------------------------------------------------------- /lib/types/node-callback.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=node-callback.js.map -------------------------------------------------------------------------------- /lib/types/node-callback.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"node-callback.js","sourceRoot":"","sources":["../../src/types/node-callback.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/types/url-storage-options.d.ts: -------------------------------------------------------------------------------- 1 | import { MulterGfsOptions } from './multer-gfs-options'; 2 | export interface UrlStorageOptions extends MulterGfsOptions { 3 | url: string; 4 | options?: any; 5 | cache?: boolean | string; 6 | } 7 | -------------------------------------------------------------------------------- /lib/types/url-storage-options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | //# sourceMappingURL=url-storage-options.js.map -------------------------------------------------------------------------------- /lib/types/url-storage-options.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"url-storage-options.js","sourceRoot":"","sources":["../../src/types/url-storage-options.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/utils.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions 3 | * @module multer-gridfs-storage/utils 4 | */ 5 | import { Db } from 'mongodb'; 6 | import { ComparatorResult } from './types'; 7 | export declare function shouldListenOnDb(v?: string): boolean; 8 | /** 9 | * Compare two objects by value. 10 | * 11 | * This function is designed taking into account how mongodb connection parsing routines work. 12 | * @param object1 The target object to compare 13 | * @param object2 The second object to compare with the first 14 | * @return Return true if both objects are equal by value 15 | */ 16 | export declare function compare(object1: any, object2: any): boolean; 17 | /** 18 | * Compare arrays by reference unless the values are strings or buffers 19 | * @param array1 The source array to compare 20 | * @param array2 The target array to compare with 21 | * @return Returns true if both arrays are equivalent 22 | */ 23 | export declare function compareArrays(array1: any[], array2: any[]): boolean; 24 | /** 25 | * Indicates how objects should be compared. 26 | * @param object1 The source object to compare 27 | * @param object2 The target object to compare with 28 | * @return Always returns 'identity' unless both objects have the same type and they are plain objects, arrays 29 | * or buffers 30 | */ 31 | export declare function compareBy(object1: any, object2: any): ComparatorResult; 32 | /** 33 | * Return true if the object has at least one property inherited or not 34 | * @param object The object to inspect 35 | * @return If the object has any properties or not 36 | */ 37 | export declare function hasKeys(object: any): boolean; 38 | /** 39 | * Compare two parsed uris checking if they are equivalent 40 | * @param {*} uri1 The source parsed uri 41 | * @param {*} uri2 The target parsed uri to compare 42 | * @return {boolean} Return true if both uris are equivalent 43 | */ 44 | export declare function compareUris(uri1: any, uri2: any): boolean; 45 | /** 46 | * Checks if an object is a mongoose instance, a connection or a mongo Db object 47 | * @param {*} object The object to check 48 | * @return The database object 49 | */ 50 | export declare function getDatabase(object: any): Db; 51 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Utility functions 4 | * @module multer-gridfs-storage/utils 5 | */ 6 | var __importDefault = (this && this.__importDefault) || function (mod) { 7 | return (mod && mod.__esModule) ? mod : { "default": mod }; 8 | }; 9 | Object.defineProperty(exports, "__esModule", { value: true }); 10 | exports.getDatabase = exports.compareUris = exports.hasKeys = exports.compareBy = exports.compareArrays = exports.compare = exports.shouldListenOnDb = void 0; 11 | const lodash_isplainobject_1 = __importDefault(require("lodash.isplainobject")); 12 | const package_json_1 = require("mongodb/package.json"); 13 | function shouldListenOnDb(v = package_json_1.version) { 14 | const [major, minor, patch] = v.split('.').map((vn) => Number(vn)); 15 | if (major === 3) { 16 | if (minor <= 5) { 17 | return true; 18 | } 19 | return minor === 6 && patch < 4; 20 | } 21 | return major < 4; 22 | } 23 | exports.shouldListenOnDb = shouldListenOnDb; 24 | /** 25 | * Compare two objects by value. 26 | * 27 | * This function is designed taking into account how mongodb connection parsing routines work. 28 | * @param object1 The target object to compare 29 | * @param object2 The second object to compare with the first 30 | * @return Return true if both objects are equal by value 31 | */ 32 | function compare(object1, object2) { 33 | let prop; 34 | let comp; 35 | let value1; 36 | let value2; 37 | let keys1 = 0; 38 | let keys2 = 0; 39 | // If objects are equal by identity stop testing 40 | if (object1 === object2) { 41 | return true; 42 | } 43 | // Falsey and plain objects with no properties are equivalent 44 | if (!object1 || !object2) { 45 | if (!object1 && !object2) { 46 | return true; 47 | } 48 | return !(object1 ? hasKeys(object1) : hasKeys(object2)); 49 | } 50 | // Check both own and inherited properties, MongoDb doesn't care where the property was defined 51 | /* eslint-disable-next-line guard-for-in */ 52 | for (prop in object1) { 53 | value1 = object1[prop]; 54 | value2 = object2[prop]; 55 | // If one object has one property not present in the other they are different 56 | if (prop in object2) { 57 | comp = compareBy(value1, value2); 58 | switch (comp) { 59 | case 'object': 60 | // If both values are plain objects recursively compare its properties 61 | if (!compare(value1, value2)) { 62 | return false; 63 | } 64 | break; 65 | case 'array': 66 | // If both values are arrays compare buffers and strings by content and every other value by identity 67 | if (!compareArrays(value1, value2)) { 68 | return false; 69 | } 70 | break; 71 | case 'buffer': 72 | // If both values are buffers compare them by content 73 | if (Buffer.compare(value1, value2) !== 0) { 74 | return false; 75 | } 76 | break; 77 | default: 78 | // All other values are compared by identity 79 | if (value1 !== value2) { 80 | return false; 81 | } 82 | break; 83 | } 84 | keys1++; 85 | } 86 | else { 87 | return false; 88 | } 89 | } 90 | // Count all properties from the target object 91 | /* eslint-disable-next-line guard-for-in */ 92 | for (prop in object2) { 93 | keys2++; 94 | } 95 | // If the target object has more properties than source they are different 96 | return keys1 === keys2; 97 | } 98 | exports.compare = compare; 99 | /** 100 | * Compare arrays by reference unless the values are strings or buffers 101 | * @param array1 The source array to compare 102 | * @param array2 The target array to compare with 103 | * @return Returns true if both arrays are equivalent 104 | */ 105 | function compareArrays(array1, array2) { 106 | let value1; 107 | let value2; 108 | if (array1.length !== array2.length) { 109 | return false; 110 | } 111 | for (const [i, element] of array1.entries()) { 112 | value1 = element; 113 | value2 = array2[i]; 114 | // Types other than string or buffers are compared by reference because MongoDb only accepts those two types 115 | // for configuration inside arrays 116 | if (compareBy(value1, value2) === 'buffer') { 117 | if (Buffer.compare(value1, value2) !== 0) { 118 | return false; 119 | } 120 | } 121 | else if (value1 !== value2) { 122 | return false; 123 | } 124 | } 125 | return true; 126 | } 127 | exports.compareArrays = compareArrays; 128 | /** 129 | * Indicates how objects should be compared. 130 | * @param object1 The source object to compare 131 | * @param object2 The target object to compare with 132 | * @return Always returns 'identity' unless both objects have the same type and they are plain objects, arrays 133 | * or buffers 134 | */ 135 | function compareBy(object1, object2) { 136 | if ((0, lodash_isplainobject_1.default)(object1) && (0, lodash_isplainobject_1.default)(object2)) { 137 | return 'object'; 138 | } 139 | if (Array.isArray(object1) && Array.isArray(object2)) { 140 | return 'array'; 141 | } 142 | if (Buffer.isBuffer(object1) && Buffer.isBuffer(object2)) { 143 | return 'buffer'; 144 | } 145 | // All values are compared by identity unless they are both arrays, buffers or plain objects 146 | return 'identity'; 147 | } 148 | exports.compareBy = compareBy; 149 | /** 150 | * Return true if the object has at least one property inherited or not 151 | * @param object The object to inspect 152 | * @return If the object has any properties or not 153 | */ 154 | function hasKeys(object) { 155 | /* eslint-disable-next-line guard-for-in, no-unreachable-loop */ 156 | for (const prop in object) { 157 | // Stop testing if the object has at least one property 158 | return true; 159 | } 160 | return false; 161 | } 162 | exports.hasKeys = hasKeys; 163 | /** 164 | * Compare two parsed uris checking if they are equivalent 165 | * @param {*} uri1 The source parsed uri 166 | * @param {*} uri2 The target parsed uri to compare 167 | * @return {boolean} Return true if both uris are equivalent 168 | */ 169 | function compareUris(uri1, uri2) { 170 | // Compare properties that are string values 171 | const stringProps = ['scheme', 'username', 'password', 'database']; 172 | const diff = stringProps.find((prop) => uri1[prop] !== uri2[prop]); 173 | if (diff) { 174 | return false; 175 | } 176 | // Compare query parameter values 177 | if (!compare(uri1.options, uri2.options)) { 178 | return false; 179 | } 180 | const hosts1 = uri1.hosts; 181 | const hosts2 = uri2.hosts; 182 | // Check if both uris have the same number of hosts 183 | if (hosts1.length !== hosts2.length) { 184 | return false; 185 | } 186 | // Check if every host in one array is present on the other array no matter where is positioned 187 | for (const hostObject of hosts1) { 188 | if (!hosts2.some((h) => h.host === hostObject.host && h.port === hostObject.port)) { 189 | return false; 190 | } 191 | } 192 | return true; 193 | } 194 | exports.compareUris = compareUris; 195 | /** 196 | * Checks if an object is a mongoose instance, a connection or a mongo Db object 197 | * @param {*} object The object to check 198 | * @return The database object 199 | */ 200 | function getDatabase(object) { 201 | var _a; 202 | // If the object has a db property should be a mongoose connection instance 203 | // Mongo 2 has a db property but its a function. See issue #14 204 | if (object.db && typeof object.db !== 'function') { 205 | return object.db; 206 | } 207 | // If it has a connection property with a db property on it is a mongoose instance 208 | if ((_a = object === null || object === void 0 ? void 0 : object.connection) === null || _a === void 0 ? void 0 : _a.db) { 209 | return object.connection.db; 210 | } 211 | // If none of the above are true it should be a mongo database object 212 | return object; 213 | } 214 | exports.getDatabase = getDatabase; 215 | //# sourceMappingURL=utils.js.map -------------------------------------------------------------------------------- /lib/utils.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multer-gridfs-storage", 3 | "version": "5.0.2", 4 | "description": "Multer storage engine for GridFS", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "compile": "del-cli ./lib/** && tsc --declaration", 8 | "watch": "del-cli ./lib/** && tsc --declaration -w", 9 | "test": "npm run compile && ava", 10 | "lint": "xo --fix", 11 | "coverage": "npm run compile && nyc --reporter=lcov ava --timeout=30s", 12 | "docs": "jsdoc -c ./conf.json --readme ./README.md", 13 | "coveralls": "npm run coverage | coveralls" 14 | }, 15 | "xo": { 16 | "prettier": true, 17 | "rules": { 18 | "@typescript-eslint/restrict-plus-operands": 0 19 | } 20 | }, 21 | "keywords": [ 22 | "multer", 23 | "mongodb", 24 | "storage", 25 | "gridfs" 26 | ], 27 | "author": { 28 | "name": "devconcept", 29 | "email": "devconcept@outlook.com" 30 | }, 31 | "license": "MIT", 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/devconcept/multer-gridfs-storage.git" 35 | }, 36 | "dependencies": { 37 | "@types/node": ">=14", 38 | "@types/pump": "^1.1.0", 39 | "has-own-prop": "^2.0.0", 40 | "is-generator": "^1.0.3", 41 | "is-promise": "^4.0.0", 42 | "lodash.isplainobject": ">=0.8.0", 43 | "mongodb-uri": "^0.9.7", 44 | "pump": "^3.0.0" 45 | }, 46 | "peerDependencies": { 47 | "multer": ">1.4 || ~1.4.4-lts.1", 48 | "mongodb": ">=6" 49 | }, 50 | "devDependencies": { 51 | "@ava/typescript": "^2.0.0", 52 | "@types/express": ">=4", 53 | "@types/express-serve-static-core": ">=4.17.21", 54 | "@types/sinon": "^10.0.2", 55 | "ava": "^3.1.0", 56 | "coveralls": "^3.0.9", 57 | "crypto-random-string": "^3.1.0", 58 | "del-cli": "^4.0.1", 59 | "delay": "^5.0.0", 60 | "eslint": "^8.4.0", 61 | "express": "^4.17.1", 62 | "jsdoc": "^3.6.3", 63 | "md5-file": "^5.0.0", 64 | "mongoose": "^5.8.9", 65 | "nyc": "^15.0.0", 66 | "pify": "^5.0.0", 67 | "sinon": "^14.0.0", 68 | "supertest": "^6.1.3", 69 | "ts-node": "^10.0.0", 70 | "typescript": "^4.2.2", 71 | "xo": "^0.48.0" 72 | }, 73 | "engines": { 74 | "node": ">=14" 75 | }, 76 | "bugs": { 77 | "url": "https://github.com/devconcept/multer-gridfs-storage/issues" 78 | }, 79 | "homepage": "https://github.com/devconcept/multer-gridfs-storage", 80 | "files": [ 81 | "LICENSE", 82 | "CHANGELOG.md", 83 | "README.md", 84 | "lib/" 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Storage cache 3 | * @module multer-gridfs-storage/cache 4 | */ 5 | import {EventEmitter} from 'node:events'; 6 | import mongoUri from 'mongodb-uri'; 7 | import {Db, MongoClient} from 'mongodb'; 8 | import {compare, compareUris} from './utils'; 9 | import {CacheIndex, CacheValue} from './types'; 10 | 11 | /** 12 | * Plugin cached connection handling class. 13 | * @version 3.1.0 14 | */ 15 | export class Cache { 16 | private store: Map>> = new Map(); 17 | private readonly emitter = new EventEmitter(); 18 | 19 | constructor() { 20 | this.emitter.setMaxListeners(0); 21 | } 22 | 23 | /** 24 | * Handles creating a new connection from an url and caching if necessary 25 | * @param {object} options - Options to initialize the cache 26 | * @param {string} options.url - The url to cache 27 | * @param {string} options.cacheName - The name of the cache to use 28 | * @param {any} options.init - The connection options provided 29 | **/ 30 | initialize(options): CacheIndex { 31 | let {url, cacheName: name} = options; 32 | // If the option is a falsey value or empty object use null as initial value 33 | const init = compare(options.init, null) ? null : options.init; 34 | 35 | // If a cache under that name does not exist create one 36 | if (!this.store.has(name)) { 37 | this.store.set(name, new Map()); 38 | } 39 | 40 | // Check if the url has been used for that cache before 41 | let cached = this.store.get(name).get(url); 42 | if (!this.store.get(name).has(url)) { 43 | // If the url matches any equivalent url used before use that connection instead 44 | const eqUrl = this.findUri(name, url); 45 | if (!eqUrl) { 46 | const store = new Map(); 47 | store.set(0, { 48 | db: null, 49 | client: null, 50 | pending: true, 51 | opening: false, 52 | init, 53 | }); 54 | this.store.get(name).set(url, store); 55 | 56 | return { 57 | url, 58 | name, 59 | index: 0, 60 | }; 61 | } 62 | 63 | url = eqUrl; 64 | cached = this.store.get(name).get(url); 65 | } 66 | 67 | // Compare connection options to create more only if they are semantically different 68 | for (const [index, value] of cached) { 69 | if (compare(value.init, options.init)) { 70 | return { 71 | url, 72 | name, 73 | index, 74 | }; 75 | } 76 | } 77 | 78 | cached.set(cached.size, { 79 | db: null, 80 | client: null, 81 | pending: true, 82 | opening: false, 83 | init, 84 | }); 85 | 86 | return { 87 | url, 88 | name, 89 | index: cached.size - 1, 90 | }; 91 | } 92 | 93 | /** 94 | * Search the cache for a space stored under an equivalent url. 95 | * 96 | * Just swapping parameters can cause two url to be deemed different when in fact they are not. 97 | * This method finds an url in the cache where another url could be stored even when they are not strictly equal 98 | * @param cacheName The name of the cache to search 99 | * @param url The mongodb url to compare 100 | * @return The similar url already in the cache 101 | */ 102 | findUri(cacheName: string, url: string): string { 103 | for (const [storedUrl] of this.store.get(cacheName)) { 104 | const parsedUri = mongoUri.parse(storedUrl); 105 | const parsedCache = mongoUri.parse(url); 106 | if (compareUris(parsedUri, parsedCache)) { 107 | return storedUrl; 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * Returns true if the cache has an entry matching the given index 114 | * @param cacheIndex The index to look for 115 | * @return Returns if the cache was found 116 | */ 117 | has(cacheIndex: CacheIndex): boolean { 118 | return Boolean(this.get(cacheIndex)); 119 | } 120 | 121 | /** 122 | * Returns the contents of the cache in a given index 123 | * @param cacheIndex {object} The index to look for 124 | * @return {object} The cache contents or null if was not found 125 | */ 126 | get(cacheIndex: CacheIndex): CacheValue { 127 | const {name, url, index} = cacheIndex; 128 | if (!this.store.has(name)) { 129 | return null; 130 | } 131 | 132 | if (!this.store.get(name).has(url)) { 133 | return null; 134 | } 135 | 136 | if (!this.store.get(name).get(url).has(index)) { 137 | return null; 138 | } 139 | 140 | return this.store.get(name).get(url).get(index); 141 | } 142 | 143 | /** 144 | * Sets the contents of the cache in a given index 145 | * @param cacheIndex The index to look for 146 | * @param value The value to set 147 | */ 148 | set(cacheIndex: CacheIndex, value: CacheValue): void { 149 | const {name, url, index} = cacheIndex; 150 | this.store.get(name).get(url).set(index, value); 151 | } 152 | 153 | /** 154 | * Returns true if a given cache is resolving its associated connection 155 | * @param cacheIndex {object} The index to look for 156 | * @return Return true if the connection is not found yet 157 | */ 158 | isPending(cacheIndex: CacheIndex): boolean { 159 | const cached = this.get(cacheIndex); 160 | return Boolean(cached) && cached.pending; 161 | } 162 | 163 | /** 164 | * Return true if a given cache started resolving a connection for itself 165 | * @param cacheIndex {object} The index to look for 166 | * @return Return true if no instances have started creating a connection for this cache 167 | */ 168 | isOpening(cacheIndex: CacheIndex): boolean { 169 | const cached = this.get(cacheIndex); 170 | return Boolean(cached?.opening); 171 | } 172 | 173 | /** 174 | * Sets the database and client for a given cache and resolves all instances waiting for it 175 | * @param cacheIndex {object} The index to look for 176 | * @param db The database used to store files 177 | * @param [client] The client used to open the connection or null if none is provided 178 | */ 179 | resolve(cacheIndex: CacheIndex, db: Db, client?: MongoClient): void { 180 | const cached = this.get(cacheIndex); 181 | cached.db = db; 182 | cached.client = client; 183 | cached.pending = false; 184 | cached.opening = false; 185 | this.emitter.emit('resolve', cacheIndex); 186 | } 187 | 188 | /** 189 | * Rejects all instances waiting for this connections 190 | * @param cacheIndex The index to look for 191 | * @param err The error thrown by the driver 192 | */ 193 | reject(cacheIndex: CacheIndex, error: any): void { 194 | const cached = this.get(cacheIndex); 195 | cached.pending = false; 196 | this.emitter.emit('reject', cacheIndex, error); 197 | this.remove(cacheIndex); 198 | } 199 | 200 | /** 201 | * Allows waiting for a connection associated to a given cache 202 | * @param cacheIndex The index to look for 203 | * @return A promise that will resolve when the connection for this cache is created 204 | */ 205 | async waitFor(cacheIndex: CacheIndex): Promise { 206 | if (!this.isPending(cacheIndex) && !this.isOpening(cacheIndex)) { 207 | return this.get(cacheIndex); 208 | } 209 | 210 | return new Promise((resolve, reject) => { 211 | const _resolve = (index) => { 212 | if (compare(cacheIndex, index)) { 213 | this.emitter.removeListener('resolve', _resolve); 214 | this.emitter.removeListener('reject', _reject); 215 | resolve(this.get(cacheIndex)); 216 | } 217 | }; 218 | 219 | const _reject = (index, error) => { 220 | if (compare(cacheIndex, index)) { 221 | this.emitter.removeListener('resolve', _resolve); 222 | this.emitter.removeListener('reject', _reject); 223 | reject(error); 224 | } 225 | }; 226 | 227 | this.emitter.on('resolve', _resolve); 228 | this.emitter.on('reject', _reject); 229 | }); 230 | } 231 | 232 | /** 233 | * Gives the number of connections created by all cache instances 234 | * @return {number} The number of created connections 235 | */ 236 | connections(): number { 237 | let total = 0; 238 | for (const urlStore of this.store.values()) { 239 | for (const store of urlStore.values()) { 240 | total += store.size; 241 | } 242 | } 243 | 244 | return total; 245 | } 246 | 247 | /** 248 | * Removes a cache entry. 249 | * 250 | * > If the cache hasn't resolved yet it will be rejected. 251 | * @param cacheIndex The index to look for 252 | */ 253 | remove(cacheIndex: CacheIndex): void { 254 | if (this.has(cacheIndex)) { 255 | if (this.isPending(cacheIndex)) { 256 | this.emitter.emit( 257 | 'reject', 258 | cacheIndex, 259 | new Error('The cache entry was deleted'), 260 | ); 261 | } 262 | 263 | const {name, url, index} = cacheIndex; 264 | this.store.get(name).get(url).delete(index); 265 | } 266 | } 267 | 268 | /** 269 | * Removes all entries in the cache and all listeners 270 | */ 271 | clear(): void { 272 | this.store = new Map(); 273 | this.emitter.removeAllListeners(); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/gridfs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Plugin definition 4 | * @module multer-gridfs-storage/gridfs 5 | * 6 | */ 7 | import crypto from 'node:crypto'; 8 | import {EventEmitter} from 'node:events'; 9 | import { 10 | Db, 11 | GridFSBucket, 12 | GridFSBucketWriteStream, 13 | MongoClient, 14 | MongoClientOptions, 15 | ObjectId, 16 | } from 'mongodb'; 17 | import isPromise from 'is-promise'; 18 | import isGenerator from 'is-generator'; 19 | import pump from 'pump'; 20 | import {StorageEngine} from 'multer'; 21 | import mongoUri from 'mongodb-uri'; 22 | 23 | import {getDatabase, shouldListenOnDb} from './utils'; 24 | import {Cache} from './cache'; 25 | import { 26 | CacheIndex, 27 | GridFile, 28 | ConnectionResult, 29 | NodeCallback, 30 | UrlStorageOptions, 31 | DbStorageOptions, 32 | } from './types'; 33 | 34 | const isGeneratorFn: any = isGenerator.fn; 35 | 36 | /** 37 | * Default file information 38 | * @const defaults 39 | **/ 40 | const defaults: any = { 41 | metadata: null, 42 | chunkSize: 261_120, 43 | bucketName: 'fs', 44 | aliases: null, 45 | }; 46 | 47 | /** 48 | * Multer GridFS Storage Engine class definition. 49 | * @extends EventEmitter 50 | * @param {object} configuration 51 | * @param {string} [configuration.url] - The url pointing to a MongoDb database 52 | * @param {object} [configuration.options] - Options to use when connection with an url. 53 | * @param {object} [configuration.connectionOpts] - DEPRECATED: Use options instead. 54 | * @param {boolean | string} [configuration.cache] - Store this connection in the internal cache. 55 | * @param {Db | Promise} [configuration.db] - The MongoDb database instance to use or a promise that resolves with it 56 | * @param {Function} [configuration.file] - A function to control the file naming in the database 57 | * @fires GridFsStorage#connection 58 | * @fires GridFsStorage#connectionFailed 59 | * @fires GridFsStorage#file 60 | * @fires GridFsStorage#streamError 61 | * @fires GridFsStorage#dbError 62 | * @version 0.0.3 63 | */ 64 | export class GridFsStorage extends EventEmitter implements StorageEngine { 65 | static cache: Cache = new Cache(); 66 | db: Db = null; 67 | client: MongoClient = null; 68 | configuration: DbStorageOptions | UrlStorageOptions; 69 | connected = false; 70 | connecting = false; 71 | caching = false; 72 | error: any = null; 73 | private _file: any; 74 | private readonly _options: any; 75 | private readonly cacheName: string; 76 | private readonly cacheIndex: CacheIndex; 77 | 78 | constructor(configuration: UrlStorageOptions | DbStorageOptions) { 79 | super(); 80 | 81 | if ( 82 | !configuration || 83 | (!(configuration as UrlStorageOptions).url && 84 | !(configuration as DbStorageOptions).db) 85 | ) { 86 | throw new Error( 87 | 'Error creating storage engine. At least one of url or db option must be provided.', 88 | ); 89 | } 90 | 91 | this.setMaxListeners(0); 92 | this.configuration = configuration; 93 | this._file = this.configuration.file; 94 | const {url, cache, options}: UrlStorageOptions = this 95 | .configuration as UrlStorageOptions; 96 | if (url) { 97 | this.caching = Boolean(cache); 98 | this._options = options; 99 | } 100 | 101 | if (this.caching) { 102 | const {cache, url} = configuration as UrlStorageOptions; 103 | const cacheName = typeof cache === 'string' ? cache : 'default'; 104 | this.cacheName = cacheName; 105 | this.cacheIndex = GridFsStorage.cache.initialize({ 106 | url, 107 | cacheName, 108 | init: this._options, 109 | }); 110 | } 111 | 112 | this._connect(); 113 | } 114 | 115 | /** 116 | * Generates 16 bytes long strings in hexadecimal format 117 | */ 118 | static async generateBytes(): Promise<{filename: string}> { 119 | return new Promise((resolve, reject) => { 120 | crypto.randomBytes(16, (error, buffer) => { 121 | if (error) { 122 | reject(error); 123 | return; 124 | } 125 | 126 | resolve({filename: buffer.toString('hex')}); 127 | }); 128 | }); 129 | } 130 | 131 | /** 132 | * Merge the properties received in the file function with default values 133 | * @param extra Extra properties like contentType 134 | * @param fileSettings Properties received in the file function 135 | * @return An object with the merged properties wrapped in a promise 136 | */ 137 | private static async _mergeProps(extra, fileSettings): Promise { 138 | // If the filename is not provided generate one 139 | const previous: any = await (fileSettings.filename 140 | ? {} 141 | : GridFsStorage.generateBytes()); 142 | // If no id is provided generate one 143 | // If an error occurs the emitted file information will contain the id 144 | const hasId = fileSettings.id; 145 | if (!hasId) { 146 | previous.id = new ObjectId(); 147 | } 148 | 149 | return {...previous, ...defaults, ...extra, ...fileSettings}; 150 | } 151 | 152 | /** 153 | * Handles generator function and promise results 154 | * @param result - Can be a promise or a generator yielded value 155 | * @param isGen - True if is a yielded value 156 | **/ 157 | private static async _handleResult( 158 | result: any, 159 | isGen: boolean, 160 | ): Promise { 161 | let value = result; 162 | 163 | if (isGen) { 164 | if (result.done) { 165 | throw new Error('Generator ended unexpectedly'); 166 | } 167 | 168 | value = result.value; 169 | } 170 | 171 | return value; 172 | } 173 | 174 | /** 175 | * Storage interface method to handle incoming files 176 | * @param {Request} request - The request that trigger the upload 177 | * @param {File} file - The uploaded file stream 178 | * @param cb - A standard node callback to signal the end of the upload or an error 179 | **/ 180 | _handleFile(request: any, file, cb: NodeCallback): void { 181 | if (this.connecting) { 182 | this.ready() 183 | /* eslint-disable-next-line promise/prefer-await-to-then */ 184 | .then(async () => this.fromFile(request, file)) 185 | /* eslint-disable-next-line promise/prefer-await-to-then */ 186 | .then((file) => { 187 | cb(null, file); 188 | }) 189 | .catch(cb); 190 | return; 191 | } 192 | 193 | this._updateConnectionStatus(); 194 | if (this.connected) { 195 | this.fromFile(request, file) 196 | /* eslint-disable-next-line promise/prefer-await-to-then */ 197 | .then((file) => { 198 | cb(null, file); 199 | }) 200 | .catch(cb); 201 | return; 202 | } 203 | 204 | cb(new Error('The database connection must be open to store files')); 205 | } 206 | 207 | /** 208 | * Storage interface method to delete files in case an error turns the request invalid 209 | * @param request - The request that trigger the upload 210 | * @param {File} file - The uploaded file stream 211 | * @param cb - A standard node callback to signal the end of the upload or an error 212 | **/ 213 | _removeFile(request: any, file, cb: NodeCallback): void { 214 | const options = {bucketName: file.bucketName}; 215 | const bucket = new GridFSBucket(this.db, options); 216 | bucket.delete(file.id, cb); 217 | } 218 | 219 | /** 220 | * Waits for the MongoDb connection associated to the storage to succeed or fail 221 | */ 222 | async ready(): Promise { 223 | if (this.error) { 224 | throw this.error; 225 | } 226 | 227 | if (this.connected) { 228 | return {db: this.db, client: this.client}; 229 | } 230 | 231 | return new Promise((resolve, reject) => { 232 | const done = (result) => { 233 | this.removeListener('connectionFailed', fail); 234 | resolve(result); 235 | }; 236 | 237 | const fail = (error) => { 238 | this.removeListener('connection', done); 239 | reject(error); 240 | }; 241 | 242 | this.once('connection', done); 243 | this.once('connectionFailed', fail); 244 | }); 245 | } 246 | 247 | /** 248 | * Pipes the file stream to the MongoDb database. The file requires a property named `file` which is a readable stream 249 | * @param request - The http request where the file was uploaded 250 | * @param {File} file - The file stream to pipe 251 | * @return {Promise} Resolves with the uploaded file 252 | */ 253 | async fromFile(request: any, file): Promise { 254 | return this.fromStream(file.stream, request, file); 255 | } 256 | 257 | /** 258 | * Pipes the file stream to the MongoDb database. The request and file parameters are optional and used for file generation only 259 | * @param readStream - The http request where the file was uploaded 260 | * @param [request] - The http request where the file was uploaded 261 | * @param {File} [file] - The file stream to pipe 262 | * @return Resolves with the uploaded file 263 | */ 264 | async fromStream( 265 | readStream: NodeJS.ReadableStream, 266 | request: any, 267 | file: any, 268 | ): Promise { 269 | return new Promise((resolve, reject) => { 270 | readStream.on('error', reject); 271 | this.fromMulterStream(readStream, request, file) 272 | /* eslint-disable-next-line promise/prefer-await-to-then */ 273 | .then(resolve) 274 | .catch(reject); 275 | }); 276 | } 277 | 278 | protected async _openConnection( 279 | url: string, 280 | options: MongoClientOptions, 281 | ): Promise { 282 | let client = null; 283 | let db; 284 | const connection = await MongoClient.connect(url, options); 285 | if (connection instanceof MongoClient) { 286 | client = connection; 287 | const parsedUri = mongoUri.parse(url); 288 | db = client.db(parsedUri.database); 289 | } else { 290 | db = connection; 291 | } 292 | 293 | return {client, db}; 294 | } 295 | 296 | /** 297 | * Create a writable stream with backwards compatibility with GridStore 298 | * @param {object} options - The stream options 299 | */ 300 | protected createStream(options): GridFSBucketWriteStream { 301 | const settings = { 302 | id: options.id, 303 | chunkSizeBytes: options.chunkSize, 304 | contentType: options.contentType, 305 | metadata: options.metadata, 306 | aliases: options.aliases, 307 | disableMD5: options.disableMD5, 308 | }; 309 | const gfs = new GridFSBucket(this.db, {bucketName: options.bucketName}); 310 | return gfs.openUploadStream(options.filename, settings); 311 | } 312 | 313 | private async fromMulterStream( 314 | readStream: NodeJS.ReadableStream, 315 | request: any, 316 | file: any, 317 | ): Promise { 318 | if (this.connecting) { 319 | await this.ready(); 320 | } 321 | 322 | const fileSettings = await this._generate(request, file); 323 | let settings; 324 | const setType = typeof fileSettings; 325 | const allowedTypes = new Set(['undefined', 'number', 'string', 'object']); 326 | if (!allowedTypes.has(setType)) { 327 | throw new Error('Invalid type for file settings, got ' + setType); 328 | } 329 | 330 | if (fileSettings === null || fileSettings === undefined) { 331 | settings = {}; 332 | } else if (setType === 'string' || setType === 'number') { 333 | settings = { 334 | filename: fileSettings.toString(), 335 | }; 336 | } else { 337 | settings = fileSettings; 338 | } 339 | 340 | const contentType = file ? file.mimetype : undefined; 341 | const streamOptions = await GridFsStorage._mergeProps( 342 | {contentType}, 343 | settings, 344 | ); 345 | return new Promise((resolve, reject) => { 346 | const emitError = (streamError) => { 347 | this.emit('streamError', streamError, streamOptions); 348 | reject(streamError); 349 | }; 350 | 351 | const emitFile = (f) => { 352 | const storedFile: GridFile = { 353 | id: f._id, 354 | filename: f.filename, 355 | metadata: f.metadata || null, 356 | bucketName: streamOptions.bucketName, 357 | chunkSize: f.chunkSize, 358 | size: f.length, 359 | md5: f.md5, 360 | uploadDate: f.uploadDate, 361 | contentType: f.contentType, 362 | }; 363 | this.emit('file', storedFile); 364 | resolve(storedFile); 365 | }; 366 | 367 | const writeStream = this.createStream(streamOptions); 368 | 369 | // Multer already handles the error event on the readable stream(Busboy). 370 | // Invoking the callback with an error will cause file removal and aborting routines to be called twice 371 | writeStream.on('error', emitError); 372 | writeStream.on('finish', emitFile); 373 | pump([readStream, writeStream]); 374 | }); 375 | } 376 | 377 | /** 378 | * Determines if a new connection should be created, a explicit connection is provided or a cached instance is required. 379 | */ 380 | private _connect() { 381 | const {db, client = null} = this.configuration as DbStorageOptions; 382 | 383 | if (db && !isPromise(db) && !isPromise(client)) { 384 | this._setDb(db, client); 385 | return; 386 | } 387 | 388 | this._resolveConnection() 389 | /* eslint-disable-next-line promise/prefer-await-to-then */ 390 | .then(({db, client}) => { 391 | this._setDb(db, client); 392 | }) 393 | .catch((error) => { 394 | this._fail(error); 395 | }); 396 | } 397 | 398 | /** 399 | * Returns a promise that will resolve to the db and client from the cache or a new connection depending on the provided configuration 400 | */ 401 | private async _resolveConnection(): Promise { 402 | this.connecting = true; 403 | const {db, client = null} = this.configuration as DbStorageOptions; 404 | if (db) { 405 | const [_db, _client] = await Promise.all([db, client]); 406 | return {db: _db, client: _client}; 407 | } 408 | 409 | if (!this.caching) { 410 | return this._createConnection(); 411 | } 412 | 413 | const {cache} = GridFsStorage; 414 | if (!cache.isOpening(this.cacheIndex) && cache.isPending(this.cacheIndex)) { 415 | const cached = cache.get(this.cacheIndex); 416 | cached.opening = true; 417 | return this._createConnection(); 418 | } 419 | 420 | return cache.waitFor(this.cacheIndex); 421 | } 422 | 423 | /** 424 | * Handles creating a new connection from an url and storing it in the cache if necessary*}>} 425 | */ 426 | private async _createConnection(): Promise { 427 | const {url} = this.configuration as UrlStorageOptions; 428 | const options: MongoClientOptions = this._options; 429 | 430 | const {cache} = GridFsStorage; 431 | try { 432 | const {db, client} = await this._openConnection(url, options); 433 | if (this.caching) { 434 | cache.resolve(this.cacheIndex, db, client); 435 | } 436 | 437 | return {db, client}; 438 | } catch (error: unknown) { 439 | if (this.cacheIndex) { 440 | cache.reject(this.cacheIndex, error); 441 | } 442 | 443 | throw error; 444 | } 445 | } 446 | 447 | /** 448 | * Updates the connection status based on the internal db or client object 449 | **/ 450 | private _updateConnectionStatus(): void { 451 | if (!this.db) { 452 | this.connected = false; 453 | this.connecting = false; 454 | return; 455 | } 456 | 457 | if (this.client) { 458 | this.connected = this.client.isConnected 459 | ? this.client.isConnected() 460 | : true; 461 | return; 462 | } 463 | 464 | // @ts-expect-error 465 | this.connected = this.db?.topology?.isConnected() || true; 466 | } 467 | 468 | /** 469 | * Sets the database connection and emit the connection event 470 | * @param db - Database instance or Mongoose instance to set 471 | * @param [client] - Optional Mongo client for MongoDb v3 472 | **/ 473 | private _setDb(db: Db, client?: MongoClient): void { 474 | this.connecting = false; 475 | // Check if the object is a mongoose instance, a mongoose Connection or a mongo Db object 476 | this.db = getDatabase(db); 477 | if (client) { 478 | this.client = client; 479 | } 480 | 481 | const errorEvent = (error_) => { 482 | // Needs verification. Sometimes the event fires without an error object 483 | // although the docs specify each of the events has a MongoError argument 484 | this._updateConnectionStatus(); 485 | const error = error_ || new Error('Unknown database error'); 486 | this.emit('dbError', error); 487 | }; 488 | 489 | // This are all the events that emit errors 490 | const errorEventNames = ['error', 'parseError', 'timeout', 'close']; 491 | let eventSource; 492 | if (shouldListenOnDb()) { 493 | eventSource = this.db; 494 | } else if (this.client) { 495 | eventSource = this.client; 496 | } 497 | 498 | if (eventSource) { 499 | for (const evt of errorEventNames) eventSource.on(evt, errorEvent); 500 | } 501 | 502 | this._updateConnectionStatus(); 503 | 504 | // Emit on next tick so user code can set listeners in case the db object is already available 505 | process.nextTick(() => { 506 | this.emit('connection', {db: this.db, client: this.client}); 507 | }); 508 | } 509 | 510 | /** 511 | * Removes the database reference and emit the connectionFailed event 512 | * @param err - The error received while trying to connect 513 | **/ 514 | private _fail(error: any): void { 515 | this.connecting = false; 516 | this.db = null; 517 | this.client = null; 518 | this.error = error; 519 | this._updateConnectionStatus(); 520 | // Fail event is only emitted after either a then promise handler or an I/O phase so is guaranteed to be asynchronous 521 | this.emit('connectionFailed', error); 522 | } 523 | 524 | /** 525 | * Tests for generator functions or plain functions and delegates to the appropriate method 526 | * @param request - The request that trigger the upload as received in _handleFile 527 | * @param {File} file - The uploaded file stream as received in _handleFile 528 | * @return A promise with the value generated by the file function 529 | **/ 530 | private async _generate(request: any, file): Promise { 531 | let result; 532 | let generator; 533 | let isGen = false; 534 | 535 | if (!this._file) { 536 | return {}; 537 | } 538 | 539 | if (isGeneratorFn(this._file)) { 540 | isGen = true; 541 | generator = this._file(request, file); 542 | this._file = generator; 543 | result = generator.next(); 544 | } else if (isGenerator(this._file)) { 545 | isGen = true; 546 | generator = this._file; 547 | result = generator.next([request, file]); 548 | } else { 549 | result = this._file(request, file); 550 | } 551 | 552 | return GridFsStorage._handleResult(result, isGen); 553 | } 554 | } 555 | 556 | /** 557 | * Event emitted when the MongoDb connection is ready to use 558 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#connection 559 | * @param {{db: Db, client: MongoClient}} result - An object containing the mongodb database and client 560 | * @version 0.0.3 561 | */ 562 | 563 | /** 564 | * Event emitted when the MongoDb connection fails to open 565 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#connectionFailed 566 | * @param {Error} err - The error received when attempting to connect 567 | * @version 2.0.0 568 | */ 569 | 570 | /** 571 | * Event emitted when a new file is uploaded 572 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#file 573 | * @param {File} file - The uploaded file 574 | * @version 0.0.3 575 | */ 576 | 577 | /** 578 | * Event emitted when an error occurs streaming to MongoDb 579 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#streamError 580 | * @param {Error} error - The error thrown by the stream 581 | * @param {Object} conf - The failed file configuration 582 | * @version 1.3 583 | */ 584 | 585 | /** 586 | * Event emitted when the internal database connection emits an error 587 | * @event module:multer-gridfs-storage/gridfs~GridFSStorage#dbError 588 | * @param {Error} error - The error thrown by the database connection 589 | * @version 1.2.2 590 | **/ 591 | 592 | export const GridFsStorageCtr = new Proxy(GridFsStorage, { 593 | apply(target, thisArg, argumentsList) { 594 | // @ts-expect-error 595 | return new target(...argumentsList); // eslint-disable-line new-cap 596 | }, 597 | }); 598 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Module entry point 3 | * @module multer-gridfs-storage 4 | */ 5 | 6 | export * from './cache'; 7 | export * from './types'; 8 | 9 | export {GridFsStorageCtr as GridFsStorage} from './gridfs'; 10 | -------------------------------------------------------------------------------- /src/types/cache-index.ts: -------------------------------------------------------------------------------- 1 | export interface CacheIndex { 2 | name: string; 3 | url: string; 4 | index: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/cache-value.ts: -------------------------------------------------------------------------------- 1 | export interface CacheValue { 2 | db: any; 3 | client: any; 4 | pending: boolean; 5 | opening: boolean; 6 | init: any; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/comparator-result.ts: -------------------------------------------------------------------------------- 1 | export type ComparatorResult = 'object' | 'array' | 'buffer' | 'identity'; 2 | -------------------------------------------------------------------------------- /src/types/connection-result.ts: -------------------------------------------------------------------------------- 1 | import {Db, MongoClient} from 'mongodb'; 2 | 3 | export interface ConnectionResult { 4 | db: Db; 5 | client?: MongoClient; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/db-storage-options.ts: -------------------------------------------------------------------------------- 1 | import {MongoClient} from 'mongodb'; 2 | import {MulterGfsOptions} from './multer-gfs-options'; 3 | 4 | import {DbTypes} from './db-types'; 5 | 6 | export interface DbStorageOptions extends MulterGfsOptions { 7 | db: T | Promise; 8 | client?: MongoClient; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/db-types.ts: -------------------------------------------------------------------------------- 1 | import {Db} from 'mongodb'; 2 | 3 | export interface MongooseConnectionInstance { 4 | db: Db; 5 | } 6 | 7 | export interface MongooseInstance { 8 | connection: MongooseConnectionInstance; 9 | } 10 | 11 | export type DbTypes = MongooseInstance | MongooseConnectionInstance | Db; 12 | -------------------------------------------------------------------------------- /src/types/grid-file.ts: -------------------------------------------------------------------------------- 1 | export interface GridFile { 2 | id: any; 3 | filename: string; 4 | metadata: any; 5 | contentType: string; 6 | chunkSize: number; 7 | bucketName: string; 8 | uploadDate: Date; 9 | md5: string; 10 | size: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cache-index'; 2 | export * from './cache-value'; 3 | export * from './comparator-result'; 4 | export * from './connection-result'; 5 | export * from './db-storage-options'; 6 | export * from './db-types'; 7 | export * from './grid-file'; 8 | export * from './multer-gfs-options'; 9 | export * from './node-callback'; 10 | export * from './url-storage-options'; 11 | -------------------------------------------------------------------------------- /src/types/multer-gfs-options.ts: -------------------------------------------------------------------------------- 1 | import {Request} from 'express'; 2 | 3 | export interface MulterGfsOptions { 4 | file?: (request: Request, file: any) => any; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/node-callback.ts: -------------------------------------------------------------------------------- 1 | export interface NodeCallback { 2 | (error: E, result?: undefined | undefined): void; 3 | (error: undefined | undefined, result: T): void; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/url-storage-options.ts: -------------------------------------------------------------------------------- 1 | import {MulterGfsOptions} from './multer-gfs-options'; 2 | 3 | export interface UrlStorageOptions extends MulterGfsOptions { 4 | url: string; 5 | options?: any; 6 | cache?: boolean | string; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions 3 | * @module multer-gridfs-storage/utils 4 | */ 5 | 6 | import isPlainObject from 'lodash.isplainobject'; 7 | import {Db} from 'mongodb'; 8 | import {version} from 'mongodb/package.json'; 9 | 10 | import {ComparatorResult} from './types'; 11 | 12 | export function shouldListenOnDb(v = version): boolean { 13 | const [major, minor, patch] = v.split('.').map((vn) => Number(vn)); 14 | if (major === 3) { 15 | if (minor <= 5) { 16 | return true; 17 | } 18 | 19 | return minor === 6 && patch < 4; 20 | } 21 | 22 | return major < 4; 23 | } 24 | 25 | /** 26 | * Compare two objects by value. 27 | * 28 | * This function is designed taking into account how mongodb connection parsing routines work. 29 | * @param object1 The target object to compare 30 | * @param object2 The second object to compare with the first 31 | * @return Return true if both objects are equal by value 32 | */ 33 | export function compare(object1: any, object2: any): boolean { 34 | let prop; 35 | let comp; 36 | let value1; 37 | let value2; 38 | let keys1 = 0; 39 | let keys2 = 0; 40 | 41 | // If objects are equal by identity stop testing 42 | if (object1 === object2) { 43 | return true; 44 | } 45 | 46 | // Falsey and plain objects with no properties are equivalent 47 | if (!object1 || !object2) { 48 | if (!object1 && !object2) { 49 | return true; 50 | } 51 | 52 | return !(object1 ? hasKeys(object1) : hasKeys(object2)); 53 | } 54 | 55 | // Check both own and inherited properties, MongoDb doesn't care where the property was defined 56 | /* eslint-disable-next-line guard-for-in */ 57 | for (prop in object1) { 58 | value1 = object1[prop]; 59 | value2 = object2[prop]; 60 | // If one object has one property not present in the other they are different 61 | if (prop in object2) { 62 | comp = compareBy(value1, value2); 63 | switch (comp) { 64 | case 'object': 65 | // If both values are plain objects recursively compare its properties 66 | if (!compare(value1, value2)) { 67 | return false; 68 | } 69 | 70 | break; 71 | case 'array': 72 | // If both values are arrays compare buffers and strings by content and every other value by identity 73 | if (!compareArrays(value1, value2)) { 74 | return false; 75 | } 76 | 77 | break; 78 | case 'buffer': 79 | // If both values are buffers compare them by content 80 | if (Buffer.compare(value1, value2) !== 0) { 81 | return false; 82 | } 83 | 84 | break; 85 | default: 86 | // All other values are compared by identity 87 | if (value1 !== value2) { 88 | return false; 89 | } 90 | 91 | break; 92 | } 93 | 94 | keys1++; 95 | } else { 96 | return false; 97 | } 98 | } 99 | 100 | // Count all properties from the target object 101 | /* eslint-disable-next-line guard-for-in */ 102 | for (prop in object2) { 103 | keys2++; 104 | } 105 | 106 | // If the target object has more properties than source they are different 107 | return keys1 === keys2; 108 | } 109 | 110 | /** 111 | * Compare arrays by reference unless the values are strings or buffers 112 | * @param array1 The source array to compare 113 | * @param array2 The target array to compare with 114 | * @return Returns true if both arrays are equivalent 115 | */ 116 | export function compareArrays(array1: any[], array2: any[]): boolean { 117 | let value1; 118 | let value2; 119 | if (array1.length !== array2.length) { 120 | return false; 121 | } 122 | 123 | for (const [i, element] of array1.entries()) { 124 | value1 = element; 125 | value2 = array2[i]; 126 | // Types other than string or buffers are compared by reference because MongoDb only accepts those two types 127 | // for configuration inside arrays 128 | if (compareBy(value1, value2) === 'buffer') { 129 | if (Buffer.compare(value1, value2) !== 0) { 130 | return false; 131 | } 132 | } else if (value1 !== value2) { 133 | return false; 134 | } 135 | } 136 | 137 | return true; 138 | } 139 | 140 | /** 141 | * Indicates how objects should be compared. 142 | * @param object1 The source object to compare 143 | * @param object2 The target object to compare with 144 | * @return Always returns 'identity' unless both objects have the same type and they are plain objects, arrays 145 | * or buffers 146 | */ 147 | export function compareBy(object1: any, object2: any): ComparatorResult { 148 | if (isPlainObject(object1) && isPlainObject(object2)) { 149 | return 'object'; 150 | } 151 | 152 | if (Array.isArray(object1) && Array.isArray(object2)) { 153 | return 'array'; 154 | } 155 | 156 | if (Buffer.isBuffer(object1) && Buffer.isBuffer(object2)) { 157 | return 'buffer'; 158 | } 159 | 160 | // All values are compared by identity unless they are both arrays, buffers or plain objects 161 | return 'identity'; 162 | } 163 | 164 | /** 165 | * Return true if the object has at least one property inherited or not 166 | * @param object The object to inspect 167 | * @return If the object has any properties or not 168 | */ 169 | export function hasKeys(object: any): boolean { 170 | /* eslint-disable-next-line guard-for-in, no-unreachable-loop */ 171 | for (const prop in object) { 172 | // Stop testing if the object has at least one property 173 | return true; 174 | } 175 | 176 | return false; 177 | } 178 | 179 | /** 180 | * Compare two parsed uris checking if they are equivalent 181 | * @param {*} uri1 The source parsed uri 182 | * @param {*} uri2 The target parsed uri to compare 183 | * @return {boolean} Return true if both uris are equivalent 184 | */ 185 | export function compareUris(uri1, uri2): boolean { 186 | // Compare properties that are string values 187 | const stringProps = ['scheme', 'username', 'password', 'database']; 188 | const diff = stringProps.find((prop) => uri1[prop] !== uri2[prop]); 189 | if (diff) { 190 | return false; 191 | } 192 | 193 | // Compare query parameter values 194 | if (!compare(uri1.options, uri2.options)) { 195 | return false; 196 | } 197 | 198 | const hosts1 = uri1.hosts; 199 | const hosts2 = uri2.hosts; 200 | // Check if both uris have the same number of hosts 201 | if (hosts1.length !== hosts2.length) { 202 | return false; 203 | } 204 | 205 | // Check if every host in one array is present on the other array no matter where is positioned 206 | for (const hostObject of hosts1) { 207 | if ( 208 | !hosts2.some( 209 | (h) => h.host === hostObject.host && h.port === hostObject.port, 210 | ) 211 | ) { 212 | return false; 213 | } 214 | } 215 | 216 | return true; 217 | } 218 | 219 | /** 220 | * Checks if an object is a mongoose instance, a connection or a mongo Db object 221 | * @param {*} object The object to check 222 | * @return The database object 223 | */ 224 | export function getDatabase(object: any): Db { 225 | // If the object has a db property should be a mongoose connection instance 226 | // Mongo 2 has a db property but its a function. See issue #14 227 | if (object.db && typeof object.db !== 'function') { 228 | return object.db; 229 | } 230 | 231 | // If it has a connection property with a db property on it is a mongoose instance 232 | if (object?.connection?.db) { 233 | return object.connection.db; 234 | } 235 | 236 | // If none of the above are true it should be a mongo database object 237 | return object; 238 | } 239 | -------------------------------------------------------------------------------- /test/attachments/sample1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devconcept/multer-gridfs-storage/ec1803d8f65de4c45f317a537bf885d62d762581/test/attachments/sample1.jpg -------------------------------------------------------------------------------- /test/attachments/sample2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devconcept/multer-gridfs-storage/ec1803d8f65de4c45f317a537bf885d62d762581/test/attachments/sample2.jpg -------------------------------------------------------------------------------- /test/cache-class.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import {restore, stub} from 'sinon'; 3 | 4 | import {Cache} from '../src'; 5 | import {storageOptions} from './utils/settings'; 6 | import {CacheClassContext} from './types/cache-class-context'; 7 | 8 | const test = anyTest as TestInterface; 9 | const {url} = storageOptions(); 10 | const url2 = 'mongodb://mongoserver.com:27017/testdatabase'; 11 | 12 | test.beforeEach((t) => { 13 | t.context.cache = new Cache(); 14 | }); 15 | 16 | test.afterEach.always(() => { 17 | restore(); 18 | }); 19 | 20 | test('cache initializes with a url and a cache name and no connection options', (t) => { 21 | const {cache} = t.context; 22 | const cacheName = 'b'; 23 | cache.initialize({url, cacheName}); 24 | t.not(cache.store.get(cacheName), undefined); 25 | t.not(cache.store.get(cacheName).get(url), undefined); 26 | t.deepEqual(cache.store.get(cacheName).get(url).get(0), { 27 | db: null, 28 | client: null, 29 | pending: true, 30 | opening: false, 31 | init: null, 32 | }); 33 | t.is(cache.connections(), 1); 34 | }); 35 | 36 | test('cache is reused if the same url and option is used in the same cache', (t) => { 37 | const {cache} = t.context; 38 | const cacheName = 'b'; 39 | cache.initialize({url, cacheName, init: {}}); 40 | cache.initialize({url, cacheName, init: null}); 41 | t.not(cache.store.get(cacheName), undefined); 42 | t.not(cache.store.get(cacheName).get(url), undefined); 43 | t.deepEqual(cache.store.get(cacheName).get(url).get(0), { 44 | db: null, 45 | client: null, 46 | pending: true, 47 | opening: false, 48 | init: null, 49 | }); 50 | t.is(cache.connections(), 1); 51 | }); 52 | 53 | test('new cache is created if the same url and different options are used', (t) => { 54 | const {cache} = t.context; 55 | const cacheName = 'b'; 56 | cache.initialize({url, cacheName, init: {}}); 57 | cache.initialize({url, cacheName, init: {db: 1}}); 58 | t.not(cache.store.get(cacheName), undefined); 59 | t.not(cache.store.get(cacheName).get(url), undefined); 60 | t.deepEqual(cache.store.get(cacheName).get(url).get(0), { 61 | db: null, 62 | client: null, 63 | pending: true, 64 | opening: false, 65 | init: null, 66 | }); 67 | t.deepEqual(cache.store.get(cacheName).get(url).get(1), { 68 | db: null, 69 | client: null, 70 | pending: true, 71 | opening: false, 72 | init: {db: 1}, 73 | }); 74 | t.is(cache.connections(), 2); 75 | }); 76 | 77 | test('cache is reused if the same url is used in the same cache', (t) => { 78 | cachesShouldBeEqual(t, url, url); 79 | }); 80 | 81 | test('new cache is created if a different url is used', (t) => { 82 | cachesShouldBeDifferent(t, url, url2); 83 | }); 84 | 85 | test('cache is reused if a similar url is used', (t) => { 86 | cachesShouldBeEqual( 87 | t, 88 | 'mongodb://host1:1234,host2:5678/database', 89 | 'mongodb://host2:5678,host1:1234/database', 90 | ); 91 | }); 92 | 93 | test('new cache is created if an url with more hosts is used', (t) => { 94 | cachesShouldBeDifferent( 95 | t, 96 | 'mongodb://host1:1234/database', 97 | 'mongodb://host1:1234,host2:5678/database', 98 | ); 99 | }); 100 | 101 | test('new cache is created if urls with different hosts are used', (t) => { 102 | cachesShouldBeDifferent( 103 | t, 104 | 'mongodb://host1:1234/database', 105 | 'mongodb://host2:5678/database', 106 | ); 107 | }); 108 | 109 | test('cache is reused if similar options are used in the url', (t) => { 110 | const firstUrl = 111 | 'mongodb://host1:1234/database?authSource=admin&connectTimeoutMS=300000'; 112 | const secondUrl = 113 | 'mongodb://host1:1234/database?connectTimeoutMS=300000&authSource=admin'; 114 | cachesShouldBeEqual(t, firstUrl, secondUrl); 115 | }); 116 | 117 | test('new cache is created if urls with different options are used', (t) => { 118 | const firstUrl = 'mongodb://host1:1234/database?authSource=admin'; 119 | const secondUrl = 120 | 'mongodb://host1:1234/database?connectTimeoutMS=300000&authSource=admin'; 121 | cachesShouldBeDifferent(t, firstUrl, secondUrl); 122 | }); 123 | 124 | function cachesShouldBeDifferent(t, firstUrl, secondUrl) { 125 | const {cache} = t.context; 126 | const cacheName = 'a'; 127 | cache.initialize({url: firstUrl, cacheName}); 128 | cache.initialize({url: secondUrl, cacheName}); 129 | t.not(cache.store.get(cacheName), undefined); 130 | t.not(cache.store.get(cacheName).get(firstUrl), undefined); 131 | t.deepEqual(cache.store.get(cacheName).get(firstUrl).get(0), { 132 | db: null, 133 | client: null, 134 | pending: true, 135 | opening: false, 136 | init: null, 137 | }); 138 | t.is(cache.store.get(cacheName).get(firstUrl).get(1), undefined); 139 | t.not(cache.store.get(cacheName).get(secondUrl), undefined); 140 | t.deepEqual(cache.store.get(cacheName).get(secondUrl).get(0), { 141 | db: null, 142 | client: null, 143 | pending: true, 144 | opening: false, 145 | init: null, 146 | }); 147 | t.is(cache.connections(), 2); 148 | } 149 | 150 | function cachesShouldBeEqual(t, firstUrl, secondUrl) { 151 | const {cache} = t.context; 152 | const cacheName = 'a'; 153 | cache.initialize({url: firstUrl, cacheName}); 154 | cache.initialize({url: secondUrl, cacheName}); 155 | t.not(cache.store.get(cacheName), undefined); 156 | t.not(cache.store.get(cacheName).get(firstUrl), undefined); 157 | t.deepEqual(cache.store.get(cacheName).get(firstUrl).get(0), { 158 | db: null, 159 | client: null, 160 | pending: true, 161 | opening: false, 162 | init: null, 163 | }); 164 | t.is(cache.store.get(cacheName).get(firstUrl).get(1), undefined); 165 | if (firstUrl !== secondUrl) { 166 | t.is(cache.store.get(cacheName).get(secondUrl), undefined); 167 | } 168 | 169 | t.is(cache.connections(), 1); 170 | } 171 | 172 | test('returns an existing cache', (t) => { 173 | const {cache} = t.context; 174 | const index = cache.initialize({url, cacheName: 'b'}); 175 | t.true(cache.has(index)); 176 | t.false(cache.has({url, name: 'b', index: 2})); 177 | t.is(cache.connections(), 1); 178 | }); 179 | 180 | test('returns a cache by its index', (t) => { 181 | const {cache} = t.context; 182 | const index = cache.initialize({url, cacheName: 'a'}); 183 | t.deepEqual(cache.get(index), { 184 | db: null, 185 | client: null, 186 | pending: true, 187 | opening: false, 188 | init: null, 189 | }); 190 | t.is(cache.get({url, name: 'a', index: 1}), null); 191 | t.is(cache.get({url, name: 'b', index: 0}), null); 192 | t.is(cache.get({url: url2, name: 'a', index: 0}), null); 193 | t.is(cache.connections(), 1); 194 | }); 195 | 196 | test('sets a cache by its index', (t) => { 197 | const {cache} = t.context; 198 | const index = cache.initialize({url, cacheName: 'b'}); 199 | const data = {}; 200 | t.true(cache.has(index)); 201 | cache.set(index, data); 202 | t.is(cache.get(index), data); 203 | t.is(cache.connections(), 1); 204 | }); 205 | 206 | test('removes a cache by its index', (t) => { 207 | const {cache} = t.context; 208 | const spy = stub(cache.emitter, 'emit').callThrough(); 209 | const index = cache.initialize({url, cacheName: 'b'}); 210 | t.true(cache.has(index)); 211 | cache.remove(index); 212 | t.is(spy.callCount, 1); 213 | const call = spy.getCall(0); 214 | t.is(call.args[0], 'reject'); 215 | t.is(call.args[1], index); 216 | t.true(call.args[2] instanceof Error); 217 | t.false(cache.has(index)); 218 | t.is(cache.connections(), 0); 219 | }); 220 | 221 | test('does not reject the cache if is not pending', (t) => { 222 | const {cache} = t.context; 223 | const spy = stub(cache.emitter, 'emit').callThrough(); 224 | const index = cache.initialize({url, cacheName: 'b'}); 225 | const entry = cache.get(index); 226 | entry.pending = false; 227 | t.true(cache.has(index)); 228 | cache.remove(index); 229 | t.is(spy.callCount, 0); 230 | t.false(cache.has(index)); 231 | t.is(cache.connections(), 0); 232 | }); 233 | 234 | test('does not remove other caches than the specified', (t) => { 235 | const {cache} = t.context; 236 | const index = cache.initialize({url, cacheName: 'a'}); 237 | cache.initialize({url: url2, cacheName: 'a'}); 238 | t.is(cache.connections(), 2); 239 | t.true(cache.has(index)); 240 | cache.remove(index); 241 | t.false(cache.has(index)); 242 | t.is(cache.connections(), 1); 243 | }); 244 | 245 | test('does not remove all caches when there are different options', (t) => { 246 | const {cache} = t.context; 247 | const index = cache.initialize({url, cacheName: 'a'}); 248 | cache.initialize({url: url2, cacheName: 'a', init: {db: 1}}); 249 | t.is(cache.connections(), 2); 250 | t.true(cache.has(index)); 251 | cache.remove(index); 252 | t.false(cache.has(index)); 253 | t.is(cache.connections(), 1); 254 | }); 255 | 256 | test('should not remove any caches when there are no matches', (t) => { 257 | const {cache} = t.context; 258 | const index = {url, name: 'c'}; 259 | cache.initialize({url, cacheName: 'a'}); 260 | cache.initialize({url, cacheName: 'b'}); 261 | t.is(cache.connections(), 2); 262 | t.false(cache.has(index)); 263 | cache.remove(index); 264 | t.is(cache.connections(), 2); 265 | }); 266 | 267 | test('should remove all entries from the cache', (t) => { 268 | const {cache} = t.context; 269 | cache.initialize({url, cacheName: 'a'}); 270 | t.is(cache.connections(), 1); 271 | cache.clear(); 272 | t.is(cache.connections(), 0); 273 | }); 274 | -------------------------------------------------------------------------------- /test/cache-errors.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import {MongoClient, Db} from 'mongodb'; 3 | import {spy, stub, restore} from 'sinon'; 4 | 5 | import {Cache, GridFsStorage} from '../src'; 6 | import {storageOptions} from './utils/settings'; 7 | import {cleanStorage, fakeConnectCb} from './utils/testutils'; 8 | import {CacheErrorsContext} from './types/cache-errors-context'; 9 | 10 | const {url, options} = storageOptions(); 11 | const test = anyTest as TestInterface; 12 | 13 | function createStorage(settings, {t = null, key = ''} = {}) { 14 | const storage = new GridFsStorage({url, options, ...settings}); 15 | if (t && key) { 16 | t.context[key] = storage; 17 | } 18 | 19 | return storage; 20 | } 21 | 22 | test.serial.before((t) => { 23 | t.context.oldCache = GridFsStorage.cache; 24 | const cache = new Cache(); 25 | GridFsStorage.cache = cache; 26 | t.context.cache = cache; 27 | t.context.error = new Error('reason'); 28 | t.context.mongoSpy = stub(MongoClient, 'connect') 29 | .callThrough() 30 | .onSecondCall() 31 | .callsFake(fakeConnectCb(t.context.error)); 32 | createStorage({cache: '1'}, {t, key: 'storage1'}); 33 | createStorage({cache: '2'}, {t, key: 'storage2'}); 34 | createStorage({cache: '1'}, {t, key: 'storage3'}); 35 | createStorage({cache: '2'}, {t, key: 'storage4'}); 36 | }); 37 | 38 | test.serial( 39 | ' rejects only connections associated to the same cache', 40 | async (t) => { 41 | const {storage1, storage2, storage3, storage4, mongoSpy, cache} = t.context; 42 | const conSpy = spy(); 43 | const rejectSpy = spy(); 44 | t.is(mongoSpy.callCount, 2); 45 | 46 | storage2.on('connectionFailed', conSpy); 47 | storage1.on('connectionFailed', rejectSpy); 48 | 49 | await storage1.ready(); 50 | t.true(storage1.db instanceof Db); 51 | t.is(storage2.db, null); 52 | t.true(storage3.db instanceof Db); 53 | t.is(storage4.db, null); 54 | t.is(conSpy.callCount, 1); 55 | t.is(rejectSpy.callCount, 0); 56 | t.is(cache.connections(), 1); 57 | }, 58 | ); 59 | 60 | test.serial.afterEach.always(async (t) => { 61 | const {storage1, storage2, oldCache} = t.context; 62 | GridFsStorage.cache = oldCache; 63 | restore(); 64 | await Promise.all([cleanStorage(storage1), cleanStorage(storage2)]); 65 | }); 66 | -------------------------------------------------------------------------------- /test/cache-handling.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import {MongoClient} from 'mongodb'; 3 | import delay from 'delay'; 4 | import {spy, stub, restore} from 'sinon'; 5 | 6 | import {Cache, GridFsStorage} from '../src'; 7 | import {storageOptions} from './utils/settings'; 8 | import {cleanStorage} from './utils/testutils'; 9 | import {CacheHandlingContext} from './types/cache-handling-context'; 10 | 11 | const test = anyTest as TestInterface; 12 | const {url, options} = storageOptions(); 13 | 14 | test.serial.beforeEach((t) => { 15 | t.context.oldCache = GridFsStorage.cache; 16 | const cache = new Cache(); 17 | GridFsStorage.cache = cache; 18 | t.context.cache = cache; 19 | t.context.mongoSpy = stub(MongoClient, 'connect').callThrough(); 20 | }); 21 | 22 | test.serial.afterEach.always(async (t) => { 23 | const {storage1, storage2, oldCache} = t.context; 24 | GridFsStorage.cache = oldCache; 25 | restore(); 26 | await Promise.all([cleanStorage(storage1), cleanStorage(storage2)]); 27 | }); 28 | 29 | function createStorage(settings, {t = null, key = ''} = {}) { 30 | const storage = new GridFsStorage({url, options, ...settings}); 31 | if (t && key) { 32 | t.context[key] = storage; 33 | } 34 | 35 | return storage; 36 | } 37 | 38 | test.serial( 39 | 'creates one connection when several cached modules are invoked', 40 | async (t) => { 41 | const storage1 = createStorage({cache: true}, {t, key: 'storage1'}); 42 | const storage2 = createStorage({cache: true}); 43 | const {mongoSpy, cache} = t.context; 44 | 45 | const eventSpy = spy(); 46 | storage2.on('connection', eventSpy); 47 | 48 | await storage1.ready(); 49 | await delay(100); 50 | t.is(storage1.db, storage2.db); 51 | t.is(eventSpy.callCount, 1); 52 | const call = eventSpy.getCall(0); 53 | t.is(call.args[0].db, storage1.db); 54 | t.is(mongoSpy.callCount, 1); 55 | t.is(cache.connections(), 1); 56 | }, 57 | ); 58 | 59 | test.serial( 60 | 'creates only one connection when several named cached modules are invoked', 61 | async (t) => { 62 | const storage1 = createStorage({cache: '1'}, {t, key: 'storage1'}); 63 | const storage2 = createStorage({cache: '1'}); 64 | const {mongoSpy, cache} = t.context; 65 | 66 | const eventSpy = spy(); 67 | storage2.on('connection', eventSpy); 68 | 69 | await storage1.ready(); 70 | await delay(100); 71 | t.is(storage1.db, storage2.db); 72 | t.is(eventSpy.callCount, 1); 73 | const call = eventSpy.getCall(0); 74 | t.is(call.args[0].db, storage1.db); 75 | t.is(mongoSpy.callCount, 1); 76 | t.is(cache.connections(), 1); 77 | }, 78 | ); 79 | 80 | test.serial( 81 | 'reuses the connection when a cache with the same name is already created', 82 | async (t) => { 83 | const eventSpy = spy(); 84 | const storage1 = createStorage({cache: true}, {t, key: 'storage1'}); 85 | const {mongoSpy, cache} = t.context; 86 | 87 | await storage1.ready(); 88 | const storage2 = createStorage({cache: true}); 89 | storage2.once('connection', eventSpy); 90 | 91 | await storage2.ready(); 92 | t.is(storage1.db, storage2.db); 93 | t.is(eventSpy.callCount, 1); 94 | const call = eventSpy.getCall(0); 95 | t.is(call.args[0].db, storage1.db); 96 | t.is(mongoSpy.callCount, 1); 97 | t.is(cache.connections(), 1); 98 | }, 99 | ); 100 | 101 | test.serial('creates different connections for different caches', async (t) => { 102 | const {mongoSpy, cache} = t.context; 103 | const eventSpy = spy(); 104 | const eventSpy2 = spy(); 105 | const storage1 = createStorage({cache: '1'}, {t, key: 'storage1'}); 106 | const storage2 = createStorage({cache: '2'}, {t, key: 'storage2'}); 107 | 108 | storage1.once('connection', eventSpy); 109 | storage2.once('connection', eventSpy2); 110 | 111 | await Promise.all([storage1.ready(), storage2.ready()]); 112 | t.not(storage1.db, storage2.db); 113 | t.is(mongoSpy.callCount, 2); 114 | t.is(eventSpy.callCount, 1); 115 | const call = eventSpy.getCall(0); 116 | t.is(call.args[0].db, storage1.db); 117 | t.is(eventSpy2.callCount, 1); 118 | const call2 = eventSpy2.getCall(0); 119 | t.is(call2.args[0].db, storage2.db); 120 | t.is(cache.connections(), 2); 121 | }); 122 | -------------------------------------------------------------------------------- /test/connection-options.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import {GridFsStorage} from '../src'; 3 | import {cleanStorage, mongoVersion} from './utils/testutils'; 4 | import {storageOptions} from './utils/settings'; 5 | import {ConnectionOptionsContext} from './types/connection-options-context'; 6 | 7 | const test = anyTest as TestInterface; 8 | 9 | test.afterEach.always('cleanup', async (t) => { 10 | await cleanStorage(t.context.storage); 11 | }); 12 | 13 | test('is compatible with an options object on url based connections', async (t) => { 14 | const [major] = mongoVersion; 15 | const {url, options} = storageOptions(); 16 | const storage = new GridFsStorage({ 17 | url, 18 | options: {...options, poolSize: 10}, 19 | }); 20 | t.context.storage = storage; 21 | 22 | await storage.ready(); 23 | const value = 24 | major === 3 25 | ? storage.db.serverConfig.s.options.poolSize 26 | : storage.db.serverConfig.s.poolSize; 27 | t.is(value, 10); 28 | }); 29 | -------------------------------------------------------------------------------- /test/connection-ready.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import {MongoClient} from 'mongodb'; 3 | import {spy, restore, stub} from 'sinon'; 4 | 5 | import {GridFsStorage} from '../src'; 6 | import {cleanStorage, fakeConnectCb} from './utils/testutils'; 7 | import {storageOptions} from './utils/settings'; 8 | import {ConnectionReadyContext} from './types/connection-ready-context'; 9 | 10 | const test = anyTest as TestInterface; 11 | 12 | test.afterEach.always('cleanup', async (t) => { 13 | const {storage} = t.context; 14 | restore(); 15 | await cleanStorage(storage); 16 | }); 17 | 18 | function createStorage(t) { 19 | t.context.storage = new GridFsStorage(storageOptions()); 20 | } 21 | 22 | function forceFailure(t) { 23 | t.context.error = new Error('Fake error'); 24 | stub(MongoClient, 'connect').callsFake(fakeConnectCb(t.context.error)); 25 | createStorage(t); 26 | } 27 | 28 | test.serial( 29 | 'returns a promise that rejects when the connection fails', 30 | async (t) => { 31 | forceFailure(t); 32 | const {storage} = t.context; 33 | const resolveSpy = spy(); 34 | const rejectSpy = spy(); 35 | storage.once('connection', resolveSpy); 36 | storage.once('connectionFailed', rejectSpy); 37 | 38 | const result = storage.ready(); 39 | /* eslint-disable-next-line promise/prefer-await-to-then */ 40 | t.is(typeof result.then, 'function'); 41 | const error = await t.throwsAsync(async () => { 42 | await result; 43 | t.is(resolveSpy.callCount, 0); 44 | t.is(rejectSpy, 1); 45 | }); 46 | t.is(error, rejectSpy.getCall(0).args[0]); 47 | t.is(error, t.context.error); 48 | }, 49 | ); 50 | 51 | test.serial.cb( 52 | 'returns a promise that rejects if the module already failed connecting', 53 | (t) => { 54 | forceFailure(t); 55 | const {storage} = t.context; 56 | storage.once('connectionFailed', (evtError) => { 57 | const result = storage.ready(); 58 | /* eslint-disable-next-line promise/prefer-await-to-then */ 59 | t.is(typeof result.then, 'function'); 60 | result.catch((error) => { 61 | t.is(error, evtError); 62 | t.is(error, t.context.error); 63 | t.end(); 64 | }); 65 | }); 66 | }, 67 | ); 68 | 69 | test('returns a promise that resolves when the connection is created', async (t) => { 70 | createStorage(t); 71 | const {storage} = t.context; 72 | const resolveSpy = spy(); 73 | const rejectSpy = spy(); 74 | storage.once('connection', resolveSpy); 75 | storage.once('connectionFailed', rejectSpy); 76 | const result = storage.ready(); 77 | const {db, client} = await result; 78 | /* eslint-disable-next-line promise/prefer-await-to-then */ 79 | t.is(typeof result.then, 'function'); 80 | t.is(resolveSpy.callCount, 1); 81 | t.is(rejectSpy.callCount, 0); 82 | t.is(db, storage.db); 83 | t.is(client, storage.client); 84 | t.not(db, null); 85 | }); 86 | 87 | test.cb( 88 | 'returns a promise that resolves if the connection is already created', 89 | (t) => { 90 | createStorage(t); 91 | const {storage} = t.context; 92 | storage.once('connection', () => { 93 | const result = storage.ready(); 94 | /* eslint-disable-next-line promise/prefer-await-to-then */ 95 | t.is(typeof result.then, 'function'); 96 | 97 | result 98 | /* eslint-disable-next-line promise/prefer-await-to-then */ 99 | .then((result) => { 100 | t.truthy(result); 101 | t.is(result.db, storage.db); 102 | t.is(result.client, storage.client); 103 | t.end(); 104 | }) 105 | .catch(t.end); 106 | }); 107 | }, 108 | ); 109 | -------------------------------------------------------------------------------- /test/default-generator.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import express from 'express'; 3 | import request from 'supertest'; 4 | import multer from 'multer'; 5 | import {ObjectId} from 'mongodb'; 6 | import hasOwn from 'has-own-prop'; 7 | 8 | import {GridFsStorage} from '../src'; 9 | import {files, cleanStorage} from './utils/testutils'; 10 | import {storageOptions} from './utils/settings'; 11 | import {DefaultGeneratorContext} from './types/default-generator-context'; 12 | 13 | const test = anyTest as TestInterface; 14 | 15 | test.before(async (t) => { 16 | const app = express(); 17 | t.context.filePrefix = 'file'; 18 | t.context.metadatas = ['foo', 'bar']; 19 | t.context.ids = [new ObjectId(), new ObjectId()]; 20 | t.context.sizes = [102_400, 204_800]; 21 | t.context.collections = ['plants', 'animals']; 22 | t.context.contentTypes = ['text/plain', 'image/jpeg']; 23 | const storage = new GridFsStorage({ 24 | ...storageOptions(), 25 | *file(request_, file) { 26 | let counter = 0; 27 | t.context.params = [{req: request_, file}]; 28 | for (;;) { 29 | const response = yield { 30 | filename: t.context.filePrefix + (counter + 1).toString(), 31 | metadata: t.context.metadatas[counter], 32 | id: t.context.ids[counter], 33 | chunkSize: t.context.sizes[counter], 34 | bucketName: t.context.collections[counter], 35 | contentType: t.context.contentTypes[counter], 36 | }; 37 | t.context.params.push({req: response[0], file: response[1]}); 38 | counter++; 39 | } 40 | }, 41 | }); 42 | t.context.storage = storage; 43 | 44 | const upload = multer({storage}); 45 | 46 | app.post('/url', upload.array('photos', 2), (request_, response) => { 47 | t.context.req = request_; 48 | t.context.result = { 49 | headers: request_.headers, 50 | files: request_.files, 51 | body: request_.body, 52 | }; 53 | response.end(); 54 | }); 55 | 56 | await storage.ready(); 57 | await request(app) 58 | .post('/url') 59 | .attach('photos', files[0]) 60 | .attach('photos', files[1]); 61 | }); 62 | 63 | test.after.always('cleanup', async (t) => { 64 | await cleanStorage(t.context.storage); 65 | }); 66 | 67 | test('the request contains the two uploaded files', (t) => { 68 | const {result} = t.context; 69 | t.true(Array.isArray(result.files)); 70 | t.is(result.files.length, 2); 71 | }); 72 | 73 | test('files are named with the yielded value', (t) => { 74 | const {result} = t.context; 75 | for (const [idx, f] of result.files.entries()) 76 | t.is(f.filename, t.context.filePrefix + (idx + 1).toString()); 77 | }); 78 | 79 | test('files contain a metadata object with the yielded object', (t) => { 80 | const {result} = t.context; 81 | for (const [idx, f] of result.files.entries()) 82 | t.is(f.metadata, t.context.metadatas[idx]); 83 | }); 84 | 85 | test('should be stored with the yielded chunkSize value', (t) => { 86 | const {result} = t.context; 87 | for (const [idx, f] of result.files.entries()) 88 | t.is(f.chunkSize, t.context.sizes[idx]); 89 | }); 90 | 91 | test('should change the id with the yielded value', (t) => { 92 | const {result} = t.context; 93 | for (const [idx, f] of result.files.entries()) t.is(f.id, t.context.ids[idx]); 94 | }); 95 | 96 | test('files are stored under a collection with the yielded name', async (t) => { 97 | const {storage} = t.context; 98 | const {db} = storage; 99 | const collections = await db 100 | .listCollections({name: {$in: ['plants.files', 'animals.files']}}) 101 | .toArray(); 102 | t.is(collections.length, 2); 103 | }); 104 | 105 | test('files are stored with the yielded content-type value', (t) => { 106 | const {result} = t.context; 107 | for (const [idx, f] of result.files.entries()) 108 | t.is(f.contentType, t.context.contentTypes[idx]); 109 | }); 110 | 111 | test('should the parameters be a request and a file objects', (t) => { 112 | const {req: appRequest, params} = t.context; 113 | for (const p of params) { 114 | const {req, file} = p; 115 | t.is(req, appRequest); 116 | for (const k of ['body', 'query', 'params', 'files']) { 117 | t.true(hasOwn(req, k)); 118 | } 119 | 120 | for (const k of ['fieldname', 'originalname', 'encoding', 'mimetype']) { 121 | t.true(hasOwn(file, k)); 122 | } 123 | } 124 | }); 125 | -------------------------------------------------------------------------------- /test/edge-cases.spec.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import anyTest, {TestInterface} from 'ava'; 3 | import multer from 'multer'; 4 | import request from 'supertest'; 5 | import express from 'express'; 6 | import {MongoClient} from 'mongodb'; 7 | import delay from 'delay'; 8 | import {spy, stub, restore} from 'sinon'; 9 | 10 | import {GridFsStorage} from '../src'; 11 | import {storageOptions} from './utils/settings'; 12 | import {files, cleanStorage, fakeConnectCb} from './utils/testutils'; 13 | import {EdgeCasesContext} from './types/edge-cases-context'; 14 | 15 | const test = anyTest as TestInterface; 16 | 17 | test.serial('connection function fails to connect', async (t) => { 18 | const error = new Error('Failed connection'); 19 | const mongoSpy = stub(MongoClient, 'connect').callsFake(fakeConnectCb(error)); 20 | 21 | const connectionSpy = spy(); 22 | const storage = new GridFsStorage(storageOptions()); 23 | 24 | storage.once('connectionFailed', connectionSpy); 25 | 26 | await delay(50); 27 | t.is(connectionSpy.callCount, 1); 28 | t.is(mongoSpy.callCount, 1); 29 | }); 30 | 31 | test.serial('errors generating random bytes', async (t) => { 32 | const app = express(); 33 | const generatedError = new Error('Random bytes error'); 34 | let error: any = {}; 35 | 36 | const storage = new GridFsStorage(storageOptions()); 37 | const randomBytesSpy = stub(crypto, 'randomBytes').callsFake((size, cb) => { 38 | if (cb) { 39 | cb(generatedError, null); 40 | return; 41 | } 42 | 43 | throw generatedError; 44 | }); 45 | t.context.storage = storage; 46 | const upload = multer({storage}); 47 | 48 | app.post( 49 | '/url', 50 | upload.single('photo'), 51 | (error_, request_, response, next) => { 52 | error = error_; 53 | next(); 54 | }, 55 | ); 56 | 57 | await storage.ready(); 58 | await request(app).post('/url').attach('photo', files[0]); 59 | 60 | t.is(error, generatedError); 61 | t.is(error.message, 'Random bytes error'); 62 | t.is(randomBytesSpy.callCount, 1); 63 | }); 64 | 65 | test.serial.afterEach.always(async (t) => { 66 | restore(); 67 | await cleanStorage(t.context.storage); 68 | }); 69 | -------------------------------------------------------------------------------- /test/error-handling.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import multer from 'multer'; 3 | import request from 'supertest'; 4 | import express from 'express'; 5 | import {MongoClient} from 'mongodb'; 6 | import {spy, restore} from 'sinon'; 7 | 8 | import {GridFsStorage} from '../src'; 9 | import {shouldListenOnDb} from '../src/utils'; 10 | import {storageOptions} from './utils/settings'; 11 | import { 12 | files, 13 | cleanStorage, 14 | getDb, 15 | getClient, 16 | dropDatabase, 17 | ErrorReadableStream, 18 | ErrorWritableStream, 19 | } from './utils/testutils'; 20 | import {ErrorHandlingContext} from './types/error-handling-context'; 21 | 22 | const test = anyTest as TestInterface; 23 | 24 | test.afterEach.always(async (t) => { 25 | restore(); 26 | await cleanStorage(t.context.storage); 27 | return dropDatabase(t.context.url); 28 | }); 29 | 30 | test('invalid configurations', (t) => { 31 | // @ts-expect-error 32 | const errorFn = () => new GridFsStorage({}); 33 | // @ts-expect-error 34 | const errorFn2 = () => new GridFsStorage(); 35 | 36 | t.throws(errorFn, { 37 | message: 38 | 'Error creating storage engine. At least one of url or db option must be provided.', 39 | }); 40 | t.throws(errorFn2, { 41 | message: 42 | 'Error creating storage engine. At least one of url or db option must be provided.', 43 | }); 44 | }); 45 | 46 | test('invalid types as file configurations', async (t) => { 47 | let error: any = {}; 48 | const app = express(); 49 | const storage = new GridFsStorage({ 50 | ...storageOptions(), 51 | file: () => true, 52 | }); 53 | t.context.storage = storage; 54 | const upload = multer({storage}); 55 | app.post( 56 | '/url', 57 | upload.single('photo'), 58 | (error_, request_, response, next) => { 59 | error = error_; 60 | next(); 61 | }, 62 | ); 63 | 64 | await storage.ready(); 65 | await request(app).post('/url').attach('photo', files[0]); 66 | 67 | t.true(error instanceof Error); 68 | t.is(error.message, 'Invalid type for file settings, got boolean'); 69 | }); 70 | 71 | test('fails gracefully if an error is thrown inside the configuration function', async (t) => { 72 | let error: any = {}; 73 | const app = express(); 74 | const storage = new GridFsStorage({ 75 | ...storageOptions(), 76 | file: () => { 77 | throw new Error('Error thrown'); 78 | }, 79 | }); 80 | 81 | const upload = multer({storage}); 82 | 83 | app.post( 84 | '/url', 85 | upload.single('photo'), 86 | (error_, request_, response, next) => { 87 | error = error_; 88 | next(); 89 | }, 90 | ); 91 | 92 | await storage.ready(); 93 | await request(app).post('/url').attach('photo', files[0]); 94 | 95 | t.true(error instanceof Error); 96 | t.is(error.message, 'Error thrown'); 97 | }); 98 | 99 | test('fails gracefully if an error is thrown inside a generator function', async (t) => { 100 | let error: any = {}; 101 | const app = express(); 102 | const storage = new GridFsStorage({ 103 | ...storageOptions(), 104 | /* eslint-disable-next-line require-yield */ 105 | *file() { 106 | throw new Error('File error'); 107 | }, 108 | }); 109 | 110 | const upload = multer({storage}); 111 | 112 | app.post( 113 | '/url', 114 | upload.single('photo'), 115 | (error_, request_, response, next) => { 116 | error = error_; 117 | next(); 118 | }, 119 | ); 120 | 121 | await storage.ready(); 122 | await request(app).post('/url').attach('photo', files[0]); 123 | 124 | t.true(error instanceof Error); 125 | t.is(error.message, 'File error'); 126 | }); 127 | 128 | test('connection promise fails to connect', async (t) => { 129 | const error = new Error('Failed promise'); 130 | const app = express(); 131 | const errorSpy = spy(); 132 | 133 | const promise: Promise = new Promise((resolve, reject) => { 134 | setTimeout(() => { 135 | reject(error); 136 | }, 200); 137 | }); 138 | 139 | const storage = new GridFsStorage({db: promise}); 140 | 141 | const upload = multer({storage}); 142 | 143 | app.post( 144 | '/url', 145 | upload.single('photo'), 146 | (error_, request_, response, _next) => { 147 | response.end(); 148 | }, 149 | ); 150 | 151 | storage.on('connectionFailed', errorSpy); 152 | 153 | await request(app).post('/url').attach('photo', files[0]); 154 | 155 | t.is(errorSpy.callCount, 1); 156 | t.true(errorSpy.calledWith(error)); 157 | t.is(storage.db, null); 158 | }); 159 | 160 | test('connection is not opened', async (t) => { 161 | const {url, options} = storageOptions(); 162 | t.context.url = url; 163 | let error: any = {}; 164 | const app = express(); 165 | const _db = await MongoClient.connect(url, options); 166 | const db = getDb(_db, url); 167 | const client = getClient(_db); 168 | await (client ? client.close(true) : db.close()); 169 | 170 | const storage = new GridFsStorage({db, client}); 171 | const upload = multer({storage}); 172 | 173 | app.post( 174 | '/url', 175 | upload.array('photos', 2), 176 | (error_, request_, response, next) => { 177 | error = error_; 178 | next(); 179 | }, 180 | ); 181 | 182 | await request(app) 183 | .post('/url') 184 | .attach('photos', files[0]) 185 | .attach('photos', files[0]); 186 | 187 | t.true(error instanceof Error); 188 | t.is(error.message, 'The database connection must be open to store files'); 189 | }); 190 | 191 | test('event is emitted when there is an error in the database', async (t) => { 192 | const {url, options} = storageOptions(); 193 | t.context.url = url; 194 | const error = new Error('Database error'); 195 | const errorSpy = spy(); 196 | const client = await MongoClient.connect(url, options); 197 | const db = getDb(client, url); 198 | 199 | const storage = new GridFsStorage({db, client}); 200 | storage.on('dbError', errorSpy); 201 | const evtSource = shouldListenOnDb() ? db : client; 202 | evtSource.emit('error', error); 203 | evtSource.emit('error'); 204 | 205 | t.is(errorSpy.callCount, 2); 206 | t.is(errorSpy.getCall(0).args[0], error); 207 | t.true(errorSpy.getCall(1).args[0] instanceof Error); 208 | }); 209 | 210 | test('error event is emitted when there is an error in the readable stream using fromStream', async (t) => { 211 | const {url, options} = storageOptions(); 212 | t.context.url = url; 213 | const _db = await MongoClient.connect(url, options); 214 | const db = getDb(_db, url); 215 | 216 | const stream = new ErrorReadableStream(); 217 | 218 | const storage = new GridFsStorage({db}); 219 | 220 | await t.throwsAsync(async () => storage.fromStream(stream, {} as any, {})); 221 | }); 222 | 223 | test('error event is emitted when there is an error in the writable stream', async (t) => { 224 | class StorageStub extends GridFsStorage { 225 | createStream(options): any { 226 | return new ErrorWritableStream(); 227 | } 228 | } 229 | 230 | const {url, options} = storageOptions(); 231 | t.context.url = url; 232 | const _db = await MongoClient.connect(url, options); 233 | const db = getDb(_db, url); 234 | let error; 235 | const storage = new StorageStub({db}); 236 | const errorSpy = spy(); 237 | const upload = multer({storage}); 238 | const app = express(); 239 | 240 | storage.on('streamError', errorSpy); 241 | app.post( 242 | '/url', 243 | upload.single('photo'), 244 | (error_, request_, response, next) => { 245 | error = error_; 246 | next(); 247 | }, 248 | ); 249 | 250 | await request(app).post('/url').attach('photo', files[0]); 251 | 252 | t.is(errorSpy.callCount, 1); 253 | }); 254 | -------------------------------------------------------------------------------- /test/file-concurrency.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import express from 'express'; 3 | import request from 'supertest'; 4 | import multer from 'multer'; 5 | import {MongoClient} from 'mongodb'; 6 | import delay from 'delay'; 7 | 8 | import {GridFsStorage} from '../src'; 9 | import {storageOptions} from './utils/settings'; 10 | import {fileMatchMd5Hash} from './utils/macros'; 11 | import { 12 | files, 13 | cleanStorage, 14 | getDb, 15 | getClient, 16 | dropDatabase, 17 | } from './utils/testutils'; 18 | import {FileConcurrencyContext} from './types/file-concurrency-context'; 19 | 20 | const test = anyTest as TestInterface; 21 | 22 | function prepareTest(t, error?) { 23 | const {url, options} = storageOptions(); 24 | t.context.url = url; 25 | const app = express(); 26 | const promised = 27 | error /* eslint-disable-next-line promise/prefer-await-to-then */ 28 | ? delay(500).then(async () => Promise.reject(error)) 29 | : delay(500) 30 | /* eslint-disable-next-line promise/prefer-await-to-then */ 31 | .then(async () => MongoClient.connect(url, options)) 32 | /* eslint-disable-next-line promise/prefer-await-to-then */ 33 | .then((db) => { 34 | t.context.db = getDb(db, url); 35 | t.context.client = getClient(db); 36 | return t.context.db; 37 | }); 38 | 39 | const storage = new GridFsStorage({db: promised}); 40 | const upload = multer({storage}); 41 | t.context.storage = storage; 42 | t.context.upload = upload; 43 | t.context.app = app; 44 | } 45 | 46 | test.afterEach.always('cleanup', async (t) => { 47 | const {db, client, storage, url} = t.context; 48 | await cleanStorage(storage, {db, client}); 49 | return dropDatabase(url); 50 | }); 51 | 52 | test('buffers incoming files while the connection is opening', async (t) => { 53 | let result: any = {}; 54 | prepareTest(t); 55 | const {storage, app, upload} = t.context; 56 | 57 | app.post('/url', upload.array('photos', 2), (request_, response) => { 58 | result = { 59 | headers: request_.headers, 60 | files: request_.files, 61 | body: request_.body, 62 | }; 63 | response.end(); 64 | }); 65 | 66 | await request(app) 67 | .post('/url') 68 | .attach('photos', files[0]) 69 | .attach('photos', files[1]); 70 | 71 | await storage.ready(); 72 | return fileMatchMd5Hash(t, result.files); 73 | }); 74 | 75 | test('rejects incoming files if the connection does not open', async (t) => { 76 | let result: any = {}; 77 | const error = new Error('Failed error'); 78 | prepareTest(t, error); 79 | const {storage, app, upload} = t.context; 80 | 81 | app.post( 82 | '/url', 83 | upload.array('photos', 2), 84 | (error_, request_, response, _next) => { 85 | result = error_; 86 | response.end(); 87 | }, 88 | ); 89 | await request(app) 90 | .post('/url') 91 | .attach('photos', files[0]) 92 | .attach('photos', files[1]); 93 | 94 | await storage.ready().catch(() => ''); 95 | t.is(result, error); 96 | t.is(result.message, 'Failed error'); 97 | }); 98 | -------------------------------------------------------------------------------- /test/file-function.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import express from 'express'; 3 | import request from 'supertest'; 4 | import multer from 'multer'; 5 | import {ObjectId} from 'mongodb'; 6 | 7 | import {GridFsStorage} from '../src'; 8 | import {files, cleanStorage} from './utils/testutils'; 9 | import {storageOptions} from './utils/settings'; 10 | import {FileFunctionContext} from './types/file-function-context'; 11 | 12 | const test = anyTest as TestInterface; 13 | 14 | test.before(async (t) => { 15 | const app = express(); 16 | let counter = 0; 17 | t.context.filenamePrefix = 'file'; 18 | t.context.ids = [new ObjectId(), new ObjectId()]; 19 | t.context.metadatas = ['foo', 'bar']; 20 | t.context.sizes = [102_400, 204_800]; 21 | t.context.bucketNames = ['plants', 'animals']; 22 | t.context.contentTypes = ['text/plain', 'image/jpeg']; 23 | const storage = new GridFsStorage({ 24 | ...storageOptions(), 25 | file: () => { 26 | counter++; 27 | return { 28 | filename: `${t.context.filenamePrefix}${counter}`, 29 | metadata: t.context.metadatas[counter - 1], 30 | id: t.context.ids[counter - 1], 31 | chunkSize: t.context.sizes[counter - 1], 32 | bucketName: t.context.bucketNames[counter - 1], 33 | contentType: t.context.contentTypes[counter - 1], 34 | }; 35 | }, 36 | }); 37 | 38 | t.context.storage = storage; 39 | const upload = multer({storage}); 40 | 41 | app.post('/url', upload.array('photos', 2), (request_, response) => { 42 | t.context.result = { 43 | headers: request_.headers, 44 | files: request_.files, 45 | body: request_.body, 46 | }; 47 | response.end(); 48 | }); 49 | 50 | await storage.ready(); 51 | await request(app) 52 | .post('/url') 53 | .attach('photos', files[0]) 54 | .attach('photos', files[1]); 55 | }); 56 | 57 | test.after.always('cleanup', async (t) => { 58 | await cleanStorage(t.context.storage); 59 | }); 60 | 61 | test('request contains the two uploaded files', (t) => { 62 | const {result} = t.context; 63 | t.truthy(result.files); 64 | t.true(Array.isArray(result.files)); 65 | t.is(result.files.length, 2); 66 | }); 67 | 68 | test('files are named with the provided value', (t) => { 69 | const {result} = t.context; 70 | for (const [idx, f] of result.files.entries()) 71 | t.is(f.filename, t.context.filenamePrefix + (idx + 1)); 72 | }); 73 | 74 | test('files contain a metadata object with the provided object', (t) => { 75 | const {result} = t.context; 76 | for (const [idx, f] of result.files.entries()) 77 | t.is(f.metadata, t.context.metadatas[idx]); 78 | }); 79 | 80 | test('files are stored with the provided chunkSize value', (t) => { 81 | const {result} = t.context; 82 | for (const [idx, f] of result.files.entries()) 83 | t.is(f.chunkSize, t.context.sizes[idx]); 84 | }); 85 | 86 | test('files have the provided id value', (t) => { 87 | const {result} = t.context; 88 | for (const [idx, f] of result.files.entries()) t.is(f.id, t.context.ids[idx]); 89 | }); 90 | 91 | test('files are stored under a collection with the provided name', async (t) => { 92 | const {storage} = t.context; 93 | const {db} = storage; 94 | const collections = await db 95 | .listCollections({name: {$in: ['plants.files', 'animals.files']}}) 96 | .toArray(); 97 | t.is(collections.length, 2); 98 | }); 99 | 100 | test('files are stored with the provided content-type value', (t) => { 101 | const {result} = t.context; 102 | for (const [idx, f] of result.files.entries()) 103 | t.is(f.contentType, t.context.contentTypes[idx]); 104 | }); 105 | -------------------------------------------------------------------------------- /test/generator-promises.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import express from 'express'; 3 | import request from 'supertest'; 4 | import multer from 'multer'; 5 | 6 | import {GridFsStorage} from '../src'; 7 | import {files, cleanStorage} from './utils/testutils'; 8 | import {storageOptions} from './utils/settings'; 9 | import {GeneratorPromisesContext} from './types/generator-promises-context'; 10 | 11 | const test = anyTest as TestInterface; 12 | 13 | async function successfulPromiseSetup(t) { 14 | const app = express(); 15 | t.context.filePrefix = 'file'; 16 | const storage = new GridFsStorage({ 17 | ...storageOptions(), 18 | *file() { 19 | let counter = 0; 20 | for (;;) { 21 | yield Promise.resolve({ 22 | filename: t.context.filePrefix + (counter + 1).toString(), 23 | }); 24 | counter++; 25 | } 26 | }, 27 | }); 28 | t.context.storage = storage; 29 | 30 | const upload = multer({storage}); 31 | 32 | app.post('/url', upload.array('photos', 2), (request_, response) => { 33 | t.context.result = { 34 | headers: request_.headers, 35 | files: request_.files, 36 | body: request_.body, 37 | }; 38 | response.end(); 39 | }); 40 | 41 | await storage.ready(); 42 | await request(app) 43 | .post('/url') 44 | .attach('photos', files[0]) 45 | .attach('photos', files[1]); 46 | } 47 | 48 | test.afterEach.always('cleanup', async (t) => { 49 | await cleanStorage(t.context.storage); 50 | }); 51 | 52 | test('yielding a promise is resolved as file configuration', async (t) => { 53 | await successfulPromiseSetup(t); 54 | const {result} = t.context; 55 | t.true(Array.isArray(result.files)); 56 | t.is(result.files.length, 2); 57 | for (const [idx, f] of result.files.entries()) 58 | t.is(f.filename, t.context.filePrefix + (idx + 1)); 59 | }); 60 | 61 | async function failedPromiseSetup(t) { 62 | const app = express(); 63 | t.context.rejectedError = new Error('reason'); 64 | const storage = new GridFsStorage({ 65 | ...storageOptions(), 66 | *file() { 67 | yield Promise.reject(t.context.rejectedError); 68 | }, 69 | }); 70 | t.context.storage = storage; 71 | const upload = multer({storage}); 72 | 73 | app.post( 74 | '/url', 75 | upload.array('photos', 2), 76 | (error, request_, response, next) => { 77 | t.context.error = error; 78 | next(); 79 | }, 80 | ); 81 | 82 | await storage.ready(); 83 | await request(app).post('/url').attach('photos', files[0]); 84 | } 85 | 86 | test('yielding a promise rejection is handled properly', async (t) => { 87 | await failedPromiseSetup(t); 88 | const {error, storage} = t.context; 89 | const {db} = storage; 90 | t.true(error instanceof Error); 91 | t.is(error, t.context.rejectedError); 92 | const collection = db.collection('fs.files'); 93 | const count = await (collection.estimatedDocumentCount 94 | ? collection.estimatedDocumentCount() 95 | : collection.count()); 96 | t.is(count, 0); 97 | }); 98 | -------------------------------------------------------------------------------- /test/handling-names.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import express from 'express'; 3 | import request from 'supertest'; 4 | import multer from 'multer'; 5 | 6 | import {GridFsStorage} from '../src'; 7 | import {files, cleanStorage} from './utils/testutils'; 8 | import {storageOptions} from './utils/settings'; 9 | import {HandlingNamesContext} from './types/handling-names-context'; 10 | 11 | const test = anyTest as TestInterface; 12 | 13 | test.afterEach.always('cleanup', async (t) => { 14 | await cleanStorage(t.context.storage); 15 | }); 16 | 17 | test('handling empty name values', async (t) => { 18 | const app = express(); 19 | const values = [null, undefined, {}]; 20 | let counter = -1; 21 | let result: any = {}; 22 | 23 | const storage = new GridFsStorage({ 24 | ...storageOptions(), 25 | file: () => { 26 | counter++; 27 | return values[counter]; 28 | }, 29 | }); 30 | t.context.storage = storage; 31 | const upload = multer({storage}); 32 | 33 | app.post('/url', upload.array('photo', 3), (request_, response) => { 34 | result = { 35 | headers: request_.headers, 36 | files: request_.files, 37 | body: request_.body, 38 | }; 39 | response.end(); 40 | }); 41 | 42 | await storage.ready(); 43 | await request(app) 44 | .post('/url') 45 | .attach('photo', files[0]) 46 | .attach('photo', files[0]) 47 | .attach('photo', files[0]); 48 | 49 | for (const file of result.files) t.regex(file.filename, /^[\da-f]{32}$/); 50 | for (const file of result.files) t.is(file.metadata, null); 51 | for (const file of result.files) t.is(file.bucketName, 'fs'); 52 | for (const file of result.files) t.is(file.chunkSize, 261_120); 53 | }); 54 | 55 | test('handling primitive values as names', async (t) => { 56 | const app = express(); 57 | const values = ['name', 10]; 58 | let counter = -1; 59 | let result: any = {}; 60 | 61 | const storage = new GridFsStorage({ 62 | ...storageOptions(), 63 | file: () => { 64 | counter++; 65 | return values[counter]; 66 | }, 67 | }); 68 | t.context.storage = storage; 69 | const upload = multer({storage}); 70 | 71 | app.post('/url', upload.array('photo', 2), (request_, response) => { 72 | result = { 73 | headers: request_.headers, 74 | files: request_.files, 75 | body: request_.body, 76 | }; 77 | response.end(); 78 | }); 79 | 80 | await storage.ready(); 81 | await request(app) 82 | .post('/url') 83 | .attach('photo', files[0]) 84 | .attach('photo', files[0]); 85 | 86 | for (const [idx, f] of result.files.entries()) 87 | t.is(f.filename, values[idx].toString()); 88 | for (const file of result.files) t.is(file.metadata, null); 89 | for (const file of result.files) t.is(file.bucketName, 'fs'); 90 | for (const file of result.files) t.is(file.chunkSize, 261_120); 91 | }); 92 | -------------------------------------------------------------------------------- /test/incomplete-generators.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import express from 'express'; 3 | import request from 'supertest'; 4 | import multer from 'multer'; 5 | 6 | import {GridFsStorage} from '../src'; 7 | import {files, cleanStorage} from './utils/testutils'; 8 | import {storageOptions} from './utils/settings'; 9 | import {IncompleteGeneratorsContext} from './types/incomplete-generators-context'; 10 | 11 | const test = anyTest as TestInterface; 12 | 13 | test.before(async (t) => { 14 | const app = express(); 15 | const storage = new GridFsStorage({ 16 | ...storageOptions(), 17 | *file() { 18 | yield {filename: 'name'}; 19 | }, 20 | }); 21 | t.context.storage = storage; 22 | const upload = multer({storage}); 23 | 24 | app.post( 25 | '/url', 26 | upload.array('photos', 2), 27 | (error, request_, response, _next) => { 28 | t.context.error = error; 29 | response.end(); 30 | }, 31 | ); 32 | 33 | await storage.ready(); 34 | await request(app) 35 | .post('/url') 36 | .attach('photos', files[0]) 37 | .attach('photos', files[1]); 38 | }); 39 | 40 | test.after.always('cleanup', async (t) => { 41 | await cleanStorage(t.context.storage); 42 | }); 43 | 44 | test('is a failed request', (t) => { 45 | const {error} = t.context; 46 | t.true(error instanceof Error); 47 | t.is(error.storageErrors.length, 0); 48 | }); 49 | 50 | test('does not upload any file', async (t) => { 51 | const {storage} = t.context; 52 | const {db} = storage; 53 | const collection = await db.collection('fs.files'); 54 | const count = await (collection.estimatedDocumentCount 55 | ? collection.estimatedDocumentCount() 56 | : collection.count()); 57 | t.is(count, 0); 58 | }); 59 | 60 | test('throws an error about the ended generator', (t) => { 61 | const {error} = t.context; 62 | t.regex(error.message, /Generator ended unexpectedly/); 63 | }); 64 | -------------------------------------------------------------------------------- /test/md5-hash.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import express from 'express'; 3 | import request from 'supertest'; 4 | import multer from 'multer'; 5 | 6 | import {GridFsStorage} from '../src'; 7 | import {files, cleanStorage, mongoVersion} from './utils/testutils'; 8 | import {storageOptions} from './utils/settings'; 9 | import {Md5HashContext} from './types/md5-hash-context'; 10 | 11 | const test = anyTest as TestInterface; 12 | 13 | test.before(async (t) => { 14 | const app = express(); 15 | const storage = new GridFsStorage({ 16 | ...storageOptions(), 17 | file: () => ({disableMD5: true}), 18 | }); 19 | t.context.storage = storage; 20 | const upload = multer({storage}); 21 | 22 | app.post('/url', upload.array('photo', 2), (request_, response) => { 23 | t.context.result = { 24 | headers: request_.headers, 25 | files: request_.files, 26 | body: request_.body, 27 | }; 28 | response.end(); 29 | }); 30 | 31 | await storage.ready(); 32 | await request(app) 33 | .post('/url') 34 | .attach('photo', files[0]) 35 | .attach('photo', files[0]); 36 | }); 37 | 38 | test.after.always('cleanup', async (t) => { 39 | await cleanStorage(t.context.storage); 40 | }); 41 | 42 | test('files don’t have a computed MD5 hash', (t) => { 43 | const [major, minor] = mongoVersion; 44 | if (major < 3 || (major === 3 && minor < 1)) { 45 | t.pass('Md5 hash is not supported in this mongo version'); 46 | return; 47 | } 48 | 49 | const {result} = t.context; 50 | t.is(result.files[0].md5, undefined); 51 | t.is(result.files[1].md5, undefined); 52 | }); 53 | -------------------------------------------------------------------------------- /test/storage-constructor.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import express from 'express'; 3 | import request from 'supertest'; 4 | import multer from 'multer'; 5 | import mongoose from 'mongoose'; 6 | import {MongoClient} from 'mongodb'; 7 | import delay from 'delay'; 8 | import {GridFsStorage} from '../src'; 9 | import { 10 | files, 11 | cleanStorage, 12 | getDb, 13 | getClient, 14 | dropDatabase, 15 | mongoVersion, 16 | } from './utils/testutils'; 17 | import {storageOptions} from './utils/settings'; 18 | import {fileMatchMd5Hash} from './utils/macros'; 19 | import {StorageConstructorContext} from './types/storage-constructor-context'; 20 | 21 | const test = anyTest as TestInterface; 22 | const [major] = mongoVersion; 23 | 24 | function prepareTest(t, options) { 25 | const app = express(); 26 | const storage = new GridFsStorage(options); 27 | const upload = multer({storage}); 28 | t.context.storage = storage; 29 | t.context.upload = upload; 30 | t.context.app = app; 31 | } 32 | 33 | test.afterEach.always('cleanup', async (t) => { 34 | const {storage, url} = t.context; 35 | await cleanStorage(storage); 36 | return dropDatabase(url); 37 | }); 38 | 39 | test('create storage from url parameter', async (t) => { 40 | let result: any = {}; 41 | prepareTest(t, storageOptions()); 42 | const {app, storage, upload} = t.context; 43 | 44 | app.post('/url', upload.array('photos', 2), (request_, response) => { 45 | result = { 46 | headers: request_.headers, 47 | files: request_.files, 48 | body: request_.body, 49 | }; 50 | response.end(); 51 | }); 52 | 53 | await storage.ready(); 54 | await request(app) 55 | .post('/url') 56 | .attach('photos', files[0]) 57 | .attach('photos', files[1]); 58 | 59 | return fileMatchMd5Hash(t, result.files); 60 | }); 61 | 62 | test('create storage from db parameter', async (t) => { 63 | const {url, options} = storageOptions(); 64 | t.context.url = url; 65 | let result: any = {}; 66 | const _db = await MongoClient.connect(url, options); 67 | const db = getDb(_db, url); 68 | prepareTest(t, {db}); 69 | const {app, storage, upload} = t.context; 70 | storage.client = getClient(_db); 71 | 72 | app.post('/url', upload.array('photos', 2), (request_, response) => { 73 | result = { 74 | headers: request_.headers, 75 | files: request_.files, 76 | body: request_.body, 77 | }; 78 | response.end(); 79 | }); 80 | 81 | await storage.ready(); 82 | await request(app) 83 | .post('/url') 84 | .attach('photos', files[0]) 85 | .attach('photos', files[1]); 86 | 87 | return fileMatchMd5Hash(t, result.files); 88 | }); 89 | 90 | test('connects to a mongoose instance', async (t) => { 91 | const {url, options} = storageOptions(); 92 | t.context.url = url; 93 | let result: any = {}; 94 | const promise = mongoose.connect(url, options); 95 | prepareTest(t, {db: promise}); 96 | const {app, storage, upload} = t.context; 97 | 98 | app.post('/url', upload.array('photos', 2), (request_, response) => { 99 | result = { 100 | headers: request_.headers, 101 | files: request_.files, 102 | body: request_.body, 103 | }; 104 | response.end(); 105 | }); 106 | 107 | const {db} = await storage.ready(); 108 | await request(app) 109 | .post('/url') 110 | .attach('photos', files[0]) 111 | .attach('photos', files[1]); 112 | 113 | t.true(db instanceof mongoose.mongo.Db); 114 | await fileMatchMd5Hash(t, result.files); 115 | 116 | storage.client = mongoose.connection; 117 | }); 118 | 119 | test('creates an instance without the new keyword', async (t) => { 120 | let result: any = {}; 121 | const app = express(); 122 | /* eslint-disable new-cap */ 123 | // @ts-expect-error 124 | const storage = GridFsStorage(storageOptions()); 125 | /* eslint-enable new-cap */ 126 | const upload = multer({storage}); 127 | t.context.storage = storage; 128 | 129 | app.post('/url', upload.array('photos', 2), (request_, response) => { 130 | result = { 131 | headers: request_.headers, 132 | files: request_.files, 133 | body: request_.body, 134 | }; 135 | response.end(); 136 | }); 137 | 138 | await storage.ready(); 139 | await request(app) 140 | .post('/url') 141 | .attach('photos', files[0]) 142 | .attach('photos', files[1]); 143 | 144 | return fileMatchMd5Hash(t, result.files); 145 | }); 146 | if (major >= 3) { 147 | test('accept the client as one of the parameters', async (t) => { 148 | const {url, options} = storageOptions(); 149 | t.context.url = url; 150 | let result: any = {}; 151 | const _db = await MongoClient.connect(url, options); 152 | const db = getDb(_db, url); 153 | const client = getClient(_db); 154 | prepareTest(t, {db, client}); 155 | const {app, storage, upload} = t.context; 156 | t.is(storage.client, client); 157 | 158 | app.post('/url', upload.array('photos', 2), (request_, response) => { 159 | result = { 160 | headers: request_.headers, 161 | files: request_.files, 162 | body: request_.body, 163 | }; 164 | response.end(); 165 | }); 166 | 167 | await storage.ready(); 168 | await request(app) 169 | .post('/url') 170 | .attach('photos', files[0]) 171 | .attach('photos', files[1]); 172 | 173 | return fileMatchMd5Hash(t, result.files); 174 | }); 175 | 176 | test('waits for the client if is a promise', async (t) => { 177 | const {url, options} = storageOptions(); 178 | t.context.url = url; 179 | let result: any = {}; 180 | const _db = await MongoClient.connect(url, options); 181 | const db = getDb(_db, url); 182 | /* eslint-disable-next-line promise/prefer-await-to-then */ 183 | const client = delay(100).then(() => getClient(_db)); 184 | prepareTest(t, {db, client}); 185 | const {app, storage, upload} = t.context; 186 | t.is(storage.client, null); 187 | 188 | app.post('/url', upload.array('photos', 2), (request_, response) => { 189 | result = { 190 | headers: request_.headers, 191 | files: request_.files, 192 | body: request_.body, 193 | }; 194 | response.end(); 195 | }); 196 | 197 | await storage.ready(); 198 | await request(app) 199 | .post('/url') 200 | .attach('photos', files[0]) 201 | .attach('photos', files[1]); 202 | 203 | t.not(storage.client, null); 204 | return fileMatchMd5Hash(t, result.files); 205 | }); 206 | } 207 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true 5 | }, 6 | "include": [ 7 | "./test/**/*" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/types/cache-class-context.ts: -------------------------------------------------------------------------------- 1 | export interface CacheClassContext { 2 | cache: any; 3 | } 4 | -------------------------------------------------------------------------------- /test/types/cache-errors-context.ts: -------------------------------------------------------------------------------- 1 | export interface CacheErrorsContext { 2 | oldCache: any; 3 | cache: any; 4 | error: any; 5 | mongoSpy: any; 6 | storage1: any; 7 | storage2: any; 8 | storage3: any; 9 | storage4: any; 10 | } 11 | -------------------------------------------------------------------------------- /test/types/cache-handling-context.ts: -------------------------------------------------------------------------------- 1 | export interface CacheHandlingContext { 2 | oldCache: any; 3 | cache: any; 4 | mongoSpy: any; 5 | storage1: any; 6 | storage2: any; 7 | } 8 | -------------------------------------------------------------------------------- /test/types/connection-options-context.ts: -------------------------------------------------------------------------------- 1 | export interface ConnectionOptionsContext { 2 | storage: any; 3 | } 4 | -------------------------------------------------------------------------------- /test/types/connection-ready-context.ts: -------------------------------------------------------------------------------- 1 | export interface ConnectionReadyContext { 2 | storage: any; 3 | error: any; 4 | } 5 | -------------------------------------------------------------------------------- /test/types/default-generator-context.ts: -------------------------------------------------------------------------------- 1 | export interface DefaultGeneratorContext { 2 | filePrefix: string; 3 | storage: any; 4 | result: any; 5 | ids: any[]; 6 | metadatas: string[]; 7 | sizes: number[]; 8 | collections: string[]; 9 | contentTypes: string[]; 10 | params: any[]; 11 | req: any; 12 | } 13 | -------------------------------------------------------------------------------- /test/types/edge-cases-context.ts: -------------------------------------------------------------------------------- 1 | export interface EdgeCasesContext { 2 | storage: any; 3 | } 4 | -------------------------------------------------------------------------------- /test/types/error-handling-context.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorHandlingContext { 2 | storage: any; 3 | url: string; 4 | } 5 | -------------------------------------------------------------------------------- /test/types/file-concurrency-context.ts: -------------------------------------------------------------------------------- 1 | export interface FileConcurrencyContext { 2 | url: string; 3 | db: any; 4 | client: any; 5 | storage: any; 6 | app: any; 7 | upload: any; 8 | } 9 | -------------------------------------------------------------------------------- /test/types/file-function-context.ts: -------------------------------------------------------------------------------- 1 | export interface FileFunctionContext { 2 | filenamePrefix: string; 3 | storage: any; 4 | result: any; 5 | ids: any[]; 6 | metadatas: string[]; 7 | sizes: number[]; 8 | bucketNames: string[]; 9 | contentTypes: string[]; 10 | } 11 | -------------------------------------------------------------------------------- /test/types/generator-promises-context.ts: -------------------------------------------------------------------------------- 1 | export interface GeneratorPromisesContext { 2 | storage: any; 3 | result: any; 4 | filePrefix: string; 5 | error: any; 6 | rejectedError: any; 7 | } 8 | -------------------------------------------------------------------------------- /test/types/handling-names-context.ts: -------------------------------------------------------------------------------- 1 | export interface HandlingNamesContext { 2 | storage: any; 3 | } 4 | -------------------------------------------------------------------------------- /test/types/incomplete-generators-context.ts: -------------------------------------------------------------------------------- 1 | export interface IncompleteGeneratorsContext { 2 | storage: any; 3 | error: any; 4 | } 5 | -------------------------------------------------------------------------------- /test/types/md5-hash-context.ts: -------------------------------------------------------------------------------- 1 | export interface Md5HashContext { 2 | storage: any; 3 | result: any; 4 | } 5 | -------------------------------------------------------------------------------- /test/types/storage-constructor-context.ts: -------------------------------------------------------------------------------- 1 | export interface StorageConstructorContext { 2 | url: string; 3 | storage: any; 4 | app: any; 5 | upload: any; 6 | } 7 | -------------------------------------------------------------------------------- /test/types/uploaded-file-context.ts: -------------------------------------------------------------------------------- 1 | export interface UploadedFileContext { 2 | storage: any; 3 | result: any; 4 | size: number; 5 | } 6 | -------------------------------------------------------------------------------- /test/types/utility-functions-context.ts: -------------------------------------------------------------------------------- 1 | export type UtilityFunctionsContext = void; 2 | -------------------------------------------------------------------------------- /test/types/utility-methods-context.ts: -------------------------------------------------------------------------------- 1 | export interface UtilityMethodsContext { 2 | storage: any; 3 | result: any; 4 | } 5 | -------------------------------------------------------------------------------- /test/uploaded-file.spec.ts: -------------------------------------------------------------------------------- 1 | import {readFile as readFileCb} from 'fs'; 2 | import anyTest, {TestInterface} from 'ava'; 3 | import express from 'express'; 4 | import request from 'supertest'; 5 | import multer from 'multer'; 6 | import pify from 'pify'; 7 | import hasOwn from 'has-own-prop'; 8 | 9 | import {GridFsStorage} from '../src'; 10 | import {files, cleanStorage} from './utils/testutils'; 11 | import {storageOptions} from './utils/settings'; 12 | import {UploadedFileContext} from './types/uploaded-file-context'; 13 | 14 | const test = anyTest as TestInterface; 15 | const readFile = pify(readFileCb); 16 | 17 | test.before(async (t) => { 18 | const app = express(); 19 | const storage = new GridFsStorage(storageOptions()); 20 | const upload = multer({storage}); 21 | t.context.storage = storage; 22 | 23 | app.post('/url', upload.single('photo'), (request_, response) => { 24 | t.context.result = { 25 | headers: request_.headers, 26 | file: request_.file, 27 | body: request_.body, 28 | }; 29 | response.end(); 30 | }); 31 | 32 | await storage.ready(); 33 | await request(app).post('/url').attach('photo', files[0]); 34 | 35 | const f = await readFile(files[0]); 36 | t.context.size = f.length; 37 | }); 38 | 39 | test.after.always('cleanup', async (t) => { 40 | await cleanStorage(t.context.storage); 41 | }); 42 | 43 | test('uploaded file have a filename property', (t) => { 44 | const {result} = t.context; 45 | t.true(hasOwn(result.file, 'filename')); 46 | t.is(typeof result.file.filename, 'string'); 47 | t.regex(result.file.filename, /^[\da-f]{32}$/); 48 | }); 49 | 50 | test('uploaded file have a metadata property', (t) => { 51 | const {result} = t.context; 52 | t.true(hasOwn(result.file, 'metadata')); 53 | t.is(result.file.metadata, null); 54 | }); 55 | 56 | test('uploaded file have a id property', (t) => { 57 | const {result} = t.context; 58 | t.true(hasOwn(result.file, 'id')); 59 | t.regex(result.file.id.toHexString(), /^[\da-f]{24}$/); 60 | }); 61 | 62 | test('uploaded file have a size property with the length of the file', (t) => { 63 | const {result, size} = t.context; 64 | t.true(hasOwn(result.file, 'size')); 65 | t.is(result.file.size, size); 66 | }); 67 | 68 | test('uploaded file have the default bucket name pointing to the fs collection', (t) => { 69 | const {result} = t.context; 70 | t.true(hasOwn(result.file, 'bucketName')); 71 | t.is(result.file.bucketName, 'fs'); 72 | }); 73 | 74 | test('uploaded file have the date of the upload', (t) => { 75 | const {result} = t.context; 76 | t.true(hasOwn(result.file, 'uploadDate')); 77 | t.true(result.file.uploadDate instanceof Date); 78 | }); 79 | -------------------------------------------------------------------------------- /test/utility-functions.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import {parse} from 'mongodb-uri'; 3 | import { 4 | compare, 5 | compareArrays, 6 | compareBy, 7 | compareUris, 8 | getDatabase, 9 | hasKeys, 10 | shouldListenOnDb, 11 | } from '../src/utils'; 12 | import {UtilityFunctionsContext} from './types/utility-functions-context'; 13 | 14 | const test = anyTest as TestInterface; 15 | 16 | /* Compare */ 17 | test('compare considers equal any falsey values', (t) => { 18 | t.true(compare(null, undefined)); 19 | t.true(compare(undefined, null)); 20 | }); 21 | 22 | test('compare considers equal objects with no keys and falsey values', (t) => { 23 | t.true(compare(null, {})); 24 | t.true(compare({}, null)); 25 | t.true(compare({}, undefined)); 26 | t.true(compare(undefined, {})); 27 | t.true(compare({}, {})); 28 | t.true(compare({}, Object.create(null))); 29 | }); 30 | 31 | test('compare considers different objects with keys and falsey values', (t) => { 32 | t.false(compare(null, {a: 1})); 33 | t.false(compare({a: 1}, null)); 34 | t.false(compare({a: 1}, undefined)); 35 | t.false(compare(undefined, {a: 1})); 36 | }); 37 | 38 | test('compare considers equal objects by reference', (t) => { 39 | const ob1 = {a: 1}; 40 | const ob2 = {b: 2}; 41 | t.true(compare(ob1, ob1)); 42 | t.true(compare(ob2, ob2)); 43 | }); 44 | 45 | test('compare considers equal objects with same property values', (t) => { 46 | function Object_() { 47 | this.a = 1; 48 | } 49 | 50 | Object_.prototype.b = 2; 51 | t.true(compare({a: 1}, {a: 1})); 52 | t.true(compare({a: 1, b: 2}, new Object_())); 53 | }); 54 | 55 | test('compare considers different objects with different keys values', (t) => { 56 | t.false(compare({a: 1}, {b: 1})); 57 | t.false(compare({c: 1}, {d: 1})); 58 | t.false(compare({c: 1}, {})); 59 | t.false(compare({}, {c: 1})); 60 | t.false(compare({c: 1}, {c: 1, d: 1})); 61 | }); 62 | 63 | test('compare considers different objects with different keys length', (t) => { 64 | t.false(compare({a: 1, b: 2}, {a: 1})); 65 | }); 66 | 67 | test('compare includes deep properties when comparing', (t) => { 68 | t.true(compare({a: {b: 1}}, {a: {b: 1}})); 69 | t.false(compare({a: {b: 1}}, {a: {b: 2}})); 70 | t.true(compare({a: {}}, {a: {}})); 71 | t.true(compare({a: {b: {}}}, {a: {b: Object.create(null)}})); 72 | }); 73 | 74 | test('compare includes arrays when comparing', (t) => { 75 | t.true(compare({a: {b: ['1', '2']}}, {a: {b: ['1', '2']}})); 76 | t.false(compare({a: {b: ['1', '2']}}, {a: {b: ['2', '2']}})); 77 | t.false(compare({a: {b: ['1']}}, {a: {b: ['1', '1']}})); 78 | t.true(compare({a: []}, {a: []})); 79 | }); 80 | 81 | test('compare includes buffers when comparing', (t) => { 82 | t.true(compare({a: {b: Buffer.from([1, 2])}}, {a: {b: Buffer.from([1, 2])}})); 83 | t.false( 84 | compare({a: {b: Buffer.from([1, 2])}}, {a: {b: Buffer.from([2, 2])}}), 85 | ); 86 | }); 87 | 88 | test('compare includes buffers inside arrays when comparing', (t) => { 89 | t.true( 90 | compare( 91 | {a: {b: ['1', Buffer.from([1, 2])]}}, 92 | {a: {b: ['1', Buffer.from([1, 2])]}}, 93 | ), 94 | ); 95 | t.false( 96 | compare( 97 | {a: {b: ['1', Buffer.from([1, 2])]}}, 98 | {a: {b: ['1', Buffer.from([2, 2])]}}, 99 | ), 100 | ); 101 | }); 102 | 103 | /* HasKeys */ 104 | test('returns true when the object has at least one property', (t) => { 105 | t.true(hasKeys({a: 1})); 106 | }); 107 | 108 | test('returns false when the object has no properties', (t) => { 109 | t.false(hasKeys({})); 110 | /* eslint-disable-next-line no-new-object */ 111 | t.false(hasKeys(new Object())); 112 | }); 113 | 114 | /* CompareArrays */ 115 | test('returns true when the arrays contains identical string or buffer values', (t) => { 116 | t.true(compareArrays(['a', 'b'], ['a', 'b'])); 117 | t.true(compareArrays([Buffer.from([1, 2]), 'b'], [Buffer.from([1, 2]), 'b'])); 118 | }); 119 | 120 | test('returns false when the arrays contains different values or they are compared by reference', (t) => { 121 | t.false(compareArrays(['a', 'b'], ['b', 'b'])); 122 | t.false(compareArrays([undefined], [null])); 123 | t.false(compareArrays([{a: 1}], [{a: 1}])); 124 | }); 125 | 126 | /* CompareBy */ 127 | test('returns identity when the objects have different types', (t) => { 128 | t.is(compareBy(Buffer.from([1, 2]), ['a', 'b']), 'identity'); 129 | }); 130 | 131 | test('returns the type of the objects when they have the same type', (t) => { 132 | t.is(compareBy([], ['a', 'b']), 'array'); 133 | t.is(compareBy(Buffer.from([1, 2]), Buffer.from(['a', 'b'])), 'buffer'); 134 | t.is(compareBy({}, {a: 1}), 'object'); 135 | }); 136 | 137 | /* CompareUris */ 138 | test('returns true for urls that contain the same hosts in different order', (t) => { 139 | t.true( 140 | compareUris( 141 | parse('mongodb://host1:1234,host2:5678/database'), 142 | parse('mongodb://host2:5678,host1:1234/database'), 143 | ), 144 | ); 145 | }); 146 | 147 | test('returns false for urls with different parameters', (t) => { 148 | t.false( 149 | compareUris( 150 | parse('mongodb://host1:1234,host2:5678/database?authSource=admin'), 151 | parse('mongodb://host2:5678,host1:1234/database'), 152 | ), 153 | ); 154 | }); 155 | 156 | test('returns true for urls with the same parameters in different order', (t) => { 157 | t.true( 158 | compareUris( 159 | parse( 160 | 'mongodb://host1:1234/database?authSource=admin&connectTimeoutMS=300000', 161 | ), 162 | parse( 163 | 'mongodb://host1:1234/database?connectTimeoutMS=300000&authSource=admin', 164 | ), 165 | ), 166 | ); 167 | }); 168 | 169 | /* GetDatabase */ 170 | test('returns the database object fom a mongoose instance', (t) => { 171 | const database = {}; 172 | t.is(getDatabase({connection: {db: database}}), database); 173 | }); 174 | 175 | test('returns the database object fom a mongoose connection instance', (t) => { 176 | const database = {}; 177 | t.is(getDatabase({db: database}), database); 178 | }); 179 | 180 | test('returns the database object directly if is not a mongoose object', (t) => { 181 | const database = {}; 182 | t.is(getDatabase(database), database); 183 | }); 184 | 185 | test('returns the true if the version number is lower that 3.6.4', (t) => { 186 | t.false(shouldListenOnDb('4.0.0')); 187 | t.false(shouldListenOnDb('3.7.0')); 188 | t.false(shouldListenOnDb('3.6.4')); 189 | t.true(shouldListenOnDb('3.6.3')); 190 | t.true(shouldListenOnDb('3.5.1')); 191 | t.true(shouldListenOnDb('2.7.8')); 192 | t.true(shouldListenOnDb('2.0.0')); 193 | }); 194 | -------------------------------------------------------------------------------- /test/utility-methods.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import anyTest, {TestInterface} from 'ava'; 3 | import hasOwn from 'has-own-prop'; 4 | import multer from 'multer'; 5 | import express from 'express'; 6 | import request from 'supertest'; 7 | import path from 'path'; 8 | import util from 'util'; 9 | 10 | import {GridFsStorage} from '../src'; 11 | import {cleanStorage, defer, files} from './utils/testutils'; 12 | import {storageOptions} from './utils/settings'; 13 | import {UtilityMethodsContext} from './types/utility-methods-context'; 14 | 15 | const test = anyTest as TestInterface; 16 | const unlink = util.promisify(fs.unlink); 17 | 18 | test.afterEach.always('cleanup', async (t) => { 19 | const testFile = path.join(__dirname, 'attachments', 'test_disk.jpg'); 20 | if (fs.existsSync(testFile)) { 21 | await unlink(testFile); 22 | } 23 | 24 | return cleanStorage(t.context.storage); 25 | }); 26 | 27 | test('generate 16 byte hex string', async (t) => { 28 | const {generateBytes} = GridFsStorage; 29 | const result: any = await generateBytes(); 30 | t.true(hasOwn(result, 'filename')); 31 | t.regex(result.filename, /^[a-f\d]{32}$/); 32 | }); 33 | 34 | test('upload a file using the fromFile method', async (t) => { 35 | t.context.storage = new GridFsStorage({ 36 | ...storageOptions(), 37 | file: () => 'test.jpg', 38 | }); 39 | const {storage} = t.context; 40 | await storage.ready(); 41 | const file = {stream: fs.createReadStream(files[0]), mimetype: 'image/jpeg'}; 42 | t.context.result = await storage.fromFile(null, file); 43 | const {result} = t.context; 44 | t.true(hasOwn(result, 'filename')); 45 | t.is(result.filename, 'test.jpg'); 46 | t.is(result.contentType, 'image/jpeg'); 47 | }); 48 | 49 | test('upload a file using the fromStream method', async (t) => { 50 | t.context.storage = new GridFsStorage({ 51 | ...storageOptions(), 52 | file: () => 'test.jpg', 53 | }); 54 | const {storage} = t.context; 55 | await storage.ready(); 56 | const stream = fs.createReadStream(files[0]); 57 | t.context.result = await storage.fromStream(stream); 58 | const {result} = t.context; 59 | t.true(hasOwn(result, 'filename')); 60 | t.is(result.filename, 'test.jpg'); 61 | t.is(result.contentType, undefined); 62 | }); 63 | 64 | test('upload a file using the fromStream method after another upload', async (t) => { 65 | const diskStorage = multer.diskStorage({ 66 | destination: path.join(__dirname, 'attachments'), 67 | filename: (request_, file, cb) => { 68 | cb(null, 'test_disk.jpg'); 69 | }, 70 | }); 71 | const upload = multer({storage: diskStorage}); 72 | const app = express(); 73 | const route = defer(); 74 | app.post('/url', upload.single('photos'), (request, response) => { 75 | const storage = new GridFsStorage({ 76 | ...storageOptions(), 77 | file: () => 'test.jpg', 78 | }); 79 | t.context.storage = storage; 80 | const {file} = request; 81 | const stream = fs.createReadStream(file.path); 82 | storage 83 | .fromStream(stream, request, file) 84 | /* eslint-disable-next-line promise/prefer-await-to-then */ 85 | .then((file) => route.resolve(file)) 86 | .catch((error) => route.reject(error)); 87 | response.end(); 88 | }); 89 | 90 | await request(app).post('/url').attach('photos', files[0]); 91 | const result = await route.promise; 92 | t.true(hasOwn(result, 'filename')); 93 | t.is(result.filename, 'test.jpg'); 94 | t.is(result.contentType, 'image/jpeg'); 95 | }); 96 | -------------------------------------------------------------------------------- /test/utils/macros.ts: -------------------------------------------------------------------------------- 1 | import pify from 'pify'; 2 | import md5 from 'md5-file'; 3 | import {files as testFiles} from './testutils'; 4 | 5 | const md5File = md5.sync ? md5 : pify(md5); 6 | 7 | export async function fileMatchMd5Hash(t, files, count = 2) { 8 | t.truthy(files); 9 | t.true(Array.isArray(files)); 10 | t.is(files.length, count); 11 | const md5 = await Promise.all( 12 | files.map(async (f, idx) => { 13 | const computed = await md5File(testFiles[idx]); 14 | return {md5: f.md5, computed}; 15 | }), 16 | ); 17 | t.true(md5.every((f: any) => f.md5 === f.computed)); 18 | } 19 | -------------------------------------------------------------------------------- /test/utils/settings.ts: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import random from 'crypto-random-string'; 3 | import {version} from 'mongodb/package.json'; 4 | 5 | const [major, minor, patch] = version.split('.').map(v => Number(v)); 6 | const hostname = process.env.MONGO_HOST || '127.0.0.1'; 7 | const port = process.env.MONGO_PORT || 27_017; 8 | const database = 'grid_storage'; 9 | 10 | interface ConnectionSettings { 11 | host: string; 12 | port: string | number; 13 | database: string; 14 | } 15 | 16 | export function getMongoDbMajorVersion(): number { 17 | return major; 18 | } 19 | 20 | export function getMongoDbMinorVersion(): number { 21 | return minor; 22 | } 23 | 24 | export function getMongoDbPatchVersion(): number { 25 | return patch; 26 | } 27 | 28 | export const connection: ConnectionSettings = { 29 | host: hostname, 30 | port, 31 | database, 32 | }; 33 | 34 | type KeyValuePair = Record; 35 | 36 | interface StorageOptionsSettings { 37 | url: string; 38 | options: KeyValuePair; 39 | } 40 | 41 | export const storageOptions = function (): StorageOptionsSettings { 42 | return { 43 | url: url.format({ 44 | protocol: 'mongodb', 45 | slashes: true, 46 | hostname, 47 | port, 48 | pathname: database + '_' + random({length: 10, type: 'hex'}), 49 | }), 50 | options: major < 4 ? {useNewUrlParser: true, useUnifiedTopology: true} : {}, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /test/utils/testutils.ts: -------------------------------------------------------------------------------- 1 | import {Readable, Writable} from 'stream'; 2 | import path from 'path'; 3 | import {parse} from 'mongodb-uri'; 4 | import {MongoClient} from 'mongodb'; 5 | import hasOwn from 'has-own-prop'; 6 | import delay from 'delay'; 7 | 8 | import {version} from 'mongodb/package.json'; 9 | import {connection, storageOptions} from './settings'; 10 | 11 | export const mongoVersion = version.split('.').map((v) => Number(v)); 12 | 13 | export const files = ['sample1.jpg', 'sample2.jpg'].map((file) => 14 | path.join(__dirname, '/../attachments/', file), 15 | ); 16 | 17 | export async function cleanStorage( 18 | storage: any, 19 | {client = null, db = null} = {}, 20 | ) { 21 | if (storage) { 22 | storage.removeAllListeners(); 23 | if (!db && !client) { 24 | db = storage.db; 25 | client = storage.client; 26 | } 27 | 28 | if (db) { 29 | await db.dropDatabase(); 30 | return closeConnections({db, client}); 31 | } 32 | } 33 | } 34 | 35 | export function closeConnections({db, client}) { 36 | if (client) { 37 | if (hasOwn(client, 'readyState') && client.readyState === 1) { 38 | return client.close(); 39 | } 40 | 41 | if (hasOwn(client, 'isConnected') && client.isConnected()) { 42 | return client.close(); 43 | } 44 | } else { 45 | return db.close(); 46 | } 47 | } 48 | 49 | export async function dropDatabase(url: string): Promise { 50 | if (url) { 51 | const {options} = storageOptions(); 52 | const _db = await MongoClient.connect(url, options); 53 | const db = getDb(_db, url); 54 | const client = getClient(_db); 55 | await db.dropDatabase(); 56 | if (client) { 57 | return client.close(); 58 | } 59 | 60 | return db.close(); 61 | } 62 | } 63 | 64 | export function getDb(client, url) { 65 | if (client instanceof MongoClient) { 66 | const {database} = parse(url); 67 | return client.db(database || connection.database); 68 | } 69 | 70 | return client; 71 | } 72 | 73 | export function getClient(client) { 74 | return client instanceof MongoClient ? client : null; 75 | } 76 | 77 | export function fakeConnectCb(error = null) { 78 | return async (...args) => { 79 | if (args.length === 3) { 80 | const cb = args[2]; 81 | setTimeout(() => { 82 | cb(error); 83 | }); 84 | return; 85 | } 86 | 87 | await delay(1); 88 | if (error) { 89 | return Promise.reject(error); 90 | } 91 | }; 92 | } 93 | 94 | export function defer() { 95 | const d = { 96 | promise: null, 97 | resolve: null, 98 | reject: null, 99 | }; 100 | d.promise = new Promise((resolve, reject) => { 101 | d.resolve = resolve; 102 | d.reject = reject; 103 | }); 104 | return d; 105 | } 106 | 107 | export class ErrorReadableStream extends Readable { 108 | err: Error; 109 | 110 | _read(size: number) { 111 | this.err = new Error('Stream error'); 112 | this.emit('error', this.err); 113 | } 114 | } 115 | export class ErrorWritableStream extends Writable { 116 | err: Error; 117 | 118 | _write(size: number) { 119 | this.err = new Error('Stream error'); 120 | this.emit('error', this.err); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "sourceMap": true, 6 | "outDir": "./lib", 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ], 13 | "include": [ 14 | "./src/**/*", 15 | "./src/index.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /wiki/Generators.md: -------------------------------------------------------------------------------- 1 | This module supports the optional use of [generator functions][gen] in the [`file`][file-option] option as a way to simplify generation of values and to remove the need for global variables to accomplish certain tasks. 2 | 3 | Is important to know that if you want to use them; you **must** write only *infinite generators*, that is, generator functions that never reach the `{done: true}` state. This can be easily accomplished by wrapping the `yield` statement in an infinite loop like `for(;;)` or `while(true) {}`. While you can ignore this warning and yield only a few values; in real life scenarios is not recommended to do so because after the generator is consumed **every** file that comes next will fail to upload. 4 | 5 | Asynchronous work inside generators must yield promises instead of the value. This module will handle that case and wait for the promise to resolve or reject. 6 | 7 | This is an example of using generator functions 8 | 9 | ```javascript 10 | const GridFSStorage = require('multer-gridfs-storage'); 11 | const storage = new GridFSStorage({ 12 | url: 'mongodb://yourhost:27017/database', 13 | file: function* () { 14 | let counter = 1; 15 | for (;;) { 16 | yield { 17 | filename: 'name' + counter 18 | }; 19 | counter++; 20 | } 21 | } 22 | }); 23 | var upload = multer({ storage: storage }); 24 | ``` 25 | 26 | File and request information can be obtained too but this happens differently than in normal functions because in generator functions every execution resumes in the expression that follows the `yield`. The first time those objects are available as the parameters of the function, the following times can be obtained as result of the `yield` as an array. 27 | 28 | This is an example of using the `req` and file objects 29 | 30 | ```javascript 31 | const storage = new GridFsStorage({ 32 | url: 'mongodb://yourhost:27017/database', 33 | file: function* (req, file) { 34 | let counter = 1; 35 | for (;;) { 36 | // variables req and file are automatically reasigned from the array using destructuring assignment 37 | [req, file] = yield { 38 | filename: `${file.originalname}_${counter}` 39 | }; 40 | counter++; 41 | } 42 | } 43 | }); 44 | ``` 45 | 46 | Generators are only supported on Node versions >= 4 and polyfills are not supported. 47 | 48 | [file-option]: https://github.com/devconcept/multer-gridfs-storage#file 49 | [gen]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function* "Generator function" 50 | 51 | 52 | --------------------------------------------------------------------------------