├── .codacy.yaml ├── .codeclimate.yml ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── examples ├── api.service.js ├── data │ └── tmp │ │ └── .gitignore ├── docker-compose.yml ├── minio.service.js └── minio │ └── data │ └── .gitignore ├── index.js ├── package.json ├── src ├── errors │ ├── MinioInitializationError.js │ ├── MinioPingError.js │ └── index.js └── service.js ├── test └── unit │ ├── errors │ ├── MinioInitializationError.spec.js │ └── MinioPingError.spec.js │ ├── index.spec.js │ └── service │ ├── actions │ ├── bucketExists.spec.js │ ├── copyObject.spec.js │ ├── fGetObject.spec.js │ ├── fPutObject.spec.js │ ├── getObject.spec.js │ ├── getPartialObject.spec.js │ ├── listBuckets.spec.js │ ├── listIncompleteUploads.spec.js │ ├── listObjects.spec.js │ ├── listObjectsV2.spec.js │ ├── makeBucket.spec.js │ ├── presignedGetObject.spec.js │ ├── presignedPostPolicy.spec.js │ ├── presignedPutObject.spec.js │ ├── presignedUrl.spec.js │ ├── putObject.spec.js │ ├── removeBucket.spec.js │ ├── removeIncompleteUpload.spec.js │ ├── removeObject.spec.js │ ├── removeObjects.spec.js │ └── statObject.spec.js │ ├── created.spec.js │ ├── methods │ ├── createMinioClient.spec.js │ └── ping.spec.js │ ├── name.spec.js │ ├── settings.spec.js │ └── stopped.spec.js └── yarn.lock /.codacy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - 'README.md' 4 | - 'CHANGELOG.md' 5 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | checks: 4 | argument-count: 5 | enabled: false 6 | complex-logic: 7 | enabled: false 8 | file-lines: 9 | enabled: false 10 | method-complexity: 11 | enabled: false 12 | method-count: 13 | enabled: false 14 | method-lines: 15 | enabled: false 16 | nested-control-flow: 17 | enabled: false 18 | return-statements: 19 | enabled: false 20 | similar-code: 21 | enabled: false 22 | identical-code: 23 | enabled: false 24 | 25 | plugins: 26 | duplication: 27 | enabled: false 28 | config: 29 | languages: 30 | - javascript 31 | eslint: 32 | enabled: true 33 | channel: "eslint-4" 34 | fixme: 35 | enabled: true 36 | 37 | exclude_paths: 38 | - test/ 39 | - benchmark/ 40 | - examples/ 41 | - typings/ 42 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = tab 11 | indent_size = 4 12 | space_after_anon_function = true 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [{package,bower}.json] 24 | indent_style = space 25 | indent_size = 2 26 | 27 | [*.js] 28 | quote_type = "double" 29 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | "env": { 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "jquery": false, 8 | "jest": true, 9 | "jasmine": true 10 | }, 11 | "extends": "eslint:recommended", 12 | "parserOptions": { 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "indent": [ 17 | "warn", 18 | "tab" 19 | ], 20 | "quotes": [ 21 | "warn", 22 | "double" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ], 28 | "no-var": [ 29 | "error" 30 | ], 31 | "no-console": [ 32 | "off" 33 | ], 34 | "no-unused-vars": [ 35 | "warn" 36 | ] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | coverage/ 4 | npm-debug.log 5 | stats.json 6 | yarn-error.log 7 | .idea 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | benchmarks 4 | coverage 5 | dev 6 | docs 7 | examples 8 | typings 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | node_js: 6 | - "10" 7 | - "8" 8 | after_success: 9 | - npm run coverall 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ----------------------------- 2 | 3 | # 1.0.0 (2018-12-17) 4 | 5 | ## New 6 | 7 | ### Bucket Management 8 | The service exposes actions for creating, listing and removing buckets 9 | 10 | ### Object Management 11 | The service exposes actions for creating, listing and removing objects 12 | 13 | ### Presigned URL Management 14 | The service exposes actions for creating Presigned URLs for the S3 Backend 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.18-alpine 2 | 3 | # Install git for jest watcher for virus scanning 4 | RUN apk add --update --no-cache git 5 | 6 | # Copy over all Source files 7 | COPY . /usr/src 8 | 9 | # Change the working directory to the source files 10 | WORKDIR /usr/src 11 | 12 | # Set the entrypoint to yarn script runner 13 | ENTRYPOINT ["yarn","run"] 14 | 15 | # Execute the ci task by default 16 | CMD ["ci"] 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 designtesbrot 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Moleculer logo](http://moleculer.services/images/banner.png)](https://github.com/moleculerjs/moleculer) 2 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdesigntesbrot%2Fmoleculer-minio.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdesigntesbrot%2Fmoleculer-minio?ref=badge_shield) 3 | 4 | [![Build Status](https://travis-ci.com/designtesbrot/moleculer-minio.svg?branch=master)](https://travis-ci.com/designtesbrot/moleculer-minio) 5 | [![Coverage Status](https://coveralls.io/repos/github/designtesbrot/moleculer-minio/badge.svg?branch=master)](https://coveralls.io/github/designtesbrot/moleculer-minio?branch=master) 6 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/7f8245b6a42249a7b3f5de62d88a9ef4)](https://www.codacy.com/app/designtesbrot/moleculer-minio?utm_source=github.com&utm_medium=referral&utm_content=designtesbrot/moleculer-minio&utm_campaign=Badge_Grade) 7 | [![Maintainability](https://api.codeclimate.com/v1/badges/92a1e223f18762feb513/maintainability)](https://codeclimate.com/github/designtesbrot/moleculer-minio/maintainability) 8 | [![Known Vulnerabilities](https://snyk.io/test/github/designtesbrot/moleculer-minio/badge.svg)](https://snyk.io/test/github/designtesbrot/moleculer-minio) 9 | [![npm version](https://badge.fury.io/js/moleculer-minio.svg)](https://badge.fury.io/js/moleculer-minio) 10 | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/39e921971bba8ef74126) 11 | 12 | # Minio Service for the Moleculer framework 13 | 14 | This Services provides actions for managing buckets and objects in an AWS S3 or [Minio](https://www.minio.io) powered backend. It 15 | utilizes the file streaming capabilities of the moleculer framework 16 | 17 | ## Features 18 | 19 | The following List details which features are implemented 20 | 21 | - Bucket Management (Create, Delete, List) 22 | - Object Management (Put, List, Delete, Stat) 23 | - Presigned URL Management (Generate presigned URLs and Post Policy signed URLs) 24 | 25 | ## Roadmap 26 | 27 | The following List details which features are yet to be implemented 28 | 29 | - Caching 30 | - Broadcasting Bucket Notification as moleculer events 31 | 32 | ## Requirements 33 | 34 | This service relies on [Minio](https://www.minio.io) or a generic AWS S3 endpoint available. Make sure to configure the service properly in 35 | order to connect to your endpoint. Using Minio Gatewy capabilities, you can easily fan out to Azure and the like. This repository includes 36 | an example, which itself includes a docker-compose file connecting to an inlcuded minio backend. 37 | 38 | ## Install 39 | 40 | This package is available in the npm-registry. In order to use it simply install it with yarn (or npm): 41 | 42 | ```bash 43 | yarn add moleculer-minio 44 | ``` 45 | 46 | ## Usage 47 | 48 | To make use of this Service, simply require it and create a new service: 49 | 50 | ```js 51 | const fs = require("fs"); 52 | let { ServiceBroker } = require("moleculer"); 53 | let MinioService = require("moleculer-minio"); 54 | 55 | let broker = new ServiceBroker({ logger: console }); 56 | 57 | // Create a service 58 | broker.createService({ 59 | mixins: MinioService, 60 | settings: { 61 | // ... see settings for connectivity 62 | } 63 | }); 64 | 65 | // Start server 66 | broker.start() 67 | .then(() => broker.call('minio.makeBucket', {bucketName: 'my-bucket', region: 'us-east-1'})) 68 | .then(() => 69 | broker.call( 70 | 'minio.putObject', 71 | fs.createReadStream('./archive.tar.gz'), 72 | { 73 | meta: { 74 | bucketName: 'my-bucket', 75 | objectName: 'my-object.tar.gz', 76 | metaData: { 77 | foo: 'bar' 78 | } 79 | } 80 | } 81 | )) 82 | .then(() => 83 | broker.call('minio.presignedGetObject', { 84 | bucketName: 'my-bucket', 85 | objectName: 'my-object.tar.gz', 86 | expires: 600 87 | })) 88 | .then(console.log); 89 | ``` 90 | 91 | For a more indepth example checkout out the `examples folder`. It includes a docker-compose file, running `docker-compose up` will boot a broker with a minio service, a connected minio backend 92 | and an API Gateway to upload files to. This project includes a [published postman collection](https://app.getpostman.com/run-collection/39e921971bba8ef74126) enabling you to quickly explore the service in your local environment. 93 | 94 | ## Settings 95 | 96 | 97 | | Property | Type | Default | Description | 98 | | -------- | ---- | ------- | ----------- | 99 | | `endPoint` | `String` | **required** | The Hostname minio is running on and available at. Hostname or IP-Address | 100 | | `port` | `Number` | **required** | TCP/IP port number minio is listening on. Default value set to 80 for HTTP and 443 for HTTPs. | 101 | | `useSSL` | `Boolean` | `null` | If set to true, https is used instead of http. Default is true. | 102 | | `accessKey` | `String` | **required** | The AccessKey to use when connecting to minio | 103 | | `secretKey` | `String` | **required** | The SecretKey to use when connecting to minio | 104 | | `region` | `String` | `null` | Set this value to override region cache | 105 | | `transport` | `String` | `null` | Set this value to pass in a custom transport. (Optional) | 106 | | `sessionToken` | `String` | `null` | Set this value to provide x-amz-security-token (AWS S3 specific). (Optional) | 107 | | `minioHealthCheckInterval` | `Number` | `null` | This service will perform a periodic healthcheck of Minio. Use this setting to configure the inverval in which the healthcheck is performed. Set to `0` to turn healthcheks of | 108 | 109 | 110 | 111 | 122 | 123 | ## Actions 124 | 125 | 126 | ## `makeBucket` 127 | 128 | Creates a new Bucket 129 | 130 | ### Parameters 131 | | Property | Type | Default | Description | 132 | | -------- | ---- | ------- | ----------- | 133 | | `bucketName` | `string` | **required** | The name of the bucket | 134 | | `region` | `string` | **required** | The region to create the bucket in. Defaults to "us-east-1" | 135 | 136 | ### Results 137 | **Type:** `PromiseLike.<(undefined|Error)>` 138 | 139 | 140 | 141 | 142 | ## `listBuckets` 143 | 144 | Lists all buckets. 145 | 146 | ### Parameters 147 | | Property | Type | Default | Description | 148 | | -------- | ---- | ------- | ----------- | 149 | *No input parameters.* 150 | 151 | ### Results 152 | **Type:** `PromiseLike.<(Array.|Error)>` 153 | 154 | 155 | 156 | 157 | ## `bucketExists` 158 | 159 | Checks if a bucket exists. 160 | 161 | ### Parameters 162 | | Property | Type | Default | Description | 163 | | -------- | ---- | ------- | ----------- | 164 | | `bucketName` | `string` | **required** | Name of the bucket | 165 | 166 | ### Results 167 | **Type:** `PromiseLike.<(boolean|Error)>` 168 | 169 | 170 | 171 | 172 | ## `removeBucket` 173 | 174 | Removes a bucket. 175 | 176 | ### Parameters 177 | | Property | Type | Default | Description | 178 | | -------- | ---- | ------- | ----------- | 179 | | `bucketName` | `string` | **required** | Name of the bucket | 180 | 181 | ### Results 182 | **Type:** `PromiseLike.<(boolean|Error)>` 183 | 184 | 185 | 186 | 187 | ## `listObjects` 188 | 189 | Lists all objects in a bucket. 190 | 191 | ### Parameters 192 | | Property | Type | Default | Description | 193 | | -------- | ---- | ------- | ----------- | 194 | | `bucketName` | `string` | **required** | Name of the bucket | 195 | | `prefix` | `string` | **required** | The prefix of the objects that should be listed (optional, default ''). | 196 | | `recursive` | `boolean` | **required** | `true` indicates recursive style listing and false indicates directory style listing delimited by '/'. (optional, default `false`). | 197 | 198 | ### Results 199 | **Type:** `PromiseLike.<(Array.|Error)>` 200 | 201 | 202 | 203 | 204 | ## `listObjectsV2` 205 | 206 | Lists all objects in a bucket using S3 listing objects V2 API 207 | 208 | ### Parameters 209 | | Property | Type | Default | Description | 210 | | -------- | ---- | ------- | ----------- | 211 | | `bucketName` | `string` | **required** | Name of the bucket | 212 | | `prefix` | `string` | **required** | The prefix of the objects that should be listed (optional, default ''). | 213 | | `recursive` | `boolean` | **required** | `true` indicates recursive style listing and false indicates directory style listing delimited by '/'. (optional, default `false`). | 214 | | `startAfter` | `string` | **required** | Specifies the object name to start after when listing objects in a bucket. (optional, default ''). | 215 | 216 | ### Results 217 | **Type:** `PromiseLike.<(Array.|Error)>` 218 | 219 | 220 | 221 | 222 | ## `listIncompleteUploads` 223 | 224 | Lists partially uploaded objects in a bucket. 225 | 226 | ### Parameters 227 | | Property | Type | Default | Description | 228 | | -------- | ---- | ------- | ----------- | 229 | | `bucketName` | `string` | **required** | Name of the bucket | 230 | | `prefix` | `string` | **required** | The prefix of the objects that should be listed (optional, default ''). | 231 | | `recursive` | `boolean` | **required** | `true` indicates recursive style listing and false indicates directory style listing delimited by '/'. (optional, default `false`). | 232 | 233 | ### Results 234 | **Type:** `PromiseLike.<(Array.|Error)>` 235 | 236 | 237 | 238 | 239 | ## `getObject` 240 | 241 | Downloads an object as a stream. 242 | 243 | ### Parameters 244 | | Property | Type | Default | Description | 245 | | -------- | ---- | ------- | ----------- | 246 | | `bucketName` | `string` | **required** | Name of the bucket | 247 | | `objectName` | `string` | **required** | Name of the object. | 248 | 249 | ### Results 250 | **Type:** `PromiseLike.<(ReadableStream|Error)>` 251 | 252 | 253 | 254 | 255 | ## `getPartialObject` 256 | 257 | Downloads the specified range bytes of an object as a stream. 258 | 259 | ### Parameters 260 | | Property | Type | Default | Description | 261 | | -------- | ---- | ------- | ----------- | 262 | | `bucketName` | `string` | **required** | Name of the bucket. | 263 | | `objectName` | `string` | **required** | Name of the object. | 264 | | `offset` | `number` | **required** | `offset` of the object from where the stream will start. | 265 | | `length` | `number` | **required** | `length` of the object that will be read in the stream (optional, if not specified we read the rest of the file from the offset). | 266 | 267 | ### Results 268 | **Type:** `PromiseLike.<(ReadableStream|Error)>` 269 | 270 | 271 | 272 | 273 | ## `fGetObject` 274 | 275 | Downloads and saves the object as a file in the local filesystem. 276 | 277 | ### Parameters 278 | | Property | Type | Default | Description | 279 | | -------- | ---- | ------- | ----------- | 280 | | `bucketName` | `string` | **required** | Name of the bucket. | 281 | | `objectName` | `string` | **required** | Name of the object. | 282 | | `filePath` | `string` | **required** | Path on the local filesystem to which the object data will be written. | 283 | 284 | ### Results 285 | **Type:** `PromiseLike.<(undefined|Error)>` 286 | 287 | 288 | 289 | 290 | ## `putObject` 291 | 292 | Uploads an object from a stream/Buffer. 293 | 294 | ### Parameters 295 | | Property | Type | Default | Description | 296 | | -------- | ---- | ------- | ----------- | 297 | | `params` | `ReadableStream` | **required** | Readable stream. | 298 | | `bucketName` | `string` | **required** | Name of the bucket. | 299 | | `objectName` | `string` | **required** | Name of the object. | 300 | | `size` | `number` | **required** | Size of the object (optional). | 301 | | `metaData` | `object` | **required** | metaData of the object (optional). | 302 | 303 | ### Results 304 | **Type:** `PromiseLike.<(undefined|Error)>` 305 | 306 | 307 | 308 | 309 | ## `fPutObject` 310 | 311 | Uploads contents from a file to objectName. 312 | 313 | ### Parameters 314 | | Property | Type | Default | Description | 315 | | -------- | ---- | ------- | ----------- | 316 | | `bucketName` | `string` | **required** | Name of the bucket. | 317 | | `objectName` | `string` | **required** | Name of the object. | 318 | | `filePath` | `string` | **required** | Path of the file to be uploaded. | 319 | | `metaData` | `object` | **required** | metaData of the object (optional). | 320 | 321 | ### Results 322 | **Type:** `PromiseLike.<(undefined|Error)>` 323 | 324 | 325 | 326 | 327 | ## `copyObject` 328 | 329 | Copy a source object into a new object in the specified bucket. 330 | 331 | ### Parameters 332 | | Property | Type | Default | Description | 333 | | -------- | ---- | ------- | ----------- | 334 | | `bucketName` | `string` | **required** | Name of the bucket. | 335 | | `objectName` | `string` | **required** | Name of the object. | 336 | | `sourceObject` | `string` | **required** | Path of the file to be copied. | 337 | | `conditions` | `object` | **required** | Conditions to be satisfied before allowing object copy. | 338 | | `metaData` | `object` | **required** | metaData of the object (optional). | 339 | 340 | ### Results 341 | **Type:** `PromiseLike.<({etag: {string}, lastModified: {string}}|Error)>` 342 | 343 | 344 | 345 | 346 | ## `statObject` 347 | 348 | Gets metadata of an object. 349 | 350 | ### Parameters 351 | | Property | Type | Default | Description | 352 | | -------- | ---- | ------- | ----------- | 353 | | `bucketName` | `string` | **required** | Name of the bucket. | 354 | | `objectName` | `string` | **required** | Name of the object. | 355 | 356 | ### Results 357 | **Type:** `PromiseLike.<({size: {number}, metaData: {object}, lastModified: {string}, etag: {string}}|Error)>` 358 | 359 | 360 | 361 | 362 | ## `removeObject` 363 | 364 | Removes an Object 365 | 366 | ### Parameters 367 | | Property | Type | Default | Description | 368 | | -------- | ---- | ------- | ----------- | 369 | | `bucketName` | `string` | **required** | Name of the bucket. | 370 | | `objectName` | `string` | **required** | Name of the object. | 371 | 372 | ### Results 373 | **Type:** `PromiseLike.<(undefined|Error)>` 374 | 375 | 376 | 377 | 378 | ## `removeObjects` 379 | 380 | Removes a list of Objects 381 | 382 | ### Parameters 383 | | Property | Type | Default | Description | 384 | | -------- | ---- | ------- | ----------- | 385 | | `bucketName` | `string` | **required** | Name of the bucket. | 386 | | `objectNames` | `Array.` | **required** | Names of the objects. | 387 | 388 | ### Results 389 | **Type:** `PromiseLike.<(undefined|Error)>` 390 | 391 | 392 | 393 | 394 | ## `removeIncompleteUpload` 395 | 396 | Removes a partially uploaded object. 397 | 398 | ### Parameters 399 | | Property | Type | Default | Description | 400 | | -------- | ---- | ------- | ----------- | 401 | | `bucketName` | `string` | **required** | Name of the bucket. | 402 | | `objectName` | `string` | **required** | Name of the object. | 403 | 404 | ### Results 405 | **Type:** `PromiseLike.<(undefined|Error)>` 406 | 407 | 408 | 409 | 410 | ## `presignedUrl` 411 | 412 | Generates a presigned URL for the provided HTTP method, 'httpMethod'. Browsers/Mobile clients may point to this URL to directly download objects even if the bucket is private. This 413 | presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days. 414 | 415 | ### Parameters 416 | | Property | Type | Default | Description | 417 | | -------- | ---- | ------- | ----------- | 418 | | `httpMethod` | `string` | **required** | The HTTP-Method (eg. `GET`). | 419 | | `bucketName` | `string` | **required** | Name of the bucket. | 420 | | `objectName` | `string` | **required** | Name of the object. | 421 | | `expires` | `number` | **required** | Expiry time in seconds. Default value is 7 days. (optional) | 422 | | `reqParams` | `object` | **required** | request parameters. (optional) | 423 | | `requestDate` | `string` | **required** | An ISO date string, the url will be issued at. Default value is now. (optional) | 424 | 425 | ### Results 426 | **Type:** `PromiseLike.<(String|Error)>` 427 | 428 | 429 | 430 | 431 | ## `presignedGetObject` 432 | 433 | Generates a presigned URL for HTTP GET operations. Browsers/Mobile clients may point to this URL to directly download objects even if the bucket is private. This presigned URL can have an 434 | associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days. 435 | 436 | ### Parameters 437 | | Property | Type | Default | Description | 438 | | -------- | ---- | ------- | ----------- | 439 | | `bucketName` | `string` | **required** | Name of the bucket. | 440 | | `objectName` | `string` | **required** | Name of the object. | 441 | | `expires` | `number` | **required** | Expiry time in seconds. Default value is 7 days. (optional) | 442 | | `reqParams` | `object` | **required** | request parameters. (optional) | 443 | | `requestDate` | `string` | **required** | An ISO date string, the url will be issued at. Default value is now. (optional) | 444 | 445 | ### Results 446 | **Type:** `PromiseLike.<(String|Error)>` 447 | 448 | 449 | 450 | 451 | ## `presignedPutObject` 452 | 453 | Generates a presigned URL for HTTP PUT operations. Browsers/Mobile clients may point to this URL to upload objects directly to a bucket even if it is private. This presigned URL can have 454 | an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days. 455 | 456 | ### Parameters 457 | | Property | Type | Default | Description | 458 | | -------- | ---- | ------- | ----------- | 459 | | `bucketName` | `string` | **required** | Name of the bucket. | 460 | | `objectName` | `string` | **required** | Name of the object. | 461 | | `expires` | `number` | **required** | Expiry time in seconds. Default value is 7 days. (optional) | 462 | 463 | ### Results 464 | **Type:** `PromiseLike.<(String|Error)>` 465 | 466 | 467 | 468 | 469 | ## `presignedPostPolicy` 470 | 471 | Allows setting policy conditions to a presigned URL for POST operations. Policies such as bucket name to receive object uploads, key name prefixes, expiry policy may be set. 472 | 473 | ### Parameters 474 | | Property | Type | Default | Description | 475 | | -------- | ---- | ------- | ----------- | 476 | | `policy` | `object` | **required** | Policy object created by minioClient.newPostPolicy() | 477 | 478 | ### Results 479 | **Type:** `PromiseLike.<({postURL: {string}, formData: {object}}|Error)>` 480 | 481 | 482 | 483 | 484 | 485 | 486 | 521 | 522 | # Methods 523 | 524 | 525 | ## `createMinioClient` 526 | 527 | Creates and returns a new Minio client 528 | 529 | ### Parameters 530 | | Property | Type | Default | Description | 531 | | -------- | ---- | ------- | ----------- | 532 | *No input parameters.* 533 | 534 | ### Results 535 | **Type:** `Client` 536 | 537 | 538 | 539 | 540 | 541 | 542 | 577 | 578 | ## Test 579 | ``` 580 | $ docker-compose exec package yarn test 581 | ``` 582 | 583 | In development with watching 584 | 585 | ``` 586 | $ docker-compose up 587 | ``` 588 | 589 | ## License 590 | moleculer-minio is available under the [MIT license](https://tldrlegal.com/license/mit-license). 591 | 592 | 593 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdesigntesbrot%2Fmoleculer-minio.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdesigntesbrot%2Fmoleculer-minio?ref=badge_large) 594 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | package: 5 | build: ./ 6 | command: ci 7 | volumes: 8 | - ./:/usr/src 9 | -------------------------------------------------------------------------------- /examples/api.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const multer = require('multer'); 4 | const fs = require('fs'); 5 | const nodeRes = require('node-res'); 6 | const {ServiceBroker} = require('moleculer'); 7 | const ApiGatewayService = require('moleculer-web'); 8 | 9 | const upload = multer({dest: '/var/tmp/'}); 10 | 11 | // Create broker 12 | let broker = new ServiceBroker({ 13 | logger: console, 14 | transporter: 'nats://nats:4222' 15 | }); 16 | 17 | // Load services 18 | broker.createService({ 19 | mixins: ApiGatewayService, 20 | settings: { 21 | routes: [ 22 | { 23 | path: '/upload', 24 | use: [ 25 | upload.single('file') 26 | ], 27 | bodyParsers: { 28 | json: false, 29 | urlencoded: false 30 | }, 31 | aliases: { 32 | 'POST /'(req, res) { 33 | let stream = fs.createReadStream(req.file.path); 34 | return this.broker.call('minio.putObject', stream, {meta: req.body}) 35 | .then(result => { 36 | this.logger.info('Object saved', result); 37 | nodeRes.send(req, res, result); 38 | }).catch(err => { 39 | this.logger.error('Object save error', err); 40 | this.sendError(req, res, err); 41 | }); 42 | } 43 | } 44 | }, 45 | { 46 | path: '/putfrompath', 47 | bodyParsers: { 48 | json: true 49 | }, 50 | aliases: { 51 | 'POST /'(req, res) { 52 | return this.broker.call('minio.fPutObject', req.body) 53 | .then(result => { 54 | this.logger.info('Object saved', result); 55 | nodeRes.send(req, res, result); 56 | }).catch(err => { 57 | this.logger.error('Object save error', err); 58 | this.sendError(req, res, err); 59 | }); 60 | } 61 | } 62 | }, 63 | { 64 | path: '/presignedurl', 65 | bodyParsers: { 66 | json: true 67 | }, 68 | aliases: { 69 | 'POST /'(req, res) { 70 | const options = Object.assign({}, req.body, {requestDate: new Date()}); 71 | return this.broker.call('minio.presignedUrl', options) 72 | .then(result => { 73 | nodeRes.send(req, res, result); 74 | }).catch(err => { 75 | this.sendError(req, res, err); 76 | }); 77 | } 78 | } 79 | }, 80 | { 81 | path: '/presignedgetobject', 82 | bodyParsers: { 83 | json: true 84 | }, 85 | aliases: { 86 | 'POST /'(req, res) { 87 | const options = Object.assign({}, req.body, {requestDate: new Date()}); 88 | return this.broker.call('minio.presignedGetObject', options) 89 | .then(result => { 90 | nodeRes.send(req, res, result); 91 | }).catch(err => { 92 | this.sendError(req, res, err); 93 | }); 94 | } 95 | } 96 | }, 97 | { 98 | path: '/presignedputobject', 99 | bodyParsers: { 100 | json: true 101 | }, 102 | aliases: { 103 | 'POST /'(req, res) { 104 | const options = Object.assign({}, req.body, {requestDate: new Date()}); 105 | return this.broker.call('minio.presignedPutObject', options) 106 | .then(result => { 107 | nodeRes.send(req, res, result); 108 | }).catch(err => { 109 | this.sendError(req, res, err); 110 | }); 111 | } 112 | } 113 | }, 114 | { 115 | path: '/presignedpostpolicy', 116 | bodyParsers: { 117 | json: true 118 | }, 119 | aliases: { 120 | 'POST /'(req, res) { 121 | return this.broker.call('minio.presignedPostPolicy', req.body) 122 | .then(result => { 123 | nodeRes.send(req, res, result); 124 | }).catch(err => { 125 | this.sendError(req, res, err); 126 | }); 127 | } 128 | } 129 | }, 130 | { 131 | path: '/', 132 | whitelist: [ 133 | // Access any actions in 'minio' service 134 | 'minio.*' 135 | ] 136 | } 137 | ] 138 | } 139 | }); 140 | 141 | process.once('SIGUSR2', function() { 142 | broker.stop().then(() => { 143 | process.kill(process.pid, 'SIGUSR2'); 144 | }); 145 | }); 146 | 147 | // Start server 148 | broker.start().then(() => broker.repl()); 149 | -------------------------------------------------------------------------------- /examples/data/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | api: 5 | build: ./../ 6 | entrypoint: ./node_modules/.bin/nodemon -L 7 | command: examples/api.service.js 8 | volumes: 9 | - ./../:/usr/src 10 | ports: 11 | - 3000:3000 12 | service: 13 | build: ./../ 14 | entrypoint: ./node_modules/.bin/nodemon -L 15 | command: examples/minio.service.js 16 | volumes: 17 | - ./../:/usr/src 18 | - ./data/tmp:/tmp 19 | minio: 20 | image: minio/minio 21 | environment: 22 | MINIO_ACCESS_KEY: AKIAIOSFODNN7EXAMPLE 23 | MINIO_SECRET_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY 24 | volumes: 25 | - ./minio/data:/data 26 | ports: 27 | - 9000:9000 28 | command: server /data 29 | nats: 30 | image: nats 31 | -------------------------------------------------------------------------------- /examples/minio.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {ServiceBroker} = require('moleculer'); 4 | const MinioService = require('./..'); 5 | 6 | // Create broker 7 | let broker = new ServiceBroker({ 8 | logger: console, 9 | transporter: 'nats://nats:4222', 10 | }); 11 | 12 | // Load services 13 | broker.createService({ 14 | mixins: MinioService, 15 | settings: { 16 | endPoint: 'minio', 17 | port: 9000, 18 | useSSL: false, 19 | accessKey: 'AKIAIOSFODNN7EXAMPLE', 20 | secretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', 21 | }, 22 | }); 23 | 24 | process.once('SIGUSR2', function() { 25 | broker.stop().then(() => { 26 | process.kill(process.pid, 'SIGUSR2'); 27 | }); 28 | }); 29 | 30 | // Start server 31 | broker.start().then(() => broker.repl()); 32 | -------------------------------------------------------------------------------- /examples/minio/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./src/service"); 2 | 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moleculer-minio", 3 | "version": "2.0.0", 4 | "description": "A minio sdk wrapper as a service for the moleculer framework", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/designtesbrot/moleculer-minio" 9 | }, 10 | "author": "caspar.bauer0@gmail.com", 11 | "license": "MIT", 12 | "scripts": { 13 | "dev": "yarn install && nodemon examples/index.js", 14 | "demo": "yarn install && node examples/index.js", 15 | "ci": "yarn install && jest --watch", 16 | "test": "yarn install && jest --coverage --detectOpenHandles", 17 | "lint": "yarn install && eslint --ext=.js --fix src", 18 | "deps": "yarn install && yarn-check -u", 19 | "postdeps": "yarn test", 20 | "coverall": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 21 | "docgen": "node ./node_modules/moleculer-docgen src/service.js" 22 | }, 23 | "keywords": [ 24 | "minio", 25 | "s3", 26 | "microservices", 27 | "microservice", 28 | "moleculer" 29 | ], 30 | "devDependencies": { 31 | "coveralls": "3.0.2", 32 | "eslint": "5.10.0", 33 | "jest": "23.6.0", 34 | "jest-cli": "23.6.0", 35 | "moleculer": "^0.13.3", 36 | "moleculer-docgen": "^0.2.1", 37 | "moleculer-repl": "^0.5.2", 38 | "moleculer-web": "^0.8.2", 39 | "multer": "^1.4.1", 40 | "nats": "^1.0.1", 41 | "node-res": "^5.0.1", 42 | "nodemon": "1.18.9", 43 | "yarn-check": "0.0.3" 44 | }, 45 | "peerDependencies": { 46 | "moleculer": ">= 0.13.0" 47 | }, 48 | "dependencies": { 49 | "minio": "^7.0.21", 50 | "ramda": "0.27.1", 51 | "ramda-adjunct": "^2.35.0" 52 | }, 53 | "engines": { 54 | "node": ">= 14.x.x" 55 | }, 56 | "jest": { 57 | "coverageDirectory": "../coverage", 58 | "testEnvironment": "node", 59 | "rootDir": "./src", 60 | "roots": [ 61 | "../test" 62 | ], 63 | "coveragePathIgnorePatterns": [ 64 | "/node_modules/", 65 | "/test/services/" 66 | ], 67 | "moduleDirectories": [ 68 | "node_modules", 69 | "" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/errors/MinioInitializationError.js: -------------------------------------------------------------------------------- 1 | const {MoleculerError} = require("moleculer/src/errors"); 2 | 3 | /** 4 | * Error that should be thrown when the Minio Service can not be Initialized 5 | * 6 | * @class MinioInitializationError 7 | * @extends {MoleculerError} 8 | */ 9 | module.exports = class MinioInitializationError extends MoleculerError { 10 | /** 11 | * Creates an instance of MinioInitializationError. 12 | * 13 | * @param {String?} message 14 | * @param {Number?} code 15 | * @param {String?} type 16 | * @param {any} data 17 | * 18 | * @memberof MinioInitializationError 19 | */ 20 | constructor( 21 | message = "Minio can not be initialized", code = 500, 22 | type = "MINIO_INITIALIZATION_ERROR", data = {}) { 23 | super(message); 24 | this.code = code; 25 | this.type = type; 26 | this.data = data; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/errors/MinioPingError.js: -------------------------------------------------------------------------------- 1 | const {MoleculerRetryableError} = require("moleculer/src/errors"); 2 | 3 | /** 4 | * Error that should be thrown when the Minio Backend can not be pinged 5 | * 6 | * @class MinioPingError 7 | * @extends {MoleculerRetryableError} 8 | */ 9 | module.exports = class MinioPingError extends MoleculerRetryableError { 10 | /** 11 | * Creates an instance of MinioPingError. 12 | * 13 | * @param {String?} message 14 | * @param {Number?} code 15 | * @param {String?} type 16 | * @param {any} data 17 | * 18 | * @memberof MinioPingError 19 | */ 20 | constructor( 21 | message = "Minio Backend not reachable", code = 502, 22 | type = "MINIO_PING_ERROR", data = {}) { 23 | super(message); 24 | this.code = code; 25 | this.type = type; 26 | this.data = data; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/errors/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | MinioPingError: require("./MinioPingError"), 3 | MinioInitializationError: require("./MinioInitializationError"), 4 | }; 5 | -------------------------------------------------------------------------------- /src/service.js: -------------------------------------------------------------------------------- 1 | const Minio = require("minio"); 2 | const {MinioPingError, MinioInitializationError} = require("./errors"); 3 | const {isString, isUndefined} = require("ramda-adjunct"); 4 | /** 5 | * Service mixin for managing files in a Minio S3 backend 6 | * 7 | * @name moleculer-minio 8 | * @module Service 9 | */ 10 | module.exports = { 11 | // Service name 12 | name: "minio", 13 | 14 | // Default settings 15 | settings: { 16 | /** @type {String} The Hostname minio is running on and available at. Hostname or IP-Address */ 17 | endPoint: undefined, 18 | /** @type {Number} TCP/IP port number minio is listening on. Default value set to 80 for HTTP and 443 for HTTPs.*/ 19 | port: undefined, 20 | /** @type {Boolean?} If set to true, https is used instead of http. Default is true.*/ 21 | useSSL: true, 22 | /** @type {String} The AccessKey to use when connecting to minio */ 23 | accessKey: undefined, 24 | /** @type {String} The SecretKey to use when connecting to minio */ 25 | secretKey: undefined, 26 | /** @type {String?} Set this value to override region cache*/ 27 | region: undefined, 28 | /** @type {String?} Set this value to pass in a custom transport. (Optional)*/ 29 | transport: undefined, 30 | /** @type {String?} Set this value to provide x-amz-security-token (AWS S3 specific). (Optional)*/ 31 | sessionToken: undefined, 32 | /** @type {Number?} This service will perform a periodic healthcheck of Minio. Use this setting to configure the inverval in which the healthcheck is performed. Set to `0` to turn healthcheks of */ 33 | minioHealthCheckInterval: 5000 34 | }, 35 | 36 | methods: { 37 | /** 38 | * Creates and returns a new Minio client 39 | * 40 | * @methods 41 | * 42 | * @returns {Client} 43 | */ 44 | createMinioClient() { 45 | return new Minio.Client({ 46 | endPoint: this.settings.endPoint, 47 | port: this.settings.port, 48 | useSSL: this.settings.useSSL, 49 | accessKey: this.settings.accessKey, 50 | secretKey: this.settings.secretKey, 51 | region: this.settings.region, 52 | transport: this.settings.transport, 53 | sessionToken: this.settings.sessionToken 54 | }); 55 | }, 56 | /** 57 | * Pings the configured minio backend 58 | * 59 | * @param {number} timeout - Amount of miliseconds to wait for a ping response 60 | * @returns {PromiseLike} 61 | */ 62 | ping({timeout = 5000} = {}) { 63 | return this.Promise.race([ 64 | this.client.listBuckets().then(() => true), 65 | this.Promise.delay(timeout).then(() => {throw new MinioPingError();}) 66 | ]); 67 | } 68 | }, 69 | actions: { 70 | /** 71 | * Creates a new Bucket 72 | * 73 | * @actions 74 | * 75 | * @param {string} bucketName - The name of the bucket 76 | * @param {string} region - The region to create the bucket in. Defaults to "us-east-1" 77 | * 78 | * @returns {PromiseLike} 79 | */ 80 | makeBucket: { 81 | params: { 82 | bucketName: {type: "string"}, 83 | region: {type: "string", optional: true} 84 | }, 85 | handler(ctx) { 86 | return this.Promise.resolve(ctx.params) 87 | .then(({bucketName, region = ""}) => this.client.makeBucket(bucketName, region)); 88 | } 89 | }, 90 | /** 91 | * Lists all buckets. 92 | * 93 | * @actions 94 | * 95 | * @returns {PromiseLike} 96 | */ 97 | listBuckets: { 98 | handler() { 99 | return this.client.listBuckets().then(buckets => isUndefined(buckets) ? [] : buckets); 100 | } 101 | }, 102 | /** 103 | * Checks if a bucket exists. 104 | * 105 | * @actions 106 | * @param {string} bucketName - Name of the bucket 107 | * 108 | * @returns {PromiseLike} 109 | */ 110 | bucketExists: { 111 | params: { 112 | bucketName: {type: "string"} 113 | }, 114 | handler(ctx) { 115 | return this.client.bucketExists(ctx.params.bucketName); 116 | } 117 | }, 118 | /** 119 | * Removes a bucket. 120 | * 121 | * @actions 122 | * @param {string} bucketName - Name of the bucket 123 | * 124 | * @returns {PromiseLike} 125 | */ 126 | removeBucket: { 127 | params: { 128 | bucketName: {type: "string"} 129 | }, 130 | handler(ctx) { 131 | return this.client.removeBucket(ctx.params.bucketName); 132 | } 133 | }, 134 | /** 135 | * Lists all objects in a bucket. 136 | * 137 | * @actions 138 | * @param {string} bucketName - Name of the bucket 139 | * @param {string} prefix - The prefix of the objects that should be listed (optional, default ''). 140 | * @param {boolean} recursive - `true` indicates recursive style listing and false indicates directory style listing delimited by '/'. (optional, default `false`). 141 | * 142 | * @returns {PromiseLike} 143 | */ 144 | listObjects: { 145 | params: { 146 | bucketName: {type: "string"}, 147 | prefix: {type: "string", optional: true}, 148 | recursive: {type: "boolean", optional: true} 149 | }, 150 | handler(ctx) { 151 | return this.Promise.resolve(ctx.params) 152 | .then(({bucketName, prefix = "", recursive = false}) => { 153 | return new this.Promise((resolve, reject) => { 154 | try { 155 | const stream = this.client.listObjects(bucketName, prefix, recursive); 156 | const objects = []; 157 | stream.on("data", el => objects.push(el)); 158 | stream.on("end", () => resolve(objects)); 159 | stream.on("error", reject); 160 | } 161 | catch (e) { 162 | reject(e); 163 | } 164 | }); 165 | }); 166 | } 167 | }, 168 | /** 169 | * Lists all objects in a bucket using S3 listing objects V2 API 170 | * 171 | * @actions 172 | * @param {string} bucketName - Name of the bucket 173 | * @param {string} prefix - The prefix of the objects that should be listed (optional, default ''). 174 | * @param {boolean} recursive - `true` indicates recursive style listing and false indicates directory style listing delimited by '/'. (optional, default `false`). 175 | * @param {string} startAfter - Specifies the object name to start after when listing objects in a bucket. (optional, default ''). 176 | * 177 | * @returns {PromiseLike} 178 | */ 179 | listObjectsV2: { 180 | params: { 181 | bucketName: {type: "string"}, 182 | prefix: {type: "string", optional: true}, 183 | recursive: {type: "boolean", optional: true}, 184 | startAfter: {type: "string", optional: true} 185 | }, 186 | handler(ctx) { 187 | return this.Promise.resolve(ctx.params) 188 | .then(({bucketName, prefix = "", recursive = false, startAfter = ""}) => { 189 | return new this.Promise((resolve, reject) => { 190 | try { 191 | const stream = this.client.listObjectsV2(bucketName, prefix, recursive, startAfter); 192 | const objects = []; 193 | stream.on("data", el => objects.push(el)); 194 | stream.on("end", () => resolve(objects)); 195 | stream.on("error", reject); 196 | } 197 | catch (e) { 198 | reject(e); 199 | } 200 | }); 201 | }); 202 | } 203 | }, 204 | /** 205 | * Lists partially uploaded objects in a bucket. 206 | * 207 | * @actions 208 | * @param {string} bucketName - Name of the bucket 209 | * @param {string} prefix - The prefix of the objects that should be listed (optional, default ''). 210 | * @param {boolean} recursive - `true` indicates recursive style listing and false indicates directory style listing delimited by '/'. (optional, default `false`). 211 | * 212 | * @returns {PromiseLike} 213 | */ 214 | listIncompleteUploads: { 215 | params: { 216 | bucketName: {type: "string"}, 217 | prefix: {type: "string", optional: true}, 218 | recursive: {type: "boolean", optional: true} 219 | }, 220 | handler(ctx) { 221 | return this.Promise.resolve(ctx.params) 222 | .then(({bucketName, prefix = "", recursive = false}) => { 223 | return new this.Promise((resolve, reject) => { 224 | try { 225 | const stream = this.client.listIncompleteUploads(bucketName, prefix, recursive); 226 | const objects = []; 227 | stream.on("data", el => objects.push(el)); 228 | stream.on("end", () => resolve(objects)); 229 | stream.on("error", reject); 230 | } 231 | catch (e) { 232 | reject(e); 233 | } 234 | }); 235 | }); 236 | } 237 | }, 238 | /** 239 | * Downloads an object as a stream. 240 | * 241 | * @actions 242 | * @param {string} bucketName - Name of the bucket 243 | * @param {string} objectName - Name of the object. 244 | * 245 | * @returns {PromiseLike} 246 | */ 247 | getObject: { 248 | params: { 249 | bucketName: {type: "string"}, 250 | objectName: {type: "string"} 251 | }, 252 | handler(ctx) { 253 | return this.client.getObject(ctx.params.bucketName, ctx.params.objectName); 254 | } 255 | }, 256 | /** 257 | * Downloads the specified range bytes of an object as a stream. 258 | * 259 | * @actions 260 | * @param {string} bucketName - Name of the bucket. 261 | * @param {string} objectName - Name of the object. 262 | * @param {number} offset - `offset` of the object from where the stream will start. 263 | * @param {number} length - `length` of the object that will be read in the stream (optional, if not specified we read the rest of the file from the offset). 264 | * 265 | * @returns {PromiseLike} 266 | */ 267 | getPartialObject: { 268 | params: { 269 | bucketName: {type: "string"}, 270 | objectName: {type: "string"}, 271 | offset: {type: "number"}, 272 | length: {type: "number", optional: true} 273 | }, 274 | handler(ctx) { 275 | return this.client.getPartialObject(ctx.params.bucketName, ctx.params.objectName, ctx.params.offset, ctx.params.length); 276 | } 277 | }, 278 | /** 279 | * Downloads and saves the object as a file in the local filesystem. 280 | * 281 | * @actions 282 | * @param {string} bucketName - Name of the bucket. 283 | * @param {string} objectName - Name of the object. 284 | * @param {string} filePath - Path on the local filesystem to which the object data will be written. 285 | * 286 | * @returns {PromiseLike} 287 | */ 288 | fGetObject: { 289 | params: { 290 | bucketName: {type: "string"}, 291 | objectName: {type: "string"}, 292 | filePath: {type: "string"} 293 | }, 294 | handler(ctx) { 295 | return this.client.fGetObject(ctx.params.bucketName, ctx.params.objectName, ctx.params.filePath); 296 | } 297 | }, 298 | /** 299 | * Uploads an object from a stream/Buffer. 300 | * 301 | * @actions 302 | * @param {ReadableStream} params - Readable stream. 303 | * 304 | * @meta 305 | * @param {string} bucketName - Name of the bucket. 306 | * @param {string} objectName - Name of the object. 307 | * @param {number} size - Size of the object (optional). 308 | * @param {object} metaData - metaData of the object (optional). 309 | * 310 | * @returns {PromiseLike} 311 | */ 312 | putObject: { 313 | handler(ctx) { 314 | return this.Promise.resolve({stream: ctx.params, meta: ctx.meta}) 315 | .then(({stream, meta}) => this.client.putObject(meta.bucketName, meta.objectName, stream, meta.size, meta.metaData)); 316 | } 317 | }, 318 | /** 319 | * Uploads contents from a file to objectName. 320 | * 321 | * @actions 322 | * @param {string} bucketName - Name of the bucket. 323 | * @param {string} objectName - Name of the object. 324 | * @param {string} filePath - Path of the file to be uploaded. 325 | * @param {object} metaData - metaData of the object (optional). 326 | * 327 | * @returns {PromiseLike} 328 | */ 329 | fPutObject: { 330 | params: { 331 | bucketName: {type: "string"}, 332 | objectName: {type: "string"}, 333 | filePath: {type: "string"}, 334 | metaData: {type: "object", optional: true} 335 | }, 336 | handler(ctx) { 337 | return this.client.fPutObject( 338 | ctx.params.bucketName, 339 | ctx.params.objectName, 340 | ctx.params.filePath, 341 | ctx.params.metaData 342 | ); 343 | } 344 | }, 345 | /** 346 | * Copy a source object into a new object in the specified bucket. 347 | * 348 | * @actions 349 | * @param {string} bucketName - Name of the bucket. 350 | * @param {string} objectName - Name of the object. 351 | * @param {string} sourceObject - Path of the file to be copied. 352 | * @param {object} conditions - Conditions to be satisfied before allowing object copy. 353 | * @param {object} metaData - metaData of the object (optional). 354 | * 355 | * @returns {PromiseLike<{etag: {string}, lastModified: {string}}|Error>} 356 | */ 357 | copyObject: { 358 | params: { 359 | bucketName: {type: "string"}, 360 | objectName: {type: "string"}, 361 | sourceObject: {type: "string"}, 362 | conditions: { 363 | type: "object", properties: { 364 | modified: {type: "string", optional: true}, 365 | unmodified: {type: "string", optional: true}, 366 | matchETag: {type: "string", optional: true}, 367 | matchETagExcept: {type: "string", optional: true} 368 | } 369 | } 370 | }, 371 | handler(ctx) { 372 | return this.Promise.resolve(ctx.params) 373 | .then(({bucketName, objectName, sourceObject, conditions}) => { 374 | const _conditions = new Minio.CopyConditions(); 375 | if (conditions.modified) { 376 | _conditions.setModified(new Date(conditions.modified)); 377 | } 378 | if (conditions.unmodified) { 379 | _conditions.setUnmodified(new Date(conditions.unmodified)); 380 | } 381 | if (conditions.matchETag) { 382 | _conditions.setMatchETag(conditions.matchETag); 383 | } 384 | if (conditions.matchETagExcept) { 385 | _conditions.setMatchETagExcept(conditions.matchETagExcept); 386 | } 387 | conditions = _conditions; 388 | return this.client.copyObject(bucketName, objectName, sourceObject, conditions); 389 | }); 390 | } 391 | }, 392 | /** 393 | * Gets metadata of an object. 394 | * 395 | * @actions 396 | * @param {string} bucketName - Name of the bucket. 397 | * @param {string} objectName - Name of the object. 398 | * 399 | * @returns {PromiseLike<{size: {number}, metaData: {object}, lastModified: {string}, etag: {string}}|Error>} 400 | */ 401 | statObject: { 402 | params: { 403 | bucketName: {type: "string"}, 404 | objectName: {type: "string"} 405 | }, 406 | handler(ctx) { 407 | return this.client.statObject(ctx.params.bucketName, ctx.params.objectName); 408 | } 409 | }, 410 | /** 411 | * Removes an Object 412 | * 413 | * @actions 414 | * @param {string} bucketName - Name of the bucket. 415 | * @param {string} objectName - Name of the object. 416 | * 417 | * @returns {PromiseLike} 418 | */ 419 | removeObject: { 420 | params: { 421 | bucketName: {type: "string"}, 422 | objectName: {type: "string"} 423 | }, 424 | handler(ctx) { 425 | return this.client.removeObject(ctx.params.bucketName, ctx.params.objectName); 426 | } 427 | }, 428 | /** 429 | * Removes a list of Objects 430 | * 431 | * @actions 432 | * @param {string} bucketName - Name of the bucket. 433 | * @param {string[]} objectNames - Names of the objects. 434 | * 435 | * @returns {PromiseLike} 436 | */ 437 | removeObjects: { 438 | params: { 439 | bucketName: {type: "string"}, 440 | objectNames: {type: "array", items: "string"} 441 | }, 442 | handler(ctx) { 443 | return this.client.removeObjects(ctx.params.bucketName, ctx.params.objectNames); 444 | } 445 | }, 446 | /** 447 | * Removes a partially uploaded object. 448 | * 449 | * @actions 450 | * @param {string} bucketName - Name of the bucket. 451 | * @param {string} objectName - Name of the object. 452 | * 453 | * @returns {PromiseLike} 454 | */ 455 | removeIncompleteUpload: { 456 | params: { 457 | bucketName: {type: "string"}, 458 | objectName: {type: "string"} 459 | }, 460 | handler(ctx) { 461 | return this.Promise.resolve(ctx.params) 462 | .then(({bucketName, objectName}) => this.client.removeIncompleteUpload(bucketName, objectName)); 463 | } 464 | }, 465 | /** 466 | * Generates a presigned URL for the provided HTTP method, 'httpMethod'. Browsers/Mobile clients may point to this URL to directly download objects even if the bucket is private. This 467 | * presigned URL can have an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days. 468 | * 469 | * @actions 470 | * @param {string} httpMethod - The HTTP-Method (eg. `GET`). 471 | * @param {string} bucketName - Name of the bucket. 472 | * @param {string} objectName - Name of the object. 473 | * @param {number} expires - Expiry time in seconds. Default value is 7 days. (optional) 474 | * @param {object} reqParams - request parameters. (optional) 475 | * @param {string} requestDate - An ISO date string, the url will be issued at. Default value is now. (optional) 476 | * @returns {PromiseLike} 477 | */ 478 | presignedUrl: { 479 | params: { 480 | httpMethod: {type: "string"}, 481 | bucketName: {type: "string"}, 482 | objectName: {type: "string"}, 483 | expires: {type: "number", integer: true, optional: true}, 484 | reqParams: {type: "object", optional: true}, 485 | requestDate: {type: "string", optional: true} 486 | }, 487 | handler(ctx) { 488 | return this.Promise.resolve(ctx.params) 489 | .then(({ 490 | httpMethod, 491 | bucketName, 492 | objectName, 493 | expires, 494 | reqParams, 495 | requestDate 496 | }) => { 497 | 498 | if (isString(requestDate)) { 499 | requestDate = new Date(requestDate); 500 | } 501 | 502 | return new this.Promise((resolve, reject) => { 503 | this.client.presignedUrl( 504 | httpMethod, 505 | bucketName, 506 | objectName, 507 | expires, 508 | reqParams, 509 | requestDate, 510 | (error, url) => { 511 | if (error) { 512 | reject(error); 513 | } 514 | else { 515 | resolve(url); 516 | } 517 | } 518 | ); 519 | }); 520 | }); 521 | } 522 | }, 523 | /** 524 | * Generates a presigned URL for HTTP GET operations. Browsers/Mobile clients may point to this URL to directly download objects even if the bucket is private. This presigned URL can have an 525 | * associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days. 526 | * 527 | * @actions 528 | * @param {string} bucketName - Name of the bucket. 529 | * @param {string} objectName - Name of the object. 530 | * @param {number} expires - Expiry time in seconds. Default value is 7 days. (optional) 531 | * @param {object} reqParams - request parameters. (optional) 532 | * @param {string} requestDate - An ISO date string, the url will be issued at. Default value is now. (optional) 533 | * @returns {PromiseLike} 534 | */ 535 | presignedGetObject: { 536 | params: { 537 | bucketName: {type: "string"}, 538 | objectName: {type: "string"}, 539 | expires: {type: "number", integer: true, optional: true}, 540 | reqParams: {type: "object", optional: true}, 541 | requestDate: {type: "string", optional: true} 542 | }, 543 | handler(ctx) { 544 | return this.Promise.resolve(ctx.params) 545 | .then(({ 546 | bucketName, 547 | objectName, 548 | expires, 549 | reqParams, 550 | requestDate 551 | }) => { 552 | if (isString(requestDate)) { 553 | requestDate = new Date(requestDate); 554 | } 555 | 556 | return new this.Promise((resolve, reject) => { 557 | this.client.presignedGetObject( 558 | bucketName, 559 | objectName, 560 | expires, 561 | reqParams, 562 | requestDate, 563 | (error, url) => { 564 | if (error) { 565 | reject(error); 566 | } 567 | else { 568 | resolve(url); 569 | } 570 | } 571 | ); 572 | }); 573 | }); 574 | } 575 | }, 576 | /** 577 | * Generates a presigned URL for HTTP PUT operations. Browsers/Mobile clients may point to this URL to upload objects directly to a bucket even if it is private. This presigned URL can have 578 | * an associated expiration time in seconds after which the URL is no longer valid. The default value is 7 days. 579 | * 580 | * @actions 581 | * @param {string} bucketName - Name of the bucket. 582 | * @param {string} objectName - Name of the object. 583 | * @param {number} expires - Expiry time in seconds. Default value is 7 days. (optional) 584 | * @returns {PromiseLike} 585 | */ 586 | presignedPutObject: { 587 | params: { 588 | bucketName: {type: "string"}, 589 | objectName: {type: "string"}, 590 | expires: {type: "number", integer: true, optional: true} 591 | }, 592 | handler(ctx) { 593 | return this.Promise.resolve(ctx.params) 594 | .then(({ 595 | bucketName, 596 | objectName, 597 | expires 598 | }) => { 599 | return new this.Promise((resolve, reject) => { 600 | this.client.presignedPutObject( 601 | bucketName, 602 | objectName, 603 | expires, 604 | (error, url) => { 605 | if (error) { 606 | reject(error); 607 | } 608 | else { 609 | resolve(url); 610 | } 611 | } 612 | ); 613 | }); 614 | }); 615 | } 616 | }, 617 | /** 618 | * Allows setting policy conditions to a presigned URL for POST operations. Policies such as bucket name to receive object uploads, key name prefixes, expiry policy may be set. 619 | * 620 | * @actions 621 | * @param {object} policy - Policy object created by minioClient.newPostPolicy() 622 | * @returns {PromiseLike<{postURL: {string}, formData: {object}}|Error>} 623 | */ 624 | presignedPostPolicy: { 625 | params: { 626 | policy: { 627 | type: "object", properties: { 628 | expires: {type: "string", optional: true}, 629 | key: {type: "string", optional: true}, 630 | keyStartsWith: {type: "string", optional: true}, 631 | bucket: {type: "string", optional: true}, 632 | contentType: {type: "string", optional: true}, 633 | contentLengthRangeMin: {type: "number", integer: true, optional: true}, 634 | contentLengthRangeMax: {type: "number", integer: true, optional: true} 635 | } 636 | } 637 | }, 638 | handler(ctx) { 639 | return this.Promise.resolve(ctx.params) 640 | .then(({policy}) => { 641 | const _policy = this.client.newPostPolicy(); 642 | if (policy.expires) { 643 | _policy.setExpires(new Date(policy.expires)); 644 | } 645 | if (policy.key) { 646 | _policy.setKey(policy.key); 647 | } 648 | if (policy.keyStartsWith) { 649 | _policy.setKeyStartsWith(policy.keyStartsWith); 650 | } 651 | if (policy.bucket) { 652 | _policy.setBucket(policy.bucket); 653 | } 654 | if (policy.contentType) { 655 | _policy.setContentType(policy.contentType); 656 | } 657 | if (policy.contentLengthRangeMin && policy.contentLengthRangeMax) { 658 | _policy.setContentLengthRange(policy.contentLengthRangeMin, policy.contentLengthRangeMax); 659 | } 660 | return this.client.presignedPostPolicy(_policy); 661 | }); 662 | } 663 | } 664 | }, 665 | 666 | /** 667 | * Service created lifecycle event handler. 668 | * Constructs a new minio client entity 669 | */ 670 | created() { 671 | this.client = this.createMinioClient(); 672 | }, 673 | /** 674 | * Service started lifecycle event handler. Resolves when: 675 | * * ping of S3 backend has been successful 676 | * * a healthCheck has been registered, given minioHealthCheckInterval > 0 677 | * @returns {PromiseLike} 678 | */ 679 | started() { 680 | /* istanbul ignore next */ 681 | return this.Promise.resolve() 682 | .then(() => this.ping()) 683 | .then(() => { 684 | this.settings.minioHealthCheckInterval ? 685 | this.healthCheckInterval = setInterval( 686 | () => this.ping().catch(e => this.logger.error("Minio backend can not be reached", e)), 687 | this.settings.minioHealthCheckInterval) 688 | : undefined; 689 | return undefined; 690 | }).catch(e => { 691 | throw new MinioInitializationError(e.message); 692 | }); 693 | }, 694 | /** 695 | * Service stopped lifecycle event handler. 696 | * Removes the healthCheckInterval 697 | */ 698 | stopped() { 699 | this.healthCheckInterval && clearInterval(this.healthCheckInterval); 700 | } 701 | 702 | }; 703 | -------------------------------------------------------------------------------- /test/unit/errors/MinioInitializationError.spec.js: -------------------------------------------------------------------------------- 1 | const {MinioInitializationError} = require("errors"); 2 | 3 | describe("Errors", () => { 4 | describe("MinioInitializationError", () => { 5 | describe("constructor", () => { 6 | it("constructs with sensitive defaults", () => { 7 | let error = new MinioInitializationError(); 8 | expect(error.message).toEqual("Minio can not be initialized"); 9 | expect(error.code).toEqual(500); 10 | expect(error.type).toEqual("MINIO_INITIALIZATION_ERROR"); 11 | expect(error.data).toEqual({}); 12 | expect(error.retryable).toEqual(false); 13 | }); 14 | 15 | it("constructs with given arguments", () => { 16 | let error = new MinioInitializationError("foo", 500, "BAR", 17 | {fooz: "barz"}); 18 | expect(error.message).toEqual("foo"); 19 | expect(error.code).toEqual(500); 20 | expect(error.type).toEqual("BAR"); 21 | expect(error.data).toEqual({fooz: "barz"}); 22 | expect(error.retryable).toEqual(false); 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/unit/errors/MinioPingError.spec.js: -------------------------------------------------------------------------------- 1 | const {MinioPingError} = require("errors"); 2 | 3 | describe("Errors", () => { 4 | describe("MinioPingError", () => { 5 | describe("constructor", () => { 6 | it("constructs with sensitive defaults", () => { 7 | let error = new MinioPingError(); 8 | expect(error.message).toEqual("Minio Backend not reachable"); 9 | expect(error.code).toEqual(502); 10 | expect(error.type).toEqual("MINIO_PING_ERROR"); 11 | expect(error.data).toEqual({}); 12 | expect(error.retryable).toEqual(true); 13 | }); 14 | 15 | it("constructs with given arguments", () => { 16 | let error = new MinioPingError("foo", 500, "BAR", {fooz: "barz"}); 17 | expect(error.message).toEqual("foo"); 18 | expect(error.code).toEqual(500); 19 | expect(error.type).toEqual("BAR"); 20 | expect(error.data).toEqual({fooz: "barz"}); 21 | expect(error.retryable).toEqual(true); 22 | }); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/unit/index.spec.js: -------------------------------------------------------------------------------- 1 | describe("index.js", () => { 2 | it("can be required", () => { 3 | expect(() => require("./../../index")).not.toThrow(); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/unit/service/actions/bucketExists.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('bucketExists', () => { 6 | it('checks if the bucket exists', () => { 7 | let context = { 8 | client: { 9 | bucketExists: jest.fn().mockReturnValue(Promise.resolve(true)) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'someBucket'; 14 | return Service().actions.bucketExists.handler.bind(context)({params: {bucketName}}).then(r => { 15 | expect(r).toEqual(true); 16 | expect(context.client.bucketExists.mock.calls[0]).toEqual([bucketName]); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/service/actions/copyObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require("service"); 2 | const Promise = require("bluebird"); 3 | describe("Service", () => { 4 | describe("actions", () => { 5 | describe("copyObject", () => { 6 | it("accepts a bucket name, an object name, a sourceObject and conditions", () => { 7 | let context = { 8 | client: { 9 | copyObject: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = "some-bucket"; 14 | const objectName = "some-object"; 15 | const sourceObject = "/var/tmp.png"; 16 | const conditions = { 17 | modified: "Mon, 01 Jan 2018 00:00:00 GMT", 18 | unmodified: "Tue, 01 Jan 2019 00:00:00 GMT", 19 | matchETag: "asdgdf", 20 | matchETagExcept: "asdgdf" 21 | }; 22 | return Service().actions.copyObject.handler.bind(context)({params: {sourceObject, bucketName, objectName, conditions}}).then(r => { 23 | expect(context.client.copyObject.mock.calls[0][0]).toEqual(bucketName); 24 | expect(context.client.copyObject.mock.calls[0][1]).toEqual(objectName); 25 | expect(context.client.copyObject.mock.calls[0][2]).toEqual(sourceObject); 26 | expect(context.client.copyObject.mock.calls[0][3]).toEqual(conditions); 27 | }); 28 | }); 29 | 30 | it("works without any conditions", () => { 31 | let context = { 32 | client: { 33 | copyObject: jest.fn().mockReturnValue(Promise.resolve()) 34 | }, 35 | Promise 36 | }; 37 | const bucketName = "some-bucket"; 38 | const objectName = "some-object"; 39 | const sourceObject = "/var/tmp.png"; 40 | const conditions = {}; 41 | return Service().actions.copyObject.handler.bind(context)({params: {sourceObject, bucketName, objectName, conditions}}).then(r => { 42 | expect(context.client.copyObject.mock.calls[0][0]).toEqual(bucketName); 43 | expect(context.client.copyObject.mock.calls[0][1]).toEqual(objectName); 44 | expect(context.client.copyObject.mock.calls[0][2]).toEqual(sourceObject); 45 | expect(context.client.copyObject.mock.calls[0][3]).toEqual({ 46 | matchETag: "", 47 | matchETagExcept: "", 48 | modified: "", 49 | unmodified: "" 50 | }); 51 | }); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/unit/service/actions/fGetObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('fGetObject', () => { 6 | it('accepts a bucket name, an object name and a file path', () => { 7 | let context = { 8 | client: { 9 | fGetObject: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | const filePath = '/tmp/file.png'; 16 | return Service().actions.fGetObject.handler.bind(context)({params: {bucketName, objectName, filePath}}).then(r => { 17 | expect(context.client.fGetObject.mock.calls[0]).toEqual([bucketName, objectName, filePath]); 18 | }); 19 | }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/unit/service/actions/fPutObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('fPutObject', () => { 6 | it('accepts a bucket name, an object name and a file path', () => { 7 | let context = { 8 | client: { 9 | fPutObject: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | const filePath = '/var/tmp.png'; 16 | const metaData = {foo: 'bar'}; 17 | return Service().actions.fPutObject.handler.bind(context)({params: {filePath, bucketName, objectName, metaData}}).then(r => { 18 | expect(context.client.fPutObject.mock.calls[0]).toEqual([bucketName, objectName, filePath, metaData]); 19 | }); 20 | }); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/unit/service/actions/getObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('getObject', () => { 6 | it('accepts a bucket name and an object name', () => { 7 | let context = { 8 | client: { 9 | getObject: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | return Service().actions.getObject.handler.bind(context)({params: {bucketName, objectName}}).then(r => { 16 | expect(context.client.getObject.mock.calls[0]).toEqual([bucketName, objectName]); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/service/actions/getPartialObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('getPartialObject', () => { 6 | it('accepts a bucket name, an object name, an offest and a length', () => { 7 | let context = { 8 | client: { 9 | getPartialObject: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | const offset = 0; 16 | const length = 100; 17 | return Service().actions.getPartialObject.handler.bind(context)({params: {bucketName, objectName, offset, length}}).then(r => { 18 | expect(context.client.getPartialObject.mock.calls[0]).toEqual([bucketName, objectName, offset, length]); 19 | }); 20 | }); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/unit/service/actions/listBuckets.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('listBuckets', () => { 6 | it('returns an empty string if there are no buckest', () => { 7 | let context = { 8 | client: { 9 | listBuckets: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | return Service().actions.listBuckets.handler.bind(context)().then(r => { 14 | expect(r).toEqual([]); 15 | }); 16 | }); 17 | 18 | it('uses an empty string as the default region', () => { 19 | let context = { 20 | client: { 21 | listBuckets: jest.fn().mockReturnValue(Promise.resolve(['foo'])) 22 | }, 23 | Promise 24 | }; 25 | return Service().actions.listBuckets.handler.bind(context)().then(r => { 26 | expect(r).toEqual(['foo']); 27 | }); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/unit/service/actions/listIncompleteUploads.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('listIncompleteUploads', () => { 6 | it('lists all incomplete uploads in a Bucket', () => { 7 | let stream = { 8 | on: jest.fn() 9 | }; 10 | let context = { 11 | client: { 12 | listIncompleteUploads: jest.fn().mockReturnValue(stream) 13 | }, 14 | Promise 15 | }; 16 | const bucketName = 'someBucket'; 17 | const prefix = 'some-prefix'; 18 | const recursive = true; 19 | const listing = Service().actions.listIncompleteUploads.handler.bind(context)({params: {bucketName, prefix, recursive}}); 20 | return Promise.delay(100).then(() => { 21 | stream.on.mock.calls.find(e => e[0] === 'data')[1]({foo: 'bar'}); 22 | stream.on.mock.calls.find(e => e[0] === 'end')[1](); 23 | }) 24 | .then(() => listing) 25 | .then(r => { 26 | expect(context.client.listIncompleteUploads.mock.calls[0]).toEqual([bucketName, prefix, recursive]); 27 | expect(r).toEqual([{foo: 'bar'}]); 28 | }); 29 | }); 30 | 31 | it('assumes prefix and recursive if not given', () => { 32 | let stream = { 33 | on: jest.fn() 34 | }; 35 | let context = { 36 | client: { 37 | listIncompleteUploads: jest.fn().mockReturnValue(stream) 38 | }, 39 | Promise 40 | }; 41 | const bucketName = 'someBucket'; 42 | const listing = Service().actions.listIncompleteUploads.handler.bind(context)({params: {bucketName}}); 43 | return Promise.delay(100).then(() => { 44 | stream.on.mock.calls.find(e => e[0] === 'data')[1]({foo: 'bar'}); 45 | stream.on.mock.calls.find(e => e[0] === 'end')[1](); 46 | }) 47 | .then(() => listing) 48 | .then(r => { 49 | expect(context.client.listIncompleteUploads.mock.calls[0]).toEqual([bucketName, '', false]); 50 | expect(r).toEqual([{foo: 'bar'}]); 51 | }); 52 | }); 53 | 54 | it('rejects if the stream encountered an error', () => { 55 | let stream = { 56 | on: jest.fn() 57 | }; 58 | let context = { 59 | client: { 60 | listIncompleteUploads: jest.fn().mockReturnValue(stream) 61 | }, 62 | Promise 63 | }; 64 | const bucketName = 'someBucket'; 65 | const prefix = 'some-prefix'; 66 | const recursive = true; 67 | const listing = Service().actions.listIncompleteUploads.handler.bind(context)({params: {bucketName, prefix, recursive}}); 68 | return Promise.delay(100).then(() => { 69 | stream.on.mock.calls.find(e => e[0] === 'error')[1](new Error('something went wrong')); 70 | }) 71 | .then(() => listing) 72 | .catch(e => { 73 | expect(e.message).toEqual('something went wrong'); 74 | }); 75 | }); 76 | 77 | it('rejects if the stream acquisition encountered an error', () => { 78 | let stream = { 79 | on: jest.fn() 80 | }; 81 | let context = { 82 | client: { 83 | listIncompleteUploads: () => {throw new Error('something went wrong');} 84 | }, 85 | Promise 86 | }; 87 | const bucketName = 'someBucket'; 88 | const prefix = 'some-prefix'; 89 | const recursive = true; 90 | return Service().actions.listIncompleteUploads.handler.bind(context)({params: {bucketName, prefix, recursive}}) 91 | .catch(e => { 92 | expect(e.message).toEqual('something went wrong'); 93 | }); 94 | }); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/unit/service/actions/listObjects.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('listObjects', () => { 6 | it('lists all objects in a Bucket', () => { 7 | let stream = { 8 | on: jest.fn() 9 | }; 10 | let context = { 11 | client: { 12 | listObjects: jest.fn().mockReturnValue(stream) 13 | }, 14 | Promise 15 | }; 16 | const bucketName = 'someBucket'; 17 | const prefix = 'some-prefix'; 18 | const recursive = true; 19 | const listing = Service().actions.listObjects.handler.bind(context)({params: {bucketName, prefix, recursive}}); 20 | return Promise.delay(100).then(() => { 21 | stream.on.mock.calls.find(e => e[0] === 'data')[1]({foo: 'bar'}); 22 | stream.on.mock.calls.find(e => e[0] === 'end')[1](); 23 | }) 24 | .then(() => listing) 25 | .then(r => { 26 | expect(context.client.listObjects.mock.calls[0]).toEqual([bucketName, prefix, recursive]); 27 | expect(r).toEqual([{foo: 'bar'}]); 28 | }); 29 | }); 30 | 31 | it('assumes prefix and recursive if not given', () => { 32 | let stream = { 33 | on: jest.fn() 34 | }; 35 | let context = { 36 | client: { 37 | listObjects: jest.fn().mockReturnValue(stream) 38 | }, 39 | Promise 40 | }; 41 | const bucketName = 'someBucket'; 42 | const listing = Service().actions.listObjects.handler.bind(context)({params: {bucketName}}); 43 | return Promise.delay(100).then(() => { 44 | stream.on.mock.calls.find(e => e[0] === 'data')[1]({foo: 'bar'}); 45 | stream.on.mock.calls.find(e => e[0] === 'end')[1](); 46 | }) 47 | .then(() => listing) 48 | .then(r => { 49 | expect(context.client.listObjects.mock.calls[0]).toEqual([bucketName, '', false]); 50 | expect(r).toEqual([{foo: 'bar'}]); 51 | }); 52 | }); 53 | 54 | it('rejects if the stream encountered an error', () => { 55 | let stream = { 56 | on: jest.fn() 57 | }; 58 | let context = { 59 | client: { 60 | listObjects: jest.fn().mockReturnValue(stream) 61 | }, 62 | Promise 63 | }; 64 | const bucketName = 'someBucket'; 65 | const prefix = 'some-prefix'; 66 | const recursive = true; 67 | const listing = Service().actions.listObjects.handler.bind(context)({params: {bucketName, prefix, recursive}}); 68 | return Promise.delay(100).then(() => { 69 | stream.on.mock.calls.find(e => e[0] === 'error')[1](new Error('something went wrong')); 70 | }) 71 | .then(() => listing) 72 | .catch(e => { 73 | expect(e.message).toEqual('something went wrong'); 74 | }); 75 | }); 76 | 77 | it('rejects if the stream acquisition encountered an error', () => { 78 | let stream = { 79 | on: jest.fn() 80 | }; 81 | let context = { 82 | client: { 83 | listObjects: () => {throw new Error('something went wrong');} 84 | }, 85 | Promise 86 | }; 87 | const bucketName = 'someBucket'; 88 | const prefix = 'some-prefix'; 89 | const recursive = true; 90 | return Service().actions.listObjects.handler.bind(context)({params: {bucketName, prefix, recursive}}) 91 | .catch(e => { 92 | expect(e.message).toEqual('something went wrong'); 93 | }); 94 | }); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/unit/service/actions/listObjectsV2.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('listObjectsV2', () => { 6 | it('lists all objects in a Bucket', () => { 7 | let stream = { 8 | on: jest.fn() 9 | }; 10 | let context = { 11 | client: { 12 | listObjectsV2: jest.fn().mockReturnValue(stream) 13 | }, 14 | Promise 15 | }; 16 | const bucketName = 'someBucket'; 17 | const prefix = 'some-prefix'; 18 | const recursive = true; 19 | const startAfter = 'that'; 20 | const listing = Service().actions.listObjectsV2.handler.bind(context)({params: {bucketName, prefix, recursive, startAfter}}); 21 | return Promise.delay(100).then(() => { 22 | stream.on.mock.calls.find(e => e[0] === 'data')[1]({foo: 'bar'}); 23 | stream.on.mock.calls.find(e => e[0] === 'end')[1](); 24 | }) 25 | .then(() => listing) 26 | .then(r => { 27 | expect(context.client.listObjectsV2.mock.calls[0]).toEqual([bucketName, prefix, recursive, startAfter]); 28 | expect(r).toEqual([{foo: 'bar'}]); 29 | }); 30 | }); 31 | 32 | it('assumes prefix and recursive if not given', () => { 33 | let stream = { 34 | on: jest.fn() 35 | }; 36 | let context = { 37 | client: { 38 | listObjectsV2: jest.fn().mockReturnValue(stream) 39 | }, 40 | Promise 41 | }; 42 | const bucketName = 'someBucket'; 43 | const listing = Service().actions.listObjectsV2.handler.bind(context)({params: {bucketName}}); 44 | return Promise.delay(100).then(() => { 45 | stream.on.mock.calls.find(e => e[0] === 'data')[1]({foo: 'bar'}); 46 | stream.on.mock.calls.find(e => e[0] === 'end')[1](); 47 | }) 48 | .then(() => listing) 49 | .then(r => { 50 | expect(context.client.listObjectsV2.mock.calls[0]).toEqual([bucketName, '', false, '']); 51 | expect(r).toEqual([{foo: 'bar'}]); 52 | }); 53 | }); 54 | 55 | it('rejects if the stream encountered an error', () => { 56 | let stream = { 57 | on: jest.fn() 58 | }; 59 | let context = { 60 | client: { 61 | listObjectsV2: jest.fn().mockReturnValue(stream) 62 | }, 63 | Promise 64 | }; 65 | const bucketName = 'someBucket'; 66 | const prefix = 'some-prefix'; 67 | const recursive = true; 68 | const startAfter = 'that'; 69 | const listing = Service().actions.listObjectsV2.handler.bind(context)({params: {bucketName, prefix, recursive, startAfter}}); 70 | return Promise.delay(100).then(() => { 71 | stream.on.mock.calls.find(e => e[0] === 'error')[1](new Error('something went wrong')); 72 | }) 73 | .then(() => listing) 74 | .catch(e => { 75 | expect(e.message).toEqual('something went wrong'); 76 | }); 77 | }); 78 | 79 | it('rejects if the stream acquisition encountered an error', () => { 80 | let stream = { 81 | on: jest.fn() 82 | }; 83 | let context = { 84 | client: { 85 | listObjectsV2: () => {throw new Error('something went wrong');} 86 | }, 87 | Promise 88 | }; 89 | const bucketName = 'someBucket'; 90 | const prefix = 'some-prefix'; 91 | const recursive = true; 92 | const startAfter = 'that'; 93 | return Service().actions.listObjectsV2.handler.bind(context)({params: {bucketName, prefix, recursive, startAfter}}) 94 | .catch(e => { 95 | expect(e.message).toEqual('something went wrong'); 96 | }); 97 | }); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/unit/service/actions/makeBucket.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('makeBucket', () => { 6 | it('accepts a bucket name and a region', () => { 7 | let context = { 8 | client: { 9 | makeBucket: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const region = 'some-region'; 15 | return Service().actions.makeBucket.handler.bind(context)({params: {bucketName, region}}).then(r => { 16 | expect(context.client.makeBucket.mock.calls[0]).toEqual([bucketName, region]); 17 | }); 18 | }); 19 | 20 | it('uses an empty string as the default region', () => { 21 | let context = { 22 | client: { 23 | makeBucket: jest.fn().mockReturnValue(Promise.resolve()) 24 | }, 25 | Promise 26 | }; 27 | const bucketName = 'some-bucket'; 28 | return Service().actions.makeBucket.handler.bind(context)({params: {bucketName}}).then(r => { 29 | expect(context.client.makeBucket.mock.calls[0]).toEqual([bucketName, '']); 30 | }); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/unit/service/actions/presignedGetObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('presigedUrl', () => { 6 | it('creates and returns a Presigned URL for obtaining an Object', () => { 7 | let context = { 8 | client: { 9 | presignedGetObject: jest.fn() 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | const expires = 1535; 16 | const reqParams = {foo: 'bar'}; 17 | const requestDate = 'Mon, 01 Jan 2018 00:00:00 GMT'; 18 | let creating = Service().actions.presignedGetObject.handler.bind(context)({ 19 | params: { 20 | bucketName, 21 | objectName, 22 | expires, 23 | reqParams, 24 | requestDate 25 | } 26 | }); 27 | return Promise.delay(100) 28 | .then(() => context.client.presignedGetObject.mock.calls[0][5](null, 'https://example.com')) 29 | .then(() => creating) 30 | .then(r => { 31 | expect(r).toEqual('https://example.com'); 32 | expect(context.client.presignedGetObject.mock.calls[0][0]).toEqual(bucketName); 33 | expect(context.client.presignedGetObject.mock.calls[0][1]).toEqual(objectName); 34 | expect(context.client.presignedGetObject.mock.calls[0][2]).toEqual(expires); 35 | expect(context.client.presignedGetObject.mock.calls[0][3]).toEqual(reqParams); 36 | expect(context.client.presignedGetObject.mock.calls[0][4]).toEqual(new Date(requestDate)); 37 | }); 38 | }); 39 | 40 | it('rejects with errors encountered', () => { 41 | let context = { 42 | client: { 43 | presignedGetObject: jest.fn() 44 | }, 45 | Promise 46 | }; 47 | const bucketName = 'some-bucket'; 48 | const objectName = 'some-object'; 49 | const expires = 1535; 50 | const reqParams = {foo: 'bar'}; 51 | let creating = Service().actions.presignedGetObject.handler.bind(context)({ 52 | params: { 53 | bucketName, 54 | objectName, 55 | expires, 56 | reqParams 57 | } 58 | }); 59 | return Promise.delay(100) 60 | .then(() => context.client.presignedGetObject.mock.calls[0][5](new Error('Something went wrong'))) 61 | .then(() => creating) 62 | .catch(e => { 63 | expect(e.message).toEqual('Something went wrong'); 64 | expect(context.client.presignedGetObject.mock.calls[0][0]).toEqual(bucketName); 65 | expect(context.client.presignedGetObject.mock.calls[0][1]).toEqual(objectName); 66 | expect(context.client.presignedGetObject.mock.calls[0][2]).toEqual(expires); 67 | expect(context.client.presignedGetObject.mock.calls[0][3]).toEqual(reqParams); 68 | expect(context.client.presignedGetObject.mock.calls[0][4]).toEqual(undefined); 69 | }); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/unit/service/actions/presignedPostPolicy.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('presigedUrl', () => { 6 | it('creates and returns a Presigned URL for performing an Operation', () => { 7 | let postPolicy = { 8 | setExpires: jest.fn(), 9 | setKey: jest.fn(), 10 | setKeyStartsWith: jest.fn(), 11 | setBucket: jest.fn(), 12 | setContentType: jest.fn(), 13 | setContentLengthRange: jest.fn() 14 | }; 15 | let context = { 16 | client: { 17 | presignedPostPolicy: jest.fn().mockReturnValue({foo: 'bar'}), 18 | newPostPolicy: jest.fn().mockReturnValue(postPolicy) 19 | }, 20 | Promise 21 | }; 22 | const policy = { 23 | expires: 'Mon, 01 Jan 2018 00:00:00 GMT', 24 | key: 'some-key', 25 | keyStartsWith: 'some-start', 26 | bucket: 'some-bucket', 27 | contentType: 'some-content', 28 | contentLengthRangeMin: 535, 29 | contentLengthRangeMax: 34084 30 | }; 31 | return Service().actions.presignedPostPolicy.handler.bind(context)({ 32 | params: {policy} 33 | }).then(r => { 34 | expect(postPolicy.setExpires.mock.calls[0]).toEqual([new Date(policy.expires)]); 35 | expect(postPolicy.setKey.mock.calls[0]).toEqual([policy.key]); 36 | expect(postPolicy.setKeyStartsWith.mock.calls[0]).toEqual([policy.keyStartsWith]); 37 | expect(postPolicy.setBucket.mock.calls[0]).toEqual([policy.bucket]); 38 | expect(postPolicy.setContentType.mock.calls[0]).toEqual([policy.contentType]); 39 | expect(postPolicy.setContentLengthRange.mock.calls[0]).toEqual([policy.contentLengthRangeMin, policy.contentLengthRangeMax]); 40 | expect(r).toEqual({foo: 'bar'}); 41 | }); 42 | }); 43 | 44 | it('creates a PostPolicy with no privileges', () => { 45 | let postPolicy = { 46 | }; 47 | let context = { 48 | client: { 49 | presignedPostPolicy: jest.fn().mockReturnValue({foo: 'bar'}), 50 | newPostPolicy: jest.fn().mockReturnValue(postPolicy) 51 | }, 52 | Promise 53 | }; 54 | const policy = {}; 55 | return Service().actions.presignedPostPolicy.handler.bind(context)({ 56 | params: {policy} 57 | }).then(r => { 58 | expect(r).toEqual({foo: 'bar'}); 59 | }); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/unit/service/actions/presignedPutObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('presigedUrl', () => { 6 | it('creates and returns a Presigned URL for creating an Object', () => { 7 | let context = { 8 | client: { 9 | presignedPutObject: jest.fn() 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | const expires = 1535; 16 | let creating = Service().actions.presignedPutObject.handler.bind(context)({ 17 | params: { 18 | bucketName, 19 | objectName, 20 | expires, 21 | } 22 | }); 23 | return Promise.delay(100) 24 | .then(() => context.client.presignedPutObject.mock.calls[0][3](null, 'https://example.com')) 25 | .then(() => creating) 26 | .then(r => { 27 | expect(r).toEqual('https://example.com'); 28 | expect(context.client.presignedPutObject.mock.calls[0][0]).toEqual(bucketName); 29 | expect(context.client.presignedPutObject.mock.calls[0][1]).toEqual(objectName); 30 | expect(context.client.presignedPutObject.mock.calls[0][2]).toEqual(expires); 31 | }); 32 | }); 33 | 34 | it('rejects with errors encountered', () => { 35 | let context = { 36 | client: { 37 | presignedPutObject: jest.fn() 38 | }, 39 | Promise 40 | }; 41 | const bucketName = 'some-bucket'; 42 | const objectName = 'some-object'; 43 | const expires = 1535; 44 | let creating = Service().actions.presignedPutObject.handler.bind(context)({ 45 | params: { 46 | bucketName, 47 | objectName, 48 | expires 49 | } 50 | }); 51 | return Promise.delay(100) 52 | .then(() => context.client.presignedPutObject.mock.calls[0][3](new Error('Something went wrong'))) 53 | .then(() => creating) 54 | .catch(e => { 55 | expect(e.message).toEqual('Something went wrong'); 56 | expect(context.client.presignedPutObject.mock.calls[0][0]).toEqual(bucketName); 57 | expect(context.client.presignedPutObject.mock.calls[0][1]).toEqual(objectName); 58 | expect(context.client.presignedPutObject.mock.calls[0][2]).toEqual(expires); 59 | }); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/unit/service/actions/presignedUrl.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('presigedUrl', () => { 6 | it('creates and returns a Presigned URL', () => { 7 | let context = { 8 | client: { 9 | presignedUrl: jest.fn() 10 | }, 11 | Promise 12 | }; 13 | const httpMethod = 'PATCH'; 14 | const bucketName = 'some-bucket'; 15 | const objectName = 'some-object'; 16 | const expires = 1535; 17 | const reqParams = {foo: 'bar'}; 18 | const requestDate = 'Mon, 01 Jan 2018 00:00:00 GMT'; 19 | let creating = Service().actions.presignedUrl.handler.bind(context)({ 20 | params: { 21 | httpMethod, 22 | bucketName, 23 | objectName, 24 | expires, 25 | reqParams, 26 | requestDate 27 | } 28 | }); 29 | return Promise.delay(100) 30 | .then(() => context.client.presignedUrl.mock.calls[0][6](null, 'https://example.com')) 31 | .then(() => creating) 32 | .then(r => { 33 | expect(r).toEqual('https://example.com'); 34 | expect(context.client.presignedUrl.mock.calls[0][0]).toEqual(httpMethod); 35 | expect(context.client.presignedUrl.mock.calls[0][1]).toEqual(bucketName); 36 | expect(context.client.presignedUrl.mock.calls[0][2]).toEqual(objectName); 37 | expect(context.client.presignedUrl.mock.calls[0][3]).toEqual(expires); 38 | expect(context.client.presignedUrl.mock.calls[0][4]).toEqual(reqParams); 39 | expect(context.client.presignedUrl.mock.calls[0][5]).toEqual(new Date(requestDate)); 40 | }); 41 | }); 42 | 43 | it('rejects with errors encountered', () => { 44 | let context = { 45 | client: { 46 | presignedUrl: jest.fn() 47 | }, 48 | Promise 49 | }; 50 | const httpMethod = 'PATCH'; 51 | const bucketName = 'some-bucket'; 52 | const objectName = 'some-object'; 53 | const expires = 1535; 54 | const reqParams = {foo: 'bar'}; 55 | let creating = Service().actions.presignedUrl.handler.bind(context)({ 56 | params: { 57 | httpMethod, 58 | bucketName, 59 | objectName, 60 | expires, 61 | reqParams 62 | } 63 | }); 64 | return Promise.delay(100) 65 | .then(() => context.client.presignedUrl.mock.calls[0][6](new Error('Something went wrong'))) 66 | .then(() => creating) 67 | .catch(e => { 68 | expect(e.message).toEqual('Something went wrong'); 69 | expect(context.client.presignedUrl.mock.calls[0][0]).toEqual(httpMethod); 70 | expect(context.client.presignedUrl.mock.calls[0][1]).toEqual(bucketName); 71 | expect(context.client.presignedUrl.mock.calls[0][2]).toEqual(objectName); 72 | expect(context.client.presignedUrl.mock.calls[0][3]).toEqual(expires); 73 | expect(context.client.presignedUrl.mock.calls[0][4]).toEqual(reqParams); 74 | expect(context.client.presignedUrl.mock.calls[0][5]).toEqual(undefined); 75 | }); 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/unit/service/actions/putObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('putObject', () => { 6 | it('accepts a bucket name, an object name and a file path', () => { 7 | let context = { 8 | client: { 9 | putObject: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | const size = 1535; 16 | const metaData = {foo: 'bar'}; 17 | const stream = {fooz: 'barz'}; 18 | return Service().actions.putObject.handler.bind(context)({params: stream, meta: {bucketName, objectName, size, metaData}}).then(r => { 19 | expect(context.client.putObject.mock.calls[0]).toEqual([bucketName, objectName, stream, size, metaData]); 20 | }); 21 | }); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/unit/service/actions/removeBucket.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('removeBucket', () => { 6 | it('removes a Bucket', () => { 7 | let context = { 8 | client: { 9 | removeBucket: jest.fn().mockReturnValue(Promise.resolve(true)) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'someBucket'; 14 | return Service().actions.removeBucket.handler.bind(context)({params: {bucketName}}).then(r => { 15 | expect(r).toEqual(true); 16 | expect(context.client.removeBucket.mock.calls[0]).toEqual([bucketName]); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/service/actions/removeIncompleteUpload.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('removeIncompleteUpload', () => { 6 | it('accepts a bucket name and an object name', () => { 7 | let context = { 8 | client: { 9 | removeIncompleteUpload: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | return Service().actions.removeIncompleteUpload.handler.bind(context)({params: {bucketName, objectName}}).then(r => { 16 | expect(context.client.removeIncompleteUpload.mock.calls[0]).toEqual([bucketName, objectName]); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/service/actions/removeObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('removeObject', () => { 6 | it('accepts a bucket name and an object name', () => { 7 | let context = { 8 | client: { 9 | removeObject: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | return Service().actions.removeObject.handler.bind(context)({params: {bucketName, objectName}}).then(r => { 16 | expect(context.client.removeObject.mock.calls[0]).toEqual([bucketName, objectName]); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/service/actions/removeObjects.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('removeObjects', () => { 6 | it('accepts a bucket name and an object name', () => { 7 | let context = { 8 | client: { 9 | removeObjects: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectNames = ['some-object']; 15 | return Service().actions.removeObjects.handler.bind(context)({params: {bucketName, objectNames}}).then(r => { 16 | expect(context.client.removeObjects.mock.calls[0]).toEqual([bucketName, objectNames]); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/service/actions/statObject.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | const Promise = require('bluebird'); 3 | describe('Service', () => { 4 | describe('actions', () => { 5 | describe('statObject', () => { 6 | it('accepts a bucket name and an object name', () => { 7 | let context = { 8 | client: { 9 | statObject: jest.fn().mockReturnValue(Promise.resolve()) 10 | }, 11 | Promise 12 | }; 13 | const bucketName = 'some-bucket'; 14 | const objectName = 'some-object'; 15 | return Service().actions.statObject.handler.bind(context)({params: {bucketName, objectName}}).then(r => { 16 | expect(context.client.statObject.mock.calls[0]).toEqual([bucketName, objectName]); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/service/created.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | describe('Service', () => { 3 | describe('created', () => { 4 | it('constructs a new mini client', () => { 5 | let context = { 6 | createMinioClient: jest.fn().mockReturnValue({foo: 'bar'}) 7 | }; 8 | let service = Service(); 9 | service.created.bind(context)(); 10 | expect(context.client).toEqual({foo: 'bar'}); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/unit/service/methods/createMinioClient.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | describe('Service', () => { 3 | describe('methods', () => { 4 | describe('createMinioClient', () => { 5 | it('constructs a new Minio Client', () => { 6 | let service = Service(); 7 | service.settings.endPoint = 'dfght'; 8 | service.settings.port = 12345; 9 | service.settings.useSSL = false; 10 | service.settings.accessKey = 'sadgds'; 11 | service.settings.secretKey = 'dfgdfg'; 12 | service.settings.region = 'sgegd'; 13 | service.settings.transport = {foo: 'bar'}; 14 | const client = service.methods.createMinioClient.bind(service)(); 15 | expect(client.constructor.name).toEqual('Client'); 16 | expect(client.accessKey).toEqual(service.settings.accessKey); 17 | expect(client.secretKey).toEqual(service.settings.secretKey); 18 | expect(client.host).toEqual(service.settings.endPoint); 19 | expect(client.port).toEqual(service.settings.port); 20 | expect(client.protocol).toEqual('http:'); 21 | expect(client.region).toEqual(service.settings.region); 22 | expect(client.transport).toEqual(service.settings.transport); 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/unit/service/methods/ping.spec.js: -------------------------------------------------------------------------------- 1 | const Service = require('service'); 2 | const Promise = require('bluebird'); 3 | 4 | describe('Service', () => { 5 | describe('methods', () => { 6 | describe('ping', () => { 7 | it('resolves if the backend is reachable', () => { 8 | let context = { 9 | client: { 10 | listBuckets: jest.fn().mockReturnValue(Promise.resolve()) 11 | }, 12 | Promise 13 | }; 14 | return Service.methods.ping.bind(context)({timeout: 10}).then(result => { 15 | expect(result).toEqual(true); 16 | }); 17 | }); 18 | 19 | it('rejects if the backend is not reachable', () => { 20 | let context = { 21 | client: { 22 | listBuckets: jest.fn().mockReturnValue(Promise.delay(1000)) 23 | }, 24 | Promise 25 | }; 26 | return Service.methods.ping.bind(context)({timeout: 10}).catch(e => { 27 | expect(e.constructor.name).toEqual('MinioPingError'); 28 | }); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/service/name.spec.js: -------------------------------------------------------------------------------- 1 | const Service = require("service"); 2 | describe("Service", () => { 3 | describe("name", () => { 4 | it("uses a sensitive default", () => { 5 | expect(Service.name).toEqual("minio"); 6 | }); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/unit/service/settings.spec.js: -------------------------------------------------------------------------------- 1 | const Service = require('service'); 2 | describe('Service', () => { 3 | describe('settings', () => { 4 | it('uses sensitive defaults', () => { 5 | expect(Service.settings).toEqual({ 6 | endpoint: undefined, 7 | port: undefined, 8 | useSSL: true, 9 | accessKey: undefined, 10 | secretKey: undefined, 11 | region: undefined, 12 | transport: undefined, 13 | sessionToken: undefined, 14 | minioHealthCheckInterval: 5000 15 | }); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/unit/service/stopped.spec.js: -------------------------------------------------------------------------------- 1 | const Service = () => require('service'); 2 | describe('Service', () => { 3 | describe('stopped', () => { 4 | it('does nothing if there is no healthCheckInterval', () => { 5 | let context = {}; 6 | let service = Service(); 7 | expect(service.stopped.bind(context)()).toEqual(undefined); 8 | }); 9 | 10 | it('stops the healthcheck interval', () => { 11 | let context = { 12 | healthCheckInterval: setInterval(() => {}, 10000) 13 | }; 14 | expect(Service().stopped.bind(context)()).toEqual(undefined); 15 | }); 16 | }); 17 | }); 18 | --------------------------------------------------------------------------------