import mongoose from 'mongoose'; import log4js from 'log4js'; import multer from 'multer'; import path from 'path'; import config from '../../config.js'; import { fileModel } from '../../database/schemas/management/file.schema.js'; import { printerModel } from '../../database/schemas/production/printer.schema.js'; import { deleteObject, editObject, newObject } from '../../database/database.js'; import { BUCKETS, deleteFile as deleteCephFile, uploadFile } from '../../database/ceph.js'; import { getFileMeta } from '../../utils.js'; import { createSlicerAuthCode, createSlicerSession } from './sessionStore.js'; const logger = log4js.getLogger('Slicer'); logger.level = config.server.logLevel; const slicerFileUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 500000000 }, }).single('file'); const receiveSlicerFile = (req, res) => new Promise((resolve, reject) => { slicerFileUpload(req, res, (error) => { if (error) { reject(error); } else { resolve(); } }); }); const parseFormBoolean = (value) => value === true || (typeof value === 'string' && value.toLowerCase() === 'true'); const getSlicerContentType = (extension, reportedContentType) => { // Slicers commonly label G-code as text/plain. That allows reverse proxies // to compress the response later, which can invalidate a known byte length. if (extension.toLowerCase() === '.gcode') { return 'application/octet-stream'; } return reportedContentType || 'application/octet-stream'; }; const SLICER_VERSION = { server: '1.5.0', api: '0.1', text: 'OctoPrint (Moonraker v0.9.3-72-g7cdcca3)', }; export const slicerUIRouteHandler = async (req, res) => { try { const { printerId } = req.params; console.log('req.user', req.user); const session = await createSlicerSession(req.user); const authCode = await createSlicerAuthCode(session); const uiUrl = new URL('/slicer', config.app.urlClient); uiUrl.searchParams.set('printerId', printerId); uiUrl.searchParams.set('authCode', authCode); return res.redirect(uiUrl.toString()); } catch (error) { logger.error('Failed to create slicer UI session:', error); return res.status(500).json({ error: 'Failed to authenticate slicer UI' }); } }; export const slicerVersionRouteHandler = async (req, res) => { const { printerId } = req.params; if (!mongoose.isValidObjectId(printerId)) { return res.status(404).send({ error: 'Printer not found.', code: 404 }); } const printer = await mongoose.connection .collection('printers') .findOne({ _id: new mongoose.Types.ObjectId(printerId) }, { projection: { _id: 1 } }); if (!printer) { return res.status(404).send({ error: 'Printer not found.', code: 404 }); } return res.json(SLICER_VERSION); }; export const slicerUploadRouteHandler = async (req, res) => { const { printerId } = req.params; if (!mongoose.isValidObjectId(printerId)) { return res.status(404).send({ error: 'Printer not found.', code: 404 }); } const printer = await printerModel.findById(printerId).select('_id pendingSlicerUploads').lean(); if (!printer) { return res.status(404).send({ error: 'Printer not found.', code: 404 }); } try { await receiveSlicerFile(req, res); } catch (uploadError) { logger.error('Error receiving slicer upload:', uploadError); const status = uploadError.code === 'LIMIT_FILE_SIZE' ? 413 : 400; return res.status(status).send({ error: uploadError.message }); } // OctoPrint uploads use a multipart `file` part. Multer places the binary // part on req.file and the accompanying select/print fields on req.body. const file = req.file; if (!file?.buffer || !file.originalname) { return res.status(400).send({ error: 'No file selected.' }); } const originalName = path.basename(file.originalname); const extension = path.extname(originalName); const baseName = path.parse(originalName).name; if (!extension || !baseName) { return res.status(400).send({ error: 'The uploaded file must have a valid filename.' }); } const shouldPrint = parseFormBoolean(req.body?.print); const shouldSelect = shouldPrint || parseFormBoolean(req.body?.select); const contentType = getSlicerContentType(extension, file.mimetype); let createdFile; let cephKey; try { const meta = await getFileMeta({ ...file, originalname: originalName }); createdFile = await newObject({ model: fileModel, newData: { name: baseName, type: contentType, extension, size: file.buffer.length, metaData: { originalName, ...meta, }, }, user: req.user, }); if (createdFile.error) { logger.error('Error creating slicer file:', createdFile.error); return res.status(createdFile.code || 500).send(createdFile); } cephKey = `files/${createdFile._id}${extension}`; await uploadFile(BUCKETS.FILES, cephKey, file.buffer, contentType, { originalName, uploadedBy: req.user?.username || 'slicer', }); const now = new Date(); const updatedPrinter = await editObject({ model: printerModel, id: printerId, updateData: { pendingSlicerUploads: [ ...(printer.pendingSlicerUploads || []), { file: createdFile, gcodeFile: null, shouldPrint, new: true, properties: {}, createdAt: now, updatedAt: now, }, ], }, user: req.user, }); if (updatedPrinter.error) { throw new Error(updatedPrinter.error); } logger.debug(`Slicer file ${createdFile._id} uploaded and queued for printer ${printerId}`); const uploadPath = (req.originalUrl || req.url).split('?')[0].replace(/\/$/, ''); const resource = `${uploadPath}/${encodeURIComponent(originalName)}`; res.location(resource); return res.status(201).send({ files: { local: { name: originalName, origin: 'local', path: originalName, type: 'machinecode', typePath: ['machinecode', extension.slice(1).toLowerCase()], refs: { resource, download: resource, }, }, }, done: true, effectiveSelect: shouldSelect, effectivePrint: shouldPrint, }); } catch (error) { logger.error('Error processing slicer upload:', error); if (cephKey) { try { await deleteCephFile(BUCKETS.FILES, cephKey); } catch (cleanupError) { logger.error('Error cleaning up slicer file from storage:', cleanupError); } } if (createdFile?._id) { try { await deleteObject({ model: fileModel, id: createdFile._id, user: req.user, }); } catch (cleanupError) { logger.error('Error cleaning up slicer file record:', cleanupError); } } return res.status(500).send({ error: error.message }); } };