@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
JavaScript
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