web-font/backend/upload.ts
崮生(子虚) 7fceccfe4d 重构前端UI,新增字体上传/下载/模糊匹配功能
- 前端UI重构:字体选择器、文本预览、CSS输出+复制、下载字体、上传区域
- 后端新增API:/api/fonts(列出字体)、/api/config(公开配置)、/api/upload(上传字体)
- 字体查找支持模糊匹配(精确 > 前缀 > 包含)
- 上传功能分两种模式:临时上传(ENV开关,FIFO上限10个)和管理员上传(API Key认证)
- 新增multipart解析器、配置模块、文件系统抽象(writeFile/readdir/mkdir/unlink)
- 后端启动时等待运行时初始化完成,确保接口可用
- dev-all.ts后端启用tsx watch模式自动重载

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 16:18:46 +08:00

99 lines
3.0 KiB
TypeScript

import { writeFile, unlink, readdir, path_join } from "./interface";
import { enableTempUpload, adminApiKey, tempMaxFiles } from "./config";
/** 允许的字体文件扩展名 */
const ALLOWED_EXTENSIONS = [".ttf", ".otf", ".woff", ".woff2"];
function isAllowedFontFile(filename: string): boolean {
const lower = filename.toLowerCase();
return ALLOWED_EXTENSIONS.some((ext) => lower.endsWith(ext));
}
/** 清理文件名,移除路径分隔符和危险字符 */
function sanitizeFilename(filename: string): string {
return filename.replace(/[/\\]/g, "").replace(/[\x00-\x1f]/g, "");
}
/** 生成带时间戳前缀的文件名,用于 FIFO 排序 */
function generateTempFilename(originalName: string): string {
const ts = Date.now().toString(36);
const ext = originalName.includes(".") ? "." + originalName.split(".").pop() : ".ttf";
const base = originalName.replace(/\.[^.]+$/, "");
return `${ts}_${base}${ext}`;
}
/** 获取临时目录中的字体文件列表 */
async function getTempFiles(): Promise<string[]> {
const entries = await readdir("font/temp");
return entries
.filter((e) => e.isFile() && isAllowedFontFile(e.name))
.map((e) => e.name)
.sort();
}
/** 确保目录存在,不存在则创建 */
async function ensureDir(dir: string) {
const { stat, mkdir } = await import("./interface");
try {
await stat(dir);
} catch {
await mkdir(dir);
}
}
export interface UploadResult {
success: boolean;
error?: string;
}
export async function handleTempUpload(fileData: { data: Uint8Array; filename: string }): Promise<UploadResult> {
if (!enableTempUpload) {
return { success: false, error: "临时上传功能未启用" };
}
if (!isAllowedFontFile(fileData.filename)) {
return { success: false, error: "不支持的字体文件格式,仅支持 ttf/otf/woff/woff2" };
}
await ensureDir("font/temp");
const existingFiles = await getTempFiles();
// FIFO: 超出上限时删除最早的文件
if (existingFiles.length >= tempMaxFiles) {
const toDelete = existingFiles[0];
try {
await unlink(path_join("font/temp", toDelete));
} catch {
// 删除失败不影响上传
}
}
const filename = generateTempFilename(sanitizeFilename(fileData.filename));
await writeFile(path_join("font/temp", filename), fileData.data);
return { success: true };
}
export async function handleAdminUpload(
fileData: { data: Uint8Array; filename: string },
apiKey: string,
): Promise<UploadResult> {
if (!adminApiKey) {
return { success: false, error: "管理员上传功能未启用" };
}
if (apiKey !== adminApiKey) {
return { success: false, error: "API Key 无效" };
}
if (!isAllowedFontFile(fileData.filename)) {
return { success: false, error: "不支持的字体文件格式,仅支持 ttf/otf/woff/woff2" };
}
await ensureDir("font/admin");
const filename = sanitizeFilename(fileData.filename);
await writeFile(path_join("font/admin", filename), fileData.data);
return { success: true };
}