You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

88 lines
2.7 KiB

import { resolve, join, sep, extname } from "node:path";
import { promises as fsp } from "node:fs";
import {
decodePath,
withLeadingSlash,
withoutTrailingSlash,
parseURL,
} from "ufo";
import type { H3Event } from "h3";
import mime from "mime";
const METHODS = new Set(["HEAD", "GET"]);
const SAFE_BASE_DIR = resolve("public");
// 缓存配置
const CACHE_CONTROL = "public, max-age=31536000, immutable";
const NOT_MODIFIED = 304;
const FORBIDDEN = 403;
const NOT_FOUND = 404;
const SERVER_ERROR = 500;
export default eventHandler(async (event: H3Event) => {
if (!event.path.startsWith("/public")) return;
const { req, res } = event.node;
const method = req.method;
if (method && !METHODS.has(method)) return;
try {
// 安全解析路径
const url = event.path.replace(/^\/public/, "");
const pathname = decodePath(
withLeadingSlash(withoutTrailingSlash(parseURL(url).pathname))
);
const targetPath = join(SAFE_BASE_DIR, pathname);
const resolvedPath = resolve(targetPath);
// 安全校验
if (!resolvedPath.startsWith(SAFE_BASE_DIR + sep)) {
res.statusCode = FORBIDDEN;
return "Forbidden";
}
const stat = await fsp.stat(resolvedPath);
if (!stat.isFile()) {
res.statusCode = NOT_FOUND;
return "Not Found";
}
// ====================== 缓存逻辑 ======================
const mtime = stat.mtime.toUTCString();
const etag = `"${stat.mtime.getTime().toString(16)}-${stat.size.toString(16)}"`;
const contentType = mime.getType(resolvedPath) || "application/octet-stream";
// 设置缓存头
res.setHeader("Cache-Control", CACHE_CONTROL);
res.setHeader("ETag", etag);
res.setHeader("Last-Modified", mtime);
res.setHeader("Content-Type", contentType);
res.setHeader("Content-Length", stat.size);
// 禁用 keep-alive
res.setHeader("Connection", "close");
// 304 协商缓存
const ifNoneMatch = req.headers["if-none-match"];
const ifModifiedSince = req.headers["if-modified-since"];
if (ifNoneMatch === etag || (ifModifiedSince && ifModifiedSince === mtime)) {
res.statusCode = NOT_MODIFIED;
return "";
}
// ======================================================
if (method === "HEAD") return "";
return fsp.readFile(resolvedPath);
} catch (err: any) {
if (err.code === "ENOENT") {
res.statusCode = NOT_FOUND;
return "Not Found";
}
res.statusCode = SERVER_ERROR;
return "Server Error";
}
});