65 lines
2.4 KiB
JavaScript
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 };
|