import { Router, type IRouter } from 'express'; import multer from 'multer'; import { validate, schemas } from '../middleware/validation.middleware.js'; import { NotFoundError } from '../middleware/error.middleware.js'; import * as modelsService from '../services/models.service.js'; import { addThumbnailJob } from '../services/queue.service.js'; import logger from '../utils/logger.js'; const router: IRouter = Router(); // Configure multer for thumbnail uploads (memory storage for small images) const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 2 * 1024 * 1024, // 2MB max }, fileFilter: (req, file, cb) => { if (file.mimetype === 'image/png' || file.mimetype === 'image/jpeg') { cb(null, true); } else { cb(new Error('Only PNG and JPEG images are allowed')); } }, }); /** * GET /models - List all models */ router.get('/', validate(schemas.modelListQuery, 'query'), async (req, res, next) => { try { const query = req.query as unknown as { search?: string; status?: 'pending' | 'processing' | 'completed' | 'failed'; format?: string; limit: number; offset: number; }; const result = await modelsService.getModels({ search: query.search, status: query.status, format: query.format, limit: Number(query.limit) || 20, offset: Number(query.offset) || 0, }); res.json({ success: true, data: result.models, meta: { total: result.total, limit: Number(query.limit) || 20, offset: Number(query.offset) || 0, }, }); } catch (error) { next(error); } }); /** * GET /models/:id - Get a single model */ router.get('/:id', validate(schemas.uuidParam, 'params'), async (req, res, next) => { try { const { id } = req.params; const model = await modelsService.getModelById(id); if (!model) { throw new NotFoundError('Model not found'); } res.json({ success: true, data: model, }); } catch (error) { next(error); } }); /** * GET /models/:id/parts - Get model parts */ router.get('/:id/parts', validate(schemas.uuidParam, 'params'), async (req, res, next) => { try { const { id } = req.params; const model = await modelsService.getModelById(id); if (!model) { throw new NotFoundError('Model not found'); } const parts = await modelsService.getModelParts(id); res.json({ success: true, data: parts, }); } catch (error) { next(error); } }); /** * GET /models/:id/url - Get download URL for viewing */ router.get('/:id/url', validate(schemas.uuidParam, 'params'), async (req, res, next) => { try { const { id } = req.params; const url = await modelsService.getModelDownloadUrl(id); if (!url) { throw new NotFoundError('Model not ready or not found'); } res.json({ success: true, data: { url }, }); } catch (error) { next(error); } }); /** * GET /models/:id/lod - Get all LOD URLs for a model */ router.get('/:id/lod', validate(schemas.uuidParam, 'params'), async (req, res, next) => { try { const { id } = req.params; const lodUrls = await modelsService.getModelLodUrls(id); if (!lodUrls) { throw new NotFoundError('Model not ready or not found'); } res.json({ success: true, data: lodUrls, }); } catch (error) { next(error); } }); /** * GET /models/:id/lod/:level - Get URL for specific LOD level */ router.get('/:id/lod/:level', validate(schemas.uuidParam, 'params'), async (req, res, next) => { try { const { id, level } = req.params; const lodLevel = parseInt(level, 10); if (isNaN(lodLevel) || lodLevel < 0 || lodLevel > 2) { res.status(400).json({ success: false, error: 'Invalid LOD level. Must be 0, 1, or 2.', }); return; } const url = await modelsService.getModelLodUrl(id, lodLevel); if (!url) { throw new NotFoundError('Model not ready or not found'); } res.json({ success: true, data: { url, level: lodLevel }, }); } catch (error) { next(error); } }); /** * PATCH /models/:id - Update model metadata */ router.patch( '/:id', validate(schemas.uuidParam, 'params'), validate(schemas.updateModel), async (req, res, next) => { try { const { id } = req.params; const updates = req.body; const model = await modelsService.updateModel(id, updates); if (!model) { throw new NotFoundError('Model not found'); } logger.info({ modelId: id }, 'Model updated'); res.json({ success: true, data: model, }); } catch (error) { next(error); } } ); /** * DELETE /models/:id - Delete a model */ router.delete('/:id', validate(schemas.uuidParam, 'params'), async (req, res, next) => { try { const { id } = req.params; const deleted = await modelsService.deleteModel(id); if (!deleted) { throw new NotFoundError('Model not found'); } logger.info({ modelId: id }, 'Model deleted'); res.json({ success: true, message: 'Model deleted successfully', }); } catch (error) { next(error); } }); /** * POST /models/:id/thumbnail - Upload a thumbnail for a model */ router.post( '/:id/thumbnail', validate(schemas.uuidParam, 'params'), upload.single('thumbnail'), async (req, res, next) => { try { const { id } = req.params; if (!req.file) { res.status(400).json({ success: false, error: 'No thumbnail file provided', }); return; } const model = await modelsService.uploadThumbnail(id, req.file.buffer); if (!model) { throw new NotFoundError('Model not found'); } logger.info({ modelId: id }, 'Thumbnail uploaded'); res.json({ success: true, data: model, thumbnail_url: model.thumbnail_url, }); } catch (error) { next(error); } } ); /** * POST /models/:id/regenerate-thumbnail - Regenerate thumbnail for a model */ router.post( '/:id/regenerate-thumbnail', validate(schemas.uuidParam, 'params'), async (req, res, next) => { try { const { id } = req.params; const model = await modelsService.getModelById(id); if (!model) { throw new NotFoundError('Model not found'); } if (!model.model_url) { res.status(400).json({ success: false, error: 'Model not ready for thumbnail generation', }); return; } await addThumbnailJob({ modelId: id, modelUrl: model.model_url }); logger.info({ modelId: id }, 'Thumbnail regeneration job queued'); res.json({ success: true, message: 'Thumbnail regeneration job queued', }); } catch (error) { next(error); } } ); export default router;