feat: Change slice url
Signed-off-by: Avior <github@avior.me>
This commit is contained in:
parent
ff07f8f4a5
commit
b2b829ec54
@ -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
|
||||
|
||||
|
@ -9,6 +9,11 @@ const endpointsPermissions: Record<string, Permission> = {
|
||||
api: true,
|
||||
cookie: true,
|
||||
name: 'configs.get'
|
||||
},
|
||||
'/api/v1/slice/[configId]': {
|
||||
api: true,
|
||||
cookie: true,
|
||||
name: 'slice.slice'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<string>
|
||||
}
|
||||
|
||||
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<void>((res, rej) => {
|
||||
const logs: Array<string> = []
|
||||
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<string>
|
||||
}
|
||||
|
||||
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<void>((res, rej) => {
|
||||
const logs: Array<string> = []
|
||||
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()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user