From b2b829ec54f85087dff03609fe1708131981a29a Mon Sep 17 00:00:00 2001 From: Avior Date: Wed, 28 Jun 2023 19:15:34 +0200 Subject: [PATCH] feat: Change slice url Signed-off-by: Avior --- README.md | 2 +- src/middleware/apiAuth.ts | 5 + .../api/v1/{process => slice}/[configId].ts | 524 +++++++++--------- 3 files changed, 268 insertions(+), 263 deletions(-) rename src/pages/api/v1/{process => slice}/[configId].ts (96%) diff --git a/README.md b/README.md index bc39917..f7e3192 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ add `Authorization: Bearer {token}` | /api/v1/users/{userId}/configs | POST | configs.create | yes | yes | Add a new configuration to the user | | /api/v1/users/{userId}/keys | GET | keys.get | yes | no | get the list of API key for the user | | /api/v1/users/{userId}/keys | POST | keys.create | yes | no | create a new API key for the user | -| /api/v1/users/{userId}/process | POST | slicing.slice | yes | yes | run the main website action | +| /api/v1/slice/{configId} | POST | slice.slice | yes | yes | run the main website action | endpoints not available through API can still be accessed by admins with the `admin.` prefix to the permission diff --git a/src/middleware/apiAuth.ts b/src/middleware/apiAuth.ts index 5581c1c..5656f5c 100644 --- a/src/middleware/apiAuth.ts +++ b/src/middleware/apiAuth.ts @@ -9,6 +9,11 @@ const endpointsPermissions: Record = { api: true, cookie: true, name: 'configs.get' + }, + '/api/v1/slice/[configId]': { + api: true, + cookie: true, + name: 'slice.slice' } } diff --git a/src/pages/api/v1/process/[configId].ts b/src/pages/api/v1/slice/[configId].ts similarity index 96% rename from src/pages/api/v1/process/[configId].ts rename to src/pages/api/v1/slice/[configId].ts index 436770e..0d2be4f 100644 --- a/src/pages/api/v1/process/[configId].ts +++ b/src/pages/api/v1/slice/[configId].ts @@ -1,262 +1,262 @@ -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() -} +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() +}