feat: Move to Astro
This commit is contained in:
parent
908d43f299
commit
0f7128814c
24
.gitignore
vendored
24
.gitignore
vendored
@ -1,3 +1,23 @@
|
|||||||
node_modules
|
# build output
|
||||||
files/
|
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
slicers/*
|
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
Dockerfile
16
Dockerfile
@ -1,7 +1,13 @@
|
|||||||
|
# This Dockerfile allows you to run NextJS in server mode
|
||||||
|
|
||||||
#########
|
#########
|
||||||
# Build #
|
# Build #
|
||||||
#########
|
#########
|
||||||
FROM node:alpine as BUILD_IMAGE
|
FROM docker.io/node:alpine as BUILD_IMAGE
|
||||||
|
|
||||||
|
# External deps (for node-gyp add: "python3 make g++")
|
||||||
|
# git is used to install the npm packages with git deps
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
# run as non root user
|
# run as non root user
|
||||||
USER node
|
USER node
|
||||||
@ -21,10 +27,15 @@ ADD --chown=node:node . .
|
|||||||
# build
|
# build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
RUN npm prune --omit=dev
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# Production #
|
# Production #
|
||||||
##############
|
##############
|
||||||
FROM node:latest as PROD_IMAGE
|
# inform software to be in production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV CONFIGS_PATH=./configs
|
||||||
|
|
||||||
# Download & Install Slic3r
|
# Download & Install Slic3r
|
||||||
# RUN apt-get update \
|
# RUN apt-get update \
|
||||||
@ -41,6 +52,7 @@ RUN apt-get update \
|
|||||||
prusa-slicer \
|
prusa-slicer \
|
||||||
&& apt-get remove prusa-slicer -y \
|
&& apt-get remove prusa-slicer -y \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV PATH /opt/PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201/bin:$PATH
|
ENV PATH /opt/PrusaSlicer-2.5.2+linux-x64-GTK3-202303231201/bin:$PATH
|
||||||
ENV SLICER_PATH prusa-slicer
|
ENV SLICER_PATH prusa-slicer
|
||||||
|
|
||||||
|
62
README.md
62
README.md
@ -1,19 +1,55 @@
|
|||||||
example command: `http://127.0.0.1:3000/?algo=total_filament_used_g%20*%200.6%20%2B%20(estimated_printing_time_seconds%20%2F%2060%20%2F%2060%20*%2040)`
|
# Astro Starter Kit: Basics
|
||||||
|
|
||||||
example decoded algo: `total_filament_used_g * 0.6 + (estimated_printing_time_seconds / 60 / 60 * 40)`, it will result in `price` being the response to the algorithm
|
```
|
||||||
|
npm create astro@latest -- --template basics
|
||||||
|
```
|
||||||
|
|
||||||
the .stl file MUST be the only thing in the body of the request
|
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
||||||
|
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
||||||
|
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
||||||
|
|
||||||
Query parameters:
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
- `algo`: the pricing algorithm
|

|
||||||
- `config`: use another config than the default one
|
|
||||||
|
|
||||||
other queries:
|
|
||||||
- run `prusa-slicer --help-fff` in the terminal or look at the params in the response to get the full list
|
|
||||||
|
|
||||||
examples:
|
## 🚀 Project Structure
|
||||||
- `layer_height` = `0.05` change the layer height
|
|
||||||
- `scale` = `200%` Scale the model
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
- `fill_density` = `90%` change the density of the fill __note: if `fill_density` is `100%`, it will crash unless you also add `fill_pattern` = `rectilinear`__
|
|
||||||
- `fill_pattern` = `rectilinear` change the fill pattern
|
```
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
│ └── favicon.svg
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ └── Card.astro
|
||||||
|
│ ├── layouts/
|
||||||
|
│ │ └── Layout.astro
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||||
|
21
astro.config.mjs
Normal file
21
astro.config.mjs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import tailwind from "@astrojs/tailwind";
|
||||||
|
import node from "@astrojs/node";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
// site: 'https://www.nasap3d.com',
|
||||||
|
integrations: [tailwind()],
|
||||||
|
build: {
|
||||||
|
assets: 'assets'
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: true
|
||||||
|
},
|
||||||
|
trailingSlash: 'never',
|
||||||
|
output: 'server',
|
||||||
|
adapter: node({
|
||||||
|
mode: "standalone"
|
||||||
|
})
|
||||||
|
})
|
8077
package-lock.json
generated
8077
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -1,18 +1,24 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"name": "paas",
|
||||||
"@dzeio/object-util": "^1.5.0",
|
"type": "module",
|
||||||
"express": "^4.18.2",
|
"version": "0.0.1",
|
||||||
"mathjs": "^11.8.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ts-node-dev src/main.ts",
|
"dev": "astro dev",
|
||||||
"start": "node dist/main.js",
|
"start": "astro dev",
|
||||||
"build": "tsc --project tsconfig.json"
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/node": "^5.2.0",
|
||||||
|
"@astrojs/tailwind": "^3.1.3",
|
||||||
|
"@dzeio/object-util": "^1.5.0",
|
||||||
|
"@dzeio/url-manager": "^1.0.9",
|
||||||
|
"astro": "^2.6.4",
|
||||||
|
"mathjs": "^11.8.1",
|
||||||
|
"tailwindcss": "^3.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dzeio/config": "^1.1.12",
|
"@types/node": "^20.3.1"
|
||||||
"@types/express": "^4.17.17",
|
|
||||||
"ts-node-dev": "^2.0.0",
|
|
||||||
"typescript": "^5.0.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
public/favicon.svg
Normal file
9
public/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 749 B |
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>
|
8
tailwind.config.cjs
Normal file
8
tailwind.config.cjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
@ -1,9 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "./node_modules/@dzeio/config/tsconfig.base.json",
|
"extends": "astro/tsconfigs/strict"
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"src/main.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user