import Logger from '@dzeio/logger' import { objectMap, objectOmit } from '@dzeio/object-util' import URLManager from '@dzeio/url-manager' import type { APIRoute } from 'astro' import { evaluate } from 'mathjs' import { spawn } from 'node:child_process' import fs from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import StatusCode from '../../../../libs/HTTP/StatusCode' import { buildRFC7807 } from '../../../../libs/RFCs/RFC7807' import { getParams } from '../../../../libs/gcodeUtils' import DaoFactory from '../../../../models/DaoFactory' interface SliceError { code: number output: Array } let tmpDir: string /** * body is the stl * query * price: algorithm from settings * adionnal settings from https://manual.slic3r.org/advanced/command-line */ export const post: APIRoute = async ({ params, request, locals }) => { if (!tmpDir) { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'saas-')) } const configId = params.configId ?? 'undefined' const config = await DaoFactory.get('config').get(configId) if (!config) { return buildRFC7807({ type: '/docs/errors/missing-config', status: StatusCode.NOT_FOUND, title: 'The configuration does not exists', details: `The configuration ${configId} does not exists` }) } const input = new Uint8Array(Buffer.from(await request.arrayBuffer())) if (input.length <= 0) { return buildRFC7807({ type: '/docs/errors/missing-input-file', status: StatusCode.BAD_REQUEST, title: 'You are missing the STL file', details: `To process a file you need to input the file binary datas as the only body in your request` }) } const query = new URLManager(request.url).query() const processId = (Math.random() * 1000000).toFixed(0) const logger = new Logger(`process-${processId}`) const processFolder = `${tmpDir}/${processId}` const pouet = await fs.mkdir(processFolder, { recursive: true }) logger.log('poeut', pouet) logger.log('started processing request') logger.log('writing configs to dir') for (const file of config.files) { await fs.writeFile(`${processFolder}/${file.name}`, file.data) } const overrides = objectOmit(query, 'algo') const stlPath = `${processFolder}/input.stl` const gcodePath = `${processFolder}/output.gcode` logger.log('writing STL to filesystem') // write input await fs.writeFile(stlPath, input, { encoding: null }) // additionnal parameters let additionnalParams = objectMap(overrides, (value, key) => `--${(key as string).replace(/_/g, '-')} ${value}`).join(' ') let slicerPath: string let slicerCommand: string if (config.type === 'prusa' || true) { slicerPath = import.meta.env.PRUSASLICER_PATH ?? 'prusa-slicer' additionnalParams += ' --export-gcode --loglevel 4' slicerCommand = `${path.normalize(stlPath)} --load ${path.normalize(`${processFolder}/config.ini`)} --output ${path.normalize(gcodePath)} ${additionnalParams}` } // TODO: check if it does work on a linux environment // TODO: Externalise IO for the different slicers try { logger.log('Running', slicerPath, slicerCommand) await new Promise((res, rej) => { const logs: Array = [] const slicer = spawn(`"${slicerPath}"`, slicerCommand.split(' '), { shell: true }) const log = (data: Buffer) => { const line = `${data.toString('utf-8')}` logger.log(line) logs.push(line) } slicer.stdout.on('data', log) slicer.stderr.on('data', log) slicer.on('spawn', () => { logs.push('Process spawned') logger.log('Process spawned') }) slicer.on('error', (err) => { logs.push('Process error') logger.log('Process error') logger.log('error', err) logs.push(err.toString()) rej(err) }) slicer.on('close', (code, signal) => { logs.push('Process closed') logger.log('Process closed') logs.push(`with code ${code}`) logger.log(`with code ${code}`) logs.push(`and signal ${signal}`) logger.log(`and signal ${signal}`) if (typeof code === 'number' && code !== 0) { rej({ code: code, output: logs } as SliceError) return } res() }) }) } catch (e: any) { const err = e as SliceError logger.log('request finished in error :(', processId) const line = err.toString() logger.error('error', err, typeof err) if (err.code === 3221226505 || line.includes('Objects could not fit on the bed')) { await fs.rm(stlPath) return buildRFC7807({ type: '/docs/errors/object-too-large', status: StatusCode.PAYLOAD_TOO_LARGE, title: 'Object is too large', details: 'The STL you are trying to compile is too large for the configuration you chose' }, locals.responseBuilder) } else if (line.includes('No such file')) { await fs.rm(stlPath) return buildRFC7807({ type: '/docs/errors/missing-config-file', status: StatusCode.NOT_FOUND, title: 'Configuration file is missing', details: `the configuration file "${configId}" is not available on the remote server` }, locals.responseBuilder) } else if (line.includes('Unknown option')) { await fs.rm(stlPath) return buildRFC7807({ type: '/docs/errors/slicer-option-unknown', status: 400, title: ' config override doew not exists', details: 'an override does not exists, please contact an administrator or refer to the documentation' }, locals.responseBuilder) } else if ( line.includes('is not recognized as an internal or external command') || line.includes('.dll was not loaded') ) { await fs.rm(stlPath) return buildRFC7807({ type: '/docs/errors/slicer-not-found', status: StatusCode.SERVICE_UNAVAILABLE, title: 'the slicer used to process this file has not been found', details: 'the server has a misconfiguration meaning that we can\'t process the request, please contact an administrator', additionnalInfo: line.includes('dll') ? 'Missing DLL' : 'Slicer not found ' }, locals.responseBuilder) } else if (line.includes('ETIMEDOUT')) { await fs.rm(stlPath) return buildRFC7807({ type: '/docs/errors/timed-out-slicing', status: StatusCode.PAYLOAD_TOO_LARGE, title: 'Timed out slicing file', detail: `The file you are trying to process takes too long to be processed`, processingTimeoutMillis: 60000 }, locals.responseBuilder) } return buildRFC7807({ type: '/docs/errors/general-input-output-error', status: StatusCode.INTERNAL_SERVER_ERROR, title: 'General I/O error', details: 'A server error make it impossible to slice the file, please contact an administrator with the json error', fileId: processId, config: configId, // fileSize: req.body.length, overrides: overrides, serverMessage: err.output.map((line) => line.replace(new RegExp(stlPath), `***FILE***`).replace(new RegExp(processFolder), '')) }, locals.responseBuilder) } const gcode = await fs.readFile(gcodePath, 'utf-8') await fs.rm(processFolder, { recursive: true, force: true }) logger.log('Getting parameters') const gcodeParams = getParams(gcode) let price: string | undefined if (query?.algo) { let algo = decodeURI(query.algo as string) // objectLoop(params, (value, key) => { // if (typeof value !== 'number') { // return // } // while (algo.includes(key)) { // algo = algo.replace(key, value.toString()) // } // }) try { logger.log('Evaluating Alogrithm') const tmp = evaluate(algo, gcodeParams) if (typeof tmp === 'number') { price = tmp.toFixed(2) } else { return buildRFC7807({ type: '/docs/errors/algorithm-error', status: 500, title: 'Algorithm compilation error', details: 'It seems the algorithm resolution failed', algorithm: algo, algorithmError: 'Algorithm return a Unit', parameters: gcodeParams }, locals.responseBuilder) } } catch (e) { logger.dir(e) return buildRFC7807({ type: '/docs/errors/algorithm-error', status: 500, title: 'Algorithm compilation error', details: 'It seems the algorithm resolution failed', algorithm: algo, algorithmError: e, parameters: gcodeParams }, locals.responseBuilder) } } logger.log('request successfull :)') return locals.responseBuilder .body({ price: price ? parseFloat(price) : undefined, ...getParams(gcode), gcode }) .status(200) .build() }