231 lines
6.9 KiB
JavaScript

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;
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);
}
createFileThumbnails(file.buffer, 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 });
}
};