Implement texture proxy and caching mechanism: add endpoint for fetching textures with cache handling
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user