Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
231 lines
6.9 KiB
JavaScript
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 });
|
|
}
|
|
};
|