feat: Move to Astro
This commit is contained in:
63
src/components/Card.astro
Normal file
63
src/components/Card.astro
Normal file
@ -0,0 +1,63 @@
|
||||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
body: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const { href, title, body } = Astro.props;
|
||||
---
|
||||
|
||||
<li class="link-card">
|
||||
<a href={href}>
|
||||
<h2>
|
||||
{title}
|
||||
<span>→</span>
|
||||
</h2>
|
||||
<p>
|
||||
{body}
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
<style>
|
||||
.link-card {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
padding: 0.25rem;
|
||||
background-color: white;
|
||||
background-image: none;
|
||||
background-size: 400%;
|
||||
border-radius: 0.6rem;
|
||||
background-position: 100%;
|
||||
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.link-card > a {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
line-height: 1.4;
|
||||
padding: 1rem 1.3rem;
|
||||
border-radius: 0.35rem;
|
||||
color: #111;
|
||||
background-color: white;
|
||||
opacity: 0.8;
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
p {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
color: #444;
|
||||
}
|
||||
.link-card:is(:hover, :focus-within) {
|
||||
background-position: 0;
|
||||
background-image: var(--accent-gradient);
|
||||
}
|
||||
.link-card:is(:hover, :focus-within) h2 {
|
||||
color: rgb(var(--accent));
|
||||
}
|
||||
</style>
|
10
src/env.d.ts
vendored
Normal file
10
src/env.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
CONFIGS_PATH?: string
|
||||
PRUSASLICER_PATH?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
36
src/layouts/Layout.astro
Normal file
36
src/layouts/Layout.astro
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content="Astro description">
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
<style is:global>
|
||||
:root {
|
||||
--accent: 124, 58, 237;
|
||||
--accent-gradient: linear-gradient(45deg, rgb(var(--accent)), #da62c4 30%, white 60%);
|
||||
}
|
||||
html {
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
code {
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
</style>
|
12
src/libs/FilesUtils.ts
Normal file
12
src/libs/FilesUtils.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
|
||||
export default class FilesUtils {
|
||||
public static async exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.stat(path)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
62
src/libs/HTTPError.ts
Normal file
62
src/libs/HTTPError.ts
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
/**
|
||||
* Add headers:
|
||||
* Content-Type: application/problem+json
|
||||
*
|
||||
* following https://www.rfc-editor.org/rfc/rfc7807.html
|
||||
*/
|
||||
export default interface JSONError {
|
||||
/**
|
||||
* A URI reference [RFC3986] that identifies the
|
||||
* problem type.
|
||||
*
|
||||
* This specification encourages that, when
|
||||
* dereferenced, it provide human-readable documentation for the
|
||||
* problem type (e.g., using HTML [W3C.REC-html5-20141028]).
|
||||
*
|
||||
* When
|
||||
* this member is not present, its value is assumed to be
|
||||
* "about:blank"
|
||||
*/
|
||||
type?: string
|
||||
|
||||
/**
|
||||
* A short, human-readable summary of the problem
|
||||
* type.
|
||||
*
|
||||
* It SHOULD NOT change from occurrence to occurrence of the
|
||||
* problem, except for purposes of localization (e.g., using
|
||||
* proactive content negotiation; see [RFC7231], Section 3.4).
|
||||
*/
|
||||
title?: string
|
||||
|
||||
/**
|
||||
* The HTTP status code ([RFC7231], Section 6)
|
||||
* generated by the origin server for this occurrence of the problem.
|
||||
*/
|
||||
status?: number
|
||||
|
||||
/**
|
||||
* A human-readable explanation specific to this
|
||||
* occurrence of the problem.
|
||||
*/
|
||||
details?: string
|
||||
|
||||
/**
|
||||
* A URI reference that identifies the specific
|
||||
* occurrence of the problem.
|
||||
*
|
||||
* It may or may not yield further
|
||||
* information if dereferenced.
|
||||
*/
|
||||
instance?: string
|
||||
}
|
||||
|
||||
export function buildRFC7807Error(error: JSONError & Record<string, any>): Response {
|
||||
return new Response(JSON.stringify(error), {
|
||||
headers: {
|
||||
'Content-Type': 'application/problem+json'
|
||||
},
|
||||
status: error.status ?? 500
|
||||
})
|
||||
}
|
156
src/main.ts
156
src/main.ts
@ -1,156 +0,0 @@
|
||||
import { objectMap, objectOmit } from '@dzeio/object-util'
|
||||
import { exec as execAsync } from 'child_process'
|
||||
import express from 'express'
|
||||
import fs from 'fs/promises'
|
||||
import { evaluate } from 'mathjs'
|
||||
import util from 'util'
|
||||
import { createErrorResponse } from './HTTPError'
|
||||
import { getParams } from './gcodeUtils'
|
||||
|
||||
const exec = util.promisify(execAsync)
|
||||
const server = express()
|
||||
|
||||
async function exists(file: string) {
|
||||
try {
|
||||
await fs.stat(file)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// make the files dir
|
||||
|
||||
server.use((req, _, next) => {
|
||||
let data: Array<any> = []
|
||||
// req.setEncoding()
|
||||
req.on('data', function(chunk) {
|
||||
data.push(Buffer.from(chunk))
|
||||
});
|
||||
|
||||
req.on('end', function() {
|
||||
req.body = Buffer.concat(data);
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
const path = process.cwd()
|
||||
|
||||
server.get('/', (_, res) => {
|
||||
res.send('please read the README.md file...')
|
||||
})
|
||||
|
||||
server.post('/', async (req, res) => {
|
||||
const file = (Math.random() * 1000000).toFixed(0)
|
||||
console.log('started processing new request', file)
|
||||
await fs.mkdir('files', { recursive: true })
|
||||
|
||||
const overrides = objectOmit(req.query, 'algo', 'config')
|
||||
let config = `${path}/configs/` + (req.query?.config ?? 'config') + '.ini'
|
||||
if (!exists(config)) {
|
||||
console.log('request finished in error :(', file)
|
||||
return createErrorResponse(res, {
|
||||
title: 'Configuration file does not exist',
|
||||
status: 404,
|
||||
detail: `The configuration file you want to use does not exist`,
|
||||
config: req.query?.config ?? 'config'
|
||||
})
|
||||
}
|
||||
const stlPath = `${path}/files/${file}.stl`
|
||||
const gcodePath = `${path}/files/${file}.gcode`
|
||||
// console.log(stlPath, req.body)
|
||||
await fs.writeFile(stlPath, req.body, {
|
||||
encoding: null
|
||||
})
|
||||
// console.log(fs.statSync(stlPath).size, req.body.length)
|
||||
let additionnalParams = objectMap(overrides, (v, k) => `--${(k as string).replace(/_/g, '-')} ${v}`).join(' ')
|
||||
const slicer = process.env.SLICER_PATH ?? 'prusa-slicer'
|
||||
if (slicer.includes('prusa')) {
|
||||
additionnalParams += ' --export-gcode'
|
||||
}
|
||||
const cmd = `${slicer} ${stlPath} --load ${config} --output ${gcodePath} ${additionnalParams}`
|
||||
try {
|
||||
await exec(cmd, {
|
||||
timeout: 60000
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.log('request finished in error :(', file)
|
||||
const line = e.toString()
|
||||
console.error(e)
|
||||
if (line.includes('Objects could not fit on the bed')) {
|
||||
await fs.rm(stlPath)
|
||||
return createErrorResponse(res, {
|
||||
title: 'Object is too large to fit on the bed',
|
||||
status: 413,
|
||||
detail: `The object you are trying to slice is too large to fit on the current bed`
|
||||
})
|
||||
} else if (line.includes('No such file')) {
|
||||
await fs.rm(stlPath)
|
||||
return createErrorResponse(res, {
|
||||
title: 'Configuration file does not exist',
|
||||
status: 404,
|
||||
detail: `The configuration file you want to use does not exist`,
|
||||
config: req.query?.config ?? 'config'
|
||||
})
|
||||
} else if (line.includes('Unknown option')) {
|
||||
await fs.rm(stlPath)
|
||||
return createErrorResponse(res, {
|
||||
title: 'An override does exist',
|
||||
status: 404,
|
||||
detail: `One or more of your overrides are not available in the current slicer`,
|
||||
overrides: overrides
|
||||
|
||||
})
|
||||
} else if (line.includes('ETIMEDOUT')) {
|
||||
await fs.rm(stlPath)
|
||||
return createErrorResponse(res, {
|
||||
title: 'File is taking too long to process',
|
||||
status: 400,
|
||||
detail: `The file you are trying to process takes too long to be processed`,
|
||||
processingTimeoutMillis: 60000,
|
||||
fileId: file,
|
||||
config: req.query?.config ?? 'config',
|
||||
fileSize: req.body.length,
|
||||
overrides: overrides
|
||||
})
|
||||
}
|
||||
return createErrorResponse(res, {
|
||||
title: 'General I/O error',
|
||||
status: 500,
|
||||
detail: `A server error make it impossible to slice the file`,
|
||||
fileId: file,
|
||||
config: req.query?.config ?? 'config',
|
||||
fileSize: req.body.length,
|
||||
overrides: overrides,
|
||||
serverMessage: e.toString().replace(cmd, '<i>software</i>').replace(stlPath, `<i>file ${file}</i>`).replace(`${path}/configs/`, '').replace('.ini', '')
|
||||
})
|
||||
}
|
||||
const gcode = await fs.readFile(gcodePath, 'utf-8')
|
||||
await fs.rm(stlPath)
|
||||
await fs.rm(gcodePath)
|
||||
const params = getParams(gcode)
|
||||
let price = -1
|
||||
if (req.query?.algo) {
|
||||
let algo = req.query.algo as string
|
||||
// objectLoop(params, (value, key) => {
|
||||
// if (typeof value !== 'number') {
|
||||
// return
|
||||
// }
|
||||
// while (algo.includes(key)) {
|
||||
// algo = algo.replace(key, value.toString())
|
||||
// }
|
||||
// })
|
||||
price = evaluate(algo, params)
|
||||
}
|
||||
res.json({
|
||||
price: parseFloat(price.toFixed(2)),
|
||||
...getParams(gcode),
|
||||
gcode
|
||||
})
|
||||
console.log('request successfull :)', file)
|
||||
// res.sendFile(gcodePath)
|
||||
})
|
||||
|
||||
server.listen(3000, () => {
|
||||
console.log(`🚀 Server ready at localhost:3000`);
|
||||
})
|
146
src/pages/api/run.ts
Normal file
146
src/pages/api/run.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { objectMap, objectOmit } from '@dzeio/object-util'
|
||||
import URLManager from '@dzeio/url-manager'
|
||||
import type { APIRoute } from 'astro'
|
||||
import { exec as execSync } from 'child_process'
|
||||
import fs from 'fs/promises'
|
||||
import { evaluate } from 'mathjs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path/posix'
|
||||
import { promisify } from 'node:util'
|
||||
import FilesUtils from '../../libs/FilesUtils'
|
||||
import { buildRFC7807Error } from '../../libs/HTTPError'
|
||||
import { getParams } from '../../libs/gcodeUtils'
|
||||
|
||||
const exec = promisify(execSync)
|
||||
|
||||
let tmpDir: string
|
||||
|
||||
export const post: APIRoute = async ({ request }) => {
|
||||
if (!tmpDir) {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'paas-'))
|
||||
}
|
||||
|
||||
const query = new URLManager(request.url).query()
|
||||
const file = (Math.random() * 1000000).toFixed(0)
|
||||
console.log('started processing new request', file)
|
||||
await fs.mkdir(`${tmpDir}/files`, { recursive: true })
|
||||
|
||||
const overrides = objectOmit(query, 'algo', 'config')
|
||||
const configName = query?.config ?? 'config'
|
||||
let config = `${import.meta.env.CONFIGS_PATH}/` + configName + '.ini'
|
||||
if (!await FilesUtils.exists(config)) {
|
||||
console.log('request finished in error :(', file)
|
||||
return buildRFC7807Error({
|
||||
type: '/missing-config-file',
|
||||
status: 404,
|
||||
title: 'Configuration file is missing',
|
||||
details: `the configuration file "${configName}" is not available on the remote server`
|
||||
})
|
||||
}
|
||||
const stlPath = `${tmpDir}/files/${file}.stl`
|
||||
const gcodePath = `${tmpDir}/files/${file}.gcode`
|
||||
|
||||
// write file
|
||||
await fs.writeFile(stlPath, new Uint8Array(Buffer.from(await request.arrayBuffer())), {
|
||||
encoding: null
|
||||
})
|
||||
// console.log(fs.statSync(stlPath).size, req.body.length)
|
||||
let additionnalParams = objectMap(overrides, (value, key) => `--${(key as string).replace(/_/g, '-')} ${value}`).join(' ')
|
||||
const slicer = import.meta.env.PRUSASLICER_PATH ?? 'prusa-slicer'
|
||||
if (slicer.includes('prusa')) {
|
||||
additionnalParams += ' --export-gcode'
|
||||
}
|
||||
const cmd = `${slicer} ${stlPath} --load ${config} --output ${gcodePath} ${additionnalParams}`
|
||||
try {
|
||||
await exec(cmd, {
|
||||
timeout: 60000
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.log('request finished in error :(', file)
|
||||
const line = e.toString()
|
||||
console.error(e)
|
||||
if (line.includes('Objects could not fit on the bed')) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807Error({
|
||||
type: '/object-too-large',
|
||||
status: 413,
|
||||
title: 'Object is too large'
|
||||
})
|
||||
} else if (line.includes('No such file')) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807Error({
|
||||
type: '/missing-config-file',
|
||||
status: 404,
|
||||
title: 'Configuration file is missing',
|
||||
details: `the configuration file "${configName}" is not available on the remote server`
|
||||
})
|
||||
} else if (line.includes('Unknown option')) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807Error({
|
||||
type: '/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'
|
||||
})
|
||||
} else if (
|
||||
line.includes('is not recognized as an internal or external command') ||
|
||||
line.includes('.dll was not loaded')
|
||||
) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807Error({
|
||||
type: '/slicer-not-found',
|
||||
status: 408,
|
||||
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 '
|
||||
})
|
||||
} else if (line.includes('ETIMEDOUT')) {
|
||||
await fs.rm(stlPath)
|
||||
return buildRFC7807Error({
|
||||
type: '/timed-out-slicing',
|
||||
status: 408,
|
||||
title: 'Timed out slicing file',
|
||||
detail: `The file you are trying to process takes too long to be processed`,
|
||||
processingTimeoutMillis: 60000
|
||||
})
|
||||
}
|
||||
return buildRFC7807Error({
|
||||
type: '/general-input-output-error',
|
||||
status: 500,
|
||||
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: file,
|
||||
config: configName,
|
||||
// fileSize: req.body.length,
|
||||
overrides: overrides,
|
||||
serverMessage:
|
||||
e.toString().replace(cmd, '***SLICER***').replace(stlPath, configName ?? `***FILE***`).replace(`${path}/configs/`, '').replace('.ini', '')
|
||||
})
|
||||
}
|
||||
const gcode = await fs.readFile(gcodePath, 'utf-8')
|
||||
await fs.rm(stlPath)
|
||||
await fs.rm(gcodePath)
|
||||
const params = getParams(gcode)
|
||||
let price = -1
|
||||
if (query?.algo) {
|
||||
let algo = query.algo as string
|
||||
// objectLoop(params, (value, key) => {
|
||||
// if (typeof value !== 'number') {
|
||||
// return
|
||||
// }
|
||||
// while (algo.includes(key)) {
|
||||
// algo = algo.replace(key, value.toString())
|
||||
// }
|
||||
// })
|
||||
price = evaluate(algo, params)
|
||||
}
|
||||
console.log('request successfull :)', file)
|
||||
return {
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
price: parseFloat(price.toFixed(2)),
|
||||
...getParams(gcode),
|
||||
gcode
|
||||
})
|
||||
}
|
||||
}
|
36
src/pages/index.astro
Normal file
36
src/pages/index.astro
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Card from '../components/Card.astro';
|
||||
---
|
||||
|
||||
<Layout title="Welcome to Astro.">
|
||||
<main>
|
||||
<h1>Welcome to <span class="text-gradient">Astro</span></h1>
|
||||
<p class="instructions">
|
||||
To get started, open the directory <code>src/pages</code> in your project.<br />
|
||||
<strong>Code Challenge:</strong> Tweak the "Welcome to Astro" message above.
|
||||
</p>
|
||||
<ul role="list" class="link-card-grid">
|
||||
<Card
|
||||
href="https://docs.astro.build/"
|
||||
title="Documentation"
|
||||
body="Learn how Astro works and explore the official API docs."
|
||||
/>
|
||||
<Card
|
||||
href="https://astro.build/integrations/"
|
||||
title="Integrations"
|
||||
body="Supercharge your project with new frameworks and libraries."
|
||||
/>
|
||||
<Card
|
||||
href="https://astro.build/themes/"
|
||||
title="Themes"
|
||||
body="Explore a galaxy of community-built starter themes."
|
||||
/>
|
||||
<Card
|
||||
href="https://astro.build/chat/"
|
||||
title="Community"
|
||||
body="Come say hi to our amazing Discord community. ❤️"
|
||||
/>
|
||||
</ul>
|
||||
</main>
|
||||
</Layout>
|
Reference in New Issue
Block a user