146 lines
4.9 KiB
JavaScript
146 lines
4.9 KiB
JavaScript
// res_proxy.js —— 本地资源代理(res.3dtank.com → 本地缓存)
|
|
//
|
|
// 为什么需要:官方 3DS 贴图(BG1 1024.PNG / SKY1.PNG 等)没有 CORS 头,
|
|
// 浏览器从 localhost 直接请求会被拦截;同时每次开发都走公网很慢。
|
|
//
|
|
// 用法:node tools/res_proxy.js [--port 8082] [--cache build/res-cache]
|
|
// GET /res/<path> → https://res.3dtank.com/<path>,落盘缓存后返回
|
|
// GET /play/<path> → 3DTank/web/play/<path>(本地镜像,可选)
|
|
// GET /health → ok
|
|
//
|
|
// 前端通过 webpack devServer 把 /res 代理到本服务(webpack.config.d/res-proxy.js)。
|
|
'use strict';
|
|
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { URL } = require('url');
|
|
|
|
const Argv = process.argv.slice(2);
|
|
const Arg = (Name, Def) => {
|
|
const I = Argv.indexOf('--' + Name);
|
|
return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def;
|
|
};
|
|
|
|
const Root = path.resolve(__dirname, '..');
|
|
const Port = Number(Arg('port', '8082'));
|
|
const CacheDir = path.resolve(Root, Arg('cache', 'build/res-cache'));
|
|
const MirrorPlay = path.resolve(Root, '..', '3DTank', 'web', 'play');
|
|
const Upstream = 'https://res.3dtank.com';
|
|
|
|
const Mime = {
|
|
'.3ds': 'application/octet-stream',
|
|
'.a3d': 'application/octet-stream',
|
|
'.webp': 'image/webp',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.svg': 'image/svg+xml',
|
|
'.json': 'application/json',
|
|
'.info': 'application/octet-stream',
|
|
'.mp3': 'audio/mpeg',
|
|
'.ktx': 'application/octet-stream',
|
|
};
|
|
|
|
fs.mkdirSync(CacheDir, { recursive: true });
|
|
|
|
function CachePath(Key) {
|
|
const Clean = Key.replace(/^\/+/, '').replace(/[^a-zA-Z0-9._/-]/g, '_').replace(/\.\./g, '_');
|
|
return path.join(CacheDir, Clean);
|
|
}
|
|
|
|
function Send(Res, Status, Type, Body) {
|
|
Res.writeHead(Status, {
|
|
'Content-Type': Type || 'application/octet-stream',
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
'Access-Control-Allow-Headers': '*',
|
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
});
|
|
Res.end(Body);
|
|
}
|
|
|
|
async function FetchUpstream(PathAndQuery) {
|
|
const Decoded = decodeURIComponent(PathAndQuery);
|
|
const Ext = path.extname(Decoded);
|
|
const NoExt = Decoded.slice(0, Decoded.length - Ext.length);
|
|
const Candidates = [
|
|
PathAndQuery,
|
|
Decoded.toLowerCase(),
|
|
(NoExt + '.webp').toLowerCase(),
|
|
(NoExt + '.png').toLowerCase(),
|
|
(NoExt + '.webp'),
|
|
(NoExt + '.png'),
|
|
];
|
|
for (const PathTry of [...new Set(Candidates)]) {
|
|
const R = await fetch(Upstream + (PathTry.startsWith('/') ? PathTry : '/' + PathTry), {
|
|
headers: {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) TankiOffline/0.1',
|
|
Accept: '*/*',
|
|
},
|
|
});
|
|
if (R.ok) {
|
|
const Body = Buffer.from(await R.arrayBuffer());
|
|
return {
|
|
Status: 200,
|
|
Type: R.headers.get('content-type') || 'application/octet-stream',
|
|
Body,
|
|
Resolved: PathTry,
|
|
};
|
|
}
|
|
}
|
|
return { Status: 404, Type: 'text/plain', Body: Buffer.from('upstream 404') };
|
|
}
|
|
|
|
const Server = http.createServer(async (Req, Res) => {
|
|
if (Req.method === 'OPTIONS') {
|
|
Send(Res, 204, 'text/plain', Buffer.alloc(0));
|
|
return;
|
|
}
|
|
const U = new URL(Req.url, 'http://localhost');
|
|
if (U.pathname === '/health') {
|
|
Send(Res, 200, 'text/plain', Buffer.from('ok'));
|
|
return;
|
|
}
|
|
|
|
// 本地镜像:/play/... → 3DTank/web/play/...
|
|
if (U.pathname.startsWith('/play/')) {
|
|
const File = path.join(MirrorPlay, U.pathname.slice('/play/'.length));
|
|
if (File.startsWith(MirrorPlay) && fs.existsSync(File) && fs.statSync(File).isFile()) {
|
|
Send(Res, 200, Mime[path.extname(File).toLowerCase()], fs.readFileSync(File));
|
|
return;
|
|
}
|
|
Send(Res, 404, 'text/plain', Buffer.from('mirror miss'));
|
|
return;
|
|
}
|
|
|
|
// 线上资源:/res/<path> → res.3dtank.com/<path>
|
|
if (!U.pathname.startsWith('/res/')) {
|
|
Send(Res, 404, 'text/plain', Buffer.from('not found'));
|
|
return;
|
|
}
|
|
const Key = U.pathname.slice('/res'.length);
|
|
const Cache = CachePath(Key);
|
|
if (fs.existsSync(Cache) && fs.statSync(Cache).isFile()) {
|
|
Send(Res, 200, Mime[path.extname(Cache).toLowerCase()] || 'application/octet-stream', fs.readFileSync(Cache));
|
|
return;
|
|
}
|
|
try {
|
|
const Result = await FetchUpstream(Key);
|
|
if (Result.Status === 200) {
|
|
fs.mkdirSync(path.dirname(Cache), { recursive: true });
|
|
fs.writeFileSync(Cache, Result.Body);
|
|
}
|
|
Send(Res, Result.Status, Result.Type, Result.Body);
|
|
console.log(`${Result.Status === 200 ? 'cache' : 'fail '} ${Key} (${Result.Body.length}B)`);
|
|
} catch (E) {
|
|
Send(Res, 502, 'text/plain', Buffer.from('proxy error: ' + E.message));
|
|
console.log('error', Key, E.message);
|
|
}
|
|
});
|
|
|
|
Server.listen(Port, '127.0.0.1', () => {
|
|
console.log(`res proxy: http://127.0.0.1:${Port}/res/... (cache: ${CacheDir})`);
|
|
console.log(`play mirror: http://127.0.0.1:${Port}/play/... (${MirrorPlay})`);
|
|
});
|