- Complete web-based STL file storage and 3D viewer - Express.js backend with SQLite database - Interactive Three.js 3D viewer with orbit controls - File upload with drag-and-drop support - Security features: rate limiting, input validation, helmet - Container deployment with Docker/Podman - Production-ready configuration management - Comprehensive logging and monitoring 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
244 lines
6.9 KiB
JavaScript
244 lines
6.9 KiB
JavaScript
const sqlite3 = require('sqlite3').verbose();
|
|
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
|
|
class STLDatabase {
|
|
constructor(dbPath = './stl_storage.db', uploadDir = './uploads') {
|
|
// Use data directory if available (for containers)
|
|
if (process.env.NODE_ENV === 'production' && require('fs').existsSync('./data')) {
|
|
this.dbPath = './data/stl_storage.db';
|
|
} else {
|
|
this.dbPath = dbPath;
|
|
}
|
|
this.uploadDir = uploadDir;
|
|
this.db = null;
|
|
}
|
|
|
|
async initialize() {
|
|
await this.ensureDirectories();
|
|
await this.connectDatabase();
|
|
await this.initializeSchema();
|
|
}
|
|
|
|
async ensureDirectories() {
|
|
try {
|
|
await fs.access(this.uploadDir);
|
|
} catch {
|
|
await fs.mkdir(this.uploadDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
async connectDatabase() {
|
|
return new Promise((resolve, reject) => {
|
|
this.db = new sqlite3.Database(this.dbPath, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async initializeSchema() {
|
|
const schema = await fs.readFile('./schema.sql', 'utf8');
|
|
return new Promise((resolve, reject) => {
|
|
this.db.exec(schema, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async addSTLFile(fileData) {
|
|
const {
|
|
filename,
|
|
originalName,
|
|
filePath,
|
|
fileSize,
|
|
description = null,
|
|
tags = null,
|
|
printSettings = null,
|
|
dimensions = null
|
|
} = fileData;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const stmt = this.db.prepare(`
|
|
INSERT INTO stl_files
|
|
(filename, original_name, file_path, file_size, description, tags, print_settings, dimensions)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
stmt.run([
|
|
filename,
|
|
originalName,
|
|
filePath,
|
|
fileSize,
|
|
description,
|
|
tags,
|
|
printSettings ? JSON.stringify(printSettings) : null,
|
|
dimensions ? JSON.stringify(dimensions) : null
|
|
], function(err) {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(this.lastID);
|
|
}
|
|
});
|
|
|
|
stmt.finalize();
|
|
});
|
|
}
|
|
|
|
async getSTLFile(id) {
|
|
return new Promise((resolve, reject) => {
|
|
this.db.get(
|
|
'SELECT * FROM stl_files WHERE id = ?',
|
|
[id],
|
|
(err, row) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(row ? this.parseRow(row) : null);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async getAllSTLFiles(limit = 50, offset = 0) {
|
|
return new Promise((resolve, reject) => {
|
|
this.db.all(
|
|
'SELECT * FROM stl_files ORDER BY upload_date DESC LIMIT ? OFFSET ?',
|
|
[limit, offset],
|
|
(err, rows) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(rows.map(row => this.parseRow(row)));
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async searchSTLFiles(query) {
|
|
return new Promise((resolve, reject) => {
|
|
this.db.all(
|
|
`SELECT * FROM stl_files
|
|
WHERE filename LIKE ? OR original_name LIKE ? OR description LIKE ? OR tags LIKE ?
|
|
ORDER BY upload_date DESC`,
|
|
[`%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`],
|
|
(err, rows) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(rows.map(row => this.parseRow(row)));
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async updateSTLFile(id, updates) {
|
|
const allowedFields = ['description', 'tags', 'print_settings', 'dimensions'];
|
|
const updateFields = [];
|
|
const values = [];
|
|
|
|
for (const [key, value] of Object.entries(updates)) {
|
|
if (allowedFields.includes(key)) {
|
|
updateFields.push(`${key} = ?`);
|
|
if (key === 'print_settings' || key === 'dimensions') {
|
|
values.push(value ? JSON.stringify(value) : null);
|
|
} else {
|
|
values.push(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updateFields.length === 0) {
|
|
throw new Error('No valid fields to update');
|
|
}
|
|
|
|
values.push(id);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.db.run(
|
|
`UPDATE stl_files SET ${updateFields.join(', ')} WHERE id = ?`,
|
|
values,
|
|
function(err) {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(this.changes > 0);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
async deleteSTLFile(id) {
|
|
const file = await this.getSTLFile(id);
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await fs.unlink(file.file_path);
|
|
} catch (err) {
|
|
console.warn(`Could not delete file ${file.file_path}:`, err.message);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.db.run(
|
|
'DELETE FROM stl_files WHERE id = ?',
|
|
[id],
|
|
function(err) {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(this.changes > 0);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
parseRow(row) {
|
|
const parsed = { ...row };
|
|
|
|
if (parsed.print_settings) {
|
|
try {
|
|
parsed.print_settings = JSON.parse(parsed.print_settings);
|
|
} catch {
|
|
parsed.print_settings = null;
|
|
}
|
|
}
|
|
|
|
if (parsed.dimensions) {
|
|
try {
|
|
parsed.dimensions = JSON.parse(parsed.dimensions);
|
|
} catch {
|
|
parsed.dimensions = null;
|
|
}
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
async close() {
|
|
return new Promise((resolve) => {
|
|
this.db.close((err) => {
|
|
if (err) {
|
|
console.error('Error closing database:', err);
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = STLDatabase; |