import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import * as fs from 'fs/promises'; import * as path from 'path'; import { v4 as uuidv4 } from 'uuid'; import { resultsDir, uploadsDir } from '../config'; import { PipelineService } from '../pipeline/pipeline.service'; import { RunParamsInput } from '../pipeline/types'; import { DatabaseService, RunRow } from './database.service'; export interface CreateRunOptions { tmpFilePath: string; originalFilename: string; params: RunParamsInput; } interface QueueJob { id: string; inputPath: string; originalFilename: string; params: RunParamsInput; } @Injectable() export class RunsService { private readonly logger = new Logger(RunsService.name); private queue: QueueJob[] = []; private processing = false; constructor( private readonly db: DatabaseService, private readonly pipeline: PipelineService, ) {} async createRun(opts: CreateRunOptions): Promise<{ id: string }> { const id = uuidv4(); const runUploadDir = path.join(uploadsDir(), id); await fs.mkdir(runUploadDir, { recursive: true }); const ext = this.extensionOf(opts.originalFilename); const destPath = path.join(runUploadDir, `input${ext}`); await fs.rename(opts.tmpFilePath, destPath); this.db.insert({ id, original_filename: opts.originalFilename, status: 'pending', created_at: new Date().toISOString(), params_json: JSON.stringify(opts.params), }); this.queue.push({ id, inputPath: destPath, originalFilename: opts.originalFilename, params: opts.params }); void this.processQueue(); return { id }; } private async processQueue() { if (this.processing) return; this.processing = true; try { while (this.queue.length > 0) { const job = this.queue.shift()!; await this.runJob(job); } } finally { this.processing = false; } } private async runJob(job: QueueJob) { this.db.updateStatus(job.id, 'running'); const workDir = path.join(resultsDir(), job.id); await fs.mkdir(workDir, { recursive: true }); const onProgress = (msg: string) => { this.logger.log(`[${job.id}] ${msg}`); this.db.appendLog(job.id, msg); }; try { const report = await this.pipeline.execute(job.id, job.inputPath, job.originalFilename, workDir, job.params, onProgress); this.db.complete(job.id, JSON.stringify(report)); onProgress('Run termine avec succes.'); } catch (err: any) { this.logger.error(`Run ${job.id} en erreur: ${err.message}`); this.db.appendLog(job.id, `ERREUR FATALE: ${err.message}`); this.db.fail(job.id, err.message); } } list() { return this.db.list().map((r) => this.toSummary(r)); } getDetail(id: string) { const row = this.db.get(id); if (!row) throw new NotFoundException(`Run ${id} introuvable`); return { id: row.id, originalFilename: row.original_filename, status: row.status, createdAt: row.created_at, finishedAt: row.finished_at, params: row.params_json ? JSON.parse(row.params_json) : null, report: row.report_json ? JSON.parse(row.report_json) : null, log: row.log, error: row.error, }; } getRowOrThrow(id: string): RunRow { const row = this.db.get(id); if (!row) throw new NotFoundException(`Run ${id} introuvable`); return row; } workDirFor(id: string): string { return path.join(resultsDir(), id); } async delete(id: string) { this.getRowOrThrow(id); // leve NotFoundException si le run n'existe pas this.db.remove(id); const work = this.workDirFor(id); const upload = path.join(uploadsDir(), id); await fs.rm(work, { recursive: true, force: true }); await fs.rm(upload, { recursive: true, force: true }); return { id }; } private toSummary(row: RunRow) { let summary: any = null; if (row.report_json) { try { const report = JSON.parse(row.report_json); const okLevels = (report.levels ?? []).filter((l: any) => l.status === 'ok'); const first = okLevels[0]; const last = okLevels[okLevels.length - 1]; summary = { levelsOk: okLevels.length, levelsTotal: report.levels?.length ?? 0, volumeLevel0: first?.volume ?? null, volumeLastLevel: last?.volume ?? null, volumeDeltaPct: first && last && first.volume ? (100 * (last.volume - first.volume)) / first.volume : null, }; } catch { summary = null; } } return { id: row.id, originalFilename: row.original_filename, status: row.status, createdAt: row.created_at, finishedAt: row.finished_at, error: row.error, summary, }; } private extensionOf(filename: string): string { const lower = filename.toLowerCase(); if (lower.endsWith('.copc.laz')) return '.copc.laz'; const idx = lower.lastIndexOf('.'); return idx >= 0 ? filename.slice(idx) : ''; } }