chore: add service image and audio

This commit is contained in:
mahyargdz
2025-08-30 09:40:44 +03:30
parent 9869e603d3
commit 9c409bef27
28 changed files with 2111 additions and 1631 deletions
+3 -4
View File
@@ -1,10 +1,9 @@
import { File } from "@nest-lab/fastify-multer";
import { ApiProperty } from "@nestjs/swagger";
import { IFile } from "../../utils/interfaces/IFile";
export class UploadSingleFileDto {
@ApiProperty({ type: "string", format: "binary", nullable: false })
file: IFile;
file: File;
}
export class UploadMultipleFileDto {
@@ -16,5 +15,5 @@ export class UploadMultipleFileDto {
format: "binary",
},
})
files: IFile[];
files: File[];
}
@@ -0,0 +1,41 @@
import { File } from "@nest-lab/fastify-multer";
export interface FileUploadResult {
file: File;
url: string;
}
export interface FileStreamResult {
stream: NodeJS.ReadableStream;
file: File;
}
export interface FileMetadata {
width?: number;
height?: number;
duration?: number;
bitrate?: number;
codec?: string;
language?: string;
}
export type FileType = "video" | "audio" | "image" | "document" | "text" | "unknown";
export interface FileValidationOptions {
maxFileSize: number;
allowedMimeTypes: string[];
}
export interface FileUploadOptions {
roomId?: string;
metadata?: Record<string, string>;
}
// File processing status
export type FileProcessingStatus = "pending" | "processing" | "completed" | "failed";
export interface FileProcessingResult {
status: FileProcessingStatus;
metadata?: FileMetadata;
error?: string;
}
@@ -0,0 +1,37 @@
export interface S3UploadResult {
key: string;
url: string;
bucket: string;
}
export interface S3UploadOptions {
contentType: string;
metadata?: Record<string, string>;
acl?: "private" | "public-read" | "public-read-write";
}
export interface S3DownloadOptions {
expiresIn?: number;
responseContentType?: string;
responseContentDisposition?: string;
}
export interface S3FileInfo {
key: string;
bucket: string;
size: number;
lastModified: Date;
contentType: string;
etag: string;
}
export interface S3DeleteResult {
deleted: boolean;
key: string;
}
export interface S3ListResult {
files: S3FileInfo[];
continuationToken?: string;
isTruncated: boolean;
}
@@ -0,0 +1,10 @@
export interface FileSearchOptions {
userId?: string;
roomId?: string;
fileType?: "video" | "audio" | "subtitle";
isActive?: boolean;
limit?: number;
offset?: number;
sortBy?: "uploadedAt" | "size" | "originalName";
sortOrder?: "ASC" | "DESC";
}
+132
View File
@@ -0,0 +1,132 @@
import { Readable } from "stream";
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { Inject, Injectable, Logger } from "@nestjs/common";
import { IS3Configs } from "../../../configs/s3.config";
import { S3_CONFIG } from "../../utils/constants";
import { FileType } from "../interfaces/file.interface";
import { S3UploadResult } from "../interfaces/s3.interface";
@Injectable()
export class S3Service {
private readonly logger = new Logger(S3Service.name);
private readonly s3Client: S3Client;
constructor(@Inject(S3_CONFIG) private readonly s3Configs: IS3Configs) {
this.s3Client = new S3Client({
region: this.s3Configs.region,
endpoint: this.s3Configs.endpoint,
credentials: {
accessKeyId: this.s3Configs.accessKeyId,
secretAccessKey: this.s3Configs.secretAccessKey,
},
});
}
/**
* Upload file to S3
*/
async uploadFile(buffer: Buffer, key: string, contentType: string, metadata?: Record<string, string>): Promise<S3UploadResult> {
try {
const command = new PutObjectCommand({
Bucket: this.s3Configs.bucketName,
Key: key,
Body: buffer,
ACL: "public-read",
ContentType: contentType,
Metadata: metadata,
});
await this.s3Client.send(command);
const url = `${this.s3Configs.bucketUploadedUrl}/${key}`;
this.logger.log(`File uploaded to S3: ${key}`);
return {
key,
url,
bucket: this.s3Configs.bucketName,
};
} catch (error: unknown) {
this.logger.error(`Failed to upload file to S3: ${(error as Error).message}`, (error as Error).stack);
throw error;
}
}
/**
* Get file stream from S3
*/
async getFileStream(key: string): Promise<Readable> {
try {
const command = new GetObjectCommand({
Bucket: this.s3Configs.bucketName,
Key: key,
});
const response = await this.s3Client.send(command);
return response.Body as Readable;
} catch (error: unknown) {
this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack);
throw error;
}
}
/**
* Generate presigned URL for file download
*/
async getPresignedUrl(key: string, expiresIn: number = 3600): Promise<string> {
try {
const command = new GetObjectCommand({
Bucket: this.s3Configs.bucketName,
Key: key,
});
return await getSignedUrl(this.s3Client, command, { expiresIn });
} catch (error: unknown) {
this.logger.error(`Failed to generate presigned URL: ${(error as Error).message}`, (error as Error).stack);
throw error;
}
}
/**
* Delete file from S3
*/
async deleteFile(key: string): Promise<void> {
try {
const command = new DeleteObjectCommand({
Bucket: this.s3Configs.bucketName,
Key: key,
});
await this.s3Client.send(command);
this.logger.log(`File deleted from S3: ${key}`);
} catch (error: unknown) {
this.logger.error(`Failed to delete file from S3: ${(error as Error).message}`, (error as Error).stack);
throw error;
}
}
/**
* Generate S3 key for file
*/
generateFileKey(originalName: string, fileType: FileType): string {
const timestamp = Date.now();
const randomSuffix = Math.round(Math.random() * 1e9);
const extension = originalName.split(".").pop();
const basePathMap: Record<FileType, string> = {
image: "images",
video: "videos",
audio: "audio",
document: "documents",
text: "texts",
unknown: "unknowns",
};
const basePath = basePathMap[fileType];
return `${basePath}/${timestamp}-${randomSuffix}.${extension}`;
}
}
@@ -0,0 +1,111 @@
import { File } from "@nest-lab/fastify-multer";
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { S3Service } from "./s3.service";
import { UploaderMessage } from "../../../common/enums/message.enum";
import { FileType } from "../interfaces/file.interface";
@Injectable()
export class UploaderService {
private readonly logger = new Logger(UploaderService.name);
private readonly maxFileSize: number;
private readonly allowedMimeTypes: string[];
constructor(
private readonly configService: ConfigService,
private readonly s3Service: S3Service,
) {
this.maxFileSize = this.configService.get("MAX_FILE_SIZE", 1024 * 1024 * 100); // 100MB
this.allowedMimeTypes = [
"video/mp4",
"video/webm",
"video/ogg",
"video/avi",
"video/mov",
"video/mkv",
"audio/mp3",
"audio/wav",
"audio/ogg",
"audio/webm",
"audio/m4a",
"audio/aac",
"audio/flac",
"audio/m4a",
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/svg+xml",
"image/tiff",
"image/bmp",
"application/pdf",
"text/plain",
"text/csv",
];
}
async uploadMultiple(files: File[]) {
return await Promise.all(files.map((file) => this.uploadFile(file)));
}
/**
* Handle file upload
*/
async uploadFile(file: File) {
if (!file || !file.buffer || !file.size) {
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
}
this.logger.log(`Uploading file: ${file.originalname}`);
this.validateFile(file);
const fileType = await this.determineFileType(file.mimetype);
const s3Key = this.s3Service.generateFileKey(file.originalname, fileType);
// Upload to S3
const s3Result = await this.s3Service.uploadFile(file.buffer, s3Key, file.mimetype, {
originalName: file.originalname,
uploadedBy: "admin",
});
this.logger.log(`File uploaded successfully: `);
return {
file: s3Result,
url: s3Result.url,
};
}
/**
* Validate uploaded file
*/
private validateFile(file: File): void {
if (!file) throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
if (file.size && file.size > this.maxFileSize)
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_TOO_LARGE.replace("[MAX_FILE_SIZE]", this.maxFileSize.toString()));
if (!this.allowedMimeTypes.includes(file.mimetype))
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_TYPE_NOT_SUPPORTED.replace("[MIME_TYPES]", file.mimetype));
}
/**
* Determine file type based on mimetype
*/
private async determineFileType(mimetype: string): Promise<FileType> {
if (mimetype.startsWith("video/")) {
return "video";
} else if (mimetype.startsWith("audio/")) {
return "audio";
} else if (mimetype.startsWith("image/")) {
return "image";
} else if (mimetype.startsWith("text/")) {
return "text";
} else if (mimetype.startsWith("application/")) {
return "document";
}
return "unknown";
}
}
+7 -9
View File
@@ -1,25 +1,23 @@
import { FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer";
import { File, FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer";
import { Controller, Post, UploadedFile, UploadedFiles, UseInterceptors } from "@nestjs/common";
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from "@nestjs/swagger";
import { ApiBody, ApiConsumes, ApiOperation } from "@nestjs/swagger";
import { UploadMultipleFileDto, UploadSingleFileDto } from "./DTO/upload-file.dto";
import { UploaderService } from "./uploader.service";
import { UploaderService } from "./services/uploader.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { IFile } from "../utils/interfaces/IFile";
@ApiTags("Uploader")
@Controller("uploader")
@AuthGuards()
export class UploaderController {
constructor(private readonly uploaderService: UploaderService) {}
@ApiOperation({ summary: "Uploads a single file" })
@ApiOperation({ summary: "Upload a file (admin)" })
@ApiConsumes("multipart/form-data")
@UseInterceptors(FileInterceptor("file"))
@ApiBody({ type: UploadSingleFileDto })
@Post("single-file")
uploadFile(@UploadedFile() file: IFile) {
return this.uploaderService.upload(file);
uploadFile(@UploadedFile() file: File) {
return this.uploaderService.uploadFile(file);
}
@ApiOperation({ summary: "Uploads multiple files" })
@@ -27,7 +25,7 @@ export class UploaderController {
@UseInterceptors(FilesInterceptor("files"))
@ApiBody({ type: UploadMultipleFileDto })
@Post("multi-file")
uploadFiles(@UploadedFiles() files: IFile[]) {
uploadFiles(@UploadedFiles() files: File[]) {
return this.uploaderService.uploadMultiple(files);
}
}
+4 -5
View File
@@ -1,14 +1,13 @@
import { Module } from "@nestjs/common";
import { S3Service } from "./services/s3.service";
import { UploaderService } from "./services/uploader.service";
import { UploaderController } from "./uploader.controller";
import { UploaderService } from "./uploader.service";
import { S3Service } from "../utils/providers/s3.service";
import { UtilsModule } from "../utils/utils.module";
import { S3ConfigsProvider } from "../../configs/s3.config";
@Module({
imports: [UtilsModule],
providers: [UploaderService, S3Service],
controllers: [UploaderController],
providers: [UploaderService, S3Service, S3ConfigsProvider],
exports: [UploaderService],
})
export class UploaderModule {}
-20
View File
@@ -1,20 +0,0 @@
import { Injectable } from "@nestjs/common";
import { IFile } from "../utils/interfaces/IFile";
import { S3Service } from "../utils/providers/s3.service";
@Injectable()
export class UploaderService {
constructor(private s3Service: S3Service) {}
async upload(file: IFile) {
return await this.s3Service.upload(file);
}
async uploadMultiple(files: IFile[]) {
// const uploadPromises = files.map((file) => this.s3Service.upload(file));
// return await Promise.all(uploadPromises);
return this.s3Service.uploadMultiple(files);
}
}