diff --git a/README.md b/README.md index dd93cf6..c0734a6 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@

- Download - Version - License + Download + Version + License

`flydrive` is a framework-agnostic package which provides a powerful wrapper to manage file Storage in [Node.js](https://nodejs.org). @@ -27,16 +27,16 @@ This package is available in the npm registry. It can easily be installed with `npm` or `yarn`. ```bash -$ npm i @slynova/flydrive +$ npm i @pdspicer/flydrive # or -$ yarn add @slynova/flydrive +$ yarn add @pdspicer/flydrive ``` When you require the package in your file, it will give you access to the `StorageManager` class. This class is a facade for the package and should be instantiated with a [configuration object](https://github.com/Slynova-Org/flydrive/blob/master/test/stubs/config.ts). ```javascript -const { StorageManager } = require('@slynova/flydrive'); +const { StorageManager } = require('@pdspicer/flydrive'); const storage = new StorageManager(config); ``` @@ -62,7 +62,7 @@ storage.disk('local').getSignedUrl(); Since we are using TypeScript, you can make use of casting to get the real interface: ```typescript -import { LocalFileSystem } from '@slynova/flydrive'; +import { LocalFileSystem } from '@pdspicer/flydrive'; storage.disk('local'); ``` diff --git a/package.json b/package.json index 4a4c825..8c12644 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@slynova/flydrive", - "version": "1.0.0-0", + "name": "flydrive-manager", + "version": "1.0.7", "description": "Flexible and Fluent way to manage storage in Node.js.", "main": "./build/index.js", "license": "MIT", @@ -20,7 +20,8 @@ "contributors": [ "Harminder Virk ", "Michaƫl Zasso ", - "Krzysztof Chrapka " + "Krzysztof Chrapka ", + "Paul Spicer " ], "files": [ "build" diff --git a/src/Storage/AmazonWebServicesS3Storage.ts b/src/Storage/AmazonWebServicesS3Storage.ts index 08907c8..c7bc34c 100644 --- a/src/Storage/AmazonWebServicesS3Storage.ts +++ b/src/Storage/AmazonWebServicesS3Storage.ts @@ -6,7 +6,7 @@ */ import { Readable } from 'stream'; -import S3, { ClientConfiguration } from 'aws-sdk/clients/s3'; +import S3 from 'aws-sdk/clients/s3'; import {UnknownException, NoSuchBucket, FileNotFound, InvalidInput, PermissionMissing} from '../Exceptions'; import { ContentResponse, @@ -22,6 +22,8 @@ import { import { MetadataConverter } from '../utils/MetadataConverter' import { isReadableStream } from "../utils"; import { Storage } from "./Storage"; +import {Agent as httpAgent} from "http"; +import {Agent as httpsAgent} from "https"; function handleError(err: Error, path: string, bucket: string): Error { switch (err.name) { @@ -37,7 +39,7 @@ function handleError(err: Error, path: string, bucket: string): Error { } export class AmazonWebServicesS3Storage extends Storage { - constructor(protected readonly $driver: S3, protected readonly $bucket: string) { + private constructor(private readonly $driver: S3, protected readonly $bucket: string) { super(); } @@ -281,8 +283,75 @@ export class AmazonWebServicesS3Storage extends Storage { } } -export interface AWSS3Config extends ClientConfiguration { +export interface AWSS3Config { key: string; secret: string; bucket: string; + endpoint?: string; + params?: { + [key: string]: any; + }; + computeChecksums?: boolean; + convertResponseTypes?: boolean; + correctClockSkew?: boolean; + customUserAgent?: string; + credentials?: AWSCredentials|null; + credentialProvider?: AWSCredentialProviderChain; + httpOptions?: { + proxy?: string; + agent?: httpAgent | httpsAgent; + connectTimeout?: number; + timeout?: number; + xhrAsync?: boolean; + xhrWithCredentials?: boolean; + }; + logger?: { + write?: (...any) => void; + log?: (...any) => void; + }; + maxRedirects?: number; + maxRetries?: number; + paramValidation?: boolean | { + min?: boolean; + max?: boolean; + pattern?: boolean; + enum?: boolean; + }; + region?: string; + retryDelayOptions?: { + base?: number; + customBackoff?: (retryCount: number, err?: Error) => number; + }; + s3BucketEndpoint?: boolean; + s3DisableBodySigning?: boolean; + s3ForcePathStyle?: boolean; + s3UsEast1RegionalEndpoint?: "regional"|"legacy"; + s3UseArnRegion?: boolean; + signatureCache?: boolean; + signatureVersion?: "v2"|"v3"|"v4"|string; + sslEnabled?: boolean; + systemClockOffset?: number; + useAccelerateEndpoint?: boolean; + dynamoDbCrc32?: boolean; + endpointDiscoveryEnabled?: boolean; + endpointCacheSize?: number; + hostPrefixEnabled?: boolean; + stsRegionalEndpoints?: "legacy"|"regional"; + useDualstack?: boolean; + apiVersion?: "2006-03-01"|"latest"|string; } + +export interface AWSCredentials { + accessKeyId: string + secretAccessKey: string + sessionToken?: string + expired?: boolean; + expireTime?: Date; +} + +export interface AWSCredentialProviderChain { + resolve(callback:(err: any, credentials: AWSCredentials) => void): AWSCredentialProviderChain; + resolvePromise(): Promise; + providers: AWSCredentials[]|(() => AWSCredentials)[]; +} + diff --git a/src/Storage/AzureBlockBlobStorage.ts b/src/Storage/AzureBlockBlobStorage.ts index 8425eaf..969da6c 100644 --- a/src/Storage/AzureBlockBlobStorage.ts +++ b/src/Storage/AzureBlockBlobStorage.ts @@ -6,12 +6,10 @@ * @author Christopher Chrapka */ import { Storage } from "./Storage"; -import { +import AzureStorageBlob, { BlobDownloadHeaders, - BlobSASPermissions, - BlobServiceClient, BlockBlobClient, ContainerClient, - generateBlobSASQueryParameters, RestError, StorageSharedKeyCredential + RestError, StorageSharedKeyCredential } from "@azure/storage-blob"; import {Readable, PassThrough} from "stream"; import { @@ -24,9 +22,8 @@ import { SignedUrlResponse } from "../types"; import {isReadableStream} from "../utils"; -import {InvalidInput} from "../Exceptions/InvalidInput"; import {streamToBuffer} from "../utils/streamToBuffer"; -import {AuthorizationRequired, FileNotFound, UnknownException} from "../Exceptions"; +import {AuthorizationRequired, FileNotFound, UnknownException, InvalidInput} from "../Exceptions"; import {MetadataConverter} from "../utils/MetadataConverter"; import {BlockBlobUploadOptions} from "@azure/storage-blob/src/Clients"; @@ -37,13 +34,24 @@ export interface AzureBlobStorageConfig { export class AzureBlockBlobStorage extends Storage { - constructor(private readonly $containerClient: ContainerClient) { + private static _azure: typeof AzureStorageBlob; + + private static get AzureStorageBlob () { + if (this._azure) return this._azure; + this._azure = require('@azure/storage-blob'); + return this._azure; + } + private get AzureStorageBlob () { + return AzureBlockBlobStorage.AzureStorageBlob; + } + + private constructor(private readonly $containerClient: ContainerClient) { super(); } static fromConfig(config: AzureBlobStorageConfig): AzureBlockBlobStorage { return new AzureBlockBlobStorage( - BlobServiceClient + this.AzureStorageBlob.BlobServiceClient .fromConnectionString(config.connectionString) .getContainerClient(config.container), ); @@ -153,11 +161,11 @@ export class AzureBlockBlobStorage extends Storage const expiresOn = new Date(new Date().valueOf() + expiry * 1000); const client = this.blockBlobClient(location); - const token = await generateBlobSASQueryParameters( + const token = await this.AzureStorageBlob.generateBlobSASQueryParameters( { containerName: container, blobName: location, - permissions: BlobSASPermissions.parse("r"), // Required + permissions: this.AzureStorageBlob.BlobSASPermissions.parse("r"), // Required startsOn, // Required expiresOn, // Optional }, diff --git a/src/Storage/GoogleCloudStorage.ts b/src/Storage/GoogleCloudStorage.ts index a48935e..adf20ba 100644 --- a/src/Storage/GoogleCloudStorage.ts +++ b/src/Storage/GoogleCloudStorage.ts @@ -6,7 +6,7 @@ */ import { Readable } from 'stream'; -import {StorageOptions, Bucket, File, CreateWriteStreamOptions} from '@google-cloud/storage'; +import {Bucket, File, CreateWriteStreamOptions} from '@google-cloud/storage'; import { Storage } from './Storage'; import { isReadableStream, pipeline } from '../utils'; import { @@ -18,9 +18,15 @@ import { PropertiesResponse, FileListResponse, PutOptions, DeleteResponse } from '../types'; -import { FileNotFound, PermissionMissing, UnknownException, AuthorizationRequired, WrongKeyPath } from '../Exceptions'; +import { + FileNotFound, + PermissionMissing, + UnknownException, + AuthorizationRequired, + WrongKeyPath, + InvalidInput +} from '../Exceptions'; import {MetadataConverter} from "../utils/MetadataConverter"; -import {InvalidInput} from "../Exceptions/InvalidInput"; function handleError(err: Error & { code?: number | string }, path: string): Error { switch (err.code) { @@ -38,7 +44,7 @@ function handleError(err: Error & { code?: number | string }, path: string): Err } export class GoogleCloudStorage extends Storage { - public constructor(private readonly $bucket: Bucket) { + private constructor(private readonly $bucket: Bucket) { super(); } @@ -260,6 +266,48 @@ export class GoogleCloudStorage extends Storage { } } -export interface GoogleCloudStorageConfig extends StorageOptions { +export interface GoogleCloudStorageConfig { bucket: string; + autoRetry?: boolean; + maxRetries?: number; + promise?: typeof Promise; + apiEndpoint?: string; + keyFilename?: string; + keyFile?: string; + credentials?: CredentialBody; + clientOptions?: JWTOptions | OAuth2ClientOptions | UserRefreshClientOptions; + scopes?: string | string[]; + projectId?: string; +} + +export interface CredentialBody { + client_email?: string; + private_key?: string; +} + +export interface JWTOptions extends RefreshOptions { + email?: string; + keyFile?: string; + key?: string; + keyId?: string; + scopes?: string | string[]; + subject?: string; + additionalClaims?: {}; +} + +export interface OAuth2ClientOptions extends RefreshOptions { + clientId?: string; + clientSecret?: string; + redirectUri?: string; +} + +export interface UserRefreshClientOptions extends RefreshOptions { + clientId?: string; + clientSecret?: string; + refreshToken?: string; +} + +export interface RefreshOptions { + eagerRefreshThresholdMillis?: number; + forceRefreshOnFailure?: boolean; } diff --git a/src/Storage/LocalStorage.ts b/src/Storage/LocalStorage.ts index 1f3505e..75a78e1 100644 --- a/src/Storage/LocalStorage.ts +++ b/src/Storage/LocalStorage.ts @@ -24,7 +24,7 @@ import { } from '../types'; import {promisify} from "util"; import {MetadataConverter} from "../utils/MetadataConverter"; -import {InvalidInput} from "../Exceptions/InvalidInput"; +import {InvalidInput} from "../Exceptions"; function handleError(err: Error & { code: string; path?: string }, fullPath: string): Error { switch (err.code) { @@ -46,8 +46,8 @@ export class LocalStorage extends Storage { constructor(config: LocalFileSystemConfig) { super(); this.$root = resolve(config.root); - this.$dataDirectory = config.dataDirectory || join(this.$root, 'data'); - this.$metaDirectory = config.metadataDirectory || join(this.$root, 'meta'); + this.$dataDirectory = config.dataDirectory ? resolve(config.dataDirectory) : join(this.$root, 'data'); + this.$metaDirectory = config.metadataDirectory ? resolve(config.metadataDirectory) : join(this.$root, 'meta'); } static fromConfig(config: LocalFileSystemConfig): Storage { @@ -90,7 +90,7 @@ export class LocalStorage extends Storage { */ public async delete(location: string): Promise { const fullPath = this.dataPath(location); - let wasDeleted: boolean = true; + let wasDeleted = true; try { await Promise.all([ @@ -271,7 +271,7 @@ export class LocalStorage extends Storage { } private async *flatListAbsolute(prefix: string): AsyncGenerator { - let prefixDirectory = (prefix[prefix.length-1] === '/') ? prefix : dirname(prefix); + const prefixDirectory = (prefix[prefix.length-1] === '/') ? prefix : dirname(prefix); try { for (const file of await promisify(fs.readdir)(prefixDirectory, {withFileTypes: true, encoding: 'utf-8'})) { diff --git a/src/Storage/Storage.ts b/src/Storage/Storage.ts index 56cb569..9264787 100644 --- a/src/Storage/Storage.ts +++ b/src/Storage/Storage.ts @@ -18,7 +18,6 @@ import { } from '../types'; export interface StorageConstructor { - new(...args: any[]): T; fromConfig(config: object): T; } diff --git a/src/Storage/index.ts b/src/Storage/index.ts index dc66d08..f7fb052 100644 --- a/src/Storage/index.ts +++ b/src/Storage/index.ts @@ -9,10 +9,12 @@ import { AmazonWebServicesS3Storage } from './AmazonWebServicesS3Storage'; import { AzureBlockBlobStorage } from "./AzureBlockBlobStorage"; import { GoogleCloudStorage } from './GoogleCloudStorage'; import { LocalStorage } from './LocalStorage'; +import { Storage as AbstractStorage } from './Storage'; -export default { +export { AmazonWebServicesS3Storage, AzureBlockBlobStorage, GoogleCloudStorage, LocalStorage, + AbstractStorage, }; diff --git a/src/StorageManager.ts b/src/StorageManager.ts index e172da0..c424e25 100644 --- a/src/StorageManager.ts +++ b/src/StorageManager.ts @@ -17,12 +17,12 @@ export class StorageManager { /** * Configuration of the storage manager. */ - private readonly _config: StorageManagerConfig; + private _config: StorageManagerConfig; /** * Created disk instances */ - private readonly _diskInstances: Map; + private _diskInstances: Map; /** * List of available storages @@ -30,14 +30,18 @@ export class StorageManager { private _storages: Map = new Map(); constructor(config: StorageManagerConfig) { - this._config = Object.assign({disks: {}}, config); - this._diskInstances = new Map(); + this.config(config); this.registerStorage('azureBlob', AzureBlockBlobStorage); this.registerStorage('gcs', GoogleCloudStorage); this.registerStorage('local', LocalStorage); this.registerStorage('s3', AmazonWebServicesS3Storage); } + config(config: StorageManagerConfig) { + this._config = Object.assign({disks: {}}, config); + this._diskInstances = new Map(); + } + addDisk(name: string, config: StorageManagerDiskConfig) { if (this._config.disks.hasOwnProperty(name)) { throw InvalidConfig.duplicateDiskName(name); diff --git a/src/index.ts b/src/index.ts index 09c45d8..f4e3796 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,6 @@ -export { StorageManager } from './StorageManager'; +import {StorageManager} from './StorageManager'; + +export {StorageManager}; export * from './Storage'; export * from './types'; +export const manager = new StorageManager({disks: {}}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..e8dd22a --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig", + "include": ["./**/*"] +} diff --git a/test/unit/azure-bbs-driver.spec.ts b/test/unit/azure-bbs-driver.spec.ts index f82b100..0e5b025 100644 --- a/test/unit/azure-bbs-driver.spec.ts +++ b/test/unit/azure-bbs-driver.spec.ts @@ -2,7 +2,7 @@ import crypto from 'crypto'; import fs from 'fs'; import uuid from 'uuid'; -import { AzureBlockBlobStorage } from "../../src/Storage/AzureBlockBlobStorage"; +import { AzureBlockBlobStorage } from '../../src/Storage'; import { BlobServiceClient } from "@azure/storage-blob"; import { streamToString } from "../../src/utils/streamToString"; import { AuthorizationRequired } from "../../src/Exceptions"; diff --git a/test/unit/gcs-driver.spec.ts b/test/unit/gcs-driver.spec.ts index 2fc9372..87cf9ba 100644 --- a/test/unit/gcs-driver.spec.ts +++ b/test/unit/gcs-driver.spec.ts @@ -1,6 +1,6 @@ import uuid from 'uuid/v4'; -import { GoogleCloudStorage } from '../../src/Storage/GoogleCloudStorage'; +import { GoogleCloudStorage } from '../../src/Storage'; import { runGenericStorageSpec } from "../stubs/storage.generic"; const testBucket = process.env.GCS_BUCKET || 'flydrive-test'; diff --git a/test/unit/local-driver.spec.ts b/test/unit/local-driver.spec.ts index 0ae5f4a..b4f8c8a 100644 --- a/test/unit/local-driver.spec.ts +++ b/test/unit/local-driver.spec.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import { LocalStorage } from "../../src/Storage/LocalStorage"; +import { LocalStorage } from '../../src/Storage'; import { runGenericStorageSpec } from "../stubs/storage.generic"; import { MethodNotSupported } from "../../src/Exceptions"; diff --git a/test/unit/s3-driver.spec.ts b/test/unit/s3-driver.spec.ts index 9d22bb5..43d056f 100644 --- a/test/unit/s3-driver.spec.ts +++ b/test/unit/s3-driver.spec.ts @@ -6,10 +6,10 @@ */ import S3 from "aws-sdk/clients/s3"; -import { AmazonWebServicesS3Storage, AWSS3Config } from '../../src/Storage/AmazonWebServicesS3Storage'; +import { AmazonWebServicesS3Storage } from '../../src/Storage'; import { runGenericStorageSpec } from "../stubs/storage.generic"; -const config: AWSS3Config = { +const config = { key: process.env.S3_KEY || '', secret: process.env.S3_SECRET || '', bucket: process.env.S3_BUCKET || '', @@ -21,13 +21,13 @@ const driver = new S3({ secretAccessKey: config.secret, ...config, }); -const storage = new AmazonWebServicesS3Storage(driver, config.bucket); +const storage = AmazonWebServicesS3Storage.fromConfig(config); describe('Amazon Web Services S3 Storage', () => { describe('.getUrl', () => { test('get public url to a file', () => { const url = storage.getUrl('dummy-file1.txt'); - expect(url).toStrictEqual(`https://${driver.endpoint.host}/${config.bucket}/dummy-file1.txt`); + expect(url).toStrictEqual(`https://${config.bucket}.${driver.endpoint.host}/dummy-file1.txt`); }); test('get public url to a file when region is not defined', () => { diff --git a/test/unit/storage-manager.spec.ts b/test/unit/storage-manager.spec.ts index e8011c9..a6b35e2 100644 --- a/test/unit/storage-manager.spec.ts +++ b/test/unit/storage-manager.spec.ts @@ -7,8 +7,8 @@ import { resolve } from 'path'; -import { StorageManager } from '../../src/StorageManager'; -import { LocalStorage } from '../../src/Storage/LocalStorage'; +import { StorageManager } from '../../src'; +import { LocalStorage } from '../../src/Storage'; import { Storage } from "../../src/Storage/Storage"; describe('Storage Manager', () => { diff --git a/tsconfig.json b/tsconfig.json index cdc65a2..3e9f0ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,9 @@ "include": ["src/**/*"], "exclude": ["node_modules"], "compilerOptions": { - "allowSyntheticDefaultImports": true, "declaration": true, "esModuleInterop": true, - "lib": ["es2017"], + "lib": ["es2017", "dom"], "module": "commonjs", "moduleResolution": "node", "noUnusedLocals": true, @@ -13,6 +12,6 @@ "outDir": "build", "removeComments": false, "strictNullChecks": true, - "target": "es2017", + "target": "es2017" } }