UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

155 lines (154 loc) 7.19 kB
import { captureException } from "@sentry/node"; import { join } from "node:path"; import { unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; //#region src/services/print-file-downloader.service.ts /** * Service responsible for downloading files from printers for analysis * Handles the printJob.needsFileDownload event */ var PrintFileDownloaderService = class PrintFileDownloaderService { logger; constructor(loggerFactory, eventEmitter2, printJobService, fileStorageService, fileAnalysisService, printerApiFactory, printerCache) { this.eventEmitter2 = eventEmitter2; this.printJobService = printJobService; this.fileStorageService = fileStorageService; this.fileAnalysisService = fileAnalysisService; this.printerApiFactory = printerApiFactory; this.printerCache = printerCache; this.logger = loggerFactory(PrintFileDownloaderService.name); this.eventEmitter2.on("printJob.needsFileDownload", (event) => { this.handleFileDownloadRequest(event.jobId).catch((error) => { this.logger.error(`Failed to handle file download for job ${event.jobId}`, error); captureException(error); }); }); this.logger.log("Print file downloader service initialized"); } async handleFileDownloadRequest(jobId) { this.logger.log(`Handling file download request for job ${jobId}`); try { const job = await this.printJobService.getJobByIdOrFail(jobId); if (job.fileStorageId) { this.logger.log(`Job ${jobId} already has fileStorageId ${job.fileStorageId} - skipping download`); return; } if (!job.printerId) { this.logger.error(`Job ${jobId} has no printerId - cannot download file`); return; } const printer = await this.printerCache.getValue(job.printerId); if (!printer) { this.logger.error(`Printer ${job.printerId} not found for job ${jobId}`); return; } const printerApi = this.printerApiFactory.getById(job.printerId); this.logger.log(`Downloading file ${job.fileName} from printer ${printer.name} (${printer.printerType})`); let fileBuffer; try { const response = await printerApi.downloadFile(job.fileName); const chunks = []; const stream = response.data; await new Promise((resolve, reject) => { stream.on("data", (chunk) => chunks.push(chunk)); stream.on("end", () => resolve()); stream.on("error", (err) => reject(err)); }); fileBuffer = Buffer.concat(chunks); } catch (downloadError) { this.logger.error(`Failed to download file from printer: ${downloadError}`); job.analysisState = "FAILED"; job.statusReason = `File download failed: ${downloadError instanceof Error ? downloadError.message : "Unknown error"}`; await this.printJobService.printJobRepository.save(job); return; } this.logger.log(`Downloaded ${fileBuffer.length} bytes for job ${jobId}`); const tempPath = join(tmpdir(), `fdm-monster-download-${jobId}-${Date.now()}-${job.fileName}`); writeFileSync(tempPath, fileBuffer); try { const fileHash = await this.fileStorageService.calculateFileHash(tempPath); this.logger.log(`File hash for job ${jobId}: ${fileHash.substring(0, 12)}...`); const existingJob = await this.fileStorageService.findDuplicateByHash(fileHash); let metadata; let fileStorageId; if (existingJob?.fileStorageId) { const cachedMetadata = await this.fileStorageService.loadMetadata(existingJob.fileStorageId); if (cachedMetadata) { this.logger.log(`Duplicate file detected (job ${existingJob.id}, hash match) - reusing storage ${existingJob.fileStorageId}`); metadata = { ...cachedMetadata, fileName: job.fileName }; fileStorageId = existingJob.fileStorageId; } else if (existingJob.analysisState === "ANALYZED" && existingJob.metadata) { this.logger.log(`Duplicate file with DB metadata (job ${existingJob.id}) - reusing storage ${existingJob.fileStorageId}`); metadata = { ...existingJob.metadata, fileName: job.fileName }; fileStorageId = existingJob.fileStorageId; await this.fileStorageService.saveMetadata(fileStorageId, metadata, fileHash, job.fileName); } else { this.logger.log(`Duplicate file not analyzed - reusing storage ${existingJob.fileStorageId}, analyzing now`); const existingFilePath = this.fileStorageService.getFilePath(existingJob.fileStorageId); metadata = (await this.fileAnalysisService.analyzeFile(existingFilePath)).metadata; fileStorageId = existingJob.fileStorageId; await this.fileStorageService.saveMetadata(fileStorageId, metadata, fileHash, job.fileName); } } else { this.logger.log(`Analyzing downloaded file: ${job.fileName}`); const analysisResult = await this.fileAnalysisService.analyzeFile(tempPath); metadata = analysisResult.metadata; const thumbnails = analysisResult.thumbnails; this.logger.log(`Analysis complete: format=${metadata.fileFormat}, layers=${metadata.totalLayers}, time=${metadata.gcodePrintTimeSeconds}s, filament=${metadata.filamentUsedGrams}g`); const fileObject = { path: tempPath, originalname: job.fileName, mimetype: "application/octet-stream", size: fileBuffer.length }; fileStorageId = await this.fileStorageService.saveFile(fileObject, fileHash); this.logger.log(`Saved file to storage: ${fileStorageId}`); let thumbnailMetadata = []; if (thumbnails.length > 0) { thumbnailMetadata = await this.fileStorageService.saveThumbnails(fileStorageId, thumbnails); this.logger.log(`Saved ${thumbnailMetadata.length} thumbnail(s) for ${fileStorageId}`); } await this.fileStorageService.saveMetadata(fileStorageId, metadata, fileHash, job.fileName, thumbnailMetadata); this.logger.log(`Saved metadata JSON for ${fileStorageId}`); } job.fileStorageId = fileStorageId; job.fileHash = fileHash; job.fileSize = fileBuffer.length; job.fileFormat = metadata.fileFormat; job.metadata = metadata; job.analysisState = "ANALYZED"; job.analyzedAt = /* @__PURE__ */ new Date(); await this.printJobService.printJobRepository.save(job); this.logger.log(`Successfully processed downloaded file for job ${jobId}: storageId=${fileStorageId}, analysisState=${job.analysisState}`); } finally { try { unlinkSync(tempPath); } catch (cleanupError) { this.logger.warn(`Failed to clean up temp file ${tempPath}: ${cleanupError}`); } } } catch (error) { this.logger.error(`Failed to download and analyze file for job ${jobId}`, error); captureException(error); try { const job = await this.printJobService.printJobRepository.findOne({ where: { id: jobId } }); if (job) { job.analysisState = "FAILED"; job.statusReason = `File download/analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`; await this.printJobService.printJobRepository.save(job); } } catch (updateError) { this.logger.error(`Failed to mark job ${jobId} as failed`, updateError); } } } }; //#endregion export { PrintFileDownloaderService }; //# sourceMappingURL=print-file-downloader.service.js.map