diff --git a/src/data/data-sources/sqlite/sqlite-task-data-source.ts b/src/data/data-sources/sqlite/sqlite-task-data-source.ts index 07b0ae7..f49cfee 100644 --- a/src/data/data-sources/sqlite/sqlite-task-data-source.ts +++ b/src/data/data-sources/sqlite/sqlite-task-data-source.ts @@ -78,9 +78,11 @@ export class SQLiteTaskDataSource implements TaskDataSource { INSERT OR IGNORE INTO task_type (task_type_label) VALUES ('EXPORT'), + ('EXPORT_BACKUP'), ('DELETE'), ('UPDATE'), ('IMPORT'), + ('IMPORT_BACKUP'), ('IMPORT_CTD'), ('IMPORT_ECO_TAXA'); `; @@ -330,7 +332,7 @@ export class SQLiteTaskDataSource implements TaskDataSource { // generate sql and params for (const [key, value] of Object.entries(task)) { params.push(value) - placeholders = placeholders + key + "=(?) AND " + placeholders = placeholders + "task." + key + "=(?) AND " } // remove last AND placeholders = placeholders.slice(0, -4); diff --git a/src/domain/entities/task.ts b/src/domain/entities/task.ts index b68f0f7..c6067b5 100644 --- a/src/domain/entities/task.ts +++ b/src/domain/entities/task.ts @@ -12,6 +12,8 @@ export enum TaskType { Delete = "DELETE", Update = "UPDATE", Import = "IMPORT", + Import_Backup = "IMPORT_BACKUP", + Export_Backup = "EXPORT_BACKUP", Import_CTD = "IMPORT_CTD", Import_EcoTaxa = "IMPORT_ECO_TAXA", } @@ -99,8 +101,8 @@ export interface TaskResponseModel { export interface PrivateTaskRequestModel { task_id?: number; - task_type_id?: TaskType; - task_status_id?: TasksStatus; + task_type_id?: number; + task_status_id?: number; task_owner_id?: number; task_project_id?: number; task_params?: object; diff --git a/src/domain/interfaces/repositories/project-repository.ts b/src/domain/interfaces/repositories/project-repository.ts index 6a75c3c..58a78a4 100644 --- a/src/domain/interfaces/repositories/project-repository.ts +++ b/src/domain/interfaces/repositories/project-repository.ts @@ -13,4 +13,6 @@ export interface ProjectRepository { computeDefaultDepthOffset(instrument_model: string): number | undefined; deleteProject(project: ProjectRequestModel): Promise; standardGetProjects(options: PreparedSearchOptions): Promise>; + ensureFolderStructureForBackup(root_folder_path: string): Promise; + copyL0bToProjectFolder(source_folder: string, dest_folder: string, skip_already_imported: boolean): Promise; } \ No newline at end of file diff --git a/src/domain/interfaces/use-cases/project/backup-project.ts b/src/domain/interfaces/use-cases/project/backup-project.ts new file mode 100644 index 0000000..a89a820 --- /dev/null +++ b/src/domain/interfaces/use-cases/project/backup-project.ts @@ -0,0 +1,6 @@ +import { TaskResponseModel } from "../../../entities/task"; +import { UserUpdateModel } from "../../../entities/user"; + +export interface BackupProjectUseCase { + execute(current_user: UserUpdateModel, project_id: number, skip_already_imported: boolean): Promise; +} \ No newline at end of file diff --git a/src/domain/repositories/project-repository.ts b/src/domain/repositories/project-repository.ts index 7dec6e2..6445265 100644 --- a/src/domain/repositories/project-repository.ts +++ b/src/domain/repositories/project-repository.ts @@ -6,7 +6,12 @@ import { ProjectRequestCreationModel, ProjectRequestModel, ProjectUpdateModel, P import { PreparedSearchOptions, SearchResult } from "../entities/search"; import { ProjectRepository } from "../interfaces/repositories/project-repository"; -import { promises as fs } from 'fs'; +import * as fsPromises from 'fs/promises'; // For promise-based file operations//import fs from 'fs'; // Correct import for `createReadStream` +import * as fs from 'fs'; // For createWriteStream + +import archiver from 'archiver'; + +import path from "path"; export class ProjectRepositoryImpl implements ProjectRepository { projectDataSource: ProjectDataSource @@ -14,6 +19,7 @@ export class ProjectRepositoryImpl implements ProjectRepository { // TODO move to a search repository order_by_allow_params: string[] = ["asc", "desc"] filter_operator_allow_params: string[] = ["=", ">", "<", ">=", "<=", "<>", "IN", "LIKE"] + base_folder = path.join(__dirname, '..', '..', '..'); constructor(projectDataSource: ProjectDataSource) { this.projectDataSource = projectDataSource @@ -186,13 +192,160 @@ export class ProjectRepositoryImpl implements ProjectRepository { async createProjectRootFolder(root_folder_path: string): Promise { try { // Check if the folder exists - await fs.access(root_folder_path); + await fsPromises.access(root_folder_path); // If it exists, remove it recursively - await fs.rm(root_folder_path, { recursive: true, force: true }); + await fsPromises.rm(root_folder_path, { recursive: true, force: true }); } catch (error) { // Folder does not exist; no need to delete anything } // Create the root folder - await fs.mkdir(root_folder_path, { recursive: true }); + await fsPromises.mkdir(root_folder_path, { recursive: true }); + } + + async ensureFolderStructureForBackup(root_folder_path: string): Promise { + // Ensure /raw, /meta, /config EXISTS + const foldersTocheck = ['raw', 'meta', 'config']; + + for (const folder of foldersTocheck) { + const folderPath = path.join(this.base_folder, root_folder_path, folder); + try { + await fsPromises.access(folderPath); + } catch (error) { + throw new Error(`Folder does not exist at path: ${folderPath}`); + } + } } + + async copyL0bToProjectFolder(source_folder: string, dest_folder: string, skip_already_imported: boolean): Promise { + // Create destination folder if does not exist + await fsPromises.mkdir(path.join(this.base_folder, dest_folder), { recursive: true }); + + // Copy meta/*, and config/*. + await this.copy_metadata(this.base_folder, source_folder, dest_folder); + + // Copy L0b files + if (skip_already_imported === true) { + await this.copyNewL0bFolders(this.base_folder, source_folder, dest_folder); + } else { + await this.copyAllL0bFolders(this.base_folder, source_folder, dest_folder); + } + } + + async copy_metadata(base_folder: string, source_folder: string, dest_folder: string): Promise { + const foldersToCopy = [ + { source: 'meta', dest: 'meta' }, + { source: 'config', dest: 'config' } + ]; + + for (const folder of foldersToCopy) { + const sourcePath = path.join(base_folder, source_folder, folder.source); + const destPath = path.join(base_folder, dest_folder, folder.dest); + const oldDestPath = path.join(base_folder, dest_folder, `old_${folder.dest}`); + + // Ensure the destination folder exists + await fsPromises.mkdir(destPath, { recursive: true }); + + // Rename dest folder to old_{folder.dest} + await fsPromises.rename(destPath, oldDestPath); + try { + // Copy source folder to dest folder using cp + await fsPromises.cp(sourcePath, destPath, { recursive: true }); + } catch (error) { + // If an error occurs, restore the old_{folder.dest} + await fsPromises.rename(oldDestPath, destPath); + throw error; + } + + // If everything is ok, remove old_{folder.dest} + await fsPromises.rm(oldDestPath, { recursive: true, force: true }); + } + } + + + async copyAllL0bFolders(base_folder: string, source_folder: string, dest_folder: string): Promise { + const sourcePath = path.join(base_folder, source_folder, 'raw'); + const destPath = path.join(base_folder, dest_folder, 'raw'); + + // Ensure the destination folder exists + await fsPromises.mkdir(destPath, { recursive: true }); + + // Read all subfolders in the source folder + const subfolders = await fsPromises.readdir(sourcePath, { withFileTypes: true }); + + for (const entry of subfolders) { + if (entry.isDirectory()) { + const sourceSubfolder = path.join(sourcePath, entry.name); + const destZipFile = path.join(destPath, `${entry.name}.zip`); + + // Zip the folder and write to the destination + await this.zipFolder(sourceSubfolder, destZipFile); + } + } + } + + async zipFolder(sourceFolder: string, destZipFile: string): Promise { + // Create a file output stream for the destination zip file + const output = fs.createWriteStream(destZipFile); + + // Create a new archiver instance to create a zip file + const archive = archiver('zip', { + zlib: { level: 9 } // Maximum compression + }); + + // Pipe the archiver output to the file stream + output.on('close', () => { + //console.log(`Archive ${destZipFile} has been created, ${archive.pointer()} total bytes.`); + }); + + archive.on('error', (err) => { + throw err; + }); + + archive.pipe(output); + + // Append all files from the source folder to the archive + archive.directory(sourceFolder, false); // Second argument 'false' keeps directory structure intact + + // Finalize the archive (this will compress the data) + await archive.finalize(); + } + + async copyNewL0bFolders(base_folder: string, source_folder: string, dest_folder: string): Promise { + const sourcePath = path.join(base_folder, source_folder, 'raw'); + const destPath = path.join(base_folder, dest_folder, 'raw'); + + // Ensure the destination folder exists + await fsPromises.mkdir(destPath, { recursive: true }); + + // Read all subfolders in the source folder + const subfolders = await fsPromises.readdir(sourcePath, { withFileTypes: true }); + + for (const entry of subfolders) { + if (entry.isDirectory()) { + const sourceSubfolder = path.join(sourcePath, entry.name); + const destSubfolder = path.join(destPath, `${entry.name}.zip`); + + // Check if the subfolder is already copied (check if the zip file exists) + const zipExists = await this.checkFileExists(destSubfolder); + if (!zipExists) { + // If not, copy and zip the folder + //console.log(`Copying new folder: ${entry.name}`); + await this.zipFolder(sourceSubfolder, destSubfolder); + } + // else { + // console.log(`Skipping already existing folder: ${entry.name}`); + // } + } + } + } + + async checkFileExists(filePath: string): Promise { + try { + await fsPromises.access(filePath); + return true; // File exists + } catch { + return false; // File does not exist + } + } + } \ No newline at end of file diff --git a/src/domain/repositories/sample-repository.ts b/src/domain/repositories/sample-repository.ts index 37a7c65..6c5bb9a 100644 --- a/src/domain/repositories/sample-repository.ts +++ b/src/domain/repositories/sample-repository.ts @@ -30,6 +30,7 @@ export class SampleRepositoryImpl implements SampleRepository { // TODO move to a search repository order_by_allow_params: string[] = ["asc", "desc"] filter_operator_allow_params: string[] = ["=", ">", "<", ">=", "<=", "<>", "IN", "LIKE"] + base_folder = path.join(__dirname, '..', '..', '..'); constructor(sampleDataSource: SampleDataSource, DATA_STORAGE_FS_STORAGE: string) { this.sampleDataSource = sampleDataSource @@ -894,18 +895,17 @@ export class SampleRepositoryImpl implements SampleRepository { } } async UVP5copySamplesToImportFolder(source_folder: string, dest_folder: string, samples_names_to_import: string[]): Promise { - const base_folder = path.join(__dirname, '..', '..', '..'); // Ensure that none of the samples folder already exists - await this.ensureSampleFolderDoNotExists(samples_names_to_import, path.join(base_folder, dest_folder)); + await this.ensureSampleFolderDoNotExists(samples_names_to_import, path.join(this.base_folder, dest_folder)); // Create destination folder - await fsPromises.mkdir(path.join(base_folder, dest_folder), { recursive: true }); + await fsPromises.mkdir(path.join(this.base_folder, dest_folder), { recursive: true }); // Iterate over each sample name, create the sample folder, copy files, and zip the folder for (const sample of samples_names_to_import) { - const sourcePath = path.join(base_folder, source_folder); - const destPath = path.join(base_folder, dest_folder, sample); + const sourcePath = path.join(this.base_folder, source_folder); + const destPath = path.join(this.base_folder, dest_folder, sample); // Ensure the destination sample folder exists await fsPromises.mkdir(destPath, { recursive: true }); @@ -935,7 +935,7 @@ export class SampleRepositoryImpl implements SampleRepository { } // Zip the sample folder - const zipFilePath = path.join(base_folder, dest_folder, `${sample}.zip`); + const zipFilePath = path.join(this.base_folder, dest_folder, `${sample}.zip`); try { await this.zipFolder(destPath, zipFilePath); @@ -962,18 +962,16 @@ export class SampleRepositoryImpl implements SampleRepository { } async UVP6copySamplesToImportFolder(source_folder: string, dest_folder: string, samples_names_to_import: string[]): Promise { - - const base_folder = path.join(__dirname, '..', '..', '..'); // Ensure that non of the samples folder already exists - await this.ensureSampleFolderDoNotExists(samples_names_to_import, path.join(base_folder, dest_folder)); + await this.ensureSampleFolderDoNotExists(samples_names_to_import, path.join(this.base_folder, dest_folder)); // Ensure destination folder exists - await fsPromises.mkdir(path.join(base_folder, dest_folder), { recursive: true }); + await fsPromises.mkdir(path.join(this.base_folder, dest_folder), { recursive: true }); // Iterate over each sample name and copy .zip files only for (const sample of samples_names_to_import) { - const sourcePath = path.join(base_folder, source_folder, sample); - const destPath = path.join(base_folder, dest_folder, sample); + const sourcePath = path.join(this.base_folder, source_folder, sample); + const destPath = path.join(this.base_folder, dest_folder, sample); // Check if the sample directory exists and list files const files = await fsPromises.readdir(sourcePath); @@ -993,12 +991,10 @@ export class SampleRepositoryImpl implements SampleRepository { } async deleteSamplesFromImportFolder(dest_folder: string, samples_names_to_import: string[]): Promise { - const base_folder = path.join(__dirname, '..', '..', '..'); - for (const sample of samples_names_to_import) { // Construct paths for the .zip file and the folder - const zipPath = path.join(base_folder, dest_folder, `${sample}.zip`); - const folderPath = path.join(base_folder, dest_folder, sample); + const zipPath = path.join(this.base_folder, dest_folder, `${sample}.zip`); + const folderPath = path.join(this.base_folder, dest_folder, sample); // Delete the .zip file await fsPromises.rm(zipPath, { force: true }); diff --git a/src/domain/repositories/task-repository.ts b/src/domain/repositories/task-repository.ts index a214c43..0ca38c8 100644 --- a/src/domain/repositories/task-repository.ts +++ b/src/domain/repositories/task-repository.ts @@ -20,6 +20,8 @@ export class TaskRepositoryImpl implements TaskRepository { // TODO move to a search repository order_by_allow_params: string[] = ["asc", "desc"] filter_operator_allow_params: string[] = ["=", ">", "<", ">=", "<=", "<>", "IN", "LIKE"] + base_folder = path.join(__dirname, '..', '..', '..'); + constructor(taskDataSource: TaskDataSource, fs: FsWrapper, DATA_STORAGE_FOLDER: string) { this.taskDataSource = taskDataSource @@ -62,7 +64,7 @@ export class TaskRepositoryImpl implements TaskRepository { const result = await this.taskDataSource.create(private_task) //create log file based on created task_id - const log_file_path = path.join(__dirname, '..', '..', '..', this.DATA_STORAGE_FOLDER, "tasks_log", `task_${result}.log`) + const log_file_path = path.join(this.base_folder, this.DATA_STORAGE_FOLDER, "tasks_log", `task_${result}.log`) // create log file try { diff --git a/src/domain/use-cases/project/backup-project.ts b/src/domain/use-cases/project/backup-project.ts new file mode 100644 index 0000000..3aa01d3 --- /dev/null +++ b/src/domain/use-cases/project/backup-project.ts @@ -0,0 +1,161 @@ + +import { UserUpdateModel } from "../../entities/user"; +import { PrivilegeRepository } from "../../interfaces/repositories/privilege-repository"; +import { ProjectRepository } from "../../interfaces/repositories/project-repository"; +import { UserRepository } from "../../interfaces/repositories/user-repository"; +import { TaskRepository } from "../../interfaces/repositories/task-repository"; + +import { BackupProjectUseCase } from "../../interfaces/use-cases/project/backup-project"; +import { ProjectResponseModel } from "../../entities/project"; +import { PrivateTaskRequestModel, PublicTaskRequestCreationModel, TaskResponseModel, TasksStatus, TaskType } from "../../entities/task"; +import { PreparedSearchOptions } from "../../entities/search"; +import path from "path"; + +export class BackupProject implements BackupProjectUseCase { + userRepository: UserRepository + privilegeRepository: PrivilegeRepository + projectRepository: ProjectRepository + taskRepository: TaskRepository + DATA_STORAGE_FS_STORAGE: string + + constructor(userRepository: UserRepository, privilegeRepository: PrivilegeRepository, projectRepository: ProjectRepository, taskRepository: TaskRepository, DATA_STORAGE_FS_STORAGE: string) { + this.userRepository = userRepository + this.privilegeRepository = privilegeRepository + this.projectRepository = projectRepository + this.taskRepository = taskRepository + this.DATA_STORAGE_FS_STORAGE = DATA_STORAGE_FS_STORAGE + } + + async execute(current_user: UserUpdateModel, project_id: number, skip_already_imported: boolean): Promise { + + // Ensure the user is valid and can be used + await this.userRepository.ensureUserCanBeUsed(current_user.user_id); + + // Ensure the current user has permission to get the project importable samples + await this.ensureUserCanGet(current_user, project_id); + + // Get the project + const project: ProjectResponseModel = await this.getProjectIfExist(project_id); + + // Create a task to import samples + const task_id = await this.createBackupProjectTask(current_user, project, skip_already_imported); + + // get the task + const task = await this.taskRepository.getOneTask({ task_id: task_id }); + if (!task) { + throw new Error("Cannot find task"); + } + + // start the task + this.startBackupProjectTask(task, project.instrument_model, project, current_user, skip_already_imported); + + return task; + } + + async createBackupProjectTask(current_user: UserUpdateModel, project: ProjectResponseModel, skip_already_imported: boolean): Promise { + const task: PublicTaskRequestCreationModel = { + task_type: TaskType.Import_Backup, + task_status: TasksStatus.Pending, + task_owner_id: current_user.user_id, + task_project_id: project.project_id, + task_params: { + root_folder_path: project.root_folder_path, + skip_already_imported: skip_already_imported + } + } + + return await this.taskRepository.createTask(task); + } + + + private async getProjectIfExist(project_id: number): Promise { + const project = await this.projectRepository.getProject({ project_id: project_id }); + if (!project) { + throw new Error("Cannot find project"); + } + return project; + } + + private async ensureUserCanGet(current_user: UserUpdateModel, project_id: number): Promise { + const userIsAdmin = await this.userRepository.isAdmin(current_user.user_id); + const userHasPrivilege = await this.privilegeRepository.isGranted({ + user_id: current_user.user_id, + project_id: project_id + }); + if (!userIsAdmin && !userHasPrivilege) { + throw new Error("Logged user cannot list importable samples in this project"); + } + } + + private async startBackupProjectTask(task: TaskResponseModel, instrument_model: string, project: ProjectResponseModel, current_user: UserUpdateModel, skip_already_imported: boolean) { + const task_id = task.task_id; + try { + await this.taskRepository.startTask({ task_id: task_id }); + + // 1/3 Do validation before importing 0->15% + await this.ensureNoExportBackupIsRunning(project, task_id); + // 2/3 Do validation before importing 15->25% + await this.ensureFolderStructureForBackup(project.root_folder_path, task_id); + + // 3/3 Copy source files to hiden project folder 25->100% + await this.copySourcesToBackupProjectFolder(task_id, project, skip_already_imported); + + // finish task + await this.taskRepository.finishTask({ task_id: task_id }); + } catch (error) { + await this.taskRepository.failedTask(task_id, error); + return; + } + } + async ensureFolderStructureForBackup(root_folder_path: string, task_id: number) { + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 15, "Step 2/3 Validation, ensure folder structure is correct: start"); + await this.projectRepository.ensureFolderStructureForBackup(root_folder_path); + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 25, "Step 2/3 Validation, ensure folder structure is correct: done"); + } + + async ensureNoExportBackupIsRunning(project: ProjectResponseModel, task_id: number) { + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 0, "Step 1/3 Validation, no backup is running: start"); + + const options_type: PreparedSearchOptions = { + filter: [ + { field: 'task_type_label', operator: '=', value: TaskType.Export_Backup } + ], + sort_by: [], + page: 1, + limit: 1 + } + const option_status: PreparedSearchOptions = { + filter: [ + { field: 'task_status_label', operator: '=', value: TasksStatus.Running } + ], + sort_by: [], + page: 1, + limit: 1 + } + const task_type_id = (await this.taskRepository.standardGetTaskType(options_type)).items[0].task_type_id; + const task_status_id = (await this.taskRepository.standardGetTaskStatus(option_status)).items[0].task_status_id; + + const options_project: PrivateTaskRequestModel = { + task_type_id, + task_status_id, + task_project_id: project.project_id + } + const task = await this.taskRepository.getOneTask(options_project); + if (task) { + throw new Error("An export backup is already running for this project"); + } + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 15, "Step 1/3 Validation, no backup is running: done"); + } + + async copySourcesToBackupProjectFolder(task_id: number, project: ProjectResponseModel, skip_already_imported: boolean) { + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 25, "Step 3/3 L0-b backup folders copy : start"); + + const dest_folder = path.join(this.DATA_STORAGE_FS_STORAGE, `${project.project_id}`, "l0b_backup"); + const root_folder_path = project.root_folder_path; + + // Copy sources files to project folder + await this.projectRepository.copyL0bToProjectFolder(root_folder_path, dest_folder, skip_already_imported); + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 99, "Step 3/3 L0-b backup folders copy : done"); + } + +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 362f2d2..ac5af92 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import UserRouter from './presentation/routers/user-router' import AuthRouter from './presentation/routers/auth-router' import InstrumentModelRouter from './presentation/routers/instrument_model-router' import ProjectRouter from './presentation/routers/project-router' +import TaskRouter from './presentation/routers/tasks-router' import { SearchUsers } from './domain/use-cases/user/search-users' import { CreateUser } from './domain/use-cases/user/create-user' @@ -44,6 +45,7 @@ import { ProjectRepositoryImpl } from './domain/repositories/project-repository' import { PrivilegeRepositoryImpl } from './domain/repositories/privilege-repository' import { SampleRepositoryImpl } from './domain/repositories/sample-repository' import { TaskRepositoryImpl } from './domain/repositories/task-repository' +import { BackupProject } from './domain/use-cases/project/backup-project' import { SQLiteUserDataSource } from './data/data-sources/sqlite/sqlite-user-data-source' @@ -52,7 +54,6 @@ import { SQLiteProjectDataSource } from './data/data-sources/sqlite/sqlite-proje import { SQLitePrivilegeDataSource } from './data/data-sources/sqlite/sqlite-privilege-data-source' import { SQLiteTaskDataSource } from './data/data-sources/sqlite/sqlite-task-data-source' import { SQLiteSampleDataSource } from './data/data-sources/sqlite/sqlite-sample-data-source' -import sqlite3 from 'sqlite3' import { BcryptAdapter } from './infra/cryptography/bcript' import { JwtAdapter } from './infra/auth/jsonwebtoken' @@ -60,9 +61,10 @@ import { NodemailerAdapter } from './infra/mailer/nodemailer' import { CountriesAdapter } from './infra/countries/country' import { FsAdapter } from './infra/files/fs' -import 'dotenv/config' +import sqlite3 from 'sqlite3' import path from 'path' -import TaskRouter from './presentation/routers/tasks-router' + +import 'dotenv/config' sqlite3.verbose() @@ -174,6 +176,7 @@ async function getSQLiteDS() { new DeleteProject(user_repo, project_repo, privilege_repo), new UpdateProject(user_repo, project_repo, instrument_model_repo, privilege_repo), new SearchProject(user_repo, project_repo, search_repo, instrument_model_repo, privilege_repo), + new BackupProject(user_repo, privilege_repo, project_repo, task_repo, config.DATA_STORAGE_FS_STORAGE), new ListImportableSamples(sample_repo, user_repo, privilege_repo, project_repo, config.DATA_STORAGE_FS_STORAGE), new ImportSamples(sample_repo, user_repo, privilege_repo, project_repo, task_repo, config.DATA_STORAGE_FS_STORAGE), new DeleteSample(user_repo, sample_repo, privilege_repo), diff --git a/src/presentation/interfaces/middleware/project-validation.ts b/src/presentation/interfaces/middleware/project-validation.ts index a9d8584..7865607 100644 --- a/src/presentation/interfaces/middleware/project-validation.ts +++ b/src/presentation/interfaces/middleware/project-validation.ts @@ -7,6 +7,7 @@ export interface IMiddlewareProjectValidation { rulesGetProjects: (ValidationChain | ((req: Request, res: Response, next: NextFunction) => Response | undefined))[] rulesProjectRequestCreationModel: (ValidationChain | ((req: Request, res: Response, next: NextFunction) => Response | undefined))[] rulesProjectUpdateModel: ((Middleware & ContextRunner) | ((req: Request, res: Response, next: NextFunction) => Response | undefined))[] + rulesProjectBackup: (ValidationChain | ((req: Request, res: Response, next: NextFunction) => Response | undefined))[] } diff --git a/src/presentation/middleware/project-validation.ts b/src/presentation/middleware/project-validation.ts index 803de0a..21c7b49 100644 --- a/src/presentation/middleware/project-validation.ts +++ b/src/presentation/middleware/project-validation.ts @@ -290,4 +290,17 @@ export class MiddlewareProjectValidation implements IMiddlewareProjectValidation }, ]; + rulesProjectBackup = [ + check("skip_already_imported").default(true) + .isIn([true, false]).withMessage('Skip already imported must be a boolean true or false value.'), + // Error Handling Middleware + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + // Centralized error handling for validation errors + return res.status(422).json({ errors: errors.array() }); + } + next(); + }, + ] } diff --git a/src/presentation/routers/project-router.ts b/src/presentation/routers/project-router.ts index 88020fc..a0c5fb5 100644 --- a/src/presentation/routers/project-router.ts +++ b/src/presentation/routers/project-router.ts @@ -7,6 +7,8 @@ import { IMiddlewareProjectValidation } from '../interfaces/middleware/project-v import { CreateProjectUseCase } from '../../domain/interfaces/use-cases/project/create-project' import { DeleteProjectUseCase } from '../../domain/interfaces/use-cases/project/delete-project' import { UpdateProjectUseCase } from '../../domain/interfaces/use-cases/project/update-project' +import { BackupProjectUseCase } from '../../domain/interfaces/use-cases/project/backup-project' +// import { ExportBackupProjectUseCase } from '../../domain/interfaces/use-cases/project/export-backup-project' import { ImportSamplesUseCase } from '../../domain/interfaces/use-cases/sample/import-samples' import { DeleteSampleUseCase } from '../../domain/interfaces/use-cases/sample/delete-sample' import { SearchSamplesUseCase } from '../../domain/interfaces/use-cases/sample/search-samples' @@ -24,10 +26,12 @@ export default function ProjectRouter( deleteProjectUseCase: DeleteProjectUseCase, updateProjectUseCase: UpdateProjectUseCase, searchProjectUseCase: SearchProjectsUseCase, + backupProjectUseCase: BackupProjectUseCase, + // exportBackupProjectUseCase: ExportBackupProjectUseCase, listImportableSamplesUseCase: ListImportableSamplesUseCase, importSamplesUseCase: ImportSamplesUseCase, deleteSampleUseCase: DeleteSampleUseCase, - searchSamplesUseCase: SearchSamplesUseCase + searchSamplesUseCase: SearchSamplesUseCase, ) { const router = express.Router() @@ -131,6 +135,34 @@ export default function ProjectRouter( } }) + // L0-b project backup + router.post('/:project_id/backup', middlewareAuth.auth, middlewareProjectValidation.rulesProjectBackup, async (req: Request, res: Response) => { + try { + const task = await backupProjectUseCase.execute((req as CustomRequest).token, req.params.project_id as any, req.body.skip_already_imported); + res.status(200).send(task) + } catch (err) { + console.log(err) + if (err.message === "User cannot be used") res.status(403).send({ errors: [err.message] }) + else if (err.message === "Task type label not found") res.status(404).send({ errors: [err.message] }) + else if (err.message === "Task status label not found") res.status(404).send({ errors: [err.message] }) + else res.status(500).send({ errors: ["Cannot backup project"] }) + } + }) + + // // L0-b project backup export + // router.get('/:project_id/backup/export', middlewareAuth.auth, async (req: Request, res: Response) => { + // try { + // const task = await exportBackupProjectUseCase.execute((req as CustomRequest).token, req.params.project_id as any); + // res.status(200).send(task) + // } catch (err) { + // console.log(err) + // if (err.message === "User cannot be used") res.status(403).send({ errors: [err.message] }) + // else if (err.message === "Task type label not found") res.status(404).send({ errors: [err.message] }) + // else if (err.message === "Task status label not found") res.status(404).send({ errors: [err.message] }) + // else res.status(500).send({ errors: ["Cannot export project"] }) + // } + // }) + /***********************************************SAMPLES***********************************************/ router.get('/:project_id/samples/can_be_imported', middlewareAuth.auth, async (req: Request, res: Response) => {