diff --git a/libs/s3/src/constants.ts b/libs/s3/src/constants.ts new file mode 100644 index 0000000..58fc36c --- /dev/null +++ b/libs/s3/src/constants.ts @@ -0,0 +1 @@ +export const S3_CLIENT = 'S3_CLIENT'; diff --git a/libs/s3/src/interfaces/index.ts b/libs/s3/src/interfaces/index.ts index 073bc43..dc08195 100644 --- a/libs/s3/src/interfaces/index.ts +++ b/libs/s3/src/interfaces/index.ts @@ -1,5 +1 @@ -export type { - S3ModuleOptions, - S3ModuleAsyncOptions, - S3ModuleOptionsFactory, -} from './module.interface'; +export type { S3ModuleOptions } from './module.interface'; diff --git a/libs/s3/src/interfaces/module.interface.ts b/libs/s3/src/interfaces/module.interface.ts index 1edd054..922d127 100644 --- a/libs/s3/src/interfaces/module.interface.ts +++ b/libs/s3/src/interfaces/module.interface.ts @@ -1,38 +1,20 @@ import type { S3ClientConfig } from '@aws-sdk/client-s3'; -import type { FactoryProvider, ModuleMetadata, Provider, Type } from '@nestjs/common'; -export interface S3ConnectionOptions extends Pick< +export interface S3Connection extends Pick< S3ClientConfig, 'credentials' | 'endpoint' | 'region' > { - bucket: string; + endpoint: string; + region: string; } -export interface S3OtherOptions extends Omit< +export interface S3Config extends Omit< S3ClientConfig, - 'credentials' | 'endpoint' | 'region' -> {} + keyof S3Connection +> { } export interface S3ModuleOptions { - connection: S3ConnectionOptions; - config?: S3OtherOptions; - global?: boolean; -} - -export interface S3ModuleOptionsFactory { - createS3Options(): Promise | S3ModuleOptions; -} - -export interface S3ModuleAsyncOptions extends Pick< - ModuleMetadata, - 'imports' -> { - useExisting?: Type; - useClass?: Type; - useFactory?: ( - ...args: T - ) => Promise> | Omit; - inject?: FactoryProvider['inject']; - global?: boolean; - extraProviders?: Provider[]; + connection: S3Connection; + bucket: string; + config?: S3Config; } diff --git a/libs/s3/src/s3.constants.ts b/libs/s3/src/s3.constants.ts deleted file mode 100644 index c55a9ab..0000000 --- a/libs/s3/src/s3.constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const S3_OPTIONS = 'S3_OPTIONS'; diff --git a/libs/s3/src/s3.module-definition.ts b/libs/s3/src/s3.module-definition.ts new file mode 100644 index 0000000..3648deb --- /dev/null +++ b/libs/s3/src/s3.module-definition.ts @@ -0,0 +1,16 @@ +import { ConfigurableModuleBuilder } from '@nestjs/common'; +import { S3ModuleOptions } from './interfaces'; + +export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = + new ConfigurableModuleBuilder() + .setClassMethodName('register') + .setExtras( + { + global: false, + }, + (definition, extras) => ({ + ...definition, + global: extras.global, + }), + ) + .build(); diff --git a/libs/s3/src/s3.module.ts b/libs/s3/src/s3.module.ts index 2c4b1f2..656b979 100644 --- a/libs/s3/src/s3.module.ts +++ b/libs/s3/src/s3.module.ts @@ -1,37 +1,36 @@ -import { type DynamicModule, Module, type Provider } from '@nestjs/common'; -import type { S3ModuleOptions, S3ModuleAsyncOptions } from './interfaces'; +import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; +import type { S3ModuleOptions } from './interfaces'; import { S3Service } from './s3.service'; -import { S3_OPTIONS } from './s3.constants'; +import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } from './s3.module-definition'; +import { S3_CLIENT } from './constants'; +import { S3Client } from '@aws-sdk/client-s3'; -@Module({}) -export class S3Module { - static register(options: S3ModuleOptions): DynamicModule { - const { global, ...config } = options; +@Module({ + providers: [ + { + provide: S3_CLIENT, + inject: [MODULE_OPTIONS_TOKEN], + useFactory: (options: S3ModuleOptions) => { + const { connection, config } = options; - return { - global, - module: S3Module, - providers: [{ provide: S3_OPTIONS, useValue: config }, S3Service], - exports: [S3Service], - }; + return new S3Client({ + ...connection, + ...config + }); + }, + }, + S3Service, + ], + exports: [S3Service], +}) +export class S3Module extends ConfigurableModuleClass implements OnApplicationShutdown { + constructor( + @Inject(S3_CLIENT) private readonly client: S3Client + ) { + super(); } - static registerAsync(options: S3ModuleAsyncOptions): DynamicModule { - const { imports } = options; - - return { - module: S3Module, - imports: imports || [], - providers: [this.createAsyncOptionsProvider(options), S3Service], - exports: [S3Service], - }; - } - - private static createAsyncOptionsProvider(options: S3ModuleAsyncOptions): Provider { - return { - provide: S3_OPTIONS, - useFactory: options.useFactory, - inject: options.inject || [], - }; + async onApplicationShutdown() { + this.client.destroy(); } } diff --git a/libs/s3/src/s3.service.ts b/libs/s3/src/s3.service.ts index 0201a6b..e42ba07 100644 --- a/libs/s3/src/s3.service.ts +++ b/libs/s3/src/s3.service.ts @@ -1,34 +1,39 @@ import { Inject, Injectable } from '@nestjs/common'; -import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'; -import { S3_OPTIONS } from './s3.constants'; +import { DeleteObjectCommand, HeadBucketCommand, S3Client } from '@aws-sdk/client-s3'; +import { S3_CLIENT } from './constants'; import { S3ModuleOptions } from './interfaces'; import { PutObjectCommand } from '@aws-sdk/client-s3'; import { randomUUID } from 'crypto'; import { extname } from 'path'; +import { MODULE_OPTIONS_TOKEN } from './s3.module-definition'; @Injectable() export class S3Service { - private readonly s3Client: S3Client; - public readonly bucket: string; - private readonly endpoint: string; - constructor( - @Inject(S3_OPTIONS) + @Inject(S3_CLIENT) + private s3Client: S3Client, + @Inject(MODULE_OPTIONS_TOKEN) private options: S3ModuleOptions, - ) { - const { bucket, credentials, endpoint, region } = options.connection; - this.bucket = bucket; - this.endpoint = endpoint as string; + ) { } - this.s3Client = new S3Client({ - region, - endpoint, - credentials, - ...options.config, - }); + private get bucket(): string { + return this.options.bucket; + } + + async isAlive(): Promise { + try { + await this.s3Client.send( + new HeadBucketCommand({ + Bucket: this.bucket, + }), + ); + return true; + } catch (error) { + return false; + } } - async deleteFile(fileUrl: string): Promise { + async delete(fileUrl: string): Promise { try { const url = new URL(fileUrl); const pathParts = url.pathname.split('/'); @@ -45,21 +50,21 @@ export class S3Service { } } - async uploadFile( + async upload( file: Buffer, - options: { + fileOptions: { original: string; mimetype: string; cacheControl?: string; path?: - | { - folder: string; - key?: string; - } - | string; + | { + folder: string; + key?: string; + } + | string; }, ): Promise { - const { mimetype, original, path, cacheControl } = options; + const { mimetype, original, path, cacheControl } = fileOptions; const folder = typeof path === 'object' ? path.folder : ''; const key = diff --git a/src/shared/media/media.module.ts b/src/shared/media/media.module.ts index 084e0c6..d1d9747 100644 --- a/src/shared/media/media.module.ts +++ b/src/shared/media/media.module.ts @@ -15,8 +15,8 @@ import { MediaProcessor } from './workers/media.worker'; S3Module.registerAsync({ inject: [ConfigService], useFactory: (cfg: ConfigService) => ({ + bucket: cfg.getOrThrow('S3_BUCKET_NAME'), connection: { - bucket: cfg.getOrThrow('S3_BUCKET_NAME'), endpoint: cfg.getOrThrow('S3_ENDPOINT'), region: cfg.getOrThrow('S3_REGION'), credentials: { @@ -24,8 +24,11 @@ import { MediaProcessor } from './workers/media.worker'; secretAccessKey: cfg.getOrThrow('S3_SECRET_KEY'), }, }, - // FOR MINIO COMPARTABLE - config: { forcePathStyle: true }, + config: { + connectTimeout: 2000, + requestTimeout: 5000, + maxAttempts: 3, + } }), }), ImagorModule.forRootAsync({ @@ -55,4 +58,4 @@ import { MediaProcessor } from './workers/media.worker'; controllers: [MediaController], providers: [MediaProcessor, MediaService], }) -export class MediaModule {} +export class MediaModule { } diff --git a/src/shared/media/media.service.ts b/src/shared/media/media.service.ts index 9f045e0..c2f011b 100644 --- a/src/shared/media/media.service.ts +++ b/src/shared/media/media.service.ts @@ -24,7 +24,7 @@ export class MediaService { const { folder, fileName } = this.generateStoragePath(context, userId, file.filename); try { - const originalUrl = await this.s3.uploadFile(file.buffer, { + const originalUrl = await this.s3.upload(file.buffer, { mimetype: file.mimetype, original: file.filename, path: { folder, key: fileName }, diff --git a/src/shared/media/workers/media.worker.ts b/src/shared/media/workers/media.worker.ts index 00ccf2e..3f5f8b0 100644 --- a/src/shared/media/workers/media.worker.ts +++ b/src/shared/media/workers/media.worker.ts @@ -34,7 +34,7 @@ export class MediaProcessor extends WorkerHost { const processedImage = await this.imagor.get(`/${originalFilePath}`, dimensions); - const uploadedPath = await this.s3.uploadFile(processedImage, { + const uploadedPath = await this.s3.upload(processedImage, { original: targetFileName, mimetype: 'image/webp', path: {