import { createHash } from "node:crypto"; import { readFile, unlink } from "node:fs/promises"; import type { User as DomainUser } from "@cv/auth"; import { JwtAuthGuard } from "@cv/auth"; import { validateFile } from "@cv/file-upload"; import { BadRequestException, Body, Controller, Logger, Post, Req, UploadedFile, UseGuards, UseInterceptors, } from "@nestjs/common"; import { FileInterceptor } from "@nestjs/platform-express"; import { FileImportSource } from "@/modules/data-import/sources/file-import-source"; import { ImportService } from "@/modules/data-import/import.service"; @Controller("api/cv-parser") export class FileUploadController { private readonly logger = new Logger(FileUploadController.name); constructor( private readonly importService: ImportService, private readonly fileImportSource: FileImportSource, ) {} @Post("upload") @UseGuards(JwtAuthGuard) @UseInterceptors(FileInterceptor("file")) async upload( @Req() req: { user: DomainUser }, @Body("profileId") profileId: string, @UploadedFile() file?: Express.Multer.File, ) { if (!file) { throw new BadRequestException("No file provided"); } if (!profileId) { throw new BadRequestException("profileId is required"); } const filePath = file.path; try { const buffer = file.buffer ?? (await readFile(filePath)); const validation = validateFile({ buffer, mimeType: file.mimetype, originalName: file.originalname, sizeBytes: file.size, }); if (!validation.valid) { throw new Error(`File validation failed: ${validation.error}`); } const fingerprint = createHash("sha256").update(buffer).digest("hex"); const userFile = await this.importService.createImport( req.user, profileId, this.fileImportSource, { fileName: file.originalname, mimeType: file.mimetype, sizeBytes: file.size, fingerprint, }, { buffer, mimeType: file.mimetype }, ); return { userFileId: userFile.id, status: userFile.status, fileName: userFile.fileName, sizeBytes: userFile.sizeBytes, }; } catch (error) { this.logger.error(`File upload failed: ${error instanceof Error ? error.message : error}`, error instanceof Error ? error.stack : undefined); const message = error instanceof Error ? error.message : "Failed to process file"; throw new BadRequestException(message); } finally { if (filePath) { try { await unlink(filePath); } catch (err) { this.logger.warn(`Failed to clean up temporary file ${filePath}: ${err instanceof Error ? err.message : err}`); } } } } }