fix(阶段5): jieba 资源导入不能用在 dev;补上 /public/upload 的伺服
## jieba:两种形态得走两条路
上一条 commit 把 jieba 的 .node 改成 `with { type: "file" }` 内嵌,只验证了
编译产物,没跑 `bun run` —— 结果 dev 直接起不来:
TypeError: To load Node-API modules, use require() or process.dlopen
instead of import.
`.node` 的资源导入只有打包器认,运行时不认。而且必须是**动态** import:
静态 import 在模块加载时就求值,拿 isCompiled 判断也来不及。
所以分两条路:dev 用包自己的入口(那条路 bun run 下是好的),编译形态才走
内嵌资源。两边都实测过了。.wasm 没这个毛病,两种形态都正常。
withBuiltinDict 因此变成 async,连带 buildWordFrequencies 也是。
jieba 实例改成缓存 Promise 而不是结果,并发进来两个请求只会建一次词典。
## /public/upload 之前根本没人伺服
后台上传图片存盘、返回 `/public/upload/<name>`,但没有任何路由处理这个前缀 ——
题面里插的图片一律 404。补上,并和头像共用一个 serveUpload:只取路径最后一段
且要求它和原样一致,`..`、子目录、编码斜杠都在这里被拒。
生产环境这些请求也走后端(Caddy 反代整段 /public),不让 Caddy 直接读盘,
这样开发和生产是同一条代码路径。
dev 模式 8 项实测:上传图片/头像/默认头像回退 200,不存在、`..`、编码穿越、
子目录、编码斜杠全 404。编译产物在干净目录里 7 项照旧全过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,25 +49,55 @@ app.onError((error, c) => {
|
||||
)
|
||||
})
|
||||
|
||||
/** 头像取不到时的占位图,避免每个没设头像的学生都打一次 404 */
|
||||
const DEFAULT_AVATAR_SVG =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><rect width="128" height="128" rx="64" fill="#e2e8f0"/><circle cx="64" cy="48" r="24" fill="#94a3b8"/><path d="M20 120c4-28 22-42 44-42s40 14 44 42" fill="#94a3b8"/></svg>'
|
||||
|
||||
/**
|
||||
* 伺服 /public 下的用户上传文件。
|
||||
*
|
||||
* 只取路径最后一段并要求它和原样一致 —— `..`、子目录、编码过的斜杠都会在这里被拒,
|
||||
* 拼接进 resolve() 的永远只是一个纯文件名。
|
||||
*
|
||||
* 生产环境这些请求也走后端(Caddy 把 /public/* 整段反代过来),不让 Caddy 直接读盘:
|
||||
* 这样开发(Vite 代理)和生产是同一条代码路径,少一处只在服务器上才出错的差异。
|
||||
*/
|
||||
async function serveUpload(pathname: string, prefix: string, directory: string) {
|
||||
const decoded = decodeURIComponent(pathname)
|
||||
const filename = basename(decoded)
|
||||
if (!filename || filename !== decoded.slice(prefix.length + 1)) {
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
const file = Bun.file(resolve(directory, filename))
|
||||
if (await file.exists()) {
|
||||
// 文件名由后端生成且内容不变,可以放心长缓存
|
||||
return new Response(file, { headers: { "cache-control": "public, max-age=86400" } })
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const server = Bun.serve<SubmissionSocketData>({
|
||||
port: config.port,
|
||||
async fetch(request, bunServer) {
|
||||
const url = new URL(request.url)
|
||||
if (url.pathname.startsWith(`${config.avatarUriPrefix}/`)) {
|
||||
const filename = basename(decodeURIComponent(url.pathname))
|
||||
if (filename !== decodeURIComponent(url.pathname).split("/").at(-1)) {
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
const file = Bun.file(resolve(config.avatarDirectory, filename))
|
||||
if (await file.exists()) return new Response(file)
|
||||
if (filename === "default.png") {
|
||||
return new Response(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><rect width="128" height="128" rx="64" fill="#e2e8f0"/><circle cx="64" cy="48" r="24" fill="#94a3b8"/><path d="M20 120c4-28 22-42 44-42s40 14 44 42" fill="#94a3b8"/></svg>',
|
||||
{ headers: { "content-type": "image/svg+xml", "cache-control": "public, max-age=3600" } },
|
||||
)
|
||||
const hit = await serveUpload(url.pathname, config.avatarUriPrefix, config.avatarDirectory)
|
||||
if (hit) return hit
|
||||
if (basename(decodeURIComponent(url.pathname)) === "default.png") {
|
||||
return new Response(DEFAULT_AVATAR_SVG, {
|
||||
headers: { "content-type": "image/svg+xml", "cache-control": "public, max-age=3600" },
|
||||
})
|
||||
}
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
// 题面里插的图片。原来没有这一段 —— 后台上传成功、返回 /public/upload/xxx,
|
||||
// 但没有任何路由伺服它,题面图片一律 404。
|
||||
if (url.pathname.startsWith(`${config.uploadUriPrefix}/`)) {
|
||||
return (
|
||||
(await serveUpload(url.pathname, config.uploadUriPrefix, config.uploadDirectory)) ??
|
||||
new Response("Not found", { status: 404 })
|
||||
)
|
||||
}
|
||||
if (url.pathname === "/ws/submissions" || url.pathname === "/ws/config") {
|
||||
const user = await getRequestSessionUser(request)
|
||||
if (!user) return new Response("Unauthorized", { status: 401 })
|
||||
|
||||
Reference in New Issue
Block a user