feat: Add moer element

Signed-off-by: Florian BOUILLON <f.bouillon@aptatio.com>
This commit is contained in:
Florian Bouillon 2023-06-27 18:30:44 +02:00
parent 4cd2a365ae
commit d76f412b82
19 changed files with 540 additions and 90 deletions

47
package-lock.json generated
View File

@ -21,6 +21,7 @@
"tailwindcss": "^3.3.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/bcryptjs": "^2.4.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.3.1",
@ -1083,6 +1084,34 @@
"escalade": "^3.1.1"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz",
"integrity": "sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==",
"dev": true,
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz",
@ -4786,6 +4815,24 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"dev": true
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",

View File

@ -25,6 +25,7 @@
"tailwindcss": "^3.3.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/bcryptjs": "^2.4.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.3.1",

15
src/content/config.ts Normal file
View File

@ -0,0 +1,15 @@
// 1. Import utilities from `astro:content`
import { defineCollection, z } from 'astro:content'
// 2. Define your collection(s)
const docsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string()
})
})
// 3. Export a single `collections` object to register your collection(s)
// This key should match your collection directory name in "src/content"
export const collections = {
'docs': docsCollection,
};

View File

@ -0,0 +1,23 @@
---
title: 'Unauthorized Access Error'
---
# Unauthorized Access Error
## Possible Errors
### Permission Error
You need to have an API key with the correct permission to use the specified resource
### Missing API Key
To have access to most of the API Urls you need to have n API key in your headers like the following
```http
Authorization: Bearer 935b6640-25fe-402e-8b18-e60de88bc0da
```
### Cookies not found
An alternative to the API Key is to have your cookies set, this is mostly for in browser and will only work while being connected on the website

14
src/env.d.ts vendored
View File

