Implement texture proxy and caching mechanism: add endpoint for fetching textures with cache handling

This commit is contained in:
MayaTheShy
2026-03-21 19:27:17 -04:00
parent b5ae28944d
commit 6987b6950c

View File

@@ -2,12 +2,18 @@ import express from 'express';
import { WebSocketServer } from 'ws';
import cors from 'cors';
import { createServer } from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import {
loadFullState, saveFullState, recordItemHistory,
saveItems, saveFurnaces, saveAlerts, saveState,
getHistory, getHistorySummary, closeDb, flushPendingSave,
} from './db.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3001;
const HOST = process.env.HOST || '0.0.0.0';
@@ -72,6 +78,97 @@ function pushCommandToBridge(command) {
// ========== HTTP Server ==========
const server = createServer(app);
// ========== Texture Proxy / Cache ==========
const TEXTURE_CACHE_DIR = process.env.TEXTURE_CACHE_DIR || path.join('/data', 'texture-cache');
const NEGATIVE_CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days for 404s
// Upstream CDN mapping
const TEXTURE_UPSTREAMS = {
minecraft: 'https://cdn.jsdelivr.net/gh/InventivetalentDev/minecraft-assets@1.21.4/assets/minecraft/textures',
computercraft: 'https://raw.githubusercontent.com/cc-tweaked/CC-Tweaked/mc-1.20.x/projects/common/src/main/resources/assets/computercraft/textures',
create: 'https://raw.githubusercontent.com/Creators-of-Create/Create/mc1.20.1/dev/src/main/resources/assets/create/textures',
};
// Ensure cache directory exists
fs.mkdirSync(TEXTURE_CACHE_DIR, { recursive: true });
app.get('/api/texture/:namespace/*', async (req, res) => {
const { namespace } = req.params;
const texturePath = req.params[0]; // e.g. "item/diamond.png"
const upstream = TEXTURE_UPSTREAMS[namespace];
if (!upstream) {
return res.status(404).send('Unknown namespace');
}
// Sanitize path to prevent directory traversal
const safePath = path.normalize(texturePath).replace(/^(\.\.(\/|\\|$))+/, '');
if (safePath.includes('..')) {
return res.status(400).send('Invalid path');
}
const cacheFile = path.join(TEXTURE_CACHE_DIR, namespace, safePath);
const cacheDir = path.dirname(cacheFile);
const missFile = cacheFile + '.miss';
// Check positive cache
try {
if (fs.existsSync(cacheFile)) {
res.set('Content-Type', 'image/png');
res.set('Cache-Control', 'public, max-age=604800'); // 7 days
res.set('X-Texture-Cache', 'HIT');
return res.sendFile(cacheFile);
}
} catch (_) { /* proceed to fetch */ }
// Check negative cache (cached 404s)
try {
if (fs.existsSync(missFile)) {
const stat = fs.statSync(missFile);
const age = Date.now() - stat.mtimeMs;
if (age < NEGATIVE_CACHE_TTL) {
res.set('X-Texture-Cache', 'MISS-HIT');
return res.status(404).send('Not found (cached)');
}
// Expired negative cache, remove it
fs.unlinkSync(missFile);
}
} catch (_) { /* proceed to fetch */ }
// Fetch from upstream
const upstreamUrl = `${upstream}/${safePath}`;
try {
const response = await fetch(upstreamUrl);
if (!response.ok) {
// Cache the 404 so we don't keep hitting upstream
try {
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(missFile, `${response.status}`);
} catch (_) { /* ignore cache write errors */ }
res.set('X-Texture-Cache', 'UPSTREAM-MISS');
return res.status(404).send('Not found');
}
// Save to disk cache
const buffer = Buffer.from(await response.arrayBuffer());
try {
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(cacheFile, buffer);
// Remove any stale negative cache
if (fs.existsSync(missFile)) fs.unlinkSync(missFile);
} catch (_) { /* ignore cache write errors */ }
res.set('Content-Type', response.headers.get('content-type') || 'image/png');
res.set('Cache-Control', 'public, max-age=604800');
res.set('X-Texture-Cache', 'UPSTREAM-HIT');
return res.send(buffer);
} catch (err) {
console.error(`[Texture Proxy] Error fetching ${upstreamUrl}:`, err.message);
return res.status(502).send('Upstream error');
}
});
// Health check
app.get('/api/health', (req, res) => {
res.json({