This commit is contained in:
@@ -2,10 +2,12 @@ import { Router, Request, Response } from 'express'
|
||||
import multer from 'multer'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import slugify from 'slugify'
|
||||
import { db, q, Model, Category, ModelPdf } from '../db/index'
|
||||
import { requireAdmin } from '../middleware/requireAdmin'
|
||||
import { convertStepFile, geometryOutputPath } from '../services/stepConverter'
|
||||
import { convertStepFile, geometryOutputPath, GeometryFile } from '../services/stepConverter'
|
||||
import { thumbnailOutputPath, geometryToTriangles, stlBufferToTriangles, renderThumbnail } from '../services/thumbnailGenerator'
|
||||
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR ?? path.join(process.cwd(), 'uploads')
|
||||
const MAX_FILE_BYTES = parseInt(process.env.MAX_FILE_MB ?? '500', 10) * 1024 * 1024
|
||||
@@ -112,28 +114,31 @@ router.post('/admin/models',
|
||||
name: string; description?: string; category_id?: string; is_public?: string
|
||||
}
|
||||
|
||||
const ext = path.extname(req.file.originalname).toLowerCase().replace('.', '') as 'step' | 'stp' | 'stl'
|
||||
const base = slugify(name, { lower: true, strict: true })
|
||||
let slug = base
|
||||
const ext = path.extname(req.file.originalname).toLowerCase().replace('.', '') as 'step' | 'stp' | 'stl'
|
||||
|
||||
let attempt = 0
|
||||
// Generate a random, unguessable 12-character hex slug (48 bits of entropy).
|
||||
// The model name is stored separately for display — the URL reveals nothing
|
||||
// about the content, making enumeration infeasible without an admin link.
|
||||
let slug = crypto.randomBytes(6).toString('hex')
|
||||
while (q<{ id: number }>(`SELECT id FROM models WHERE slug = ?`).get(slug)) {
|
||||
attempt++
|
||||
slug = `${base}-${attempt}`
|
||||
slug = crypto.randomBytes(6).toString('hex') // collision is astronomically unlikely
|
||||
}
|
||||
|
||||
const relPath = path.relative(UPLOADS_DIR, req.file.path).replace(/\\/g, '/')
|
||||
|
||||
db.prepare(`
|
||||
const insertResult = db.prepare(`
|
||||
INSERT INTO models (slug, name, description, category_id, file_path, file_type, is_public)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(slug, name.trim(), description?.trim() || null, category_id ? parseInt(category_id, 10) : null, relPath, ext, is_public === 'on' ? 1 : 0)
|
||||
|
||||
const modelId = Number(insertResult.lastInsertRowid)
|
||||
const absModelPath = path.join(UPLOADS_DIR, relPath)
|
||||
|
||||
// Convert STEP/STP to pre-processed geometry JSON so the browser never
|
||||
// needs to download the 22 MB WASM parser.
|
||||
// needs to download the 22 MB WASM parser, then generate a thumbnail
|
||||
// from the resulting geometry data.
|
||||
if (ext === 'step' || ext === 'stp') {
|
||||
const absModelPath = path.join(UPLOADS_DIR, relPath)
|
||||
const geoOutPath = geometryOutputPath(absModelPath)
|
||||
const geoOutPath = geometryOutputPath(absModelPath)
|
||||
try {
|
||||
await convertStepFile(absModelPath, geoOutPath)
|
||||
} catch (convErr) {
|
||||
@@ -141,6 +146,33 @@ router.post('/admin/models',
|
||||
// will show a friendly error instead of crashing the upload.
|
||||
console.error('[stepConverter] conversion failed:', (convErr as Error).message)
|
||||
}
|
||||
|
||||
// Generate thumbnail from the geometry JSON (only if conversion succeeded)
|
||||
if (fs.existsSync(geoOutPath)) {
|
||||
try {
|
||||
const geo = JSON.parse(fs.readFileSync(geoOutPath, 'utf8')) as GeometryFile
|
||||
const tris = geometryToTriangles(geo)
|
||||
const thumbPath = thumbnailOutputPath(UPLOADS_DIR, modelId)
|
||||
await renderThumbnail(tris, thumbPath)
|
||||
const thumbRel = path.relative(UPLOADS_DIR, thumbPath).replace(/\\/g, '/')
|
||||
db.prepare(`UPDATE models SET thumbnail_path = ? WHERE id = ?`).run(thumbRel, modelId)
|
||||
} catch (thumbErr) {
|
||||
console.error('[thumbnail] STEP generation failed:', (thumbErr as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
} else if (ext === 'stl') {
|
||||
// Parse the STL directly on the server — no intermediate JSON needed
|
||||
try {
|
||||
const stlBuffer = fs.readFileSync(absModelPath)
|
||||
const tris = stlBufferToTriangles(stlBuffer)
|
||||
const thumbPath = thumbnailOutputPath(UPLOADS_DIR, modelId)
|
||||
await renderThumbnail(tris, thumbPath)
|
||||
const thumbRel = path.relative(UPLOADS_DIR, thumbPath).replace(/\\/g, '/')
|
||||
db.prepare(`UPDATE models SET thumbnail_path = ? WHERE id = ?`).run(thumbRel, modelId)
|
||||
} catch (thumbErr) {
|
||||
console.error('[thumbnail] STL generation failed:', (thumbErr as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect('/admin')
|
||||
|
||||
Reference in New Issue
Block a user