Files

65 lines
2.4 KiB
JavaScript

/* uploads.js — model/texture asset handling for the character manager.
* Files land in LIVE_DIR/assets (Docker volume) and are served at /assets/...
* so uploads survive image rebuilds without being baked into the repo.
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const ALLOWED = {
'.obj': 'model/obj', '.mtl': 'model/mtl',
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp',
};
const MAX_BYTES = 12 * 1024 * 1024;
function safeName(name) {
const base = path.basename(String(name || 'file')).replace(/[^a-zA-Z0-9._-]/g, '_');
return base.slice(-80) || 'file';
}
class Assets {
constructor(liveDir) {
this.dir = path.join(liveDir, 'assets');
this.indexPath = path.join(liveDir, 'assets.json');
fs.mkdirSync(this.dir, { recursive: true });
this.index = this.read();
}
read() {
try { return JSON.parse(fs.readFileSync(this.indexPath, 'utf8')); } catch { return { assets: [] }; }
}
write() { fs.writeFileSync(this.indexPath, JSON.stringify(this.index, null, 2)); }
list() { return this.index; }
/* body: { name, kind: 'model'|'texture', dataBase64 } */
save({ name, kind, dataBase64 }) {
const clean = safeName(name);
const ext = path.extname(clean).toLowerCase();
if (!ALLOWED[ext]) throw new Error(`unsupported file type ${ext || '(none)'} — allowed: ${Object.keys(ALLOWED).join(', ')}`);
const buf = Buffer.from(String(dataBase64 || ''), 'base64');
if (!buf.length) throw new Error('empty file');
if (buf.length > MAX_BYTES) throw new Error(`file too large (${(buf.length / 1048576).toFixed(1)} MB, max 12 MB)`);
const id = crypto.randomBytes(6).toString('hex');
const stored = `${id}${ext}`;
fs.writeFileSync(path.join(this.dir, stored), buf);
const rec = {
id, name: clean, kind: kind === 'texture' ? 'texture' : 'model',
url: `/assets/${stored}`, bytes: buf.length, uploadedAt: Date.now(),
};
this.index.assets.push(rec);
this.write();
return rec;
}
remove(id) {
const i = this.index.assets.findIndex(a => a.id === id);
if (i < 0) throw new Error('asset not found');
const [rec] = this.index.assets.splice(i, 1);
try { fs.unlinkSync(path.join(this.dir, path.basename(rec.url))); } catch {}
this.write();
return rec;
}
}
module.exports = { Assets, ALLOWED, MAX_BYTES };