@ -1,7 +1,8 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
/// <reference types="./libs/ResponseBuilder" />
interface ImportMetaEnv {
CONFIGS_PATH?: string
PRUSASLICER_PATH?: string
BAMBUSTUDIO_PATH?: string
MONGODB?: string
@ -12,3 +13,14 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare namespace App {
interface Locals {
/**
* authentification key is the api key or the session token
*/
authKey?: string
responseBuilder?: ResponseBuilder
}
}

View File

@ -1,6 +1,14 @@
import { promises as fs } from 'node:fs'
/**
* File manipulation utility class
*/
export default class FilesUtils {
/**
* heck if a file/folder exists at the specified location
* @param path the path to check
* @returns if the file exists or not
*/
public static async exists(path: string): Promise<boolean> {
try {
await fs.stat(path)

View File

@ -1,3 +1,4 @@
import ResponseBuilder from '../ResponseBuilder'
/**
* Add headers:
@ -57,11 +58,9 @@ export default interface RFC7807 {
* @param error the error (base items are type, status, title details and instance)
* @returns
*/
export function buildRFC7807(error: RFC7807 & Record<string, any>): Response {
return new Response(JSON.stringify(error), {
headers: {
'Content-Type': 'application/problem+json'
},
status: error.status ?? 500
})
export function buildRFC7807(error: RFC7807 & Record<string, any>, response: ResponseBuilder = new ResponseBuilder()): Response {
response.addHeader('Content-Type', 'application/problem+json')
.body(JSON.stringify(error))
.status(error.status ?? 500)
return response.build()
}

70
src/libs/RateLimiter.ts Normal file
View File

@ -0,0 +1,70 @@
import { objectLoop } from '@dzeio/object-util'
import { buildRFC7807 } from './RFCs/RFC7807'
import StatusCode from './StatusCode'
interface StorageItem {
pointsRemaining: number
timeReset: number
}
export interface RateLimitHeaders {
"Retry-After"?: string,
"X-RateLimit-Limit": string,
"X-RateLimit-Remaining": string,
"X-RateLimit-Reset": string
}
export default class RateLimiter {
/**
* number of points that can be used per {timeSpan}
*/
public static points = 10
/**
* timeSpan in seconds
*/
public static timeSpan = 600
private static instance: RateLimiter = new RateLimiter()
public static getInstance(): RateLimiter {
return this.instance
}
private storage: Record<string, StorageItem> = {}
public consume(key: string, value: number = 1): Response | RateLimitHeaders {
let item = this.storage[key]
const now = (new Date().getTime() / 1000)
if (!item) {
item = {
pointsRemaining: RateLimiter.points,
timeReset: now + RateLimiter.timeSpan
}
}
if (item.timeReset <= now) {
item.timeReset = now + RateLimiter.timeSpan
item.pointsRemaining = RateLimiter.points
}
item.pointsRemaining -= value
this.storage[key] = item
const headers: RateLimitHeaders = {
"X-RateLimit-Limit": RateLimiter.points.toFixed(0),
"X-RateLimit-Remaining": Math.max(item.pointsRemaining, 0).toFixed(0),
"X-RateLimit-Reset": item.timeReset.toFixed(0)
}
if (item.pointsRemaining < 0) {
const resp = buildRFC7807({
type: '/docs/error/rate-limited',
status: StatusCode.TOO_MANY_REQUESTS,
title: 'You are being rate limited as you have done too many requests to the server'
})
resp.headers.append('Retry-After', (item.timeReset - now).toFixed(0))
objectLoop(headers, (value, key) => {
resp.headers.append(key, value)
})
return resp
}
return headers
}
}

View File

@ -0,0 +1,61 @@
import { objectLoop } from '@dzeio/object-util'
/**
* Simple builde to create a new Response object
*/
export default class ResponseBuilder {
private _body: BodyInit | null | undefined
public body(body: string | object | null | undefined) {
if (typeof body === 'object') {
this._body = JSON.stringify(body)
this.addHeader('Content-Type', 'application/json')
} else {
this._body = body
}
return this
}
private _headers: Headers = new Headers()
public headers(headers: HeadersInit ) {
if (headers instanceof Headers) {
this._headers = headers
} else {
this._headers = new Headers(headers)
}
return this
}
public addHeader(key: string, value: string) {
this._headers.append(key, value)
return this
}
public addHeaders(headers: Record<string, string>) {
objectLoop(headers, (value, key) => {
this.addHeader(key, value)
})
return this
}
public removeHeader(key: string) {
this._headers.delete(key)
return this
}
private _status?: number
public status(status: number) {
this._status = status
return this
}
public build(): Response {
const init: ResponseInit = {
headers: this._headers
}
if (this._status) {
init.status = this._status
}
return new Response(this._body, init)
}
}

71
src/libs/StatusCode.ts Normal file
View File

@ -0,0 +1,71 @@
enum StatusCode {
CONTINUE = 100,
SWITCHING_PROTOCOLS,
PROCESSING,
EARLY_HINTS,
OK = 200,
CREATED,
ACCEPTED,
NON_AUTHORITATIVE_INFORMATION,
NO_CONTENT,
RESET_CONTENT,
PARTIAL_CONTENT,
MULTI_STATUS,
ALREADY_REPORTED,
IM_USED = 226,
MULTIPLE_CHOICES = 300,
MOVED_PERMANENTLY,
FOUND,
SEE_OTHER,
NOT_MODIFIED,
USE_PROXY,
// UNUSED
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT,
BAD_REQUEST = 400,
UNAUTHORIZED,
PAYMENT_REQUIRED,
FORBIDDEN,
NOT_FOUND,
METHOD_NOT_ALLOWED,
NOT_ACCEPTABLE,
PROXY_AUTHENTIFICATION_REQUIRED,
REQUEST_TIMEOUT,
CONFLICT,
GONE,
LENGTH_REQUIRED,
PRECONDITION_FAILED,
PAYLOAD_TOO_LARGE,
URI_TOO_LONG,
UNSUPPORTED_MEDIA_TYPE,
RANGE_NOT_SATISFIABLE,
EXPECTATION_FAILED,
IM_A_TEAPOT,
MIDIRECTED_REQUEST = 421,
UNPROCESSABLE_CONTENT,
LOCKED,
FAILED_DEPENDENCY,
TOO_EARLY,
UPGRADE_REQUIRED,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS,
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
UNAVAILABLE_OR_LEGAL_REASONS = 451,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED,
BAD_GATEWAY,
SERVICE_UNAVAILABLE,
GATEWAY_TIMEOUT,
HTTP_VERSION_NOT_SUPPORTED,
VARIANT_ALSO_NEGOTIATES,
INSUFFICIENT_STORAGE,
LOOP_DETECTED,
NOT_EXTENDED = 510,
NETWORK_AUTHENTIFICATION_REQUIRED,
}
export default StatusCode

View File

@ -1,3 +1,8 @@
/**
* try to parse a GCode config string into a number
* @param str the string to try parsing
* @returns a number if parsing happened correctly or undefined
*/
function parseNumber(str: string): number | undefined {
if (!/^(\d|\.)+$/g.test(str)) {
return undefined
@ -13,6 +18,11 @@ function parseNumber(str: string): number | undefined {
return int
}
/**
* decode a print time to a number of seconds
* @param text the text to decode
* @returns the number of seconds in the text
*/
function decodeTime(text: string): number {
let timeInSec = 0
for (const it of text.split(' ')) {
@ -39,23 +49,42 @@ function decodeTime(text: string): number {
}
export function getParams(data: string) {
// get the configuration lines
const lines = data.split('\n').filter((it) => it.startsWith(';') && it.includes('='))
// create the config object
const obj: Record<string, string | number> = {}
// loop through eacj config
for (const line of lines) {
// get its key and value
const [key, value] = line.split('=', 2).map((it) => it.slice(1).trim())
let realKey = key.replace(/ /g, '_').replace(/\[|\]|\(|\)/g, '')
// sip if it has no key or value
if (!key || !value) {
continue
}
// process the key
let realKey = key
// replace spaces by _
.replace(/ /g, '_')
// remove unparseable characters
.replace(/\[|\]|\(|\)/g, '')
// process the value
const realValue = parseNumber(value) ?? value
// add an offset if the key is already cited
let offset = 0
while (obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] && obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] !== realValue) {
offset++
}
// chnge the key to add the offset
if (offset > 0) {
realKey = `${realKey}_${offset}`
}
// detect key collisions
if (obj[realKey] && obj[realKey] !== realValue) {
throw new Error(`Key collision ${key}=${realValue} ${realKey}=${obj[realKey]}`)
}
obj[realKey] = parseNumber(value) ?? value
// set the value to the key
obj[realKey] = realValue
// transform the time to a number of seconds
if (realKey === 'estimated_printing_time_normal_mode') {
obj['estimated_printing_time_seconds'] = decodeTime(value)
}

View File

@ -25,13 +25,13 @@ interface Permission {
*
* TODO: implement rate limiting
* http/2.0 429 TOO MANY REQUESTS
* Content-Type: application/json
* X-RateLimit-Limit: 1000
* X-RateLimit-Remaining: 0
* X-RateLimit-Reset: 123456789
* X-RateLimit-Reset-After: 9 // Number of seconds before the remaining Rate go back to 0
* Content-Type: application/json+problem
* X-RateLimit-Limit: 1000 // number of request you cn make until hitting the rate limit
* X-RateLimit-Remaining: 0 // number of request remaining until the rate limit is atteined
* X-RateLimit-Reset: 123456789 // EPOCH time when the rate limit reset
* X-RateLimit-Reset-After: 9 // Number of seconds before the remaining Rate reset
*/
export async function validateAuth(request: Request, permission: Permission): Promise<Response | null> {
export async function validateAuth(request: Request, permission: Permission): Promise<Response | string> {
const apiKeyHeader = request.headers.get('Authorization')
const cookieHeader = request.headers.get('Cookie')
if (apiKeyHeader) {
@ -58,10 +58,10 @@ export async function validateAuth(request: Request, permission: Permission): Pr
// return it.endsWith(permission.name)
})
if (match && (permission.api || match.startsWith('admin.'))) {
return null
return apiKey
} else if (permission.api) {
return buildRFC7807({
type: '/unauthorized-access',
type: '/docs/errors/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `you are missing the permission "${permission.name}" or is not an admin`
@ -69,7 +69,7 @@ export async function validateAuth(request: Request, permission: Permission): Pr
}
} else if (permission.api && !permission.cookie) {
return buildRFC7807({
type: '/unauthorized-access',
type: '/docs/errors/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `You MUST define an API key fo use this endpoint`
@ -82,7 +82,7 @@ export async function validateAuth(request: Request, permission: Permission): Pr
const userCookie = cookies.get('userId')
if (!userCookie) {
return buildRFC7807({
type: '/unauthorized-access',
type: '/docs/errors/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `you must be connected to use this endpoint (missing the userId cookie)`
@ -91,33 +91,33 @@ export async function validateAuth(request: Request, permission: Permission): Pr
const dao = await DaoFactory.get('user').get(userCookie)
if (!dao) {
return buildRFC7807({
type: '/unauthorized-access',
type: '/docs/errors/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `the user does not exists`
})
}
return null
return userCookie
} else if (!permission.api && permission.cookie) {
return buildRFC7807({
type: '/unauthorized-access',
type: '/docs/errors/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `You MUST be connected to your account to use this endpoint`
})
} else if (permission.api && permission.cookie) {
return buildRFC7807({
type: '/unauthorized-access',
type: '/docs/errors/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `You must be connected or use an API key to access this endpoint`
})
}
return buildRFC7807({
type: '/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `the following endpoint is not currently accessible by any means`
type: '/docs/errors/page-not-found',
status: 404,
title: 'Page not found',
details: `the following endpoint does not exists`
})
}

20
src/middleware/apiAuth.ts Normal file
View File

@ -0,0 +1,20 @@
import { defineMiddleware } from "astro/middleware"
import { validateAuth } from '../libs/validateAuth'
// `context` and `next` are automatically typed
export default defineMiddleware(async (context, next) => {
if (!context.request.url.includes('api')) {
return next()
}
const auth = await validateAuth(context.request, {
name: 'slicing.slice',
api: true,
cookie: true
})
if (typeof auth === 'object') {
return auth
}
context.locals.authKey = auth
return next()
})

View File

@ -0,0 +1,18 @@
import { defineMiddleware } from "astro/middleware"
import RateLimiter from '../libs/RateLimiter'
// `context` and `next` are automatically typed
export default defineMiddleware(async ({ request, locals }, next) => {
if (!request.url.includes('api')) {
return next()
}
const limit = RateLimiter.getInstance().consume(locals.authKey as string)
if ('status' in limit) {
return limit
}
locals.responseBuilder.addHeaders(limit)
return next()
})

7
src/middleware/index.ts Normal file
View File

@ -0,0 +1,7 @@
import { sequence } from "astro/middleware"
import apiAuth from './apiAuth'
import apiRateLimit from './apiRateLimit'
import responseBuilder from './responseBuilder'
export const onRequest = sequence(responseBuilder, apiAuth, apiRateLimit)

View File

@ -0,0 +1,9 @@
import { defineMiddleware } from "astro/middleware"
import ResponseBuilder from '../libs/ResponseBuilder'
// `context` and `next` are automatically typed
export default defineMiddleware(async (context, next) => {
context.locals.responseBuilder = new ResponseBuilder()
return next()
})

View File

@ -3,17 +3,23 @@ import { objectMap, objectOmit } from '@dzeio/object-util'
import URLManager from '@dzeio/url-manager'
import type { APIRoute } from 'astro'
import { evaluate } from 'mathjs'
import { exec as execSync, spawn } from 'node:child_process'
import { spawn } from 'node:child_process'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { promisify } from 'node:util'
import { buildRFC7807 } from '../../../libs/RFCs/RFC7807'
import { getParams } from '../../../libs/gcodeUtils'
import { validateAuth } from '../../../libs/validateAuth'
import DaoFactory from '../../../models/DaoFactory'
import { buildRFC7807 } from '../../../../libs/RFCs/RFC7807'
import RateLimiter from '../../../../libs/RateLimiter'
import ResponseBuilder from '../../../../libs/ResponseBuilder'
import StatusCode from '../../../../libs/StatusCode'
import { getParams } from '../../../../libs/gcodeUtils'
import { validateAuth } from '../../../../libs/validateAuth'
import DaoFactory from '../../../../models/DaoFactory'
interface SliceError {
code: number
output: Array<string>
}
const exec = promisify(execSync)
let tmpDir: string
@ -23,15 +29,7 @@ let tmpDir: string
* price: algorithm from settings
* adionnal settings from https://manual.slic3r.org/advanced/command-line
*/
export const post: APIRoute = async ({ params, request }) => {
const res = await validateAuth(request, {
name: 'slicing.slice',
api: true,
cookie: true
})
if (res) {
return res
}
export const post: APIRoute = async ({ params, request, locals }) => {
if (!tmpDir) {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'saas-'))
}
@ -41,13 +39,24 @@ export const post: APIRoute = async ({ params, request }) => {
if (!config) {
return buildRFC7807({
type: '/missing-config',
status: 404,
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)
@ -72,7 +81,7 @@ export const post: APIRoute = async ({ params, request }) => {
logger.log('writing STL to filesystem')
// write input
await fs.writeFile(stlPath, new Uint8Array(Buffer.from(await request.arrayBuffer())), {
await fs.writeFile(stlPath, input, {
encoding: null
})
@ -85,87 +94,112 @@ export const post: APIRoute = async ({ params, request }) => {
if (config.type === 'prusa' || true) {
slicerPath = import.meta.env.PRUSASLICER_PATH ?? 'prusa-slicer'
additionnalParams += ' --export-gcode'
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 slicer = spawn(slicerPath, slicerCommand.split(' '))
slicer.stdout.on('data', (data) => {
logger.log('[stdout]',data.toString('utf8'))
const logs: Array<string> = []
const slicer = spawn(`"${slicerPath}"`, slicerCommand.split(' '), {
shell: true
})
slicer.stderr.on('data', (data: Buffer) => {
logger.log('[stderr]', data.toString('utf8'))
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) => {
logger.log('code', code)
logger.log('signal', signal)
if (typeof code === 'number' && code > 0) {
rej(code)
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 = e.toString()
logger.error(e)
if (line.includes('Objects could not fit on the bed')) {
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: '/object-too-large',
status: 413,
title: 'Object is too large'
})
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: '/missing-config-file',
status: 404,
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: '/slicer-option-unknown',
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: '/slicer-not-found',
status: 408,
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: '/timed-out-slicing',
status: 408,
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: '/general-input-output-error',
status: 500,
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,
@ -173,8 +207,8 @@ export const post: APIRoute = async ({ params, request }) => {
// fileSize: req.body.length,
overrides: overrides,
serverMessage:
e.toString().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)
}
const gcode = await fs.readFile(gcodePath, 'utf-8')
await fs.rm(processFolder, { recursive: true, force: true })
@ -198,35 +232,35 @@ export const post: APIRoute = async ({ params, request }) => {
price = tmp.toFixed(2)
} else {
return buildRFC7807({
type: '/algorithm-error',
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: '/algorithm-error',
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 {
status: 200,
body: JSON.stringify({
return locals.responseBuilder
.body({
price: price ? parseFloat(price) : undefined,
...getParams(gcode),
gcode
})
}
.status(200)
.build()
}

View File

@ -0,0 +1,24 @@
---
import Layout from '../../layouts/Layout.astro'
import { getEntry } from 'astro:content'
import StatusCode from '../../libs/StatusCode'
const page = Astro.params.page
let Result: any
const entry = await getEntry('docs', page as any)
if (!entry) {
Astro.response.status = StatusCode.NOT_FOUND
} else {
const { Content } = await entry.render()
Result = Content
}
---
<Layout title={entry?.data.title ?? ''}>
<main class="prose">
{Result && <Result />}
</main>
</Layout>

View File

@ -4,5 +4,7 @@ module.exports = {
theme: {
extend: {},
},
plugins: [],
plugins: [
require('@tailwindcss/typography'),
],
}