@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.
254 lines (253 loc) • 13.1 kB
JavaScript
import { __exportAll } from "../_virtual/_rolldown/runtime.js";
import { __decorateMetadata } from "../_virtual/_@oxc-project_runtime@0.129.0/helpers/decorateMetadata.js";
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.129.0/helpers/decorate.js";
import "../services/printer-api.interface.js";
import { NotFoundException, ValidationException } from "../exceptions/runtime.exceptions.js";
import { validateInput } from "../handlers/validators.js";
import { AppConstants } from "../server.constants.js";
import { errorSummary } from "../utils/error.utils.js";
import { PERMS, ROLES } from "../constants/authorization.constants.js";
import { MulterService } from "../services/core/multer.service.js";
import { authenticate, authorizeRoles, permission } from "../middleware/authenticate.js";
import { FileStorageService } from "../services/file-storage.service.js";
import { FileAnalysisService } from "../services/file-analysis.service.js";
import { PrintJobService } from "../services/orm/print-job.service.js";
import { downloadFileSchema, getFileSchema, getFilesSchema, startPrintFileSchema, uploadFileSchema } from "./validation/printer-files-controller.validation.js";
import { printerResolveMiddleware } from "../middleware/printer.js";
import { PrinterThumbnailCache } from "../state/printer-thumbnail.cache.js";
import { getScopedPrinter } from "../middleware/printer-resolver.js";
import { captureException } from "@sentry/node";
import { extname } from "node:path";
import { copyFileSync, createReadStream, existsSync, unlinkSync } from "node:fs";
import { DELETE, GET, POST, before, route } from "awilix-express";
//#region src/controllers/printer-files.controller.ts
var printer_files_controller_exports = /* @__PURE__ */ __exportAll({ PrinterFilesController: () => PrinterFilesController });
var _ref, _ref2, _ref3, _ref4, _ref5, _PrinterFilesController;
let PrinterFilesController = _PrinterFilesController = class PrinterFilesController {
logger;
constructor(loggerFactory, printerApi, printJobService, fileAnalysisService, fileStorageService, multerService, printerThumbnailCache) {
this.printerApi = printerApi;
this.printJobService = printJobService;
this.fileAnalysisService = fileAnalysisService;
this.fileStorageService = fileStorageService;
this.multerService = multerService;
this.printerThumbnailCache = printerThumbnailCache;
this.logger = loggerFactory(_PrinterFilesController.name);
}
async getThumbnails(req, res) {
const thumbnails = await this.printerThumbnailCache.getAllValues();
res.send(thumbnails);
}
async getFiles(req, res) {
const { printerApi } = getScopedPrinter(req);
const { recursive: recursiveStr, startDir } = await validateInput(req.query, getFilesSchema);
const recursive = recursiveStr === "true";
const files = await printerApi.getFiles(recursive, startDir);
res.send(files);
}
async startPrintFile(req, res) {
const { currentPrinterId } = getScopedPrinter(req);
const { filePath } = await validateInput(req.body, startPrintFileSchema);
const encodedFilePath = filePath.split("/").map(encodeURIComponent).join("/");
await this.printerApi.startPrint(encodedFilePath);
this.logger.log(`Started print for printer ${currentPrinterId}`);
res.send();
}
async downloadFile(req, res) {
this.logger.log(`Downloading file ${req.params.path}`);
const { path } = await validateInput(req.params, downloadFileSchema);
const encodedFilePath = path.split("/").map(encodeURIComponent).join("/");
const response = await this.printerApi.downloadFile(encodedFilePath);
res.setHeader("Content-Type", response.headers["content-type"]);
res.setHeader("Content-Length", response.headers["content-length"]);
res.setHeader("Content-Disposition", response.headers["content-disposition"]);
if (response.headers["etag"]?.length) res.setHeader("ETag", response.headers["etag"]);
response.data.pipe(res);
}
async deleteFileOrFolder(req, res) {
const { path } = await validateInput(req.query, getFileSchema);
const encodedFilePath = path.split("/").map(encodeURIComponent).join("/");
const result = await this.printerApi.deleteFile(encodedFilePath);
res.send(result);
}
async getPrinterThumbnail(req, res) {
const { currentPrinterId } = getScopedPrinter(req);
const printerThumbnail = await this.printerThumbnailCache.getValue(currentPrinterId);
res.send(printerThumbnail);
}
async uploadPrinterFile(req, res) {
const { currentPrinterId, currentPrinter } = getScopedPrinter(req);
const acceptedExtensions = this.getAcceptedFileExtensions(currentPrinter.printerType);
const files = await this.multerService.multerLoadFileAsync(req, res, acceptedExtensions, true);
const { startPrint: startPrintString } = await validateInput(req.body, uploadFileSchema);
const startPrint = startPrintString === "true";
if (!files?.length) throw new ValidationException({ error: `No file was available for upload. Did you upload files with one of these extensions: ${acceptedExtensions.join(", ")}?` });
if (files.length > 1) throw new ValidationException({ error: "Only 1 file can be uploaded at a time" });
const uploadedFile = files[0];
const token = this.multerService.startTrackingSession(uploadedFile, currentPrinterId);
await this.printerApi.uploadFile({
stream: createReadStream(uploadedFile.path),
fileName: uploadedFile.originalname,
contentLength: uploadedFile.size,
startPrint,
uploadToken: token
}).catch((e) => {
try {
this.multerService.clearUploadedFile(uploadedFile);
} catch (e) {
this.logger.error(`Could not remove uploaded file from temporary storage ${errorSummary(e)}`);
}
throw e;
});
const ext = extname(uploadedFile.originalname);
const tempPathWithExt = uploadedFile.path + ext;
try {
this.logger.log(`Processing uploaded file: ${uploadedFile.originalname} (ext: ${ext})`);
if (!existsSync(uploadedFile.path)) throw new NotFoundException(`Upload file does not exist: ${uploadedFile.path}`);
copyFileSync(uploadedFile.path, tempPathWithExt);
const fileHash = await this.fileStorageService.calculateFileHash(tempPathWithExt);
this.logger.log(`File hash: ${fileHash.substring(0, 12)}...`);
const existingJob = await this.fileStorageService.findDuplicateByHash(fileHash);
let metadata;
let fileStorageId;
if (existingJob && 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: uploadedFile.originalname
};
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: uploadedFile.originalname
};
fileStorageId = existingJob.fileStorageId;
await this.fileStorageService.saveMetadata(fileStorageId, metadata, fileHash, uploadedFile.originalname);
} 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, uploadedFile.originalname);
this.logger.log(`Analysis complete and cached: ${fileStorageId}`);
}
} else {
this.logger.log(`Analyzing new file: ${uploadedFile.originalname}`);
const analysisResult = await this.fileAnalysisService.analyzeFile(tempPathWithExt);
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, thumbnails=${thumbnails.length}`);
fileStorageId = await this.fileStorageService.saveFile(uploadedFile, fileHash);
this.logger.log(`Saved file to storage: ${fileStorageId} (deterministic from hash+name)`);
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, uploadedFile.originalname, thumbnailMetadata);
this.logger.log(`Saved metadata JSON for ${fileStorageId}`);
}
const job = await this.printJobService.createPendingJob(currentPrinterId, uploadedFile.originalname, metadata, currentPrinter.name);
job.fileStorageId = fileStorageId;
job.fileHash = fileHash;
await this.printJobService.updateJob(job);
this.logger.log(`Created job ${job.id}: format=${job.fileFormat}, state=${job.analysisState}, storageId=${fileStorageId}, hash=${fileHash.substring(0, 8)}...`);
if (existsSync(tempPathWithExt)) unlinkSync(tempPathWithExt);
} catch (error) {
this.logger.error(`File processing failed: ${errorSummary(error)}`);
captureException(error);
} finally {
try {
this.multerService.clearUploadedFile(uploadedFile);
} catch (e) {
this.logger.error(`Could not remove uploaded file from temporary storage ${errorSummary(e)}`);
}
}
res.send();
}
getAcceptedFileExtensions(printerType) {
if (printerType === 3) return AppConstants.defaultAcceptedBambuExtensions;
return AppConstants.defaultAcceptedGcodeExtensions;
}
};
__decorate([
GET(),
route("/thumbnails"),
before(permission(PERMS.PrinterFiles.Get)),
__decorateMetadata("design:type", Function),
__decorateMetadata("design:paramtypes", [Object, Object]),
__decorateMetadata("design:returntype", Promise)
], PrinterFilesController.prototype, "getThumbnails", null);
__decorate([
GET(),
route("/:id"),
before(permission(PERMS.PrinterFiles.Get)),
__decorateMetadata("design:type", Function),
__decorateMetadata("design:paramtypes", [Object, Object]),
__decorateMetadata("design:returntype", Promise)
], PrinterFilesController.prototype, "getFiles", null);
__decorate([
POST(),
route("/:id/select"),
route("/:id/print"),
before(permission(PERMS.PrinterFiles.Actions)),
__decorateMetadata("design:type", Function),
__decorateMetadata("design:paramtypes", [Object, Object]),
__decorateMetadata("design:returntype", Promise)
], PrinterFilesController.prototype, "startPrintFile", null);
__decorate([
GET(),
route("/:id/download/:path"),
before(permission(PERMS.PrinterFiles.Get)),
__decorateMetadata("design:type", Function),
__decorateMetadata("design:paramtypes", [Object, Object]),
__decorateMetadata("design:returntype", Promise)
], PrinterFilesController.prototype, "downloadFile", null);
__decorate([
DELETE(),
route("/:id"),
before(permission(PERMS.PrinterFiles.Delete)),
__decorateMetadata("design:type", Function),
__decorateMetadata("design:paramtypes", [Object, Object]),
__decorateMetadata("design:returntype", Promise)
], PrinterFilesController.prototype, "deleteFileOrFolder", null);
__decorate([
GET(),
route("/:id/thumbnail"),
before(permission(PERMS.PrinterFiles.Get)),
__decorateMetadata("design:type", Function),
__decorateMetadata("design:paramtypes", [Object, Object]),
__decorateMetadata("design:returntype", Promise)
], PrinterFilesController.prototype, "getPrinterThumbnail", null);
__decorate([
POST(),
route("/:id/upload"),
before(permission(PERMS.PrinterFiles.Upload)),
__decorateMetadata("design:type", Function),
__decorateMetadata("design:paramtypes", [Object, Object]),
__decorateMetadata("design:returntype", Promise)
], PrinterFilesController.prototype, "uploadPrinterFile", null);
PrinterFilesController = _PrinterFilesController = __decorate([
route(AppConstants.apiRoute + "/printer-files"),
before([
authenticate(),
authorizeRoles([ROLES.ADMIN, ROLES.OPERATOR]),
printerResolveMiddleware()
]),
__decorateMetadata("design:paramtypes", [
Object,
Object,
typeof (_ref = typeof PrintJobService !== "undefined" && PrintJobService) === "function" ? _ref : Object,
typeof (_ref2 = typeof FileAnalysisService !== "undefined" && FileAnalysisService) === "function" ? _ref2 : Object,
typeof (_ref3 = typeof FileStorageService !== "undefined" && FileStorageService) === "function" ? _ref3 : Object,
typeof (_ref4 = typeof MulterService !== "undefined" && MulterService) === "function" ? _ref4 : Object,
typeof (_ref5 = typeof PrinterThumbnailCache !== "undefined" && PrinterThumbnailCache) === "function" ? _ref5 : Object
])
], PrinterFilesController);
//#endregion
export { PrinterFilesController, printer_files_controller_exports };
//# sourceMappingURL=printer-files.controller.js.map