From d0ebe0e9dcbd876a22941eacba8bb8e2ebe5488c Mon Sep 17 00:00:00 2001 From: juliecoust Date: Tue, 3 Dec 2024 09:47:44 +0100 Subject: [PATCH] DEV - l0-b backup import/export done --- .../repositories/project-repository.ts | 3 + .../repositories/task-repository.ts | 4 +- .../project/export-backuped-project.ts | 6 + .../use-cases/task/stream-zip-file.ts | 5 + src/domain/repositories/project-repository.ts | 104 +++++++++++-- src/domain/repositories/task-repository.ts | 33 +++- .../use-cases/project/backup-project.ts | 4 +- .../project/export-backuped-project.ts | 142 ++++++++++++++++++ src/domain/use-cases/task/stream-zip-file.ts | 84 +++++++++++ src/main.ts | 7 +- .../middleware/project-validation.ts | 1 + .../middleware/project-validation.ts | 13 ++ src/presentation/routers/project-router.ts | 41 ++--- src/presentation/routers/tasks-router.ts | 19 ++- 14 files changed, 426 insertions(+), 40 deletions(-) create mode 100644 src/domain/interfaces/use-cases/project/export-backuped-project.ts create mode 100644 src/domain/interfaces/use-cases/task/stream-zip-file.ts create mode 100644 src/domain/use-cases/project/export-backuped-project.ts create mode 100644 src/domain/use-cases/task/stream-zip-file.ts diff --git a/src/domain/interfaces/repositories/project-repository.ts b/src/domain/interfaces/repositories/project-repository.ts index 58a78a4..b45eab7 100644 --- a/src/domain/interfaces/repositories/project-repository.ts +++ b/src/domain/interfaces/repositories/project-repository.ts @@ -15,4 +15,7 @@ export interface ProjectRepository { standardGetProjects(options: PreparedSearchOptions): Promise>; ensureFolderStructureForBackup(root_folder_path: string): Promise; copyL0bToProjectFolder(source_folder: string, dest_folder: string, skip_already_imported: boolean): Promise; + ensureBackupExist(project_id: number): Promise; + exportBackupedProjectToFtp(project: ProjectResponseModel, task_id: number): Promise; + exportBackupedProjectToFs(project: ProjectResponseModel, task_id: number): Promise; } \ No newline at end of file diff --git a/src/domain/interfaces/repositories/task-repository.ts b/src/domain/interfaces/repositories/task-repository.ts index beb7174..e9a7d95 100644 --- a/src/domain/interfaces/repositories/task-repository.ts +++ b/src/domain/interfaces/repositories/task-repository.ts @@ -10,7 +10,7 @@ import { UserRequestModel } from "../../entities/user"; export interface TaskRepository { getOneTask(task: PrivateTaskRequestModel): Promise; startTask(task: PublicTaskRequestModel): Promise; - finishTask(task: PublicTaskRequestModel): Promise; + finishTask(task: PublicTaskRequestModel, task_result?: string): Promise; updateTaskProgress(task: PublicTaskRequestModel, progress_pct: number, progress_msg: string): Promise; // formatTaskRequestCreationModel(public_task: PublicTaskRequestCreationModel, instrument: InstrumentModelResponseModel): TaskRequestCreationModel; // standardUpdateTask(task_to_update: TaskUpdateModel): Promise; @@ -22,6 +22,8 @@ export interface TaskRepository { standardGetTaskStatus(options: PreparedSearchOptions): Promise>; getTasksByUser(user: UserRequestModel): Promise; getLogFileTask(task_id: number): Promise; + getZipFilePath(task_id: number): Promise; failedTask(task_id: number, error: Error): Promise; logMessage(task_log_file_path: string | undefined, message: string): Promise; + updateTaskResult(task: PublicTaskRequestModel, task_result: string): Promise; } diff --git a/src/domain/interfaces/use-cases/project/export-backuped-project.ts b/src/domain/interfaces/use-cases/project/export-backuped-project.ts new file mode 100644 index 0000000..95cb114 --- /dev/null +++ b/src/domain/interfaces/use-cases/project/export-backuped-project.ts @@ -0,0 +1,6 @@ +import { TaskResponseModel } from "../../../entities/task"; +import { UserUpdateModel } from "../../../entities/user"; + +export interface ExportBackupedProjectUseCase { + execute(current_user: UserUpdateModel, project_id: number, out_to_ftp: boolean): Promise; +} \ No newline at end of file diff --git a/src/domain/interfaces/use-cases/task/stream-zip-file.ts b/src/domain/interfaces/use-cases/task/stream-zip-file.ts new file mode 100644 index 0000000..1b2a27e --- /dev/null +++ b/src/domain/interfaces/use-cases/task/stream-zip-file.ts @@ -0,0 +1,5 @@ +import { Response } from "express"; +import { UserUpdateModel } from "../../../entities/user"; +export interface StreamZipFileUseCase { + execute(current_user: UserUpdateModel, task_id: number, res: Response): Promise; +} \ No newline at end of file diff --git a/src/domain/repositories/project-repository.ts b/src/domain/repositories/project-repository.ts index 6445265..3f82afb 100644 --- a/src/domain/repositories/project-repository.ts +++ b/src/domain/repositories/project-repository.ts @@ -15,14 +15,21 @@ import path from "path"; export class ProjectRepositoryImpl implements ProjectRepository { projectDataSource: ProjectDataSource + DATA_STORAGE_FS_STORAGE: string + DATA_STORAGE_FTP_EXPORT: string + DATA_STORAGE_FOLDER: string // 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) { + constructor(projectDataSource: ProjectDataSource, DATA_STORAGE_FS_STORAGE: string, DATA_STORAGE_FTP_EXPORT: string, DATA_STORAGE_FOLDER: string) { this.projectDataSource = projectDataSource + this.DATA_STORAGE_FS_STORAGE = DATA_STORAGE_FS_STORAGE + this.DATA_STORAGE_FTP_EXPORT = DATA_STORAGE_FTP_EXPORT + this.DATA_STORAGE_FOLDER = DATA_STORAGE_FOLDER } async createProject(project: ProjectRequestCreationModel): Promise { @@ -282,34 +289,40 @@ export class ProjectRepositoryImpl implements ProjectRepository { } } } - async zipFolder(sourceFolder: string, destZipFile: string): Promise { - // Create a file output stream for the destination zip file + // Ensure the parent directory of destZipFile exists + const destDir = path.dirname(destZipFile); + if (!fs.existsSync(destDir)) { + throw new Error(`Destination directory does not exist: ${destDir}`); + } + // Ensure the destination is not an existing directory + if (fs.existsSync(destZipFile) && fs.lstatSync(destZipFile).isDirectory()) { + throw new Error(`Destination path is a directory: ${destZipFile}`); + } + + // Create a writable stream for the 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 - }); + // Create a new Archiver instance + const archive = archiver('zip', { zlib: { level: 9 } }); - // Pipe the archiver output to the file stream + // Handle events output.on('close', () => { - //console.log(`Archive ${destZipFile} has been created, ${archive.pointer()} total bytes.`); }); archive.on('error', (err) => { throw err; }); + // Pipe the archive data to the file archive.pipe(output); - // Append all files from the source folder to the archive - archive.directory(sourceFolder, false); // Second argument 'false' keeps directory structure intact + // Append the folder to the archive + archive.directory(sourceFolder, false); - // Finalize the archive (this will compress the data) + // Finalize the archive 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'); @@ -348,4 +361,69 @@ export class ProjectRepositoryImpl implements ProjectRepository { } } + getFormattedDate(date: Date) { + return date.toISOString() + .replace('T', '_') // Remplace le 'T' par un underscore + .replace(/\..+/, '') // Supprime la partie millisecondes et 'Z' + .replace(/-/g, '_') // Remplace les tirets par des underscores + .replace(/:/g, '_'); // Remplace les deux-points par des underscores + } + + async ensureBackupExist(project_id: number): Promise { + const backuped_project_path = path.join(this.base_folder, this.DATA_STORAGE_FS_STORAGE, project_id.toString(), 'l0b_backup'); + try { + await fsPromises.access(backuped_project_path); + } catch (error) { + throw new Error(`Backup folder does not exist at path: ${backuped_project_path}`); + } + } + + async exportBackupedProjectToFtp(project: ProjectResponseModel, task_id: number): Promise { + // Create the export ftp folder + const formattedDate = this.getFormattedDate(new Date()); + + const exportFolder = path.join( + this.base_folder, + this.DATA_STORAGE_FTP_EXPORT, + task_id.toString() + ); + + // Create the export zip file path + await fsPromises.mkdir(exportFolder, { recursive: true }); + const exportZip = path.join( + exportFolder, + `ecopart_export_backup_${project.project_id.toString()}_${formattedDate}.zip` + ); + await this.copyZippedL0bFoldersToExportFolder(project, exportZip); + return exportZip; + } + + async exportBackupedProjectToFs(project: ProjectResponseModel, task_id: number): Promise { + // Create the export fs folder + const formattedDate = this.getFormattedDate(new Date()); + const exportFolder = path.join( + this.base_folder, + this.DATA_STORAGE_FOLDER, + 'tasks', + task_id.toString(), + ); + await fsPromises.mkdir(exportFolder, { recursive: true }); + + // Create the export zip file path + const exportZip = path.join( + exportFolder, + `ecopart_export_backup_${project.project_id.toString()}_${formattedDate}.zip` + ); + await this.copyZippedL0bFoldersToExportFolder(project, exportZip); + + return exportZip; + } + + async copyZippedL0bFoldersToExportFolder(project: ProjectResponseModel, exportFolder: string): Promise { + // with date + const backupedProjectPath = path.join(this.base_folder, this.DATA_STORAGE_FS_STORAGE, project.project_id.toString(), 'l0b_backup'); + // zip and copy backupedProjectPath to exportFolder + await this.zipFolder(backupedProjectPath, exportFolder); + } + } \ No newline at end of file diff --git a/src/domain/repositories/task-repository.ts b/src/domain/repositories/task-repository.ts index 13fdf06..ae440c7 100644 --- a/src/domain/repositories/task-repository.ts +++ b/src/domain/repositories/task-repository.ts @@ -1,5 +1,4 @@ -import path from "path"; import { TaskDataSource } from "../../data/interfaces/data-sources/task-data-source"; import { FsWrapper } from "../../infra/files/fs-wrapper"; import { PreparedSearchOptions, SearchResult } from "../entities/search"; @@ -11,6 +10,8 @@ import { PrivateTaskRequestModel, TaskResponseModel, TaskStatusResponseModel, Ta import { UserRequestModel } from "../entities/user"; // import { PreparedSearchOptions, SearchResult } from "../entities/search"; import { TaskRepository } from "../interfaces/repositories/task-repository"; +import path from "path"; +import fs from "fs/promises"; export class TaskRepositoryImpl implements TaskRepository { taskDataSource: TaskDataSource @@ -94,7 +95,7 @@ export class TaskRepositoryImpl implements TaskRepository { await this.logMessage(task_to_start.task_log_file_path, "Task is running") } - async finishTask(task: PublicTaskRequestModel): Promise { + async finishTask(task: PublicTaskRequestModel, task_result?: string): Promise { const task_to_finish = await this.taskDataSource.getOne({ task_id: task.task_id }) if (!task_to_finish) { throw new Error("Task not found") @@ -106,6 +107,11 @@ export class TaskRepositoryImpl implements TaskRepository { // Update the task progress to 100% and add a message await this.updateTaskProgress(task, 100, "Task is done sucessfilly") + // Update the task result if provided + if (task_result) { + await this.taskDataSource.updateOne({ task_id: task_to_finish.task_id, task_result: task_result }) + } + // appendFile to log file that task is done await this.logMessage(task_to_finish.task_log_file_path, "Task is done sucessfilly") } @@ -348,6 +354,19 @@ export class TaskRepositoryImpl implements TaskRepository { } } + async getZipFilePath(task_id: number): Promise { + // ensure folder exists for this task + const zipPath = path.join(this.base_folder, this.DATA_STORAGE_FOLDER, "tasks", `${task_id}`) + + // return the first file that have extention .zip in the folder + const files = await fs.readdir(zipPath) + const zipFile = files.find(file => file.endsWith(".zip")) + if (!zipFile) { + throw new Error(`No file found for task ${task_id}`) + } + return path.join(zipPath, zipFile) + } + async failedTask(task_id: number, error: Error): Promise { this.getTask({ task_id: task_id }).then(async task => { if (!task) throw new Error("Task not found") @@ -420,4 +439,14 @@ export class TaskRepositoryImpl implements TaskRepository { // Logging task status update in the log file await this.logMessage(task_to_update.task_log_file_path, `Task status updated from ${task_to_update.task_status} to ${status}`); } + + async updateTaskResult(task: PublicTaskRequestModel, task_result: string): Promise { + const task_to_update = await this.taskDataSource.getOne({ task_id: task.task_id }); + if (!task_to_update) { + throw new Error("Task not found"); + } + await this.taskDataSource.updateOne({ + task_id: task_to_update.task_id, task_result: task_result + }) + } } \ No newline at end of file diff --git a/src/domain/use-cases/project/backup-project.ts b/src/domain/use-cases/project/backup-project.ts index 3aa01d3..51dd9c4 100644 --- a/src/domain/use-cases/project/backup-project.ts +++ b/src/domain/use-cases/project/backup-project.ts @@ -31,13 +31,13 @@ export class BackupProject implements BackupProjectUseCase { // 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 + // Ensure the current user has permission to backup project await this.ensureUserCanGet(current_user, project_id); // Get the project const project: ProjectResponseModel = await this.getProjectIfExist(project_id); - // Create a task to import samples + // Create a task to backup project const task_id = await this.createBackupProjectTask(current_user, project, skip_already_imported); // get the task diff --git a/src/domain/use-cases/project/export-backuped-project.ts b/src/domain/use-cases/project/export-backuped-project.ts new file mode 100644 index 0000000..65a0911 --- /dev/null +++ b/src/domain/use-cases/project/export-backuped-project.ts @@ -0,0 +1,142 @@ + +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 { ExportBackupedProjectUseCase } from "../../interfaces/use-cases/project/export-backuped-project"; +import { ProjectResponseModel } from "../../entities/project"; +import { PublicTaskRequestCreationModel, TaskResponseModel, TasksStatus, TaskType } from "../../entities/task"; + +export class ExportBackupedProject implements ExportBackupedProjectUseCase { + userRepository: UserRepository + privilegeRepository: PrivilegeRepository + projectRepository: ProjectRepository + taskRepository: TaskRepository + DATA_STORAGE_FS_STORAGE: string + DATA_STORAGE_FTP_EXPORT: string + base_url_path: string + + constructor(userRepository: UserRepository, privilegeRepository: PrivilegeRepository, projectRepository: ProjectRepository, taskRepository: TaskRepository, DATA_STORAGE_FS_STORAGE: string, DATA_STORAGE_FTP_EXPORT: string, base_url_path: string) { + this.userRepository = userRepository + this.privilegeRepository = privilegeRepository + this.projectRepository = projectRepository + this.taskRepository = taskRepository + this.DATA_STORAGE_FS_STORAGE = DATA_STORAGE_FS_STORAGE + this.DATA_STORAGE_FTP_EXPORT = DATA_STORAGE_FTP_EXPORT + this.base_url_path = base_url_path + } + + async execute(current_user: UserUpdateModel, project_id: number, out_to_ftp: 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 backuped project + await this.ensureUserCanGet(current_user, project_id); + + // Get the project + const project: ProjectResponseModel = await this.getProjectIfExist(project_id); + + // Create a task export backuped project + const task_id = await this.createExportBackupedProjectTask(current_user, project, out_to_ftp); + + // 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.startExportBackupedProjectTask(task, project, out_to_ftp); + + return task; + } + + async createExportBackupedProjectTask(current_user: UserUpdateModel, project: ProjectResponseModel, out_to_ftp: boolean): Promise { + const task: PublicTaskRequestCreationModel = { + task_type: TaskType.Export_Backup, + task_status: TasksStatus.Pending, + task_owner_id: current_user.user_id, + task_project_id: project.project_id, + task_params: { + out_to_ftp: out_to_ftp + } + } + 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 startExportBackupedProjectTask(task: TaskResponseModel, project: ProjectResponseModel, out_to_ftp: boolean) { + const task_id = task.task_id; + try { + await this.taskRepository.startTask({ task_id: task_id }); + + // 1/3 Do validation before importing 0->25% + await this.ensureBackupExist(project.project_id, task_id); + + // 2/3 Export backuped project 25->99% + const result = await this.exportBackupedProject(task, project, out_to_ftp); + // finish task + await this.taskRepository.finishTask({ task_id: task_id }, result); + } catch (error) { + await this.taskRepository.failedTask(task_id, error); + return; + } + } + + async exportBackupedProject(task: TaskResponseModel, project: ProjectResponseModel, out_to_ftp: boolean): Promise { + await this.taskRepository.updateTaskProgress({ task_id: task.task_id }, 25, "Step 2/3 Export backuped project, export to fs : start"); + const dl_link = await this.exportToFs(project, task.task_id); + // log dl link in task + await this.taskRepository.logMessage(task.task_log_file_path, "Exported at : " + dl_link); + // update task with download link ex : + const download_link = this.base_url_path + "/api/tasks/" + task.task_id + "/export"; + if (out_to_ftp) { + await this.taskRepository.updateTaskProgress({ task_id: task.task_id }, 60, "Step 2/3 Export backuped project, export to fs : done"); + await this.taskRepository.updateTaskProgress({ task_id: task.task_id }, 60, "Step 2/3 Export backuped project, export to ftp : start"); + const ftp_link = await this.exportToFtp(project, task.task_id); + await this.taskRepository.updateTaskProgress({ task_id: task.task_id }, 99, "Step 2/3 Export backuped project, export to ftp : done"); + return ftp_link + " " + download_link; + } else { + await this.taskRepository.updateTaskProgress({ task_id: task.task_id }, 99, "Step 2/3 Export backuped project, export to fs : done"); + return download_link; + // update task with donload link + } + } + + async exportToFs(project: ProjectResponseModel, task_id: number): Promise { + const fs_export_path = await this.projectRepository.exportBackupedProjectToFs(project, task_id); + return fs_export_path; + } + async exportToFtp(project: ProjectResponseModel, task_id: number): Promise { + const ftp_export_path = await this.projectRepository.exportBackupedProjectToFtp(project, task_id); + return ftp_export_path + } + + async ensureBackupExist(project_id: number, task_id: number) { + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 15, "Step 1/3 Validation, ensure the project has been backuped: start"); + await this.projectRepository.ensureBackupExist(project_id); + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 25, "Step 1/3 Validation, ensure the project has been backuped: done"); + } +} \ No newline at end of file diff --git a/src/domain/use-cases/task/stream-zip-file.ts b/src/domain/use-cases/task/stream-zip-file.ts new file mode 100644 index 0000000..0b46f80 --- /dev/null +++ b/src/domain/use-cases/task/stream-zip-file.ts @@ -0,0 +1,84 @@ +import { Response } from "express"; +import { UserUpdateModel } from "../../entities/user"; +import { PrivilegeRepository } from "../../interfaces/repositories/privilege-repository"; +import { TaskRepository } from "../../interfaces/repositories/task-repository"; +import { UserRepository } from "../../interfaces/repositories/user-repository"; +import { StreamZipFileUseCase } from "../../interfaces/use-cases/task/stream-zip-file"; +import * as fs from 'fs'; // For createWriteStream + +export class StreamZipFile implements StreamZipFileUseCase { + taskRepository: TaskRepository + userRepository: UserRepository + privilegeRepository: PrivilegeRepository + + constructor(taskRepository: TaskRepository, userRepository: UserRepository, privilegeRepository: PrivilegeRepository) { + this.taskRepository = taskRepository + this.userRepository = userRepository + this.privilegeRepository = privilegeRepository + } + + async execute(current_user: UserUpdateModel, task_id: number, res: Response): Promise { + // Ensure the user is valid and can be used + await this.userRepository.ensureUserCanBeUsed(current_user.user_id); + + + const task = await this.taskRepository.getOneTask({ task_id: task_id }); + // Ensure the task to get exists + if (!task) { throw new Error("Cannot find task"); } + + // Ensure the current user has permission to get the task + await this.ensureUserCanGet(current_user, task.task_owner_id, task.task_project_id); + + // Get the file path + const zipFilePath = await this.taskRepository.getZipFilePath(task_id); + + if (!fs.existsSync(zipFilePath)) { + throw new Error("ZIP file not found"); + } + + res.setHeader("Content-Type", "application/zip"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${zipFilePath}"` + ); + + const fileStream = fs.createReadStream(zipFilePath); + fileStream.pipe(res); + } + + //TODO factoriser les fonctions de vérification de permission + private async ensureUserCanGet(current_user: UserUpdateModel, owner_id: number, project_id?: number): Promise { + // Check if the user is the owner of the task + const userIsOwner = current_user.user_id === owner_id; + + // If the user is the owner, allow access immediately + if (userIsOwner) { + return; + } + + // Check if the current user is an admin + const userIsAdmin = await this.userRepository.isAdmin(current_user.user_id); + + // If the user is an admin, allow access immediately + if (userIsAdmin) { + return; + } + + // If a project_id is provided, check if the user has privileges in that project + if (project_id) { + const userHasPrivilege = await this.privilegeRepository.isGranted({ + user_id: current_user.user_id, + project_id: project_id + }); + + // If the user has the necessary privileges, allow access + if (userHasPrivilege) { + return; + } + } + + // If none of the above conditions are met, throw an error + throw new Error("User does not have the necessary permissions to access this task."); + } + +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index ac5af92..a7d56c8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,6 +34,7 @@ import { DeleteTask } from './domain/use-cases/task/delete-task' import { SearchTask } from './domain/use-cases/task/search-tasks' import { GetOneTask } from './domain/use-cases/task/get-one-task' import { GetLogFileTask } from './domain/use-cases/task/get-log-file-task' +import { StreamZipFile } from './domain/use-cases/task/stream-zip-file' import { DeleteSample } from './domain/use-cases/sample/delete-sample' import { SearchSamples } from './domain/use-cases/sample/search-samples' @@ -46,6 +47,7 @@ import { PrivilegeRepositoryImpl } from './domain/repositories/privilege-reposit import { SampleRepositoryImpl } from './domain/repositories/sample-repository' import { TaskRepositoryImpl } from './domain/repositories/task-repository' import { BackupProject } from './domain/use-cases/project/backup-project' +import { ExportBackupedProject } from './domain/use-cases/project/export-backuped-project' import { SQLiteUserDataSource } from './data/data-sources/sqlite/sqlite-user-data-source' @@ -79,6 +81,7 @@ const config = { DATA_STORAGE_FOLDER: process.env.DATA_STORAGE_FOLDER || '', DATA_STORAGE_FS_STORAGE: process.env.DATA_STORAGE_FS_STORAGE || '', + DATA_STORAGE_FTP_EXPORT: process.env.DATA_STORAGE_FTP_EXPORT || '', ACCESS_TOKEN_SECRET: process.env.ACCESS_TOKEN_SECRET || '', REFRESH_TOKEN_SECRET: process.env.REFRESH_TOKEN_SECRET || '', @@ -140,7 +143,7 @@ async function getSQLiteDS() { const auth_repo = new AuthRepositoryImpl(jwtAdapter, config.ACCESS_TOKEN_SECRET, config.REFRESH_TOKEN_SECRET) const search_repo = new SearchRepositoryImpl() const instrument_model_repo = new InstrumentModelRepositoryImpl(instrument_model_dataSource) - const project_repo = new ProjectRepositoryImpl(project_dataSource) + const project_repo = new ProjectRepositoryImpl(project_dataSource, config.DATA_STORAGE_FS_STORAGE, config.DATA_STORAGE_FTP_EXPORT, config.DATA_STORAGE_FOLDER) const privilege_repo = new PrivilegeRepositoryImpl(privilege_dataSource) const sample_repo = new SampleRepositoryImpl(sample_dataSource, config.DATA_STORAGE_FS_STORAGE) const task_repo = new TaskRepositoryImpl(task_datasource, fsAdapter, config.DATA_STORAGE_FOLDER) @@ -177,6 +180,7 @@ async function getSQLiteDS() { 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 ExportBackupedProject(user_repo, privilege_repo, project_repo, task_repo, config.DATA_STORAGE_FS_STORAGE, config.DATA_STORAGE_FTP_EXPORT, config.BASE_URL_PUBLIC), 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), @@ -188,6 +192,7 @@ async function getSQLiteDS() { new DeleteTask(user_repo, task_repo, privilege_repo), new GetOneTask(task_repo, user_repo, privilege_repo), new GetLogFileTask(task_repo, user_repo, privilege_repo), + new StreamZipFile(task_repo, user_repo, privilege_repo), new SearchTask(user_repo, task_repo, search_repo, project_repo, privilege_repo) ) diff --git a/src/presentation/interfaces/middleware/project-validation.ts b/src/presentation/interfaces/middleware/project-validation.ts index 5b7e359..e66d6ee 100644 --- a/src/presentation/interfaces/middleware/project-validation.ts +++ b/src/presentation/interfaces/middleware/project-validation.ts @@ -8,6 +8,7 @@ export interface IMiddlewareProjectValidation { 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))[] + rulesProjectExportBackup: (ValidationChain | ((req: Request, res: Response, next: NextFunction) => Response | undefined))[] rulesProjectBackupFromImport: (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 5996acf..ad54a3d 100644 --- a/src/presentation/middleware/project-validation.ts +++ b/src/presentation/middleware/project-validation.ts @@ -322,4 +322,17 @@ export class MiddlewareProjectValidation implements IMiddlewareProjectValidation next(); }, ] + rulesProjectExportBackup = [ + check("out_to_ftp").default(false) + .isIn([true, false]).withMessage('Out to ftp 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 0a8e1db..20164c1 100644 --- a/src/presentation/routers/project-router.ts +++ b/src/presentation/routers/project-router.ts @@ -8,7 +8,7 @@ import { CreateProjectUseCase } from '../../domain/interfaces/use-cases/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 { ExportBackupedProjectUseCase } from '../../domain/interfaces/use-cases/project/export-backuped-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' @@ -27,7 +27,7 @@ export default function ProjectRouter( updateProjectUseCase: UpdateProjectUseCase, searchProjectUseCase: SearchProjectsUseCase, backupProjectUseCase: BackupProjectUseCase, - // exportBackupProjectUseCase: ExportBackupProjectUseCase, + exportBackupProjectUseCase: ExportBackupedProjectUseCase, listImportableSamplesUseCase: ListImportableSamplesUseCase, importSamplesUseCase: ImportSamplesUseCase, deleteSampleUseCase: DeleteSampleUseCase, @@ -149,19 +149,19 @@ export default function ProjectRouter( } }) - // // 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"] }) - // } - // }) + // L0-b project backup export + router.post('/:project_id/backup/export', middlewareAuth.auth, middlewareProjectValidation.rulesProjectBackup, async (req: Request, res: Response) => { + try { + const task = await exportBackupProjectUseCase.execute((req as CustomRequest).token, req.params.project_id as any, req.body.out_to_ftp); + 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 backuped project"] }) + } + }) /***********************************************SAMPLES***********************************************/ @@ -171,11 +171,10 @@ export default function ProjectRouter( res.status(200).send(tasks) } 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 search tasks"] }) - res.status(500).send({ errors: ["Cannot list importable samples"] }) + if (err.message === "User cannot be used") res.status(403).send({ errors: [err.message] }) + else if (err.message.includes("Folder does not exist at path")) res.status(404).send({ errors: [err.message] }) + //TODO handle other errors + else res.status(500).send({ errors: ["Cannot list importable samples"] }) } }) @@ -190,6 +189,8 @@ export default function ProjectRouter( } } catch (err) { console.log(err) + if (err.message === "User cannot be used") res.status(403).send({ errors: [err.message] }) + else if (err.message.includes("Folder does not exist at path")) res.status(404).send({ errors: [err.message] }) // 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] }) diff --git a/src/presentation/routers/tasks-router.ts b/src/presentation/routers/tasks-router.ts index cae06e8..896fa7c 100644 --- a/src/presentation/routers/tasks-router.ts +++ b/src/presentation/routers/tasks-router.ts @@ -8,6 +8,7 @@ import { DeleteTaskUseCase } from '../../domain/interfaces/use-cases/task/delete import { SearchTasksUseCase } from '../../domain/interfaces/use-cases/task/search-task' import { GetOneTaskUseCase } from '../../domain/interfaces/use-cases/task/get-one-task' import { GetLogFileTask } from '../../domain/use-cases/task/get-log-file-task' +import { StreamZipFileUseCase } from '../../domain/interfaces/use-cases/task/stream-zip-file' import { CustomRequest } from '../../domain/entities/auth' @@ -17,6 +18,7 @@ export default function TaskRouter( deleteTaskUseCase: DeleteTaskUseCase, getOneTaskUseCase: GetOneTaskUseCase, getLogFileTask: GetLogFileTask, + streamZipFileUseCase: StreamZipFileUseCase, searchTaskUseCase: SearchTasksUseCase ) { const router = express.Router() @@ -77,7 +79,7 @@ export default function TaskRouter( } }) - // Fetch log for admin nof task owner of project member/managers + // Fetch log for admin or task owner or project member/managers router.get('/:task_id/log', middlewareAuth.auth, async (req: Request, res: Response) => { try { const taskId = parseInt(req.params.task_id); @@ -92,5 +94,20 @@ export default function TaskRouter( } }); + // Fetch task file for admin or task owner or project member/managers + router.get('/:task_id/file', middlewareAuth.auth, async (req: Request, res: Response) => { + try { + const taskId = parseInt(req.params.task_id); + await streamZipFileUseCase.execute((req as CustomRequest).token, taskId, res); + } catch (err) { + console.log(err); + if (err.message === "User cannot be used") res.status(403).send({ errors: [err.message] }) + else if (err.message === "Cannot find task") res.status(404).send({ errors: [err.message] }); + else if (err.message === "Cannot find task file") res.status(404).send({ errors: [err.message] }); + else if (err.message === "User does not have the necessary permissions to access this task.") res.status(403).send({ errors: ["Cannot get task file"] }); + else res.status(500).send({ errors: ["Cannot get task file"] }); + } + }); + return router } \ No newline at end of file