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
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";
|
|
}
|
|
});
|