feat: added upload file module
This commit is contained in:
@@ -20,7 +20,12 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1099.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1099.0",
|
||||
"@fastify/multipart": "^10.1.0",
|
||||
"@fastify/static": "^10.1.2",
|
||||
"@keyv/redis": "^5.1.6",
|
||||
"@nest-lab/fastify-multer": "^1.3.0",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
"@nestjs/cache-manager": "^3.1.3",
|
||||
@@ -30,6 +35,7 @@
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.1.28",
|
||||
"@nestjs/platform-fastify": "^11.1.28",
|
||||
"@nestjs/swagger": "^11.4.6",
|
||||
"@nestjs/typeorm": "^11.0.3",
|
||||
"axios": "^1.18.1",
|
||||
@@ -38,6 +44,8 @@
|
||||
"cache-manager": "^7.2.9",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"fastify": "^5.11.0",
|
||||
"fastify-multer": "^2.0.3",
|
||||
"ioredis": "^5.11.1",
|
||||
"joi": "^18.2.3",
|
||||
"keyv": "^5.6.0",
|
||||
@@ -59,6 +67,7 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/ms": "^2.1.0",
|
||||
"@types/multer": "^2.2.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
|
||||
Generated
+845
-2
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,45 @@
|
||||
export enum UserMessages {
|
||||
USER_ALREADY_EXISTS = 'کاربر مورد نظر با این ایمیل یا شماره همراه در حال حاضر وجود دارد. لطفا وارد حساب شوید!',
|
||||
USER_NOT_FOUND = 'کاربر مورد نظر پیدا نشد!',
|
||||
USER_PASSWORD_INVALID = 'رمز وارد شده اشتباده است!',
|
||||
USER_VALID_CREDENTIALS = 'اطلاعات وارد شده صحیح میباشد!'
|
||||
USER_ALREADY_EXISTS = 'کاربر مورد نظر با این ایمیل یا شماره همراه در حال حاضر وجود دارد. لطفا وارد حساب شوید!',
|
||||
USER_NOT_FOUND = 'کاربر مورد نظر پیدا نشد!',
|
||||
USER_PASSWORD_INVALID = 'رمز وارد شده اشتباده است!',
|
||||
USER_VALID_CREDENTIALS = 'اطلاعات وارد شده صحیح میباشد!',
|
||||
}
|
||||
|
||||
export enum OTPMessages {
|
||||
OTP_NOT_FOUND = 'این کد پیدا نشد! لطفا دوباره درخواست بدهید!',
|
||||
OTP_EXPIRED = 'این کد منسوخ شده است لطفا دوباره درخواست بدهید!',
|
||||
OTP_TOO_MANY_TRIES = 'تعداد تلاش ها بیش از حد مجاز است لطفا دوباره درخواست کد بدهید!',
|
||||
OTP_INVALID = 'کد وارد شده نا معتبر است!'
|
||||
OTP_NOT_FOUND = 'این کد پیدا نشد! لطفا دوباره درخواست بدهید!',
|
||||
OTP_EXPIRED = 'این کد منسوخ شده است لطفا دوباره درخواست بدهید!',
|
||||
OTP_TOO_MANY_TRIES = 'تعداد تلاش ها بیش از حد مجاز است لطفا دوباره درخواست کد بدهید!',
|
||||
OTP_INVALID = 'کد وارد شده نا معتبر است!',
|
||||
}
|
||||
|
||||
export enum UserRoleMessages {
|
||||
USER_ROLE_ALREADY_EXISTS = 'نقش مورد نظر برای کاربر مد نظر وجود دارد!'
|
||||
USER_ROLE_ALREADY_EXISTS = 'نقش مورد نظر برای کاربر مد نظر وجود دارد!',
|
||||
}
|
||||
|
||||
export enum AuthMessages {
|
||||
TOKEN_EXPIRED_OR_INVALID = 'توکن منسوخ شده یا نامعتبر است!',
|
||||
TOKEN_EXPIRED_OR_INVALID = 'توکن منسوخ شده یا نامعتبر است!',
|
||||
}
|
||||
|
||||
export enum OrganMessages {
|
||||
ORGAN_NOT_FOUND = 'ارگان مورد نظر پیدا نشد!'
|
||||
ORGAN_NOT_FOUND = 'ارگان مورد نظر پیدا نشد!',
|
||||
}
|
||||
|
||||
export enum ComplaintMessages {
|
||||
COMPLAINT_NOT_FOUND = 'شکایت مورد نظر پیدا نشد!'
|
||||
COMPLAINT_NOT_FOUND = 'شکایت مورد نظر پیدا نشد!',
|
||||
}
|
||||
|
||||
export enum AttachmentMessages {
|
||||
ATTACHMENT_NOT_FOUND = 'ضمیمه مورد نظر پیدا نشد!'
|
||||
ATTACHMENT_NOT_FOUND = 'ضمیمه مورد نظر پیدا نشد!',
|
||||
}
|
||||
|
||||
export enum ReplyMessages {
|
||||
REPLY_NOT_FOUND = 'پاسخ مورد نظر پیدا نشد!'
|
||||
}
|
||||
REPLY_NOT_FOUND = 'پاسخ مورد نظر پیدا نشد!',
|
||||
}
|
||||
|
||||
export const enum UploaderMessage {
|
||||
FILE_TYPE_NOT_ALLOWED = 'نوع فایل مجاز نیست',
|
||||
FILE_SIZE_TOO_SMALL = 'فایل کوچکتر از حد مجاز است',
|
||||
UPLOAD_FILE_INVALID = 'فایل معتبر نیست',
|
||||
UPLOAD_FILE_TOO_LARGE = '[MAX_FILE_SIZE] فایل بزرگتر از حد مجاز است',
|
||||
UPLOAD_FILE_TYPE_NOT_SUPPORTED = '[MIME_TYPES] نوع فایل مجاز نیست',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { FactoryProvider } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { S3_CONFIG } from 'src/modules/uploader/constants';
|
||||
|
||||
export const S3ConfigsProvider: FactoryProvider<IS3Configs> = {
|
||||
provide: S3_CONFIG,
|
||||
|
||||
inject: [ConfigService],
|
||||
useFactory(configService: ConfigService) {
|
||||
return {
|
||||
accessKeyId: configService.getOrThrow<string>('BUCKET_ACCESS_KEY'),
|
||||
secretAccessKey: configService.getOrThrow<string>('BUCKET_SECRET_KEY'),
|
||||
endpoint: configService.getOrThrow<string>('BUCKET_URL'),
|
||||
region: configService.getOrThrow<string>('BUCKET_REGION'),
|
||||
bucketName: configService.getOrThrow<string>('BUCKET_NAME'),
|
||||
bucketUploadedUrl: configService.getOrThrow<string>('BUCKET_UPLOAD_URL'),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export interface IS3Configs {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
endpoint: string;
|
||||
region: string;
|
||||
bucketName: string;
|
||||
bucketUploadedUrl: string;
|
||||
}
|
||||
+11
-1
@@ -1,5 +1,9 @@
|
||||
import { NestFactory, Reflector } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import {
|
||||
ClassSerializerInterceptor,
|
||||
Logger,
|
||||
@@ -9,11 +13,17 @@ import { setupSwagger } from './config/swagger.config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AllExceptionFilter } from './common/filters/all-exception.filter';
|
||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||
import multipart from '@fastify/multipart';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('APP');
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter(),
|
||||
);
|
||||
|
||||
await app.register(multipart);
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
import type { File } from "@nest-lab/fastify-multer";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class UploadSingleFileDto {
|
||||
@ApiProperty({ type: "string", format: "binary", nullable: false })
|
||||
file: File;
|
||||
}
|
||||
|
||||
export class UploadMultipleFileDto {
|
||||
@ApiProperty({
|
||||
required: true,
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
})
|
||||
files: File[];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const S3_CONFIG = "S3_CONFIG";
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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 type { IS3Configs } from "src/config/s3.config";
|
||||
import { S3_CONFIG } from "../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,131 @@
|
||||
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 "src/common/enums/messages.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/mpeg",
|
||||
"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);
|
||||
|
||||
// Sanitize filename for HTTP headers (S3 metadata)
|
||||
const sanitizedOriginalName = this.sanitizeFilenameForHeader(file.originalname);
|
||||
|
||||
// Upload to S3
|
||||
const s3Result = await this.s3Service.uploadFile(file.buffer, s3Key, file.mimetype, {
|
||||
originalName: sanitizedOriginalName,
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize filename for use in HTTP headers
|
||||
* Removes invalid characters that are not allowed in HTTP header values
|
||||
* HTTP headers only allow ASCII printable characters (0x20-0x7E) excluding certain special chars
|
||||
*/
|
||||
private sanitizeFilenameForHeader(filename: string): string {
|
||||
if (!filename) return "";
|
||||
|
||||
// Remove control characters (0x00-0x1F, 0x7F) and newlines/tabs
|
||||
// Replace non-ASCII characters with underscore to keep the filename readable
|
||||
// This ensures the header value is valid while preserving as much of the original name as possible
|
||||
return filename
|
||||
.replace(/[\x00-\x1F\x7F\r\n\t]/g, "") // Remove control characters, newlines, tabs
|
||||
.replace(/[^\x20-\x7E]/g, "_") // Replace non-ASCII characters with underscore
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer";
|
||||
import type { File } from "@nest-lab/fastify-multer";
|
||||
import { Controller, Post, UploadedFile, UploadedFiles, UseInterceptors } from "@nestjs/common";
|
||||
import { ApiBody, ApiConsumes, ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { UploadMultipleFileDto, UploadSingleFileDto } from "./DTO/upload-file.dto";
|
||||
import { UploaderService } from "./services/uploader.service";
|
||||
import { AuthGuard } from "src/common/decorators/auth-guard.decorator";
|
||||
|
||||
@Controller("uploader")
|
||||
@AuthGuard()
|
||||
export class UploaderController {
|
||||
constructor(private readonly uploaderService: UploaderService) {}
|
||||
|
||||
@ApiOperation({ summary: "Upload a file (admin)" })
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@UseInterceptors(FileInterceptor("file"))
|
||||
@ApiBody({ type: UploadSingleFileDto })
|
||||
@Post("single-file")
|
||||
uploadFile(@UploadedFile() file: File) {
|
||||
return this.uploaderService.uploadFile(file);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Uploads multiple files" })
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@UseInterceptors(FilesInterceptor("files"))
|
||||
@ApiBody({ type: UploadMultipleFileDto })
|
||||
@Post("multi-file")
|
||||
uploadFiles(@UploadedFiles() files: File[]) {
|
||||
return this.uploaderService.uploadMultiple(files);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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 { S3ConfigsProvider } from "src/config/s3.config";
|
||||
|
||||
@Module({
|
||||
controllers: [UploaderController],
|
||||
providers: [UploaderService, S3Service, S3ConfigsProvider],
|
||||
exports: [UploaderService],
|
||||
})
|
||||
export class UploaderModule {}
|
||||
Reference in New Issue
Block a user