feat: serveStatic을 사용하여 정적 자산 제공 기능 추가 및 createStaticRouter 제거
This commit is contained in:
parent
3424d13c11
commit
eb06208f80
2 changed files with 12 additions and 146 deletions
|
|
@ -1,10 +1,10 @@
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { cors } from "hono/cors";
|
import { cors } from "hono/cors";
|
||||||
import { serve } from "@hono/node-server";
|
import { serve } from "@hono/node-server";
|
||||||
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { createInterface as createReadlineInterface } from "node:readline";
|
import { createInterface as createReadlineInterface } from "node:readline";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
|
|
||||||
import { connectDB } from "./database.ts";
|
import { connectDB } from "./database.ts";
|
||||||
import { createDiffRouter, DiffManager } from "./diff/mod.ts";
|
import { createDiffRouter, DiffManager } from "./diff/mod.ts";
|
||||||
import { get_setting, initializeSetting } from "./SettingConfig.ts";
|
import { get_setting, initializeSetting } from "./SettingConfig.ts";
|
||||||
|
|
@ -16,7 +16,6 @@ import { createSettingsRouter } from "./route/settings.ts";
|
||||||
import { createComicWatcher } from "./diff/watcher/comic_watcher.ts";
|
import { createComicWatcher } from "./diff/watcher/comic_watcher.ts";
|
||||||
import { loadComicConfig } from "./diff/watcher/ComicConfig.ts";
|
import { loadComicConfig } from "./diff/watcher/ComicConfig.ts";
|
||||||
import { getTagRounter } from "./route/tags.ts";
|
import { getTagRounter } from "./route/tags.ts";
|
||||||
import { createStaticRouter } from "./util/static.ts";
|
|
||||||
|
|
||||||
config();
|
config();
|
||||||
|
|
||||||
|
|
@ -97,15 +96,17 @@ export async function create_server() {
|
||||||
app.use("*", cors());
|
app.use("*", cors());
|
||||||
app.use("*", createUserHandler(userController));
|
app.use("*", createUserHandler(userController));
|
||||||
|
|
||||||
const staticRouter = createStaticRouter({
|
const assetCacheControl = setting.mode === "development" ? "no-cache" : "public, max-age=3600";
|
||||||
assets: "dist/assets",
|
app.use(
|
||||||
prefix: "/assets",
|
"/assets/*",
|
||||||
headers: {
|
serveStatic<AppEnv>({
|
||||||
"X-Content-Type-Options": "nosniff",
|
root: "./dist",
|
||||||
"Cache-Control": setting.mode === "development" ? "no-cache" : "public, max-age=3600",
|
onFound: (_path, c) => {
|
||||||
},
|
c.header("X-Content-Type-Options", "nosniff");
|
||||||
});
|
c.header("Cache-Control", assetCacheControl);
|
||||||
app.route("/", staticRouter);
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.onError((err, _c) => {
|
app.onError((err, _c) => {
|
||||||
const { status, body } = mapErrorToResponse(normalizeError(err));
|
const { status, body } = mapErrorToResponse(normalizeError(err));
|
||||||
|
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
import { Hono } from "hono";
|
|
||||||
import type { Context } from "hono";
|
|
||||||
import { createReadStream } from "node:fs";
|
|
||||||
import { stat } from "node:fs/promises";
|
|
||||||
import { Readable } from "node:stream";
|
|
||||||
import { extname, resolve } from "node:path";
|
|
||||||
|
|
||||||
const MIME_TYPES: Record<string, string> = {
|
|
||||||
html: "text/html; charset=utf-8",
|
|
||||||
css: "text/css; charset=utf-8",
|
|
||||||
js: "application/javascript; charset=utf-8",
|
|
||||||
mjs: "application/javascript; charset=utf-8",
|
|
||||||
map: "application/json; charset=utf-8",
|
|
||||||
json: "application/json; charset=utf-8",
|
|
||||||
txt: "text/plain; charset=utf-8",
|
|
||||||
svg: "image/svg+xml",
|
|
||||||
png: "image/png",
|
|
||||||
jpg: "image/jpeg",
|
|
||||||
jpeg: "image/jpeg",
|
|
||||||
gif: "image/gif",
|
|
||||||
webp: "image/webp",
|
|
||||||
avif: "image/avif",
|
|
||||||
ico: "image/x-icon",
|
|
||||||
woff: "font/woff",
|
|
||||||
woff2: "font/woff2",
|
|
||||||
ttf: "font/ttf",
|
|
||||||
otf: "font/otf",
|
|
||||||
};
|
|
||||||
|
|
||||||
const toPosix = (value: string) => value.replace(/\\/g, "/");
|
|
||||||
|
|
||||||
const isPathWithinRoot = (candidate: string, root: string) => {
|
|
||||||
const normalizedCandidate = toPosix(resolve(candidate));
|
|
||||||
const normalizedRoot = toPosix(resolve(root));
|
|
||||||
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMimeType = (path: string) => {
|
|
||||||
const ext = extname(path).slice(1).toLowerCase();
|
|
||||||
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateETag = (mtimeMs: number, size: number) => `W/"${size.toString(16)}-${Math.round(mtimeMs).toString(16)}"`;
|
|
||||||
|
|
||||||
export type StaticPluginOptions = {
|
|
||||||
assets: string;
|
|
||||||
prefix?: string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildResponse = (status: number, headers: Record<string, string>, body: BodyInit | null) =>
|
|
||||||
new Response(body, { status, headers });
|
|
||||||
|
|
||||||
const resolveWildcard = (context: Context, wildcardParam: string) => {
|
|
||||||
const wildcard = context.req.param(wildcardParam) ?? "";
|
|
||||||
if (wildcard.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const decoded = decodeURI(wildcard);
|
|
||||||
if (decoded.includes("\0")) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return decoded;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStaticRequest = async (
|
|
||||||
context: Context,
|
|
||||||
rootDir: string,
|
|
||||||
headersTemplate: Record<string, string>,
|
|
||||||
sendBody: boolean,
|
|
||||||
) => {
|
|
||||||
const pathFragment = resolveWildcard(context, "*");
|
|
||||||
if (!pathFragment) {
|
|
||||||
return buildResponse(404, {}, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const absolutePath = resolve(rootDir, pathFragment);
|
|
||||||
if (!isPathWithinRoot(absolutePath, rootDir)) {
|
|
||||||
return buildResponse(404, {}, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStat = await stat(absolutePath).catch(() => undefined);
|
|
||||||
if (!fileStat || fileStat.isDirectory()) {
|
|
||||||
return buildResponse(404, {}, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseHeaders: Record<string, string> = {
|
|
||||||
...headersTemplate,
|
|
||||||
"Last-Modified": fileStat.mtime.toUTCString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const etag = generateETag(fileStat.mtimeMs, fileStat.size);
|
|
||||||
responseHeaders.ETag = etag;
|
|
||||||
|
|
||||||
const ifNoneMatch = context.req.header("if-none-match");
|
|
||||||
if (ifNoneMatch && ifNoneMatch === etag) {
|
|
||||||
return buildResponse(304, responseHeaders, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ifModifiedSince = context.req.header("if-modified-since");
|
|
||||||
if (ifModifiedSince) {
|
|
||||||
const since = new Date(ifModifiedSince);
|
|
||||||
if (!Number.isNaN(since.getTime()) && fileStat.mtime <= since) {
|
|
||||||
return buildResponse(304, responseHeaders, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
responseHeaders["Content-Type"] = getMimeType(absolutePath);
|
|
||||||
responseHeaders["Content-Length"] = fileStat.size.toString();
|
|
||||||
|
|
||||||
if (!sendBody) {
|
|
||||||
return buildResponse(200, responseHeaders, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeStream = createReadStream(absolutePath);
|
|
||||||
const body = Readable.toWeb(nodeStream) as unknown as ReadableStream<Uint8Array>;
|
|
||||||
return buildResponse(200, responseHeaders, body);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createStaticRouter = ({ assets, prefix = "/public", headers = {} }: StaticPluginOptions) => {
|
|
||||||
const trimmedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
||||||
const normalizedPrefix = trimmedPrefix.startsWith("/") ? trimmedPrefix : `/${trimmedPrefix}`;
|
|
||||||
const wildcardRoute = normalizedPrefix === "/" ? "/*" : `${normalizedPrefix}/*`;
|
|
||||||
const rootDir = resolve(process.cwd(), assets);
|
|
||||||
const headersTemplate = { ...headers };
|
|
||||||
|
|
||||||
const router = new Hono();
|
|
||||||
router.get(wildcardRoute, (c) => handleStaticRequest(c, rootDir, headersTemplate, true));
|
|
||||||
router.on("HEAD", wildcardRoute, (c) => handleStaticRequest(c, rootDir, headersTemplate, false));
|
|
||||||
return router;
|
|
||||||
};
|
|
||||||
Loading…
Add table
Reference in a new issue