From cdaaeed1b7a1e9d4e6fd9e9214056aee89bbf5b2 Mon Sep 17 00:00:00 2001 From: michee-04 Date: Fri, 8 Nov 2024 12:41:30 +0000 Subject: [PATCH 1/7] update: Updated docker-compose.yml to integrate Traefik --- docker-compose.yaml | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index c018a99..746774c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,38 @@ services: + traefik: + image: traefik:v3.1 + container_name: ntw-traefik + restart: always + command: + - --api.insecure=true + - --api.dashboard=true + - --ping=true + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.web-secure.address=:443 + - --log.level=DEBUG + - --certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory + - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.myresolver.acme.tlschallenge=true + ports: + - "80:80" + - "443:443" + - "8080:8080" + volumes: + - "/letsencrypt:/letsencrypt" + - "/var/run/docker.sock:/var/run/docker.sock:ro" + - ./certs:/certs + healthcheck: + test: [ "CMD", "traefik", "healthcheck", "--ping" ] + interval: 30s + retries: 10 + labels: + - traefik.enable=true + - traefik.http.routers.dashboard.rule=Host(`traefik.localhost`) + - traefik.http.routers.dashboard.service=api@internal + - traefik.http.routers.dashboard.entrypoints=web + app: build: . container_name: ntw-app @@ -6,6 +40,10 @@ services: - "${PORT}:${PORT}" env_file: - .env + labels: + - traefik.enable=true + - traefik.http.routers.app.rule=Host(`app.localhost`) + - traefik.http.services.app.loadbalancer.server.port=${PORT} depends_on: - mongo - redis @@ -33,7 +71,7 @@ services: container_name: ntw-minio command: server /data --console-address ":${MINIO_CONSOLE_PORT}" ports: - - "${MINIO_EXT_API_PORT}:${MINIO_API_PORT}" + - "${MINIO_EXT_API_PORT}:${MINIO_API_PORT}" - "${MINIO_EXT_CONSOLE_PORT}:${MINIO_CONSOLE_PORT}" environment: MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} @@ -53,6 +91,11 @@ services: timeout: 10s retries: 3 + labels: + - traefik.enable=true + - traefik.http.routers.app.rule=Host(`mail.localhost`) + - traefik.http.services.app.loadbalancer.server.port=${MAILDEV_WEBAPP_PORT} + volumes: mongo-data: minio-data: From ce8b374590a28b7b239c6c4c6d6f78ac6d070f31 Mon Sep 17 00:00:00 2001 From: michee-04 Date: Mon, 11 Nov 2024 17:52:10 +0000 Subject: [PATCH 2/7] feat: Implement create, read and delete fil --- .env.example | 8 +- package.json | 2 + .../core/api/controllers/file.controller.ts | 180 ++++++++++++++++++ src/apps/files/core/api/controllers/index.ts | 1 + src/apps/files/core/api/dtos/index.ts | 1 + .../core/api/dtos/request/file.create.ts | 5 + src/apps/files/core/api/dtos/request/index.ts | 1 + src/apps/files/core/api/index.ts | 4 + src/apps/files/core/api/routes/file.route.ts | 17 ++ src/apps/files/core/api/routes/index.ts | 1 + src/apps/files/core/business/index.ts | 3 + .../core/business/repositories/file.repo.ts | 9 + .../files/core/business/repositories/index.ts | 1 + .../core/business/services/file.service.ts | 144 ++++++++++++++ .../files/core/business/services/index.ts | 1 + src/apps/files/core/domain/index.ts | 3 + .../files/core/domain/models/file.model.ts | 85 +++++++++ src/apps/files/core/domain/models/index.ts | 1 + src/apps/files/core/domain/types/file.ts | 24 +++ src/apps/files/core/domain/types/index.ts | 3 + src/apps/files/core/domain/types/metaData.ts | 11 ++ src/apps/files/core/index.ts | 4 + src/apps/files/index.ts | 1 + src/apps/index.ts | 2 + src/core/config/index.ts | 8 + src/helpers/utils/crypto.ts | 32 ++++ src/helpers/utils/index.ts | 2 + src/modules/router/index.ts | 3 +- src/modules/shared/storage/disk/index.ts | 39 ++-- .../storage/disk/tests/disk-storage.spec.ts | 24 +-- .../shared/storage/disk/types/index.ts | 2 +- 31 files changed, 594 insertions(+), 28 deletions(-) create mode 100644 src/apps/files/core/api/controllers/file.controller.ts create mode 100644 src/apps/files/core/api/controllers/index.ts create mode 100644 src/apps/files/core/api/dtos/index.ts create mode 100644 src/apps/files/core/api/dtos/request/file.create.ts create mode 100644 src/apps/files/core/api/dtos/request/index.ts create mode 100644 src/apps/files/core/api/index.ts create mode 100644 src/apps/files/core/api/routes/file.route.ts create mode 100644 src/apps/files/core/api/routes/index.ts create mode 100644 src/apps/files/core/business/index.ts create mode 100644 src/apps/files/core/business/repositories/file.repo.ts create mode 100644 src/apps/files/core/business/repositories/index.ts create mode 100644 src/apps/files/core/business/services/file.service.ts create mode 100644 src/apps/files/core/business/services/index.ts create mode 100644 src/apps/files/core/domain/index.ts create mode 100644 src/apps/files/core/domain/models/file.model.ts create mode 100644 src/apps/files/core/domain/models/index.ts create mode 100644 src/apps/files/core/domain/types/file.ts create mode 100644 src/apps/files/core/domain/types/index.ts create mode 100644 src/apps/files/core/domain/types/metaData.ts create mode 100644 src/apps/files/core/index.ts create mode 100644 src/apps/files/index.ts create mode 100644 src/helpers/utils/crypto.ts diff --git a/.env.example b/.env.example index b92f603..3e55310 100644 --- a/.env.example +++ b/.env.example @@ -82,6 +82,12 @@ OTP_LENGTH=6 OTP_EXPIRATION=15 #DISK STORAGE -DISK_STORAGE_UPLOAD_FOLDER=/home/michee/projects/system-api/file +DISK_STORAGE_UPLOAD_FOLDER=/home/michee/projects/system-api/stock + +#FILES +FILE_STORES=disk,minio +DEFAULT_FILE_STORAGE=disk + +CRYPTAGE_KEY=secret-key diff --git a/package.json b/package.json index 6dd81e3..ac9b01c 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "minio": "^8.0.0", "mongoose": "^8.4.3", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.14", "rate-limiter-flexible": "^5.0.3", "tsconfig-paths": "^4.2.0", @@ -95,6 +96,7 @@ "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.6", "@types/morgan": "^1.9.9", + "@types/multer": "^1.4.12", "@types/nodemailer": "^6.4.15", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^5.57.1", diff --git a/src/apps/files/core/api/controllers/file.controller.ts b/src/apps/files/core/api/controllers/file.controller.ts new file mode 100644 index 0000000..a7e300a --- /dev/null +++ b/src/apps/files/core/api/controllers/file.controller.ts @@ -0,0 +1,180 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + ApiResponse, + ErrorResponseType, + SuccessResponseType, +} from '@nodesandbox/response-kit'; +import { NextFunction, Request, Response } from 'express'; +import { decryptAES } from 'helpers'; +import fileService from '../../business/services/file.service'; +import { IFileModel } from '../../domain'; + +export class FileController { + /** + * @param req + * @param res + * @param next + */ + static async createFile( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const payload = req.file; + + const diskUpload = await fileService.uploadFile(payload); + if (!diskUpload.success) { + throw diskUpload.error; + } + + const insertFile = { + hash: diskUpload.arguments.hash, + size: diskUpload.arguments.size, + type: diskUpload.arguments.type, + extension: diskUpload.arguments.extension, + metadata: payload, + }; + + const response = await fileService.create(insertFile); + + if (!response.success) { + throw response.error; + } + + ApiResponse.success(res, response, 201); + } catch (error) { + ApiResponse.error(res, { + success: false, + error: error, + } as ErrorResponseType); + } + } + + /** + * @param req + * @param res + * @param next + */ + static async getFilesDB( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const filters = req.query; + const response = await fileService.getFilesDB(filters); + + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response.error; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + /** + * @param req + * @param res + * @param next + */ + static async getFileById( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const fileId = req.params.fileId; + const _payload = (await fileService.findOne({ + _id: fileId, + })) as SuccessResponseType; + + if (!_payload.success) { + throw _payload.error; + } + + const response = await fileService.getFileDisk(_payload); + + if (!response.success) { + throw response.error; + } + + ApiResponse.success(res, response, 200); + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } + + // static async updateFile( + // req: Request, + // res: Response, + // next: NextFunction, + // ): Promise { + // try { + // const fileId = req.params.fileId; + // const newFile = req.file; + + // const checkFIle = (await fileService.findOne({ + // _id: fileId, + // })) as SuccessResponseType; + + // if (!checkFIle.success) { + // throw checkFIle.error; + // } + + // const payload = await fileService.updateFileDisk(checkFIle, newFile); + // if (!payload.success) { + // throw payload.error; + // } + + // const insertFile = { + // hash: payload.arguments.hash, + // size: payload.arguments.size, + // type: payload.arguments.type, + // extension: payload.arguments.extension, + // metadata: newFile, + // }; + + // const fileUpdated = await fileService.update({_id}) + // } catch (error) { + // ApiResponse.error(res, error as ErrorResponseType); + // } + // } + + /** + * @param req + * @param res + * @param next + */ + static async deleteFileAll( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const fileId = req.params.fileId; + const file = req.file; + + const _payload = (await fileService.findOne({ + _id: fileId, + })) as SuccessResponseType; + + if (!_payload.success) { + throw _payload.error; + } + await fileService.deleteFIleDIsk(_payload); + + const response = await fileService.delete({ _id: fileId }); + + if (response.success) { + ApiResponse.success(res, response); + } else { + throw response.error; + } + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } +} diff --git a/src/apps/files/core/api/controllers/index.ts b/src/apps/files/core/api/controllers/index.ts new file mode 100644 index 0000000..c28de2a --- /dev/null +++ b/src/apps/files/core/api/controllers/index.ts @@ -0,0 +1 @@ +export * from './file.controller'; diff --git a/src/apps/files/core/api/dtos/index.ts b/src/apps/files/core/api/dtos/index.ts new file mode 100644 index 0000000..56e4b05 --- /dev/null +++ b/src/apps/files/core/api/dtos/index.ts @@ -0,0 +1 @@ +export * from './request'; diff --git a/src/apps/files/core/api/dtos/request/file.create.ts b/src/apps/files/core/api/dtos/request/file.create.ts new file mode 100644 index 0000000..6f610c7 --- /dev/null +++ b/src/apps/files/core/api/dtos/request/file.create.ts @@ -0,0 +1,5 @@ +import Joi, { ObjectSchema } from 'joi'; + +export const CreateFileRequestSchema: ObjectSchema = Joi.object({ + file: Joi.binary().required(), +}); diff --git a/src/apps/files/core/api/dtos/request/index.ts b/src/apps/files/core/api/dtos/request/index.ts new file mode 100644 index 0000000..47a2e25 --- /dev/null +++ b/src/apps/files/core/api/dtos/request/index.ts @@ -0,0 +1 @@ +export * from './file.create'; diff --git a/src/apps/files/core/api/index.ts b/src/apps/files/core/api/index.ts new file mode 100644 index 0000000..54a23ca --- /dev/null +++ b/src/apps/files/core/api/index.ts @@ -0,0 +1,4 @@ +/* eslint-disable prettier/prettier */ +export * from './controllers'; +export * from './dtos'; +export * from './routes'; diff --git a/src/apps/files/core/api/routes/file.route.ts b/src/apps/files/core/api/routes/file.route.ts new file mode 100644 index 0000000..b32af97 --- /dev/null +++ b/src/apps/files/core/api/routes/file.route.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import multer from 'multer'; +import { FileController } from '../controllers'; + +const router = Router(); + +const upload = multer(); + +router.post('/', upload.single('file'), FileController.createFile); + +router.get('/', FileController.getFilesDB); + +router.get('/:fileId', FileController.getFileById); + +router.delete('/:fileId', FileController.deleteFileAll); + +export default router; diff --git a/src/apps/files/core/api/routes/index.ts b/src/apps/files/core/api/routes/index.ts new file mode 100644 index 0000000..1833271 --- /dev/null +++ b/src/apps/files/core/api/routes/index.ts @@ -0,0 +1 @@ +export { default as FileRoutes } from './file.route'; diff --git a/src/apps/files/core/business/index.ts b/src/apps/files/core/business/index.ts new file mode 100644 index 0000000..7d8a10a --- /dev/null +++ b/src/apps/files/core/business/index.ts @@ -0,0 +1,3 @@ +/* eslint-disable prettier/prettier */ +export * from './repositories'; +export * from './services'; diff --git a/src/apps/files/core/business/repositories/file.repo.ts b/src/apps/files/core/business/repositories/file.repo.ts new file mode 100644 index 0000000..a9902b7 --- /dev/null +++ b/src/apps/files/core/business/repositories/file.repo.ts @@ -0,0 +1,9 @@ +import { BaseRepository } from '@nodesandbox/repo-framework'; +import { Model } from 'mongoose'; +import { IFileModel } from '../../domain'; + +export class FileRepository extends BaseRepository { + constructor(model: Model) { + super(model); + } +} diff --git a/src/apps/files/core/business/repositories/index.ts b/src/apps/files/core/business/repositories/index.ts new file mode 100644 index 0000000..c0851bc --- /dev/null +++ b/src/apps/files/core/business/repositories/index.ts @@ -0,0 +1 @@ +export * from './file.repo'; diff --git a/src/apps/files/core/business/services/file.service.ts b/src/apps/files/core/business/services/file.service.ts new file mode 100644 index 0000000..1b1383e --- /dev/null +++ b/src/apps/files/core/business/services/file.service.ts @@ -0,0 +1,144 @@ +/* eslint-disable prettier/prettier */ +import { BaseService } from '@nodesandbox/repo-framework'; +import { + ErrorResponse, + ErrorResponseType, + SuccessResponseType, +} from '@nodesandbox/response-kit'; +import { decryptAES, parseSortParam } from 'helpers'; +import { DiskStorageService } from 'modules/shared/storage/disk'; +import { FileModel, FileStorageType, IFileModel } from '../../domain'; +import { FileRepository } from '../repositories'; + +export interface FileData { + name: string; + size: number; + type?: string; + extension?: string; + storageType?: FileStorageType; + url: string; + fileHash?: any; +} + +class FileService extends BaseService { + constructor() { + const fileRepo = new FileRepository(FileModel); + super(fileRepo, false); + + this.allowedFilterFields = ['type', 'storageType']; + this.searchFields = ['name', 'extension', 'size', 'type']; + } + + async uploadFile(file: any) { + try { + const buffer = file.buffer; + + const payload = await DiskStorageService.uploadFile(buffer); + + return { success: true, arguments: payload.data }; + } catch (error) { + return { + success: false, + error: + error instanceof ErrorResponse + ? error + : new ErrorResponse( + 'INTERNAL_SERVER_ERROR', + (error as Error).message, + ), + }; + } + } + + async getFilesDB( + filters: Record, + ): Promise | ErrorResponseType> { + const { + page = 1, + limit = 10, + sort, + search = '', + name, + type, + extension, + } = filters; + + const query: any = {}; + if (name) query.name = name; + if (type) query.type = type; + if (extension) query.extension = extension; + + const sortObject = sort ? parseSortParam(sort) : {}; + + return this.findAll({ + query, + sort: sortObject, + page: parseInt(page), + limit: parseInt(limit), + searchTerm: search as string, + }); + } + + async getFileDisk( + payload: any, + ): Promise | ErrorResponseType> { + const fileDiskName = decryptAES( + payload.document?.hash, + process.env.CRYPTAGE_KEY || 'secret-key', + ); + + const response = await DiskStorageService.getFile(fileDiskName); + + if (!response) { + throw response; + } + + console.log('⚔️⚔️⚔️⚔️⚔️ ', response); + + return { success: true, document: response }; + } + + // async updateFileDisk( + // fileId: any, + // file: any, + // ) { + // try { + // const newContent = file.buffer + + // const fileDiskName = decryptAES(fileId.document?.hash, process.env.CRYPTAGE_KEY || 'secret-key') + + // await DiskStorageService.deleteFile(fileDiskName) + + // const fileUpdate = (await DiskStorageService.uploadFile(newContent)) + + // return { success: true, arguments: fileUpdate.data }; + // } catch (error) { + // return { + // success: false, + // error: + // error instanceof ErrorResponse + // ? error + // : new ErrorResponse( + // 'INTERNAL_SERVER_ERROR', + // (error as Error).message, + // ), + // }; + // } + // } + + async deleteFIleDIsk( + fileId: any, + ): Promise | ErrorResponseType> { + const fileDiskName = decryptAES( + fileId.document?.hash, + process.env.CRYPTAGE_KEY || 'secret-key', + ); + + await DiskStorageService.deleteFile(fileDiskName), + console.log('☂️☂️☂️☂️❌❌❌❌❌ file delete ', fileDiskName); + + return { success: true }; + } +} + +export default new FileService(); diff --git a/src/apps/files/core/business/services/index.ts b/src/apps/files/core/business/services/index.ts new file mode 100644 index 0000000..3c44127 --- /dev/null +++ b/src/apps/files/core/business/services/index.ts @@ -0,0 +1 @@ +export * from './file.service'; diff --git a/src/apps/files/core/domain/index.ts b/src/apps/files/core/domain/index.ts new file mode 100644 index 0000000..685cb9a --- /dev/null +++ b/src/apps/files/core/domain/index.ts @@ -0,0 +1,3 @@ +/* eslint-disable prettier/prettier */ +export * from './models'; +export * from './types'; diff --git a/src/apps/files/core/domain/models/file.model.ts b/src/apps/files/core/domain/models/file.model.ts new file mode 100644 index 0000000..82207f9 --- /dev/null +++ b/src/apps/files/core/domain/models/file.model.ts @@ -0,0 +1,85 @@ +import { BaseModel, createBaseSchema } from '@nodesandbox/repo-framework'; +import { IFileModel, IMetaDataModel } from '../types'; + +const FILE_MODEL_NAME = 'File'; +const METADATA_MODEL_NAME = 'File'; + +const metaDataSchema = createBaseSchema( + { + fieldname: { + type: String, + required: true, + }, + originalname: { + type: String, + required: false, + }, + encoding: { + type: String, + required: false, + }, + mimetype: { + type: String, + required: false, + }, + }, + { + modelName: METADATA_MODEL_NAME, + }, +); + +const fileSchema = createBaseSchema( + { + hash: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + extension: { + type: String, + required: true, + }, + size: { + type: Number, + required: true, + }, + storageType: { + type: String, + enum: CONFIG.fs.stores, + default: CONFIG.fs.defaultStore, + required: false, + }, + locationData: { + path: { type: String }, + minio: { + bucket: { type: String }, + objectName: { type: String }, + }, + }, + url: { + type: String, + }, + presignedUrlExpiration: { + type: Date, + }, + metadata: { + type: metaDataSchema, + }, + downloadCount: { + type: Number, + }, + }, + { + modelName: FILE_MODEL_NAME, + }, +); + +const FileModel = new BaseModel( + FILE_MODEL_NAME, + fileSchema, +).getModel(); + +export { FileModel }; diff --git a/src/apps/files/core/domain/models/index.ts b/src/apps/files/core/domain/models/index.ts new file mode 100644 index 0000000..6ef3f97 --- /dev/null +++ b/src/apps/files/core/domain/models/index.ts @@ -0,0 +1 @@ +export * from './file.model'; diff --git a/src/apps/files/core/domain/types/file.ts b/src/apps/files/core/domain/types/file.ts new file mode 100644 index 0000000..bbd256d --- /dev/null +++ b/src/apps/files/core/domain/types/file.ts @@ -0,0 +1,24 @@ +import { IBaseModel } from '@nodesandbox/repo-framework'; +import { Document } from 'mongoose'; + +export type FileStorageType = keyof typeof CONFIG.fs.stores; + +export interface IFile { + hash: string; + type: string; + extension: string; + size: number; + storageType: FileStorageType; + locationData: string; + path: string; + minio: string; + url: string; + presignedUrlExpiration: Date; + metadata: {}; + description: string; + tags: Array; + accessRoles: Array; + downloadCount: string; +} + +export interface IFileModel extends IFile, IBaseModel, Document {} diff --git a/src/apps/files/core/domain/types/index.ts b/src/apps/files/core/domain/types/index.ts new file mode 100644 index 0000000..0ca0d69 --- /dev/null +++ b/src/apps/files/core/domain/types/index.ts @@ -0,0 +1,3 @@ +/* eslint-disable prettier/prettier */ +export * from './file'; +export * from './metaData'; diff --git a/src/apps/files/core/domain/types/metaData.ts b/src/apps/files/core/domain/types/metaData.ts new file mode 100644 index 0000000..90f9ebb --- /dev/null +++ b/src/apps/files/core/domain/types/metaData.ts @@ -0,0 +1,11 @@ +import { IBaseModel } from '@nodesandbox/repo-framework'; +import { Document } from 'mongoose'; + +export interface IMetaData { + fieldname: string; + originalname: string; + encoding: string; + mimetype: string; +} + +export interface IMetaDataModel extends IBaseModel, Document {} diff --git a/src/apps/files/core/index.ts b/src/apps/files/core/index.ts new file mode 100644 index 0000000..9526c2c --- /dev/null +++ b/src/apps/files/core/index.ts @@ -0,0 +1,4 @@ +/* eslint-disable prettier/prettier */ +export * from './api'; +export * from './business'; +export * from './domain'; diff --git a/src/apps/files/index.ts b/src/apps/files/index.ts new file mode 100644 index 0000000..4b0e041 --- /dev/null +++ b/src/apps/files/index.ts @@ -0,0 +1 @@ +export * from './core'; diff --git a/src/apps/index.ts b/src/apps/index.ts index 57ebf74..5b300e3 100644 --- a/src/apps/index.ts +++ b/src/apps/index.ts @@ -1,5 +1,7 @@ +/* eslint-disable prettier/prettier */ /** * Export all the apps routes here */ export * from './demo'; +export * from './files'; diff --git a/src/core/config/index.ts b/src/core/config/index.ts index fa8fb5f..fc6fcea 100644 --- a/src/core/config/index.ts +++ b/src/core/config/index.ts @@ -74,6 +74,10 @@ export interface Config { { code: string; title: string; description: string; message: string } >; }; + fs: { + stores?: Array; + defaultStore: string; + }; } export class ConfigService { @@ -225,6 +229,10 @@ export class ConfigService { }, }, }, + fs: { + stores: process.env.FILE_STORES?.split(','), + defaultStore: process.env.DEFAULT_FILE_STORAGE || 'disk', + }, }; } diff --git a/src/helpers/utils/crypto.ts b/src/helpers/utils/crypto.ts new file mode 100644 index 0000000..6d3d422 --- /dev/null +++ b/src/helpers/utils/crypto.ts @@ -0,0 +1,32 @@ +import * as crypto from 'crypto'; + +/** + * @param text + * @param secretKey + * @returns + */ + +export function encryptAES(text: string, secretKey: string): string { + const iv = crypto.randomBytes(16); + const key = crypto.scryptSync(secretKey, 'salt', 32); + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return iv.toString('hex') + ':' + encrypted; +} + +/** + * @param encryptedText + * @param secretKey + * @returns + */ +export function decryptAES(encryptedText: string, secretKey: string): string { + const textParts = encryptedText.split(':'); + const iv = Buffer.from(textParts.shift() as string, 'hex'); + const encrypted = textParts.join(':'); + const key = crypto.scryptSync(secretKey, 'salt', 32); + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; +} diff --git a/src/helpers/utils/index.ts b/src/helpers/utils/index.ts index 4393854..736991a 100644 --- a/src/helpers/utils/index.ts +++ b/src/helpers/utils/index.ts @@ -1,4 +1,6 @@ /* eslint-disable prettier/prettier */ +export * from './crypto'; +export { decryptAES, encryptAES } from './crypto'; export * from './file'; export * from './generator'; export * from './string'; diff --git a/src/modules/router/index.ts b/src/modules/router/index.ts index d8ce7f7..19df553 100644 --- a/src/modules/router/index.ts +++ b/src/modules/router/index.ts @@ -1,4 +1,4 @@ -import { TodoRoutes } from 'apps'; +import { FileRoutes, TodoRoutes } from 'apps'; import { Router } from 'express'; import { DevRoutes } from 'modules/features'; @@ -16,5 +16,6 @@ export class RouterModule { private static initializeRoutes(): void { RouterModule.router.use('', DevRoutes); RouterModule.router.use('/todos', TodoRoutes); + RouterModule.router.use('/file', FileRoutes); } } diff --git a/src/modules/shared/storage/disk/index.ts b/src/modules/shared/storage/disk/index.ts index f9c6cc1..4b39816 100644 --- a/src/modules/shared/storage/disk/index.ts +++ b/src/modules/shared/storage/disk/index.ts @@ -4,7 +4,7 @@ dotenv.config(); import crypto from 'crypto'; import fs, { promises as fsPromise } from 'fs'; import path from 'path'; -import { detectFileType } from '../../../../helpers/utils/file'; +import { detectFileType, encryptAES } from '../../../../helpers/utils'; import { FileMetadata } from './types'; export class DiskStorageService { @@ -33,11 +33,18 @@ export class DiskStorageService { if (fs.existsSync(this.uploadDir)) { return this.handleResponse(false, 'Dossier existant', 409); } - fs.mkdirSync(this.uploadDir); + fs.mkdirSync(this.uploadDir, { recursive: true }); return this.handleResponse(true, 'Dossier créer avec succès', 201); } catch (error) { - return this.handleResponse(false, 'Erreur lors de la création', 500); + console.error('Erreur lors de la création du dossier :', error); + return this.handleResponse( + false, + 'Erreur lors de la création', + 500, + undefined, + error as Error, + ); } } @@ -45,6 +52,8 @@ export class DiskStorageService { try { await this.CreateUploadFolder(); + console.log('⚔️⚔️⚔️⚔️⚔️ contact reusi'); + const hash = crypto .createHash('sha256') .update(contentBuffer) @@ -57,19 +66,24 @@ export class DiskStorageService { await fsPromise.writeFile(filePath, contentBuffer); + const hashedName = encryptAES( + fileId, + process.env.CRYPTAGE_KEY || 'secret-key', + ); + const fileData: FileMetadata = { - name: fileId, + // name: fileId, size: contentBuffer.length, type: fileType, extension: `.${fileType || 'bin'}`, - hash: hash, + hash: hashedName, }; return this.handleResponse( true, 'FIchier uploader avec succes', 201, - fileData.name, + fileData, ); } catch (error) { return this.handleResponse( @@ -212,17 +226,20 @@ export class DiskStorageService { } } - static async moveFile(sourceId: string, destinationId: string): Promise { + static async updateFile(fileId: any, newContent: any): Promise { try { - const sourcePath = path.join(this.uploadDir, sourceId); - const destinationPath = path.join(this.uploadDir, destinationId); - const newName = await fsPromise.rename(sourcePath, destinationPath); + const file = await this.getFile(fileId); + if (!file.error) { + throw file.error; + } + + const updateFile = await fsPromise.writeFile(this.uploadDir, newContent); return this.handleResponse( true, 'La modification du nom du fichier a été effectuer avec succès', 201, - newName, + updateFile, ); } catch (error) { return this.handleResponse( diff --git a/src/modules/shared/storage/disk/tests/disk-storage.spec.ts b/src/modules/shared/storage/disk/tests/disk-storage.spec.ts index 5e29ff8..08b2a8e 100644 --- a/src/modules/shared/storage/disk/tests/disk-storage.spec.ts +++ b/src/modules/shared/storage/disk/tests/disk-storage.spec.ts @@ -161,30 +161,26 @@ describe('DiskStorageService', () => { }); }); - describe('moveFile', () => { - let sourceId: string; - - beforeEach(async () => { + describe('updateFile', () => { + it('should move file successfully', async () => { const uploadResult = await DiskStorageService.uploadFile(TEST_FILE_CONTENT); - sourceId = uploadResult.data; - }); + const fileId = uploadResult.data; - it('should move file successfully', async () => { - const destinationId = 'moved-file'; - const result = await DiskStorageService.moveFile(sourceId, destinationId); + const newContent = 'moved-file'; + const result = await DiskStorageService.updateFile(fileId, newContent); expect(result.success).toBe(true); expect(result.code).toBe(201); - const sourcePath = path.join(TEST_UPLOAD_DIR, sourceId); - const destinationPath = path.join(TEST_UPLOAD_DIR, destinationId); - expect(fs.existsSync(sourcePath)).toBe(false); - expect(fs.existsSync(destinationPath)).toBe(true); + expect(fs.existsSync(newContent)).toBe(true); }); it('should handle moving non-existent files', async () => { - const result = await DiskStorageService.moveFile('non-existent', 'dest'); + const result = await DiskStorageService.updateFile( + 'fileId', + 'non-existent', + ); expect(result.success).toBe(false); expect(result.code).toBe(500); diff --git a/src/modules/shared/storage/disk/types/index.ts b/src/modules/shared/storage/disk/types/index.ts index b1162eb..e72759d 100644 --- a/src/modules/shared/storage/disk/types/index.ts +++ b/src/modules/shared/storage/disk/types/index.ts @@ -1,5 +1,5 @@ export interface FileMetadata { - name: string; + name?: string; size: number; type: string; extension: string; From 4803420ee7e5a085b61365d98d309dc2591ce43e Mon Sep 17 00:00:00 2001 From: michee-04 Date: Fri, 15 Nov 2024 08:27:58 +0000 Subject: [PATCH 3/7] feat: implementation du CRUD pour le FileService --- .gitignore | 3 +- .../core/api/controllers/todo.controller.ts | 10 +- src/apps/demo/core/api/routes/todo.routes.ts | 4 +- .../demo/core/domain/models/todo.model.ts | 3 + src/apps/demo/core/domain/types/todo.ts | 1 + .../core/api/controllers/file.controller.ts | 97 ++++-------- src/apps/files/core/api/routes/file.route.ts | 2 + .../core/business/services/file.service.ts | 149 +++++++++++------- .../files/core/domain/models/file.model.ts | 6 +- src/helpers/utils/file.ts | 13 ++ src/modules/shared/storage/disk/index.ts | 82 +++++----- .../shared/storage/disk/types/index.ts | 2 + 12 files changed, 198 insertions(+), 174 deletions(-) diff --git a/.gitignore b/.gitignore index c811f5d..1947187 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules package-lock.json logs -.vscode \ No newline at end of file +.vscode +upload \ No newline at end of file diff --git a/src/apps/demo/core/api/controllers/todo.controller.ts b/src/apps/demo/core/api/controllers/todo.controller.ts index 99c20ce..572c543 100644 --- a/src/apps/demo/core/api/controllers/todo.controller.ts +++ b/src/apps/demo/core/api/controllers/todo.controller.ts @@ -1,13 +1,14 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Request, Response, NextFunction } from 'express'; import { ApiResponse, ErrorResponse, ErrorResponseType, } from '@nodesandbox/response-kit'; import { TodoService } from 'apps/demo/core/business'; -import { CreateTodoRequestSchema } from '../dtos'; +import fileService from 'apps/files/core/business/services/file.service'; +import { NextFunction, Request, Response } from 'express'; import { sanitize } from 'helpers'; +import { CreateTodoRequestSchema } from '../dtos'; /** * Controller to handle the operations related to the Todo resource. @@ -26,11 +27,16 @@ export class TodoController { ): Promise { try { const _payload = sanitize(req.body, CreateTodoRequestSchema); + const image = req.file; if (!_payload.success) { throw _payload.error; } + const todoImage = await fileService.createFile(image); + + _payload.data.image = todoImage.document?._id; + const response = await TodoService.create(_payload.data); if (!response.success) { diff --git a/src/apps/demo/core/api/routes/todo.routes.ts b/src/apps/demo/core/api/routes/todo.routes.ts index e37544b..2f96f2d 100644 --- a/src/apps/demo/core/api/routes/todo.routes.ts +++ b/src/apps/demo/core/api/routes/todo.routes.ts @@ -1,13 +1,15 @@ import { Router } from 'express'; +import multer from 'multer'; import { TodoController } from '../controllers'; const router = Router(); +const upload = multer(); /** * Route for creating a new Todo * POST /todos */ -router.post('/', TodoController.createTodo); +router.post('/', upload.single('file'), TodoController.createTodo); /** * Route for retrieving all Todos, filtered by query parameters diff --git a/src/apps/demo/core/domain/models/todo.model.ts b/src/apps/demo/core/domain/models/todo.model.ts index be6ceb0..0fcbf50 100644 --- a/src/apps/demo/core/domain/models/todo.model.ts +++ b/src/apps/demo/core/domain/models/todo.model.ts @@ -18,6 +18,9 @@ const todoSchema = createBaseSchema( description: { type: String, }, + image: { + type: String, + }, completed: { type: Boolean, default: false, diff --git a/src/apps/demo/core/domain/types/todo.ts b/src/apps/demo/core/domain/types/todo.ts index 69744fe..59acef2 100644 --- a/src/apps/demo/core/domain/types/todo.ts +++ b/src/apps/demo/core/domain/types/todo.ts @@ -6,6 +6,7 @@ export type TodoPriority = 'low' | 'medium' | 'high'; export interface ITodo { title: string; description?: string; + image: string; completed: boolean; dueDate?: Date; priority: TodoPriority; diff --git a/src/apps/files/core/api/controllers/file.controller.ts b/src/apps/files/core/api/controllers/file.controller.ts index a7e300a..2f6f14b 100644 --- a/src/apps/files/core/api/controllers/file.controller.ts +++ b/src/apps/files/core/api/controllers/file.controller.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; /* eslint-disable @typescript-eslint/no-unused-vars */ import { ApiResponse, @@ -22,22 +23,8 @@ export class FileController { ): Promise { try { const payload = req.file; - - const diskUpload = await fileService.uploadFile(payload); - if (!diskUpload.success) { - throw diskUpload.error; - } - - const insertFile = { - hash: diskUpload.arguments.hash, - size: diskUpload.arguments.size, - type: diskUpload.arguments.type, - extension: diskUpload.arguments.extension, - metadata: payload, - }; - - const response = await fileService.create(insertFile); - + // const fileService = new FileService(source:CONFIG.file) + const response = await fileService.createFile(payload); if (!response.success) { throw response.error; } @@ -87,16 +74,7 @@ export class FileController { ): Promise { try { const fileId = req.params.fileId; - const _payload = (await fileService.findOne({ - _id: fileId, - })) as SuccessResponseType; - - if (!_payload.success) { - throw _payload.error; - } - - const response = await fileService.getFileDisk(_payload); - + const response = await fileService.getFileDIsk(fileId); if (!response.success) { throw response.error; } @@ -107,41 +85,34 @@ export class FileController { } } - // static async updateFile( - // req: Request, - // res: Response, - // next: NextFunction, - // ): Promise { - // try { - // const fileId = req.params.fileId; - // const newFile = req.file; - - // const checkFIle = (await fileService.findOne({ - // _id: fileId, - // })) as SuccessResponseType; + static async downloadFile( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + try { + const fileId = req.params.fileId; - // if (!checkFIle.success) { - // throw checkFIle.error; - // } + const response = (await fileService.sendFile( + fileId, + )) as SuccessResponseType; - // const payload = await fileService.updateFileDisk(checkFIle, newFile); - // if (!payload.success) { - // throw payload.error; - // } + if (!response.success) { + throw response.error; + } - // const insertFile = { - // hash: payload.arguments.hash, - // size: payload.arguments.size, - // type: payload.arguments.type, - // extension: payload.arguments.extension, - // metadata: newFile, - // }; + res.writeHead(200, { + 'content-type': response.document?.mimetype, + 'content-length': response.document?.size, + }); - // const fileUpdated = await fileService.update({_id}) - // } catch (error) { - // ApiResponse.error(res, error as ErrorResponseType); - // } - // } + const filepath = response.document?.path as string; + const file = fs.readFileSync(filepath); + res.end(file); + } catch (error) { + ApiResponse.error(res, error as ErrorResponseType); + } + } /** * @param req @@ -155,18 +126,8 @@ export class FileController { ): Promise { try { const fileId = req.params.fileId; - const file = req.file; - - const _payload = (await fileService.findOne({ - _id: fileId, - })) as SuccessResponseType; - - if (!_payload.success) { - throw _payload.error; - } - await fileService.deleteFIleDIsk(_payload); - const response = await fileService.delete({ _id: fileId }); + const response = await fileService.deleteFIle(fileId); if (response.success) { ApiResponse.success(res, response); diff --git a/src/apps/files/core/api/routes/file.route.ts b/src/apps/files/core/api/routes/file.route.ts index b32af97..0684569 100644 --- a/src/apps/files/core/api/routes/file.route.ts +++ b/src/apps/files/core/api/routes/file.route.ts @@ -12,6 +12,8 @@ router.get('/', FileController.getFilesDB); router.get('/:fileId', FileController.getFileById); +router.get('/:fileId/download', FileController.downloadFile); + router.delete('/:fileId', FileController.deleteFileAll); export default router; diff --git a/src/apps/files/core/business/services/file.service.ts b/src/apps/files/core/business/services/file.service.ts index 1b1383e..b0e6290 100644 --- a/src/apps/files/core/business/services/file.service.ts +++ b/src/apps/files/core/business/services/file.service.ts @@ -6,20 +6,10 @@ import { SuccessResponseType, } from '@nodesandbox/response-kit'; import { decryptAES, parseSortParam } from 'helpers'; -import { DiskStorageService } from 'modules/shared/storage/disk'; -import { FileModel, FileStorageType, IFileModel } from '../../domain'; +import { storage } from 'modules/shared/storage'; +import { FileModel, IFileModel } from '../../domain'; import { FileRepository } from '../repositories'; -export interface FileData { - name: string; - size: number; - type?: string; - extension?: string; - storageType?: FileStorageType; - url: string; - fileHash?: any; -} - class FileService extends BaseService { constructor() { const fileRepo = new FileRepository(FileModel); @@ -29,13 +19,24 @@ class FileService extends BaseService { this.searchFields = ['name', 'extension', 'size', 'type']; } - async uploadFile(file: any) { + async createFile(file: any) { try { + const meta = file; const buffer = file.buffer; - const payload = await DiskStorageService.uploadFile(buffer); + const payload = await storage.disk.uploadFile(buffer); + + const insertFile = { + hash: payload.data.hash, + size: payload.data.size, + type: payload.data.type, + extension: payload.data.extension, + metadata: meta, + }; - return { success: true, arguments: payload.data }; + const response = await this.repository.create(insertFile); + + return { success: true, document: response }; } catch (error) { return { success: false, @@ -79,65 +80,93 @@ class FileService extends BaseService { }); } - async getFileDisk( - payload: any, + async getFileDIsk( + fileId: any, ): Promise | ErrorResponseType> { + const payload = await this.repository.findOne({ _id: fileId }); + // TODO Gerer les erreurs liés au fichier introuvable avec (if) apres la modification du package ErrorResponseType + + const hash = payload?.hash as string; + const fileDiskName = decryptAES( - payload.document?.hash, + hash, process.env.CRYPTAGE_KEY || 'secret-key', ); - const response = await DiskStorageService.getFile(fileDiskName); + const response = await storage.disk.getFile(fileDiskName); - if (!response) { - throw response; - } + return { success: true, document: response }; + } - console.log('⚔️⚔️⚔️⚔️⚔️ ', response); + async sendFile( + fileId: any, + ): Promise | ErrorResponseType> { + try { + const file = await this.repository.findOne({ _id: fileId }); - return { success: true, document: response }; + // TODO Gerer les erreurs liés au fichier introuvable avec (if) apres la modification du package ErrorResponseType + if (!file) { + throw file; + } + const fileDiskName = decryptAES( + file.hash, + process.env.CRYPTAGE_KEY || 'secret-key', + ); + + const response = await storage.disk.getFile(fileDiskName); + + if (!response.success) { + throw response.error; + } + + response.data.mimetype = (file.metadata as { mimetype: string }).mimetype; + + return { success: true, document: response.data }; + } catch (error) { + return { + success: false, + error: + error instanceof ErrorResponse + ? error + : new ErrorResponse( + 'INTERNAL_SERVER_ERROR', + (error as Error).message, + ), + }; + } } - // async updateFileDisk( - // fileId: any, - // file: any, - // ) { - // try { - // const newContent = file.buffer - - // const fileDiskName = decryptAES(fileId.document?.hash, process.env.CRYPTAGE_KEY || 'secret-key') - - // await DiskStorageService.deleteFile(fileDiskName) - - // const fileUpdate = (await DiskStorageService.uploadFile(newContent)) - - // return { success: true, arguments: fileUpdate.data }; - // } catch (error) { - // return { - // success: false, - // error: - // error instanceof ErrorResponse - // ? error - // : new ErrorResponse( - // 'INTERNAL_SERVER_ERROR', - // (error as Error).message, - // ), - // }; - // } - // } - - async deleteFIleDIsk( + async deleteFIle( fileId: any, ): Promise | ErrorResponseType> { - const fileDiskName = decryptAES( - fileId.document?.hash, - process.env.CRYPTAGE_KEY || 'secret-key', - ); + try { + const payload = await this.repository.findOne({ _id: fileId }); + // TODO Gerer les erreurs liés au fichier introuvable avec (if) apres la modification du package ErrorResponseType + + const hash = payload?.hash as string; + + const fileDiskName = decryptAES( + hash, + process.env.CRYPTAGE_KEY || 'secret-key', + ); + + await storage.disk.deleteFile(fileDiskName); - await DiskStorageService.deleteFile(fileDiskName), - console.log('☂️☂️☂️☂️❌❌❌❌❌ file delete ', fileDiskName); + await this.repository.delete({ _id: fileId }); - return { success: true }; + return { success: true }; + } catch (error) { + return { + success: false, + error: + error instanceof ErrorResponse + ? error + : new ErrorResponse( + 'INTERNAL_SERVER_ERROR', + (error as Error).message, + ), + }; + } } } diff --git a/src/apps/files/core/domain/models/file.model.ts b/src/apps/files/core/domain/models/file.model.ts index 82207f9..ba50e03 100644 --- a/src/apps/files/core/domain/models/file.model.ts +++ b/src/apps/files/core/domain/models/file.model.ts @@ -12,15 +12,15 @@ const metaDataSchema = createBaseSchema( }, originalname: { type: String, - required: false, + required: true, }, encoding: { type: String, - required: false, + required: true, }, mimetype: { type: String, - required: false, + required: true, }, }, { diff --git a/src/helpers/utils/file.ts b/src/helpers/utils/file.ts index c5d1330..be7cf2b 100644 --- a/src/helpers/utils/file.ts +++ b/src/helpers/utils/file.ts @@ -1,10 +1,23 @@ const magicNumbers: { type: string; signature?: number[] }[] = [ + // fichiers { type: 'JPEG', signature: [0xff, 0xd8, 0xff] }, { type: 'PNG', signature: [0x89, 0x50, 0x4e, 0x47] }, { type: 'GIF', signature: [0x47, 0x49, 0x46, 0x38] }, { type: 'PDF', signature: [0x25, 0x50, 0x44, 0x46] }, { type: 'ZIP', signature: [0x50, 0x4b, 0x03, 0x04] }, { type: 'TXT' }, + // Vidéos + { type: 'MP4', signature: [0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70] }, + { type: 'MP4', signature: [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70] }, + { type: 'AVI', signature: [0x52, 0x49, 0x46, 0x46] }, + { type: 'MKV', signature: [0x1a, 0x45, 0xdf, 0xa3] }, + { type: 'MOV', signature: [0x00, 0x00, 0x00, 0x14, 0x66, 0x74, 0x79, 0x70] }, + // Audio + { type: 'MP3', signature: [0x49, 0x44, 0x33] }, + { type: 'WAV', signature: [0x52, 0x49, 0x46, 0x46] }, + { type: 'FLAC', signature: [0x66, 0x4c, 0x61, 0x43] }, + { type: 'AAC', signature: [0xff, 0xf1] }, + { type: 'OGG', signature: [0x4f, 0x67, 0x67, 0x53] }, ]; export const detectFileType = async (buffer: Buffer): Promise => { diff --git a/src/modules/shared/storage/disk/index.ts b/src/modules/shared/storage/disk/index.ts index 4b39816..49bc90f 100644 --- a/src/modules/shared/storage/disk/index.ts +++ b/src/modules/shared/storage/disk/index.ts @@ -8,11 +8,15 @@ import { detectFileType, encryptAES } from '../../../../helpers/utils'; import { FileMetadata } from './types'; export class DiskStorageService { - private static uploadDir: string = path.resolve( - process.env.DISK_STORAGE_UPLOAD_FOLDER || 'uploadtest', + // private static uploadDir: string = path.resolve('./upload'); + private uploadDir: string = path.resolve( + process.env.DISK_STORAGE_UPLOAD_FOLDER || './upload', ); + static CreateUploadFolder: any; + static uploadDir: string; + static handleResponse: any; - private static handleResponse( + private handleResponse( success: boolean, message: string, code: number, @@ -28,7 +32,7 @@ export class DiskStorageService { }; } - static async CreateUploadFolder() { + async CreateUploadFolder() { try { if (fs.existsSync(this.uploadDir)) { return this.handleResponse(false, 'Dossier existant', 409); @@ -48,12 +52,10 @@ export class DiskStorageService { } } - static async uploadFile(contentBuffer: Buffer): Promise { + async uploadFile(contentBuffer: Buffer): Promise { try { await this.CreateUploadFolder(); - console.log('⚔️⚔️⚔️⚔️⚔️ contact reusi'); - const hash = crypto .createHash('sha256') .update(contentBuffer) @@ -77,6 +79,7 @@ export class DiskStorageService { type: fileType, extension: `.${fileType || 'bin'}`, hash: hashedName, + path: filePath, }; return this.handleResponse( @@ -96,7 +99,7 @@ export class DiskStorageService { } } - static async getFile(fileId: string): Promise { + async getFile(fileId: string): Promise { try { const filePath = path.join(this.uploadDir, fileId); const contentBuffer = await fsPromise.readFile(filePath); @@ -108,6 +111,7 @@ export class DiskStorageService { size: contentBuffer.length, type: fileType, extension: fileType, + path: filePath, hash: crypto.createHash('sha256').update(contentBuffer).digest('hex'), }; @@ -128,7 +132,7 @@ export class DiskStorageService { } } - static async listFiles(): Promise { + async listFiles(): Promise { try { const files = await fsPromise.readdir(this.uploadDir); @@ -144,7 +148,33 @@ export class DiskStorageService { } } - static async deleteFile(fileId: string): Promise { + async updateFile(fileId: any, newContent: any): Promise { + try { + const file = await this.getFile(fileId); + if (!file.error) { + throw file.error; + } + + const updateFile = await fsPromise.writeFile(this.uploadDir, newContent); + + return this.handleResponse( + true, + 'La modification du nom du fichier a été effectuer avec succès', + 201, + updateFile, + ); + } catch (error) { + return this.handleResponse( + false, + 'Erreur lors de la modification du nom', + 500, + undefined, + error as Error, + ); + } + } + + async deleteFile(fileId: string): Promise { try { const filePath = path.join(this.uploadDir, fileId); await fsPromise.unlink(filePath); @@ -160,7 +190,7 @@ export class DiskStorageService { } } - static async emptyDirectory(): Promise { + async emptyDirectory(): Promise { try { const files = await fsPromise.readdir(this.uploadDir); const deleteFile = files.map((files) => @@ -184,7 +214,7 @@ export class DiskStorageService { } } - static async deleteDirectory(): Promise { + async deleteDirectory(): Promise { try { await fsPromise.rmdir(this.uploadDir); @@ -204,7 +234,7 @@ export class DiskStorageService { } } - static async copyFile(sourceId: string, destinationId: string): Promise { + async copyFile(sourceId: string, destinationId: string): Promise { try { const sourcePath = path.join(this.uploadDir, sourceId); const destinationPath = path.join(this.uploadDir, destinationId); @@ -225,30 +255,4 @@ export class DiskStorageService { ); } } - - static async updateFile(fileId: any, newContent: any): Promise { - try { - const file = await this.getFile(fileId); - if (!file.error) { - throw file.error; - } - - const updateFile = await fsPromise.writeFile(this.uploadDir, newContent); - - return this.handleResponse( - true, - 'La modification du nom du fichier a été effectuer avec succès', - 201, - updateFile, - ); - } catch (error) { - return this.handleResponse( - false, - 'Erreur lors de la modification du nom', - 500, - undefined, - error as Error, - ); - } - } } diff --git a/src/modules/shared/storage/disk/types/index.ts b/src/modules/shared/storage/disk/types/index.ts index e72759d..1b81e38 100644 --- a/src/modules/shared/storage/disk/types/index.ts +++ b/src/modules/shared/storage/disk/types/index.ts @@ -4,4 +4,6 @@ export interface FileMetadata { type: string; extension: string; hash: string; + path: string; + mimetype?: string; } From 5c463b16e7253cf658d846f6bb453dffc6f6f0e4 Mon Sep 17 00:00:00 2001 From: michee-04 Date: Fri, 15 Nov 2024 09:11:15 +0000 Subject: [PATCH 4/7] update: modification de l'appellation de methode de DIskStorageService --- .env.example | 2 +- src/core/config/index.ts | 2 +- src/modules/shared/storage/disk/index.ts | 3 -- .../storage/disk/tests/disk-storage.spec.ts | 47 ++++++++----------- 4 files changed, 22 insertions(+), 32 deletions(-) diff --git a/.env.example b/.env.example index 3e55310..0b91f1c 100644 --- a/.env.example +++ b/.env.example @@ -86,7 +86,7 @@ DISK_STORAGE_UPLOAD_FOLDER=/home/michee/projects/system-api/stock #FILES FILE_STORES=disk,minio -DEFAULT_FILE_STORAGE=disk +FILE_STORAGE=disk CRYPTAGE_KEY=secret-key diff --git a/src/core/config/index.ts b/src/core/config/index.ts index fc6fcea..906cd8e 100644 --- a/src/core/config/index.ts +++ b/src/core/config/index.ts @@ -231,7 +231,7 @@ export class ConfigService { }, fs: { stores: process.env.FILE_STORES?.split(','), - defaultStore: process.env.DEFAULT_FILE_STORAGE || 'disk', + defaultStore: process.env.FILE_STORAGE || 'disk', }, }; } diff --git a/src/modules/shared/storage/disk/index.ts b/src/modules/shared/storage/disk/index.ts index 49bc90f..1c12512 100644 --- a/src/modules/shared/storage/disk/index.ts +++ b/src/modules/shared/storage/disk/index.ts @@ -12,9 +12,6 @@ export class DiskStorageService { private uploadDir: string = path.resolve( process.env.DISK_STORAGE_UPLOAD_FOLDER || './upload', ); - static CreateUploadFolder: any; - static uploadDir: string; - static handleResponse: any; private handleResponse( success: boolean, diff --git a/src/modules/shared/storage/disk/tests/disk-storage.spec.ts b/src/modules/shared/storage/disk/tests/disk-storage.spec.ts index 08b2a8e..46911b3 100644 --- a/src/modules/shared/storage/disk/tests/disk-storage.spec.ts +++ b/src/modules/shared/storage/disk/tests/disk-storage.spec.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import { DiskStorageService } from '..'; +import { storage } from '../..'; describe('DiskStorageService', () => { const TEST_UPLOAD_DIR = path.resolve( @@ -24,13 +24,13 @@ describe('DiskStorageService', () => { beforeAll(async () => { if (!fs.existsSync(TEST_UPLOAD_DIR)) { - await DiskStorageService.CreateUploadFolder(); + await storage.disk.CreateUploadFolder(); } }); afterAll(async () => { try { - await Promise.all(await DiskStorageService.deleteDirectory()); + await Promise.all(await storage.disk.deleteDirectory()); } catch (error) { console.error('Cleanup error:', error); } @@ -38,7 +38,7 @@ describe('DiskStorageService', () => { describe('uploadFile', () => { it('should upload a file successfully', async () => { - const result = await DiskStorageService.uploadFile(TEST_FILE_CONTENT); + const result = await storage.disk.uploadFile(TEST_FILE_CONTENT); expect(result.success).toBe(true); expect(result.code).toBe(201); @@ -53,13 +53,12 @@ describe('DiskStorageService', () => { describe('getFile', () => { beforeEach(async () => { - const uploadResult = - await DiskStorageService.uploadFile(TEST_FILE_CONTENT); + const uploadResult = await storage.disk.uploadFile(TEST_FILE_CONTENT); testFileId = uploadResult.data; }); it('should retrieve file metadata successfully', async () => { - const result = await DiskStorageService.getFile(testFileId); + const result = await storage.disk.getFile(testFileId); expect(result.success).toBe(true); expect(result.code).toBe(200); @@ -69,7 +68,7 @@ describe('DiskStorageService', () => { }); it('should handle non-existent files', async () => { - const result = await DiskStorageService.getFile('non-existent-file'); + const result = await storage.disk.getFile('non-existent-file'); expect(result.success).toBe(false); expect(result.code).toBe(500); @@ -78,12 +77,12 @@ describe('DiskStorageService', () => { describe('listFiles', () => { beforeEach(async () => { - await DiskStorageService.uploadFile(Buffer.from('File 1')); - await DiskStorageService.uploadFile(Buffer.from('File 2')); + await storage.disk.uploadFile(Buffer.from('File 1')); + await storage.disk.uploadFile(Buffer.from('File 2')); }); it('should list all files in directory', async () => { - const result = await DiskStorageService.listFiles(); + const result = await storage.disk.listFiles(); expect(result.success).toBe(true); expect(result.code).toBe(200); @@ -94,13 +93,12 @@ describe('DiskStorageService', () => { describe('deleteFile', () => { beforeEach(async () => { - const uploadResult = - await DiskStorageService.uploadFile(TEST_FILE_CONTENT); + const uploadResult = await storage.disk.uploadFile(TEST_FILE_CONTENT); testFileId = uploadResult.data; }); it('should delete file successfully', async () => { - const result = await DiskStorageService.deleteFile(testFileId); + const result = await storage.disk.deleteFile(testFileId); expect(result.success).toBe(true); expect(result.code).toBe(200); @@ -111,7 +109,7 @@ describe('DiskStorageService', () => { }); it('should handle deletion of non-existent files', async () => { - const result = await DiskStorageService.deleteFile('non-existent-file'); + const result = await storage.disk.deleteFile('non-existent-file'); expect(result.success).toBe(false); expect(result.code).toBe(500); @@ -120,7 +118,7 @@ describe('DiskStorageService', () => { describe('emptyDirectory', () => { it('should empty directory successfully', async () => { - const result = await DiskStorageService.emptyDirectory(); + const result = await storage.disk.emptyDirectory(); expect(result.success).toBe(true); expect(result.code).toBe(200); @@ -134,14 +132,13 @@ describe('DiskStorageService', () => { let sourceId: string; beforeEach(async () => { - const uploadResult = - await DiskStorageService.uploadFile(TEST_FILE_CONTENT); + const uploadResult = await storage.disk.uploadFile(TEST_FILE_CONTENT); sourceId = uploadResult.data; }); it('should copy file successfully', async () => { const destinationId = 'copied-file'; - const result = await DiskStorageService.copyFile(sourceId, destinationId); + const result = await storage.disk.copyFile(sourceId, destinationId); expect(result.success).toBe(true); expect(result.code).toBe(200); @@ -154,7 +151,7 @@ describe('DiskStorageService', () => { }); it('should handle copying non-existent files', async () => { - const result = await DiskStorageService.copyFile('non-existent', 'dest'); + const result = await storage.disk.copyFile('non-existent', 'dest'); expect(result.success).toBe(false); expect(result.code).toBe(500); @@ -163,12 +160,11 @@ describe('DiskStorageService', () => { describe('updateFile', () => { it('should move file successfully', async () => { - const uploadResult = - await DiskStorageService.uploadFile(TEST_FILE_CONTENT); + const uploadResult = await storage.disk.uploadFile(TEST_FILE_CONTENT); const fileId = uploadResult.data; const newContent = 'moved-file'; - const result = await DiskStorageService.updateFile(fileId, newContent); + const result = await storage.disk.updateFile(fileId, newContent); expect(result.success).toBe(true); expect(result.code).toBe(201); @@ -177,10 +173,7 @@ describe('DiskStorageService', () => { }); it('should handle moving non-existent files', async () => { - const result = await DiskStorageService.updateFile( - 'fileId', - 'non-existent', - ); + const result = await storage.disk.updateFile('fileId', 'non-existent'); expect(result.success).toBe(false); expect(result.code).toBe(500); From 8c929f2337ddfb2c447002f242cbdeb6367998d5 Mon Sep 17 00:00:00 2001 From: michee-04 Date: Mon, 18 Nov 2024 08:56:17 +0000 Subject: [PATCH 5/7] feat: implementation du CRUD pour le fileService --- .env.example | 3 +- .../core/api/controllers/file.controller.ts | 77 +++--- src/apps/files/core/api/dtos/index.ts | 1 - .../core/api/dtos/request/file.create.ts | 5 - src/apps/files/core/api/dtos/request/index.ts | 1 - src/apps/files/core/api/index.ts | 1 - src/apps/files/core/api/routes/file.route.ts | 4 +- .../core/business/services/file.service.ts | 255 ++++++++++++------ .../files/core/domain/models/file.model.ts | 2 +- src/core/config/index.ts | 2 + src/modules/shared/storage/minio/index.ts | 4 +- 11 files changed, 221 insertions(+), 134 deletions(-) delete mode 100644 src/apps/files/core/api/dtos/index.ts delete mode 100644 src/apps/files/core/api/dtos/request/file.create.ts delete mode 100644 src/apps/files/core/api/dtos/request/index.ts diff --git a/.env.example b/.env.example index 0b91f1c..536e5f1 100644 --- a/.env.example +++ b/.env.example @@ -84,9 +84,10 @@ OTP_EXPIRATION=15 #DISK STORAGE DISK_STORAGE_UPLOAD_FOLDER=/home/michee/projects/system-api/stock -#FILES +#FILES_STORAGE FILE_STORES=disk,minio FILE_STORAGE=disk +MINIO_BUCKET=new-xflow-test CRYPTAGE_KEY=secret-key diff --git a/src/apps/files/core/api/controllers/file.controller.ts b/src/apps/files/core/api/controllers/file.controller.ts index 2f6f14b..2a68f78 100644 --- a/src/apps/files/core/api/controllers/file.controller.ts +++ b/src/apps/files/core/api/controllers/file.controller.ts @@ -1,3 +1,4 @@ +import FileService from 'apps/files/core/business/services/file.service'; import fs from 'fs'; /* eslint-disable @typescript-eslint/no-unused-vars */ import { @@ -5,10 +6,8 @@ import { ErrorResponseType, SuccessResponseType, } from '@nodesandbox/response-kit'; +import { IFileModel } from 'apps/files'; import { NextFunction, Request, Response } from 'express'; -import { decryptAES } from 'helpers'; -import fileService from '../../business/services/file.service'; -import { IFileModel } from '../../domain'; export class FileController { /** @@ -23,8 +22,10 @@ export class FileController { ): Promise { try { const payload = req.file; - // const fileService = new FileService(source:CONFIG.file) - const response = await fileService.createFile(payload); + const service = CONFIG.fs.defaultStore; + + const fileService = new FileService(); + const response = await fileService.createFIle(service, payload); if (!response.success) { throw response.error; } @@ -43,14 +44,20 @@ export class FileController { * @param res * @param next */ - static async getFilesDB( + static async getFileById( req: Request, res: Response, next: NextFunction, ): Promise { try { - const filters = req.query; - const response = await fileService.getFilesDB(filters); + const fileId = req.params.fileId; + const service = CONFIG.fs.defaultStore; + + const fileService = new FileService(); + const response = (await fileService.getFile( + service, + fileId, + )) as SuccessResponseType; if (response.success) { ApiResponse.success(res, response); @@ -58,30 +65,10 @@ export class FileController { throw response.error; } } catch (error) { - ApiResponse.error(res, error as ErrorResponseType); - } - } - - /** - * @param req - * @param res - * @param next - */ - static async getFileById( - req: Request, - res: Response, - next: NextFunction, - ): Promise { - try { - const fileId = req.params.fileId; - const response = await fileService.getFileDIsk(fileId); - if (!response.success) { - throw response.error; - } - - ApiResponse.success(res, response, 200); - } catch (error) { - ApiResponse.error(res, error as ErrorResponseType); + ApiResponse.error(res, { + success: false, + error: error, + } as ErrorResponseType); } } @@ -92,8 +79,11 @@ export class FileController { ): Promise { try { const fileId = req.params.fileId; + const service = CONFIG.fs.defaultStore; + const fileService = new FileService(); const response = (await fileService.sendFile( + service, fileId, )) as SuccessResponseType; @@ -101,6 +91,7 @@ export class FileController { throw response.error; } + // TODO Corriger le telechargement des vidéos res.writeHead(200, { 'content-type': response.document?.mimetype, 'content-length': response.document?.size, @@ -110,7 +101,10 @@ export class FileController { const file = fs.readFileSync(filepath); res.end(file); } catch (error) { - ApiResponse.error(res, error as ErrorResponseType); + ApiResponse.error(res, { + success: false, + error: error, + } as ErrorResponseType); } } @@ -119,23 +113,28 @@ export class FileController { * @param res * @param next */ - static async deleteFileAll( + static async deleteFile( req: Request, res: Response, next: NextFunction, ): Promise { try { const fileId = req.params.fileId; + const service = CONFIG.fs.defaultStore; - const response = await fileService.deleteFIle(fileId); + const fileService = new FileService(); + const response = await fileService.deleteFile(service, fileId); - if (response.success) { - ApiResponse.success(res, response); - } else { + if (!response.success) { throw response.error; } + + ApiResponse.success(res, response, 201); } catch (error) { - ApiResponse.error(res, error as ErrorResponseType); + ApiResponse.error(res, { + success: false, + error: error, + } as ErrorResponseType); } } } diff --git a/src/apps/files/core/api/dtos/index.ts b/src/apps/files/core/api/dtos/index.ts deleted file mode 100644 index 56e4b05..0000000 --- a/src/apps/files/core/api/dtos/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './request'; diff --git a/src/apps/files/core/api/dtos/request/file.create.ts b/src/apps/files/core/api/dtos/request/file.create.ts deleted file mode 100644 index 6f610c7..0000000 --- a/src/apps/files/core/api/dtos/request/file.create.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Joi, { ObjectSchema } from 'joi'; - -export const CreateFileRequestSchema: ObjectSchema = Joi.object({ - file: Joi.binary().required(), -}); diff --git a/src/apps/files/core/api/dtos/request/index.ts b/src/apps/files/core/api/dtos/request/index.ts deleted file mode 100644 index 47a2e25..0000000 --- a/src/apps/files/core/api/dtos/request/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './file.create'; diff --git a/src/apps/files/core/api/index.ts b/src/apps/files/core/api/index.ts index 54a23ca..8bd7daa 100644 --- a/src/apps/files/core/api/index.ts +++ b/src/apps/files/core/api/index.ts @@ -1,4 +1,3 @@ /* eslint-disable prettier/prettier */ export * from './controllers'; -export * from './dtos'; export * from './routes'; diff --git a/src/apps/files/core/api/routes/file.route.ts b/src/apps/files/core/api/routes/file.route.ts index 0684569..5fb7c23 100644 --- a/src/apps/files/core/api/routes/file.route.ts +++ b/src/apps/files/core/api/routes/file.route.ts @@ -8,12 +8,10 @@ const upload = multer(); router.post('/', upload.single('file'), FileController.createFile); -router.get('/', FileController.getFilesDB); - router.get('/:fileId', FileController.getFileById); router.get('/:fileId/download', FileController.downloadFile); -router.delete('/:fileId', FileController.deleteFileAll); +router.delete('/:fileId', FileController.deleteFile); export default router; diff --git a/src/apps/files/core/business/services/file.service.ts b/src/apps/files/core/business/services/file.service.ts index b0e6290..036629d 100644 --- a/src/apps/files/core/business/services/file.service.ts +++ b/src/apps/files/core/business/services/file.service.ts @@ -5,7 +5,7 @@ import { ErrorResponseType, SuccessResponseType, } from '@nodesandbox/response-kit'; -import { decryptAES, parseSortParam } from 'helpers'; +import { decryptAES, encryptAES } from 'helpers'; import { storage } from 'modules/shared/storage'; import { FileModel, IFileModel } from '../../domain'; import { FileRepository } from '../repositories'; @@ -19,24 +19,56 @@ class FileService extends BaseService { this.searchFields = ['name', 'extension', 'size', 'type']; } - async createFile(file: any) { + async createFIle( + service: any, + file: any, + ): Promise | ErrorResponseType> { try { - const meta = file; - const buffer = file.buffer; + if (service !== CONFIG.minio.host) { + const meta = file; + const buffer = file.buffer; - const payload = await storage.disk.uploadFile(buffer); + const payload = await storage.disk.uploadFile(buffer); - const insertFile = { - hash: payload.data.hash, - size: payload.data.size, - type: payload.data.type, - extension: payload.data.extension, - metadata: meta, - }; + const insertFile = { + hash: payload.data.hash, + size: payload.data.size, + type: payload.data.type, + extension: payload.data.extension, + metadata: meta, + }; + + const response = await this.repository.create(insertFile); - const response = await this.repository.create(insertFile); + return { success: true, document: response }; + } else { + const meta = file; - return { success: true, document: response }; + await storage.minio.createBucket(CONFIG.minio.bucketName); + const payload = await storage.minio.uploadBuffer( + CONFIG.minio.bucketName, + meta.originalname, + meta.buffer, + { ...meta, buffer: undefined }, + ); + if (!payload.success) { + throw payload.error; + } + const hashedName = encryptAES( + meta.originalname, + process.env.CRYPTAGE_KEY || 'secret-key', + ); + const insertFile = { + hash: hashedName, + size: meta.size, + type: meta.mimetype, + metadata: meta, + url: payload.data, + }; + const response = await this.repository.create(insertFile); + + return { success: true, document: response }; + } } catch (error) { return { success: false, @@ -51,77 +83,112 @@ class FileService extends BaseService { } } - async getFilesDB( - filters: Record, - ): Promise | ErrorResponseType> { - const { - page = 1, - limit = 10, - sort, - search = '', - name, - type, - extension, - } = filters; - - const query: any = {}; - if (name) query.name = name; - if (type) query.type = type; - if (extension) query.extension = extension; - - const sortObject = sort ? parseSortParam(sort) : {}; - - return this.findAll({ - query, - sort: sortObject, - page: parseInt(page), - limit: parseInt(limit), - searchTerm: search as string, - }); - } - - async getFileDIsk( + async getFile( + service: any, fileId: any, ): Promise | ErrorResponseType> { - const payload = await this.repository.findOne({ _id: fileId }); - // TODO Gerer les erreurs liés au fichier introuvable avec (if) apres la modification du package ErrorResponseType + try { + if (service !== CONFIG.minio.host) { + const payload = await this.repository.findOne({ _id: fileId }); + // TODO Gerer les erreurs liés au fichier introuvable avec (if) apres la modification du package ErrorResponseType + + const hash = payload?.hash as string; + + const fileDiskName = decryptAES( + hash, + process.env.CRYPTAGE_KEY || 'secret-key', + ); - const hash = payload?.hash as string; + const response = await storage.disk.getFile(fileDiskName); - const fileDiskName = decryptAES( - hash, - process.env.CRYPTAGE_KEY || 'secret-key', - ); + return { success: true, document: response }; + } else { + console.log('❌❌❌❌❌❌'); + const file = await this.repository.findOne({ _id: fileId }); - const response = await storage.disk.getFile(fileDiskName); + if (!file) { + throw file; + } - return { success: true, document: response }; + file.originalname = ( + file.metadata as { originalname: string } + ).originalname; + const { originalname } = file; + + const payload = await storage.minio.getFileStats( + CONFIG.minio.bucketName, + originalname, + ); + + return { success: true, document: payload }; + } + } catch (error) { + return { + success: false, + error: + error instanceof ErrorResponse + ? error + : new ErrorResponse( + 'INTERNAL_SERVER_ERROR', + (error as Error).message, + ), + }; + } } async sendFile( + service: any, fileId: any, ): Promise | ErrorResponseType> { try { - const file = await this.repository.findOne({ _id: fileId }); + if (service !== CONFIG.minio.host) { + const file = await this.repository.findOne({ _id: fileId }); - // TODO Gerer les erreurs liés au fichier introuvable avec (if) apres la modification du package ErrorResponseType - if (!file) { - throw file; - } - const fileDiskName = decryptAES( - file.hash, - process.env.CRYPTAGE_KEY || 'secret-key', - ); + // TODO Gerer les erreurs liés au fichier introuvable avec (if) apres la modification du package ErrorResponseType + if (!file) { + throw file; + } + const fileDiskName = decryptAES( + file.hash, + process.env.CRYPTAGE_KEY || 'secret-key', + ); - const response = await storage.disk.getFile(fileDiskName); + const response = await storage.disk.getFile(fileDiskName); + if (!response.success) { + throw response.error; + } - if (!response.success) { - throw response.error; - } + response.data.mimetype = ( + file.metadata as { mimetype: string } + ).mimetype; + + return { success: true, document: response.data }; + } else { + const file = await this.repository.findOne({ _id: fileId }); + + if (!file) { + throw file; + } + + file.originalname = ( + file.metadata as { originalname: string } + ).originalname; + const { originalname } = file; - response.data.mimetype = (file.metadata as { mimetype: string }).mimetype; + const payload = await storage.minio.downloadFile( + CONFIG.minio.bucketName, + originalname, + file.url, + ); - return { success: true, document: response.data }; + const response = { + path: payload.data?.path, + mimetype: file.type, + size: file.size, + } as any; + + return { success: true, document: response }; + } } catch (error) { return { success: false, @@ -136,25 +203,53 @@ class FileService extends BaseService { } } - async deleteFIle( + async deleteFile( + service: any, fileId: any, ): Promise | ErrorResponseType> { try { - const payload = await this.repository.findOne({ _id: fileId }); - // TODO Gerer les erreurs liés au fichier introuvable avec (if) apres la modification du package ErrorResponseType + if (service !== CONFIG.minio.host) { + const payload = await this.repository.findOne({ _id: fileId }); + + // TODO Gerer les erreurs liés au fichier introuvable avec (if) apres la modification du package ErrorResponseType + + const hash = payload?.hash as string; + + const fileDiskName = decryptAES( + hash, + process.env.CRYPTAGE_KEY || 'secret-key', + ); - const hash = payload?.hash as string; + await storage.disk.deleteFile(fileDiskName); - const fileDiskName = decryptAES( - hash, - process.env.CRYPTAGE_KEY || 'secret-key', - ); + await this.repository.delete({ _id: fileId }); - await storage.disk.deleteFile(fileDiskName); + return { success: true }; + } else { + const file = await this.repository.findOne({ _id: fileId }); - await this.repository.delete({ _id: fileId }); + if (!file) { + throw file; + } - return { success: true }; + file.originalname = ( + file.metadata as { originalname: string } + ).originalname; + const { originalname } = file; + + const payload = await storage.minio.deleteSingleFile( + CONFIG.minio.bucketName, + originalname, + ); + + if (!payload) { + throw payload; + } + + await this.repository.delete({ _id: fileId }); + + return { success: true }; + } } catch (error) { return { success: false, @@ -170,4 +265,4 @@ class FileService extends BaseService { } } -export default new FileService(); +export default FileService; diff --git a/src/apps/files/core/domain/models/file.model.ts b/src/apps/files/core/domain/models/file.model.ts index ba50e03..b83a3d2 100644 --- a/src/apps/files/core/domain/models/file.model.ts +++ b/src/apps/files/core/domain/models/file.model.ts @@ -40,7 +40,7 @@ const fileSchema = createBaseSchema( }, extension: { type: String, - required: true, + required: false, }, size: { type: Number, diff --git a/src/core/config/index.ts b/src/core/config/index.ts index 906cd8e..237d85f 100644 --- a/src/core/config/index.ts +++ b/src/core/config/index.ts @@ -49,6 +49,7 @@ export interface Config { apiPort: number; consolePort: number; useSSL: boolean; + bucketName: string; }; mail: { host: string; @@ -138,6 +139,7 @@ export class ConfigService { apiPort: parseInt(process.env.MINIO_API_PORT || '9000', 10), consolePort: parseInt(process.env.MINIO_EXT_CONSOLE_PORT || '5050', 10), useSSL: process.env.MINIO_USE_SSL === 'true', + bucketName: process.env.MINIO_BUCKET || 'my-new-bucket', }, mail: { host: diff --git a/src/modules/shared/storage/minio/index.ts b/src/modules/shared/storage/minio/index.ts index 8a6f559..b94f951 100644 --- a/src/modules/shared/storage/minio/index.ts +++ b/src/modules/shared/storage/minio/index.ts @@ -1,6 +1,6 @@ -import { Client, BucketItem, ItemBucketMetadata, CopyConditions } from 'minio'; -import { Readable } from 'stream'; +import { BucketItem, Client, CopyConditions, ItemBucketMetadata } from 'minio'; import * as path from 'path'; +import { Readable } from 'stream'; import { BucketPolicy, FileStats } from './types'; export class MinioStorageService { From 3490158d093fa78412326ced400abfae7259ea85 Mon Sep 17 00:00:00 2001 From: michee-04 Date: Mon, 18 Nov 2024 08:57:05 +0000 Subject: [PATCH 6/7] update: modification du createTodo pour ajouter upload de l'image --- .env.example | 2 +- src/apps/demo/core/api/controllers/todo.controller.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 536e5f1..b4d4c7e 100644 --- a/.env.example +++ b/.env.example @@ -86,7 +86,7 @@ DISK_STORAGE_UPLOAD_FOLDER=/home/michee/projects/system-api/stock #FILES_STORAGE FILE_STORES=disk,minio -FILE_STORAGE=disk +FILE_STORAGE=minio MINIO_BUCKET=new-xflow-test CRYPTAGE_KEY=secret-key diff --git a/src/apps/demo/core/api/controllers/todo.controller.ts b/src/apps/demo/core/api/controllers/todo.controller.ts index 572c543..f0e20c9 100644 --- a/src/apps/demo/core/api/controllers/todo.controller.ts +++ b/src/apps/demo/core/api/controllers/todo.controller.ts @@ -3,9 +3,11 @@ import { ApiResponse, ErrorResponse, ErrorResponseType, + SuccessResponseType, } from '@nodesandbox/response-kit'; import { TodoService } from 'apps/demo/core/business'; -import fileService from 'apps/files/core/business/services/file.service'; +import { IFileModel } from 'apps/files'; +import FileService from 'apps/files/core/business/services/file.service'; import { NextFunction, Request, Response } from 'express'; import { sanitize } from 'helpers'; import { CreateTodoRequestSchema } from '../dtos'; @@ -28,12 +30,17 @@ export class TodoController { try { const _payload = sanitize(req.body, CreateTodoRequestSchema); const image = req.file; + const service = CONFIG.fs.defaultStore; if (!_payload.success) { throw _payload.error; } - const todoImage = await fileService.createFile(image); + const fileService = new FileService(); + const todoImage = (await fileService.createFIle( + service, + image, + )) as SuccessResponseType; _payload.data.image = todoImage.document?._id; From 15b9a8cfef80179553bcbd7afcc3583d01cb32ca Mon Sep 17 00:00:00 2001 From: michee-04 Date: Fri, 10 Jan 2025 09:07:47 +0000 Subject: [PATCH 7/7] refactor: delete console.log --- .../core/api/controllers/todo.controller.ts | 3 +++ .../core/business/services/todo.service.ts | 19 ++++++++++++++----- .../core/business/services/file.service.ts | 5 ++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/apps/demo/core/api/controllers/todo.controller.ts b/src/apps/demo/core/api/controllers/todo.controller.ts index f0e20c9..126b9d8 100644 --- a/src/apps/demo/core/api/controllers/todo.controller.ts +++ b/src/apps/demo/core/api/controllers/todo.controller.ts @@ -72,6 +72,9 @@ export class TodoController { ): Promise { try { const filters = req.query; // Extract query params for filtering. + + console.log('⚡⚡⚡⚡☂️☂️☂️☂️ filters : ', filters); + const response = await TodoService.getTodos(filters); if (response.success) { diff --git a/src/apps/demo/core/business/services/todo.service.ts b/src/apps/demo/core/business/services/todo.service.ts index 9e9793c..8b7f888 100644 --- a/src/apps/demo/core/business/services/todo.service.ts +++ b/src/apps/demo/core/business/services/todo.service.ts @@ -1,11 +1,11 @@ -import { TodoRepository } from '../repositories'; -import { ITodoModel, TodoModel } from 'apps/demo/core/domain'; -import { parseSortParam } from 'helpers'; +import { BaseService } from '@nodesandbox/repo-framework'; import { ErrorResponseType, SuccessResponseType, } from '@nodesandbox/response-kit'; -import { BaseService } from '@nodesandbox/repo-framework'; +import { ITodoModel, TodoModel } from 'apps/demo/core/domain'; +import { parseSortParam } from 'helpers'; +import { TodoRepository } from '../repositories'; class TodoService extends BaseService { constructor() { @@ -13,7 +13,7 @@ class TodoService extends BaseService { super(todoRepo, true, [ /*'attribute_to_populate'*/ ]); // This will populate the entity field - this.allowedFilterFields = ['dueDate', 'completed', 'priority']; // To filter on these fields, we need to set this + this.allowedFilterFields = ['dueDate', 'completed', 'priority', 'image']; // To filter on these fields, we need to set this this.searchFields = ['title', 'description']; // This will use the search keyword /** @@ -34,6 +34,7 @@ class TodoService extends BaseService { search = '', priority, completed, + image, upcoming, } = filters; @@ -42,6 +43,12 @@ class TodoService extends BaseService { if (priority) query.priority = priority; if (completed !== undefined) query.completed = completed === 'true'; + if (image !== 'true') { + query.image = { $exists: false }; + } else { + query.image = { $exists: true, $ne: null }; + } + // Handle upcoming due dates if (upcoming) { const days = parseInt(upcoming as string) || 7; @@ -50,6 +57,8 @@ class TodoService extends BaseService { query.dueDate = { $gte: new Date(), $lte: futureDate }; } + console.log('⚔️⚔️⚔️⚔️⚔️⚔️ query : ', query); + // Parse sorting parameter using helper function const sortObject = sort ? parseSortParam(sort) : {}; diff --git a/src/apps/files/core/business/services/file.service.ts b/src/apps/files/core/business/services/file.service.ts index 036629d..a40f114 100644 --- a/src/apps/files/core/business/services/file.service.ts +++ b/src/apps/files/core/business/services/file.service.ts @@ -101,9 +101,8 @@ class FileService extends BaseService { const response = await storage.disk.getFile(fileDiskName); - return { success: true, document: response }; + return { success: true, document: response.data }; } else { - console.log('❌❌❌❌❌❌'); const file = await this.repository.findOne({ _id: fileId }); if (!file) { @@ -120,7 +119,7 @@ class FileService extends BaseService { originalname, ); - return { success: true, document: payload }; + return { success: true, document: payload.data }; } } catch (error) { return {