feat: refactor storage module (#521)

* feat: refactor storage module

* fix: folder need to be kebab case

* fix: comment wrong auth
This commit is contained in:
Jérémy M
2023-07-05 16:34:39 +02:00
committed by GitHub
parent 6e1ffdcc72
commit 2961fed932
74 changed files with 330 additions and 355 deletions

View File

@ -0,0 +1,11 @@
import { Readable } from 'stream';
export interface StorageDriver {
read(params: { folderPath: string; filename: string }): Promise<Readable>;
write(params: {
file: Buffer | Uint8Array | string;
name: string;
folder: string;
mimeType: string | undefined;
}): Promise<void>;
}

View File

@ -0,0 +1,57 @@
import * as fs from 'fs/promises';
import { createReadStream, existsSync } from 'fs';
import { join, dirname } from 'path';
import { StorageDriver } from './interfaces/storage-driver.interface';
import { Readable } from 'stream';
import { kebabCase } from 'src/utils/kebab-case';
export interface LocalDriverOptions {
storagePath: string;
}
export class LocalDriver implements StorageDriver {
private options: LocalDriverOptions;
constructor(options: LocalDriverOptions) {
this.options = options;
}
async createFolder(path: string) {
if (existsSync(path)) {
return;
}
return fs.mkdir(path, { recursive: true });
}
async write(params: {
file: Buffer | Uint8Array | string;
name: string;
folder: string;
mimeType: string | undefined;
}): Promise<void> {
const filePath = join(
`${this.options.storagePath}/`,
kebabCase(params.folder),
params.name,
);
const folderPath = dirname(filePath);
await this.createFolder(folderPath);
await fs.writeFile(filePath, params.file);
}
async read(params: {
folderPath: string;
filename: string;
}): Promise<Readable> {
const filePath = join(
`${this.options.storagePath}/`,
params.folderPath,
params.filename,
);
return createReadStream(filePath);
}
}

View File

@ -1,28 +1,26 @@
import { Injectable, Inject } from '@nestjs/common';
import { MODULE_OPTIONS_TOKEN } from './s3-storage.module-definition';
import { S3StorageModuleOptions } from './interfaces';
import {
CreateBucketCommandInput,
GetObjectCommand,
GetObjectCommandInput,
GetObjectCommandOutput,
HeadBucketCommandInput,
NotFound,
PutObjectCommand,
PutObjectCommandInput,
PutObjectCommandOutput,
S3,
S3ClientConfig,
} from '@aws-sdk/client-s3';
import { StorageDriver } from './interfaces/storage-driver.interface';
import { Readable } from 'stream';
import { kebabCase } from 'src/utils/kebab-case';
@Injectable()
export class S3StorageService {
export interface S3DriverOptions extends S3ClientConfig {
bucketName: string;
region: string;
}
export class S3Driver implements StorageDriver {
private s3Client: S3;
private bucketName: string;
constructor(
@Inject(MODULE_OPTIONS_TOKEN)
private readonly options: S3StorageModuleOptions,
) {
constructor(options: S3DriverOptions) {
const { bucketName, region, ...s3Options } = options;
if (!bucketName || !region) {
@ -37,28 +35,39 @@ export class S3StorageService {
return this.s3Client;
}
async uploadFile(
params: Omit<PutObjectCommandInput, 'Bucket'>,
): Promise<PutObjectCommandOutput> {
async write(params: {
file: Buffer | Uint8Array | string;
name: string;
folder: string;
mimeType: string | undefined;
}): Promise<void> {
const command = new PutObjectCommand({
...params,
Key: `${kebabCase(params.folder)}/${params.name}`,
Body: params.file,
ContentType: params.mimeType,
Bucket: this.bucketName,
});
await this.createBucket({ Bucket: this.bucketName });
return this.s3Client.send(command);
await this.s3Client.send(command);
}
async getFile(
params: Omit<GetObjectCommandInput, 'Bucket'>,
): Promise<GetObjectCommandOutput> {
async read(params: {
folderPath: string;
filename: string;
}): Promise<Readable> {
const command = new GetObjectCommand({
...params,
Key: `${params.folderPath}/${params.filename}`,
Bucket: this.bucketName,
});
const file = await this.s3Client.send(command);
return this.s3Client.send(command);
if (!file || !file.Body || !(file.Body instanceof Readable)) {
throw new Error('Unable to get file stream');
}
return Readable.from(file.Body);
}
async checkBucketExists(args: HeadBucketCommandInput) {

View File

@ -0,0 +1 @@
export const STORAGE_DRIVER = Symbol('STORAGE_DRIVER');

View File

@ -0,0 +1,13 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { FileStorageModuleOptions } from './interfaces';
export const {
ConfigurableModuleClass,
MODULE_OPTIONS_TOKEN,
OPTIONS_TYPE,
ASYNC_OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<FileStorageModuleOptions>({
moduleName: 'FileStorage',
})
.setClassMethodName('forRoot')
.build();

View File

@ -0,0 +1,48 @@
import { DynamicModule, Global } from '@nestjs/common';
import { FileStorageService } from './file-storage.service';
import { LocalDriver } from './drivers/local.driver';
import { S3Driver } from './drivers/s3.driver';
import {
FileStorageModuleAsyncOptions,
FileStorageModuleOptions,
} from './interfaces';
import { STORAGE_DRIVER } from './file-storage.constants';
@Global()
export class FileStorageModule {
static forRoot(options: FileStorageModuleOptions): DynamicModule {
const provider = {
provide: STORAGE_DRIVER,
useValue:
options.type === 's3'
? new S3Driver(options.options)
: new LocalDriver(options.options),
};
return {
module: FileStorageModule,
providers: [FileStorageService, provider],
exports: [FileStorageService],
};
}
static forRootAsync(options: FileStorageModuleAsyncOptions): DynamicModule {
const provider = {
provide: STORAGE_DRIVER,
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
return config?.type === 's3'
? new S3Driver(config.options)
: new LocalDriver(config.options);
},
inject: options.inject || [],
};
return {
module: FileStorageModule,
imports: options.imports || [],
providers: [FileStorageService, provider],
exports: [FileStorageService],
};
}
}

View File

@ -1,22 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { S3StorageService } from './s3-storage.service';
import { MODULE_OPTIONS_TOKEN } from './s3-storage.module-definition';
import { FileStorageService } from './file-storage.service';
import { STORAGE_DRIVER } from './file-storage.constants';
describe('S3StorageService', () => {
let service: S3StorageService;
describe('FileStorageService', () => {
let service: FileStorageService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
S3StorageService,
FileStorageService,
{
provide: MODULE_OPTIONS_TOKEN,
provide: STORAGE_DRIVER,
useValue: {},
},
],
}).compile();
service = module.get<S3StorageService>(S3StorageService);
service = module.get<FileStorageService>(FileStorageService);
});
it('should be defined', () => {

View File

@ -0,0 +1,22 @@
import { Inject, Injectable } from '@nestjs/common';
import { STORAGE_DRIVER } from './file-storage.constants';
import { StorageDriver } from './drivers/interfaces/storage-driver.interface';
import { Readable } from 'stream';
@Injectable()
export class FileStorageService implements StorageDriver {
constructor(@Inject(STORAGE_DRIVER) private driver: StorageDriver) {}
write(params: {
file: string | Buffer | Uint8Array;
name: string;
folder: string;
mimeType: string | undefined;
}): Promise<void> {
return this.driver.write(params);
}
read(params: { folderPath: string; filename: string }): Promise<Readable> {
return this.driver.read(params);
}
}

View File

@ -0,0 +1,25 @@
import { StorageType } from 'src/integrations/environment/interfaces/storage.interface';
import { S3DriverOptions } from '../drivers/s3.driver';
import { LocalDriverOptions } from '../drivers/local.driver';
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
export interface S3DriverFactoryOptions {
type: StorageType.S3;
options: S3DriverOptions;
}
export interface LocalDriverFactoryOptions {
type: StorageType.Local;
options: LocalDriverOptions;
}
export type FileStorageModuleOptions =
| S3DriverFactoryOptions
| LocalDriverFactoryOptions;
export type FileStorageModuleAsyncOptions = {
useFactory: (
...args: any[]
) => FileStorageModuleOptions | Promise<FileStorageModuleOptions>;
} & Pick<ModuleMetadata, 'imports'> &
Pick<FactoryProvider, 'inject'>;

View File

@ -0,0 +1 @@
export * from './file-storage.interface';

View File

@ -1,59 +1,61 @@
import { Module } from '@nestjs/common';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { S3StorageModule } from './s3-storage/s3-storage.module';
import { S3StorageModuleOptions } from './s3-storage/interfaces';
import { LocalStorageModule } from './local-storage/local-storage.module';
import { LocalStorageModuleOptions } from './local-storage/interfaces';
import { EnvironmentModule } from './environment/environment.module';
import { EnvironmentService } from './environment/environment.service';
import { FileStorageModule } from './file-storage/file-storage.module';
import { FileStorageModuleOptions } from './file-storage/interfaces';
import { StorageType } from './environment/interfaces/storage.interface';
/**
* S3 Storage Module factory
* @param config
* @returns S3ModuleOptions
*/
const S3StorageModuleFactory = async (
environmentService: EnvironmentService,
): Promise<S3StorageModuleOptions> => {
const bucketName = environmentService.getStorageS3Name();
const region = environmentService.getStorageS3Region();
return {
bucketName: bucketName ?? '',
credentials: fromNodeProviderChain({
clientConfig: { region },
}),
forcePathStyle: true,
region: region ?? '',
};
};
/**
* LocalStorage Module factory
* FileStorage Module factory
* @param environment
* @returns LocalStorageModuleOptions
* @returns FileStorageModuleOptions
*/
const localStorageModuleFactory = async (
const fileStorageModuleFactory = async (
environmentService: EnvironmentService,
): Promise<LocalStorageModuleOptions> => {
const storagePath = environmentService.getStorageLocalPath();
): Promise<FileStorageModuleOptions> => {
const type = environmentService.getStorageType();
return {
storagePath: process.cwd() + '/' + storagePath,
};
switch (type) {
case undefined:
case StorageType.Local: {
const storagePath = environmentService.getStorageLocalPath();
return {
type: StorageType.Local,
options: {
storagePath: process.cwd() + '/' + storagePath,
},
};
}
case StorageType.S3: {
const bucketName = environmentService.getStorageS3Name();
const region = environmentService.getStorageS3Region();
return {
type: StorageType.S3,
options: {
bucketName: bucketName ?? '',
credentials: fromNodeProviderChain({
clientConfig: { region },
}),
forcePathStyle: true,
region: region ?? '',
},
};
}
default:
throw new Error(`Invalid storage type (${type}), check your .env file`);
}
};
@Module({
imports: [
S3StorageModule.forRootAsync({
useFactory: S3StorageModuleFactory,
inject: [EnvironmentService],
}),
LocalStorageModule.forRootAsync({
useFactory: localStorageModuleFactory,
inject: [EnvironmentService],
}),
EnvironmentModule.forRoot({}),
FileStorageModule.forRootAsync({
useFactory: fileStorageModuleFactory,
inject: [EnvironmentService],
}),
],
exports: [],
providers: [],

View File

@ -1 +0,0 @@
export * from './local-storage.interface';

View File

@ -1,3 +0,0 @@
export interface LocalStorageModuleOptions {
storagePath: string;
}

View File

@ -1,9 +0,0 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { LocalStorageModuleOptions } from './interfaces';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<LocalStorageModuleOptions>({
moduleName: 'LocalStorage',
})
.setClassMethodName('forRoot')
.build();

View File

@ -1,10 +0,0 @@
import { Global, Module } from '@nestjs/common';
import { LocalStorageService } from './local-storage.service';
import { ConfigurableModuleClass } from './local-storage.module-definition';
@Global()
@Module({
providers: [LocalStorageService],
exports: [LocalStorageService],
})
export class LocalStorageModule extends ConfigurableModuleClass {}

View File

@ -1,25 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LocalStorageService } from './local-storage.service';
import { MODULE_OPTIONS_TOKEN } from './local-storage.module-definition';
describe('LocalStorageService', () => {
let service: LocalStorageService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
LocalStorageService,
{
provide: MODULE_OPTIONS_TOKEN,
useValue: {},
},
],
}).compile();
service = module.get<LocalStorageService>(LocalStorageService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,35 +0,0 @@
import { Injectable, Inject } from '@nestjs/common';
import * as fs from 'fs/promises';
import { existsSync } from 'fs';
import * as path from 'path';
import { MODULE_OPTIONS_TOKEN } from './local-storage.module-definition';
import { LocalStorageModuleOptions } from './interfaces';
@Injectable()
export class LocalStorageService {
constructor(
@Inject(MODULE_OPTIONS_TOKEN)
private readonly options: LocalStorageModuleOptions,
) {}
async createFolder(path: string) {
if (existsSync(path)) {
return;
}
return fs.mkdir(path, { recursive: true });
}
async uploadFile(params: {
file: Buffer | Uint8Array | string;
name: string;
folder: string;
}) {
const filePath = `${this.options.storagePath}/${params.folder}/${params.name}`;
const folderPath = path.dirname(filePath);
await this.createFolder(folderPath);
return fs.writeFile(filePath, params.file);
}
}

View File

@ -1 +0,0 @@
export * from './s3-storage-module.interface';

View File

@ -1,6 +0,0 @@
import { S3ClientConfig } from '@aws-sdk/client-s3';
export interface S3StorageModuleOptions extends S3ClientConfig {
bucketName: string;
region: string;
}

View File

@ -1,9 +0,0 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { S3StorageModuleOptions } from './interfaces';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<S3StorageModuleOptions>({
moduleName: 'S3Storage',
})
.setClassMethodName('forRoot')
.build();

View File

@ -1,10 +0,0 @@
import { Global, Module } from '@nestjs/common';
import { S3StorageService } from './s3-storage.service';
import { ConfigurableModuleClass } from './s3-storage.module-definition';
@Global()
@Module({
providers: [S3StorageService],
exports: [S3StorageService],
})
export class S3StorageModule extends ConfigurableModuleClass {}