├── .editorconfig ├── .github ├── contributing.md ├── issue_template.md ├── pull_request_template.md ├── stale.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── app.js └── app.ts ├── lib ├── hooks.js ├── index.js └── util.js ├── mocharc.js ├── nycrc.json ├── package-lock.json ├── package.json ├── test ├── index.test.js └── s3.test.js └── types ├── index.d.ts ├── test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. To make this easier to manage we currently use [Zenhub](https://www.zenhub.com/) for issue triage and visibility. They have a free browser plugin you can install so that you can see what is in flight at any time, but of course you also always see current issues in Github. 6 | 7 | ## Report a bug 8 | 9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 10 | 11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 12 | 13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. 14 | 15 | ## Report a Security Concern 16 | 17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. 18 | 19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. 26 | 27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 28 | 29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 36 | 37 | ### ES6 compilation 38 | 39 | Feathers uses [Babel](https://babeljs.io/) to leverage the latest developments of the JavaScript language. All code and samples are currently written in ES2015. To transpile the code in this repository run 40 | 41 | > npm run compile 42 | 43 | __Note:__ `npm test` will run the compilation automatically before the tests. 44 | 45 | ### Tests 46 | 47 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 48 | 49 | ### Documentation 50 | 51 | Feathers documentation is contained in Markdown files in the [feathers-docs](https://github.com/feathersjs/feathers-docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release. 52 | 53 | ## External Modules 54 | 55 | If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [showcase](https://docs.feathersjs.com/why/showcase.html). You also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start. 56 | 57 | If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: 58 | 59 | ## Contributor Code of Conduct 60 | 61 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 62 | 63 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 64 | 65 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 66 | 67 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 72 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | (First please check that this issue is not already solved as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) 5 | 6 | - [ ] Tell us what broke. The more detailed the better. 7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 8 | 9 | ### Expected behavior 10 | Tell us what should happen 11 | 12 | ### Actual behavior 13 | Tell us what happens instead 14 | 15 | ### System configuration 16 | 17 | Tell us about the applicable parts of your setup. 18 | 19 | **Module versions** (especially the part that's not working): 20 | 21 | **NodeJS version**: 22 | 23 | **Operating System**: 24 | 25 | **Browser Version**: 26 | 27 | **React Native Version**: 28 | 29 | **Module Loader**: -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | ### Other Information 13 | 14 | If there's anything else that's important and relevant to your pull 15 | request, mention that information here. This could include 16 | benchmarks, or other information. 17 | 18 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. 19 | 20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 21 | 22 | Thanks for contributing to Feathers! :heart: -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 84 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - greenkeeper 8 | - bug 9 | - security 10 | - enhancement 11 | # Label to use when marking an issue as stale 12 | staleLabel: wontfix 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. 17 | Apologies if the issue could not be resolved. FeathersJS ecosystem 18 | modules are community maintained so there may be a chance that there isn't anybody 19 | available to address the issue at the moment. 20 | For other ways to get help [see here](https://docs.feathersjs.com/help/readme.html). 21 | # Comment to post when closing a stale issue. Set to `false` to disable 22 | closeComment: false 23 | # Only close stale issues 24 | only: issues 25 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: [push, pull_request] 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [12.x, 14.x, 16.x] 15 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 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 | - run: npm ci 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | .nyc_output 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # Commenting this out is preferred by some people, see 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 28 | node_modules 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | 33 | dist/ 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | install: npm install 4 | cache: 5 | directories: 6 | - /home/travis/.dts/typescript-installs 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v2.6.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v2.6.0) (2021-06-20) 4 | 5 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v2.5.0...v2.6.0) 6 | 7 | **Fixed bugs:** 8 | 9 | - Object which key contains '/' cannot be retrieved by REST on S3 [\#70](https://github.com/feathersjs-ecosystem/feathers-blob/issues/70) 10 | 11 | **Closed issues:** 12 | 13 | - Route.post\(\) requires a callback function but got a Object [\#86](https://github.com/feathersjs-ecosystem/feathers-blob/issues/86) 14 | 15 | **Merged pull requests:** 16 | 17 | - feat\(s3\): add support for fetching versioned bucket items [\#88](https://github.com/feathersjs-ecosystem/feathers-blob/pull/88) ([roemhildtg](https://github.com/roemhildtg)) 18 | - Added missing property of InitOptions [\#87](https://github.com/feathersjs-ecosystem/feathers-blob/pull/87) ([Nuran-Jafarov](https://github.com/Nuran-Jafarov)) 19 | 20 | ## [v2.5.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v2.5.0) (2021-04-25) 21 | 22 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v2.4.0...v2.5.0) 23 | 24 | **Closed issues:** 25 | 26 | - Could we have the contentType in the create result ? [\#83](https://github.com/feathersjs-ecosystem/feathers-blob/issues/83) 27 | 28 | **Merged pull requests:** 29 | 30 | - Add contentType in get / create response [\#85](https://github.com/feathersjs-ecosystem/feathers-blob/pull/85) ([mdartic](https://github.com/mdartic)) 31 | - Move CI to GitHub action [\#84](https://github.com/feathersjs-ecosystem/feathers-blob/pull/84) ([daffl](https://github.com/daffl)) 32 | 33 | ## [v2.4.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v2.4.0) (2021-04-15) 34 | 35 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v2.3.0...v2.4.0) 36 | 37 | **Closed issues:** 38 | 39 | - Typescript: Cannot use namespace 'AbstractBlobStore' as a type [\#81](https://github.com/feathersjs-ecosystem/feathers-blob/issues/81) 40 | 41 | **Merged pull requests:** 42 | 43 | - Fix option returnUri to have false value [\#82](https://github.com/feathersjs-ecosystem/feathers-blob/pull/82) ([belal-mazlom](https://github.com/belal-mazlom)) 44 | 45 | ## [v2.3.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v2.3.0) (2020-10-04) 46 | 47 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v2.2.0...v2.3.0) 48 | 49 | **Implemented enhancements:** 50 | 51 | - Add TypeScript definitions [\#78](https://github.com/feathersjs-ecosystem/feathers-blob/issues/78) 52 | 53 | **Closed issues:** 54 | 55 | - Metadata does not been used on cb of createWriteStream [\#80](https://github.com/feathersjs-ecosystem/feathers-blob/issues/80) 56 | - docs: how to use feathers-blob with typescript [\#77](https://github.com/feathersjs-ecosystem/feathers-blob/issues/77) 57 | - passing option returnUri: false and returns uri... [\#75](https://github.com/feathersjs-ecosystem/feathers-blob/issues/75) 58 | - How to render uploaded photos? [\#74](https://github.com/feathersjs-ecosystem/feathers-blob/issues/74) 59 | 60 | **Merged pull requests:** 61 | 62 | - feat: add typescript typings with dtslint tests [\#79](https://github.com/feathersjs-ecosystem/feathers-blob/pull/79) ([Mairu](https://github.com/Mairu)) 63 | - Small optimization to `create\(\)`. [\#76](https://github.com/feathersjs-ecosystem/feathers-blob/pull/76) ([davidbludlow](https://github.com/davidbludlow)) 64 | - Update mocha to the latest version 🚀 [\#73](https://github.com/feathersjs-ecosystem/feathers-blob/pull/73) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 65 | 66 | ## [v2.2.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v2.2.0) (2019-12-18) 67 | 68 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v2.1.0...v2.2.0) 69 | 70 | **Implemented enhancements:** 71 | 72 | - Make data uri encoding optional [\#69](https://github.com/feathersjs-ecosystem/feathers-blob/issues/69) 73 | - Support Multipart File Upload [\#25](https://github.com/feathersjs-ecosystem/feathers-blob/issues/25) 74 | 75 | **Fixed bugs:** 76 | 77 | - Extension can be ".false" if mime type is not recognized [\#47](https://github.com/feathersjs-ecosystem/feathers-blob/issues/47) 78 | - feathers client resolves to service path instead of store model on get and remove [\#4](https://github.com/feathersjs-ecosystem/feathers-blob/issues/4) 79 | 80 | **Closed issues:** 81 | 82 | - docs: get does not return buffer [\#71](https://github.com/feathersjs-ecosystem/feathers-blob/issues/71) 83 | - Large file upload to S3 errors with TimeOut from S3 [\#67](https://github.com/feathersjs-ecosystem/feathers-blob/issues/67) 84 | 85 | **Merged pull requests:** 86 | 87 | - feat: make data uri encoding optional [\#72](https://github.com/feathersjs-ecosystem/feathers-blob/pull/72) ([florianbepunkt](https://github.com/florianbepunkt)) 88 | - Fix comment typo [\#68](https://github.com/feathersjs-ecosystem/feathers-blob/pull/68) ([andys8](https://github.com/andys8)) 89 | - Update s3-blob-store to the latest version 🚀 [\#66](https://github.com/feathersjs-ecosystem/feathers-blob/pull/66) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 90 | - Update @feathersjs/errors to the latest version 🚀 [\#65](https://github.com/feathersjs-ecosystem/feathers-blob/pull/65) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 91 | - Update semistandard to the latest version 🚀 [\#64](https://github.com/feathersjs-ecosystem/feathers-blob/pull/64) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 92 | 93 | ## [v2.1.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v2.1.0) (2019-03-09) 94 | 95 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v2.0.1...v2.1.0) 96 | 97 | **Closed issues:** 98 | 99 | - An in-range update of debug is breaking the build 🚨 [\#61](https://github.com/feathersjs-ecosystem/feathers-blob/issues/61) 100 | - feathers blobs service save docx with .bin extension [\#59](https://github.com/feathersjs-ecosystem/feathers-blob/issues/59) 101 | - An in-range update of aws-sdk is breaking the build 🚨 [\#58](https://github.com/feathersjs-ecosystem/feathers-blob/issues/58) 102 | - An in-range update of aws-sdk is breaking the build 🚨 [\#55](https://github.com/feathersjs-ecosystem/feathers-blob/issues/55) 103 | - An in-range update of aws-sdk is breaking the build 🚨 [\#54](https://github.com/feathersjs-ecosystem/feathers-blob/issues/54) 104 | - An in-range update of aws-sdk is breaking the build 🚨 [\#53](https://github.com/feathersjs-ecosystem/feathers-blob/issues/53) 105 | - An in-range update of aws-sdk is breaking the build 🚨 [\#51](https://github.com/feathersjs-ecosystem/feathers-blob/issues/51) 106 | - An in-range update of aws-sdk is breaking the build 🚨 [\#50](https://github.com/feathersjs-ecosystem/feathers-blob/issues/50) 107 | - An in-range update of aws-sdk is breaking the build 🚨 [\#49](https://github.com/feathersjs-ecosystem/feathers-blob/issues/49) 108 | 109 | **Merged pull requests:** 110 | 111 | - Feat: use params for get operation [\#63](https://github.com/feathersjs-ecosystem/feathers-blob/pull/63) ([florianbepunkt](https://github.com/florianbepunkt)) 112 | - Update mocha to the latest version 🚀 [\#62](https://github.com/feathersjs-ecosystem/feathers-blob/pull/62) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 113 | - Update concat-stream to the latest version 🚀 [\#60](https://github.com/feathersjs-ecosystem/feathers-blob/pull/60) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 114 | - Greenkeeper/aws sdk 2.350.0 [\#57](https://github.com/feathersjs-ecosystem/feathers-blob/pull/57) ([daffl](https://github.com/daffl)) 115 | - Update semistandard to the latest version 🚀 [\#56](https://github.com/feathersjs-ecosystem/feathers-blob/pull/56) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 116 | - Greenkeeper/aws sdk 2.335.0 [\#52](https://github.com/feathersjs-ecosystem/feathers-blob/pull/52) ([daffl](https://github.com/daffl)) 117 | - Update debug to the latest version 🚀 [\#48](https://github.com/feathersjs-ecosystem/feathers-blob/pull/48) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 118 | 119 | ## [v2.0.1](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v2.0.1) (2018-06-03) 120 | 121 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v2.0.0...v2.0.1) 122 | 123 | **Closed issues:** 124 | 125 | - get\(\) does not work with latest s3-blob-store \(v3.0.0\) [\#37](https://github.com/feathersjs-ecosystem/feathers-blob/issues/37) 126 | - How to resize an image before create it ? [\#35](https://github.com/feathersjs-ecosystem/feathers-blob/issues/35) 127 | - So I need help pointing me in right direction, I would like to return a like to return a file url instead of base64 [\#18](https://github.com/feathersjs-ecosystem/feathers-blob/issues/18) 128 | 129 | **Merged pull requests:** 130 | 131 | - Update uberproto to the latest version 🚀 [\#46](https://github.com/feathersjs-ecosystem/feathers-blob/pull/46) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 132 | - Update dependencies and documentation to be up to date [\#45](https://github.com/feathersjs-ecosystem/feathers-blob/pull/45) ([daffl](https://github.com/daffl)) 133 | - Update s3-blob-store to the latest version 🚀 [\#44](https://github.com/feathersjs-ecosystem/feathers-blob/pull/44) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 134 | 135 | ## [v2.0.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v2.0.0) (2018-03-09) 136 | 137 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.5.0...v2.0.0) 138 | 139 | **Closed issues:** 140 | 141 | - Custom ID not returned on remove [\#39](https://github.com/feathersjs-ecosystem/feathers-blob/issues/39) 142 | 143 | **Merged pull requests:** 144 | 145 | - Upgrade to Feathers v3 and latest plugin infrastructure [\#43](https://github.com/feathersjs-ecosystem/feathers-blob/pull/43) ([daffl](https://github.com/daffl)) 146 | - Finalize S3 tests [\#42](https://github.com/feathersjs-ecosystem/feathers-blob/pull/42) ([daffl](https://github.com/daffl)) 147 | - Added tests for S3 blob store [\#38](https://github.com/feathersjs-ecosystem/feathers-blob/pull/38) ([claustres](https://github.com/claustres)) 148 | 149 | ## [v1.5.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.5.0) (2018-03-07) 150 | 151 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.4.1...v1.5.0) 152 | 153 | **Merged pull requests:** 154 | 155 | - Added support to create blob from raw buffer [\#41](https://github.com/feathersjs-ecosystem/feathers-blob/pull/41) ([claustres](https://github.com/claustres)) 156 | 157 | ## [v1.4.1](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.4.1) (2018-03-02) 158 | 159 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.4.0...v1.4.1) 160 | 161 | **Merged pull requests:** 162 | 163 | - Implemented custom id field on remove [\#40](https://github.com/feathersjs-ecosystem/feathers-blob/pull/40) ([claustres](https://github.com/claustres)) 164 | 165 | ## [v1.4.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.4.0) (2018-02-16) 166 | 167 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.3.1...v1.4.0) 168 | 169 | **Closed issues:** 170 | 171 | - List of files/blob [\#5](https://github.com/feathersjs-ecosystem/feathers-blob/issues/5) 172 | 173 | **Merged pull requests:** 174 | 175 | - return deleted id to avoid 404 error [\#36](https://github.com/feathersjs-ecosystem/feathers-blob/pull/36) ([lwhiteley](https://github.com/lwhiteley)) 176 | - Update s3-blob-store to the latest version 🚀 [\#34](https://github.com/feathersjs-ecosystem/feathers-blob/pull/34) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 177 | - endpoint option [\#33](https://github.com/feathersjs-ecosystem/feathers-blob/pull/33) ([sarkistlt](https://github.com/sarkistlt)) 178 | - Update mocha to the latest version 🚀 [\#32](https://github.com/feathersjs-ecosystem/feathers-blob/pull/32) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 179 | - Update semistandard to the latest version 🚀 [\#31](https://github.com/feathersjs-ecosystem/feathers-blob/pull/31) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 180 | - Wrong field name for custom id in create method [\#28](https://github.com/feathersjs-ecosystem/feathers-blob/pull/28) ([NikitaVlaznev](https://github.com/NikitaVlaznev)) 181 | - Update mocha to the latest version 🚀 [\#27](https://github.com/feathersjs-ecosystem/feathers-blob/pull/27) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 182 | - Update debug to the latest version 🚀 [\#26](https://github.com/feathersjs-ecosystem/feathers-blob/pull/26) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 183 | 184 | ## [v1.3.1](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.3.1) (2017-06-30) 185 | 186 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.3.0...v1.3.1) 187 | 188 | **Merged pull requests:** 189 | 190 | - Reject if Model.createWriteStream returns with error [\#24](https://github.com/feathersjs-ecosystem/feathers-blob/pull/24) ([3bola](https://github.com/3bola)) 191 | 192 | ## [v1.3.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.3.0) (2017-06-21) 193 | 194 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.2.0...v1.3.0) 195 | 196 | **Closed issues:** 197 | 198 | - How to change upload folder dynamically? [\#21](https://github.com/feathersjs-ecosystem/feathers-blob/issues/21) 199 | - del [\#15](https://github.com/feathersjs-ecosystem/feathers-blob/issues/15) 200 | - Get this repo consistent with all other repos [\#10](https://github.com/feathersjs-ecosystem/feathers-blob/issues/10) 201 | 202 | **Merged pull requests:** 203 | 204 | - Refactor to use promises internally [\#23](https://github.com/feathersjs-ecosystem/feathers-blob/pull/23) ([daffl](https://github.com/daffl)) 205 | - Update s3-blob-store to the latest version 🚀 [\#20](https://github.com/feathersjs-ecosystem/feathers-blob/pull/20) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 206 | - Update dauria to the latest version 🚀 [\#19](https://github.com/feathersjs-ecosystem/feathers-blob/pull/19) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 207 | - Update semistandard to the latest version 🚀 [\#17](https://github.com/feathersjs-ecosystem/feathers-blob/pull/17) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 208 | - Update dependencies to enable Greenkeeper 🌴 [\#16](https://github.com/feathersjs-ecosystem/feathers-blob/pull/16) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 209 | - Update README.md [\#14](https://github.com/feathersjs-ecosystem/feathers-blob/pull/14) ([bertho-zero](https://github.com/bertho-zero)) 210 | - jshint —\> semistandard [\#12](https://github.com/feathersjs-ecosystem/feathers-blob/pull/12) ([corymsmith](https://github.com/corymsmith)) 211 | - \[ci skip\] Add S3 options documentation to README [\#9](https://github.com/feathersjs-ecosystem/feathers-blob/pull/9) ([silvestreh](https://github.com/silvestreh)) 212 | 213 | ## [v1.2.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.2.0) (2016-08-18) 214 | 215 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.1.0...v1.2.0) 216 | 217 | **Closed issues:** 218 | 219 | - Unable to set ACL permissions for S3 [\#7](https://github.com/feathersjs-ecosystem/feathers-blob/issues/7) 220 | - unsupported mime types [\#6](https://github.com/feathersjs-ecosystem/feathers-blob/issues/6) 221 | 222 | **Merged pull requests:** 223 | 224 | - Add S3 Params when uploading files. [\#8](https://github.com/feathersjs-ecosystem/feathers-blob/pull/8) ([silvestreh](https://github.com/silvestreh)) 225 | 226 | ## [v1.1.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.1.0) (2016-06-17) 227 | 228 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.0.5...v1.1.0) 229 | 230 | **Closed issues:** 231 | 232 | - Why datauris? [\#2](https://github.com/feathersjs-ecosystem/feathers-blob/issues/2) 233 | 234 | **Merged pull requests:** 235 | 236 | - allow custom blob key [\#3](https://github.com/feathersjs-ecosystem/feathers-blob/pull/3) ([mcchrish](https://github.com/mcchrish)) 237 | 238 | ## [v1.0.5](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.0.5) (2016-04-03) 239 | 240 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.0.4...v1.0.5) 241 | 242 | **Merged pull requests:** 243 | 244 | - don't convert buffer to string [\#1](https://github.com/feathersjs-ecosystem/feathers-blob/pull/1) ([ahdinosaur](https://github.com/ahdinosaur)) 245 | 246 | ## [v1.0.4](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.0.4) (2016-03-05) 247 | 248 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.0.3...v1.0.4) 249 | 250 | ## [v1.0.3](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.0.3) (2016-03-01) 251 | 252 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.0.2...v1.0.3) 253 | 254 | ## [v1.0.2](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.0.2) (2016-02-29) 255 | 256 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/1.0.1...v1.0.2) 257 | 258 | ## [1.0.1](https://github.com/feathersjs-ecosystem/feathers-blob/tree/1.0.1) (2016-02-29) 259 | 260 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/v1.0.1...1.0.1) 261 | 262 | ## [v1.0.1](https://github.com/feathersjs-ecosystem/feathers-blob/tree/v1.0.1) (2016-02-29) 263 | 264 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/1.0.0...v1.0.1) 265 | 266 | ## [1.0.0](https://github.com/feathersjs-ecosystem/feathers-blob/tree/1.0.0) (2016-02-23) 267 | 268 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-blob/compare/a5e0f834e358790c244a410d707930387f38a8a6...1.0.0) 269 | 270 | 271 | 272 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-blob 2 | 3 | [![Node.js CI](https://github.com/feathersjs-ecosystem/feathers-blob/actions/workflows/node.js.yml/badge.svg)](https://github.com/feathersjs-ecosystem/feathers-blob/actions/workflows/node.js.yml) 4 | [![Dependency Status](https://img.shields.io/david/feathersjs-ecosystem/feathers-blob.svg?style=flat-square)](https://david-dm.org/feathersjs-ecosystem/feathers-blob) 5 | [![Download Status](https://img.shields.io/npm/dm/feathers-blob.svg?style=flat-square)](https://www.npmjs.com/package/feathers-blob) 6 | 7 | > [Feathers](http://feathersjs.com) [`abstract blob store`](https://github.com/maxogden/abstract-blob-store) service 8 | 9 | ## Installation 10 | 11 | ```shell 12 | npm install feathers-blob --save 13 | ``` 14 | 15 | Also install a [`abstract-blob-store` compatible module](https://github.com/maxogden/abstract-blob-store#some-modules-that-use-this). 16 | 17 | 18 | ## API 19 | 20 | ### `const BlobService = require('feathers-blob')` 21 | 22 | ### `blobService = BlobService(options)` 23 | 24 | - `options.Model` is an instantiated interface [that implements the `abstract-blob-store` API](https://github.com/maxogden/abstract-blob-store#api) 25 | - `options.id` is a string 'key' for the blob identifier. 26 | - `returnUri` defaults is `true`, set it to `false` to remove it from output. 27 | - `returnBuffer` defaults is `false` , set it to `true` to return buffer in the output. 28 | 29 | **Tip**: `returnUri`/`returnBuffer` are mutually exclusive. 30 | 31 | If you only want a buffer output instead of a data URI on create/get operations, you need to set `returnBuffer` to be `true`, also to set `retuarnUri` to be `false`. 32 | 33 | If you need both, use the default options, then extract the buffer from the data URI on the client-side to avoid transferring the data twice over the wire. 34 | 35 | ### `blobService.create(body, params)` 36 | 37 | where input `body` is an object with either: 38 | * a key `uri` pointing to [data URI](https://en.wikipedia.org/wiki/Data_URI_scheme) of the blob, 39 | * a key `buffer` pointing to [raw data buffer](https://nodejs.org/api/buffer.html) of the blob along with its `contentType` (i.e. MIME type). 40 | 41 | Optionally, you can specify in the `body` the blob `id` which can be the file 42 | path where you want to store the file, otherwise it would default to 43 | `${hash(content)}.${extension(contentType)}`. 44 | 45 | **Tip**: You can use feathers hooks to customize the `id`. You might not want the 46 | client-side to write whereever they want. 47 | 48 | returns output 'data' of the form: 49 | 50 | ```js 51 | { 52 | [this.id]: `${hash(content)}.${extension(contentType)}`, 53 | uri: body.uri, // When returnUri options is set true 54 | buffer: body.buffer, // When returnBuffer options is set true 55 | size: length(content) 56 | } 57 | ``` 58 | 59 | ### `blobService.get(id, params)` 60 | 61 | returns output `data` of the same form as `create`. 62 | 63 | ### `blobService.remove(id, params)` 64 | 65 | #### Params: 66 | 67 | Query: 68 | 69 | - `VersionId` (string): Version ID of document to access if using a versioned s3 bucket 70 | 71 | Example: 72 | 73 | ```js 74 | blobService.get('my-file.pdf', { 75 | query: {VersionId: 'xslkdfjlskdjfskljf.sdjfdkjfkdjfd'}, 76 | }) 77 | ``` 78 | 79 | ## Example 80 | 81 | ```js 82 | const { getBase64DataURI } = require('dauria'); 83 | const AWS = require('aws-sdk'); 84 | const S3BlobStore = require('s3-blob-store'); 85 | const feathers = require('@feathersjs/feathers'); 86 | const BlobService = require('feathers-blob'); 87 | 88 | const s3 = new AWS.S3({ 89 | endpoint: 'https://{service}.{region}.{provider}.com', 90 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 91 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 92 | }); 93 | 94 | const blobStore = S3BlobStore({ 95 | client: s3, 96 | bucket: 'feathers-blob' 97 | }); 98 | 99 | const blob = { 100 | uri: getBase64DataURI(new Buffer('hello world'), 'text/plain') 101 | } 102 | 103 | const app = feathers(); 104 | 105 | app.use('/upload', BlobService({ 106 | Model: blobStore 107 | })); 108 | 109 | const blobService = app.service('upload'); 110 | 111 | blobService.create(blob).then(function (result) { 112 | console.log('Stored blob with id', result.id); 113 | }).catch(err => { 114 | console.error(err); 115 | }); 116 | ``` 117 | 118 | Should you need to change your bucket's [options](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property), such as permissions, pass a `params.s3` object using a before hook. 119 | 120 | ```js 121 | app.service('upload').before({ 122 | create(hook) { 123 |    hook.params.s3 = { ACL: 'public-read' }; // makes uploaded files public 124 |  } 125 | }); 126 | ``` 127 | 128 | For a more complete example, see [examples/app](./examples/app.js) which can be run with `npm run example`. 129 | 130 | ## Tests 131 | 132 | Tests can be run by installing the node modules and running `npm run test`. 133 | 134 | To test the S3 read/write capabilities set the environmental variable `S3_BUCKET` to the name of a bucket you have read/write access to. Enable the versioning functionality on the bucket. 135 | 136 | ## License 137 | 138 | Copyright (c) 2018 139 | 140 | Licensed under the [MIT license](LICENSE). 141 | -------------------------------------------------------------------------------- /examples/app.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const Store = require('s3-blob-store'); 3 | const feathers = require('@feathersjs/feathers'); 4 | const express = require('@feathersjs/express'); 5 | const BlobService = require('feathers-blob'); 6 | 7 | const s3 = new AWS.S3({ 8 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 9 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY 10 | }); 11 | 12 | const blobStore = Store({ 13 | client: s3, 14 | bucket: 'feathers-blob-store' 15 | }); 16 | 17 | const blobService = BlobService({ 18 | Model: blobStore 19 | }); 20 | 21 | // Create a feathers instance. 22 | const app = express(feathers()) 23 | // Turn on JSON parser for REST services 24 | .use(express.json()) 25 | // Turn on URL-encoded parser for REST services 26 | .use(express.urlencoded({ extended: true })) 27 | .use('/blobs', blobService); 28 | 29 | // A basic error handler, just like Express 30 | app.use(function (error, req, res, next) { 31 | res.json(error); 32 | }); 33 | 34 | // Start the server 35 | module.exports = app.listen(3030); 36 | 37 | console.log('feathers-blob-store service running on 127.0.0.1:3030'); 38 | -------------------------------------------------------------------------------- /examples/app.ts: -------------------------------------------------------------------------------- 1 | import { S3 } from 'aws-sdk'; 2 | // @ts-ignore 3 | import S3BlobStore from 's3-blob-store'; 4 | import createBlobService from 'feathers-blob'; 5 | import feathers from '@feathersjs/feathers'; 6 | import express from '@feathersjs/express'; 7 | 8 | const s3 = new S3({ 9 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 10 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY 11 | }); 12 | 13 | const blobStore = S3BlobStore({ 14 | client: s3, 15 | bucket: 'feathers-blob-store' 16 | }); 17 | 18 | const blobService = createBlobService({ 19 | Model: blobStore 20 | }); 21 | 22 | // Create a feathers instance. 23 | var app = express(feathers()) 24 | // Turn on JSON parser for REST services 25 | .use(express.json()) 26 | // Turn on URL-encoded parser for REST services 27 | .use(express.urlencoded({ extended: true })) 28 | .use('/blobs', blobService); 29 | 30 | app.use(express.errorHandler()); 31 | 32 | // Start the server 33 | module.exports = app.listen(3030); 34 | 35 | console.log('feathers-blob-store service running on 127.0.0.1:3030'); 36 | -------------------------------------------------------------------------------- /lib/hooks.js: -------------------------------------------------------------------------------- 1 | const convertQueryOverrides = () => { 2 | return context => { 3 | const { query = {} } = context.params; 4 | const convertQueryParam = key => { 5 | if (typeof query[key] === 'string') query[key] = Boolean(JSON.parse(query[key].toLowerCase())); 6 | }; 7 | convertQueryParam('returnUri'); 8 | convertQueryParam('returnBuffer'); 9 | return context; 10 | }; 11 | }; 12 | 13 | module.exports = { 14 | convertQueryOverrides 15 | }; 16 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { extname } = require('path'); 2 | const Proto = require('uberproto'); 3 | const errors = require('@feathersjs/errors'); 4 | const makeDebug = require('debug'); 5 | 6 | const { 7 | getBase64DataURI, 8 | parseDataURI 9 | } = require('dauria'); 10 | 11 | const toBuffer = require('concat-stream'); 12 | const mimeTypes = require('mime-types'); 13 | const debug = makeDebug('feathers-blob-store'); 14 | 15 | const { 16 | fromBuffer, 17 | bufferToHash 18 | } = require('./util'); 19 | 20 | const { convertQueryOverrides } = require('./hooks'); 21 | 22 | class Service { 23 | constructor (options) { 24 | if (!options) { 25 | throw new Error('feathers-blob-store: constructor `options` must be provided'); 26 | } 27 | 28 | if (!options.Model) { 29 | throw new Error('feathers-blob-store: constructor `options.Model` must be provided'); 30 | } 31 | 32 | this.returnBuffer = options.returnBuffer || false; 33 | this.returnUri = options.returnUri !== undefined ? options.returnUri : true; 34 | this.Model = options.Model; 35 | this.id = options.id || 'id'; 36 | } 37 | 38 | extend (obj) { 39 | return Proto.extend(obj, this); 40 | } 41 | 42 | get (id, params = {}) { 43 | const { query } = params; 44 | const s3Params = { 45 | ...params.s3, 46 | VersionId: (query && query.VersionId) ? query.VersionId : undefined 47 | }; 48 | const returnBuffer = query && 'returnBuffer' in query ? query.returnBuffer : this.returnBuffer; 49 | const returnUri = query && 'returnUri' in query ? query.returnUri : this.returnUri; 50 | 51 | const ext = extname(id); 52 | let contentType = mimeTypes.lookup(ext); 53 | // Unrecognized mime type 54 | if ((typeof contentType === 'boolean') && !contentType) { 55 | // Fallback to binary content 56 | contentType = 'application/octet-stream'; 57 | } 58 | debug(`Retrieving blob ${id} with ext ${ext} and content type ${contentType}`); 59 | 60 | return new Promise((resolve, reject) => { 61 | this.Model.createReadStream({ 62 | key: id, 63 | params: s3Params 64 | }) 65 | .on('error', reject) 66 | .pipe(toBuffer(buffer => { 67 | resolve({ 68 | [this.id]: id, 69 | ...(returnBuffer && { buffer }), 70 | ...(returnUri && { 71 | uri: getBase64DataURI(buffer, contentType) 72 | }), 73 | size: buffer.length, 74 | contentType 75 | }); 76 | })); 77 | }); 78 | } 79 | 80 | create (body, params = {}) { 81 | const { query } = params; 82 | const returnBuffer = query && 'returnBuffer' in query ? query.returnBuffer : this.returnBuffer; 83 | const returnUri = query && 'returnUri' in query ? query.returnUri : this.returnUri; 84 | 85 | let { id, uri, buffer, contentType } = body; 86 | if (uri) { 87 | const result = parseDataURI(uri); 88 | contentType = result.MIME; 89 | buffer = result.buffer; 90 | } else { 91 | if (returnUri) { 92 | uri = getBase64DataURI(buffer, contentType); 93 | } 94 | } 95 | 96 | if (!uri && (!buffer || !contentType)) { 97 | throw new errors.BadRequest('Buffer or URI with valid content type must be provided to create a blob'); 98 | } 99 | let ext = mimeTypes.extension(contentType); 100 | 101 | // Unrocognized mime type 102 | if ((typeof ext === 'boolean') && !ext) { 103 | // Fallback to binary content 104 | ext = 'bin'; 105 | contentType = 'application/octet-stream'; 106 | } 107 | 108 | if (!id) { 109 | const hash = bufferToHash(buffer); 110 | id = `${hash}.${ext}`; 111 | } 112 | 113 | debug(`Creating blob ${id} with ext ${ext} and content type ${contentType}`); 114 | return new Promise((resolve, reject) => { 115 | fromBuffer(buffer) 116 | .pipe(this.Model.createWriteStream({ 117 | key: id, 118 | params: params.s3 119 | }, (error) => 120 | error 121 | ? reject(error) 122 | : resolve({ 123 | [this.id]: id, 124 | ...(returnBuffer && { buffer }), 125 | ...(returnUri && { uri }), 126 | size: buffer.length, 127 | contentType 128 | }) 129 | )) 130 | .on('error', reject); 131 | }); 132 | } 133 | 134 | remove (id, params = {}) { 135 | debug(`Removing blob ${id}`); 136 | return new Promise((resolve, reject) => { 137 | this.Model.remove({ 138 | key: id, 139 | params: params.s3 140 | }, error => error ? reject(error) : resolve({ [this.id]: id })); 141 | }); 142 | } 143 | } 144 | 145 | module.exports = function init (options) { 146 | return new Service(options); 147 | }; 148 | 149 | module.exports.Service = Service; 150 | 151 | module.exports.hooks = { 152 | convertQueryOverrides 153 | }; 154 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const from = require('from2'); 3 | 4 | exports.fromBuffer = function fromBuffer (buffer) { 5 | // assert.ok(Buffer.isBuffer(buffer)) 6 | 7 | return from(function (size, next) { 8 | if (buffer.length <= 0) { 9 | return this.push(null); 10 | } 11 | 12 | const chunk = buffer.slice(0, size); 13 | buffer = buffer.slice(size); 14 | 15 | next(null, chunk); 16 | }); 17 | }; 18 | 19 | exports.bufferToHash = function bufferToHash (buffer) { 20 | const hash = crypto.createHash('sha256'); 21 | hash.update(buffer); 22 | return hash.digest('hex'); 23 | }; 24 | -------------------------------------------------------------------------------- /mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | package: path.join(__dirname, './package.json'), 6 | ui: 'bdd', 7 | spec: [ 8 | './test/**/*.test.js' 9 | ], 10 | timeout: 20000 11 | }; 12 | -------------------------------------------------------------------------------- /nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "include": [ 4 | "lib/**/*.js" 5 | ], 6 | "reporter": [ 7 | "html", 8 | "text", 9 | "text-summary", 10 | "lcov" 11 | ], 12 | "watermarks": { 13 | "statements": [50, 80], 14 | "lines": [50, 80], 15 | "functions": [50, 80], 16 | "branches": [50, 80] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-blob", 3 | "description": "Feathers blob service", 4 | "version": "2.6.0", 5 | "homepage": "https://github.com/feathersjs-ecosystem/feathers-blob", 6 | "main": "lib/", 7 | "keywords": [ 8 | "feathers", 9 | "feathers-plugin", 10 | "blob", 11 | "abstract-blob-store", 12 | "fs", 13 | "s3", 14 | "file", 15 | "files", 16 | "binary" 17 | ], 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/feathersjs-ecosystem/feathers-blob.git" 22 | }, 23 | "author": { 24 | "name": "Feathers contributors", 25 | "email": "hello@feathersjs.com", 26 | "url": "https://feathersjs.com" 27 | }, 28 | "contributors": [], 29 | "bugs": { 30 | "url": "https://github.com/feathersjs-ecosystem/feathers-blob/issues" 31 | }, 32 | "engines": { 33 | "node": ">= 6" 34 | }, 35 | "files": [ 36 | "CHANGELOG.md", 37 | "LICENSE", 38 | "README.md", 39 | "lib/**", 40 | "types/**" 41 | ], 42 | "scripts": { 43 | "publish": "git push origin --tags && npm run changelog && git push origin", 44 | "changelog": "github_changelog_generator -u feathersjs-ecosystem -p feathers-blob && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 45 | "release:patch": "npm version patch && npm publish", 46 | "release:minor": "npm version minor && npm publish", 47 | "release:major": "npm version major && npm publish", 48 | "lint": "semistandard --fix", 49 | "dtslint": "dtslint types", 50 | "mocha": "mocha", 51 | "test": "npm run lint && npm run coverage && npm run dtslint", 52 | "example": "babel-node examples/app", 53 | "coverage": "nyc npm run mocha" 54 | }, 55 | "semistandard": { 56 | "env": [ 57 | "mocha" 58 | ] 59 | }, 60 | "directories": { 61 | "lib": "lib" 62 | }, 63 | "types": "types", 64 | "greenkeeper": { 65 | "ignore": [ 66 | "aws-sdk" 67 | ] 68 | }, 69 | "dependencies": { 70 | "@feathersjs/errors": "^4.5.12", 71 | "concat-stream": "^2.0.0", 72 | "dauria": "^2.0.0", 73 | "debug": "^4.3.3", 74 | "from2": "^2.3.0", 75 | "mime-types": "^2.1.34", 76 | "uberproto": "^2.0.6" 77 | }, 78 | "devDependencies": { 79 | "@feathersjs/client": "^4.5.13", 80 | "@feathersjs/express": "^4.5.12", 81 | "@feathersjs/feathers": "^4.5.12", 82 | "abstract-blob-store": "^3.3.5", 83 | "aws-sdk": "^2.1067.0", 84 | "body-parser": "^1.19.1", 85 | "dtslint": "^4.2.1", 86 | "fs-blob-store": "^6.0.0", 87 | "mocha": "^9.2.0", 88 | "nyc": "^15.1.0", 89 | "s3-blob-store": "^4.1.1", 90 | "semistandard": "^16.0.1", 91 | "superagent": "^7.1.1", 92 | "typescript": "^4.5.5" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { join } = require('path'); 3 | const feathers = require('@feathersjs/feathers'); 4 | const express = require('@feathersjs/express'); 5 | const rest = require('@feathersjs/express/rest'); 6 | const client = require('@feathersjs/client'); 7 | const request = require('superagent'); 8 | const bodyParser = require('body-parser'); 9 | const FsBlobStore = require('fs-blob-store'); 10 | const BlobService = require('../lib'); 11 | 12 | const { getBase64DataURI } = require('dauria'); 13 | const { bufferToHash } = require('../lib/util'); 14 | 15 | describe('feathers-blob-store-basic', () => { 16 | const content = Buffer.from('hello world!'); 17 | const contentHash = bufferToHash(content); 18 | const contentType = 'text/plain'; 19 | const contentUri = getBase64DataURI(content, contentType); 20 | const unknownContentUri = getBase64DataURI(content); 21 | const contentExt = 'txt'; 22 | const contentId = `${contentHash}.${contentExt}`; 23 | let blobStore, store, server; 24 | 25 | it('is CommonJS compatible', () => { 26 | assert.strictEqual(typeof BlobService, 'function'); 27 | blobStore = FsBlobStore(join(__dirname, 'blobs')); 28 | store = BlobService({ 29 | Model: blobStore 30 | }); 31 | }); 32 | 33 | it('service operations with data URI input', async () => { 34 | let res = await store.create({ uri: contentUri }); 35 | assert.strictEqual(res.id, contentId); 36 | assert.strictEqual(res.uri, contentUri); 37 | assert.strictEqual(res.size, content.length); 38 | assert.strictEqual(res.contentType, contentType); 39 | // test successful get 40 | res = await store.get(contentId); 41 | assert.strictEqual(res.id, contentId); 42 | assert.strictEqual(res.uri, contentUri); 43 | assert.strictEqual(res.size, content.length); 44 | assert.strictEqual(res.contentType, contentType); 45 | 46 | // test successful remove 47 | res = await store.remove(contentId); 48 | assert.deepStrictEqual(res, { id: contentId }); 49 | 50 | // test failing get 51 | try { 52 | await store.get(contentId); 53 | } catch (err) { 54 | assert.ok(err, '.get() to non-existent id should error'); 55 | } 56 | }); 57 | 58 | it('service operations with buffer input', async () => { 59 | let res = await store.create({ buffer: content, contentType }); 60 | assert.strictEqual(res.id, contentId); 61 | assert.strictEqual(res.uri, contentUri); 62 | assert.strictEqual(res.size, content.length); 63 | assert.strictEqual(res.contentType, contentType); 64 | 65 | // test successful get 66 | res = await store.get(contentId); 67 | assert.strictEqual(res.id, contentId); 68 | assert.strictEqual(res.uri, contentUri); 69 | assert.strictEqual(res.size, content.length); 70 | assert.strictEqual(res.contentType, contentType); 71 | 72 | // test successful remove 73 | res = await store.remove(contentId); 74 | assert.deepStrictEqual(res, { id: contentId }); 75 | 76 | // test failing get 77 | try { 78 | await store.get(contentId); 79 | } catch (err) { 80 | assert.ok(err, '.get() to non-existent id should error'); 81 | } 82 | }); 83 | 84 | it('service operations with custom object id', async () => { 85 | const customId = `custom/id/${contentHash}.${contentExt}`; 86 | 87 | let res = await store.create({ id: customId, uri: contentUri }); 88 | assert.strictEqual(res.id, customId); 89 | assert.strictEqual(res.uri, contentUri); 90 | assert.strictEqual(res.size, content.length); 91 | assert.strictEqual(res.contentType, contentType); 92 | 93 | // test successful get 94 | res = await store.get(customId); 95 | assert.strictEqual(res.id, customId); 96 | assert.strictEqual(res.uri, contentUri); 97 | assert.strictEqual(res.size, content.length); 98 | assert.strictEqual(res.contentType, contentType); 99 | 100 | // test successful remove 101 | res = await store.remove(customId); 102 | assert.deepStrictEqual(res, { id: customId }); 103 | 104 | // test failing get 105 | try { 106 | await store.get(customId); 107 | } catch (err) { 108 | assert.ok(err, '.get() to non-existent id should error'); 109 | } 110 | }); 111 | 112 | it('service operations with buffer output', async () => { 113 | const store = BlobService({ 114 | Model: blobStore, 115 | returnBuffer: true, 116 | returnUri: false 117 | }); 118 | 119 | let res = await store.create({ buffer: content, contentType }); 120 | assert.strictEqual(res.id, contentId); 121 | assert.strictEqual(res.buffer.equals(content), true); 122 | assert.strictEqual(res.uri, undefined); 123 | assert.strictEqual(res.size, content.length); 124 | assert.strictEqual(res.contentType, contentType); 125 | 126 | // test successful get 127 | res = await store.get(contentId); 128 | assert.strictEqual(res.id, contentId); 129 | assert.strictEqual(res.buffer.equals(content), true); 130 | assert.strictEqual(res.uri, undefined); 131 | assert.strictEqual(res.size, content.length); 132 | assert.strictEqual(res.contentType, contentType); 133 | 134 | // test successful remove 135 | res = await store.remove(contentId); 136 | assert.deepStrictEqual(res, { id: contentId }); 137 | 138 | try { 139 | // test failing get 140 | await store.get(contentId); 141 | } catch (err) { 142 | assert.ok(err, '.get() to non-existent id should error'); 143 | } 144 | }); 145 | 146 | it('service operations without uri output', async () => { 147 | const store = BlobService({ 148 | Model: blobStore, 149 | returnUri: false 150 | }); 151 | 152 | let res = await store.create({ buffer: content, contentType }); 153 | assert.strictEqual(res.id, contentId); 154 | assert.strictEqual(res.buffer, undefined); 155 | assert.strictEqual(res.uri, undefined); 156 | assert.strictEqual(res.size, content.length); 157 | assert.strictEqual(res.contentType, contentType); 158 | 159 | // test successful get 160 | res = await store.get(contentId); 161 | assert.strictEqual(res.id, contentId); 162 | assert.strictEqual(res.buffer, undefined); 163 | assert.strictEqual(res.uri, undefined); 164 | assert.strictEqual(res.size, content.length); 165 | assert.strictEqual(res.contentType, contentType); 166 | 167 | // test successful remove 168 | res = await store.remove(contentId); 169 | assert.deepStrictEqual(res, { id: contentId }); 170 | 171 | try { 172 | // test failing get 173 | await store.get(contentId); 174 | } catch (err) { 175 | assert.ok(err, '.get() to non-existent id should error'); 176 | } 177 | }); 178 | 179 | it('service operations with custom output id field', async () => { 180 | const store = BlobService({ 181 | Model: blobStore, 182 | id: '_id' 183 | }); 184 | 185 | let res = await store.create({ id: contentId, uri: contentUri }); 186 | assert.strictEqual(res._id, contentId); 187 | assert.strictEqual(res.uri, contentUri); 188 | assert.strictEqual(res.size, content.length); 189 | assert.strictEqual(res.contentType, contentType); 190 | 191 | // test successful get 192 | res = await store.get(contentId); 193 | assert.strictEqual(res._id, contentId); 194 | assert.strictEqual(res.uri, contentUri); 195 | assert.strictEqual(res.size, content.length); 196 | assert.strictEqual(res.contentType, contentType); 197 | 198 | // test successful remove 199 | res = await store.remove(contentId); 200 | assert.deepStrictEqual(res, { _id: contentId }); 201 | 202 | try { 203 | // test failing get 204 | await store.get(contentId); 205 | } catch (err) { 206 | assert.ok(err, '.get() to non-existent id should error'); 207 | } 208 | }); 209 | 210 | it('service operations with custom extension and unrecognized mime type', async () => { 211 | const customId = `${contentHash}.zzz`; 212 | 213 | let res = await store.create({ id: customId, uri: unknownContentUri }); 214 | assert.strictEqual(res.id, customId); 215 | assert.strictEqual(res.uri, unknownContentUri); 216 | assert.strictEqual(res.size, content.length); 217 | assert.strictEqual(res.contentType, 'application/octet-stream'); 218 | 219 | // test successful get 220 | res = await store.get(customId); 221 | assert.strictEqual(res.id, customId); 222 | assert.strictEqual(res.uri, unknownContentUri); 223 | assert.strictEqual(res.size, content.length); 224 | assert.strictEqual(res.contentType, 'application/octet-stream'); 225 | 226 | // test successful remove 227 | res = await store.remove(customId); 228 | assert.deepStrictEqual(res, { id: customId }); 229 | 230 | try { 231 | // test failing get 232 | await store.get(customId); 233 | } catch (err) { 234 | assert.ok(err, '.get() to non-existent id should error'); 235 | } 236 | }); 237 | 238 | it('service operations without extension and unrecognized mime type', async () => { 239 | const customId = `${contentHash}`; 240 | 241 | let res = await store.create({ id: customId, uri: unknownContentUri }); 242 | assert.strictEqual(res.id, customId); 243 | assert.strictEqual(res.uri, unknownContentUri); 244 | assert.strictEqual(res.size, content.length); 245 | assert.strictEqual(res.contentType, 'application/octet-stream'); 246 | 247 | // test successful get 248 | res = await store.get(customId); 249 | assert.strictEqual(res.id, customId); 250 | assert.strictEqual(res.uri, unknownContentUri); 251 | assert.strictEqual(res.size, content.length); 252 | assert.strictEqual(res.contentType, 'application/octet-stream'); 253 | 254 | // test successful remove 255 | res = await store.remove(customId); 256 | assert.deepStrictEqual(res, { id: customId }); 257 | 258 | try { 259 | // test failing get 260 | await store.get(customId); 261 | } catch (err) { 262 | assert.ok(err, '.get() to non-existent id should error'); 263 | } 264 | }); 265 | 266 | it('service operations overriding returnUri per request', async () => { 267 | // default service with returnUri on 268 | let store = BlobService({ 269 | Model: blobStore 270 | }); 271 | 272 | // disable returnUri for create from buffer 273 | let res = await store.create({ buffer: content, contentType }, { query: { returnUri: false } }); 274 | assert.strictEqual(res.uri, undefined); 275 | 276 | // disable returnUri for create from uri 277 | res = await store.create({ uri: contentUri }, { query: { returnUri: false } }); 278 | assert.strictEqual(res.uri, undefined); 279 | 280 | // disable returnUri for get 281 | res = await store.get(contentId, { query: { returnUri: false } }); 282 | assert.strictEqual(res.uri, undefined); 283 | 284 | // service with default returnBuffer turned off 285 | store = BlobService({ 286 | Model: blobStore, 287 | returnBuffer: false, 288 | returnUri: false 289 | }); 290 | 291 | // enable returnUri for create from buffer 292 | res = await store.create({ buffer: content, contentType }, { query: { returnUri: true } }); 293 | assert.strictEqual(res.uri, contentUri); 294 | 295 | // enable returnUri for create from uri 296 | res = await store.create({ uri: contentUri }, { query: { returnUri: true } }); 297 | assert.strictEqual(res.uri, contentUri); 298 | 299 | // enable returnUri for get 300 | res = await store.get(contentId, { query: { returnUri: true } }); 301 | assert.strictEqual(res.uri, contentUri); 302 | }); 303 | 304 | it('service operations overriding returnBuffer per request', async () => { 305 | // service with returnBuffer on 306 | let store = BlobService({ 307 | Model: blobStore, 308 | returnBuffer: true, 309 | returnUri: false 310 | }); 311 | 312 | // disable returnBuffer for create from buffer 313 | let res = await store.create({ buffer: content, contentType }, { query: { returnBuffer: false } }); 314 | assert.strictEqual(res.buffer, undefined); 315 | 316 | // disable returnBuffer for create from uri 317 | res = await store.create({ uri: contentUri }, { query: { returnBuffer: false } }); 318 | assert.strictEqual(res.buffer, undefined); 319 | 320 | // disable returnBuffer for get 321 | res = await store.get(contentId, { query: { returnBuffer: false } }); 322 | assert.strictEqual(res.buffer, undefined); 323 | 324 | // service with returnBuffer turned off 325 | store = BlobService({ 326 | Model: blobStore, 327 | returnBuffer: false, 328 | returnUri: false 329 | }); 330 | 331 | // enable returnBuffer for create from buffer 332 | res = await store.create({ buffer: content, contentType }, { query: { returnBuffer: true } }); 333 | assert.strictEqual(res.buffer.equals(content), true); 334 | 335 | // enable returnBuffer for create from uri 336 | res = await store.create({ uri: contentUri }, { query: { returnBuffer: true } }); 337 | assert.strictEqual(res.buffer.equals(content), true); 338 | 339 | // enable returnBuffer for get 340 | res = await store.get(contentId, { query: { returnBuffer: true } }); 341 | assert.strictEqual(res.buffer.equals(content), true); 342 | }); 343 | 344 | it('service operations from client', (done) => { 345 | const blobStore = FsBlobStore(join(__dirname, 'blobs')); 346 | const store = BlobService({ 347 | Model: blobStore 348 | }); 349 | const app = express(feathers()); 350 | app.use(bodyParser.json()); 351 | app.configure(rest()); 352 | app.use('/storage', store); 353 | server = app.listen(3030); 354 | server.once('listening', async _ => { 355 | const url = 'http://localhost:3030'; 356 | const restClient = client() 357 | .configure(client.rest(url).superagent(request)); 358 | const service = restClient.service('storage'); 359 | 360 | const contentId = `${contentHash}.${contentExt}`; 361 | 362 | let res = await service.create({ uri: contentUri }); 363 | assert.strictEqual(res.id, contentId); 364 | assert.strictEqual(res.uri, contentUri); 365 | assert.strictEqual(res.size, content.length); 366 | assert.strictEqual(res.contentType, contentType); 367 | 368 | // test successful get 369 | res = await service.get(contentId); 370 | assert.strictEqual(res.id, contentId); 371 | assert.strictEqual(res.uri, contentUri); 372 | assert.strictEqual(res.size, content.length); 373 | assert.strictEqual(res.contentType, contentType); 374 | 375 | // test create with returnUri turned off without hook (no effect) 376 | res = await service.create({ uri: contentUri }, { query: { returnUri: false } }); 377 | assert.strictEqual(res.uri, contentUri); 378 | 379 | // test create with returnUri turned off with hook (works) 380 | const storage = app.service('storage'); 381 | const { convertQueryOverrides } = require('../lib').hooks; 382 | storage.hooks({ 383 | before: { 384 | create: [convertQueryOverrides()] 385 | } 386 | }); 387 | res = await service.create({ uri: contentUri }, { query: { returnUri: false } }); 388 | assert.strictEqual(res.uri, undefined); 389 | 390 | // test successful remove 391 | res = await service.remove(contentId); 392 | assert.deepStrictEqual(res, { id: contentId }); 393 | 394 | try { 395 | // test failing get 396 | await service.get(contentId); 397 | } catch (err) { 398 | assert.ok(err, '.get() to non-existent id should error'); 399 | done(); 400 | } 401 | }); 402 | }); 403 | 404 | // Cleanup 405 | after(async () => { 406 | await server.close(); 407 | }); 408 | }); 409 | -------------------------------------------------------------------------------- /test/s3.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const BlobService = require('../lib'); 3 | const aws = require('aws-sdk'); 4 | const FsBlobStore = require('s3-blob-store'); 5 | 6 | const { getBase64DataURI } = require('dauria'); 7 | const { bufferToHash } = require('../lib/util'); 8 | const _describe = process.env.S3_BUCKET ? describe : describe.skip; 9 | 10 | _describe('feathers-blob-store-s3', () => { 11 | let s3, blobStore; 12 | const bucket = process.env.S3_BUCKET; 13 | 14 | before(() => { 15 | s3 = new aws.S3(); 16 | blobStore = FsBlobStore({ 17 | client: s3, 18 | bucket: bucket 19 | }); 20 | }); 21 | 22 | it('service operations on S3 storage', async () => { 23 | const store = BlobService({ 24 | Model: blobStore 25 | }); 26 | 27 | const content = Buffer.from('hello world!'); 28 | const contentHash = bufferToHash(content); 29 | const contentType = 'text/plain'; 30 | const contentUri = getBase64DataURI(content, contentType); 31 | const contentExt = 'txt'; 32 | const contentId = `${contentHash}.${contentExt}`; 33 | 34 | let res = await store.create({ uri: contentUri }); 35 | assert.strictEqual(res.id, contentId); 36 | assert.strictEqual(res.uri, contentUri); 37 | assert.strictEqual(res.size, content.length); 38 | assert.strictEqual(res.contentType, contentType); 39 | 40 | // test successful get 41 | res = await store.get(contentId); 42 | assert.strictEqual(res.id, contentId); 43 | assert.strictEqual(res.uri, contentUri); 44 | assert.strictEqual(res.size, content.length); 45 | assert.strictEqual(res.contentType, contentType); 46 | 47 | // test successful remove 48 | res = await store.remove(contentId); 49 | assert.deepStrictEqual(res, { id: contentId }); 50 | 51 | try { 52 | // test failing get 53 | await store.get(contentId); 54 | } catch (err) { 55 | assert.ok(err, '.get() to non-existent id should error'); 56 | } 57 | }); 58 | 59 | it('service operations on S3 storage with custom id', async () => { 60 | const store = BlobService({ 61 | Model: blobStore 62 | }); 63 | 64 | const content = Buffer.from('hello world!'); 65 | const contentHash = bufferToHash(content); 66 | const contentType = 'text/plain'; 67 | const contentUri = getBase64DataURI(content, contentType); 68 | const contentExt = 'txt'; 69 | const contentId = `custom/id/${contentHash}.${contentExt}`; 70 | 71 | let res = await store.create({ id: contentId, uri: contentUri }); 72 | assert.strictEqual(res.id, contentId); 73 | assert.strictEqual(res.uri, contentUri); 74 | assert.strictEqual(res.size, content.length); 75 | assert.strictEqual(res.contentType, contentType); 76 | 77 | // test successful get 78 | res = await store.get(contentId); 79 | assert.strictEqual(res.id, contentId); 80 | assert.strictEqual(res.uri, contentUri); 81 | assert.strictEqual(res.size, content.length); 82 | assert.strictEqual(res.contentType, contentType); 83 | 84 | // test successful remove 85 | res = await store.remove(contentId); 86 | assert.deepStrictEqual(res, { id: contentId }); 87 | 88 | try { 89 | // test failing get 90 | await store.get(contentId); 91 | } catch (err) { 92 | assert.ok(err, '.get() to non-existent id should error'); 93 | } 94 | }); 95 | 96 | it('service operations on S3 storage with a large binary file from a buffer', async () => { 97 | const store = BlobService({ 98 | Model: blobStore 99 | }); 100 | 101 | const content = Buffer.alloc(0.01 * 1024 * 1024); // 20Mb 102 | const contentHash = bufferToHash(content); 103 | const contentType = 'application/octet-stream'; 104 | const contentUri = getBase64DataURI(content, contentType); 105 | const contentExt = 'bin'; 106 | const contentId = `${contentHash}.${contentExt}`; 107 | 108 | let res = await store.create({ buffer: content, contentType }); 109 | assert.strictEqual(res.id, contentId); 110 | assert.strictEqual(res.uri, contentUri); 111 | assert.strictEqual(res.size, content.length); 112 | assert.strictEqual(res.contentType, contentType); 113 | 114 | // test successful get 115 | res = await store.get(contentId); 116 | assert.strictEqual(res.id, contentId); 117 | assert.strictEqual(res.uri, contentUri); 118 | assert.strictEqual(res.size, content.length); 119 | assert.strictEqual(res.contentType, contentType); 120 | 121 | // test successful remove 122 | res = await store.remove(contentId); 123 | assert.deepStrictEqual(res, { id: contentId }); 124 | 125 | try { 126 | // test failing get 127 | await store.get(contentId); 128 | } catch (err) { 129 | assert.ok(err, '.get() to non-existent id should error'); 130 | } 131 | }).timeout(300000); 132 | 133 | it('service operations on S3 storage using VersionId and a versioned bucket', async () => { 134 | const store = BlobService({ 135 | Model: blobStore 136 | }); 137 | 138 | const content = Buffer.from('hello world!'); 139 | const contentType = 'text/plain'; 140 | const contentUri = getBase64DataURI(content, contentType); 141 | const id = 'versioned_test.txt'; 142 | 143 | await store.create({ id, uri: contentUri }); 144 | 145 | // get the version ID for the version we just created (assuming this is a versioned bucket) 146 | const firstVersion = await new Promise(function (resolve, reject) { 147 | blobStore.s3.listObjectVersions({ 148 | Prefix: id, 149 | Bucket: bucket 150 | }, function (err, data) { 151 | if (err) { 152 | reject(err); 153 | } 154 | const latestVersion = data.Versions.find(function (version) { 155 | return version.IsLatest; 156 | }); 157 | resolve(latestVersion.VersionId); 158 | }); 159 | }); 160 | 161 | const content2 = Buffer.from('hello world v2'); 162 | const contentUri2 = getBase64DataURI(content2, contentType); 163 | await store.create({ id, uri: contentUri2 }); 164 | 165 | const resGet1 = await store.get(id, { 166 | query: { VersionId: firstVersion } 167 | }); // the first version is null 168 | const resGet2 = await store.get(id); // the default version is the latest 169 | 170 | assert.strictEqual(resGet1.uri, contentUri); 171 | assert.strictEqual(resGet2.uri, contentUri2); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 3.0 2 | 3 | import { AbstractBlobStore } from 'abstract-blob-store'; 4 | import { Params, Id } from '@feathersjs/feathers'; 5 | 6 | export = feathersBlob; 7 | 8 | declare function feathersBlob(config: feathersBlob.InitOptions): feathersBlob.Service; 9 | 10 | interface UnknownObject { 11 | [propName: string]: any; 12 | } 13 | 14 | interface RemoveResultWithCustomIdParam { 15 | [idParam: string]: string; 16 | } 17 | 18 | interface RemoveResultWithId { 19 | id: string; 20 | } 21 | 22 | interface CreateUriBody { 23 | uri: string; 24 | } 25 | 26 | interface CreateBufferBody { 27 | buffer: Buffer; 28 | contentType: string; 29 | } 30 | 31 | declare namespace feathersBlob { 32 | interface InitOptions { 33 | returnUri?: boolean; 34 | returnBuffer?: boolean; 35 | Model: AbstractBlobStore; 36 | id?: string; 37 | } 38 | 39 | interface BlobParams extends Params { 40 | s3?: UnknownObject; 41 | } 42 | 43 | type CreateBody = CreateUriBody | CreateBufferBody; 44 | 45 | type RemoveResult = RemoveResultWithId | RemoveResultWithCustomIdParam; 46 | 47 | interface BlobResult { 48 | id?: string; 49 | buffer?: Buffer; 50 | uri?: string; 51 | size: number; 52 | [idParam: string]: any; 53 | contentType: string; 54 | } 55 | 56 | interface Service { 57 | extend(obj: UnknownObject): Service; 58 | 59 | get(id: Id, params?: BlobParams): Promise; 60 | 61 | create(body: CreateBody, params?: BlobParams): Promise; 62 | 63 | remove(id: Id, params?: Params): Promise; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /types/test.ts: -------------------------------------------------------------------------------- 1 | import createBlobService, { Service, BlobResult, RemoveResult } from 'feathers-blob'; 2 | import { AbstractBlobStore } from 'abstract-blob-store'; 3 | 4 | const blobStore = {} as any as AbstractBlobStore; 5 | 6 | // $ExpectType Service 7 | const service = createBlobService({ 8 | Model: blobStore, 9 | returnBuffer: true, 10 | id: 'other', 11 | }); 12 | 13 | // $ExpectError 14 | createBlobService({}); 15 | 16 | const buffer = Buffer.from('test'); 17 | 18 | // $ExpectType Promise 19 | service.create({ uri: 'data://...' }, { s3: { ACL: 'public-read' }}); 20 | // $ExpectType Promise 21 | service.create({ buffer, contentType: 'text/plain' }, { s3: { ACL: 'public-read' }}); 22 | 23 | // $ExpectError 24 | service.create({ buffer }); // missing contentType 25 | 26 | // $ExpectError 27 | service.create({ other: 'something' }); // missing uri or buffer 28 | 29 | // $ExpectType Promise 30 | service.get('name.txt', { s3: { ACL: 'public-read' }}); 31 | 32 | // $ExpectType Promise 33 | service.remove('name.txt'); 34 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "strictFunctionTypes": true, 9 | "noEmit": true, 10 | "target": "es6", 11 | 12 | // needed for test.ts 13 | "esModuleInterop": true, 14 | // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". 15 | // If the library is global (cannot be imported via `import` or `require`), leave this out. 16 | "baseUrl": ".", 17 | "paths": { "feathers-blob": ["."] } 18 | } 19 | } 20 | --------------------------------------------------------------------------------