import { readFileSync, readdirSync, statSync } from "node:fs"; import path from "node:path"; import matter from "gray-matter"; import { adConfig1, adConfig2, announcementConfig, backgroundWallpaper, commentConfig, coverImageConfig, fontConfig, footerConfig, friendsConfig, friendsPageConfig, licenseConfig, live2dModelConfig, musicPlayerConfig, navBarConfig, profileConfig, sakuraConfig, sidebarLayoutConfig, siteConfig, sponsorConfig, spineModelConfig, } from "@/config"; import type { AdminConfigBundle, AdminImageRecord, AdminPostRecord, AdminSnapshot, AdminSpecPageRecord, } from "./types"; const PROJECT_ROOT = process.cwd(); const POSTS_DIR = path.join(PROJECT_ROOT, "src", "content", "posts"); const SPEC_DIR = path.join(PROJECT_ROOT, "src", "content", "spec"); const PUBLIC_DIR = path.join(PROJECT_ROOT, "public"); const IMAGE_EXTENSIONS = new Set([ ".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif", ]); const MARKDOWN_EXTENSIONS = new Set([".md", ".mdx"]); function toUnixPath(value: string): string { return value.replace(/\\/g, "/"); } function readUtf8(filePath: string): string { return readFileSync(filePath, "utf8"); } function statSafe(filePath: string) { try { return statSync(filePath); } catch { return null; } } function walkFiles(rootDir: string, matcher: (filePath: string) => boolean): string[] { if (!statSafe(rootDir)?.isDirectory()) { return []; } const results: string[] = []; const stack = [rootDir]; while (stack.length > 0) { const currentDir = stack.pop(); if (!currentDir) continue; for (const entry of readdirSync(currentDir, { withFileTypes: true })) { const absolutePath = path.join(currentDir, entry.name); if (entry.isDirectory()) { stack.push(absolutePath); continue; } if (matcher(absolutePath)) { results.push(absolutePath); } } } return results.sort((left, right) => left.localeCompare(right)); } function toPreviewUrl(origin: AdminImageRecord["origin"], relativeToRoot: string): string { const encodedPath = relativeToRoot .split("/") .map((segment) => encodeURIComponent(segment)) .join("/"); return `/_admin/files/${origin}/${encodedPath}`; } function getPostRecords(): AdminPostRecord[] { const markdownFiles = walkFiles(POSTS_DIR, (filePath) => MARKDOWN_EXTENSIONS.has(path.extname(filePath).toLowerCase()), ); return markdownFiles .map((filePath) => { const raw = readUtf8(filePath); const parsed = matter(raw); const stats = statSync(filePath); const relativePath = toUnixPath(path.relative(PROJECT_ROOT, filePath)); const relativeToPosts = toUnixPath(path.relative(POSTS_DIR, filePath)); const slug = relativeToPosts.replace(/\.(md|mdx)$/i, ""); const data = parsed.data as Record; return { slug, filePath: relativePath, title: String(data.title ?? slug), published: String(data.published ?? ""), updated: String(data.updated ?? ""), description: String(data.description ?? ""), image: String(data.image ?? ""), tags: Array.isArray(data.tags) ? data.tags.map((item) => String(item)) : [], category: String(data.category ?? ""), lang: String(data.lang ?? ""), draft: Boolean(data.draft ?? false), pinned: Boolean(data.pinned ?? false), comment: Boolean(data.comment ?? true), author: String(data.author ?? ""), sourceLink: String(data.sourceLink ?? ""), licenseName: String(data.licenseName ?? ""), licenseUrl: String(data.licenseUrl ?? ""), body: parsed.content, extension: path.extname(filePath).toLowerCase() === ".mdx" ? "mdx" : "md", excerpt: parsed.content.trim().slice(0, 180), modifiedAt: stats.mtime.toISOString(), }; }) .sort((left, right) => { const pinnedDiff = Number(right.pinned) - Number(left.pinned); if (pinnedDiff !== 0) return pinnedDiff; return right.published.localeCompare(left.published); }); } function getSpecPageRecords(): AdminSpecPageRecord[] { const markdownFiles = walkFiles(SPEC_DIR, (filePath) => MARKDOWN_EXTENSIONS.has(path.extname(filePath).toLowerCase()), ); return markdownFiles.map((filePath) => ({ slug: path.basename(filePath).replace(/\.(md|mdx)$/i, ""), filePath: toUnixPath(path.relative(PROJECT_ROOT, filePath)), body: readUtf8(filePath), extension: path.extname(filePath).toLowerCase() === ".mdx" ? "mdx" : "md", modifiedAt: statSync(filePath).mtime.toISOString(), })); } function getImageRecords(): AdminImageRecord[] { const imageRoots: Array<{ dir: string; origin: AdminImageRecord["origin"]; }> = [ { dir: PUBLIC_DIR, origin: "public" }, { dir: POSTS_DIR, origin: "content" }, ]; return imageRoots.flatMap(({ dir, origin }) => { return walkFiles(dir, (filePath) => IMAGE_EXTENSIONS.has(path.extname(filePath).toLowerCase()), ).map((filePath) => { const stats = statSync(filePath); const relativePath = toUnixPath(path.relative(PROJECT_ROOT, filePath)); const relativeToRoot = toUnixPath(path.relative(dir, filePath)); return { id: relativePath, name: path.basename(filePath), path: relativePath, url: toPreviewUrl(origin, relativeToRoot), directory: toUnixPath(path.dirname(relativePath)), origin, size: stats.size, updatedAt: stats.mtime.toISOString(), sitePath: origin === "public" ? `/${relativeToRoot}` : relativeToRoot, }; }); }); } function getConfigBundle(): AdminConfigBundle { return { siteConfig, profileConfig, navBarConfig, sidebarConfig: sidebarLayoutConfig, backgroundWallpaper, announcementConfig, footerConfig, adConfig: { adConfig1, adConfig2, }, commentConfig, musicConfig: musicPlayerConfig, pioConfig: { spineModelConfig, live2dModelConfig, }, sponsorConfig, friendsConfig: { friendsPageConfig, friendsConfig, }, licenseConfig, coverImageConfig, sakuraConfig, fontConfig, }; } export function getAdminSnapshot(): AdminSnapshot { return { generatedAt: new Date().toISOString(), configs: getConfigBundle(), posts: getPostRecords(), specPages: getSpecPageRecords(), images: getImageRecords(), }; }