├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitpod.yml ├── .husky └── pre-commit ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENCE ├── README.md ├── babel.config.js ├── create-dist-modules.sh ├── docker-compose.yml ├── lerna.json ├── package.json ├── packages ├── common │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── lib │ │ ├── common-disk.ts │ │ ├── driver.class.ts │ │ ├── enums │ │ │ └── driver-name.enum.ts │ │ ├── errors │ │ │ ├── move-failed.error.ts │ │ │ ├── not-found.error.ts │ │ │ └── unauthenticated.error.ts │ │ ├── index.ts │ │ ├── plugin.class.ts │ │ ├── types │ │ │ ├── any-object.type.ts │ │ │ ├── class.type.ts │ │ │ ├── disk-config.interface.ts │ │ │ ├── ftp-disk-config.interface.ts │ │ │ ├── gcs-disk-config.interface.ts │ │ │ ├── image-stats.interface.ts │ │ │ ├── local-disk-config.interface.ts │ │ │ ├── put-result.interface.ts │ │ │ ├── s3-disk-config.interface.ts │ │ │ ├── sftp-disk-config.interface.ts │ │ │ └── stats-result.interface.ts │ │ ├── utils.spec.ts │ │ └── utils.ts │ ├── package.json │ ├── tsconfig-cjs.build.json │ ├── tsconfig-esm.build.json │ └── tsconfig.json ├── core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── lib │ │ ├── file-storage.spec.ts │ │ ├── file-storage.ts │ │ ├── index.ts │ │ └── types.ts │ ├── package.json │ ├── tsconfig-cjs.build.json │ ├── tsconfig-esm.build.json │ └── tsconfig.json ├── ftp-driver │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── lib │ │ ├── __snapshots__ │ │ │ └── ftp-driver.spec.ts.snap │ │ ├── ftp-disk-config.interface.ts │ │ ├── ftp-driver.spec.ts │ │ ├── ftp-driver.ts │ │ └── index.ts │ ├── package.json │ ├── tsconfig-cjs.build.json │ ├── tsconfig-esm.build.json │ └── tsconfig.json ├── gcs-driver │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── lib │ │ ├── __snapshots__ │ │ │ └── gcs-driver.spec.ts.snap │ │ ├── gcs-disk-config.interface.ts │ │ ├── gcs-driver.spec.ts │ │ ├── gcs-driver.ts │ │ └── index.ts │ ├── package.json │ ├── tsconfig-cjs.build.json │ ├── tsconfig-esm.build.json │ └── tsconfig.json ├── image-manipulation │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── lib │ │ ├── __snapshots__ │ │ │ └── image-manipulation.spec.ts.snap │ │ ├── config.ts │ │ ├── image-manipulation.spec.ts │ │ ├── image-manipulation.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── package.json │ ├── tsconfig-cjs.build.json │ ├── tsconfig-esm.build.json │ └── tsconfig.json ├── local-driver │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── lib │ │ ├── __snapshots__ │ │ │ └── local-driver.spec.ts.snap │ │ ├── index.ts │ │ ├── local-disk-config.interface.ts │ │ ├── local-driver.spec.ts │ │ └── local-driver.ts │ ├── package.json │ ├── tsconfig-cjs.build.json │ ├── tsconfig-esm.build.json │ └── tsconfig.json ├── s3-driver │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── lib │ │ ├── index.ts │ │ ├── s3-disk-config.interface.ts │ │ ├── s3-driver.spec.ts │ │ └── s3-driver.ts │ ├── package.json │ ├── tsconfig-cjs.build.json │ ├── tsconfig-esm.build.json │ ├── tsconfig.build.json │ └── tsconfig.json └── sftp-driver │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── lib │ ├── __snapshots__ │ │ └── sftp-driver.spec.ts.snap │ ├── index.ts │ ├── sftp-disk-config.interface.ts │ ├── sftp-driver.spec.ts │ └── sftp-driver.ts │ ├── package.json │ ├── tsconfig-cjs.build.json │ ├── tsconfig-esm.build.json │ ├── tsconfig.build.json │ └── tsconfig.json ├── test └── support │ ├── files │ └── test-file.txt │ └── images │ ├── bird.jpeg │ └── photo-1000x750.jpeg ├── tsconfig-cjs.build.json ├── tsconfig-esm.build.json ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | babel.config.js 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 12, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["@typescript-eslint", "prettier"], 17 | "rules": { 18 | "@typescript-eslint/explicit-module-boundary-types": "off", 19 | "@typescript-eslint/no-var-requires": "off", 20 | "@typescript-eslint/no-explicit-any": "off", 21 | "prettier/prettier": "error", 22 | "no-console": "warn" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint_build_test: 14 | timeout-minutes: 10 15 | runs-on: ubuntu-22.04 16 | strategy: 17 | fail-fast: true 18 | matrix: 19 | os: [ubuntu-22.04] 20 | node-version: [v18.x] 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Start containers 27 | run: | 28 | docker-compose -f "docker-compose.yml" up -d 29 | echo "Waiting for all container healthy..." 30 | sleep 30 31 | docker-compose logs 32 | echo "Containers ready." 33 | 34 | # Setup node with specific version 35 | - name: Install node ${{ matrix.node-version }} 36 | uses: actions/setup-node@v2 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | 40 | - name: Receive yarn cached dependencies 41 | uses: actions/cache@v2 42 | id: yarn-cache # use this to check for `cache-hit` ==> if: steps.yarn-cache.outputs.cache-hit != 'true' 43 | with: 44 | path: '**/node_modules' 45 | key: ${{ runner.os }}-Yarn-${{ hashFiles('**/yarn.lock') }} 46 | 47 | - name: Install dependencies 48 | if: steps.yarn-cache.outputs.cache-hit != 'true' 49 | run: yarn --prefer-offline 50 | 51 | - name: Run lint, build, and test 52 | run: | 53 | yarn lint 54 | yarn build 55 | yarn test:detectOpenHandles 56 | 57 | - name: Stop containers 58 | if: always() 59 | run: docker-compose -f "docker-compose.yml" down 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | /storage 9 | 10 | # production 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | *.pem 16 | 17 | # debug 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | lerna-debug.log* 22 | 23 | # Note 24 | NOTE.md 25 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | 7 | tasks: 8 | - init: yarn install && yarn run build 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint 5 | yarn build -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.3.9](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.9) (2023-07-16) 7 | 8 | **Note:** Version bump only for package file-storage 9 | 10 | 11 | 12 | 13 | 14 | ## [1.3.8](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.8) (2023-07-16) 15 | 16 | **Note:** Version bump only for package file-storage 17 | 18 | 19 | 20 | 21 | 22 | ## [1.3.7](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.7) (2023-07-10) 23 | 24 | **Note:** Version bump only for package file-storage 25 | 26 | 27 | 28 | 29 | 30 | ## [1.3.6](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.6) (2023-06-04) 31 | 32 | **Note:** Version bump only for package file-storage 33 | 34 | 35 | 36 | 37 | 38 | ## [1.3.5](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.5) (2023-01-28) 39 | 40 | **Note:** Version bump only for package file-storage 41 | 42 | 43 | 44 | 45 | 46 | ## [1.3.4](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.2...v1.3.4) (2023-01-28) 47 | 48 | **Note:** Version bump only for package file-storage 49 | 50 | 51 | 52 | 53 | 54 | ## [1.3.4-alpha.2](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.1...v1.3.4-alpha.2) (2023-01-27) 55 | 56 | **Note:** Version bump only for package file-storage 57 | 58 | 59 | 60 | 61 | 62 | ## [1.3.4-alpha.1](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.0...v1.3.4-alpha.1) (2023-01-27) 63 | 64 | **Note:** Version bump only for package file-storage 65 | 66 | 67 | 68 | 69 | 70 | ## [1.3.4-alpha.0](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.4-alpha.0) (2023-01-26) 71 | 72 | **Note:** Version bump only for package file-storage 73 | 74 | 75 | 76 | 77 | 78 | ## [1.3.3](https://github.com/googlicius/file-storage/compare/v1.3.2...v1.3.3) (2022-04-22) 79 | 80 | **Note:** Version bump only for package file-storage 81 | 82 | 83 | 84 | 85 | 86 | ## [1.3.2](https://github.com/googlicius/file-storage/compare/v1.3.1...v1.3.2) (2022-01-01) 87 | 88 | **Note:** Version bump only for package file-storage 89 | 90 | 91 | 92 | 93 | 94 | ## [1.3.1](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.3.1) (2021-12-30) 95 | 96 | **Note:** Version bump only for package file-storage 97 | 98 | 99 | 100 | 101 | 102 | ## [1.2.9](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.2.9) (2021-11-13) 103 | 104 | **Note:** Version bump only for package file-storage 105 | 106 | 107 | 108 | 109 | 110 | ## [1.2.8](https://github.com/googlicius/file-storage/compare/v1.2.7...v1.2.8) (2021-10-28) 111 | 112 | **Note:** Version bump only for package file-storage 113 | 114 | 115 | 116 | 117 | 118 | ## [1.2.7](https://github.com/googlicius/file-storage/compare/v1.2.6...v1.2.7) (2021-10-22) 119 | 120 | **Note:** Version bump only for package file-storage 121 | 122 | 123 | 124 | 125 | 126 | ## [1.2.6](https://github.com/googlicius/file-storage/compare/v1.2.5...v1.2.6) (2021-10-11) 127 | 128 | **Note:** Version bump only for package file-storage 129 | 130 | 131 | 132 | 133 | 134 | ## [1.2.5](https://github.com/googlicius/file-storage/compare/v1.2.4...v1.2.5) (2021-09-09) 135 | 136 | **Note:** Version bump only for package file-storage 137 | 138 | 139 | 140 | 141 | 142 | ## 1.2.3 (2021-09-08) 143 | 144 | **Note:** Version bump only for package file-storage 145 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) googlicius 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 | ``` 2 | ____|_) | ___| | 3 | | | | _ \\___ \ __| _ \ __| _` | _` | _ \ 4 | __| | | __/ | | ( | | ( | ( | __/ 5 | _| _|_|\___|_____/ \__|\___/ _| \__,_|\__, |\___| 6 | A file system abstraction for Node.js |___/ 7 | ``` 8 | 9 | [![Test](https://github.com/googlicius/file-storage/actions/workflows/ci.yml/badge.svg)](https://github.com/googlicius/file-storage/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 10 | 11 | A simple abstraction to interact with file system inspired by [Laravel File System](https://laravel.com/docs/8.x/filesystem), provide one interface for many kind of drivers: `local`, `ftp`, `sftp`, `Amazon S3`, and `Google Cloud Storage`, even your custom driver. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | $ yarn add @file-storage/core @file-storage/common 17 | 18 | # Or npm 19 | $ npm install @file-storage/core @file-storage/common 20 | ``` 21 | 22 | And upload a file to local, it will be stored in `storage` folder in your root project directory by default: 23 | 24 | ```javascript 25 | import Storage from '@file-storage/core'; 26 | 27 | // Upload from file path. 28 | Storage.put('/my-image.png', '/path/of/destination/my-image.png'); 29 | 30 | // Or from a read-stream/buffer: 31 | Storage.put(stream, '/path/of/destination/my-image.png'); 32 | ``` 33 | 34 | ## Configuration 35 | 36 | By default only local driver is supported. To use another driver, you need to install corresponding package: 37 | 38 | - Amazon S3: `yarn add @file-storage/s3` 39 | - FTP: `yarn add @file-storage/ftp` 40 | - SFTP: `yarn add @file-storage/sftp` 41 | - Google Cloud Storage: `yarn add @file-storage/gcs` 42 | 43 | If there is no configuration, it will uploads to local disk. You can specific yours by using `config` method: 44 | 45 | ```typescript 46 | import Storage from '@file-storage/core'; 47 | import S3Driver, { S3DiskConfig } from '@file-storage/s3'; 48 | import LocalDriver, { LocalDiskConfig } from '@file-storage/local'; 49 | 50 | const localDisk: LocalDiskConfig = { 51 | driver: LocalDriver, 52 | name: 'local', 53 | root: 'public', 54 | }; 55 | 56 | const s3Disk: S3DiskConfig = { 57 | driver: S3Driver, 58 | name: 'mys3', 59 | bucketName: 'mybucket', 60 | // Uncomment if you want specify credentials manually. 61 | // region: 'ap-southeast-1', 62 | // credentials: { 63 | // accessKeyId: '123abc', 64 | // secretAccessKey: '123abc', 65 | // }, 66 | }; 67 | 68 | Storage.config({ 69 | // Default disk that you can access directly via Storage facade. 70 | defaultDiskName: 'mys3', 71 | diskConfigs: [localDisk, s3Disk], 72 | }); 73 | 74 | // Somewhere in your code... 75 | // Get file from s3: 76 | Storage.get('/path/to/s3-bucket/my-image.png'); 77 | ``` 78 | 79 | ## Unique file name 80 | 81 | Enable unique file name to prevent a file get replaced when uploading same file (or same name). 82 | The unique name generated by `uuid` to secure your file path. 83 | 84 | ```javascript 85 | Storage.config({ 86 | ... 87 | uniqueFileName: true, 88 | }); 89 | 90 | // The uploaded path could be like this: /path/to/e8a3e633-fc7f-4dde-b7f0-d2686bcd6836.jpeg 91 | ``` 92 | 93 | ## Obtain specific disk: 94 | 95 | To interact with a specific disk instead of the default, use `disk` method: 96 | 97 | ```typescript 98 | Storage.disk('local').get('/path/to/local/my-image.png'); 99 | 100 | // To adjust the configuration on the fly, you can specify the settings in the second argument: 101 | Storage.disk('local', { uniqueFileName: false }).put(...); 102 | ``` 103 | 104 | ## Create your custom driver 105 | 106 | If built-in drivers doesn't match your need, just defines a custom driver by extends `Driver` abstract class: 107 | 108 | ```typescript 109 | import Storage from '@file-storage/core'; 110 | import { Driver, DiskConfig } from '@file-storage/common'; 111 | 112 | interface MyCustomDiskConfig extends DiskConfig { 113 | driver: typeof MyCustomDriver; 114 | ... 115 | } 116 | 117 | class MyCustomDriver extends Driver { 118 | constructor(config: MyCustomDiskConfig) { 119 | super(config); 120 | ... 121 | } 122 | 123 | // Implement all Driver's methods here. 124 | } 125 | 126 | ``` 127 | 128 | And provide it to Storage.diskConfigs: 129 | 130 | ```typescript 131 | Storage.config({ 132 | diskConfigs: [ 133 | { 134 | driver: MyCustomDriver, 135 | name: 'myCustomDisk', 136 | ... 137 | } 138 | ], 139 | }); 140 | ``` 141 | 142 | ## Image manipulation 143 | 144 | To upload image and also creates many diferent sizes for web resonsive, install this package, it is acting as a plugin, will generates those images automatically. Images will be generated if the size reach given breakpoints. We provide 3 breakpoints by default: large: 1000, medium: 750, small: 500. And the thumbnail is also generaged by default. 145 | 146 | ```bash 147 | $ yarn add @file-storage/image-manipulation 148 | ``` 149 | 150 | And provide it to Storage config: 151 | 152 | ```typescript 153 | import ImageManipulation from '@file-storage/image-manipulation'; 154 | 155 | Storage.config({ 156 | ... 157 | plugins: [ImageManipulation], 158 | }); 159 | ``` 160 | 161 | #### Image manipulation customize 162 | 163 | You can customize responsive formats and thumbnail size: 164 | 165 | ```typescript 166 | import ImageManipulation from '@file-storage/image-manipulation'; 167 | 168 | ImageManipulation.config({ 169 | breakpoints: { 170 | size1: 500, 171 | size2: 800, 172 | }, 173 | thumbnailResizeOptions: { 174 | width: 333, 175 | height: 222, 176 | fit: 'contain', 177 | }, 178 | }); 179 | ``` 180 | 181 | ## TODO 182 | 183 | - [x] Create interface for all result (Need same result format for all drivers). 184 | - [x] Refactor `customDrivers` option: provides disk defination is enough. 185 | - [x] Implement GCS disk. 186 | - [ ] Put file from a local path. 187 | - [ ] API section: detailed of each driver. 188 | - [x] Remove `customDrivers` option, pass custom driver class directly to `diskConfigs.driver`. 189 | - [x] Unique file name. 190 | - [x] Update `aws-sdk` to v3. 191 | - [x] Replace `request` module with another module as it was deprecated. 192 | - [ ] Remove deprecated: BuiltInDiskConfig, DriverName. 193 | 194 | ## License 195 | 196 | MIT 197 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /create-dist-modules.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | cat >dist/cjs/package.json <dist/esm/package.json <; 6 | 7 | async uploadImageFromExternalUri( 8 | uri: string, 9 | path: string, 10 | ignoreHeaderContentType = false, 11 | ): Promise { 12 | const head = bent('HEAD'); 13 | const get = bent('GET', 200, 'buffer'); 14 | 15 | const res = await head(uri); 16 | if ( 17 | ignoreHeaderContentType || 18 | (res.headers['content-type'] && res.headers['content-type'].includes('image')) 19 | ) { 20 | const imageBuffer = await get(uri); 21 | return this.put(imageBuffer, path); 22 | } 23 | 24 | throw new Error('Not an image: ' + uri); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/common/lib/driver.class.ts: -------------------------------------------------------------------------------- 1 | import { Stream, Readable } from 'stream'; 2 | import sharp from 'sharp'; 3 | import { DriverName } from './enums/driver-name.enum'; 4 | import { DiskConfig } from './types/disk-config.interface'; 5 | import { ImageStats } from './types/image-stats.interface'; 6 | import { bytesToKbytes, getExt, getFileName, streamToBuffer } from './utils'; 7 | import { PutResult } from './types/put-result.interface'; 8 | import bent from 'bent'; 9 | 10 | export abstract class Driver { 11 | name: DriverName | string; 12 | 13 | constructor({ name }: DiskConfig) { 14 | this.name = name; 15 | 16 | return new Proxy(this, { 17 | get: (target, prop) => { 18 | if (typeof target[prop] !== 'function' || target[prop].name === 'url') { 19 | return target[prop]; 20 | } 21 | return async (...args: any[]) => { 22 | try { 23 | return await target[prop](...args); 24 | } catch (error) { 25 | if (!this.errorHandler) { 26 | throw error; 27 | } 28 | this.errorHandler(error); 29 | } 30 | }; 31 | }, 32 | }); 33 | } 34 | 35 | init?: () => Promise; 36 | 37 | protected errorHandler?(error: any): void; 38 | 39 | /** 40 | * Get file information. 41 | * 42 | * @param path File path. 43 | * @throws If file does not exists. 44 | */ 45 | protected stats?(path: string): Promise; 46 | 47 | /** 48 | * Get full url of the file 49 | * @param path string 50 | */ 51 | abstract url(path: string): string; 52 | 53 | /** 54 | * Determine if a file exists on the disk 55 | */ 56 | abstract exists(path: string): Promise; 57 | 58 | /** 59 | * Get size of a file in bytes 60 | */ 61 | abstract size(path: string): Promise; 62 | 63 | /** 64 | * This methods returns the UNIX timestamp of the last time the file was modified. 65 | */ 66 | abstract lastModified(path: string): Promise; 67 | 68 | /** 69 | * Put to specific disk from a stream or buffer. 70 | * 71 | * @param data stream.Stream | Buffer 72 | * @param path string 73 | * @throws If file does not exists. 74 | */ 75 | abstract put(data: Stream | Buffer, path: string): Promise>; 76 | 77 | /** 78 | * Get a file. 79 | * @param path string 80 | */ 81 | abstract get(path: string): Stream | Readable | Promise; 82 | 83 | /** 84 | * Delete a file 85 | * 86 | * @param path Path of file. 87 | * @throws If deleting failed. 88 | */ 89 | abstract delete(path: string): Promise; 90 | 91 | /** 92 | * Copy a file to new location. 93 | * 94 | * @param path File path. 95 | * @param newPath New file path. 96 | * @throws If file does not exists. 97 | */ 98 | abstract copy(path: string, newPath: string): Promise; 99 | 100 | /** 101 | * Move a file to new location. 102 | * 103 | * @param path File path. 104 | * @param newPath New file path. 105 | * @throws If file does not exists. 106 | */ 107 | abstract move(path: string, newPath: string): Promise; 108 | 109 | /** 110 | * Append to a file. 111 | * 112 | * @param data string | Buffer 113 | * @param path File path. 114 | */ 115 | abstract append(data: string | Buffer, path: string): Promise; 116 | 117 | /** 118 | * Prepend to a file. 119 | * 120 | * @param data string | Buffer 121 | * @param path File path. 122 | */ 123 | // abstract prepend(data: string | Buffer, path: string): Promise; 124 | 125 | /** 126 | * This method will create the given directory, including any needed subdirectories. 127 | * 128 | * @throws If directory already exists. 129 | */ 130 | abstract makeDir(dir: string): Promise; 131 | 132 | /** 133 | * Remove given directory and all of its files. 134 | * 135 | * @throws If cannot remove. 136 | */ 137 | abstract removeDir(dir: string): Promise; 138 | 139 | /** 140 | * Upload image from specific URI and store it into `filename` 141 | * 142 | * @param uri URI of image 143 | * @param path Filename included location to store image. 144 | * @param ignoreHeaderContentType ignore checking content-type header. 145 | * @returns Promise 146 | */ 147 | async uploadImageFromExternalUri( 148 | uri: string, 149 | path: string, 150 | ignoreHeaderContentType = false, 151 | ): Promise { 152 | const head = bent('HEAD'); 153 | const get = bent('GET', 200, 'buffer'); 154 | 155 | const res = await head(uri); 156 | if ( 157 | ignoreHeaderContentType || 158 | (res.headers['content-type'] && res.headers['content-type'].includes('image')) 159 | ) { 160 | const imageBuffer = await get(uri); 161 | return this.put(imageBuffer, path); 162 | } 163 | 164 | throw new Error('Not an image: ' + uri); 165 | } 166 | 167 | /** 168 | * Get image metadatas. 169 | * 170 | * @param path Path of image 171 | * @param keepBuffer Put image buffer in the result. 172 | */ 173 | imageStats(path: string, keepBuffer: true): Promise; 174 | imageStats(path: string, keepBuffer?: boolean): Promise; 175 | 176 | async imageStats(path: string, keepBuffer = false): Promise { 177 | const stream = await this.get(path); 178 | const buffer = await streamToBuffer(stream); 179 | const { format, size, width, height } = await sharp(buffer).metadata(); 180 | 181 | return { 182 | name: getFileName(path), 183 | path, 184 | size: bytesToKbytes(size), 185 | width, 186 | height, 187 | mime: format, 188 | ext: getExt(path), 189 | hash: null, 190 | ...(keepBuffer && { buffer }), 191 | }; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /packages/common/lib/enums/driver-name.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supported driver name. 3 | * @deprecated Use driver class when providing driver instead. 4 | */ 5 | export enum DriverName { 6 | LOCAL = 'local', 7 | S3 = 's3', 8 | FTP = 'ftp', 9 | SFTP = 'sftp', 10 | GCS = 'gcs', 11 | } 12 | -------------------------------------------------------------------------------- /packages/common/lib/errors/move-failed.error.ts: -------------------------------------------------------------------------------- 1 | export class MoveFailedError extends Error { 2 | message = 'File move failed.'; 3 | code = 'MoveFailed'; 4 | 5 | constructor(message?: string) { 6 | super(message); 7 | if (message) { 8 | this.message = message; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/common/lib/errors/not-found.error.ts: -------------------------------------------------------------------------------- 1 | export class FileNotFoundError extends Error { 2 | message = 'File not found'; 3 | code = 'FileNotFound'; 4 | 5 | constructor(message?: string) { 6 | super(message); 7 | if (message) { 8 | this.message = message; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/common/lib/errors/unauthenticated.error.ts: -------------------------------------------------------------------------------- 1 | export class UnauthenticatedError extends Error { 2 | code = 'Unauthenticated'; 3 | message = 'Unauthenticated'; 4 | 5 | constructor(message?: string) { 6 | super(); 7 | if (message) { 8 | this.message = message; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/common/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common-disk'; 2 | export * from './utils'; 3 | export * from './types/any-object.type'; 4 | export * from './types/disk-config.interface'; 5 | export * from './types/local-disk-config.interface'; 6 | export * from './types/s3-disk-config.interface'; 7 | export * from './types/ftp-disk-config.interface'; 8 | export * from './types/sftp-disk-config.interface'; 9 | export * from './types/image-stats.interface'; 10 | export * from './types/class.type'; 11 | export * from './types/put-result.interface'; 12 | export * from './types/gcs-disk-config.interface'; 13 | export * from './driver.class'; 14 | export * from './plugin.class'; 15 | export * from './enums/driver-name.enum'; 16 | export * from './errors/not-found.error'; 17 | export * from './errors/unauthenticated.error'; 18 | export * from './errors/move-failed.error'; 19 | -------------------------------------------------------------------------------- /packages/common/lib/plugin.class.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'stream'; 2 | import { Driver } from './driver.class'; 3 | 4 | export abstract class Plugin { 5 | disk: Driver; 6 | 7 | beforePutKey?: string; 8 | afterPutKey?: string; 9 | 10 | init(disk: Driver) { 11 | this.disk = disk; 12 | } 13 | 14 | /** 15 | * Hook runs before put. 16 | */ 17 | beforePut?(stream: Stream | Buffer, path: string): Promise; 18 | 19 | /** 20 | * Hook runs after put. 21 | */ 22 | afterPut?(path: string): Promise; 23 | } 24 | -------------------------------------------------------------------------------- /packages/common/lib/types/any-object.type.ts: -------------------------------------------------------------------------------- 1 | export type AnyObject = { 2 | [x: string]: any; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/common/lib/types/class.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type of classes that implemented I. 3 | */ 4 | export type Class = new (...args: Args) => I; 5 | -------------------------------------------------------------------------------- /packages/common/lib/types/disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Driver } from '../driver.class'; 2 | import { Class } from './class.type'; 3 | 4 | export interface DiskConfig { 5 | driver: string | Class; 6 | name: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/lib/types/ftp-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { AccessOptions, FTPContext } from 'basic-ftp'; 2 | import { Driver } from '../driver.class'; 3 | import { DriverName } from '../enums/driver-name.enum'; 4 | import { Class } from './class.type'; 5 | import { DiskConfig } from './disk-config.interface'; 6 | 7 | export interface FtpDiskConfig extends DiskConfig { 8 | driver: DriverName.FTP | Class; 9 | root?: string; 10 | accessOptions: AccessOptions; 11 | ftpContext?: Partial; 12 | } 13 | -------------------------------------------------------------------------------- /packages/common/lib/types/gcs-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { StorageOptions } from '@google-cloud/storage'; 2 | import { Driver } from '../driver.class'; 3 | import { DriverName } from '../enums/driver-name.enum'; 4 | import { Class } from './class.type'; 5 | import { DiskConfig } from './disk-config.interface'; 6 | 7 | export interface GCSDiskConfig extends DiskConfig, StorageOptions { 8 | driver: DriverName.GCS | Class; 9 | bucketName: string; 10 | /** 11 | * A custom public url instead of default public path. 12 | */ 13 | publicUrl?: string; 14 | } 15 | -------------------------------------------------------------------------------- /packages/common/lib/types/image-stats.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ImageStats { 2 | name?: string; 3 | hash?: string; 4 | ext: string; 5 | mime: string; 6 | width?: number; 7 | height?: number; 8 | size: number; 9 | path: string; 10 | buffer?: Buffer; 11 | } 12 | -------------------------------------------------------------------------------- /packages/common/lib/types/local-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Driver } from '../driver.class'; 2 | import { DriverName } from '../enums/driver-name.enum'; 3 | import { Class } from './class.type'; 4 | import { DiskConfig } from './disk-config.interface'; 5 | 6 | export interface LocalDiskConfig extends DiskConfig { 7 | driver: DriverName.LOCAL | Class; 8 | root?: string; 9 | publicUrl?: string; 10 | } 11 | -------------------------------------------------------------------------------- /packages/common/lib/types/put-result.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PutResult { 2 | success: boolean; 3 | message: string; 4 | name?: string; 5 | path: string; 6 | [x: string]: any; 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/lib/types/s3-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { S3ClientConfig } from '@aws-sdk/client-s3'; 2 | import { Driver } from '../driver.class'; 3 | import { DriverName } from '../enums/driver-name.enum'; 4 | import { Class } from './class.type'; 5 | import { DiskConfig } from './disk-config.interface'; 6 | 7 | export interface S3DiskConfig extends DiskConfig, S3ClientConfig { 8 | driver: DriverName.S3 | Class; 9 | bucketName: string; 10 | /** 11 | * A custom public url instead of default s3 public path. 12 | */ 13 | publicUrl?: string; 14 | } 15 | -------------------------------------------------------------------------------- /packages/common/lib/types/sftp-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Driver } from '../driver.class'; 2 | import { DriverName } from '../enums/driver-name.enum'; 3 | import { Class } from './class.type'; 4 | import { DiskConfig } from './disk-config.interface'; 5 | 6 | export interface SftpDiskConfig extends DiskConfig { 7 | driver: DriverName.SFTP | Class; 8 | root?: string; 9 | accessOptions: { 10 | host: string; 11 | port?: number; 12 | username: string; 13 | password: string; 14 | privateKey?: string | Buffer; 15 | [x: string]: any; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/common/lib/types/stats-result.interface.ts: -------------------------------------------------------------------------------- 1 | export interface StatsResult { 2 | lastModified: number; 3 | size: number; 4 | } 5 | -------------------------------------------------------------------------------- /packages/common/lib/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { getExt } from '../dist'; 2 | import { getFileName } from './utils'; 3 | 4 | describe('Common utilities', () => { 5 | test('getFileName', () => { 6 | expect(getFileName('file-name.png')).toEqual('file-name.png'); 7 | expect(getFileName('/file-name.png')).toEqual('file-name.png'); 8 | expect(getFileName('path/to/file-name.png')).toEqual('file-name.png'); 9 | expect(getFileName('path/to/file-name.png?v=foo&t=bar')).toEqual('file-name.png'); 10 | }); 11 | 12 | test('getExt', () => { 13 | expect(getExt('path/to/file-name.png')).toEqual('png'); 14 | expect(getExt('path/to/file-name.png?v=foo&t=bar')).toEqual('png'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/common/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { Readable, Stream, PassThrough } from 'stream'; 4 | 5 | export const ensureDirectoryExistence = (filePath: string) => { 6 | const dirname = path.dirname(filePath); 7 | if (fs.existsSync(dirname)) { 8 | return true; 9 | } 10 | fs.mkdirSync(dirname, { recursive: true }); 11 | }; 12 | 13 | /** 14 | * Convert data to readable stream if it is a buffer or string. 15 | */ 16 | // TODO rename to toReadable. 17 | export function toStream(buf: Buffer | Stream | string, chunkSize?: number): Readable { 18 | if (typeof buf === 'string') { 19 | buf = Buffer.from(buf, 'utf8'); 20 | } 21 | if (!Buffer.isBuffer(buf)) { 22 | if (!(buf instanceof Readable)) { 23 | const pass = new PassThrough(); 24 | buf.pipe(pass); 25 | return pass; 26 | } 27 | 28 | return buf; 29 | } 30 | 31 | const reader = new Readable(); 32 | const hwm = reader.readableHighWaterMark; 33 | 34 | // If chunkSize is invalid, set to highWaterMark. 35 | if (!chunkSize || typeof chunkSize !== 'number' || chunkSize < 1 || chunkSize > hwm) { 36 | chunkSize = hwm; 37 | } 38 | 39 | const len = buf.length; 40 | let start = 0; 41 | 42 | // Overwrite _read method to push data from buffer. 43 | reader._read = function () { 44 | while (reader.push((buf).slice(start, (start += chunkSize)))) { 45 | // If all data pushed, just break the loop. 46 | if (start >= len) { 47 | reader.push(null); 48 | break; 49 | } 50 | } 51 | }; 52 | return reader; 53 | } 54 | 55 | /** 56 | * Check if file/directory exists in local 57 | */ 58 | export function exists(path: string): Promise { 59 | return new Promise((resolve, reject) => { 60 | fs.stat(path, (err) => { 61 | if (err) { 62 | reject(err); 63 | return; 64 | } 65 | resolve(); 66 | }); 67 | }); 68 | } 69 | 70 | /** 71 | * Require a module es module as its default value. 72 | */ 73 | export function requireDefaultModule(path: string, notThrow = true) { 74 | try { 75 | return require(path).default; 76 | } catch (error) { 77 | if (notThrow) { 78 | return null; 79 | } 80 | throw error; 81 | } 82 | } 83 | 84 | /** 85 | * Get root directory. 86 | */ 87 | export function getRootCwd() { 88 | const cwd = process.cwd(); 89 | const cwdArr = cwd.split('/'); 90 | 91 | if (cwdArr[cwdArr.length - 2] === 'packages') { 92 | cwdArr.splice(cwdArr.length - 2, 2); 93 | return cwdArr.join('/'); 94 | } 95 | return cwd; 96 | } 97 | 98 | export async function streamToBuffer(stream: Stream): Promise { 99 | return new Promise((resolve, reject) => { 100 | const _buf = Array(); 101 | 102 | stream.on('data', (chunk) => { 103 | _buf.push(chunk); 104 | }); 105 | stream.on('end', () => { 106 | resolve(Buffer.concat(_buf)); 107 | }); 108 | stream.on('error', (err) => reject(`error converting stream - ${err}`)); 109 | }); 110 | } 111 | 112 | export function getExt(filepath: string) { 113 | return filepath.split('?')[0].split('#')[0].split('.').pop(); 114 | } 115 | 116 | /** 117 | * Get file name from give path. 118 | */ 119 | export function getFileName(filePath: string) { 120 | return filePath 121 | .split('?')[0] 122 | .split('#')[0] 123 | .replace(/^.*[\\/]/, ''); 124 | } 125 | 126 | export const bytesToKbytes = (bytes: number) => Math.round((bytes / 1000) * 100) / 100; 127 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@file-storage/common", 3 | "version": "1.3.9", 4 | "description": "Common utilities, types, interfaces,... for `file-storage`.", 5 | "author": "Dang Nguyen ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "dist/esm/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "exports": { 14 | "import": "./dist/esm/index.js", 15 | "require": "./dist/cjs/index.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/googlicius/file-storage.git" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org", 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "yarn clean && yarn compile", 27 | "clean": "rimraf -rf ./dist", 28 | "compile": "tsc -p tsconfig-cjs.build.json && tsc -p tsconfig-esm.build.json && ../../create-dist-modules.sh" 29 | }, 30 | "dependencies": { 31 | "bent": "^7.3.12", 32 | "sharp": "^0.32.6" 33 | }, 34 | "devDependencies": { 35 | "@types/sharp": "^0.28.5", 36 | "rimraf": "~3.0.2", 37 | "typescript": "~4.3.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/common/tsconfig-cjs.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-cjs.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "declarationDir": "./dist/types" 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/common/tsconfig-esm.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-esm.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "declaration": false 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | storage 5 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.3.9](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.9) (2023-07-16) 7 | 8 | **Note:** Version bump only for package @file-storage/core 9 | 10 | 11 | 12 | 13 | 14 | ## [1.3.8](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.8) (2023-07-16) 15 | 16 | **Note:** Version bump only for package @file-storage/core 17 | 18 | 19 | 20 | 21 | 22 | ## [1.3.7](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.7) (2023-07-10) 23 | 24 | **Note:** Version bump only for package @file-storage/core 25 | 26 | 27 | 28 | 29 | 30 | ## [1.3.6](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.6) (2023-06-04) 31 | 32 | **Note:** Version bump only for package @file-storage/core 33 | 34 | 35 | 36 | 37 | 38 | ## [1.3.5](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.5) (2023-01-28) 39 | 40 | **Note:** Version bump only for package @file-storage/core 41 | 42 | 43 | 44 | 45 | 46 | ## [1.3.4](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.2...v1.3.4) (2023-01-28) 47 | 48 | **Note:** Version bump only for package @file-storage/core 49 | 50 | 51 | 52 | 53 | 54 | ## [1.3.4-alpha.2](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.1...v1.3.4-alpha.2) (2023-01-27) 55 | 56 | **Note:** Version bump only for package @file-storage/core 57 | 58 | 59 | 60 | 61 | 62 | ## [1.3.4-alpha.1](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.0...v1.3.4-alpha.1) (2023-01-27) 63 | 64 | **Note:** Version bump only for package @file-storage/core 65 | 66 | 67 | 68 | 69 | 70 | ## [1.3.4-alpha.0](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.4-alpha.0) (2023-01-26) 71 | 72 | **Note:** Version bump only for package @file-storage/core 73 | 74 | 75 | 76 | 77 | 78 | ## [1.3.3](https://github.com/googlicius/file-storage/compare/v1.3.2...v1.3.3) (2022-04-22) 79 | 80 | **Note:** Version bump only for package @file-storage/core 81 | 82 | 83 | 84 | 85 | 86 | ## [1.3.2](https://github.com/googlicius/file-storage/compare/v1.3.1...v1.3.2) (2022-01-01) 87 | 88 | **Note:** Version bump only for package @file-storage/core 89 | 90 | 91 | 92 | 93 | 94 | ## [1.3.1](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.3.1) (2021-12-30) 95 | 96 | **Note:** Version bump only for package @file-storage/core 97 | 98 | 99 | 100 | 101 | 102 | ## [1.2.9](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.2.9) (2021-11-13) 103 | 104 | **Note:** Version bump only for package @file-storage/core 105 | 106 | 107 | 108 | 109 | 110 | ## [1.2.8](https://github.com/googlicius/file-storage/compare/v1.2.7...v1.2.8) (2021-10-28) 111 | 112 | **Note:** Version bump only for package @file-storage/core 113 | 114 | 115 | 116 | 117 | 118 | ## [1.2.7](https://github.com/googlicius/file-storage/compare/v1.2.6...v1.2.7) (2021-10-22) 119 | 120 | **Note:** Version bump only for package @file-storage/core 121 | 122 | 123 | 124 | 125 | 126 | ## [1.2.6](https://github.com/googlicius/file-storage/compare/v1.2.5...v1.2.6) (2021-10-11) 127 | 128 | **Note:** Version bump only for package @file-storage/core 129 | 130 | 131 | 132 | 133 | 134 | ## [1.2.5](https://github.com/googlicius/file-storage/compare/v1.2.4...v1.2.5) (2021-09-09) 135 | 136 | **Note:** Version bump only for package @file-storage/core 137 | 138 | 139 | 140 | 141 | 142 | ## 1.2.3 (2021-09-08) 143 | 144 | **Note:** Version bump only for package @file-storage/core 145 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____|_) | ___| | 3 | | | | _ \\___ \ __| _ \ __| _` | _` | _ \ 4 | __| | | __/ | | ( | | ( | ( | __/ 5 | _| _|_|\___|_____/ \__|\___/ _| \__,_|\__, |\___| 6 | A file system abstraction for Node.js |___/ 7 | ``` 8 | 9 | [![Test](https://github.com/googlicius/file-storage/actions/workflows/ci.yml/badge.svg)](https://github.com/googlicius/file-storage/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 10 | 11 | A simple abstraction to interact with file system inspired by [Laravel File System](https://laravel.com/docs/8.x/filesystem), provide one interface for many kind of drivers: `local`, `ftp`, `sftp`, `Amazon S3`, and `Google Cloud Storage`, even your custom driver. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | $ yarn add @file-storage/core @file-storage/common 17 | 18 | # Or npm 19 | $ npm install @file-storage/core @file-storage/common 20 | ``` 21 | 22 | And upload a file to local, it will be stored in `storage` folder in your root project directory by default: 23 | 24 | ```javascript 25 | import Storage from '@file-storage/core'; 26 | 27 | // Upload from file path. 28 | Storage.put('/my-image.png', '/path/of/destination/my-image.png'); 29 | 30 | // Or from a read-stream/buffer: 31 | Storage.put(stream, '/path/of/destination/my-image.png'); 32 | ``` 33 | 34 | ## Configuration 35 | 36 | By default only local driver is supported. To use another driver, you need to install corresponding package: 37 | 38 | - Amazon S3: `yarn add @file-storage/s3` 39 | - FTP: `yarn add @file-storage/ftp` 40 | - SFTP: `yarn add @file-storage/sftp` 41 | - Google Cloud Storage: `yarn add @file-storage/gcs` 42 | 43 | If there is no configuration, it will uploads to local disk. You can specific yours by using `config` method: 44 | 45 | ```typescript 46 | import Storage from '@file-storage/core'; 47 | import S3Driver, { S3DiskConfig } from '@file-storage/s3'; 48 | import LocalDriver, { LocalDiskConfig } from '@file-storage/local'; 49 | 50 | const localDisk: LocalDiskConfig = { 51 | driver: LocalDriver, 52 | name: 'local', 53 | root: 'public', 54 | }; 55 | 56 | const s3Disk: S3DiskConfig = { 57 | driver: S3Driver, 58 | name: 'mys3', 59 | bucketName: 'mybucket', 60 | // Uncomment if you want specify credentials manually. 61 | // region: 'ap-southeast-1', 62 | // credentials: { 63 | // accessKeyId: '123abc', 64 | // secretAccessKey: '123abc', 65 | // }, 66 | }; 67 | 68 | Storage.config({ 69 | // Default disk that you can access directly via Storage facade. 70 | defaultDiskName: 'mys3', 71 | diskConfigs: [localDisk, s3Disk], 72 | }); 73 | 74 | // Somewhere in your code... 75 | // Get file from s3: 76 | Storage.get('/path/to/s3-bucket/my-image.png'); 77 | ``` 78 | 79 | ## Unique file name 80 | 81 | Enable unique file name to prevent a file get replaced when uploading same file (or same name). 82 | The unique name generated by `uuid` to secure your file path. 83 | 84 | ```javascript 85 | Storage.config({ 86 | ... 87 | uniqueFileName: true, 88 | }); 89 | 90 | // The uploaded path could be like this: /path/to/e8a3e633-fc7f-4dde-b7f0-d2686bcd6836.jpeg 91 | ``` 92 | 93 | ## Obtain specific disk: 94 | 95 | To interact with a specific disk instead of the default, use `disk` method: 96 | 97 | ```typescript 98 | Storage.disk('local').get('/path/to/local/my-image.png'); 99 | 100 | // To adjust the configuration on the fly, you can specify the settings in the second argument: 101 | Storage.disk('local', { uniqueFileName: false }).put(...); 102 | ``` 103 | 104 | ## Create your custom driver 105 | 106 | If built-in drivers doesn't match your need, just defines a custom driver by extends `Driver` abstract class: 107 | 108 | ```typescript 109 | import Storage from '@file-storage/core'; 110 | import { Driver, DiskConfig } from '@file-storage/common'; 111 | 112 | interface MyCustomDiskConfig extends DiskConfig { 113 | driver: typeof MyCustomDriver; 114 | ... 115 | } 116 | 117 | class MyCustomDriver extends Driver { 118 | constructor(config: MyCustomDiskConfig) { 119 | super(config); 120 | ... 121 | } 122 | 123 | // Implement all Driver's methods here. 124 | } 125 | 126 | ``` 127 | 128 | And provide it to Storage.diskConfigs: 129 | 130 | ```typescript 131 | Storage.config({ 132 | diskConfigs: [ 133 | { 134 | driver: MyCustomDriver, 135 | name: 'myCustomDisk', 136 | ... 137 | } 138 | ], 139 | }); 140 | ``` 141 | 142 | ## Image manipulation 143 | 144 | To upload image and also creates many diferent sizes for web resonsive, install this package, it is acting as a plugin, will generates those images automatically. Images will be generated if the size reach given breakpoints. We provide 3 breakpoints by default: large: 1000, medium: 750, small: 500. And the thumbnail is also generaged by default. 145 | 146 | ```bash 147 | $ yarn add @file-storage/image-manipulation 148 | ``` 149 | 150 | And provide it to Storage config: 151 | 152 | ```typescript 153 | import ImageManipulation from '@file-storage/image-manipulation'; 154 | 155 | Storage.config({ 156 | ... 157 | plugins: [ImageManipulation], 158 | }); 159 | ``` 160 | 161 | #### Image manipulation customize 162 | 163 | You can customize responsive formats and thumbnail size: 164 | 165 | ```typescript 166 | import ImageManipulation from '@file-storage/image-manipulation'; 167 | 168 | ImageManipulation.config({ 169 | breakpoints: { 170 | size1: 500, 171 | size2: 800, 172 | }, 173 | thumbnailResizeOptions: { 174 | width: 333, 175 | height: 222, 176 | fit: 'contain', 177 | }, 178 | }); 179 | ``` 180 | 181 | ## TODO 182 | 183 | - [x] Create interface for all result (Need same result format for all drivers). 184 | - [x] Refactor `customDrivers` option: provides disk defination is enough. 185 | - [x] Implement GCS disk. 186 | - [ ] Put file from a local path. 187 | - [ ] API section: detailed of each driver. 188 | - [x] Remove `customDrivers` option, pass custom driver class directly to `diskConfigs.driver`. 189 | - [x] Unique file name. 190 | - [x] Update `aws-sdk` to v3. 191 | - [x] Replace `request` module with another module as it was deprecated. 192 | - [ ] Remove deprecated: BuiltInDiskConfig, DriverName. 193 | 194 | ## License 195 | 196 | MIT 197 | -------------------------------------------------------------------------------- /packages/core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../../babel.config.js', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/core/lib/file-storage.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { Storage, StorageClass } from './file-storage'; 3 | import { DriverName, getExt, getRootCwd, LocalDiskConfig } from '@file-storage/common'; 4 | import S3Driver, { S3DiskConfig } from '@file-storage/s3'; 5 | import { BuiltInDiskConfig } from './types'; 6 | 7 | describe('Storage', () => { 8 | test('Auto set default disk name when there is only one disk-config', () => { 9 | const TestStorage = new StorageClass(); 10 | 11 | TestStorage.config({ 12 | diskConfigs: [ 13 | { 14 | driver: DriverName.LOCAL, 15 | name: 'my-local', 16 | }, 17 | ], 18 | }); 19 | 20 | expect(TestStorage.name).toEqual('my-local'); 21 | }); 22 | 23 | describe('Storage: No config specified.', () => { 24 | let TestStorage: StorageClass; 25 | 26 | beforeAll(() => { 27 | TestStorage = new StorageClass(); 28 | }); 29 | 30 | test('Default disk is local if there is no diskType specific.', () => { 31 | expect(TestStorage.name).toEqual('local'); 32 | }); 33 | 34 | test('Upload image to local success.', () => { 35 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 36 | return expect( 37 | TestStorage.put(fileReadStream, 'test_upload/bird.jpeg'), 38 | ).resolves.toMatchObject({ 39 | success: true, 40 | message: 'Uploading success!', 41 | }); 42 | }); 43 | }); 44 | 45 | describe('Storage as a disk', () => { 46 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 47 | 48 | test('Upload image to local disk', async () => { 49 | const result = await Storage.put(fileReadStream, 'test_upload/bird.jpeg'); 50 | expect(result).toMatchObject({ 51 | success: true, 52 | message: 'Uploading success!', 53 | path: 'test_upload/bird.jpeg', 54 | name: 'bird.jpeg', 55 | }); 56 | }); 57 | }); 58 | 59 | describe('Unique file name', () => { 60 | beforeAll(() => { 61 | Storage.config({ 62 | uniqueFileName: true, 63 | }); 64 | }); 65 | 66 | test('Put unique file name', async () => { 67 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 68 | const result = await Storage.put(fileReadStream, 'bird-image/bird.jpeg'); 69 | expect(getExt(result.path)).toEqual('jpeg'); 70 | expect(result.path).not.toEqual('bird-image/bird.jpeg'); 71 | expect(result).toMatchObject({ 72 | success: true, 73 | message: 'Uploading success!', 74 | }); 75 | }); 76 | }); 77 | 78 | describe('Storage: config errors.', () => { 79 | test('Duplicated disk name.', () => { 80 | expect(() => 81 | Storage.config({ 82 | defaultDiskName: 'myDisk', 83 | diskConfigs: [ 84 | { 85 | driver: DriverName.LOCAL, 86 | name: 'myDisk', 87 | root: 'storage', 88 | }, 89 | { 90 | driver: DriverName.S3, 91 | name: 'myDisk', 92 | bucketName: 'startover', 93 | }, 94 | ], 95 | }), 96 | ).toThrowError('Duplicated disk name.'); 97 | }); 98 | 99 | test('Please specify a default disk name.', () => { 100 | const MyStorage = new StorageClass(); 101 | 102 | expect(() => 103 | MyStorage.config({ 104 | diskConfigs: [ 105 | { 106 | driver: DriverName.LOCAL, 107 | name: 'myDisk', 108 | root: 'storage', 109 | }, 110 | { 111 | driver: DriverName.S3, 112 | name: 's3', 113 | bucketName: 'startover', 114 | }, 115 | ], 116 | }), 117 | ).toThrowError('Please specify a default disk name.'); 118 | }); 119 | 120 | test('Driver is not declared', () => { 121 | expect(() => { 122 | Storage.config({ 123 | defaultDiskName: 'not-exists-disk', 124 | diskConfigs: [ 125 | { 126 | driver: 'onedriver', 127 | name: 'not-exists-disk', 128 | }, 129 | ], 130 | }); 131 | Storage.disk('not-exists-disk'); 132 | }).toThrowError(`Driver 'onedriver' is not declared.`); 133 | }); 134 | 135 | test('Given disk is not defined', () => { 136 | expect(() => { 137 | Storage.config({ 138 | defaultDiskName: 'myDisk2', 139 | diskConfigs: [ 140 | { 141 | driver: DriverName.LOCAL, 142 | name: 'myDisk', 143 | root: 'storage', 144 | }, 145 | ], 146 | }); 147 | Storage.disk('not-exists-disk'); 148 | }).toThrowError('Given disk is not defined: myDisk2'); 149 | }); 150 | 151 | // TODO Able to test this case. 152 | // test('Driver is not installed', () => { 153 | // expect(() => { 154 | // Storage.config({ 155 | // diskConfigs: [ 156 | // { 157 | // driver: Driver.S3, 158 | // name: 'mys3', 159 | // bucketName: 'myBucket', 160 | // }, 161 | // ], 162 | // }); 163 | // }).toThrowError('Please install `@file-storage/s3` for s3 driver'); 164 | // }); 165 | }); 166 | 167 | describe('disk', () => { 168 | let NewStorage: StorageClass; 169 | 170 | beforeAll(async () => { 171 | NewStorage = new StorageClass(); 172 | 173 | NewStorage.config({ 174 | defaultDiskName: 'local', 175 | uniqueFileName: true, 176 | diskConfigs: [ 177 | { 178 | driver: DriverName.LOCAL, 179 | name: 'local', 180 | root: 'storage', 181 | }, 182 | { 183 | driver: S3Driver, 184 | name: 's3', 185 | bucketName: 'mybucket1', 186 | endpoint: 'http://localhost:4566', 187 | forcePathStyle: true, 188 | region: 'us-east-1', 189 | credentials: { 190 | accessKeyId: 'test', 191 | secretAccessKey: 'test123', 192 | }, 193 | } as S3DiskConfig, 194 | ], 195 | }); 196 | 197 | await NewStorage.disk('s3').instance().setupMockS3('mybucket1'); 198 | }); 199 | 200 | it('should return storage of another disk', () => { 201 | expect(NewStorage.disk('s3').name).toEqual('s3'); 202 | }); 203 | 204 | it('should return current disk', () => { 205 | expect(NewStorage.disk({}).name).toEqual('local'); 206 | }); 207 | 208 | it('should respect uniqueFileName when changing to another disk', async () => { 209 | const result = await NewStorage.disk('s3').put(Buffer.from('Test text'), 'test.txt'); 210 | 211 | expect(result.path.length).toEqual(36 + '.txt'.length); 212 | }); 213 | 214 | it('should not mutate uniqueFileName of Storage facade', async () => { 215 | const s3Result = await NewStorage.disk('s3', { 216 | uniqueFileName: false, 217 | }).put(Buffer.from('Test text'), 'test.txt'); 218 | 219 | const localResult = await NewStorage.put(Buffer.from('Test text'), 'test.txt'); 220 | 221 | expect(s3Result.path).toEqual('test.txt'); 222 | expect(localResult.path.length).toEqual(36 + '.txt'.length); 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /packages/core/lib/file-storage.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'stream'; 2 | import { 3 | DiskConfig, 4 | DriverName, 5 | Driver, 6 | requireDefaultModule, 7 | LocalDiskConfig, 8 | Class, 9 | Plugin, 10 | PutResult, 11 | getFileName, 12 | ImageStats, 13 | } from '@file-storage/common'; 14 | import LocalDriver from '@file-storage/local'; 15 | import { BuiltInDiskConfig, StorageConfiguration } from './types'; 16 | import { v4 as uuidv4 } from 'uuid'; 17 | import { parse, format } from 'path'; 18 | 19 | const defaultDiskConfig: LocalDiskConfig = { 20 | driver: DriverName.LOCAL, 21 | name: 'local', 22 | root: 'storage', 23 | }; 24 | 25 | const drivers: Class[] = [ 26 | LocalDriver, 27 | // TODO Should remove all requires since drivers provided as their corresponding driver class from now on. 28 | requireDefaultModule('@file-storage/s3'), 29 | requireDefaultModule('@file-storage/ftp'), 30 | requireDefaultModule('@file-storage/sftp'), 31 | requireDefaultModule('@file-storage/gcs'), 32 | ].filter((item) => !!item); 33 | 34 | function handleDiskConfigs(diskConfigs: DiskConfig[]) { 35 | const seen = new Set(); 36 | const availableDisks: (DiskConfig | BuiltInDiskConfig)[] = []; 37 | 38 | const hasDuplicatesName = () => 39 | diskConfigs.some((diskConfig) => seen.size === seen.add(diskConfig.name).size); 40 | 41 | if (hasDuplicatesName()) { 42 | throw new Error('Duplicated disk name.'); 43 | } 44 | 45 | if (diskConfigs.length > 0) { 46 | availableDisks.push(...diskConfigs); 47 | } else { 48 | availableDisks.push(defaultDiskConfig); 49 | } 50 | 51 | return availableDisks; 52 | } 53 | 54 | function driverNotLoaded(driver: Class): boolean { 55 | return !drivers.find((item) => item.name === driver.name); 56 | } 57 | 58 | export class StorageClass { 59 | private availableDisks: (DiskConfig | BuiltInDiskConfig)[] = [defaultDiskConfig]; 60 | 61 | private plugins: Class[] = []; 62 | 63 | /** 64 | * Get default disk instance. 65 | */ 66 | private defaultDisk: Driver; 67 | 68 | /** 69 | * All plugin instances. 70 | */ 71 | private pluginInstances: Plugin[]; 72 | 73 | private uniqueFileName = false; 74 | 75 | constructor() { 76 | this.config(); 77 | } 78 | 79 | get name() { 80 | return this.defaultDisk.name; 81 | } 82 | 83 | private getDriversFromAvailableDisks(): Class[] { 84 | return this.availableDisks 85 | .filter((disk) => typeof disk.driver !== 'string' && driverNotLoaded(disk.driver)) 86 | .map((disk) => disk.driver as Class); 87 | } 88 | 89 | /** 90 | * Config for storage methods supported in the application. 91 | */ 92 | config(options: StorageConfiguration = {}) { 93 | const { diskConfigs, uniqueFileName } = options; 94 | let { defaultDiskName } = options; 95 | 96 | if (typeof diskConfigs !== 'undefined') { 97 | this.availableDisks = handleDiskConfigs(diskConfigs); 98 | } 99 | 100 | drivers.push(...this.getDriversFromAvailableDisks()); 101 | 102 | if (options.plugins && options.plugins.length > 0) { 103 | this.plugins = options.plugins; 104 | } 105 | 106 | if (!defaultDiskName) { 107 | if (this.availableDisks.length > 1) { 108 | throw new Error('Please specify a default disk name.'); 109 | } 110 | defaultDiskName = this.availableDisks[0].name; 111 | } 112 | 113 | if (typeof uniqueFileName !== 'undefined') { 114 | this.uniqueFileName = uniqueFileName; 115 | } 116 | 117 | this.defaultDisk = this.getDisk(defaultDiskName); 118 | 119 | this.pluginInstances = this.plugins.map((pluginClass) => { 120 | const plugin = new pluginClass(); 121 | plugin.init(this.defaultDisk); 122 | return plugin; 123 | }); 124 | } 125 | 126 | private getDisk(diskName: string): U { 127 | const diskConfig = this.availableDisks.find((item) => item.name === diskName); 128 | 129 | if (!diskConfig) { 130 | throw new Error(`Given disk is not defined: ${diskName}`); 131 | } 132 | 133 | const driver: Class = 134 | typeof diskConfig.driver !== 'string' 135 | ? diskConfig.driver 136 | : drivers.find((item) => item['driverName'] === diskConfig.driver); 137 | 138 | if (!driver) { 139 | // Throw error missing built-in driver package. 140 | if ((Object).values(DriverName).includes(diskConfig.driver)) { 141 | throw new Error( 142 | `Please install \`@file-storage/${diskConfig.driver}\` for ${diskConfig.driver} driver`, 143 | ); 144 | } 145 | const name = 146 | typeof diskConfig.driver !== 'string' ? diskConfig.driver.name : diskConfig.driver; 147 | throw new Error(`Driver '${name}' is not declared.`); 148 | } 149 | 150 | return new driver(diskConfig) as U; 151 | } 152 | 153 | /** 154 | * Get current disk instance. 155 | */ 156 | instance(): U { 157 | return this.defaultDisk as U; 158 | } 159 | 160 | /** 161 | * Get StorageClass instance by diskName. 162 | * 163 | * @param diskName Disk name. 164 | * @param options Adjust default settings on the fly. 165 | */ 166 | disk(diskName: string): StorageClass; 167 | disk(options: StorageConfiguration): StorageClass; 168 | disk(diskName: string, options: StorageConfiguration): StorageClass; 169 | disk( 170 | diskNameOrOptions: string | StorageConfiguration, 171 | options?: StorageConfiguration, 172 | ): StorageClass { 173 | options = typeof diskNameOrOptions === 'string' ? options : diskNameOrOptions; 174 | 175 | const storage: StorageClass = Object.assign(new StorageClass(), this); 176 | 177 | const diskName = 178 | typeof diskNameOrOptions === 'string' 179 | ? diskNameOrOptions 180 | : options.defaultDiskName || this.name; 181 | 182 | storage.config({ 183 | ...options, 184 | defaultDiskName: diskName, 185 | }); 186 | 187 | return storage; 188 | } 189 | 190 | url(path: string) { 191 | return this.defaultDisk.url(path); 192 | } 193 | 194 | exists(path: string) { 195 | return this.defaultDisk.exists(path); 196 | } 197 | 198 | size(path: string): Promise { 199 | return this.defaultDisk.size(path); 200 | } 201 | 202 | lastModified(path: string): Promise { 203 | return this.defaultDisk.lastModified(path); 204 | } 205 | 206 | async put(data: Stream | Buffer, path: string): Promise { 207 | let result: PutResult = { 208 | success: true, 209 | message: 'Uploading success', 210 | name: getFileName(path), 211 | path, 212 | }; 213 | 214 | if (this.uniqueFileName) { 215 | const parsedPath = parse(path); 216 | parsedPath.base = uuidv4() + parsedPath.ext; 217 | result.path = format(parsedPath); 218 | } 219 | 220 | const putData = await this.defaultDisk.put(data, result.path); 221 | 222 | result = Object.assign({}, result, putData); 223 | 224 | for (const plugin of this.pluginInstances || []) { 225 | if (plugin.afterPutKey && plugin.afterPut) { 226 | const afterPutData = await plugin.afterPut(result.path); 227 | result[plugin.afterPutKey] = afterPutData; 228 | } 229 | } 230 | 231 | return result; 232 | } 233 | 234 | get(path: string): Stream | Promise { 235 | return this.defaultDisk.get(path); 236 | } 237 | 238 | delete(path: string): Promise { 239 | return this.defaultDisk.delete(path); 240 | } 241 | 242 | copy(path: string, newPath: string): Promise { 243 | return this.defaultDisk.copy(path, newPath); 244 | } 245 | 246 | move(path: string, newPath: string) { 247 | return this.defaultDisk.move(path, newPath); 248 | } 249 | 250 | append(data: string | Buffer, path: string): Promise { 251 | return this.defaultDisk.append(data, path); 252 | } 253 | 254 | makeDir(dir: string): Promise { 255 | return this.defaultDisk.makeDir(dir); 256 | } 257 | 258 | removeDir(dir: string): Promise { 259 | return this.defaultDisk.removeDir(dir); 260 | } 261 | 262 | uploadImageFromExternalUri( 263 | uri: string, 264 | path: string, 265 | ignoreHeaderContentType = false, 266 | ): Promise { 267 | return this.defaultDisk.uploadImageFromExternalUri(uri, path, ignoreHeaderContentType); 268 | } 269 | 270 | imageStats(path: string, keepBuffer: true): Promise; 271 | imageStats(path: string, keepBuffer?: boolean): Promise; 272 | 273 | imageStats(path: string, keepBuffer = false) { 274 | return this.defaultDisk.imageStats(path, keepBuffer); 275 | } 276 | } 277 | 278 | /** 279 | * `Storage` provides a filesystem abstraction, simple way to uses drivers for working with local filesystems, Amazon S3,... 280 | */ 281 | export const Storage = new StorageClass(); 282 | -------------------------------------------------------------------------------- /packages/core/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { Storage as default } from './file-storage'; 2 | export { StorageClass } from './file-storage'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/core/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Class, 3 | DiskConfig, 4 | FtpDiskConfig, 5 | GCSDiskConfig, 6 | LocalDiskConfig, 7 | Plugin, 8 | S3DiskConfig, 9 | SftpDiskConfig, 10 | } from '@file-storage/common'; 11 | 12 | /** 13 | * @deprecated Use interface/type from specific package instead. 14 | */ 15 | export type BuiltInDiskConfig = 16 | | LocalDiskConfig 17 | | S3DiskConfig 18 | | SftpDiskConfig 19 | | FtpDiskConfig 20 | | GCSDiskConfig; 21 | 22 | export interface StorageConfiguration { 23 | /** 24 | * Default disk name. 25 | */ 26 | defaultDiskName?: string; 27 | 28 | /** 29 | * List of disks available in your application. 30 | */ 31 | diskConfigs?: T[]; 32 | 33 | /** 34 | * List of plugins available in your application. 35 | */ 36 | plugins?: Class[]; 37 | 38 | /** 39 | * Enable unique file name. 40 | */ 41 | uniqueFileName?: boolean; 42 | } 43 | 44 | // export interface GetDiskOptions { 45 | // /** 46 | // * Return a storage instance. 47 | // */ 48 | // asStorage?: boolean; 49 | // } 50 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@file-storage/core", 3 | "version": "1.3.9", 4 | "description": "> TODO: description", 5 | "author": "Dang Nguyen ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "dist/esm/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "exports": { 14 | "import": "./dist/esm/index.js", 15 | "require": "./dist/cjs/index.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/googlicius/file-storage.git" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org", 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "yarn clean && yarn compile", 27 | "clean": "rimraf -rf ./dist", 28 | "compile": "tsc -p tsconfig-cjs.build.json && tsc -p tsconfig-esm.build.json && ../../create-dist-modules.sh", 29 | "test": "jest", 30 | "test:coverage": "jest --coverage", 31 | "test:detectOpenHandles": "jest --detectOpenHandles" 32 | }, 33 | "dependencies": { 34 | "@file-storage/local": "^1.3.9", 35 | "uuid": "^8.3.2" 36 | }, 37 | "devDependencies": { 38 | "@file-storage/common": "^1.3.9", 39 | "rimraf": "~3.0.2", 40 | "typescript": "~4.3.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/tsconfig-cjs.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-cjs.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "declarationDir": "./dist/types" 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/tsconfig-esm.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-esm.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "declaration": false 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/ftp-driver/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /packages/ftp-driver/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.3.9](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.9) (2023-07-16) 7 | 8 | **Note:** Version bump only for package @file-storage/ftp 9 | 10 | 11 | 12 | 13 | 14 | ## [1.3.8](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.8) (2023-07-16) 15 | 16 | **Note:** Version bump only for package @file-storage/ftp 17 | 18 | 19 | 20 | 21 | 22 | ## [1.3.7](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.7) (2023-07-10) 23 | 24 | **Note:** Version bump only for package @file-storage/ftp 25 | 26 | 27 | 28 | 29 | 30 | ## [1.3.6](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.6) (2023-06-04) 31 | 32 | **Note:** Version bump only for package @file-storage/ftp 33 | 34 | 35 | 36 | 37 | 38 | ## [1.3.5](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.5) (2023-01-28) 39 | 40 | **Note:** Version bump only for package @file-storage/ftp 41 | 42 | 43 | 44 | 45 | 46 | ## [1.3.4](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.2...v1.3.4) (2023-01-28) 47 | 48 | **Note:** Version bump only for package @file-storage/ftp 49 | 50 | 51 | 52 | 53 | 54 | ## [1.3.4-alpha.2](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.1...v1.3.4-alpha.2) (2023-01-27) 55 | 56 | **Note:** Version bump only for package @file-storage/ftp 57 | 58 | 59 | 60 | 61 | 62 | ## [1.3.4-alpha.1](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.0...v1.3.4-alpha.1) (2023-01-27) 63 | 64 | **Note:** Version bump only for package @file-storage/ftp 65 | 66 | 67 | 68 | 69 | 70 | ## [1.3.4-alpha.0](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.4-alpha.0) (2023-01-26) 71 | 72 | **Note:** Version bump only for package @file-storage/ftp 73 | 74 | 75 | 76 | 77 | 78 | ## [1.3.3](https://github.com/googlicius/file-storage/compare/v1.3.2...v1.3.3) (2022-04-22) 79 | 80 | **Note:** Version bump only for package @file-storage/ftp 81 | 82 | 83 | 84 | 85 | 86 | ## [1.3.2](https://github.com/googlicius/file-storage/compare/v1.3.1...v1.3.2) (2022-01-01) 87 | 88 | **Note:** Version bump only for package @file-storage/ftp 89 | 90 | 91 | 92 | 93 | 94 | ## [1.3.1](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.3.1) (2021-12-30) 95 | 96 | **Note:** Version bump only for package @file-storage/ftp 97 | 98 | 99 | 100 | 101 | 102 | ## [1.2.9](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.2.9) (2021-11-13) 103 | 104 | **Note:** Version bump only for package @file-storage/ftp 105 | 106 | 107 | 108 | 109 | 110 | ## [1.2.8](https://github.com/googlicius/file-storage/compare/v1.2.7...v1.2.8) (2021-10-28) 111 | 112 | **Note:** Version bump only for package @file-storage/ftp 113 | 114 | 115 | 116 | 117 | 118 | ## [1.2.7](https://github.com/googlicius/file-storage/compare/v1.2.6...v1.2.7) (2021-10-22) 119 | 120 | **Note:** Version bump only for package @file-storage/ftp 121 | 122 | 123 | 124 | 125 | 126 | ## [1.2.6](https://github.com/googlicius/file-storage/compare/v1.2.5...v1.2.6) (2021-10-11) 127 | 128 | **Note:** Version bump only for package @file-storage/ftp 129 | 130 | 131 | 132 | 133 | 134 | ## 1.2.3 (2021-09-08) 135 | 136 | **Note:** Version bump only for package @file-storage/ftp 137 | -------------------------------------------------------------------------------- /packages/ftp-driver/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____|_) | ___| | 3 | | | | _ \\___ \ __| _ \ __| _` | _` | _ \ 4 | __| | | __/ | | ( | | ( | ( | __/ 5 | _| _|_|\___|_____/ \__|\___/ _| \__,_|\__, |\___| 6 | A file system abstraction for Node.js |___/ 7 | ``` 8 | 9 | FTP driver for `file-storage` 10 | -------------------------------------------------------------------------------- /packages/ftp-driver/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../../babel.config.js', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/ftp-driver/lib/__snapshots__/ftp-driver.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FTP Disk test Upload ftp large image will uploaded to many formats 1`] = ` 4 | { 5 | "code": 226, 6 | "formats": { 7 | "large": { 8 | "ext": "jpeg", 9 | "hash": null, 10 | "height": 750, 11 | "mime": "jpeg", 12 | "name": "large_photo-1000x750.jpeg", 13 | "path": "my-photo/large_photo-1000x750.jpeg", 14 | "size": 184.49, 15 | "width": 1000, 16 | }, 17 | "medium": { 18 | "ext": "jpeg", 19 | "hash": null, 20 | "height": 562, 21 | "mime": "jpeg", 22 | "name": "medium_photo-1000x750.jpeg", 23 | "path": "my-photo/medium_photo-1000x750.jpeg", 24 | "size": 107.23, 25 | "width": 750, 26 | }, 27 | "small": { 28 | "ext": "jpeg", 29 | "hash": null, 30 | "height": 375, 31 | "mime": "jpeg", 32 | "name": "small_photo-1000x750.jpeg", 33 | "path": "my-photo/small_photo-1000x750.jpeg", 34 | "size": 52.4, 35 | "width": 500, 36 | }, 37 | "thumbnail": { 38 | "ext": "jpeg", 39 | "hash": null, 40 | "height": 156, 41 | "mime": "jpeg", 42 | "name": "thumbnail_photo-1000x750.jpeg", 43 | "path": "my-photo/thumbnail_photo-1000x750.jpeg", 44 | "size": 16.14, 45 | "width": 208, 46 | }, 47 | }, 48 | "message": "226 Transfer complete.", 49 | "name": "photo-1000x750.jpeg", 50 | "path": "my-photo/photo-1000x750.jpeg", 51 | "success": true, 52 | } 53 | `; 54 | 55 | exports[`FTP Disk test append should append a text to a file 1`] = ` 56 | "First line 57 | Appended line" 58 | `; 59 | -------------------------------------------------------------------------------- /packages/ftp-driver/lib/ftp-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Class, FtpDiskConfig as FtpDiskConfigCommon } from '@file-storage/common'; 2 | import { FtpDriver } from './ftp-driver'; 3 | 4 | export interface FtpDiskConfig extends FtpDiskConfigCommon { 5 | driver: Class; 6 | } 7 | -------------------------------------------------------------------------------- /packages/ftp-driver/lib/ftp-driver.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Storage from '@file-storage/core'; 3 | import { DriverName, FtpDiskConfig, getRootCwd, streamToBuffer } from '@file-storage/common'; 4 | import ImageManipulation from '@file-storage/image-manipulation'; 5 | 6 | function sleep(ms: number) { 7 | return new Promise((resolve) => setTimeout(resolve, ms)); 8 | } 9 | 10 | describe('FTP Disk test', () => { 11 | beforeAll(() => { 12 | Storage.config({ 13 | diskConfigs: [ 14 | { 15 | driver: DriverName.FTP, 16 | name: 'sammy', 17 | root: '/upload', 18 | accessOptions: { 19 | host: '127.0.0.1', 20 | user: 'usertest', 21 | password: 'P@ssw0rd', 22 | }, 23 | }, 24 | ], 25 | plugins: [ImageManipulation], 26 | }); 27 | }); 28 | 29 | test('Default disk is sammy', () => { 30 | expect(Storage.name).toEqual('sammy'); 31 | }); 32 | 33 | test('Upload image to ftp', async () => { 34 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 35 | return expect(Storage.disk('sammy').put(fileReadStream, 'bird.jpeg')).resolves.toMatchObject({ 36 | success: true, 37 | code: 226, 38 | message: '226 Transfer complete.', 39 | }); 40 | }); 41 | 42 | test('Upload ftp using Storage facade', () => { 43 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 44 | return expect(Storage.put(fileReadStream, 'path/to/bird.jpeg')).resolves.toMatchObject({ 45 | success: true, 46 | code: 226, 47 | message: '226 Transfer complete.', 48 | name: 'bird.jpeg', 49 | path: 'path/to/bird.jpeg', 50 | }); 51 | }); 52 | 53 | test('Upload ftp large image will uploaded to many formats', () => { 54 | const imageFileStream = fs.createReadStream( 55 | getRootCwd() + '/test/support/images/photo-1000x750.jpeg', 56 | ); 57 | return expect( 58 | Storage.put(imageFileStream, 'my-photo/photo-1000x750.jpeg'), 59 | ).resolves.toMatchSnapshot(); 60 | }); 61 | 62 | // FIXME Request timed out even this test success. 63 | // test('Download image from ftp', async () => { 64 | // const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 65 | // await Storage.disk('sammy').put(fileReadStream, 'test_upload/bird.jpeg'); 66 | 67 | // return expect(Storage.disk('sammy').get('test_upload/bird.jpeg')).resolves.toBeTruthy(); 68 | // }); 69 | 70 | test('Delete image from ftp', async () => { 71 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 72 | await Storage.disk('sammy').put(fileReadStream, 'test_upload/bird.jpeg'); 73 | 74 | return expect(Storage.disk('sammy').delete('test_upload/bird.jpeg')).resolves.toMatchObject({ 75 | code: 250, 76 | message: '250 Delete operation successful.', 77 | }); 78 | }); 79 | 80 | test('File is exists', async () => { 81 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 82 | await Storage.disk('sammy').put(fileReadStream, 'test_upload/bird.jpeg'); 83 | 84 | return expect(Storage.exists('test_upload/bird.jpeg')).resolves.toEqual(true); 85 | }); 86 | 87 | test('File is not exists', async () => { 88 | return expect(Storage.disk('sammy').exists('not-exists.jpeg')).resolves.toEqual(false); 89 | }); 90 | 91 | test('Get file size', async () => { 92 | const fileReadStream2 = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 93 | await Storage.disk('sammy').put(fileReadStream2, 'bird-images/bird.jpeg'); 94 | 95 | return expect(Storage.size('bird-images/bird.jpeg')).resolves.toEqual(56199); 96 | }); 97 | 98 | test('Last modified', async () => { 99 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 100 | await Storage.disk('sammy').put(fileReadStream, 'bird-images/bird2.jpeg'); 101 | const lastMod = await Storage.lastModified('bird-images/bird2.jpeg'); 102 | expect(typeof lastMod).toBe('number'); 103 | }); 104 | 105 | // TODO Should test this case. 106 | // test('Copy file', async () => { 107 | // const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 108 | // const putResult = await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 109 | // await Storage.copy(putResult.path, 'photos/bird-copy.jpeg'); 110 | 111 | // const size = await Storage.size('photos/bird-copy.jpeg'); 112 | // expect(typeof size).toBe('number'); 113 | // }); 114 | 115 | test('Move file', async () => { 116 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 117 | const putResult = await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 118 | await Storage.move(putResult.path, 'photos/new-path.jpeg'); 119 | 120 | const size = await Storage.size('photos/new-path.jpeg'); 121 | expect(typeof size).toBe('number'); 122 | 123 | return expect(Storage.size('bird-images/bird.jpeg')).rejects.toThrowError(); 124 | }); 125 | 126 | describe('append', () => { 127 | it('should append a text to a file', async () => { 128 | const putResult = await Storage.put(Buffer.from('First line'), 'to-be-appended.txt'); 129 | await sleep(10); 130 | await Storage.append('\nAppended line', putResult.path); 131 | const buff = await streamToBuffer(await Storage.get(putResult.path)); 132 | 133 | return expect(buff.toString()).toMatchSnapshot(); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /packages/ftp-driver/lib/ftp-driver.ts: -------------------------------------------------------------------------------- 1 | import { AccessOptions, Client, FileInfo, FTPResponse } from 'basic-ftp'; 2 | import { dirname } from 'path'; 3 | import { PassThrough, Readable, Stream } from 'stream'; 4 | import { Driver, DriverName, FtpDiskConfig, PutResult, toStream } from '@file-storage/common'; 5 | 6 | export class FtpDriver extends Driver { 7 | static readonly driverName = DriverName.FTP; 8 | readonly client: Client; 9 | 10 | private accessOptions: AccessOptions; 11 | private root: string; 12 | 13 | constructor(diskConfig: FtpDiskConfig) { 14 | super(diskConfig); 15 | const { accessOptions, ftpContext, root = '' } = diskConfig; 16 | this.accessOptions = accessOptions; 17 | this.root = root; 18 | this.client = new Client(); 19 | 20 | // Ftp context 21 | if (ftpContext) { 22 | for (const item in ftpContext) { 23 | if (Object.prototype.hasOwnProperty.call(ftpContext, item)) { 24 | this.client.ftp[item] = ftpContext[item]; 25 | } 26 | } 27 | } 28 | } 29 | 30 | private async connectToFTPServer(): Promise { 31 | if (this.client.closed) { 32 | return await this.client.access(this.accessOptions); 33 | } 34 | } 35 | 36 | private async ensureDirectoryExistence(path: string): Promise { 37 | await this.client.ensureDir(dirname(path)); 38 | } 39 | 40 | private rootPath(path = '') { 41 | return this.root + '/' + path; 42 | } 43 | 44 | /** 45 | * A helper function to call client's specific function which auto connect and auto close connection after response. 46 | */ 47 | private async clientFunc(name: string, ...args: any[]): Promise { 48 | await this.connectToFTPServer(); 49 | return this.client[name](...args).finally(() => { 50 | this.client.close(); 51 | }); 52 | } 53 | 54 | url(path: string) { 55 | return this.root + '/' + path; 56 | } 57 | 58 | async exists(path: string) { 59 | try { 60 | await this.clientFunc('size', this.rootPath(path)); 61 | return true; 62 | } catch (error) { 63 | return false; 64 | } 65 | } 66 | 67 | size(path: string): Promise { 68 | return this.clientFunc('size', this.rootPath(path)); 69 | } 70 | 71 | async lastModified(path: string): Promise { 72 | const date: Date = await this.clientFunc('lastMod', this.rootPath(path)); 73 | return date.getTime(); 74 | } 75 | 76 | async put(data: Readable | Buffer, path: string): Promise & FTPResponse> { 77 | await this.connectToFTPServer(); 78 | await this.ensureDirectoryExistence(this.rootPath(path)); 79 | 80 | return this.client 81 | .uploadFrom(toStream(data) as Readable, this.rootPath(path)) 82 | .then((result) => { 83 | return { 84 | success: true, 85 | ...result, 86 | }; 87 | }) 88 | .finally(() => { 89 | this.client.close(); 90 | }); 91 | } 92 | 93 | async get(path: string): Promise { 94 | const passThrough = new PassThrough(); 95 | this.clientFunc('downloadTo', passThrough, this.rootPath(path)); 96 | return passThrough; 97 | } 98 | 99 | async append(data: string | Buffer, path: string): Promise { 100 | await this.clientFunc('appendFrom', toStream(data), this.rootPath(path)); 101 | } 102 | 103 | list(path?: string): Promise { 104 | return this.clientFunc('list', this.rootPath(path)); 105 | } 106 | 107 | delete(path: string): Promise { 108 | return this.clientFunc('remove', this.rootPath(path)); 109 | } 110 | 111 | async copy(path: string, newPath: string): Promise { 112 | const file = (await this.get(path)) as Readable; 113 | await this.put(file, newPath); 114 | } 115 | 116 | async move(path: string, newPath: string): Promise { 117 | await this.connectToFTPServer(); 118 | await this.ensureDirectoryExistence(this.rootPath(newPath)); 119 | await this.clientFunc('rename', this.rootPath(path), this.rootPath(newPath)); 120 | } 121 | 122 | async makeDir(dir: string): Promise { 123 | try { 124 | await this.clientFunc('cd', this.rootPath(dir)); 125 | throw new Error('Directory already exists.'); 126 | } catch (e) { 127 | await this.clientFunc('ensureDir', this.rootPath(dir)); 128 | return dir; 129 | } 130 | } 131 | 132 | removeDir(dir: string): Promise { 133 | return this.clientFunc('removeDir', this.rootPath(dir)); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/ftp-driver/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { FtpDriver as default } from './ftp-driver'; 2 | export * from './ftp-disk-config.interface'; 3 | -------------------------------------------------------------------------------- /packages/ftp-driver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@file-storage/ftp", 3 | "version": "1.3.9", 4 | "description": "Ftp disk driver for file-storage", 5 | "author": "Dang Nguyen ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "dist/esm/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "exports": { 14 | "import": "./dist/esm/index.js", 15 | "require": "./dist/cjs/index.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/googlicius/file-storage.git" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org", 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "yarn clean && yarn compile", 27 | "clean": "rimraf -rf ./dist", 28 | "compile": "tsc -p tsconfig-cjs.build.json && tsc -p tsconfig-esm.build.json && ../../create-dist-modules.sh", 29 | "test": "jest", 30 | "test:coverage": "jest --coverage", 31 | "test:detectOpenHandles": "jest --detectOpenHandles" 32 | }, 33 | "dependencies": { 34 | "basic-ftp": "^4.6.6" 35 | }, 36 | "peerDependencies": { 37 | "@file-storage/core": "^1.0.0" 38 | }, 39 | "devDependencies": { 40 | "@file-storage/common": "^1.3.9", 41 | "rimraf": "~3.0.2", 42 | "typescript": "~4.3.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/ftp-driver/tsconfig-cjs.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-cjs.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "declarationDir": "./dist/types" 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/ftp-driver/tsconfig-esm.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-esm.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "declaration": false 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/ftp-driver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/gcs-driver/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | storage -------------------------------------------------------------------------------- /packages/gcs-driver/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.3.9](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.9) (2023-07-16) 7 | 8 | **Note:** Version bump only for package @file-storage/gcs 9 | 10 | 11 | 12 | 13 | 14 | ## [1.3.8](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.8) (2023-07-16) 15 | 16 | **Note:** Version bump only for package @file-storage/gcs 17 | 18 | 19 | 20 | 21 | 22 | ## [1.3.7](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.7) (2023-07-10) 23 | 24 | **Note:** Version bump only for package @file-storage/gcs 25 | 26 | 27 | 28 | 29 | 30 | ## [1.3.6](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.6) (2023-06-04) 31 | 32 | **Note:** Version bump only for package @file-storage/gcs 33 | 34 | 35 | 36 | 37 | 38 | ## [1.3.5](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.5) (2023-01-28) 39 | 40 | **Note:** Version bump only for package @file-storage/gcs 41 | 42 | 43 | 44 | 45 | 46 | ## [1.3.4](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.2...v1.3.4) (2023-01-28) 47 | 48 | **Note:** Version bump only for package @file-storage/gcs 49 | 50 | 51 | 52 | 53 | 54 | ## [1.3.4-alpha.2](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.1...v1.3.4-alpha.2) (2023-01-27) 55 | 56 | **Note:** Version bump only for package @file-storage/gcs 57 | 58 | 59 | 60 | 61 | 62 | ## [1.3.4-alpha.1](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.0...v1.3.4-alpha.1) (2023-01-27) 63 | 64 | **Note:** Version bump only for package @file-storage/gcs 65 | 66 | 67 | 68 | 69 | 70 | ## [1.3.4-alpha.0](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.4-alpha.0) (2023-01-26) 71 | 72 | **Note:** Version bump only for package @file-storage/gcs 73 | 74 | 75 | 76 | 77 | 78 | ## [1.3.3](https://github.com/googlicius/file-storage/compare/v1.3.2...v1.3.3) (2022-04-22) 79 | 80 | **Note:** Version bump only for package @file-storage/gcs 81 | 82 | 83 | 84 | 85 | 86 | ## [1.3.1](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.3.1) (2021-12-30) 87 | 88 | **Note:** Version bump only for package @file-storage/gcs 89 | -------------------------------------------------------------------------------- /packages/gcs-driver/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____|_) | ___| | 3 | | | | _ \\___ \ __| _ \ __| _` | _` | _ \ 4 | __| | | __/ | | ( | | ( | ( | __/ 5 | _| _|_|\___|_____/ \__|\___/ _| \__,_|\__, |\___| 6 | A file system abstraction for Node.js |___/ 7 | ``` 8 | 9 | Google Cloud Storage driver for `file-storage` 10 | -------------------------------------------------------------------------------- /packages/gcs-driver/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../../babel.config.js', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/gcs-driver/lib/__snapshots__/gcs-driver.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Google Cloud Storage upload GCS large image will upload to many formats 1`] = ` 4 | { 5 | "formats": { 6 | "large": { 7 | "ext": "jpeg", 8 | "hash": null, 9 | "height": 750, 10 | "mime": "jpeg", 11 | "name": "large_photo-1000x750.jpeg", 12 | "path": "my-photo/large_photo-1000x750.jpeg", 13 | "size": 184.49, 14 | "width": 1000, 15 | }, 16 | "medium": { 17 | "ext": "jpeg", 18 | "hash": null, 19 | "height": 562, 20 | "mime": "jpeg", 21 | "name": "medium_photo-1000x750.jpeg", 22 | "path": "my-photo/medium_photo-1000x750.jpeg", 23 | "size": 107.23, 24 | "width": 750, 25 | }, 26 | "small": { 27 | "ext": "jpeg", 28 | "hash": null, 29 | "height": 375, 30 | "mime": "jpeg", 31 | "name": "small_photo-1000x750.jpeg", 32 | "path": "my-photo/small_photo-1000x750.jpeg", 33 | "size": 52.4, 34 | "width": 500, 35 | }, 36 | "thumbnail": { 37 | "ext": "jpeg", 38 | "hash": null, 39 | "height": 156, 40 | "mime": "jpeg", 41 | "name": "thumbnail_photo-1000x750.jpeg", 42 | "path": "my-photo/thumbnail_photo-1000x750.jpeg", 43 | "size": 16.14, 44 | "width": 208, 45 | }, 46 | }, 47 | "message": "Uploading success!", 48 | "name": "photo-1000x750.jpeg", 49 | "path": "my-photo/photo-1000x750.jpeg", 50 | "success": true, 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /packages/gcs-driver/lib/gcs-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Class, GCSDiskConfig as GCSDiskConfigCommon } from '@file-storage/common'; 2 | import { GoogleCloudStorageDriver } from './gcs-driver'; 3 | 4 | export interface GCSDiskConfig extends GCSDiskConfigCommon { 5 | driver: Class; 6 | } 7 | -------------------------------------------------------------------------------- /packages/gcs-driver/lib/gcs-driver.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Storage from '@file-storage/core'; 3 | import { FileNotFoundError, getRootCwd } from '@file-storage/common'; 4 | import ImageManipulation from '@file-storage/image-manipulation'; 5 | import { GoogleCloudStorageDriver } from './gcs-driver'; 6 | import { GCSDiskConfig } from './gcs-disk-config.interface'; 7 | 8 | describe('Google Cloud Storage', () => { 9 | const bucketName1 = 'my_gcs_bucket'; 10 | 11 | beforeAll(async () => { 12 | Storage.config({ 13 | diskConfigs: [ 14 | { 15 | driver: GoogleCloudStorageDriver, 16 | name: 'my_gcs', 17 | bucketName: bucketName1, 18 | apiEndpoint: 'http://localhost:4443', 19 | projectId: 'test', 20 | }, 21 | ], 22 | plugins: [ImageManipulation], 23 | }); 24 | 25 | try { 26 | await Storage.disk('my_gcs').instance().createBucket(bucketName1); 27 | } catch (error) { 28 | // eslint-disable-next-line no-console 29 | console.warn(error.message); 30 | } 31 | }); 32 | 33 | test('default disk is my_gcs', () => { 34 | expect(Storage.name).toEqual('my_gcs'); 35 | }); 36 | 37 | test('upload image from URI to GCS', () => { 38 | return expect( 39 | Storage.disk('my_gcs').uploadImageFromExternalUri( 40 | 'https://raw.githubusercontent.com/googlicius/file-storage/main/test/support/images/bird.jpeg', 41 | 'test_upload/test_image_from_uri.jpeg', 42 | ), 43 | ).resolves.toMatchObject({ 44 | success: true, 45 | message: 'Uploading success!', 46 | }); 47 | }); 48 | 49 | test('upload image to gcs success', () => { 50 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 51 | return expect(Storage.put(fileReadStream, 'test_upload/bird2.jpeg')).resolves.toMatchObject({ 52 | success: true, 53 | message: 'Uploading success!', 54 | }); 55 | }); 56 | 57 | test('upload GCS large image will upload to many formats', () => { 58 | const imageFileStream = fs.createReadStream( 59 | getRootCwd() + '/test/support/images/photo-1000x750.jpeg', 60 | ); 61 | 62 | return expect( 63 | Storage.put(imageFileStream, 'my-photo/photo-1000x750.jpeg'), 64 | ).resolves.toMatchSnapshot(); 65 | }); 66 | 67 | test('download image from GCS', async () => { 68 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 69 | await Storage.put(fileReadStream, 'test_upload/bird2.jpeg'); 70 | return expect(Storage.get('test_upload/bird2.jpeg')).resolves.toBeTruthy(); 71 | }); 72 | 73 | test('download not exists image from GCS error', async () => { 74 | return expect(Storage.disk('my_gcs').get('not-exists.jpeg')).rejects.toThrowError( 75 | FileNotFoundError, 76 | ); 77 | }); 78 | 79 | test('File is exists', async () => { 80 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 81 | await Storage.disk('my_gcs').put(fileReadStream, 'bird-images/bird.jpeg'); 82 | 83 | return expect(Storage.exists('bird-images/bird.jpeg')).resolves.toEqual(true); 84 | }); 85 | 86 | test('file is not exists', async () => { 87 | const exist = await Storage.disk('my_gcs').exists('not-exists.jpeg'); 88 | const exist2 = await Storage.exists('not-exists.jpeg'); 89 | expect(exist).toEqual(false); 90 | expect(exist2).toEqual(false); 91 | }); 92 | 93 | test('get file size', async () => { 94 | const fileReadStream2 = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 95 | await Storage.disk('my_gcs').put(fileReadStream2, 'bird-images/bird-size.jpeg'); 96 | 97 | return expect(Storage.size('bird-images/bird-size.jpeg')).resolves.toEqual(56199); 98 | }); 99 | 100 | test('last modified', async () => { 101 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 102 | await Storage.disk('my_gcs').put(fileReadStream, 'bird-images/bird.jpeg'); 103 | const lastMod = await Storage.lastModified('bird-images/bird.jpeg'); 104 | const lastMod2 = await Storage.lastModified('bird-images/bird.jpeg'); 105 | expect(typeof lastMod).toBe('number'); 106 | expect(typeof lastMod2).toBe('number'); 107 | }); 108 | 109 | test('copy file', async () => { 110 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 111 | const putResult = await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 112 | await Storage.copy(putResult.path, 'photos/bird-copy.jpeg'); 113 | 114 | const size = await Storage.size('photos/bird-copy.jpeg'); 115 | expect(typeof size).toBe('number'); 116 | }); 117 | 118 | test('move file', async () => { 119 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 120 | const putResult = await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 121 | await Storage.move(putResult.path, 'photos/new-path.jpeg'); 122 | 123 | const size = await Storage.size('photos/new-path.jpeg'); 124 | expect(typeof size).toBe('number'); 125 | 126 | return expect(Storage.size('bird-images/bird.jpeg')).rejects.toThrowError(FileNotFoundError); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /packages/gcs-driver/lib/gcs-driver.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'stream'; 2 | import { Storage, File } from '@google-cloud/storage'; 3 | import { 4 | Driver, 5 | DriverName, 6 | FileNotFoundError, 7 | GCSDiskConfig, 8 | PutResult, 9 | toStream, 10 | UnauthenticatedError, 11 | } from '@file-storage/common'; 12 | 13 | export class GoogleCloudStorageDriver extends Driver { 14 | private bucketName: string; 15 | private publicUrl?: string; 16 | readonly client: Storage; 17 | static readonly driverName = DriverName.GCS; 18 | 19 | constructor(config: GCSDiskConfig) { 20 | super(config); 21 | const { bucketName, publicUrl, ...options } = config; 22 | 23 | if (!bucketName) { 24 | throw new Error('Bucket name is required'); 25 | } 26 | 27 | this.bucketName = bucketName; 28 | this.publicUrl = publicUrl; 29 | this.client = new Storage(options); 30 | } 31 | 32 | protected errorHandler(error: any) { 33 | switch (error.code) { 34 | case 401: 35 | throw new UnauthenticatedError(error.message); 36 | 37 | case 404: 38 | throw new FileNotFoundError(error.message); 39 | 40 | default: 41 | throw error; 42 | } 43 | } 44 | 45 | /** 46 | * Create a reference to a file object. 47 | * 48 | * @inheritdoc 49 | * @param path Path of file. 50 | */ 51 | private file(path: string): File { 52 | return this.client.bucket(this.bucketName).file(path); 53 | } 54 | 55 | url(path: string): string { 56 | return this.publicUrl 57 | ? `${this.publicUrl}/${path}` 58 | : `https://storage.googleapis.com/${this.bucketName}/${path}`; 59 | } 60 | 61 | /** 62 | * List of file metadata. 63 | * 64 | * `Bucket: ${metadata.bucket}` 65 | * `CacheControl: ${metadata.cacheControl}` 66 | * `ComponentCount: ${metadata.componentCount}` 67 | * `ContentDisposition: ${metadata.contentDisposition}` 68 | * `ContentEncoding: ${metadata.contentEncoding}` 69 | * `ContentLanguage: ${metadata.contentLanguage}` 70 | * `ContentType: ${metadata.contentType}` 71 | * `CustomTime: ${metadata.customTime}` 72 | * `Crc32c: ${metadata.crc32c}` 73 | * `ETag: ${metadata.etag}` 74 | * `Generation: ${metadata.generation}` 75 | * `Id: ${metadata.id}` 76 | * `KmsKeyName: ${metadata.kmsKeyName}` 77 | * `Md5Hash: ${metadata.md5Hash}` 78 | * `MediaLink: ${metadata.mediaLink}` 79 | * `Metageneration: ${metadata.metageneration}` 80 | * `Name: ${metadata.name}` 81 | * `Size: ${metadata.size}` 82 | * `StorageClass: ${metadata.storageClass}` 83 | * `TimeCreated: ${new Date(metadata.timeCreated)}` 84 | * `Last Metadata Update: ${new Date(metadata.updated)}` 85 | * `temporaryHold: ${metadata.temporaryHold ? 'enabled' : 'disabled'}` 86 | * `eventBasedHold: ${metadata.eventBasedHold ? 'enabled' : 'disabled'}` 87 | */ 88 | protected stats(path: string) { 89 | return this.file(path) 90 | .getMetadata() 91 | .then((data) => data[0]); 92 | } 93 | 94 | async size(path: string): Promise { 95 | const metadata = await this.stats(path); 96 | return +metadata.size; 97 | } 98 | 99 | async lastModified(path: string): Promise { 100 | const metadata = await this.stats(path); 101 | return +metadata.updated; 102 | } 103 | 104 | exists(path: string): Promise { 105 | return this.file(path) 106 | .exists() 107 | .then((data) => data[0]); 108 | } 109 | 110 | async get(path: string): Promise { 111 | await this.stats(path); 112 | return this.file(path).createReadStream(); 113 | } 114 | 115 | put(data: Stream | Buffer, path: string): Promise> { 116 | return new Promise((resolve, reject) => { 117 | toStream(data) 118 | .pipe(this.file(path).createWriteStream()) 119 | .on('finish', () => { 120 | resolve({ 121 | success: true, 122 | message: 'Uploading success!', 123 | }); 124 | }) 125 | .on('error', (error) => { 126 | reject(error); 127 | }); 128 | }); 129 | } 130 | 131 | delete(path: string): Promise { 132 | return this.file(path) 133 | .delete() 134 | .then(() => true); 135 | } 136 | 137 | async copy(path: string, newPath: string): Promise { 138 | await this.file(path).copy(this.file(newPath)); 139 | } 140 | 141 | async move(path: string, newPath: string): Promise { 142 | await this.file(path).move(newPath); 143 | } 144 | 145 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 146 | async append(data: string | Buffer, path: string): Promise { 147 | return new Promise((_, reject) => 148 | reject('Appending to a file currently not supported with Google Cloud Storage.'), 149 | ); 150 | } 151 | 152 | makeDir(dir: string): Promise { 153 | return this.client 154 | .bucket(this.bucketName) 155 | .create(dir) 156 | .then(() => dir); 157 | } 158 | 159 | removeDir(dir: string): Promise { 160 | return this.client 161 | .bucket(this.bucketName) 162 | .deleteFiles({ 163 | prefix: dir, 164 | }) 165 | .then(() => dir); 166 | } 167 | 168 | /** 169 | * Create a bucket. 170 | * 171 | * @param name 172 | */ 173 | createBucket(name: string) { 174 | return this.client.bucket(name).create(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /packages/gcs-driver/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { GoogleCloudStorageDriver as default } from './gcs-driver'; 2 | export * from './gcs-disk-config.interface'; 3 | -------------------------------------------------------------------------------- /packages/gcs-driver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@file-storage/gcs", 3 | "version": "1.3.9", 4 | "description": "Google Cloud Storage driver for File Storage.", 5 | "keywords": [ 6 | "FileStorage", 7 | "gcs", 8 | "driver" 9 | ], 10 | "author": "Dang Nguyen ", 11 | "homepage": "https://github.com/googlicius/file-storage#readme", 12 | "license": "MIT", 13 | "main": "dist/esm/index.js", 14 | "types": "dist/types/index.d.ts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "exports": { 19 | "import": "./dist/esm/index.js", 20 | "require": "./dist/cjs/index.js" 21 | }, 22 | "publishConfig": { 23 | "registry": "https://registry.npmjs.org", 24 | "access": "public" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/googlicius/file-storage.git" 29 | }, 30 | "scripts": { 31 | "build": "yarn clean && yarn compile", 32 | "clean": "rimraf -rf ./dist", 33 | "compile": "tsc -p tsconfig-cjs.build.json && tsc -p tsconfig-esm.build.json && ../../create-dist-modules.sh", 34 | "test": "jest", 35 | "test:coverage": "jest --coverage", 36 | "test:detectOpenHandles": "jest --detectOpenHandles" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/googlicius/file-storage/issues" 40 | }, 41 | "devDependencies": { 42 | "@file-storage/common": "^1.3.9", 43 | "rimraf": "~3.0.2", 44 | "typescript": "~4.3.5" 45 | }, 46 | "dependencies": { 47 | "@google-cloud/storage": "^5.16.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/gcs-driver/tsconfig-cjs.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-cjs.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "declarationDir": "./dist/types" 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/gcs-driver/tsconfig-esm.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-esm.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "declaration": false 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/gcs-driver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/image-manipulation/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | storage -------------------------------------------------------------------------------- /packages/image-manipulation/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.3.9](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.9) (2023-07-16) 7 | 8 | **Note:** Version bump only for package @file-storage/image-manipulation 9 | 10 | 11 | 12 | 13 | 14 | ## [1.3.8](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.8) (2023-07-16) 15 | 16 | **Note:** Version bump only for package @file-storage/image-manipulation 17 | 18 | 19 | 20 | 21 | 22 | ## [1.3.7](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.7) (2023-07-10) 23 | 24 | **Note:** Version bump only for package @file-storage/image-manipulation 25 | 26 | 27 | 28 | 29 | 30 | ## [1.3.6](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.6) (2023-06-04) 31 | 32 | **Note:** Version bump only for package @file-storage/image-manipulation 33 | 34 | 35 | 36 | 37 | 38 | ## [1.3.5](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.5) (2023-01-28) 39 | 40 | **Note:** Version bump only for package @file-storage/image-manipulation 41 | 42 | 43 | 44 | 45 | 46 | ## [1.3.4](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.2...v1.3.4) (2023-01-28) 47 | 48 | **Note:** Version bump only for package @file-storage/image-manipulation 49 | 50 | 51 | 52 | 53 | 54 | ## [1.3.4-alpha.2](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.1...v1.3.4-alpha.2) (2023-01-27) 55 | 56 | **Note:** Version bump only for package @file-storage/image-manipulation 57 | 58 | 59 | 60 | 61 | 62 | ## [1.3.4-alpha.1](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.0...v1.3.4-alpha.1) (2023-01-27) 63 | 64 | **Note:** Version bump only for package @file-storage/image-manipulation 65 | 66 | 67 | 68 | 69 | 70 | ## [1.3.4-alpha.0](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.4-alpha.0) (2023-01-26) 71 | 72 | **Note:** Version bump only for package @file-storage/image-manipulation 73 | 74 | 75 | 76 | 77 | 78 | ## [1.3.3](https://github.com/googlicius/file-storage/compare/v1.3.2...v1.3.3) (2022-04-22) 79 | 80 | **Note:** Version bump only for package @file-storage/image-manipulation 81 | 82 | 83 | 84 | 85 | 86 | ## [1.3.1](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.3.1) (2021-12-30) 87 | 88 | **Note:** Version bump only for package @file-storage/image-manipulation 89 | 90 | 91 | 92 | 93 | 94 | ## [1.2.9](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.2.9) (2021-11-13) 95 | 96 | **Note:** Version bump only for package @file-storage/image-manipulation 97 | 98 | 99 | 100 | 101 | 102 | ## [1.2.8](https://github.com/googlicius/file-storage/compare/v1.2.7...v1.2.8) (2021-10-28) 103 | 104 | **Note:** Version bump only for package @file-storage/image-manipulation 105 | 106 | 107 | 108 | 109 | 110 | ## [1.2.7](https://github.com/googlicius/file-storage/compare/v1.2.6...v1.2.7) (2021-10-22) 111 | 112 | **Note:** Version bump only for package @file-storage/image-manipulation 113 | 114 | 115 | 116 | 117 | 118 | ## [1.2.6](https://github.com/googlicius/file-storage/compare/v1.2.5...v1.2.6) (2021-10-11) 119 | 120 | **Note:** Version bump only for package @file-storage/image-manipulation 121 | 122 | 123 | 124 | 125 | 126 | ## 1.2.3 (2021-09-08) 127 | 128 | **Note:** Version bump only for package @file-storage/image-manipulation 129 | -------------------------------------------------------------------------------- /packages/image-manipulation/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____|_) | ___| | 3 | | | | _ \\___ \ __| _ \ __| _` | _` | _ \ 4 | __| | | __/ | | ( | | ( | ( | __/ 5 | _| _|_|\___|_____/ \__|\___/ _| \__,_|\__, |\___| 6 | A file system abstraction for Node.js |___/ 7 | ``` 8 | 9 | Local driver for `file-storage` 10 | -------------------------------------------------------------------------------- /packages/image-manipulation/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../../babel.config.js', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/image-manipulation/lib/__snapshots__/image-manipulation.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Image Manipulation should generates 2 custom breakpoints: size1, size2 1`] = ` 4 | { 5 | "formats": { 6 | "size1": { 7 | "ext": "jpeg", 8 | "hash": null, 9 | "height": 300, 10 | "mime": "jpeg", 11 | "name": "size1_bird.jpeg", 12 | "path": "size1_bird.jpeg", 13 | "size": 36.76, 14 | "width": 400, 15 | }, 16 | "size2": { 17 | "ext": "jpeg", 18 | "hash": null, 19 | "height": 450, 20 | "mime": "jpeg", 21 | "name": "size2_bird.jpeg", 22 | "path": "size2_bird.jpeg", 23 | "size": 70.96, 24 | "width": 600, 25 | }, 26 | "thumbnail": { 27 | "ext": "jpeg", 28 | "hash": null, 29 | "height": 156, 30 | "mime": "jpeg", 31 | "name": "thumbnail_bird.jpeg", 32 | "path": "thumbnail_bird.jpeg", 33 | "size": 16.14, 34 | "width": 208, 35 | }, 36 | }, 37 | "message": "Uploading success!", 38 | "name": "bird.jpeg", 39 | "path": "bird.jpeg", 40 | "success": true, 41 | } 42 | `; 43 | 44 | exports[`Image Manipulation should generates custom thumbnail size 1`] = ` 45 | { 46 | "formats": { 47 | "thumbnail": { 48 | "ext": "jpeg", 49 | "hash": null, 50 | "height": 222, 51 | "mime": "jpeg", 52 | "name": "thumbnail_bird.jpeg", 53 | "path": "thumbnail_bird.jpeg", 54 | "size": 24.87, 55 | "width": 333, 56 | }, 57 | }, 58 | "message": "Uploading success!", 59 | "name": "bird.jpeg", 60 | "path": "bird.jpeg", 61 | "success": true, 62 | } 63 | `; 64 | 65 | exports[`Image Manipulation should have no responsive formats 1`] = ` 66 | { 67 | "formats": { 68 | "thumbnail": { 69 | "ext": "jpeg", 70 | "hash": null, 71 | "height": 156, 72 | "mime": "jpeg", 73 | "name": "thumbnail_bird.jpeg", 74 | "path": "thumbnail_bird.jpeg", 75 | "size": 16.14, 76 | "width": 208, 77 | }, 78 | }, 79 | "message": "Uploading success!", 80 | "name": "bird.jpeg", 81 | "path": "bird.jpeg", 82 | "success": true, 83 | } 84 | `; 85 | 86 | exports[`Image Manipulation should have no thumbnail 1`] = ` 87 | { 88 | "formats": { 89 | "large": { 90 | "ext": "jpeg", 91 | "hash": null, 92 | "height": 750, 93 | "mime": "jpeg", 94 | "name": "large_bird.jpeg", 95 | "path": "large_bird.jpeg", 96 | "size": 184.49, 97 | "width": 1000, 98 | }, 99 | "medium": { 100 | "ext": "jpeg", 101 | "hash": null, 102 | "height": 562, 103 | "mime": "jpeg", 104 | "name": "medium_bird.jpeg", 105 | "path": "medium_bird.jpeg", 106 | "size": 107.23, 107 | "width": 750, 108 | }, 109 | "small": { 110 | "ext": "jpeg", 111 | "hash": null, 112 | "height": 375, 113 | "mime": "jpeg", 114 | "name": "small_bird.jpeg", 115 | "path": "small_bird.jpeg", 116 | "size": 52.4, 117 | "width": 500, 118 | }, 119 | }, 120 | "message": "Uploading success!", 121 | "name": "bird.jpeg", 122 | "path": "bird.jpeg", 123 | "success": true, 124 | } 125 | `; 126 | 127 | exports[`Image Manipulation should not generate image sizes when uploading a file that not an image 1`] = ` 128 | { 129 | "formats": undefined, 130 | "message": "Uploading success!", 131 | "name": "test-file.txt", 132 | "path": "test-file.txt", 133 | "success": true, 134 | } 135 | `; 136 | -------------------------------------------------------------------------------- /packages/image-manipulation/lib/config.ts: -------------------------------------------------------------------------------- 1 | import { ResizeOptions } from 'sharp'; 2 | 3 | export interface Breakpoints { 4 | [x: string]: number; 5 | } 6 | 7 | export const DEFAULT_THUBNAIL_RESIZE_OPTIONS: ResizeOptions = { 8 | width: 245, 9 | height: 156, 10 | fit: 'inside', 11 | }; 12 | 13 | export const DEFAULT_BREAKPOINTS: Breakpoints = { 14 | large: 1000, 15 | medium: 750, 16 | small: 500, 17 | }; 18 | 19 | class ImageManipulationConfig { 20 | private thumbnailResizeOptions: ResizeOptions = DEFAULT_THUBNAIL_RESIZE_OPTIONS; 21 | 22 | private breakpoints: Breakpoints = DEFAULT_BREAKPOINTS; 23 | 24 | getThumbnailResizeOptions(): ResizeOptions { 25 | return this.thumbnailResizeOptions; 26 | } 27 | 28 | setThumbnailResizeOptions(options: ResizeOptions) { 29 | this.thumbnailResizeOptions = options; 30 | } 31 | 32 | getBreakpoints(): Breakpoints { 33 | return this.breakpoints; 34 | } 35 | 36 | setBreakpoints(newBreakpoints: Breakpoints) { 37 | this.breakpoints = newBreakpoints; 38 | } 39 | } 40 | 41 | export const config = new ImageManipulationConfig(); 42 | -------------------------------------------------------------------------------- /packages/image-manipulation/lib/image-manipulation.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Storage from '@file-storage/core'; 3 | import { getRootCwd } from '@file-storage/common'; 4 | import ImageManipulation from '@file-storage/image-manipulation'; 5 | 6 | describe('Image Manipulation', () => { 7 | beforeAll(() => { 8 | Storage.config({ 9 | plugins: [ImageManipulation], 10 | }); 11 | }); 12 | 13 | it('should generates 2 custom breakpoints: size1, size2', () => { 14 | const imageFileStream = fs.createReadStream( 15 | getRootCwd() + '/test/support/images/photo-1000x750.jpeg', 16 | ); 17 | 18 | ImageManipulation.config({ 19 | breakpoints: { 20 | size1: 400, 21 | size2: 600, 22 | }, 23 | }); 24 | 25 | return expect(Storage.put(imageFileStream, 'bird.jpeg')).resolves.toMatchSnapshot(); 26 | }); 27 | 28 | it('should generates custom thumbnail size', () => { 29 | const imageFileStream = fs.createReadStream( 30 | getRootCwd() + '/test/support/images/photo-1000x750.jpeg', 31 | ); 32 | 33 | ImageManipulation.config({ 34 | breakpoints: null, 35 | thumbnailResizeOptions: { 36 | width: 333, 37 | height: 222, 38 | fit: 'contain', 39 | }, 40 | }); 41 | 42 | return expect(Storage.put(imageFileStream, 'bird.jpeg')).resolves.toMatchSnapshot(); 43 | }); 44 | 45 | it('should have no responsive formats', () => { 46 | const imageFileStream = fs.createReadStream( 47 | getRootCwd() + '/test/support/images/photo-1000x750.jpeg', 48 | ); 49 | 50 | ImageManipulation.config({ 51 | breakpoints: null, 52 | }); 53 | 54 | return expect(Storage.put(imageFileStream, 'bird.jpeg')).resolves.toMatchSnapshot(); 55 | }); 56 | 57 | it('should have no thumbnail', () => { 58 | const imageFileStream = fs.createReadStream( 59 | getRootCwd() + '/test/support/images/photo-1000x750.jpeg', 60 | ); 61 | 62 | ImageManipulation.config({ 63 | thumbnailResizeOptions: null, 64 | }); 65 | 66 | return expect(Storage.put(imageFileStream, 'bird.jpeg')).resolves.toMatchSnapshot(); 67 | }); 68 | 69 | it('should not generate image sizes when uploading a file that not an image', () => { 70 | const fileStream = fs.createReadStream(getRootCwd() + '/test/support/files/test-file.txt'); 71 | 72 | return expect(Storage.put(fileStream, 'test-file.txt')).resolves.toMatchSnapshot(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/image-manipulation/lib/image-manipulation.ts: -------------------------------------------------------------------------------- 1 | import { ImageStats, Plugin } from '@file-storage/common'; 2 | import { ResizeOptions } from 'sharp'; 3 | import { 4 | config, 5 | Breakpoints, 6 | DEFAULT_BREAKPOINTS, 7 | DEFAULT_THUBNAIL_RESIZE_OPTIONS, 8 | } from './config'; 9 | import { generateResponsiveFormats, generateThumbnail } from './utils'; 10 | 11 | interface ImageManipulationOptions { 12 | /** 13 | * Thumbnail Resize Options, default: 14 | * ``` 15 | * width: 245 16 | * height: 156 17 | * fit: inside 18 | * ``` 19 | */ 20 | thumbnailResizeOptions?: ResizeOptions; 21 | /** 22 | * Responsive formats breakpoints, defaults: 23 | * ``` 24 | * large: 1000 25 | * medium: 750 26 | * small: 500 27 | * ``` 28 | */ 29 | breakpoints?: Breakpoints; 30 | } 31 | 32 | interface AfterPutData { 33 | [x: string]: ImageStats; 34 | } 35 | 36 | export class ImageManipulation extends Plugin { 37 | static readonly pluginName = 'image_manipulation'; 38 | afterPutKey = 'formats'; 39 | 40 | static config(options: ImageManipulationOptions = {}) { 41 | const { 42 | breakpoints = DEFAULT_BREAKPOINTS, 43 | thumbnailResizeOptions = DEFAULT_THUBNAIL_RESIZE_OPTIONS, 44 | } = options; 45 | 46 | config.setBreakpoints(breakpoints); 47 | config.setThumbnailResizeOptions(thumbnailResizeOptions); 48 | } 49 | 50 | async afterPut(path: string): Promise { 51 | let file: ImageStats & { 52 | buffer: Buffer; 53 | }; 54 | 55 | try { 56 | file = await this.disk.imageStats(path, true); 57 | } catch (error) { 58 | return; 59 | } 60 | 61 | const thumbnailFile = await generateThumbnail(file); 62 | const fileData: { [x: string]: ImageStats } = {}; 63 | 64 | if (thumbnailFile) { 65 | await this.disk.put(thumbnailFile.buffer, thumbnailFile.path); 66 | 67 | delete thumbnailFile.buffer; 68 | 69 | fileData.thumbnail = thumbnailFile; 70 | } 71 | 72 | const formats = await generateResponsiveFormats(file); 73 | 74 | if (Array.isArray(formats) && formats.length > 0) { 75 | for (const format of formats) { 76 | if (!format) continue; 77 | 78 | const { key, file } = format; 79 | 80 | await this.disk.put(file.buffer, file.path); 81 | 82 | delete file.buffer; 83 | 84 | fileData[key] = file; 85 | } 86 | } 87 | 88 | return fileData; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/image-manipulation/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { ImageManipulation as default } from './image-manipulation'; 2 | -------------------------------------------------------------------------------- /packages/image-manipulation/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { ImageStats } from '@file-storage/common'; 2 | 3 | export interface FileStats extends ImageStats { 4 | buffer: Buffer; 5 | } 6 | -------------------------------------------------------------------------------- /packages/image-manipulation/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { posix } from 'path'; 2 | import sharp, { ResizeOptions } from 'sharp'; 3 | import { config } from './config'; 4 | import { FileStats } from './types'; 5 | 6 | const { parse, format } = posix; 7 | 8 | interface Dimension { 9 | width: number; 10 | height: number; 11 | } 12 | 13 | const settings = { 14 | sizeOptimization: true, 15 | autoOrientation: true, 16 | }; 17 | 18 | const getMetadatas = (buffer: Buffer): Promise => 19 | sharp(buffer) 20 | .metadata() 21 | .catch(() => null); 22 | 23 | const getDimensions = (buffer: Buffer): Promise => 24 | getMetadatas(buffer).then(({ width = null, height = null }) => ({ width, height })); 25 | 26 | const resizeTo = (buffer: Buffer, options: ResizeOptions) => 27 | sharp(buffer) 28 | .withMetadata() 29 | .resize(options) 30 | .toBuffer() 31 | .catch(() => null); 32 | 33 | const generateThumbnail = async (file: FileStats): Promise => { 34 | if (!(await canBeProccessed(file.buffer))) { 35 | return null; 36 | } 37 | 38 | const thumbnailResizeOptions = config.getThumbnailResizeOptions(); 39 | 40 | const { width, height } = await getDimensions(file.buffer); 41 | 42 | if ( 43 | thumbnailResizeOptions && 44 | (width > thumbnailResizeOptions.width || height > thumbnailResizeOptions.height) 45 | ) { 46 | const newBuff = await resizeTo(file.buffer, thumbnailResizeOptions); 47 | 48 | if (newBuff) { 49 | const { width, height, size } = await getMetadatas(newBuff); 50 | const thumbnailName = `thumbnail_${file.name}`; 51 | const parsedPath = parse(file.path); 52 | parsedPath.base = thumbnailName; 53 | 54 | return { 55 | name: thumbnailName, 56 | hash: null, 57 | ext: file.ext, 58 | mime: file.mime, 59 | width, 60 | height, 61 | size: bytesToKbytes(size), 62 | buffer: newBuff, 63 | path: `${format(parsedPath)}`, 64 | }; 65 | } 66 | } 67 | 68 | return null; 69 | }; 70 | 71 | const optimize = async (buffer: Buffer) => { 72 | const { sizeOptimization = false, autoOrientation = false } = settings; 73 | 74 | if (!sizeOptimization || !(await canBeProccessed(buffer))) { 75 | return { buffer }; 76 | } 77 | 78 | const sharpInstance = autoOrientation ? sharp(buffer).rotate() : sharp(buffer); 79 | 80 | return sharpInstance 81 | .toBuffer({ resolveWithObject: true }) 82 | .then(({ data, info }) => { 83 | const output = buffer.length < data.length ? buffer : data; 84 | 85 | return { 86 | buffer: output, 87 | info: { 88 | width: info.width, 89 | height: info.height, 90 | size: bytesToKbytes(output.length), 91 | }, 92 | }; 93 | }) 94 | .catch(() => ({ buffer })); 95 | }; 96 | 97 | const generateResponsiveFormats = async (file: FileStats) => { 98 | if (!(await canBeProccessed(file.buffer))) { 99 | return []; 100 | } 101 | 102 | const originalDimensions = await getDimensions(file.buffer); 103 | 104 | const breakpoints = config.getBreakpoints(); 105 | return ( 106 | breakpoints && 107 | Promise.all( 108 | Object.keys(breakpoints).map((key) => { 109 | const breakpoint: number = breakpoints[key]; 110 | 111 | if (breakpointSmallerThan(breakpoint, originalDimensions)) { 112 | return generateBreakpoint(key, { file, breakpoint }); 113 | } 114 | }), 115 | ) 116 | ); 117 | }; 118 | 119 | interface GenerateBreakpointOptions { 120 | file: FileStats; 121 | breakpoint: number; 122 | } 123 | 124 | const generateBreakpoint = async ( 125 | key: string, 126 | { file, breakpoint }: GenerateBreakpointOptions, 127 | ): Promise<{ key: string; file: FileStats }> => { 128 | const newBuff = await resizeTo(file.buffer, { 129 | width: breakpoint, 130 | height: breakpoint, 131 | fit: 'inside', 132 | }); 133 | 134 | if (newBuff) { 135 | const { width, height, size } = await getMetadatas(newBuff); 136 | const name = `${key}_${file.name}`; 137 | const parsedPath = parse(file.path); 138 | parsedPath.base = name; 139 | 140 | return { 141 | key, 142 | file: { 143 | name, 144 | hash: null, 145 | ext: file.ext, 146 | mime: file.mime, 147 | width, 148 | height, 149 | size: bytesToKbytes(size), 150 | buffer: newBuff, 151 | path: `${format(parsedPath)}`, 152 | }, 153 | }; 154 | } 155 | }; 156 | 157 | const breakpointSmallerThan = (breakpoint: number, { width, height }: Dimension) => { 158 | return breakpoint < width || breakpoint < height; 159 | }; 160 | 161 | const formatsToProccess = ['jpeg', 'png', 'webp', 'tiff']; 162 | const canBeProccessed = async (buffer: Buffer) => { 163 | const { format } = await getMetadatas(buffer); 164 | return format && formatsToProccess.includes(format); 165 | }; 166 | 167 | const bytesToKbytes = (bytes: number) => Math.round((bytes / 1000) * 100) / 100; 168 | 169 | export { generateThumbnail, generateResponsiveFormats, optimize }; 170 | -------------------------------------------------------------------------------- /packages/image-manipulation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@file-storage/image-manipulation", 3 | "version": "1.3.9", 4 | "description": "Image manipulation for file-storage", 5 | "author": "Dang Nguyen ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "dist/esm/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "exports": { 14 | "import": "./dist/esm/index.js", 15 | "require": "./dist/cjs/index.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/googlicius/file-storage.git" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org", 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "yarn clean && yarn compile", 27 | "clean": "rimraf -rf ./dist", 28 | "compile": "tsc -p tsconfig-cjs.build.json && tsc -p tsconfig-esm.build.json && ../../create-dist-modules.sh", 29 | "test": "jest", 30 | "test:coverage": "jest --coverage", 31 | "test:detectOpenHandles": "jest --detectOpenHandles" 32 | }, 33 | "dependencies": { 34 | "sharp": "^0.32.6" 35 | }, 36 | "devDependencies": { 37 | "@file-storage/common": "^1.3.9", 38 | "@types/sharp": "^0.28.5", 39 | "rimraf": "~3.0.2", 40 | "typescript": "~4.3.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/image-manipulation/tsconfig-cjs.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-cjs.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "declarationDir": "./dist/types" 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/image-manipulation/tsconfig-esm.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-esm.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "declaration": false 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/image-manipulation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/local-driver/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | storage -------------------------------------------------------------------------------- /packages/local-driver/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.3.9](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.9) (2023-07-16) 7 | 8 | **Note:** Version bump only for package @file-storage/local 9 | 10 | 11 | 12 | 13 | 14 | ## [1.3.8](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.8) (2023-07-16) 15 | 16 | **Note:** Version bump only for package @file-storage/local 17 | 18 | 19 | 20 | 21 | 22 | ## [1.3.7](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.7) (2023-07-10) 23 | 24 | **Note:** Version bump only for package @file-storage/local 25 | 26 | 27 | 28 | 29 | 30 | ## [1.3.6](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.6) (2023-06-04) 31 | 32 | **Note:** Version bump only for package @file-storage/local 33 | 34 | 35 | 36 | 37 | 38 | ## [1.3.5](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.5) (2023-01-28) 39 | 40 | **Note:** Version bump only for package @file-storage/local 41 | 42 | 43 | 44 | 45 | 46 | ## [1.3.4](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.2...v1.3.4) (2023-01-28) 47 | 48 | **Note:** Version bump only for package @file-storage/local 49 | 50 | 51 | 52 | 53 | 54 | ## [1.3.4-alpha.2](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.1...v1.3.4-alpha.2) (2023-01-27) 55 | 56 | **Note:** Version bump only for package @file-storage/local 57 | 58 | 59 | 60 | 61 | 62 | ## [1.3.4-alpha.1](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.0...v1.3.4-alpha.1) (2023-01-27) 63 | 64 | **Note:** Version bump only for package @file-storage/local 65 | 66 | 67 | 68 | 69 | 70 | ## [1.3.4-alpha.0](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.4-alpha.0) (2023-01-26) 71 | 72 | **Note:** Version bump only for package @file-storage/local 73 | 74 | 75 | 76 | 77 | 78 | ## [1.3.3](https://github.com/googlicius/file-storage/compare/v1.3.2...v1.3.3) (2022-04-22) 79 | 80 | **Note:** Version bump only for package @file-storage/local 81 | 82 | 83 | 84 | 85 | 86 | ## [1.3.1](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.3.1) (2021-12-30) 87 | 88 | **Note:** Version bump only for package @file-storage/local 89 | 90 | 91 | 92 | 93 | 94 | ## [1.2.9](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.2.9) (2021-11-13) 95 | 96 | **Note:** Version bump only for package @file-storage/local 97 | 98 | 99 | 100 | 101 | 102 | ## [1.2.8](https://github.com/googlicius/file-storage/compare/v1.2.7...v1.2.8) (2021-10-28) 103 | 104 | **Note:** Version bump only for package @file-storage/local 105 | 106 | 107 | 108 | 109 | 110 | ## [1.2.7](https://github.com/googlicius/file-storage/compare/v1.2.6...v1.2.7) (2021-10-22) 111 | 112 | **Note:** Version bump only for package @file-storage/local 113 | 114 | 115 | 116 | 117 | 118 | ## [1.2.6](https://github.com/googlicius/file-storage/compare/v1.2.5...v1.2.6) (2021-10-11) 119 | 120 | **Note:** Version bump only for package @file-storage/local 121 | 122 | 123 | 124 | 125 | 126 | ## 1.2.3 (2021-09-08) 127 | 128 | **Note:** Version bump only for package @file-storage/local 129 | -------------------------------------------------------------------------------- /packages/local-driver/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____|_) | ___| | 3 | | | | _ \\___ \ __| _ \ __| _` | _` | _ \ 4 | __| | | __/ | | ( | | ( | ( | __/ 5 | _| _|_|\___|_____/ \__|\___/ _| \__,_|\__, |\___| 6 | A file system abstraction for Node.js |___/ 7 | ``` 8 | 9 | Local driver for `file-storage` 10 | -------------------------------------------------------------------------------- /packages/local-driver/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../../babel.config.js', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/local-driver/lib/__snapshots__/local-driver.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Local Disk Upload image from URI to local 1`] = ` 4 | { 5 | "message": "Uploading success!", 6 | "success": true, 7 | } 8 | `; 9 | 10 | exports[`Local Disk Upload local large image will uploaded to many formats 1`] = ` 11 | { 12 | "formats": { 13 | "large": { 14 | "ext": "jpeg", 15 | "hash": null, 16 | "height": 750, 17 | "mime": "jpeg", 18 | "name": "large_photo-1000x750.jpeg", 19 | "path": "my-photo/large_photo-1000x750.jpeg", 20 | "size": 184.49, 21 | "width": 1000, 22 | }, 23 | "medium": { 24 | "ext": "jpeg", 25 | "hash": null, 26 | "height": 562, 27 | "mime": "jpeg", 28 | "name": "medium_photo-1000x750.jpeg", 29 | "path": "my-photo/medium_photo-1000x750.jpeg", 30 | "size": 107.23, 31 | "width": 750, 32 | }, 33 | "small": { 34 | "ext": "jpeg", 35 | "hash": null, 36 | "height": 375, 37 | "mime": "jpeg", 38 | "name": "small_photo-1000x750.jpeg", 39 | "path": "my-photo/small_photo-1000x750.jpeg", 40 | "size": 52.4, 41 | "width": 500, 42 | }, 43 | "thumbnail": { 44 | "ext": "jpeg", 45 | "hash": null, 46 | "height": 156, 47 | "mime": "jpeg", 48 | "name": "thumbnail_photo-1000x750.jpeg", 49 | "path": "my-photo/thumbnail_photo-1000x750.jpeg", 50 | "size": 16.14, 51 | "width": 208, 52 | }, 53 | }, 54 | "message": "Uploading success!", 55 | "name": "photo-1000x750.jpeg", 56 | "path": "my-photo/photo-1000x750.jpeg", 57 | "success": true, 58 | } 59 | `; 60 | 61 | exports[`Local Disk append should append a text to a file 1`] = ` 62 | "First line 63 | Appended text" 64 | `; 65 | -------------------------------------------------------------------------------- /packages/local-driver/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { LocalDriver as default } from './local-driver'; 2 | export * from './local-disk-config.interface'; 3 | -------------------------------------------------------------------------------- /packages/local-driver/lib/local-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Class, LocalDiskConfig as LocalDiskConfigCommon } from '@file-storage/common'; 2 | import { LocalDriver } from './local-driver'; 3 | 4 | export interface LocalDiskConfig extends LocalDiskConfigCommon { 5 | driver: Class; 6 | } 7 | -------------------------------------------------------------------------------- /packages/local-driver/lib/local-driver.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Storage from '@file-storage/core'; 3 | import { 4 | DriverName, 5 | FileNotFoundError, 6 | getRootCwd, 7 | LocalDiskConfig, 8 | streamToBuffer, 9 | } from '@file-storage/common'; 10 | import ImageManipulation from '@file-storage/image-manipulation'; 11 | 12 | describe('Local Disk', () => { 13 | beforeAll(() => { 14 | Storage.config({ 15 | diskConfigs: [ 16 | { 17 | driver: DriverName.LOCAL, 18 | name: 'local', 19 | root: 'storage', 20 | }, 21 | ], 22 | plugins: [ImageManipulation], 23 | }); 24 | }); 25 | 26 | test('Default disk is local', () => { 27 | expect(Storage.name).toEqual('local'); 28 | }); 29 | 30 | test('Upload image to local success', () => { 31 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 32 | return expect( 33 | Storage.disk('local').put(fileReadStream, 'test_upload/bird.jpeg'), 34 | ).resolves.toMatchObject({ 35 | success: true, 36 | message: 'Uploading success!', 37 | }); 38 | }); 39 | 40 | test('Upload image to local success (Using default disk)', () => { 41 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 42 | return expect(Storage.put(fileReadStream, 'test_upload/bird$$.jpeg')).resolves.toMatchObject({ 43 | success: true, 44 | message: 'Uploading success!', 45 | }); 46 | }); 47 | 48 | test('Upload local large image will uploaded to many formats', () => { 49 | const imageFileStream = fs.createReadStream( 50 | getRootCwd() + '/test/support/images/photo-1000x750.jpeg', 51 | ); 52 | return expect( 53 | Storage.put(imageFileStream, 'my-photo/photo-1000x750.jpeg'), 54 | ).resolves.toMatchSnapshot(); 55 | }); 56 | 57 | test('Upload image from URI to local', () => { 58 | return expect( 59 | Storage.disk('local').uploadImageFromExternalUri( 60 | 'https://raw.githubusercontent.com/googlicius/file-storage/main/test/support/images/bird.jpeg', 61 | 'test_upload/test_image_from_uri.jpeg', 62 | ), 63 | ).resolves.toMatchSnapshot(); 64 | }); 65 | 66 | test('Upload image from URI to local (Using default disk)', () => { 67 | return expect( 68 | Storage.uploadImageFromExternalUri( 69 | 'https://raw.githubusercontent.com/googlicius/file-storage/main/test/support/images/bird.jpeg', 70 | 'test_upload/test_image_from_uri.jpeg', 71 | ), 72 | ).resolves.toMatchObject({ 73 | success: true, 74 | message: 'Uploading success!', 75 | }); 76 | }); 77 | 78 | test('Upload a file is not an image from URI to local', () => { 79 | const uri = 'https://previews.magnoliabox.com/kew/mb_hero/kewgar066/MUS-FAPC1114_850.jpg'; 80 | return expect( 81 | Storage.disk('local').uploadImageFromExternalUri(uri, 'test_upload/test_image_from_uri.jpeg'), 82 | ).rejects.toEqual(new Error('Not an image: ' + uri)); 83 | }); 84 | 85 | test('Delete image from local (Using default disk)', async () => { 86 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 87 | const filePath = 'test_upload/image123.jpeg'; 88 | await Storage.put(fileReadStream, filePath); 89 | 90 | return expect(Storage.delete(filePath)).resolves.toBeTruthy(); 91 | }); 92 | 93 | test('Delete file does not exists', () => { 94 | return expect(Storage.delete('not-exists.jpeg')).rejects.toThrowError(FileNotFoundError); 95 | }); 96 | 97 | test('Download image from local', async () => { 98 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 99 | // Storage.put 100 | await Storage.put(fileReadStream, 'test_upload/bird2.jpeg'); 101 | return expect(Storage.get('test_upload/bird2.jpeg')).resolves.toBeTruthy(); 102 | }); 103 | 104 | test('Download not exists image from local error', async () => { 105 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 106 | await Storage.put(fileReadStream, 'test_upload/bird2.jpeg'); 107 | return expect(Storage.get('not-exist.jpeg')).rejects.toThrowError(FileNotFoundError); 108 | }); 109 | 110 | test('File is exists', async () => { 111 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 112 | await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 113 | 114 | return expect(Storage.exists('bird-images/bird.jpeg')).resolves.toEqual(true); 115 | }); 116 | 117 | test('File is not exists', async () => { 118 | return expect(Storage.exists('not-exists.jpeg')).resolves.toEqual(false); 119 | }); 120 | 121 | test('Get file size', async () => { 122 | const fileReadStream2 = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 123 | await Storage.disk('local').put(fileReadStream2, 'bird-images/bird.jpeg'); 124 | 125 | return expect(Storage.size('bird-images/bird.jpeg')).resolves.toEqual(56199); 126 | }); 127 | 128 | test('Last modified', async () => { 129 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 130 | await Storage.disk('local').put(fileReadStream, 'bird-images/bird.jpeg'); 131 | const lastMod = await Storage.lastModified('bird-images/bird.jpeg'); 132 | expect(typeof lastMod).toBe('number'); 133 | }); 134 | 135 | test('Copy file', async () => { 136 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 137 | const putResult = await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 138 | await Storage.copy(putResult.path, 'photos/bird-copy.jpeg'); 139 | 140 | const size = await Storage.size('photos/bird-copy.jpeg'); 141 | expect(typeof size).toBe('number'); 142 | }); 143 | 144 | test('Move file', async () => { 145 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 146 | const putResult = await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 147 | await Storage.move(putResult.path, 'photoss/new-path.jpeg'); 148 | 149 | const size = await Storage.size('photoss/new-path.jpeg'); 150 | expect(typeof size).toBe('number'); 151 | 152 | return expect(Storage.size('bird-images/bird.jpeg')).rejects.toThrowError(FileNotFoundError); 153 | }); 154 | 155 | describe('append', () => { 156 | it('should append a text to a file', async () => { 157 | const putResult = await Storage.put(Buffer.from('First line'), 'to-be-appended.txt'); 158 | await Storage.append('\nAppended text', putResult.path); 159 | 160 | const buff = await streamToBuffer(await Storage.get(putResult.path)); 161 | 162 | return expect(buff.toString()).toMatchSnapshot(); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /packages/local-driver/lib/local-driver.ts: -------------------------------------------------------------------------------- 1 | import fs, { ReadStream } from 'fs'; 2 | import { dirname } from 'path'; 3 | import { Stream } from 'stream'; 4 | import { 5 | Driver, 6 | DriverName, 7 | ensureDirectoryExistence, 8 | exists, 9 | FileNotFoundError, 10 | LocalDiskConfig, 11 | PutResult, 12 | toStream, 13 | } from '@file-storage/common'; 14 | 15 | export class LocalDriver extends Driver { 16 | private root: string; 17 | private publicUrl: string; 18 | static readonly driverName = DriverName.LOCAL; 19 | 20 | constructor(config: LocalDiskConfig) { 21 | super(config); 22 | const { root = '', publicUrl } = config; 23 | this.root = root; 24 | this.publicUrl = publicUrl; 25 | } 26 | 27 | private rootPath(path: string): string { 28 | return this.root + '/' + path; 29 | } 30 | 31 | protected errorHandler(error: any) { 32 | if (error.code === 'ENOENT') { 33 | throw new FileNotFoundError(error.message); 34 | } 35 | throw error; 36 | } 37 | 38 | protected async stats(path: string): Promise { 39 | return new Promise((resolve, reject) => { 40 | fs.stat(this.rootPath(path), (err, stats) => { 41 | if (err) { 42 | return reject(err); 43 | } 44 | resolve(stats); 45 | }); 46 | }); 47 | } 48 | 49 | url(path: string): string { 50 | return `${this.publicUrl}/${path}`; 51 | } 52 | 53 | async exists(path: string) { 54 | try { 55 | await exists(this.rootPath(path)); 56 | return true; 57 | } catch (e) { 58 | return false; 59 | } 60 | } 61 | 62 | async size(path: string): Promise { 63 | const stats = await this.stats(path); 64 | return stats.size; 65 | } 66 | 67 | async lastModified(path: string): Promise { 68 | const stats = await this.stats(path); 69 | return stats.ctimeMs; 70 | } 71 | 72 | put(data: Stream | Buffer, path: string): Promise> { 73 | ensureDirectoryExistence(this.rootPath(path)); 74 | const writeStream = fs.createWriteStream(this.rootPath(path)); 75 | 76 | return new Promise((resolve, reject) => { 77 | toStream(data) 78 | .pipe(writeStream) 79 | .on('finish', () => { 80 | resolve({ 81 | success: true, 82 | message: 'Uploading success!', 83 | }); 84 | }) 85 | .on('error', (error) => { 86 | reject(error); 87 | }); 88 | }); 89 | } 90 | 91 | async get(path: string): Promise { 92 | await exists(this.rootPath(path)); 93 | return fs.createReadStream(this.rootPath(path)); 94 | } 95 | 96 | delete(path: string): Promise { 97 | return new Promise((resolve, reject) => { 98 | fs.unlink(this.rootPath(path), (err) => { 99 | if (err) { 100 | return reject(err); 101 | } 102 | resolve(true); 103 | }); 104 | }); 105 | } 106 | 107 | copy(path: string, newPath: string): Promise { 108 | ensureDirectoryExistence(this.rootPath(newPath)); 109 | 110 | return new Promise((resolve, reject) => { 111 | fs.copyFile(this.rootPath(path), this.rootPath(newPath), (err) => { 112 | if (err) { 113 | return reject(err); 114 | } 115 | resolve(); 116 | }); 117 | }); 118 | } 119 | 120 | move(path: string, newPath: string): Promise { 121 | ensureDirectoryExistence(this.rootPath(newPath)); 122 | 123 | return new Promise((resolve, reject) => { 124 | fs.rename(this.rootPath(path), this.rootPath(newPath), (err) => { 125 | if (err) { 126 | return reject(err); 127 | } 128 | resolve(); 129 | }); 130 | }); 131 | } 132 | 133 | append(data: string | Buffer, path: string): Promise { 134 | return new Promise((resolve, reject) => { 135 | fs.appendFile(this.rootPath(path), data, (err) => { 136 | if (err) { 137 | return reject(err); 138 | } 139 | resolve(); 140 | }); 141 | }); 142 | } 143 | 144 | async makeDir(path: string): Promise { 145 | const dir = dirname(path); 146 | if (fs.existsSync(dir)) { 147 | throw new Error('Directory already exists'); 148 | } 149 | fs.mkdirSync(dir, { recursive: true }); 150 | 151 | return dir; 152 | } 153 | 154 | removeDir(dir: string): Promise { 155 | return new Promise((resolve, reject) => { 156 | fs.rm(dir, { recursive: true }, (err) => { 157 | if (err) { 158 | reject(err); 159 | return; 160 | } 161 | resolve(dir); 162 | }); 163 | }); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /packages/local-driver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@file-storage/local", 3 | "version": "1.3.9", 4 | "description": "Local driver for file-storage", 5 | "author": "Dang Nguyen ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "dist/esm/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "exports": { 14 | "import": "./dist/esm/index.js", 15 | "require": "./dist/cjs/index.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/googlicius/file-storage.git" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org", 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "yarn clean && yarn compile", 27 | "clean": "rimraf -rf ./dist", 28 | "compile": "tsc -p tsconfig-cjs.build.json && tsc -p tsconfig-esm.build.json && ../../create-dist-modules.sh", 29 | "test": "jest", 30 | "test:coverage": "jest --coverage", 31 | "test:detectOpenHandles": "jest --detectOpenHandles" 32 | }, 33 | "devDependencies": { 34 | "@file-storage/common": "^1.3.9", 35 | "rimraf": "~3.0.2", 36 | "typescript": "~4.3.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/local-driver/tsconfig-cjs.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-cjs.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "declarationDir": "./dist/types" 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/local-driver/tsconfig-esm.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-esm.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "declaration": false 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/local-driver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/s3-driver/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /packages/s3-driver/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.3.9](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.9) (2023-07-16) 7 | 8 | **Note:** Version bump only for package @file-storage/s3 9 | 10 | 11 | 12 | 13 | 14 | ## [1.3.8](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.8) (2023-07-16) 15 | 16 | **Note:** Version bump only for package @file-storage/s3 17 | 18 | 19 | 20 | 21 | 22 | ## [1.3.7](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.7) (2023-07-10) 23 | 24 | **Note:** Version bump only for package @file-storage/s3 25 | 26 | 27 | 28 | 29 | 30 | ## [1.3.6](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.6) (2023-06-04) 31 | 32 | **Note:** Version bump only for package @file-storage/s3 33 | 34 | 35 | 36 | 37 | 38 | ## [1.3.5](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.5) (2023-01-28) 39 | 40 | **Note:** Version bump only for package @file-storage/s3 41 | 42 | 43 | 44 | 45 | 46 | ## [1.3.4](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.2...v1.3.4) (2023-01-28) 47 | 48 | **Note:** Version bump only for package @file-storage/s3 49 | 50 | 51 | 52 | 53 | 54 | ## [1.3.4-alpha.2](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.1...v1.3.4-alpha.2) (2023-01-27) 55 | 56 | **Note:** Version bump only for package @file-storage/s3 57 | 58 | 59 | 60 | 61 | 62 | ## [1.3.4-alpha.1](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.0...v1.3.4-alpha.1) (2023-01-27) 63 | 64 | **Note:** Version bump only for package @file-storage/s3 65 | 66 | 67 | 68 | 69 | 70 | ## [1.3.4-alpha.0](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.4-alpha.0) (2023-01-26) 71 | 72 | **Note:** Version bump only for package @file-storage/s3 73 | 74 | 75 | 76 | 77 | 78 | ## [1.3.3](https://github.com/googlicius/file-storage/compare/v1.3.2...v1.3.3) (2022-04-22) 79 | 80 | **Note:** Version bump only for package @file-storage/s3 81 | 82 | 83 | 84 | 85 | 86 | ## [1.3.1](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.3.1) (2021-12-30) 87 | 88 | **Note:** Version bump only for package @file-storage/s3 89 | 90 | 91 | 92 | 93 | 94 | ## [1.2.9](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.2.9) (2021-11-13) 95 | 96 | **Note:** Version bump only for package @file-storage/s3 97 | 98 | 99 | 100 | 101 | 102 | ## [1.2.8](https://github.com/googlicius/file-storage/compare/v1.2.7...v1.2.8) (2021-10-28) 103 | 104 | **Note:** Version bump only for package @file-storage/s3 105 | 106 | 107 | 108 | 109 | 110 | ## [1.2.7](https://github.com/googlicius/file-storage/compare/v1.2.6...v1.2.7) (2021-10-22) 111 | 112 | **Note:** Version bump only for package @file-storage/s3 113 | 114 | 115 | 116 | 117 | 118 | ## [1.2.6](https://github.com/googlicius/file-storage/compare/v1.2.5...v1.2.6) (2021-10-11) 119 | 120 | **Note:** Version bump only for package @file-storage/s3 121 | 122 | 123 | 124 | 125 | 126 | ## 1.2.3 (2021-09-08) 127 | 128 | **Note:** Version bump only for package @file-storage/s3 129 | -------------------------------------------------------------------------------- /packages/s3-driver/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____|_) | ___| | 3 | | | | _ \\___ \ __| _ \ __| _` | _` | _ \ 4 | __| | | __/ | | ( | | ( | ( | __/ 5 | _| _|_|\___|_____/ \__|\___/ _| \__,_|\__, |\___| 6 | A file system abstraction for Node.js |___/ 7 | ``` 8 | 9 | Amazon S3 driver for `file-storage` 10 | -------------------------------------------------------------------------------- /packages/s3-driver/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../../babel.config.js', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/s3-driver/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { S3Driver as default } from './s3-driver'; 2 | export * from './s3-disk-config.interface'; 3 | -------------------------------------------------------------------------------- /packages/s3-driver/lib/s3-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Class, S3DiskConfig as S3DiskConfigCommon } from '@file-storage/common'; 2 | import { S3Driver } from './s3-driver'; 3 | 4 | export interface S3DiskConfig extends S3DiskConfigCommon { 5 | driver: Class; 6 | } 7 | -------------------------------------------------------------------------------- /packages/s3-driver/lib/s3-driver.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { Readable } from 'stream'; 3 | import Storage from '@file-storage/core'; 4 | import { 5 | DriverName, 6 | FileNotFoundError, 7 | getRootCwd, 8 | S3DiskConfig, 9 | UnauthenticatedError, 10 | } from '@file-storage/common'; 11 | import ImageManipulation from '@file-storage/image-manipulation'; 12 | import { S3Driver } from './s3-driver'; 13 | 14 | describe('S3 Disk test', () => { 15 | const bucketName1 = 'mybucket1'; 16 | const bucketName2 = 'mybucket2'; 17 | 18 | beforeAll(async () => { 19 | Storage.config({ 20 | defaultDiskName: 's3Default', 21 | diskConfigs: [ 22 | { 23 | driver: DriverName.S3, 24 | name: 's3Test', 25 | bucketName: bucketName1, 26 | endpoint: 'http://localhost:4566', 27 | forcePathStyle: true, 28 | region: 'ap-southeast-1', 29 | credentials: { 30 | accessKeyId: '123abc', 31 | secretAccessKey: '123abc', 32 | }, 33 | }, 34 | { 35 | driver: DriverName.S3, 36 | name: 's3Default', 37 | bucketName: bucketName2, 38 | endpoint: 'http://localhost:4566', 39 | forcePathStyle: true, 40 | region: 'us-east-1', 41 | credentials: { 42 | accessKeyId: 'test', 43 | secretAccessKey: 'test123', 44 | }, 45 | }, 46 | { 47 | driver: DriverName.S3, 48 | name: 's3NoCredentials', 49 | bucketName: bucketName2, 50 | endpoint: 'http://localhost:4566', 51 | region: 'us-east-1', 52 | forcePathStyle: true, 53 | }, 54 | ], 55 | plugins: [ImageManipulation], 56 | }); 57 | 58 | await Promise.all([ 59 | Storage.disk('s3Test').instance().setupMockS3(bucketName1), 60 | Storage.disk('s3Test').instance().setupMockS3(bucketName2), 61 | ]); 62 | }); 63 | 64 | test('Default disk is s3Default', () => { 65 | expect(Storage.name).toEqual('s3Default'); 66 | }); 67 | 68 | test('Disk name is s3Test', () => { 69 | expect(Storage.disk('s3Test').name).toEqual('s3Test'); 70 | }); 71 | 72 | // test('Default disk does not have any bucket', async () => { 73 | // const bucketListResult = await (Storage as S3Driver).s3Instance 74 | // .listBuckets() 75 | // .promise(); 76 | 77 | // expect(bucketListResult.Buckets.length).toEqual(0); 78 | // }); 79 | 80 | test('Upload image from URI to S3', () => { 81 | return expect( 82 | Storage.disk('s3Test').uploadImageFromExternalUri( 83 | 'https://raw.githubusercontent.com/googlicius/file-storage/main/test/support/images/bird.jpeg', 84 | 'test_upload/test_image_from_uri.jpeg', 85 | ), 86 | ).resolves.toMatchObject({ 87 | success: true, 88 | message: 'Uploading success!', 89 | }); 90 | }); 91 | 92 | test('should upload image to s3 success', () => { 93 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 94 | return expect( 95 | Storage.disk('s3Test').put(fileReadStream, 'test_upload/bird2.jpeg'), 96 | ).resolves.toMatchObject({ 97 | success: true, 98 | message: 'Uploading success!', 99 | }); 100 | }); 101 | 102 | test('Upload s3 large image will uploaded to many formats', () => { 103 | const imageFileStream = fs.createReadStream( 104 | getRootCwd() + '/test/support/images/photo-1000x750.jpeg', 105 | ); 106 | return expect( 107 | Storage.put(imageFileStream, 'my-photo/photo-1000x750.jpeg'), 108 | ).resolves.toMatchObject({ 109 | success: true, 110 | message: 'Uploading success!', 111 | formats: { 112 | thumbnail: { 113 | name: 'thumbnail_photo-1000x750.jpeg', 114 | hash: null, 115 | ext: 'jpeg', 116 | mime: 'jpeg', 117 | width: 208, 118 | height: 156, 119 | size: 16.14, 120 | path: 'my-photo/thumbnail_photo-1000x750.jpeg', 121 | }, 122 | large: { 123 | name: 'large_photo-1000x750.jpeg', 124 | hash: null, 125 | ext: 'jpeg', 126 | mime: 'jpeg', 127 | width: 1000, 128 | height: 750, 129 | size: 184.49, 130 | path: 'my-photo/large_photo-1000x750.jpeg', 131 | }, 132 | medium: { 133 | name: 'medium_photo-1000x750.jpeg', 134 | hash: null, 135 | ext: 'jpeg', 136 | mime: 'jpeg', 137 | width: 750, 138 | height: 562, 139 | size: 107.23, 140 | path: 'my-photo/medium_photo-1000x750.jpeg', 141 | }, 142 | small: { 143 | name: 'small_photo-1000x750.jpeg', 144 | hash: null, 145 | ext: 'jpeg', 146 | mime: 'jpeg', 147 | width: 500, 148 | height: 375, 149 | size: 52.4, 150 | path: 'my-photo/small_photo-1000x750.jpeg', 151 | }, 152 | }, 153 | }); 154 | }); 155 | 156 | test('Upload image to s3 success (Using default disk)', () => { 157 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 158 | return expect(Storage.put(fileReadStream, 'test_upload/bird23.jpeg')).resolves.toMatchObject({ 159 | success: true, 160 | message: 'Uploading success!', 161 | }); 162 | }); 163 | 164 | test('Upload to s3 using Storage facade', () => { 165 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 166 | return expect(Storage.put(fileReadStream, 'bird.jpeg')).resolves.toMatchObject({ 167 | success: true, 168 | message: 'Uploading success!', 169 | }); 170 | }); 171 | 172 | test('Download image from s3', async () => { 173 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 174 | await Storage.put(fileReadStream, 'test_upload/bird2.jpeg'); 175 | const stream = await Storage.get('test_upload/bird2.jpeg'); 176 | expect(stream instanceof Readable).toBe(true); 177 | }); 178 | 179 | test('Download not exists image from s3 error', async () => { 180 | return expect(Storage.disk('s3Test').get('not-exists.jpeg')).rejects.toThrowError( 181 | FileNotFoundError, 182 | ); 183 | }); 184 | 185 | test('Delete image from s3 bucket (Using default disk)', async () => { 186 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 187 | const filePath = 'test_upload/image123.jpeg'; 188 | await Storage.put(fileReadStream, filePath); 189 | 190 | return expect(Storage.delete(filePath)).resolves.toBeTruthy(); 191 | }); 192 | 193 | test('Upload to another bucket', async () => { 194 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 195 | const s3Disk = Storage.instance(); 196 | await s3Disk.setupMockS3('another-bucket'); 197 | 198 | return expect( 199 | s3Disk.put(fileReadStream, 'test_upload/image123.jpeg', { 200 | Bucket: 'another-bucket', 201 | }), 202 | ).resolves.toMatchObject({ 203 | success: true, 204 | message: 'Uploading success!', 205 | }); 206 | }); 207 | 208 | test('File is exists', async () => { 209 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 210 | await Storage.disk('s3Default').put(fileReadStream, 'bird-images/bird.jpeg'); 211 | 212 | return expect(Storage.exists('bird-images/bird.jpeg')).resolves.toEqual(true); 213 | }); 214 | 215 | test('File is not exists', async () => { 216 | const exist = await Storage.disk('s3Test').exists('not-exists.jpeg'); 217 | const exist2 = await Storage.exists('not-exists.jpeg'); 218 | expect(exist).toEqual(false); 219 | expect(exist2).toEqual(false); 220 | }); 221 | 222 | test('Get file size', async () => { 223 | const fileReadStream2 = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 224 | await Storage.disk('s3Default').put(fileReadStream2, 'bird-images/bird-size.jpeg'); 225 | 226 | return expect(Storage.size('bird-images/bird-size.jpeg')).resolves.toEqual(56199); 227 | }); 228 | 229 | test('Last modified', async () => { 230 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 231 | await Storage.disk('s3Default').put(fileReadStream, 'bird-images/bird.jpeg'); 232 | const lastMod = await Storage.lastModified('bird-images/bird.jpeg'); 233 | const lastMod2 = await Storage.lastModified('bird-images/bird.jpeg'); 234 | expect(typeof lastMod).toBe('number'); 235 | expect(typeof lastMod2).toBe('number'); 236 | }); 237 | 238 | test('Copy file', async () => { 239 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 240 | const putResult = await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 241 | await Storage.copy(putResult.path, 'photos/bird-copy.jpeg'); 242 | 243 | const size = await Storage.size('photos/bird-copy.jpeg'); 244 | expect(typeof size).toBe('number'); 245 | }); 246 | 247 | test('Move file', async () => { 248 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 249 | const putResult = await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 250 | await Storage.move(putResult.path, 'photos/new-path.jpeg'); 251 | 252 | const size = await Storage.size('photos/new-path.jpeg'); 253 | expect(typeof size).toBe('number'); 254 | 255 | return expect(Storage.size('bird-images/bird.jpeg')).rejects.toThrowError(FileNotFoundError); 256 | }); 257 | 258 | test.skip('No credentials error', () => { 259 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 260 | return expect( 261 | Storage.disk('s3NoCredentials').put(fileReadStream, 'bird-images/bird.jpeg'), 262 | ).rejects.toThrowError(UnauthenticatedError); 263 | }, 10000); 264 | }); 265 | -------------------------------------------------------------------------------- /packages/s3-driver/lib/s3-driver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | S3, 3 | HeadObjectCommandInput, 4 | PutObjectCommandInput, 5 | GetObjectCommandInput, 6 | CopyObjectCommandInput, 7 | DeleteObjectCommandInput, 8 | Bucket, 9 | CreateBucketCommandOutput, 10 | NoSuchKey, 11 | NotFound, 12 | } from '@aws-sdk/client-s3'; 13 | import { CredentialsProviderError } from '@aws-sdk/property-provider'; 14 | import { Upload } from '@aws-sdk/lib-storage'; 15 | import { PassThrough, Stream, Readable } from 'stream'; 16 | import { 17 | Driver, 18 | DriverName, 19 | FileNotFoundError, 20 | PutResult, 21 | S3DiskConfig, 22 | toStream, 23 | UnauthenticatedError, 24 | } from '@file-storage/common'; 25 | 26 | export class S3Driver extends Driver { 27 | private bucketName: string; 28 | private publicUrl?: string; 29 | readonly s3Instance: S3; 30 | static readonly driverName = DriverName.S3; 31 | 32 | constructor(config: S3DiskConfig) { 33 | super(config); 34 | const { bucketName, publicUrl, ...clientConfig } = config; 35 | if (!bucketName) { 36 | throw new Error('Bucket name is required'); 37 | } 38 | this.bucketName = bucketName; 39 | this.publicUrl = publicUrl; 40 | this.s3Instance = new S3(clientConfig); 41 | } 42 | 43 | protected errorHandler(error: any) { 44 | if (error instanceof NoSuchKey || error instanceof NotFound) { 45 | throw new FileNotFoundError(error.message); 46 | } 47 | 48 | if (error instanceof CredentialsProviderError) { 49 | throw new UnauthenticatedError(error.message); 50 | } 51 | 52 | throw error; 53 | } 54 | 55 | protected async stats(path: string) { 56 | const getObjectParams: HeadObjectCommandInput = { 57 | Key: path, 58 | Bucket: this.bucketName, 59 | }; 60 | return this.s3Instance.headObject(getObjectParams); 61 | } 62 | 63 | url(path: string): string { 64 | return this.publicUrl 65 | ? `${this.publicUrl}/${path}` 66 | : `https://${this.bucketName}.s3.amazonaws.com/${path}`; 67 | } 68 | 69 | exists(path: string) { 70 | return this.stats(path).then( 71 | () => true, 72 | () => false, 73 | ); 74 | } 75 | 76 | async size(path: string): Promise { 77 | const data = await this.stats(path); 78 | return data.ContentLength; 79 | } 80 | 81 | async lastModified(path: string): Promise { 82 | const data = await this.stats(path); 83 | return data.LastModified.getTime(); 84 | } 85 | 86 | /** 87 | * Upload to S3. 88 | */ 89 | put( 90 | data: Stream | PassThrough | Buffer, 91 | Key: string, 92 | params?: Partial, 93 | ) { 94 | const upload = new Upload({ 95 | client: this.s3Instance, 96 | params: { 97 | Bucket: this.bucketName, 98 | Key, 99 | Body: toStream(data), 100 | ACL: 'public-read', 101 | ...(typeof params !== 'undefined' && params), 102 | }, 103 | }); 104 | 105 | return upload.done().then((result) => ({ 106 | success: true, 107 | message: 'Uploading success!', 108 | ...result, 109 | })); 110 | } 111 | 112 | /** 113 | * Get a file from s3 bucket. 114 | */ 115 | async get(Key: string): Promise { 116 | const getObjectParams: GetObjectCommandInput = { 117 | Key, 118 | Bucket: this.bucketName, 119 | }; 120 | 121 | const resonpse = await this.s3Instance.getObject(getObjectParams); 122 | 123 | if (resonpse.Body instanceof Readable) { 124 | return resonpse.Body; 125 | } 126 | 127 | throw new Error('Unknown object stream type.'); 128 | } 129 | 130 | async copy(path: string, newPath: string): Promise { 131 | const copyObjectParams: CopyObjectCommandInput = { 132 | CopySource: this.bucketName + '/' + path, 133 | Key: newPath, 134 | Bucket: this.bucketName, 135 | }; 136 | 137 | await this.s3Instance.copyObject(copyObjectParams); 138 | } 139 | 140 | /** 141 | * Delete a file from s3 bucket. 142 | */ 143 | delete(Key: string): Promise { 144 | const deleteParams: DeleteObjectCommandInput = { 145 | Bucket: this.bucketName, 146 | Key, 147 | }; 148 | return this.s3Instance.deleteObject(deleteParams).then(() => true); 149 | } 150 | 151 | async move(path: string, newPath: string): Promise { 152 | await this.copy(path, newPath); 153 | await this.delete(path); 154 | } 155 | 156 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 157 | append(_data: string | Buffer, _path: string): Promise { 158 | return new Promise((_, reject) => 159 | reject('Appending to a file currently not supported with S3'), 160 | ); 161 | } 162 | 163 | /** 164 | * Create bucket for testing purpose. 165 | */ 166 | async setupMockS3(Bucket: string): Promise> { 167 | // TODO Uncomment this; Temporary comment because endpoint is not exists in aws-sdk v3. 168 | // if (this.s3Instance.endpoint.href !== 'http://localhost:4566/') { 169 | // throw new Error('Supported only for testing'); 170 | // } 171 | 172 | const listBucketsResult = await this.s3Instance.listBuckets({}); 173 | const existingBucket = listBucketsResult.Buckets.find((bucket) => bucket.Name === Bucket); 174 | 175 | if (existingBucket) { 176 | return existingBucket; 177 | } 178 | 179 | return this.s3Instance.createBucket({ 180 | Bucket, 181 | }); 182 | } 183 | 184 | makeDir(dir: string): Promise { 185 | const putParams: PutObjectCommandInput = { 186 | Bucket: this.bucketName, 187 | Key: dir.replace(/\/$|$/, '/'), 188 | }; 189 | 190 | return this.s3Instance.putObject(putParams).then(() => dir); 191 | } 192 | 193 | async removeDir(dir: string): Promise { 194 | const deleteParams: DeleteObjectCommandInput = { 195 | Bucket: this.bucketName, 196 | Key: dir.replace(/\/$|$/, '/'), 197 | }; 198 | 199 | await this.s3Instance.deleteObject(deleteParams); 200 | 201 | return dir; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /packages/s3-driver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@file-storage/s3", 3 | "version": "1.3.9", 4 | "description": "S3 disk driver for file-storage", 5 | "author": "Dang Nguyen ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "dist/esm/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "exports": { 14 | "import": "./dist/esm/index.js", 15 | "require": "./dist/cjs/index.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/googlicius/file-storage.git" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org", 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "yarn clean && yarn compile", 27 | "clean": "rimraf -rf ./dist", 28 | "compile": "tsc -p tsconfig-cjs.build.json && tsc -p tsconfig-esm.build.json && ../../create-dist-modules.sh", 29 | "test": "jest", 30 | "test:coverage": "jest --coverage", 31 | "test:detectOpenHandles": "jest --detectOpenHandles" 32 | }, 33 | "dependencies": { 34 | "@aws-sdk/client-s3": "^3.72.0", 35 | "@aws-sdk/lib-storage": "^3.72.0", 36 | "@aws-sdk/property-provider": "^3.366.0" 37 | }, 38 | "devDependencies": { 39 | "@file-storage/common": "^1.3.9", 40 | "rimraf": "~3.0.2", 41 | "typescript": "~4.3.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/s3-driver/tsconfig-cjs.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-cjs.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "declarationDir": "./dist/types" 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/s3-driver/tsconfig-esm.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-esm.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "declaration": false 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/s3-driver/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["lib/**/*.ts"], 7 | "exclude": ["**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/s3-driver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/sftp-driver/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /packages/sftp-driver/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.3.9](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.9) (2023-07-16) 7 | 8 | **Note:** Version bump only for package @file-storage/sftp 9 | 10 | 11 | 12 | 13 | 14 | ## [1.3.8](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.8) (2023-07-16) 15 | 16 | **Note:** Version bump only for package @file-storage/sftp 17 | 18 | 19 | 20 | 21 | 22 | ## [1.3.7](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.7) (2023-07-10) 23 | 24 | **Note:** Version bump only for package @file-storage/sftp 25 | 26 | 27 | 28 | 29 | 30 | ## [1.3.6](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.6) (2023-06-04) 31 | 32 | **Note:** Version bump only for package @file-storage/sftp 33 | 34 | 35 | 36 | 37 | 38 | ## [1.3.5](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.5) (2023-01-28) 39 | 40 | **Note:** Version bump only for package @file-storage/sftp 41 | 42 | 43 | 44 | 45 | 46 | ## [1.3.4](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.2...v1.3.4) (2023-01-28) 47 | 48 | **Note:** Version bump only for package @file-storage/sftp 49 | 50 | 51 | 52 | 53 | 54 | ## [1.3.4-alpha.2](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.1...v1.3.4-alpha.2) (2023-01-27) 55 | 56 | **Note:** Version bump only for package @file-storage/sftp 57 | 58 | 59 | 60 | 61 | 62 | ## [1.3.4-alpha.1](https://github.com/googlicius/file-storage/compare/v1.3.4-alpha.0...v1.3.4-alpha.1) (2023-01-27) 63 | 64 | **Note:** Version bump only for package @file-storage/sftp 65 | 66 | 67 | 68 | 69 | 70 | ## [1.3.4-alpha.0](https://github.com/googlicius/file-storage/compare/v1.3.3...v1.3.4-alpha.0) (2023-01-26) 71 | 72 | **Note:** Version bump only for package @file-storage/sftp 73 | 74 | 75 | 76 | 77 | 78 | ## [1.3.3](https://github.com/googlicius/file-storage/compare/v1.3.2...v1.3.3) (2022-04-22) 79 | 80 | **Note:** Version bump only for package @file-storage/sftp 81 | 82 | 83 | 84 | 85 | 86 | ## [1.3.2](https://github.com/googlicius/file-storage/compare/v1.3.1...v1.3.2) (2022-01-01) 87 | 88 | **Note:** Version bump only for package @file-storage/sftp 89 | 90 | 91 | 92 | 93 | 94 | ## [1.3.1](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.3.1) (2021-12-30) 95 | 96 | **Note:** Version bump only for package @file-storage/sftp 97 | 98 | 99 | 100 | 101 | 102 | ## [1.2.9](https://github.com/googlicius/file-storage/compare/v1.2.8...v1.2.9) (2021-11-13) 103 | 104 | **Note:** Version bump only for package @file-storage/sftp 105 | 106 | 107 | 108 | 109 | 110 | ## [1.2.8](https://github.com/googlicius/file-storage/compare/v1.2.7...v1.2.8) (2021-10-28) 111 | 112 | **Note:** Version bump only for package @file-storage/sftp 113 | 114 | 115 | 116 | 117 | 118 | ## [1.2.7](https://github.com/googlicius/file-storage/compare/v1.2.6...v1.2.7) (2021-10-22) 119 | 120 | **Note:** Version bump only for package @file-storage/sftp 121 | 122 | 123 | 124 | 125 | 126 | ## [1.2.6](https://github.com/googlicius/file-storage/compare/v1.2.5...v1.2.6) (2021-10-11) 127 | 128 | **Note:** Version bump only for package @file-storage/sftp 129 | 130 | 131 | 132 | 133 | 134 | ## 1.2.3 (2021-09-08) 135 | 136 | **Note:** Version bump only for package @file-storage/sftp 137 | -------------------------------------------------------------------------------- /packages/sftp-driver/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____|_) | ___| | 3 | | | | _ \\___ \ __| _ \ __| _` | _` | _ \ 4 | __| | | __/ | | ( | | ( | ( | __/ 5 | _| _|_|\___|_____/ \__|\___/ _| \__,_|\__, |\___| 6 | A file system abstraction for Node.js |___/ 7 | ``` 8 | 9 | SFTP driver for `file-storage` 10 | -------------------------------------------------------------------------------- /packages/sftp-driver/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../../babel.config.js', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/sftp-driver/lib/__snapshots__/sftp-driver.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Sftp Disk test append should append a text to a file 1`] = ` 4 | "First line 5 | Appended line" 6 | `; 7 | -------------------------------------------------------------------------------- /packages/sftp-driver/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { SftpDriver as default } from './sftp-driver'; 2 | export * from './sftp-disk-config.interface'; 3 | -------------------------------------------------------------------------------- /packages/sftp-driver/lib/sftp-disk-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { Class, SftpDiskConfig as SftpDiskConfigCommon } from '@file-storage/common'; 2 | import { SftpDriver } from './sftp-driver'; 3 | 4 | export interface SftpDiskConfig extends SftpDiskConfigCommon { 5 | driver: Class; 6 | } 7 | -------------------------------------------------------------------------------- /packages/sftp-driver/lib/sftp-driver.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Storage from '@file-storage/core'; 3 | import { DriverName, getRootCwd, SftpDiskConfig, streamToBuffer } from '@file-storage/common'; 4 | 5 | describe('Sftp Disk test', () => { 6 | beforeAll(() => { 7 | Storage.config({ 8 | diskConfigs: [ 9 | { 10 | driver: DriverName.SFTP, 11 | name: 'sftp-test', 12 | root: '/upload', 13 | accessOptions: { 14 | host: '127.0.0.1', 15 | port: 2222, 16 | username: 'usertest', 17 | password: 'P@ssw0rd', 18 | }, 19 | }, 20 | ], 21 | }); 22 | }); 23 | 24 | const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 25 | 26 | // FIXME Timeout error on Github Actions 27 | // test('Upload image to sftp', () => { 28 | // jest.setTimeout(15000); 29 | 30 | // return expect(Storage.put(fileReadStream, 'dog.jpeg')).resolves.toMatchObject({ 31 | // success: true, 32 | // message: 'Uploading success!', 33 | // }); 34 | // }); 35 | 36 | // FIXME Timeout error on Github Actions 37 | // test('Upload sftp large image will uploaded to many formats', () => { 38 | // jest.setTimeout(15000); 39 | 40 | // const birdReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 41 | // return expect(Storage.put(birdReadStream, 'my-photo/bird.jpeg')).resolves.toMatchObject({ 42 | // success: true, 43 | // message: 'Uploading success!', 44 | // formats: { 45 | // thumbnail: { 46 | // name: 'thumbnail_bird.jpeg', 47 | // hash: null, 48 | // ext: 'jpeg', 49 | // mime: 'jpeg', 50 | // width: 234, 51 | // height: 156, 52 | // size: 16.47, 53 | // path: 'my-photo/thumbnail_bird.jpeg', 54 | // }, 55 | // small: { 56 | // name: 'small_bird.jpeg', 57 | // hash: null, 58 | // ext: 'jpeg', 59 | // mime: 'jpeg', 60 | // width: 500, 61 | // height: 333, 62 | // size: 34.76, 63 | // path: 'my-photo/small_bird.jpeg', 64 | // }, 65 | // }, 66 | // }); 67 | // }); 68 | 69 | // FIXME Timeout error on Github Actions 70 | // test('Download image from sftp', async () => { 71 | // await Storage.disk('sftp-test').put(fileReadStream, 'bird-images/bird.jpeg'); 72 | 73 | // return expect(Storage.get('bird-images/bird.jpeg')).resolves.toBeTruthy(); 74 | // }); 75 | 76 | // FIXME Timeout error on Github Actions 77 | test.skip('Delete image from sftp', async () => { 78 | await Storage.disk('sftp-test').put(fileReadStream, 'bird-images/bird-delete.jpeg'); 79 | 80 | return expect(Storage.delete('bird-images/bird-delete.jpeg')).resolves.toEqual( 81 | 'Successfully deleted /upload/bird-images/bird-delete.jpeg', 82 | ); 83 | }); 84 | 85 | test.skip('File is exists', async () => { 86 | await Storage.disk('sftp-test').put(fileReadStream, 'bird-images/bird.jpeg'); 87 | 88 | return expect(Storage.exists('bird-images/bird.jpeg')).resolves.toEqual(true); 89 | }); 90 | 91 | test.skip('Check file is not exists', () => { 92 | return expect(Storage.disk('sftp-test').exists('not-exists.jpeg')).resolves.toEqual(false); 93 | }); 94 | 95 | test.skip('Get file not exists', () => { 96 | return expect(Storage.get('my-file/not-exists.jpeg')).rejects.toThrowError(); 97 | }); 98 | 99 | test.skip('Get file size', async () => { 100 | const fileReadStream2 = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 101 | await Storage.disk('sftp-test').put(fileReadStream2, 'file-size/bird.jpeg'); 102 | 103 | return expect(Storage.size('file-size/bird.jpeg')).resolves.toEqual(56199); 104 | }); 105 | 106 | test.skip('Last modified', async () => { 107 | await Storage.disk('sftp-test').put(fileReadStream, 'bird-images/bird.jpeg'); 108 | const lastMod = await Storage.lastModified('bird-images/bird.jpeg'); 109 | expect(typeof lastMod).toBe('number'); 110 | }); 111 | 112 | // FIXME Failed when run all tests. 113 | // test('Move file', async () => { 114 | // const fileReadStream = fs.createReadStream(getRootCwd() + '/test/support/images/bird.jpeg'); 115 | // const putResult = await Storage.put(fileReadStream, 'bird-images/bird.jpeg'); 116 | // await Storage.move(putResult.path, 'photos/new-path2.jpeg'); 117 | 118 | // const size = await Storage.size('photos/new-path2.jpeg'); 119 | // expect(typeof size).toBe('number'); 120 | 121 | // return expect(Storage.size('bird-images/bird.jpeg')).rejects.toThrowError(); 122 | // }); 123 | 124 | describe('append', () => { 125 | it.skip('should append a text to a file', async () => { 126 | const putResult = await Storage.put(Buffer.from('First line'), 'to-be-appended.txt'); 127 | // await sleep(10); 128 | await Storage.append('\nAppended line', putResult.path); 129 | const buff = await streamToBuffer(await Storage.get(putResult.path)); 130 | 131 | return expect(buff.toString()).toMatchSnapshot(); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /packages/sftp-driver/lib/sftp-driver.ts: -------------------------------------------------------------------------------- 1 | import { Readable, PassThrough } from 'stream'; 2 | import { dirname } from 'path'; 3 | import { ReadStream } from 'fs'; 4 | import Client from 'ssh2-sftp-client'; 5 | import { 6 | AnyObject, 7 | Driver, 8 | DriverName, 9 | FileNotFoundError, 10 | PutResult, 11 | SftpDiskConfig, 12 | } from '@file-storage/common'; 13 | 14 | export class SftpDriver extends Driver { 15 | static readonly driverName = DriverName.SFTP; 16 | readonly client: Client; 17 | private root: string; 18 | private accessOptions: AnyObject; 19 | private timeOutToCloseConnection: NodeJS.Timeout; 20 | 21 | constructor(config: SftpDiskConfig) { 22 | super(config); 23 | const { root = '', accessOptions } = config; 24 | this.root = root; 25 | this.accessOptions = accessOptions; 26 | this.client = new Client(); 27 | } 28 | 29 | protected errorHandler(error: any) { 30 | switch (error.code) { 31 | case 2: 32 | case 4: 33 | throw new FileNotFoundError(error.message); 34 | 35 | default: 36 | throw error; 37 | } 38 | } 39 | 40 | private connectToSftpServer(): Promise { 41 | clearTimeout(this.timeOutToCloseConnection); 42 | if (!this.client.sftp) { 43 | return this.client.connect(this.accessOptions); 44 | } 45 | } 46 | 47 | /** 48 | * Close connection after 0.5s, if there is another request, this timeout will be cleared, 49 | * and the connection will be keep open. 50 | */ 51 | private closeConnection() { 52 | this.timeOutToCloseConnection = setTimeout(() => { 53 | if (this.client.sftp) { 54 | this.client.end(); 55 | } 56 | }, 500); 57 | } 58 | 59 | private rootPath(path: string): string { 60 | return this.root + '/' + path; 61 | } 62 | 63 | private async ensureDirectoryExistence(path: string): Promise { 64 | const dir = dirname(path); 65 | const data = await this.client.exists(dir); 66 | 67 | if (!data) { 68 | await this.client.mkdir(dir, true); 69 | } 70 | } 71 | 72 | /** 73 | * A helper function to call client's specific function which auto connect and auto close connection after response. 74 | */ 75 | private async clientFunc(name: string, ...args: any[]): Promise { 76 | await this.connectToSftpServer(); 77 | return this.client[name](...args).finally(() => { 78 | this.closeConnection(); 79 | }); 80 | } 81 | 82 | url(path: string): string { 83 | return this.root + '/' + path; 84 | } 85 | 86 | async exists(path: string): Promise { 87 | const data = await this.clientFunc('exists', this.rootPath(path)); 88 | return !!data; 89 | } 90 | 91 | async size(path: string): Promise { 92 | const data = await this.clientFunc('stat', this.rootPath(path)); 93 | return data.size; 94 | } 95 | 96 | async lastModified(path: string): Promise { 97 | const data = await this.clientFunc('stat', this.rootPath(path)); 98 | return data.modifyTime; 99 | } 100 | 101 | async put(src: Readable, path: string): Promise> { 102 | await this.connectToSftpServer(); 103 | await this.ensureDirectoryExistence(this.rootPath(path)); 104 | 105 | return this.client 106 | .put(src, this.rootPath(path)) 107 | .then(() => ({ 108 | success: true, 109 | message: 'Uploading success!', 110 | })) 111 | .finally(() => { 112 | this.closeConnection(); 113 | }); 114 | } 115 | 116 | get(path: string): Promise { 117 | const passThrough = new PassThrough(); 118 | return this.clientFunc('get', this.rootPath(path), passThrough); 119 | } 120 | 121 | list(path: string): Promise { 122 | return this.clientFunc('list', this.rootPath(path)); 123 | } 124 | 125 | delete(path: string): Promise { 126 | return this.clientFunc('delete', this.rootPath(path)); 127 | } 128 | 129 | async copy(path: string, newPath: string): Promise { 130 | const file = await this.get(path); 131 | await this.put(file, newPath); 132 | } 133 | 134 | async move(path: string, newPath: string): Promise { 135 | await this.connectToSftpServer(); 136 | await this.ensureDirectoryExistence(this.rootPath(newPath)); 137 | 138 | if (this.exists(newPath)) { 139 | await this.delete(newPath); 140 | } 141 | 142 | await this.clientFunc('rename', this.rootPath(path), this.rootPath(newPath)); 143 | } 144 | 145 | async append(data: string | Buffer, path: string): Promise { 146 | data = typeof data === 'string' ? Buffer.from(data) : data; 147 | await this.clientFunc('append', data, this.rootPath(path)); 148 | } 149 | 150 | makeDir(dir: string): Promise { 151 | return this.clientFunc('mkdir', dir, true); 152 | } 153 | 154 | removeDir(dir: string): Promise { 155 | return this.clientFunc('rmdir', this.rootPath(dir)); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /packages/sftp-driver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@file-storage/sftp", 3 | "version": "1.3.9", 4 | "description": "Sftp disk driver for file-storage", 5 | "author": "Dang Nguyen ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "dist/esm/index.js", 9 | "types": "dist/types/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "exports": { 14 | "import": "./dist/esm/index.js", 15 | "require": "./dist/cjs/index.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/googlicius/file-storage.git" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org", 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "yarn clean && yarn compile", 27 | "clean": "rimraf -rf ./dist", 28 | "compile": "tsc -p tsconfig-cjs.build.json && tsc -p tsconfig-esm.build.json && ../../create-dist-modules.sh", 29 | "test": "jest", 30 | "test:coverage": "jest --coverage", 31 | "test:detectOpenHandles": "jest --detectOpenHandles" 32 | }, 33 | "dependencies": { 34 | "ssh2-sftp-client": "^7.0.0" 35 | }, 36 | "peerDependencies": { 37 | "@file-storage/core": "^1.0.0" 38 | }, 39 | "devDependencies": { 40 | "@file-storage/common": "^1.3.9", 41 | "rimraf": "~3.0.2", 42 | "typescript": "~4.3.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/sftp-driver/tsconfig-cjs.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-cjs.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/cjs", 5 | "declarationDir": "./dist/types" 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/sftp-driver/tsconfig-esm.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-esm.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "declaration": false 6 | }, 7 | "include": ["lib/**/*.ts"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/sftp-driver/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["lib/**/*.ts"], 7 | "exclude": ["**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/sftp-driver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /test/support/files/test-file.txt: -------------------------------------------------------------------------------- 1 | This is a file for testing not an image. -------------------------------------------------------------------------------- /test/support/images/bird.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlicius/file-storage/31a26f6a2ae3fe4da087c52bd54586dac701a6e2/test/support/images/bird.jpeg -------------------------------------------------------------------------------- /test/support/images/photo-1000x750.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlicius/file-storage/31a26f6a2ae3fe4da087c52bd54586dac701a6e2/test/support/images/photo-1000x750.jpeg -------------------------------------------------------------------------------- /tsconfig-cjs.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig-esm.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "target": "ES2020", 6 | "moduleResolution": "node" 7 | }, 8 | "exclude": ["**/*.test.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "commonjs", 5 | "target": "es2015", 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "noEmitOnError": true, 9 | "skipLibCheck": true 10 | }, 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | --------------------------------------------------------------------------------