mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-05 15:11:15 +00:00
Backend: add CheckWebUIFontExist API and route; set --font-family-mono CSS variable in InitWebUi for aacute/custom/default. Improve webui font uploader: force saved filename to CustomFont, robustly clean old webui/CustomFont files, and log failures. Frontend: add FileManager.checkWebUIFontExists; update theme settings to show upload UI only when 'custom' is selected, display uploaded status, attempt delete-before-upload, reload after actions, and adjust Accordion props. ColorPicker: enable pointer events on PopoverContent to allow dragging. applyFont now sets --font-family-mono for all modes.
666 lines
20 KiB
TypeScript
666 lines
20 KiB
TypeScript
import type { RequestHandler } from 'express';
|
||
import { sendError, sendSuccess } from '../utils/response';
|
||
import fsProm from 'fs/promises';
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import os from 'os';
|
||
import compressing from 'compressing';
|
||
import { PassThrough } from 'stream';
|
||
import multer from 'multer';
|
||
import webUIFontUploader from '../uploader/webui_font';
|
||
import diskUploader from '../uploader/disk';
|
||
import { WebUiConfig, getInitialWebUiToken, webUiPathWrapper } from '@/napcat-webui-backend/index';
|
||
|
||
const isWindows = os.platform() === 'win32';
|
||
|
||
// 安全地从查询参数中提取字符串值,防止类型混淆
|
||
const getQueryStringParam = (param: any): string => {
|
||
if (Array.isArray(param)) {
|
||
return String(param[0] || '');
|
||
}
|
||
return String(param || '');
|
||
};
|
||
|
||
// 获取系统根目录列表(Windows返回盘符列表,其他系统返回['/'])
|
||
const getRootDirs = async (): Promise<string[]> => {
|
||
if (!isWindows) return ['/'];
|
||
|
||
// Windows 驱动器字母 (A-Z)
|
||
const drives: string[] = [];
|
||
for (let i = 65; i <= 90; i++) {
|
||
const driveLetter = String.fromCharCode(i);
|
||
try {
|
||
await fsProm.access(`${driveLetter}:\\`);
|
||
drives.push(`${driveLetter}:`);
|
||
} catch {
|
||
// 如果驱动器不存在或无法访问,跳过
|
||
continue;
|
||
}
|
||
}
|
||
return drives.length > 0 ? drives : ['C:'];
|
||
};
|
||
|
||
// 规范化路径并进行安全验证
|
||
const normalizePath = (inputPath: string): string => {
|
||
if (!inputPath) {
|
||
// 对于空路径,Windows返回用户主目录,其他系统返回根目录
|
||
return isWindows ? process.env['USERPROFILE'] || 'C:\\' : '/';
|
||
}
|
||
|
||
// 对输入路径进行清理,移除潜在的危险字符
|
||
// eslint-disable-next-line no-control-regex
|
||
const cleanedPath = inputPath.replace(/[\x01-\x1f\x7f]/g, ''); // 移除控制字符
|
||
|
||
// 如果是Windows且输入为纯盘符(可能带或不带斜杠),统一返回 "X:\"
|
||
if (isWindows && /^[A-Z]:[\\/]*$/i.test(cleanedPath)) {
|
||
return cleanedPath.slice(0, 2) + '\\';
|
||
}
|
||
|
||
// 安全验证:检查是否包含危险的路径遍历模式(在规范化之前)
|
||
if (containsPathTraversal(cleanedPath)) {
|
||
throw new Error('Invalid path: path traversal detected');
|
||
}
|
||
|
||
// 进行路径规范化
|
||
const normalized = path.resolve(cleanedPath);
|
||
|
||
// 再次检查规范化后的路径,确保没有绕过安全检查
|
||
if (containsPathTraversal(normalized)) {
|
||
throw new Error('Invalid path: path traversal detected after normalization');
|
||
}
|
||
|
||
// 额外安全检查:确保规范化后的路径不包含连续的路径分隔符
|
||
const finalPath = normalized.replace(/[\\/]+/g, path.sep);
|
||
|
||
return finalPath;
|
||
};
|
||
|
||
// 检查路径是否包含路径遍历攻击模式
|
||
const containsPathTraversal = (inputPath: string): boolean => {
|
||
// 对输入进行URL解码,防止编码绕过
|
||
let decodedPath = inputPath;
|
||
try {
|
||
decodedPath = decodeURIComponent(inputPath);
|
||
} catch {
|
||
// 如果解码失败,使用原始路径
|
||
}
|
||
|
||
// 将路径统一为正斜杠格式进行检查
|
||
const normalizedForCheck = decodedPath.replace(/\\/g, '/');
|
||
|
||
// 检查危险模式 - 更全面的路径遍历检测
|
||
const dangerousPatterns = [
|
||
/\.\.\//, // ../ 模式
|
||
/\/\.\./, // /.. 模式
|
||
/^\.\./, // 以.. 开头
|
||
/\.\.$/, // 以.. 结尾
|
||
/\.\.\\/, // ..\ 模式(Windows)
|
||
/\\\.\./, // \.. 模式(Windows)
|
||
/%2e%2e/i, // URL编码的..
|
||
/%252e%252e/i, // 双重URL编码的..
|
||
// eslint-disable-next-line no-control-regex
|
||
/\.\.\x00/, // null字节攻击
|
||
/\0/, // null字节
|
||
];
|
||
|
||
return dangerousPatterns.some(pattern => pattern.test(normalizedForCheck));
|
||
};
|
||
|
||
interface FileInfo {
|
||
name: string;
|
||
isDirectory: boolean;
|
||
size: number;
|
||
mtime: Date;
|
||
}
|
||
|
||
// 添加系统文件黑名单
|
||
const SYSTEM_FILES = new Set(['pagefile.sys', 'swapfile.sys', 'hiberfil.sys', 'System Volume Information']);
|
||
|
||
// 检查是否为WebUI配置文件
|
||
const isWebUIConfigFile = (filePath: string): boolean => {
|
||
// 先用字符串快速筛选
|
||
if (!filePath.includes('webui.json')) {
|
||
return false;
|
||
}
|
||
|
||
// 进入更严格的路径判断 - 统一路径分隔符为 /
|
||
const webUIConfigPath = path.resolve(webUiPathWrapper.configPath, 'webui.json').replace(/\\/g, '/');
|
||
const targetPath = path.resolve(filePath).replace(/\\/g, '/');
|
||
|
||
// 统一分隔符后进行路径比较
|
||
return targetPath === webUIConfigPath;
|
||
};
|
||
|
||
// WebUI配置文件脱敏处理
|
||
const sanitizeWebUIConfig = (content: string): string => {
|
||
try {
|
||
const config = JSON.parse(content);
|
||
if (config.token) {
|
||
config.token = '******';
|
||
}
|
||
return JSON.stringify(config, null, 4);
|
||
} catch {
|
||
// 如果解析失败,返回原内容
|
||
return content;
|
||
}
|
||
};
|
||
|
||
// 检查同类型的文件或目录是否存在
|
||
const checkSameTypeExists = async (pathToCheck: string, isDirectory: boolean): Promise<boolean> => {
|
||
try {
|
||
const stat = await fsProm.stat(pathToCheck);
|
||
// 只有当类型相同时才认为是冲突
|
||
return stat.isDirectory() === isDirectory;
|
||
} catch {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// 获取目录内容
|
||
export const ListFilesHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const requestPath = getQueryStringParam(req.query['path']) || (isWindows ? process.env['USERPROFILE'] || 'C:\\' : '/');
|
||
|
||
let normalizedPath: string;
|
||
try {
|
||
normalizedPath = normalizePath(requestPath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
const onlyDirectory = req.query['onlyDirectory'] === 'true';
|
||
|
||
// 如果是根路径且在Windows系统上,返回盘符列表
|
||
if (isWindows && (!requestPath || requestPath === '/' || requestPath === '\\')) {
|
||
const drives = await getRootDirs();
|
||
const driveInfos: FileInfo[] = await Promise.all(
|
||
drives.map(async (drive) => {
|
||
try {
|
||
const stat = await fsProm.stat(`${drive}\\`);
|
||
return {
|
||
name: drive,
|
||
isDirectory: true,
|
||
size: 0,
|
||
mtime: stat.mtime,
|
||
};
|
||
} catch {
|
||
return {
|
||
name: drive,
|
||
isDirectory: true,
|
||
size: 0,
|
||
mtime: new Date(),
|
||
};
|
||
}
|
||
})
|
||
);
|
||
return sendSuccess(res, driveInfos);
|
||
}
|
||
|
||
const files = await fsProm.readdir(normalizedPath);
|
||
let fileInfos: FileInfo[] = [];
|
||
|
||
for (const file of files) {
|
||
// 跳过系统文件
|
||
if (SYSTEM_FILES.has(file)) continue;
|
||
|
||
try {
|
||
const fullPath = path.join(normalizedPath, file);
|
||
const stat = await fsProm.stat(fullPath);
|
||
fileInfos.push({
|
||
name: file,
|
||
isDirectory: stat.isDirectory(),
|
||
size: stat.size,
|
||
mtime: stat.mtime,
|
||
});
|
||
} catch (_error) {
|
||
// 忽略无法访问的文件
|
||
// console.warn(`无法访问文件 ${file}:`, error);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// 如果请求参数 onlyDirectory 为 true,则只返回目录信息
|
||
if (onlyDirectory) {
|
||
fileInfos = fileInfos.filter((info) => info.isDirectory);
|
||
}
|
||
|
||
return sendSuccess(res, fileInfos);
|
||
} catch (_error) {
|
||
console.error('读取目录失败:', _error);
|
||
return sendError(res, '读取目录失败');
|
||
}
|
||
};
|
||
|
||
// 创建目录
|
||
export const CreateDirHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const { path: dirPath } = req.body;
|
||
|
||
let normalizedPath: string;
|
||
try {
|
||
normalizedPath = normalizePath(dirPath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(normalizedPath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
// 检查是否已存在同类型(目录)
|
||
if (await checkSameTypeExists(normalizedPath, true)) {
|
||
return sendError(res, '同名目录已存在');
|
||
}
|
||
|
||
await fsProm.mkdir(normalizedPath, { recursive: true });
|
||
return sendSuccess(res, true);
|
||
} catch (_error) {
|
||
return sendError(res, '创建目录失败');
|
||
}
|
||
};
|
||
|
||
// 删除文件/目录
|
||
export const DeleteHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const { path: targetPath } = req.body;
|
||
|
||
let normalizedPath: string;
|
||
try {
|
||
normalizedPath = normalizePath(targetPath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(normalizedPath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
const stat = await fsProm.stat(normalizedPath);
|
||
if (stat.isDirectory()) {
|
||
await fsProm.rm(normalizedPath, { recursive: true });
|
||
} else {
|
||
await fsProm.unlink(normalizedPath);
|
||
}
|
||
return sendSuccess(res, true);
|
||
} catch (_error) {
|
||
return sendError(res, '删除失败');
|
||
}
|
||
};
|
||
|
||
// 批量删除文件/目录
|
||
export const BatchDeleteHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const { paths } = req.body;
|
||
for (const targetPath of paths) {
|
||
let normalizedPath: string;
|
||
try {
|
||
normalizedPath = normalizePath(targetPath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(normalizedPath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
const stat = await fsProm.stat(normalizedPath);
|
||
if (stat.isDirectory()) {
|
||
await fsProm.rm(normalizedPath, { recursive: true });
|
||
} else {
|
||
await fsProm.unlink(normalizedPath);
|
||
}
|
||
}
|
||
return sendSuccess(res, true);
|
||
} catch (_error) {
|
||
return sendError(res, '批量删除失败');
|
||
}
|
||
};
|
||
|
||
// 读取文件内容
|
||
export const ReadFileHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
let filePath: string;
|
||
try {
|
||
filePath = normalizePath(getQueryStringParam(req.query['path']));
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(filePath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
let content = await fsProm.readFile(filePath, 'utf-8');
|
||
|
||
// 如果是WebUI配置文件,对token进行脱敏处理
|
||
if (isWebUIConfigFile(filePath)) {
|
||
content = sanitizeWebUIConfig(content);
|
||
}
|
||
|
||
return sendSuccess(res, content);
|
||
} catch (_error) {
|
||
return sendError(res, '读取文件失败');
|
||
}
|
||
};
|
||
|
||
// 写入文件内容
|
||
export const WriteFileHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const { path: filePath, content } = req.body;
|
||
|
||
// 安全的路径规范化,如果检测到路径遍历攻击会抛出异常
|
||
let normalizedPath: string;
|
||
try {
|
||
normalizedPath = normalizePath(filePath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(normalizedPath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
let finalContent = content;
|
||
|
||
// 检查是否为WebUI配置文件
|
||
if (isWebUIConfigFile(normalizedPath)) {
|
||
try {
|
||
// 解析要写入的配置
|
||
const configToWrite = JSON.parse(content);
|
||
// 获取内存中的token,覆盖前端传来的token
|
||
const memoryToken = getInitialWebUiToken();
|
||
if (memoryToken) {
|
||
configToWrite.token = memoryToken;
|
||
finalContent = JSON.stringify(configToWrite, null, 4);
|
||
}
|
||
} catch (_e) {
|
||
// 如果解析失败 说明不符合json格式 不允许写入
|
||
return sendError(res, '写入的WebUI配置文件内容格式错误,必须是合法的JSON');
|
||
}
|
||
}
|
||
|
||
await fsProm.writeFile(normalizedPath, finalContent, 'utf-8');
|
||
return sendSuccess(res, true);
|
||
} catch (_error) {
|
||
return sendError(res, '写入文件失败');
|
||
}
|
||
};
|
||
|
||
// 创建新文件
|
||
export const CreateFileHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const { path: filePath } = req.body;
|
||
|
||
let normalizedPath: string;
|
||
try {
|
||
normalizedPath = normalizePath(filePath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(normalizedPath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
// 检查是否已存在同类型(文件)
|
||
if (await checkSameTypeExists(normalizedPath, false)) {
|
||
return sendError(res, '同名文件已存在');
|
||
}
|
||
|
||
await fsProm.writeFile(normalizedPath, '', 'utf-8');
|
||
return sendSuccess(res, true);
|
||
} catch (_error) {
|
||
return sendError(res, '创建文件失败');
|
||
}
|
||
};
|
||
|
||
// 重命名文件/目录
|
||
export const RenameHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const { oldPath, newPath } = req.body;
|
||
|
||
let normalizedOldPath: string;
|
||
let normalizedNewPath: string;
|
||
try {
|
||
normalizedOldPath = normalizePath(oldPath);
|
||
normalizedNewPath = normalizePath(newPath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(normalizedOldPath) || !path.isAbsolute(normalizedNewPath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
await fsProm.rename(normalizedOldPath, normalizedNewPath);
|
||
return sendSuccess(res, true);
|
||
} catch (_error) {
|
||
return sendError(res, '重命名失败');
|
||
}
|
||
};
|
||
|
||
// 移动文件/目录
|
||
export const MoveHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const { sourcePath, targetPath } = req.body;
|
||
|
||
let normalizedSourcePath: string;
|
||
let normalizedTargetPath: string;
|
||
try {
|
||
normalizedSourcePath = normalizePath(sourcePath);
|
||
normalizedTargetPath = normalizePath(targetPath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(normalizedSourcePath) || !path.isAbsolute(normalizedTargetPath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
await fsProm.rename(normalizedSourcePath, normalizedTargetPath);
|
||
return sendSuccess(res, true);
|
||
} catch (_error) {
|
||
return sendError(res, '移动失败');
|
||
}
|
||
};
|
||
|
||
// 批量移动
|
||
export const BatchMoveHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const { items } = req.body;
|
||
for (const { sourcePath, targetPath } of items) {
|
||
let normalizedSourcePath: string;
|
||
let normalizedTargetPath: string;
|
||
try {
|
||
normalizedSourcePath = normalizePath(sourcePath);
|
||
normalizedTargetPath = normalizePath(targetPath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(normalizedSourcePath) || !path.isAbsolute(normalizedTargetPath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
await fsProm.rename(normalizedSourcePath, normalizedTargetPath);
|
||
}
|
||
return sendSuccess(res, true);
|
||
} catch (_error) {
|
||
return sendError(res, '批量移动失败');
|
||
}
|
||
};
|
||
|
||
// 新增:文件下载处理方法(注意流式传输,不将整个文件读入内存)
|
||
export const DownloadHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
let filePath: string;
|
||
try {
|
||
filePath = normalizePath(getQueryStringParam(req.query['path']));
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
if (!filePath) {
|
||
return sendError(res, '参数错误');
|
||
}
|
||
|
||
// 额外安全检查:确保路径是绝对路径
|
||
if (!path.isAbsolute(filePath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
const stat = await fsProm.stat(filePath);
|
||
|
||
res.setHeader('Content-Type', 'application/octet-stream');
|
||
let filename = path.basename(filePath);
|
||
if (stat.isDirectory()) {
|
||
filename = path.basename(filePath) + '.zip';
|
||
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
|
||
const zipStream = new PassThrough();
|
||
compressing.zip.compressDir(filePath, zipStream as unknown as fs.WriteStream).catch((err) => {
|
||
console.error('压缩目录失败:', err);
|
||
res.end();
|
||
});
|
||
zipStream.pipe(res);
|
||
return;
|
||
}
|
||
res.setHeader('Content-Length', stat.size);
|
||
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
|
||
const stream = fs.createReadStream(filePath);
|
||
stream.pipe(res);
|
||
} catch (_error) {
|
||
return sendError(res, '下载失败');
|
||
}
|
||
};
|
||
|
||
// 批量下载:将多个文件/目录打包为 zip 文件下载
|
||
export const BatchDownloadHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
const { paths } = req.body as { paths: string[]; };
|
||
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
||
return sendError(res, '参数错误');
|
||
}
|
||
res.setHeader('Content-Type', 'application/octet-stream');
|
||
res.setHeader('Content-Disposition', 'attachment; filename=files.zip');
|
||
|
||
const zipStream = new compressing.zip.Stream();
|
||
// 修改:根据文件类型设置 relativePath
|
||
for (const filePath of paths) {
|
||
let normalizedPath: string;
|
||
try {
|
||
normalizedPath = normalizePath(filePath);
|
||
} catch (_pathError) {
|
||
return sendError(res, '无效的文件路径');
|
||
}
|
||
|
||
// 额外安全检查:确保规范化后的路径是绝对路径
|
||
if (!path.isAbsolute(normalizedPath)) {
|
||
return sendError(res, '路径必须是绝对路径');
|
||
}
|
||
|
||
const stat = await fsProm.stat(normalizedPath);
|
||
if (stat.isDirectory()) {
|
||
zipStream.addEntry(normalizedPath, { relativePath: '' });
|
||
} else {
|
||
// 确保相对路径只使用文件名,防止路径遍历
|
||
const safeName = path.basename(normalizedPath);
|
||
zipStream.addEntry(normalizedPath, { relativePath: safeName });
|
||
}
|
||
}
|
||
zipStream.pipe(res);
|
||
res.on('finish', () => {
|
||
zipStream.destroy();
|
||
});
|
||
} catch (_error) {
|
||
return sendError(res, '下载失败');
|
||
}
|
||
};
|
||
|
||
// 修改上传处理方法
|
||
export const UploadHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
await diskUploader(req, res);
|
||
return sendSuccess(res, true, '文件上传成功', true);
|
||
} catch (_error) {
|
||
let errorMessage = '文件上传失败';
|
||
|
||
if (_error instanceof multer.MulterError) {
|
||
switch (_error.code) {
|
||
case 'LIMIT_FILE_SIZE':
|
||
errorMessage = '文件大小超过限制(40MB)';
|
||
break;
|
||
case 'LIMIT_UNEXPECTED_FILE':
|
||
errorMessage = '无效的文件上传字段';
|
||
break;
|
||
default:
|
||
errorMessage = `上传错误: ${_error.message}`;
|
||
}
|
||
} else if (_error instanceof Error) {
|
||
errorMessage = _error.message;
|
||
}
|
||
return sendError(res, errorMessage, true);
|
||
}
|
||
};
|
||
|
||
// 上传WebUI字体文件处理方法
|
||
export const UploadWebUIFontHandler: RequestHandler = async (req, res) => {
|
||
try {
|
||
await webUIFontUploader(req, res);
|
||
return sendSuccess(res, true, '字体文件上传成功', true);
|
||
} catch (error) {
|
||
let errorMessage = '字体文件上传失败';
|
||
|
||
if (error instanceof multer.MulterError) {
|
||
switch (error.code) {
|
||
case 'LIMIT_FILE_SIZE':
|
||
errorMessage = '字体文件大小超过限制(40MB)';
|
||
break;
|
||
case 'LIMIT_UNEXPECTED_FILE':
|
||
errorMessage = '无效的文件上传字段';
|
||
break;
|
||
default:
|
||
errorMessage = `上传错误: ${error.message}`;
|
||
}
|
||
} else if (error instanceof Error) {
|
||
errorMessage = error.message;
|
||
}
|
||
return sendError(res, errorMessage, true);
|
||
}
|
||
};
|
||
|
||
// 删除WebUI字体文件处理方法
|
||
export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => {
|
||
try {
|
||
const fontPath = await WebUiConfig.GetWebUIFontPath();
|
||
const exists = await WebUiConfig.CheckWebUIFontExist();
|
||
|
||
if (!exists || !fontPath) {
|
||
return sendSuccess(res, true);
|
||
}
|
||
|
||
await fsProm.unlink(fontPath);
|
||
return sendSuccess(res, true);
|
||
} catch (_error) {
|
||
return sendError(res, '删除字体文件失败');
|
||
}
|
||
};
|
||
|
||
// 检查WebUI字体文件是否存在
|
||
export const CheckWebUIFontExistHandler: RequestHandler = async (_req, res) => {
|
||
try {
|
||
const exists = await WebUiConfig.CheckWebUIFontExist();
|
||
return sendSuccess(res, exists);
|
||
} catch (_error) {
|
||
return sendError(res, '检查字体文件失败');
|
||
}
|
||
};
|