Skip to content

Commit

Permalink
DEV - l0-b backup import/export done
Browse files Browse the repository at this point in the history
  • Loading branch information
juliecoust committed Dec 3, 2024
1 parent ace5cf8 commit d0ebe0e
Show file tree
Hide file tree
Showing 14 changed files with 426 additions and 40 deletions.
3 changes: 3 additions & 0 deletions src/domain/interfaces/repositories/project-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ export interface ProjectRepository {
standardGetProjects(options: PreparedSearchOptions): Promise<SearchResult<ProjectResponseModel>>;
ensureFolderStructureForBackup(root_folder_path: string): Promise<void>;
copyL0bToProjectFolder(source_folder: string, dest_folder: string, skip_already_imported: boolean): Promise<void>;
ensureBackupExist(project_id: number): Promise<void>;
exportBackupedProjectToFtp(project: ProjectResponseModel, task_id: number): Promise<string>;
exportBackupedProjectToFs(project: ProjectResponseModel, task_id: number): Promise<string>;
}
4 changes: 3 additions & 1 deletion src/domain/interfaces/repositories/task-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { UserRequestModel } from "../../entities/user";
export interface TaskRepository {
getOneTask(task: PrivateTaskRequestModel): Promise<TaskResponseModel | null>;
startTask(task: PublicTaskRequestModel): Promise<void>;
finishTask(task: PublicTaskRequestModel): Promise<void>;
finishTask(task: PublicTaskRequestModel, task_result?: string): Promise<void>;
updateTaskProgress(task: PublicTaskRequestModel, progress_pct: number, progress_msg: string): Promise<void>;
// formatTaskRequestCreationModel(public_task: PublicTaskRequestCreationModel, instrument: InstrumentModelResponseModel): TaskRequestCreationModel;
// standardUpdateTask(task_to_update: TaskUpdateModel): Promise<number>;
Expand All @@ -22,6 +22,8 @@ export interface TaskRepository {
standardGetTaskStatus(options: PreparedSearchOptions): Promise<SearchResult<TaskStatusResponseModel>>;
getTasksByUser(user: UserRequestModel): Promise<number[]>;
getLogFileTask(task_id: number): Promise<string>;
getZipFilePath(task_id: number): Promise<string>;
failedTask(task_id: number, error: Error): Promise<void>;
logMessage(task_log_file_path: string | undefined, message: string): Promise<void>;
updateTaskResult(task: PublicTaskRequestModel, task_result: string): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -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<TaskResponseModel>;
}
5 changes: 5 additions & 0 deletions src/domain/interfaces/use-cases/task/stream-zip-file.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
}
104 changes: 91 additions & 13 deletions src/domain/repositories/project-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> {
Expand Down Expand Up @@ -282,34 +289,40 @@ export class ProjectRepositoryImpl implements ProjectRepository {
}
}
}

async zipFolder(sourceFolder: string, destZipFile: string): Promise<void> {
// 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<void> {
const sourcePath = path.join(base_folder, source_folder, 'raw');
const destPath = path.join(base_folder, dest_folder, 'raw');
Expand Down Expand Up @@ -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<void> {
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<string> {
// 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<string> {
// 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<void> {
// 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);
}

}
33 changes: 31 additions & 2 deletions src/domain/repositories/task-repository.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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<void> {
async finishTask(task: PublicTaskRequestModel, task_result?: string): Promise<void> {
const task_to_finish = await this.taskDataSource.getOne({ task_id: task.task_id })
if (!task_to_finish) {
throw new Error("Task not found")
Expand All @@ -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")
}
Expand Down Expand Up @@ -348,6 +354,19 @@ export class TaskRepositoryImpl implements TaskRepository {
}
}

async getZipFilePath(task_id: number): Promise<string> {
// 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<void> {
this.getTask({ task_id: task_id }).then(async task => {
if (!task) throw new Error("Task not found")
Expand Down Expand Up @@ -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<void> {
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
})
}
}
4 changes: 2 additions & 2 deletions src/domain/use-cases/project/backup-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit d0ebe0e

Please sign in to comment.