feat: Change slice url

Signed-off-by: Avior <github@avior.me>
This commit is contained in:
Florian Bouillon 2023-06-28 19:15:34 +02:00
parent ff07f8f4a5
commit b2b829ec54
3 changed files with 268 additions and 263 deletions

View File

@ -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}/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 | 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}/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 endpoints not available through API can still be accessed by admins with the `admin.` prefix to the permission

View File

@ -9,6 +9,11 @@ const endpointsPermissions: Record<string, Permission> = {
api: true, api: true,
cookie: true, cookie: true,
name: 'configs.get' name: 'configs.get'
},
'/api/v1/slice/[configId]': {
api: true,
cookie: true,
name: 'slice.slice'
} }
} }

View File

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