Files
bigcode c3df5d9db1 v0.15.0: Docker 化部署(OVH-US-BAK)
- Dockerfile(oven/bun 多阶段:构建 dist + 全量 node_modules 运行时)
- deploy/docker-compose.yml(postgres:18 + app,uploads 双挂载同宿主目录)
- .dockerignore(uploads 经卷挂载不进镜像,避免 3GB 膨胀)
- prod-server /uploads/* 运行时直读磁盘(媒体上传后无需重启即可访问,含路径穿越防护)
- README Docker 部署章节
2026-11-04 15:15:58 +08:00

61 lines
2.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 脚本 生产环境入口
* 包装 dist/server/server.js(TanStack Start 的 fetch handler):
* 先用 dist/client 的静态文件(构建产物 JS/CSS 等)直接应答,未命中的请求
* 交给 Start handler。没有这层包装时 /assets/* 会被路由 307 加尾斜杠后 404,
* 页面完全无 JS(水合不运行)。
*/
// 构建产物无类型声明(产物哈希文件名随构建变化)
// @ts-expect-error 构建产物无 d.ts
import serverEntry from "../dist/server/server.js";
import { readdirSync } from "node:fs";
import { resolve, sep } from "node:path";
/** # 成员变量 dist/client 静态文件表(pathname → 绝对路径,启动时递归扫描一次) **/
const CLIENT_DIR = new URL("../dist/client/", import.meta.url).pathname;
const staticFiles = new Map<string, string>();
/** # 成员变量 uploads 根目录(媒体上传写入 public/uploads,运行时直读,启动静态表不含新增文件) **/
const UPLOADS_ROOT = resolve(process.cwd(), "public", "uploads");
/** # 函数 递归收集 dist/client 下全部静态文件 */
function walk(dir: string): void {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = dir + "/" + e.name;
if (e.isDirectory()) walk(p);
// 归一化 key:物理路径去掉 CLIENT_DIR 前缀(可能残留多余斜杠)后补单斜杠
else staticFiles.set("/" + p.slice(CLIENT_DIR.length).replace(/^\/+/, ""), p);
}
}
walk(CLIENT_DIR);
export default {
/** # 成员变量 监听端口(PORT 环境变量可覆盖) **/
port: Number(process.env.PORT ?? 3312),
/** # 函数 请求入口:/uploads 直读磁盘(新增文件无需重启)→ 启动静态表 → Start handler */
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url);
// /uploads/ 运行时直读磁盘:媒体库上传落在 public/uploads,静态表启动时已定格,
// 不直读会导致运行中上传的图片 404。resolve 后校验前缀,防路径穿越。
if (url.pathname.startsWith("/uploads/")) {
const abs = resolve(UPLOADS_ROOT, "." + url.pathname.slice("/uploads".length));
if (abs.startsWith(UPLOADS_ROOT + sep)) {
const uf = Bun.file(abs);
if (await uf.exists()) {
return new Response(uf, { headers: { "cache-control": "public, max-age=3600" } });
}
}
}
// 尾斜杠归一化:偶发的 "<file>.jpg/" 形态请求(SPA 预取取回/上游转发)直接按静态
// 文件应答,避免交给 Start handler 后落入路由 catch-all 返回 404
let f = staticFiles.get(url.pathname);
if (!f && url.pathname.endsWith("/")) f = staticFiles.get(url.pathname.replace(/\/+$/, ""));
if (f) {
return new Response(Bun.file(f), {
headers: { "cache-control": "public, max-age=31536000, immutable" },
});
}
return serverEntry.fetch(req);
},
};