feat: Add Dao

This commit is contained in:
2023-06-19 00:50:46 +02:00
parent 59db3976fe
commit 5c1ed32cb6
30 changed files with 7344 additions and 6753 deletions

View File

@ -1,63 +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>&rarr;</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>
---
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>&rarr;</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>

21
src/env.d.ts vendored
View File

@ -1,10 +1,11 @@
/// <reference types="astro/client" />
interface ImportMetaEnv {
CONFIGS_PATH?: string
PRUSASLICER_PATH?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
/// <reference types="astro/client" />
interface ImportMetaEnv {
CONFIGS_PATH?: string
PRUSASLICER_PATH?: string
MONGODB?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@ -1,36 +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>
---
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>

View File

@ -1,12 +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
}
}
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
}
}
}

View File

@ -1,62 +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
})
}
/**
* Add headers:
* Content-Type: application/problem+json
*
* following https://www.rfc-editor.org/rfc/rfc7807.html
*/
export default interface RFC7807 {
/**
* 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 buildRFC7807(error: RFC7807 & Record<string, any>): Response {
return new Response(JSON.stringify(error), {
headers: {
'Content-Type': 'application/problem+json'
},
status: error.status ?? 500
})
}

View File

@ -1,64 +1,64 @@
function parseNumber(str: string): number | undefined {
if (!/^(\d|\.)+$/g.test(str)) {
return undefined
}
const float = parseFloat(str)
const int = parseInt(str, 10)
if (isNaN(int)) {
return undefined
}
if (int !== float) {
return float
}
return int
}
function decodeTime(text: string): number {
let timeInSec = 0
for (const it of text.split(' ')) {
const lastChar = it.charAt(it.length - 1)
const time = parseInt(it.slice(0, it.length - 1), 10)
switch (lastChar) {
case 'm':
timeInSec += time * 60
break;
case 's':
timeInSec += time
break;
case 'h':
timeInSec += time * 60 * 60
break;
case 'd':
timeInSec += time * 60 * 60 * 24
break;
default:
throw new Error(`error parsing time ${it} (${time})`)
}
}
return timeInSec
}
export function getParams(data: string) {
const lines = data.split('\n').filter((it) => it.startsWith(';') && it.includes('='))
const obj: Record<string, string | number> = {}
for (const line of lines) {
const [key, value] = line.split('=', 2).map((it) => it.slice(1).trim())
let realKey = key.replace(/ /g, '_').replace(/\[|\]|\(|\)/g, '')
const realValue = parseNumber(value) ?? value
let offset = 0
while (obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] && obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] !== realValue) {
offset++
}
if (offset > 0) {
realKey = `${realKey}_${offset}`
}
if (obj[realKey] && obj[realKey] !== realValue) {
throw new Error(`Key collision ${key}=${realValue} ${realKey}=${obj[realKey]}`)
}
obj[realKey] = parseNumber(value) ?? value
if (realKey === 'estimated_printing_time_normal_mode') {
obj['estimated_printing_time_seconds'] = decodeTime(value)
}
}
return obj
}
function parseNumber(str: string): number | undefined {
if (!/^(\d|\.)+$/g.test(str)) {
return undefined
}
const float = parseFloat(str)
const int = parseInt(str, 10)
if (isNaN(int)) {
return undefined
}
if (int !== float) {
return float
}
return int
}
function decodeTime(text: string): number {
let timeInSec = 0
for (const it of text.split(' ')) {
const lastChar = it.charAt(it.length - 1)
const time = parseInt(it.slice(0, it.length - 1), 10)
switch (lastChar) {
case 'm':
timeInSec += time * 60
break;
case 's':
timeInSec += time
break;
case 'h':
timeInSec += time * 60 * 60
break;
case 'd':
timeInSec += time * 60 * 60 * 24
break;
default:
throw new Error(`error parsing time ${it} (${time})`)
}
}
return timeInSec
}
export function getParams(data: string) {
const lines = data.split('\n').filter((it) => it.startsWith(';') && it.includes('='))
const obj: Record<string, string | number> = {}
for (const line of lines) {
const [key, value] = line.split('=', 2).map((it) => it.slice(1).trim())
let realKey = key.replace(/ /g, '_').replace(/\[|\]|\(|\)/g, '')
const realValue = parseNumber(value) ?? value
let offset = 0
while (obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] && obj[`${realKey}${offset > 0 ? `_${offset}` : ''}`] !== realValue) {
offset++
}
if (offset > 0) {
realKey = `${realKey}_${offset}`
}
if (obj[realKey] && obj[realKey] !== realValue) {
throw new Error(`Key collision ${key}=${realValue} ${realKey}=${obj[realKey]}`)
}
obj[realKey] = parseNumber(value) ?? value
if (realKey === 'estimated_printing_time_normal_mode') {
obj['estimated_printing_time_seconds'] = decodeTime(value)
}
}
return obj
}

View File

@ -0,0 +1,21 @@
import type APIKey from '.'
import Dao from '../Dao'
export default class APIKeyDao extends Dao<APIKey> {
private idx = 0
public async create(obj: Omit<APIKey, 'id'>): Promise<APIKey | null> {
console.log('pouet', this.idx++)
return null
// throw new Error('Method not implemented.')
}
public async findAll(query?: Partial<APIKey> | undefined): Promise<APIKey[]> {
throw new Error('Method not implemented.')
}
public async update(obj: APIKey): Promise<APIKey | null> {
throw new Error('Method not implemented.')
}
public async delete(obj: APIKey): Promise<boolean> {
throw new Error('Method not implemented.')
}
}

View File

@ -0,0 +1,4 @@
export default interface APIKey {
id: string
user: string
}

18
src/models/Client.ts Normal file
View File

@ -0,0 +1,18 @@
import { MongoClient } from 'mongodb'
import mongoose from 'mongoose'
export default class Client {
private static connectionString = import.meta.env.MONGODB
private static client = false
public static async get() {
if (!this.connectionString) {
throw new Error('Can\'t connect to the database, missing the connection string')
}
if (!this.client) {
mongoose.connect(this.connectionString)
this.client = true
}
return this.client
}
}

View File

@ -0,0 +1,21 @@
import type Config from '.'
import Dao from '../Dao'
export default class ConfigDao extends Dao<Config> {
private idx = 0
public async create(obj: Omit<Config, 'id'>): Promise<Config | null> {
console.log('pouet', this.idx++)
return null
// throw new Error('Method not implemented.')
}
public async findAll(query?: Partial<Config> | undefined): Promise<Config[]> {
throw new Error('Method not implemented.')
}
public async update(obj: Config): Promise<Config | null> {
throw new Error('Method not implemented.')
}
public async delete(obj: Config): Promise<boolean> {
throw new Error('Method not implemented.')
}
}

View File

@ -0,0 +1,6 @@
import type User from '../User'
export default interface Config {
id: string
user: string
}

105
src/models/Dao.ts Normal file
View File

@ -0,0 +1,105 @@
import Client from './Client'
/**
* the Dao is the object that connect the Database or source to the application layer
*
* you MUST call it through the `DaoFactory` file
*/
export default abstract class Dao<Object extends { id: any } = { id: any }> {
/**
* insert a new object into the source
*
* @param obj the object to create
* @returns the object with it's id filled if create or null otherwise
*/
abstract create(obj: Omit<Object, 'id'>): Promise<Object | null>
/**
* insert a new object into the source
*
* @param obj the object to create
* @returns the object with it's id filled if create or null otherwise
*/
public insert: Dao<Object>['create'] = (obj: Parameters<Dao<Object>['create']>[0]) => this.create(obj)
/**
* find the list of objects having elements from the query
*
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
* @returns an array containing the list of elements that match with the query
*/
abstract findAll(query?: Partial<Object>): Promise<Array<Object>>
/**
* find the list of objects having elements from the query
*
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
* @returns an array containing the list of elements that match with the query
*/
public find: Dao<Object>['findAll'] = (query: Parameters<Dao<Object>['findAll']>[0]) => this.findAll(query)
/**
* find an object by it's id
*
* (shortcut to findOne({id: id}))
*
* @param id the id of the object
* @returns
*/
public findById(id: Object['id']): Promise<Object | null> {
return this.findOne({id: id} as Partial<Object>)
}
/**
* find an object by it's id
*
* (shortcut to findOne({id: id}))
*
* @param id the id of the object
* @returns
*/
public get(id: Object['id']) {
return this.findById(id)
}
/**
* find the first element that match `query`
*
* @param query a partial object which filter depending on the elements, if not set it will fetch everything
* @returns the first element matching with the query or null otherwise
*/
public async findOne(query?: Partial<Object>): Promise<Object | null> {
return (await this.findAll(query))[0] ?? null
}
/**
* update the remote reference of the object
*
* note: it will not try to insert an item (use `upsert` to handle this)
*
* @param obj the object to update
* @returns an object if it was able to update or null otherwise
*/
abstract update(obj: Object): Promise<Object | null>
/**
* update the remote reference of the object or create it if not found
* @param obj the object to update/insert
* @returns the object is updated/inserted or null otherwise
*/
public async upsert(object: Object | Omit<Object, 'id'>): Promise<Object | null> {
if ('id' in object) {
return this.update(object)
}
return this.insert(object)
}
/**
* Delete the object
* @param obj the object to delete
*
* @returns if the object was deleted or not (if object is not in db it will return true)
*/
abstract delete(obj: Object): Promise<boolean>
}

63
src/models/DaoFactory.ts Normal file
View File

@ -0,0 +1,63 @@
import Client from './Client'
import ConfigDao from './Config/ConfigDao'
import Dao from './Dao'
import UserDao from './User/UserDao'
/**
* TODO:
* Add to `DaoItem` your model name
* Add to the function `initDao` the Dao
*/
/**
* the different Daos that can be initialized
*
* Touch this interface to define which key is linked to which Dao
*/
interface DaoItem {
config: ConfigDao
user: UserDao
}
/**
* Class to get any DAO
*/
export default class DaoFactory {
/**
* reference of the different Daos for a correct singleton implementation
*/
private static daos: Partial<DaoItem> = {}
/**
* Get a a dao by its key
*
* it will throw an error if no Dao exists linked to the item key
*
* @param key the dao key to get
* @returns the Dao you want as a singleton
*/
public static get<Key extends keyof DaoItem>(key: Key): DaoItem[Key] {
if (!(key in this.daos)) {
const dao = this.initDao(key)
if (!dao) {
throw new Error(`${key} has no valid Dao`)
}
this.daos[key] = dao as DaoItem[Key]
}
return this.daos[key] as DaoItem[Key]
}
/**
* init a dao by its key, it does not care if it exists or not
*
* @param item the element to init
* @returns a new initialized dao or undefined if no dao is linked
*/
private static initDao(item: keyof DaoItem): Dao<any> | undefined {
switch (item) {
case 'config': return new ConfigDao()
case 'user': return new UserDao()
default: return undefined
}
}
}

26
src/models/README.md Normal file
View File

@ -0,0 +1,26 @@
# Models
this folder contains the Application data layer
## Workflow
1. Add a {model}/index.ts contianing your `interface`
2. Add a {model}/{model}Dao.ts containing your DAO that `extends` from `Dao.ts`
3. Add your Dao to the `DaoFactory.ts` file
## **/index.ts
file containing the definition of the model
## **/\*Dao.ts
File containing the implementation of the Dao
## Dao.ts
the Dao.ts is the file each `*Dao.ts` extends from allowing to have a simple, quick and easy to comprehend connectivity
## DaoFactory.ts
The DaoFactory file is the file in which you will have the only direct reference to each `*Dao` files and will be sent from there to the rest of the applicaiton layer

View File

@ -0,0 +1,68 @@
import { objectOmit } from '@dzeio/object-util'
import mongoose, { ObjectId } from 'mongoose'
import User from '.'
import Client from '../Client'
import Dao from '../Dao'
export default class UserDao extends Dao<User> {
private model = mongoose.model('User', new mongoose.Schema({
email: { type: String, required: true }
}, {
timestamps: true
}))
private collection = 'users'
private idx = 0
public async create(obj: Omit<User, 'id' | 'created' | 'updated'>): Promise<User | null> {
await Client.get()
return this.fromSource(await this.model.create(obj))
}
public async findAll(query?: Partial<User> | undefined): Promise<User[]> {
await Client.get()
if (query?.id) {
const item = await this.model.findById(new mongoose.Types.ObjectId(query.id))
if (!item) {
return []
}
return [this.fromSource(item)]
}
const resp = await this.model.find(query ? this.toSource(query as User) : {})
return resp.map(this.fromSource)
}
public async update(obj: User): Promise<User | null> {
await Client.get()
const query = await this.model.updateOne({
_id: new mongoose.Types.ObjectId(obj.id)
}, this.toSource(obj))
if (query.matchedCount >= 1) {
obj.updated = new Date()
return obj
}
return null
// return this.fromSource()
}
public async delete(obj: User): Promise<boolean> {
await Client.get()
const res = await this.model.deleteOne({
_id: new mongoose.Types.ObjectId(obj.id)
})
return res.deletedCount > 0
}
private toSource(obj: User): Omit<User, 'id'> {
return objectOmit(obj, 'id', 'updated', 'created')
}
private fromSource(doc: mongoose.Document<any, any, User>): User {
return {
id: doc._id.toString(),
email: doc.get('email'),
updated: doc.get('updatedAt'),
created: doc.get('createdAt')
}
}
}

6
src/models/User/index.ts Normal file
View File

@ -0,0 +1,6 @@
export default interface User {
id: string
email: string
created: Date
updated: Date
}

View File

@ -1,178 +1,178 @@
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 } from 'node:child_process'
import fs from 'node:fs/promises'
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
/**
* body is the stl
* query
* price: algorithm from settings
* adionnal settings from https://manual.slic3r.org/advanced/command-line
*/
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: 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 {
const tmp = evaluate(algo, params)
if (typeof tmp === 'number') {
price = tmp.toFixed(2)
} else {
return buildRFC7807Error({
type: '/algorithm-error',
status: 500,
title: 'Algorithm compilation error',
details: 'It seems the algorithm resolution failed',
algorithm: algo,
algorithmError: 'Algorithm return a Unit',
parameters: params
})
}
} catch (e) {
console.dir(e)
return buildRFC7807Error({
type: '/algorithm-error',
status: 500,
title: 'Algorithm compilation error',
details: 'It seems the algorithm resolution failed',
algorithm: algo,
algorithmError: e,
parameters: params
})
}
}
console.log('request successfull :)', file)
return {
status: 200,
body: JSON.stringify({
price: price ? parseFloat(price) : undefined,
...getParams(gcode),
gcode
})
}
}
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 } from 'node:child_process'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path/posix'
import { promisify } from 'node:util'
import FilesUtils from '../../../libs/FilesUtils'
import { buildRFC7807 } from '../../../libs/RFCs/RFC7807'
import { getParams } from '../../../libs/gcodeUtils'
const exec = promisify(execSync)
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 ({ 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 buildRFC7807({
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 buildRFC7807({
type: '/object-too-large',
status: 413,
title: 'Object is too large'
})
} else if (line.includes('No such file')) {
await fs.rm(stlPath)
return buildRFC7807({
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 buildRFC7807({
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 buildRFC7807({
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 buildRFC7807({
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 buildRFC7807({
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: 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 {
const tmp = evaluate(algo, params)
if (typeof tmp === 'number') {
price = tmp.toFixed(2)
} else {
return buildRFC7807({
type: '/algorithm-error',
status: 500,
title: 'Algorithm compilation error',
details: 'It seems the algorithm resolution failed',
algorithm: algo,
algorithmError: 'Algorithm return a Unit',
parameters: params
})
}
} catch (e) {
console.dir(e)
return buildRFC7807({
type: '/algorithm-error',
status: 500,
title: 'Algorithm compilation error',
details: 'It seems the algorithm resolution failed',
algorithm: algo,
algorithmError: e,
parameters: params
})
}
}
console.log('request successfull :)', file)
return {
status: 200,
body: JSON.stringify({
price: price ? parseFloat(price) : undefined,
...getParams(gcode),
gcode
})
}
}

View File

@ -1,36 +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>
---
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>