diff --git a/ExpressAPI/src/routes/AssignmentRoutes.ts b/ExpressAPI/src/routes/AssignmentRoutes.ts index 211ddb17c54d5d692159868c653ea812dd48661d..30f970e75d3b4342d84732bf94dd58020d24174b 100644 --- a/ExpressAPI/src/routes/AssignmentRoutes.ts +++ b/ExpressAPI/src/routes/AssignmentRoutes.ts @@ -80,7 +80,8 @@ class AssignmentRoutes implements RoutesManager { backend.post('/assignments/:assignmentNameOrUrl/corrections', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), ParamsValidatorMiddleware.validate(this.assignmentAddCorrigeValidator), this.linkUpdateAssignmentCorrection(false).bind(this) as RequestHandler); backend.patch('/assignments/:assignmentNameOrUrl/corrections/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), ParamsValidatorMiddleware.validate(this.assignmentUpdateCorrigeValidator), this.linkUpdateAssignmentCorrection(true).bind(this) as RequestHandler); backend.delete('/assignments/:assignmentNameOrUrl/corrections/:exerciseIdOrUrl', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.unlinkAssignmentCorrection.bind(this) as RequestHandler); - backend.get('/assignments/:assignmentNameOrUrl/export', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.exportLightAssignment.bind(this) as RequestHandler); + backend.get('/assignments/:assignmentNameOrUrl/export/:folderName', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.exportLightAssignment.bind(this) as RequestHandler); + backend.get('/assignments/:folderName/zip', SecurityMiddleware.check(false, SecurityCheckType.ASSIGNMENT_STAFF), this.zipAssignment.bind(this) as RequestHandler); } // Get an assignment by its name or gitlab url @@ -301,27 +302,9 @@ class AssignmentRoutes implements RoutesManager { return req.session.sendResponse(res, StatusCodes.OK); } - // private async exportLightAssignment(req: express.Request, res: express.Response) { - // const resDl = await GitlabManager.archiveRepository(req.boundParams.assignment!.gitlabId).then(async archive => { - // const buffer = Buffer.from(await archive.arrayBuffer()); - - // const zipName = req.boundParams.assignment?.name.replace(/ /g, "_") + '.tar.gz'; - // fs.writeFile(zipName, buffer, (err) => { - // if (err) { - // console.error('Error saving archive:', err); - // } else { - // console.log('Archive saved successfully!'); - // } - // }); - // }).catch(error => { - // console.error('Error fetching archive:', error); - // }); - // return req.session.sendResponse(res, StatusCodes.OK, resDl); - // } - private async exportLightAssignment(req: express.Request, res: express.Response) { try { - const folderName = 'tmp2'; + const folderName = req.params.folderName; const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // const parentDir = path.join('/tmp', `export_${timestamp}`); const parentDir = path.join('/tmp', folderName); @@ -332,15 +315,13 @@ class AssignmentRoutes implements RoutesManager { const zipName = req.boundParams.assignment?.name.replace(/ /g, "_") + '.tar.gz'; const zipPath = path.join(parentDir, zipName); - fs.writeFileSync(zipPath, buffer); - // fs.writeFile(zipPath, buffer, (err) => { - // if (err) { - // console.error('Error saving archive:', err); - // } else { - // console.log('Archive saved successfully!'); - // } - // }); - // console.log('Archive saved successfully!'); + fs.writeFile(zipPath, buffer, (err) => { + if (err) { + console.error('Error saving archive:', err); + } else { + console.log(`Archive ${zipName} saved successfully!`); + } + }); // const finalZipPath = path.join('/tmp', `export_${timestamp}.zip`); const finalZipPath = path.join('/tmp', `${folderName}.zip`); @@ -352,9 +333,8 @@ class AssignmentRoutes implements RoutesManager { archiveZip.pipe(output); archiveZip.directory(parentDir, false); await archiveZip.finalize(); - - res.download(finalZipPath, `${folderName}.zip`, (err) => { - // res.download(finalZipPath, `export_${timestamp}.zip`, (err) => { + // output.on('close', () => { + res.download(finalZipPath, (err) => { if (err) { console.error('Error sending zip:', err); res.status(500).send('Error sending zip'); @@ -365,12 +345,24 @@ class AssignmentRoutes implements RoutesManager { // fs.unlinkSync(finalZipPath); } }); - + // }); } catch (error) { console.error('Error exporting assignment:', error); res.status(500).send('Error exporting assignment'); } } + + private async zipAssignment(req : express.Request, res : express.Response) { + const folderName = req.params.folderName; + const zipPath = path.join('/tmp', `${folderName}.zip`); + res.download(zipPath, (err) => { + if (err) { + logger.error(`Error sending the zip : ${err}`); + } else { + console.log("Zip sent successfully"); + } + }); + } } diff --git a/ExpressAPI/src/routes/ExerciseRoutes.ts b/ExpressAPI/src/routes/ExerciseRoutes.ts index b32ab8b22983feed2cef9d06768bedfc0b70b2c6..4841eaf12cf0e941a83b74207c64eec10f1b3523 100644 --- a/ExpressAPI/src/routes/ExerciseRoutes.ts +++ b/ExpressAPI/src/routes/ExerciseRoutes.ts @@ -26,6 +26,7 @@ import ExerciseManager from '../managers/ExerciseManager.js'; import * as Gitlab from '@gitbeaker/rest'; import GitlabTreeFileType from '../shared/types/Gitlab/GitlabTreeFileType.js'; import { GitbeakerRequestError } from '@gitbeaker/requester-utils'; +import archiver from 'archiver'; class ExerciseRoutes implements RoutesManager { @@ -36,7 +37,7 @@ class ExerciseRoutes implements RoutesManager { customSanitizer: DojoValidators.jsonSanitizer } }; - + private readonly resultValidator: ExpressValidator.Schema = { exitCode : { isInt: true, @@ -63,25 +64,27 @@ class ExerciseRoutes implements RoutesManager { notEmpty: true } }; - + registerOnBackend(backend: Express) { backend.post('/assignments/:assignmentNameOrUrl/exercises', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_IS_PUBLISHED), ParamsValidatorMiddleware.validate(this.exerciseValidator), this.createExercise.bind(this) as RequestHandler); - + backend.get('/exercises/:exerciseIdOrUrl/assignment', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), this.getAssignment.bind(this) as RequestHandler); - + backend.post('/exercises/:exerciseIdOrUrl/results', SecurityMiddleware.check(false, SecurityCheckType.EXERCISE_SECRET), ParamsValidatorMiddleware.validate(this.resultValidator), this.createResult.bind(this) as RequestHandler); + backend.get('/exercises/:exerciseIdOrUrl/export/:folderName', SecurityMiddleware.check(true, SecurityCheckType.ASSIGNMENT_STAFF), this.exportLightExercise.bind(this) as RequestHandler); + backend.get('/exercises/:folderName/zip', SecurityMiddleware.check(false, SecurityCheckType.ASSIGNMENT_STAFF), this.zipExercise.bind(this) as RequestHandler); } - + private getExerciseName(assignment: Assignment, members: Array<Gitlab.UserSchema>, suffix: number): string { const memberNames: string = members.map(member => member.username).sort((a, b) => a.localeCompare(b)).join(' + '); const suffixString: string = suffix > 0 ? ` - ${ suffix }` : ''; return `DojoEx - ${ assignment.name } - ${ memberNames }${ suffixString }`; } - + private getExercisePath(assignment: Assignment, exerciseId: string): string { return `dojo-ex_${ (assignment.gitlabLastInfo as unknown as Gitlab.ProjectSchema).path }_${ exerciseId }`; } - + private async checkExerciseLimit(assignment: Assignment, members: Array<Gitlab.UserSchema>): Promise<Array<Gitlab.UserSchema>> { const exercises: Array<Exercise> | undefined = await ExerciseManager.getFromAssignment(assignment.name, { members: true }); const reachedLimitUsers: Array<Gitlab.UserSchema> = []; @@ -93,13 +96,13 @@ class ExerciseRoutes implements RoutesManager { } } } - + return reachedLimitUsers; } - + private async createExerciseRepository(assignment: Assignment, members: Array<Gitlab.UserSchema>, exerciseId: string, req: express.Request, res: express.Response): Promise<Gitlab.ProjectSchema | undefined> { let repository!: Gitlab.ProjectSchema; - + let suffix: number = 0; do { try { @@ -108,7 +111,7 @@ class ExerciseRoutes implements RoutesManager { } catch ( error ) { logger.error('Repo creation error'); logger.error(JSON.stringify(error)); - + if ( error instanceof GitbeakerRequestError && error.cause?.description ) { const description = error.cause.description as unknown; if ( GlobalHelper.isRepoNameAlreadyTaken(description) ) { @@ -123,101 +126,101 @@ class ExerciseRoutes implements RoutesManager { } } } while ( suffix < Config.exercise.maxSameName ); - + return repository; } - + private async createExercise(req: express.Request, res: express.Response) { const params: { members: Array<Gitlab.UserSchema> } = req.body; params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id); const assignment: Assignment = req.boundParams.assignment!; - - + + const reachedLimitUsers: Array<Gitlab.UserSchema> = await this.checkExerciseLimit(assignment, params.members); if ( reachedLimitUsers.length > 0 ) { req.session.sendResponse(res, StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE, reachedLimitUsers, 'Max exercise per assignment reached', DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED); return; } - - + + const exerciseId: string = uuidv4(); const secret: string = uuidv4(); const repository: Gitlab.ProjectSchema | undefined = await this.createExerciseRepository(assignment, params.members, exerciseId, req, res); - + if ( !repository ) { return; } - + await new Promise(resolve => setTimeout(resolve, Config.gitlab.repository.timeoutAfterCreation)); - + const repoCreationFnExec = GlobalHelper.repoCreationFnExecCreator(req, res, DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR, DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR, repository); - + try { await repoCreationFnExec(() => GitlabManager.protectBranch(repository.id, '*', false, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.DEVELOPER, Gitlab.AccessLevel.ADMIN), 'Branch protection modification error'); await repoCreationFnExec(() => GitlabManager.addRepositoryBadge(repository.id, Config.gitlab.badges.pipeline.link, Config.gitlab.badges.pipeline.imageUrl, 'Pipeline Status'), 'Pipeline badge addition error'); - + await repoCreationFnExec(async () => { await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_EXERCISE_ID', exerciseId, false, true); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_SECRET', secret, false, true); await GitlabManager.addRepositoryVariable(repository.id, 'DOJO_RESULTS_FOLDER', Config.exercise.pipelineResultsFolder, false, false); }, 'Pipeline variables addition error'); - + await repoCreationFnExec(() => GitlabManager.updateFile(repository.id, '.gitlab-ci.yml', fs.readFileSync(path.join(__dirname, '../../assets/exercise_gitlab_ci.yml'), 'base64'), 'Add .gitlab-ci.yml (DO NOT MODIFY THIS FILE)'), 'CI/CD file update error'); - + await repoCreationFnExec(async () => Promise.all([ ...new Set([ ...assignment.staff, ...params.members ].map(member => member.id)) ].map(GlobalHelper.addRepoMember(repository.id))), 'Add repository members error'); - + const exercise: Exercise = await repoCreationFnExec(() => db.exercise.create({ - data: { - id : exerciseId, - assignmentName : assignment.name, - name : repository.name, - secret : secret, - gitlabId : repository.id, - gitlabLink : repository.web_url, - gitlabCreationInfo: repository as unknown as Prisma.JsonObject, - gitlabLastInfo : repository as unknown as Prisma.JsonObject, - gitlabLastInfoDate: new Date(), - members : { - connectOrCreate: [ ...params.members.map(gitlabUser => { - return { - create: { - id : gitlabUser.id, - gitlabUsername: gitlabUser.name - }, - where : { - id: gitlabUser.id - } - }; - }) ] - } - } - })) as Exercise; - + data: { + id : exerciseId, + assignmentName : assignment.name, + name : repository.name, + secret : secret, + gitlabId : repository.id, + gitlabLink : repository.web_url, + gitlabCreationInfo: repository as unknown as Prisma.JsonObject, + gitlabLastInfo : repository as unknown as Prisma.JsonObject, + gitlabLastInfoDate: new Date(), + members : { + connectOrCreate: [ ...params.members.map(gitlabUser => { + return { + create: { + id : gitlabUser.id, + gitlabUsername: gitlabUser.name + }, + where : { + id: gitlabUser.id + } + }; + }) ] + } + } + })) as Exercise; + req.session.sendResponse(res, StatusCodes.OK, exercise); return; } catch ( error ) { /* Empty */ } } - + private async getAssignment(req: express.Request, res: express.Response) { const repoTree: Array<Gitlab.RepositoryTreeSchema> = await GitlabManager.getRepositoryTree(req.boundParams.exercise!.assignment.gitlabId); - + let assignmentHjsonFile!: Gitlab.RepositoryFileExpandedSchema; const immutableFiles: Array<Gitlab.RepositoryFileExpandedSchema> = await Promise.all(Config.assignment.baseFiles.map(async (baseFile: string) => { const file = await GitlabManager.getFile(req.boundParams.exercise!.assignment.gitlabId, baseFile); - + if ( baseFile === Config.assignment.filename ) { assignmentHjsonFile = file; } - + return file; })); - + const dojoAssignmentFile: AssignmentFile = JSON5.parse(atob(assignmentHjsonFile.content)); - + const immutablePaths = dojoAssignmentFile.immutable.map(fileDescriptor => fileDescriptor.path); - + await Promise.all(repoTree.map(async gitlabTreeFile => { if ( gitlabTreeFile.type === GitlabTreeFileType.BLOB.valueOf() ) { for ( const immutablePath of immutablePaths ) { @@ -228,33 +231,87 @@ class ExerciseRoutes implements RoutesManager { } } })); - + return req.session.sendResponse(res, StatusCodes.OK, { assignment : (req.boundParams.exercise as Exercise).assignment, assignmentFile: dojoAssignmentFile, immutable : immutableFiles }); } - + private async createResult(req: express.Request, res: express.Response) { const params: { exitCode: number, commit: Record<string, string>, results: ExerciseResultsFile, files: Array<IFileDirStat>, archiveBase64: string } = req.body; const exercise: Exercise = req.boundParams.exercise!; - + const result = await db.result.create({ - data: { - exerciseId: exercise.id, - exitCode : params.exitCode, - success : params.results.success!, - commit : params.commit, - results : params.results as unknown as Prisma.JsonObject, - files : params.files - } - }); - + data: { + exerciseId: exercise.id, + exitCode : params.exitCode, + success : params.results.success!, + commit : params.commit, + results : params.results as unknown as Prisma.JsonObject, + files : params.files + } + }); + fs.writeFileSync(path.join(Config.getResultsFolder(exercise), `${ result.dateTime.toISOString().replace(/:/g, '_') }.tar.gz`), params.archiveBase64, 'base64'); - + req.session.sendResponse(res, StatusCodes.OK); } + + private async exportLightExercise(req: express.Request, res: express.Response) { + try { + const folderName = req.params.folderName; + const parentDir = path.join('/tmp', folderName); + fs.mkdirSync(parentDir, { recursive: true }); + + const archive = await GitlabManager.archiveRepository(req.boundParams.exercise!.gitlabId); + const buffer = Buffer.from(await archive.arrayBuffer()); + const zipName = req.boundParams.exercise?.name.replace(/ /g, "_") + '.tar.gz'; + const zipPath = path.join(parentDir, zipName); + + fs.writeFile(zipPath, buffer, (err) => { + if (err) { + console.error('Error saving archive:', err); + } else { + console.log(`Archive ${zipName} saved successfully!`); + } + }); + + const finalZipPath = path.join('/tmp', `${folderName}.zip`); + const output = fs.createWriteStream(finalZipPath); + const archiveZip = archiver('zip', { + zlib: { level: 9 } // Compression maximale + }); + + archiveZip.pipe(output); + archiveZip.directory(parentDir, false); + await archiveZip.finalize(); + res.download(finalZipPath, (err) => { + if (err) { + console.error('Error sending zip:', err); + res.status(500).send('Error sending zip'); + } else { + console.log('Zip sent successfully!'); + } + }); + } catch (error) { + console.error('Error exporting assignment:', error); + res.status(500).send('Error exporting assignment'); + } + } + + private async zipExercise(req : express.Request, res : express.Response) { + const folderName = req.params.folderName; + const zipPath = path.join('/tmp', `${folderName}.zip`); + res.download(zipPath, (err) => { + if (err) { + logger.error(`Error sending the zip : ${err}`); + } else { + console.log("Zip sent successfully"); + } + }); + } } diff --git a/ExpressAPI/src/routes/UserRoutes.ts b/ExpressAPI/src/routes/UserRoutes.ts index 3755d01ff394f30214000b6030986bd0f53be3c4..247f7d84a7f81faca1b6712ba61b1917a40fbe86 100644 --- a/ExpressAPI/src/routes/UserRoutes.ts +++ b/ExpressAPI/src/routes/UserRoutes.ts @@ -11,6 +11,7 @@ class UserRoutes implements RoutesManager { backend.get('/users', SecurityMiddleware.check(true, SecurityCheckType.ADMIN), this.getUsers.bind(this) as RequestHandler); backend.patch('/users/:userId/role', SecurityMiddleware.check(true, SecurityCheckType.ADMIN), this.changeRole.bind(this) as RequestHandler); backend.get('/users/:userId/assignments', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), this.getUsersAssignments.bind(this) as RequestHandler); + backend.get('/users/:userId/exercise', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), this.getUsersExercise.bind(this) as RequestHandler); } private async getUsers(req: express.Request, res: express.Response) { @@ -25,8 +26,12 @@ class UserRoutes implements RoutesManager { id: id, }, include: { - assignments: true, - + assignments: true + // assignments: { + // where: { + // deleted: false + // } + // } }, // Include the assignments related to the user }); return req.session.sendResponse(res, StatusCodes.OK, user); @@ -52,6 +57,19 @@ class UserRoutes implements RoutesManager { return req.session.sendResponse(res, StatusCodes.FORBIDDEN); } } + + private async getUsersExercise(req: express.Request, res: express.Response) { + const id = +req.params.userId; + const user = await db.user.findUnique({ + where: { + id: id, + }, + include: { + exercises: true + }, // Include the assignments related to the user + }); + return req.session.sendResponse(res, StatusCodes.OK, user); + } } export default new UserRoutes();