import { resolve, join, relative } from "node:path"; import { promises as fsp } from "node:fs"; import { decodePath, withLeadingSlash, withoutTrailingSlash, parseURL } from "ufo"; import mime from "mime"; import { STATIC_DIR, STATIC_PUBLIC_PREFIX } from "../constants/upload"; if (import.meta.dev) { console.log("plugin: 00.static"); } const METHODS = new Set(["HEAD", "GET"]); const SAFE_BASE_DIR = resolve(STATIC_DIR); const CACHE_CONTROL = "public, max-age=31536000, immutable"; export default defineNitroPlugin((nitroApp) => { nitroApp.hooks.hook("request", async (event) => { if (!event.path.startsWith(STATIC_PUBLIC_PREFIX)) return; const { req } = event.node; const method = req.method; if (method && !METHODS.has(method)) return; try { const url = event.path.replace(STATIC_PUBLIC_PREFIX, ""); const pathname = decodePath( withLeadingSlash(withoutTrailingSlash(parseURL(url).pathname)) ); const targetPath = join(SAFE_BASE_DIR, pathname); const resolvedPath = resolve(targetPath); // 安全校验 const rel = relative(SAFE_BASE_DIR, resolvedPath); if (rel.startsWith("..") || rel === "..") { event.respondWith(new Response("Forbidden", { status: 403 })); return; } const stat = await fsp.stat(resolvedPath); if (!stat.isFile()) { event.respondWith(new Response("Not Found", { status: 404 })); return; } const mtime = stat.mtime.toUTCString(); const etag = `"${stat.mtime.getTime().toString(16)}-${stat.size.toString(16)}"`; const contentType = mime.getType(resolvedPath) || "application/octet-stream"; const headers: Record = { "Cache-Control": CACHE_CONTROL, ETag: etag, "Last-Modified": mtime, Connection: "close", }; // 304 协商缓存 const ifNoneMatch = req.headers["if-none-match"]; const ifModifiedSince = req.headers["if-modified-since"]; if (ifNoneMatch === etag || (ifModifiedSince && ifModifiedSince === mtime)) { event.respondWith(new Response(null, { status: 304, headers })); return; } headers["Content-Type"] = contentType; headers["Content-Length"] = String(stat.size); const body = method === "HEAD" ? null : await fsp.readFile(resolvedPath); event.respondWith(new Response(body, { status: 200, headers })); } catch (err: any) { if (err.code === "ENOENT") { event.respondWith(new Response("Not Found", { status: 404 })); return; } event.respondWith(new Response("Server Error", { status: 500 })); } }); });