Files
ark-stronghold-protocol/server/http/media.js
T
sganggs 1b38e30819 feedback5 R1: startServer and main become wiring — PORT / HOST / TRUST_PROXY and the option picks go to http/config.js, the session stack and the /ws upgrade to http/websocket.js, the request listener and /healthz to http/routes.js (new), the security headers and the clientError 400 to http/common.js, the banner, signals and entry check to http/boot.js
Same order of construction, listeners and headers as before; a scripted fingerprint (export list, 51 HTTP requests'
status / header order / body hash, raw upgrade and malformed-request replies) and the process run (banner, port in
use, SIGINT) match the pre-split server exactly. test/version.test.js reads the banner and /healthz from their new
files; module headers describe each piece.
2026-10-05 16:08:41 +08:00

53 lines
3.1 KiB
JavaScript

// server/http/media.js — the extension-less audio route /media/… → public/assets/audio (see serveMedia).
import fsp from 'node:fs/promises';
import path from 'node:path';
import { AUDIO_EXTS } from '../../shared/media.js';
import { sendError } from './common.js';
import { serveFile } from './files.js';
/**
* Extension-less audio route: `/media/bgm/act1` → `public/assets/audio/bgm/act1.mp3`.
*
* Clients ask for audio through this path because download managers (IDM, 迅雷, FDM …) hijack XHR/fetch whose
* URL ends in a media extension and pop a "下载文件信息" dialog for every BGM track — see `public/js/media.js`.
* Requests for the direct `/assets/audio/…` URLs keep working (they are the fallback for plain static hosts).
* `MEDIA_PREFIX` / `AUDIO_EXTS` live in `shared/media.js`: the browser decides which URLs to rewrite with the
* same two values, and they must not drift apart.
*/
export async function serveMedia(req, res, rest, query, publicDir, gzipCache, log) {
const root = path.join(path.resolve(publicDir), 'assets', 'audio');
const segments = String(rest || '').split('/').filter((s) => s.length > 0);
if (!segments.length || rest.endsWith('/')) { sendError(req, res, 404, '页面不存在 · Not found'); return; }
if (segments.some((s) => s === '..' || s === '.')) { sendError(req, res, 403, '禁止访问 · Forbidden'); return; }
// A leading or trailing dot would address something else (dotfiles, "x..mp3") — and the client never asks for it.
if (segments.some((s) => s.startsWith('.') || s.endsWith('.'))) { sendError(req, res, 404, '页面不存在 · Not found'); return; }
const last = segments[segments.length - 1];
const given = path.extname(last).toLowerCase();
const wanted = AUDIO_EXTS.includes(given) ? given : '';
const stem = wanted ? last.slice(0, -wanted.length) : last;
if (!stem || stem.startsWith('.')) { sendError(req, res, 404, '页面不存在 · Not found'); return; }
const dir = path.join(root, ...segments.slice(0, -1));
if (dir !== root && !dir.startsWith(root + path.sep)) { sendError(req, res, 403, '禁止访问 · Forbidden'); return; }
// An explicit extension wins (`/media/bgm.ogg` → bgm.ogg), otherwise the usual order decides.
const order = wanted ? [wanted, ...AUDIO_EXTS.filter((e) => e !== wanted)] : AUDIO_EXTS;
for (const ext of order) {
const absPath = path.join(dir, stem + ext);
if (!absPath.startsWith(root + path.sep)) continue;
let stat;
try {
// eslint-disable-next-line no-await-in-loop
stat = await fsp.stat(absPath);
} catch { continue; }
if (!stat.isFile()) continue;
// serveFile decides Content-Type from the resolved name (`.mp3` → audio/mpeg) — Range/ETag handling is shared.
// Cache policy is that of the public path the client would otherwise have asked for (`/assets/audio/…`, 1 day).
// eslint-disable-next-line no-await-in-loop
await serveFile(req, res, absPath, stat, 'public', ['assets', 'audio', ...segments], query, gzipCache, log);
return;
}
sendError(req, res, 404, '页面不存在 · Not found');
}