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:
@ -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>;
|
||||
}
|
||||
57
server/src/integrations/file-storage/drivers/local.driver.ts
Normal file
57
server/src/integrations/file-storage/drivers/local.driver.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
99
server/src/integrations/file-storage/drivers/s3.driver.ts
Normal file
99
server/src/integrations/file-storage/drivers/s3.driver.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import {
|
||||
CreateBucketCommandInput,
|
||||
GetObjectCommand,
|
||||
HeadBucketCommandInput,
|
||||
NotFound,
|
||||
PutObjectCommand,
|
||||
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';
|
||||
|
||||
export interface S3DriverOptions extends S3ClientConfig {
|
||||
bucketName: string;
|
||||
region: string;
|
||||
}
|
||||
|
||||
export class S3Driver implements StorageDriver {
|
||||
private s3Client: S3;
|
||||
private bucketName: string;
|
||||
|
||||
constructor(options: S3DriverOptions) {
|
||||
const { bucketName, region, ...s3Options } = options;
|
||||
|
||||
if (!bucketName || !region) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.s3Client = new S3({ ...s3Options, region });
|
||||
this.bucketName = bucketName;
|
||||
}
|
||||
|
||||
public get client(): S3 {
|
||||
return this.s3Client;
|
||||
}
|
||||
|
||||
async write(params: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
name: string;
|
||||
folder: string;
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void> {
|
||||
const command = new PutObjectCommand({
|
||||
Key: `${kebabCase(params.folder)}/${params.name}`,
|
||||
Body: params.file,
|
||||
ContentType: params.mimeType,
|
||||
Bucket: this.bucketName,
|
||||
});
|
||||
|
||||
await this.createBucket({ Bucket: this.bucketName });
|
||||
|
||||
await this.s3Client.send(command);
|
||||
}
|
||||
|
||||
async read(params: {
|
||||
folderPath: string;
|
||||
filename: string;
|
||||
}): Promise<Readable> {
|
||||
const command = new GetObjectCommand({
|
||||
Key: `${params.folderPath}/${params.filename}`,
|
||||
Bucket: this.bucketName,
|
||||
});
|
||||
const file = await 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) {
|
||||
try {
|
||||
await this.s3Client.headBucket(args);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (error instanceof NotFound) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createBucket(args: CreateBucketCommandInput) {
|
||||
const exist = await this.checkBucketExists({
|
||||
Bucket: args.Bucket,
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.s3Client.createBucket(args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export const STORAGE_DRIVER = Symbol('STORAGE_DRIVER');
|
||||
@ -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();
|
||||
48
server/src/integrations/file-storage/file-storage.module.ts
Normal file
48
server/src/integrations/file-storage/file-storage.module.ts
Normal 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],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FileStorageService } from './file-storage.service';
|
||||
import { STORAGE_DRIVER } from './file-storage.constants';
|
||||
|
||||
describe('FileStorageService', () => {
|
||||
let service: FileStorageService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FileStorageService,
|
||||
{
|
||||
provide: STORAGE_DRIVER,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FileStorageService>(FileStorageService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
22
server/src/integrations/file-storage/file-storage.service.ts
Normal file
22
server/src/integrations/file-storage/file-storage.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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'>;
|
||||
1
server/src/integrations/file-storage/interfaces/index.ts
Normal file
1
server/src/integrations/file-storage/interfaces/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './file-storage.interface';
|
||||
Reference in New Issue
Block a user