mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 14:41:14 +00:00
feat(webui): 增加上传文件大小限制配置并优化上传处理
This commit is contained in:
parent
890736d3c7
commit
d2e44acba1
@ -1,6 +1,7 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { existsSync, createReadStream, mkdirSync, rmSync, cpSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { join, normalize } from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { webUiPathWrapper } from '@/napcat-webui-backend/index';
|
||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||||
@ -60,16 +61,8 @@ export const BackupExportConfigHandler: RequestHandler = async (_req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 导入配置
|
||||
// 将上传的zip文件解压到工作目录下的tmp目录,然后覆盖到config文件夹
|
||||
// 导入配置,将上传的zip文件解压到工作目录下的tmp目录,然后覆盖到config文件夹
|
||||
export const BackupImportConfigHandler: RequestHandler = async (req, res) => {
|
||||
// 获取QQ登录状态
|
||||
const isLogin = WebUiDataRuntime.getQQLoginStatus();
|
||||
// 如果未登录,返回错误
|
||||
if (!isLogin) {
|
||||
return sendError(res, 'Not Login');
|
||||
}
|
||||
|
||||
// 检查是否有文件上传
|
||||
if (!req.file) {
|
||||
return sendError(res, '请选择要导入的配置文件');
|
||||
@ -77,8 +70,8 @@ export const BackupImportConfigHandler: RequestHandler = async (req, res) => {
|
||||
|
||||
try {
|
||||
const configPath = webUiPathWrapper.configPath;
|
||||
const tmpPath = join(webUiPathWrapper.cachePath, './tmp');
|
||||
const backupRootPath = join(webUiPathWrapper.cachePath, './backup');
|
||||
const tmpPath = join(os.tmpdir(), 'napcat-upload', 'tmp');
|
||||
const backupRootPath = join(os.tmpdir(), 'napcat-upload', 'backup');
|
||||
let extractPath = join(tmpPath, 'imported_config');
|
||||
const uploadedFilePath = req.file.path;
|
||||
|
||||
@ -129,6 +122,14 @@ export const BackupImportConfigHandler: RequestHandler = async (req, res) => {
|
||||
for (const entry of entries) {
|
||||
const srcPath = join(src, entry.name);
|
||||
const destPath = join(dest, entry.name);
|
||||
|
||||
// 防止路径穿越攻击
|
||||
const normalizedDestPath = normalize(destPath);
|
||||
const normalizedDestDir = normalize(dest);
|
||||
if (!normalizedDestPath.startsWith(normalizedDestDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
mkdirSync(destPath, { recursive: true });
|
||||
copyRecursive(srcPath, destPath);
|
||||
@ -146,6 +147,14 @@ export const BackupImportConfigHandler: RequestHandler = async (req, res) => {
|
||||
for (const entry of entries) {
|
||||
const srcPath = join(src, entry.name);
|
||||
const destPath = join(dest, entry.name);
|
||||
|
||||
// 防止路径穿越攻击
|
||||
const normalizedDestPath = normalize(destPath);
|
||||
const normalizedDestDir = normalize(dest);
|
||||
if (!normalizedDestPath.startsWith(normalizedDestDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
mkdirSync(destPath, { recursive: true });
|
||||
copyRecursive(srcPath, destPath);
|
||||
@ -156,16 +165,13 @@ export const BackupImportConfigHandler: RequestHandler = async (req, res) => {
|
||||
};
|
||||
copyRecursive(extractPath, configPath);
|
||||
|
||||
// 清理临时文件
|
||||
rmSync(join(tmpPath, 'imported_config'), { recursive: true, force: true });
|
||||
rmSync(uploadedFilePath, { force: true });
|
||||
|
||||
return sendSuccess(res, {
|
||||
message: '配置导入成功,重启后生效~',
|
||||
backupPath: backupPath
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('导入配置失败:', error);
|
||||
const msg = (error as Error).message;
|
||||
return sendError(res, `导入配置失败: ${msg}`);
|
||||
}
|
||||
|
||||
@ -32,6 +32,8 @@ const WebUiConfigSchema = Type.Object({
|
||||
ipBlacklist: Type.Array(Type.String(), { default: [] }),
|
||||
// 是否启用 X-Forwarded-For 获取真实IP
|
||||
enableXForwardedFor: Type.Boolean({ default: false }),
|
||||
// 上传文件大小限制(MB)
|
||||
uploadSizeLimit: Type.Number({ default: 50 }),
|
||||
});
|
||||
|
||||
export type WebUiConfigType = Static<typeof WebUiConfigSchema>;
|
||||
|
||||
@ -1,17 +1,35 @@
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { join } from 'node:path';
|
||||
import { webUiPathWrapper } from '@/napcat-webui-backend/index';
|
||||
import os from 'node:os';
|
||||
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
||||
|
||||
import { OB11GetConfigHandler, OB11SetConfigHandler } from '@/napcat-webui-backend/src/api/OB11Config';
|
||||
import { BackupExportConfigHandler, BackupImportConfigHandler } from '@/napcat-webui-backend/src/api/BackupConfig';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
// 延迟初始化multer配置,避免在webUiPathWrapper初始化前访问
|
||||
// 延迟初始化multer配置
|
||||
const getUpload = () => {
|
||||
const tmpPath = join(webUiPathWrapper.cachePath, './tmp');
|
||||
return multer({ dest: tmpPath });
|
||||
// 使用系统临时目录作为基础路径,方便多个napcat用户统一读取使用
|
||||
const tmpPath = join(os.tmpdir(), 'napcat-upload');
|
||||
// 获取上传大小限制,默认50MB,最大200MB
|
||||
let uploadSizeLimit = 50;
|
||||
try {
|
||||
// 使用同步方式获取配置
|
||||
uploadSizeLimit = WebUiConfig?.WebUiConfigData?.uploadSizeLimit || 50;
|
||||
} catch (error) {
|
||||
// 如果获取失败,使用默认值
|
||||
console.warn('获取上传大小限制失败:', error);
|
||||
}
|
||||
// 确保不超过最大限制
|
||||
uploadSizeLimit = Math.min(uploadSizeLimit, 200);
|
||||
return multer({
|
||||
dest: tmpPath,
|
||||
limits: {
|
||||
fileSize: uploadSizeLimit * 1024 * 1024 // 转换为字节
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// router:读取配置
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import toast from 'react-hot-toast';
|
||||
import { LuDownload, LuUpload } from 'react-icons/lu';
|
||||
|
||||
import key from '@/const/key';
|
||||
import { requestServerWithFetch } from '@/utils/request';
|
||||
|
||||
// 导入配置
|
||||
const handleImportConfig = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@ -19,15 +18,8 @@ const handleImportConfig = async (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
const formData = new FormData();
|
||||
formData.append('configFile', file);
|
||||
|
||||
const token = localStorage.getItem(key.token);
|
||||
const headers: HeadersInit = {};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${JSON.parse(token)}`;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/OB11Config/ImportConfig', {
|
||||
const response = await requestServerWithFetch('/OB11Config/ImportConfig', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
|
||||
@ -37,7 +29,12 @@ const handleImportConfig = async (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
toast.success(result.data?.message || '配置导入成功。');
|
||||
// 检查是否成功导入
|
||||
if (result.code === 0) {
|
||||
toast.success(result.data?.message || '配置导入成功。');
|
||||
} else {
|
||||
toast.error(`配置导入失败: ${result.data?.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
@ -51,16 +48,8 @@ const handleImportConfig = async (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
// 导出配置
|
||||
const handleExportConfig = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem(key.token);
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${JSON.parse(token)}`;
|
||||
}
|
||||
const response = await fetch('/api/OB11Config/ExportConfig', {
|
||||
const response = await requestServerWithFetch('/OB11Config/ExportConfig', {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user