import dotenv from "dotenv"; import { gcodeFileModel } from "../../schemas/gcodefile.schema.js"; import { filamentModel } from "../../schemas/filament.schema.js"; import jwt from "jsonwebtoken"; import log4js from "log4js"; import multer from "multer"; import crypto from "crypto"; import path from "path"; import fs from "fs"; import mongoose from "mongoose"; import { extractConfigBlock } from "../../util/index.js"; dotenv.config(); const logger = log4js.getLogger("GCodeFiles"); logger.level = process.env.LOG_LEVEL; // Set storage engine const gcodeStorage = multer.diskStorage({ destination: process.env.GCODE_STORAGE, filename: async function (req, file, cb) { // Retrieve custom file name from request body const customFileName = req.params.id || "default"; // Default to 'default' if not provided // Create the final filename ensuring it ends with .gcode const finalFilename = `${customFileName}.gcode`; // Call callback with the final filename cb(null, finalFilename); }, }); // Initialise upload const gcodeUpload = multer({ storage: gcodeStorage, limits: { fileSize: 500000000 }, // 50MB limit fileFilter: function (req, file, cb) { checkFileType(file, cb); }, }).single("gcodeFile"); // The name attribute of the file input in the HTML form // Check file type function checkFileType(file, cb) { // Allowed ext const filetypes = /g|gco|gcode/; // Check ext const extname = filetypes.test(path.extname(file.originalname).toLowerCase()); if (extname) { console.log(file); return cb(null, true); } else { cb("Error: .g, .gco, and .gcode files only!"); } } export const listGCodeFilesRouteHandler = async ( req, res, page = 1, limit = 25, property = "", filter = {}, search = "", sort = "", order = "ascend" ) => { try { // Calculate the skip value based on the page number and limit const skip = (page - 1) * limit; let gcodeFile; let aggregateCommand = []; if (search) { // Add a text search match stage for name and brand fields aggregateCommand.push({ $match: { $text: { $search: search, }, }, }); } aggregateCommand.push({ $lookup: { from: "filaments", // The name of the Filament collection localField: "filament", foreignField: "_id", as: "filament", }, }); aggregateCommand.push({ $unwind: { path: "$filament", preserveNullAndEmptyArrays: true, // Keep documents without a matching filament }, }); aggregateCommand.push({ $addFields: { filament: "$filament", }, }); aggregateCommand.push({ $lookup: { from: "vendors", // The collection name (usually lowercase plural) localField: "filament.vendor", // The field in your current model foreignField: "_id", // The field in the products collection as: "filament.vendor", // The output field name }, }); aggregateCommand.push({ $unwind: "$filament.vendor" }); if (filter != {}) { // use filtering if present aggregateCommand.push({ $match: filter }); } if (property != "") { aggregateCommand.push({ $group: { _id: `$${property}` } }); // group all same properties aggregateCommand.push({ $project: { _id: 0, [property]: "$_id" } }); // rename _id to the property name } else { aggregateCommand.push({ $project: { "filament.gcodeFileInfo.estimatedPrintingTimeNormalMode": 0, url: 0, "filament.image": 0, "filament.createdAt": 0, "filament.updatedAt": 0, }, }); } // Add sorting if sort parameter is provided if (sort) { const sortOrder = order === "descend" ? -1 : 1; aggregateCommand.push({ $sort: { [sort]: sortOrder } }); } aggregateCommand.push({ $skip: skip }); aggregateCommand.push({ $limit: Number(limit) }); console.log(aggregateCommand); gcodeFile = await gcodeFileModel.aggregate(aggregateCommand); logger.trace( `List of gcode files (Page ${page}, Limit ${limit}, Property ${property}, Sort ${sort}, Order ${order}):`, gcodeFile, ); res.send(gcodeFile); } catch (error) { logger.error("Error listing gcode files:", error); res.status(500).send({ error: error }); } }; export const getGCodeFileContentRouteHandler = async (req, res) => { try { // Get ID from params const id = new mongoose.Types.ObjectId(req.params.id); // Fetch the gcodeFile with the given remote address const gcodeFile = await gcodeFileModel.findOne({ _id: id, }); if (!gcodeFile) { logger.warn(`GCodeFile not found with supplied id.`); return res.status(404).send({ error: "Print job not found." }); } logger.trace(`Returning GCode File contents with ID: ${id}:`); const filePath = path.join( process.env.GCODE_STORAGE, gcodeFile.gcodeFileName, ); // Read the file fs.readFile(filePath, "utf8", (err, data) => { if (err) { if (err.code === "ENOENT") { // File not found return res.status(404).send({ error: "File not found!" }); } else { // Other errors return res.status(500).send({ error: "Error reading file." }); } } // Send the file contents in the response res.send(data); }); } catch (error) { logger.error("Error fetching GCodeFile:", error); res.status(500).send({ error: error.message }); } }; export const editGCodeFileRouteHandler = async (req, res) => { try { // Get ID from params const id = new mongoose.Types.ObjectId(req.params.id); // Fetch the gcodeFile with the given remote address const gcodeFile = await gcodeFileModel.findOne({ _id: id }); if (!gcodeFile) { // Error handling logger.warn(`GCodeFile not found with supplied id.`); return res.status(404).send({ error: "Print job not found." }); } logger.trace(`GCodeFile with ID: ${id}:`, gcodeFile); try { const updateData = { updatedAt: new Date(), name: req.body.name, filament: req.body?.filament?._id, }; const result = await gcodeFileModel.updateOne( { _id: id }, { $set: updateData }, ); if (result.nModified === 0) { logger.error("No gcodeFile updated."); res.status(500).send({ error: "No gcodeFiles updated." }); } } catch (updateError) { logger.error("Error updating gcodeFile:", updateError); res.status(500).send({ error: updateError.message }); } res.send("OK"); } catch (fetchError) { logger.error("Error fetching gcodeFile:", fetchError); //res.status(500).send({ error: fetchError.message }); } }; export const newGCodeFileRouteHandler = async (req, res) => { var filament = null; try { // Get ID from params const id = new mongoose.Types.ObjectId(req.body.filament._id); // Fetch the filament with the given remote address filament = await filamentModel.findOne({ _id: id, }); if (!filament) { logger.warn(`Filament not found with supplied id.`); return res.status(404).send({ error: "Filament not found." }); } logger.trace(`Filament with ID: ${id}:`, filament); } catch (error) { logger.error("Error fetching filament:", error); return res.status(500).send({ error: error.message }); } try { const newGCodeFile = { createdAt: new Date(), updatedAt: new Date(), gcodeFileInfo: req.body.gcodeFileInfo, filament: req.body.filament._id, name: req.body.name, cost: (filament.cost / 1000) * req.body.gcodeFileInfo.filamentUsedG, }; const result = await gcodeFileModel.create(newGCodeFile); if (result.nCreated === 0) { logger.error("No gcode file created."); res.status(500).send({ error: "No gcode file created." }); } res.status(200).send(result); } catch (updateError) { logger.error("Error creating gcode file:", updateError); res.status(500).send({ error: updateError.message }); } }; export const parseGCodeFileHandler = async (req, res) => { try { // Use the same upload middleware as the uploadGCodeFileContentRouteHandler gcodeUpload(req, res, async (err) => { if (err) { return res.status(500).send({ error: err, }); } if (req.file == undefined) { return res.send({ message: "No file selected!", }); } try { // Get the path to the uploaded file const filePath = path.join(req.file.destination, req.file.filename); // Read the file content const fileContent = fs.readFileSync(filePath, "utf8"); // Extract the config block const configInfo = extractConfigBlock(fileContent); // Return the config as JSON res.json(configInfo); // Optionally clean up the file after processing if it's not needed fs.unlinkSync(filePath); } catch (parseError) { logger.error("Error parsing GCode file:", parseError); res.status(500).send({ error: parseError.message }); } }); } catch (error) { logger.error("Error in parseGCodeFileHandler:", error); res.status(500).send({ error: error.message }); } }; export const uploadGCodeFileContentRouteHandler = async (req, res) => { try { // Get ID from params const id = new mongoose.Types.ObjectId(req.params.id); // Fetch the gcodeFile with the given remote address const gcodeFile = await gcodeFileModel.findOne({ _id: id }); if (!gcodeFile) { // Error handling logger.warn(`GCodeFile not found with supplied id.`); return res.status(404).send({ error: "Print job not found." }); } logger.trace(`GCodeFile with ID: ${id}`); try { gcodeUpload(req, res, async (err) => { if (err) { res.status(500).send({ error: err, }); } else { if (req.file == undefined) { res.send({ message: "No file selected!", }); } else { // Get the path to the uploaded file const filePath = path.join(req.file.destination, req.file.filename); // Read the file content const fileContent = fs.readFileSync(filePath, "utf8"); // Update the gcodeFile document with the filename and the extracted config const result = await gcodeFileModel.updateOne( { _id: id }, { $set: { gcodeFileName: req.file.filename, }, }, ); if (result.nModified === 0) { logger.error("No gcodeFile updated."); res.status(500).send({ error: "No gcodeFiles updated." }); } res.send({ status: "OK", file: `${req.file.filename}`, }); } } }); } catch (updateError) { logger.error("Error updating gcodeFile:", updateError); res.status(500).send({ error: updateError.message }); } } catch (fetchError) { logger.error("Error fetching gcodeFile:", fetchError); res.status(500).send({ error: fetchError.message }); } }; export const getGCodeFileRouteHandler = async (req, res) => { try { // Get ID from params const id = new mongoose.Types.ObjectId(req.params.id); // Fetch the gcodeFile with the given remote address const gcodeFile = await gcodeFileModel .findOne({ _id: id, }) .populate("filament"); if (!gcodeFile) { logger.warn(`GCodeFile not found with supplied id.`); return res.status(404).send({ error: "Print job not found." }); } logger.trace(`GCodeFile with ID: ${id}:`); res.send(gcodeFile); } catch (error) { logger.error("Error fetching GCodeFile:", error); res.status(500).send({ error: error.message }); } };