aps-agent/server/entry/static.ts

53 lines
2.2 KiB
TypeScript
Raw Permalink Normal View History

/**
* 客户端静态资源托管(任务 A1.5)。
*
* 构建后的客户端(client/app/dist)由平台服务托管,这样一个端口就能跑起完整平台——
* 桌面端与浏览器都指向同一个服务地址。未构建时返回 false,由调用方按 404 处理。
*/
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import type { ServerResponse } from 'node:http';
import { extname, join, normalize, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const MIME_TYPES: Record<string, string> = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.woff2': 'font/woff2',
};
/** 默认目录:仓库内 client/app/dist;可用 APS_CLIENT_DIST 覆盖 */
export function clientDistDir(): string {
const override = process.env.APS_CLIENT_DIST;
if (override && override.trim() !== '') return resolve(override);
// server/dist/entry/static.js → 仓库根
return fileURLToPath(new URL('../../../client/app/dist', import.meta.url));
}
export function createStaticHandler(root = clientDistDir()): (urlPath: string, res: ServerResponse) => Promise<boolean> {
const rootDir = resolve(root);
const indexFile = join(rootDir, 'index.html');
return async function serveStatic(urlPath: string, res: ServerResponse): Promise<boolean> {
if (!existsSync(indexFile)) return false;
const relativePath = normalize(decodeURIComponent(urlPath)).replace(/^[/\\]+/, '');
const candidate = resolve(rootDir, relativePath);
const insideRoot = candidate === rootDir || candidate.startsWith(rootDir + sep);
if (!insideRoot) return false; // 目录穿越
const file = relativePath === '' || !existsSync(candidate) || !extname(candidate) ? indexFile : candidate;
const body = await readFile(file);
res.writeHead(200, {
'content-type': MIME_TYPES[extname(file)] ?? 'application/octet-stream',
'cache-control': file === indexFile ? 'no-store' : 'public, max-age=3600',
});
res.end(body);
return true;
};
}