Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/s3/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const S3_CLIENT = 'S3_CLIENT';
6 changes: 1 addition & 5 deletions libs/s3/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
export type {
S3ModuleOptions,
S3ModuleAsyncOptions,
S3ModuleOptionsFactory,
} from './module.interface';
export type { S3ModuleOptions } from './module.interface';
36 changes: 9 additions & 27 deletions libs/s3/src/interfaces/module.interface.ts
Original file line number Diff line number Diff line change
@@ -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> | S3ModuleOptions;
}

export interface S3ModuleAsyncOptions<T extends unknown[] = any[]> extends Pick<
ModuleMetadata,
'imports'
> {
useExisting?: Type<S3ModuleOptionsFactory>;
useClass?: Type<S3ModuleOptionsFactory>;
useFactory?: (
...args: T
) => Promise<Omit<S3ModuleOptions, 'global'>> | Omit<S3ModuleOptions, 'global'>;
inject?: FactoryProvider['inject'];
global?: boolean;
extraProviders?: Provider[];
connection: S3Connection;
bucket: string;
config?: S3Config;
}
1 change: 0 additions & 1 deletion libs/s3/src/s3.constants.ts

This file was deleted.

16 changes: 16 additions & 0 deletions libs/s3/src/s3.module-definition.ts
Original file line number Diff line number Diff line change
@@ -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<S3ModuleOptions>()
.setClassMethodName('register')
.setExtras(
{
global: false,
},
(definition, extras) => ({
...definition,
global: extras.global,
}),
)
.build();
59 changes: 29 additions & 30 deletions libs/s3/src/s3.module.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
57 changes: 31 additions & 26 deletions libs/s3/src/s3.service.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
try {
await this.s3Client.send(
new HeadBucketCommand({
Bucket: this.bucket,
}),
);
return true;
} catch (error) {
return false;
}
}

async deleteFile(fileUrl: string): Promise<void> {
async delete(fileUrl: string): Promise<void> {
try {
const url = new URL(fileUrl);
const pathParts = url.pathname.split('/');
Expand All @@ -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<string> {
const { mimetype, original, path, cacheControl } = options;
const { mimetype, original, path, cacheControl } = fileOptions;

const folder = typeof path === 'object' ? path.folder : '';
const key =
Expand Down
11 changes: 7 additions & 4 deletions src/shared/media/media.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ 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: {
accessKeyId: cfg.getOrThrow('S3_ACCESS_KEY'),
secretAccessKey: cfg.getOrThrow('S3_SECRET_KEY'),
},
},
// FOR MINIO COMPARTABLE
config: { forcePathStyle: true },
config: {
connectTimeout: 2000,
requestTimeout: 5000,
maxAttempts: 3,
}
}),
}),
ImagorModule.forRootAsync({
Expand Down Expand Up @@ -55,4 +58,4 @@ import { MediaProcessor } from './workers/media.worker';
controllers: [MediaController],
providers: [MediaProcessor, MediaService],
})
export class MediaModule {}
export class MediaModule { }
2 changes: 1 addition & 1 deletion src/shared/media/media.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
2 changes: 1 addition & 1 deletion src/shared/media/workers/media.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading