更新依赖项,使用 Busboy 替代 formidable 处理文件上传,优化上传逻辑,改进权限检查
This commit is contained in:
@@ -69,13 +69,12 @@
|
|||||||
"@kevisual/logger": "^0.0.4",
|
"@kevisual/logger": "^0.0.4",
|
||||||
"@kevisual/oss": "0.0.13",
|
"@kevisual/oss": "0.0.13",
|
||||||
"@kevisual/permission": "^0.0.3",
|
"@kevisual/permission": "^0.0.3",
|
||||||
"@kevisual/router": "0.0.46",
|
"@kevisual/router": "0.0.48",
|
||||||
"@kevisual/types": "^0.0.10",
|
"@kevisual/types": "^0.0.10",
|
||||||
"@kevisual/use-config": "^1.0.21",
|
"@kevisual/use-config": "^1.0.21",
|
||||||
"@types/archiver": "^7.0.0",
|
"@types/archiver": "^7.0.0",
|
||||||
"@types/bun": "^1.3.5",
|
"@types/bun": "^1.3.5",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/formidable": "^3.4.6",
|
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
@@ -84,7 +83,6 @@
|
|||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"formidable": "3.5.4",
|
|
||||||
"ioredis": "^5.8.2",
|
"ioredis": "^5.8.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lodash-es": "^4.17.22",
|
"lodash-es": "^4.17.22",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -86,8 +86,8 @@ importers:
|
|||||||
specifier: ^0.0.3
|
specifier: ^0.0.3
|
||||||
version: 0.0.3
|
version: 0.0.3
|
||||||
'@kevisual/router':
|
'@kevisual/router':
|
||||||
specifier: 0.0.46
|
specifier: 0.0.48
|
||||||
version: 0.0.46
|
version: 0.0.48
|
||||||
'@kevisual/types':
|
'@kevisual/types':
|
||||||
specifier: ^0.0.10
|
specifier: ^0.0.10
|
||||||
version: 0.0.10
|
version: 0.0.10
|
||||||
@@ -255,8 +255,8 @@ packages:
|
|||||||
'@kevisual/router@0.0.33':
|
'@kevisual/router@0.0.33':
|
||||||
resolution: {integrity: sha512-9z7TkSzCIGbXn9SuHPBdZpGwHlAuwA8iN5jNAZBUvbEvBRkBxlrbdCSe9fBYiAHueLm2AceFNrW74uulOiAkqA==}
|
resolution: {integrity: sha512-9z7TkSzCIGbXn9SuHPBdZpGwHlAuwA8iN5jNAZBUvbEvBRkBxlrbdCSe9fBYiAHueLm2AceFNrW74uulOiAkqA==}
|
||||||
|
|
||||||
'@kevisual/router@0.0.46':
|
'@kevisual/router@0.0.48':
|
||||||
resolution: {integrity: sha512-KMsyVTQxCZdt35yRdeDJDIwXco6w7xSG3C90NlKMkrsj5OCZlIEJaRSs2ASIb3kYgrWFDl8NTUNbObDO03Q7bA==}
|
resolution: {integrity: sha512-WsSvT+NpfC/bZbaAzE3WSKD2DRZP0JuPQJGr4YucSdO/lOLB4cEpOZRbPlV3l7G064ow8QJRAN2DUW+bRjrp1A==}
|
||||||
|
|
||||||
'@kevisual/types@0.0.10':
|
'@kevisual/types@0.0.10':
|
||||||
resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==}
|
resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==}
|
||||||
@@ -2154,7 +2154,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@kevisual/router@0.0.46':
|
'@kevisual/router@0.0.48':
|
||||||
dependencies:
|
dependencies:
|
||||||
path-to-regexp: 8.3.0
|
path-to-regexp: 8.3.0
|
||||||
selfsigned: 5.2.0
|
selfsigned: 5.2.0
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { App } from '@kevisual/router/src/app.ts';
|
import { App } from '@kevisual/router';
|
||||||
import * as redisLib from './modules/redis.ts';
|
import * as redisLib from './modules/redis.ts';
|
||||||
import * as minioLib from './modules/minio.ts';
|
import * as minioLib from './modules/minio.ts';
|
||||||
import * as sequelizeLib from './modules/sequelize.ts';
|
import * as sequelizeLib from './modules/sequelize.ts';
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import './route.ts';
|
|||||||
import { handleRequest } from './routes-simple/handle-request.ts';
|
import { handleRequest } from './routes-simple/handle-request.ts';
|
||||||
import { port } from './modules/config.ts';
|
import { port } from './modules/config.ts';
|
||||||
import { wssFun } from './modules/ws-proxy/index.ts';
|
import { wssFun } from './modules/ws-proxy/index.ts';
|
||||||
|
import { WebSocketListenerFun, HttpListenerFun } from '@kevisual/router/src/server/server-type.js';
|
||||||
console.log('Starting server...', port);
|
console.log('Starting server...', port);
|
||||||
app.listen(port, '0.0.0.0', () => {
|
app.listen(port, '0.0.0.0', () => {
|
||||||
console.log(`server is running at http://localhost:${port}`);
|
console.log(`server is running at http://localhost:${port}`);
|
||||||
});
|
});
|
||||||
app.server.on([{
|
app.server.on([{
|
||||||
id: 'handle-all',
|
id: 'handle-all',
|
||||||
fun: handleRequest,
|
func: handleRequest as any,
|
||||||
}, {
|
}, {
|
||||||
id: 'wss',
|
id: 'wss',
|
||||||
io: true,
|
io: true,
|
||||||
path: '/ws/proxy',
|
path: '/ws/proxy',
|
||||||
fun: wssFun,
|
func: wssFun as WebSocketListenerFun,
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ export * from './get-content-type.ts'
|
|||||||
export * from './utils.ts'
|
export * from './utils.ts'
|
||||||
|
|
||||||
export { pipeFileStream, pipeStream } from './pipe.ts'
|
export { pipeFileStream, pipeStream } from './pipe.ts'
|
||||||
|
|
||||||
|
export { pipeBusboy } from './pipe-busboy.ts'
|
||||||
13
src/modules/fm-manager/pipe-busboy.ts
Normal file
13
src/modules/fm-manager/pipe-busboy.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { isBun } from '@/utils/get-engine.ts';
|
||||||
|
import http from 'node:http';
|
||||||
|
export const pipeBusboy = async (req: http.IncomingMessage, res: http.ServerResponse, busboy: any) => {
|
||||||
|
if (isBun) {
|
||||||
|
// @ts-ignore
|
||||||
|
const bunRequest = req.bun.request;
|
||||||
|
const arrayBuffer = await bunRequest.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
busboy.end(buffer);
|
||||||
|
} else {
|
||||||
|
req.pipe(busboy);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,26 +11,36 @@ type ProxyOptions = {
|
|||||||
export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opts?: ProxyOptions) => {
|
export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opts?: ProxyOptions) => {
|
||||||
const { url } = req;
|
const { url } = req;
|
||||||
const { pathname } = new URL(url || '', `http://localhost`);
|
const { pathname } = new URL(url || '', `http://localhost`);
|
||||||
const [user, app, userAppKey] = pathname.split('/').slice(1);
|
let [user, app, userAppKey] = pathname.split('/').slice(1);
|
||||||
if (!user || !app || !userAppKey) {
|
if (!user || !app || !userAppKey) {
|
||||||
opts?.createNotFoundPage?.('应用未找到');
|
opts?.createNotFoundPage?.('应用未找到');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await App.handleRequest(req, res);
|
const data = await App.handleRequest(req, res);
|
||||||
const loginUser = await getLoginUser(req);
|
const loginUser = await getLoginUser(req);
|
||||||
if (!loginUser) {
|
if (!loginUser) {
|
||||||
opts?.createNotFoundPage?.('没有登录');
|
opts?.createNotFoundPage?.('没有登录');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (loginUser.tokenUser?.username !== user) {
|
const isAdmin = loginUser.tokenUser?.username === user
|
||||||
|
// TODO: 如果不是管理员,是否需要添加其他人可以访问的逻辑?
|
||||||
|
if (!isAdmin) {
|
||||||
opts?.createNotFoundPage?.('没有访问应用权限');
|
opts?.createNotFoundPage?.('没有访问应用权限');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!userAppKey.startsWith(user + '-')) {
|
||||||
|
userAppKey = user + '-' + userAppKey;
|
||||||
|
}
|
||||||
logger.debug('data', data);
|
logger.debug('data', data);
|
||||||
const client = wsProxyManager.get(userAppKey);
|
const client = wsProxyManager.get(userAppKey);
|
||||||
const ids = wsProxyManager.getIds();
|
const ids = wsProxyManager.getIds();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
if (isAdmin) {
|
||||||
opts?.createNotFoundPage?.(`未找到应用 [${userAppKey}], 当前应用列表: ${ids.join(',')}`);
|
opts?.createNotFoundPage?.(`未找到应用 [${userAppKey}], 当前应用列表: ${ids.join(',')}`);
|
||||||
|
} else {
|
||||||
|
opts?.createNotFoundPage?.('应用访问失败');
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const value = await client.sendData(data);
|
const value = await client.sendData(data);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IncomingForm } from 'formidable';
|
import Busboy from 'busboy';
|
||||||
import { checkAuth } from '../middleware/auth.ts';
|
import { checkAuth } from '../middleware/auth.ts';
|
||||||
import { router, clients, writeEvents } from '../router.ts';
|
import { router, clients, writeEvents } from '../router.ts';
|
||||||
import { error } from '../middleware/auth.ts';
|
import { error } from '../middleware/auth.ts';
|
||||||
@@ -7,26 +7,43 @@ import { useFileStore } from '@kevisual/use-config/file-store';
|
|||||||
import { app, minioClient } from '@/app.ts';
|
import { app, minioClient } from '@/app.ts';
|
||||||
import { bucketName } from '@/modules/minio.ts';
|
import { bucketName } from '@/modules/minio.ts';
|
||||||
import { getContentType } from '@/utils/get-content-type.ts';
|
import { getContentType } from '@/utils/get-content-type.ts';
|
||||||
|
import path from 'path';
|
||||||
|
import { createWriteStream } from 'fs';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { pipeBusboy } from '@/modules/fm-manager/index.ts';
|
||||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
||||||
|
|
||||||
router.post('/api/micro-app/upload', async (req, res) => {
|
router.post('/api/micro-app/upload', async (req, res) => {
|
||||||
if (res.headersSent) return; // 如果响应已发送,不再处理
|
if (res.headersSent) return; // 如果响应已发送,不再处理
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
const { tokenUser, token } = await checkAuth(req, res);
|
const { tokenUser, token } = await checkAuth(req, res);
|
||||||
if (!tokenUser) return;
|
if (!tokenUser) return;
|
||||||
//
|
|
||||||
// 使用 formidable 解析 multipart/form-data
|
// 使用 busboy 解析 multipart/form-data
|
||||||
const form = new IncomingForm({
|
const busboy = Busboy({ headers: req.headers });
|
||||||
multiples: false, // 支持多文件上传
|
const fields: any = {};
|
||||||
uploadDir: cacheFilePath, // 上传文件存储目录
|
let file: any = null;
|
||||||
allowEmptyFiles: true, // 允许空
|
let filePromise: Promise<void> | null = null;
|
||||||
minFileSize: 0, // 最小文件大小
|
let bytesReceived = 0;
|
||||||
maxFiles: 1, // 最大文件数量
|
let bytesExpected = parseInt(req.headers['content-length'] || '0');
|
||||||
createDirsFromUploads: false, // 根据上传的文件夹结构创建目录
|
|
||||||
keepExtensions: true, // 保留文件
|
busboy.on('field', (fieldname, value) => {
|
||||||
hashAlgorithm: 'md5', // 文件哈希算法
|
fields[fieldname] = value;
|
||||||
});
|
});
|
||||||
form.on('progress', (bytesReceived, bytesExpected) => {
|
|
||||||
|
busboy.on('file', (fieldname, fileStream, info) => {
|
||||||
|
const { filename, encoding, mimeType } = info;
|
||||||
|
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}-${filename}`);
|
||||||
|
const writeStream = createWriteStream(tempPath);
|
||||||
|
const hash = crypto.createHash('md5');
|
||||||
|
let size = 0;
|
||||||
|
|
||||||
|
filePromise = new Promise<void>((resolve, reject) => {
|
||||||
|
fileStream.on('data', (chunk) => {
|
||||||
|
bytesReceived += chunk.length;
|
||||||
|
size += chunk.length;
|
||||||
|
hash.update(chunk);
|
||||||
|
if (bytesExpected > 0) {
|
||||||
const progress = (bytesReceived / bytesExpected) * 100;
|
const progress = (bytesReceived / bytesExpected) * 100;
|
||||||
console.log(`Upload progress: ${progress.toFixed(2)}%`);
|
console.log(`Upload progress: ${progress.toFixed(2)}%`);
|
||||||
const data = {
|
const data = {
|
||||||
@@ -34,23 +51,50 @@ router.post('/api/micro-app/upload', async (req, res) => {
|
|||||||
message: `Upload progress: ${progress.toFixed(2)}%`,
|
message: `Upload progress: ${progress.toFixed(2)}%`,
|
||||||
};
|
};
|
||||||
writeEvents(req, data);
|
writeEvents(req, data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// 解析上传的文件
|
|
||||||
form.parse(req, async (err, fields, files) => {
|
fileStream.pipe(writeStream);
|
||||||
if (err) {
|
|
||||||
res.end(error(`Upload error: ${err.message}`));
|
writeStream.on('finish', () => {
|
||||||
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
file = {
|
||||||
uploadedFiles.forEach((file) => {
|
filepath: tempPath,
|
||||||
fs.unlinkSync(file.filepath);
|
originalFilename: filename,
|
||||||
|
mimetype: mimeType,
|
||||||
|
hash: hash.digest('hex'),
|
||||||
|
size: size,
|
||||||
|
};
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
writeStream.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on('finish', async () => {
|
||||||
|
// 等待文件写入完成
|
||||||
|
if (filePromise) {
|
||||||
|
try {
|
||||||
|
await filePromise;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`File write error: ${err.message}`);
|
||||||
|
res.end(error(`File write error: ${err.message}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const clearFiles = () => {
|
const clearFiles = () => {
|
||||||
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
if (file?.filepath && fs.existsSync(file.filepath)) {
|
||||||
uploadedFiles.forEach((file) => {
|
|
||||||
fs.unlinkSync(file.filepath);
|
fs.unlinkSync(file.filepath);
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
res.end(error('No file uploaded'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let appKey, collection;
|
let appKey, collection;
|
||||||
const { appKey: _appKey, collection: _collecion } = fields;
|
const { appKey: _appKey, collection: _collecion } = fields;
|
||||||
if (Array.isArray(_appKey)) {
|
if (Array.isArray(_appKey)) {
|
||||||
@@ -68,12 +112,9 @@ router.post('/api/micro-app/upload', async (req, res) => {
|
|||||||
appKey = appKey || 'micro-app';
|
appKey = appKey || 'micro-app';
|
||||||
console.log('Appkey', appKey);
|
console.log('Appkey', appKey);
|
||||||
console.log('collection', collection);
|
console.log('collection', collection);
|
||||||
// 逐个处理每个上传的文件
|
|
||||||
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
// 处理上传的文件
|
||||||
const uploadResults = [];
|
const uploadResults = [];
|
||||||
for (let i = 0; i < uploadedFiles.length; i++) {
|
|
||||||
const file = uploadedFiles[i];
|
|
||||||
// @ts-ignore
|
|
||||||
const tempPath = file.filepath; // 文件上传时的临时路径
|
const tempPath = file.filepath; // 文件上传时的临时路径
|
||||||
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
|
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
|
||||||
// 比如 child2/b.txt
|
// 比如 child2/b.txt
|
||||||
@@ -92,7 +133,7 @@ router.post('/api/micro-app/upload', async (req, res) => {
|
|||||||
size: file.size,
|
size: file.size,
|
||||||
});
|
});
|
||||||
fs.unlinkSync(tempPath); // 删除临时文件
|
fs.unlinkSync(tempPath); // 删除临时文件
|
||||||
}
|
|
||||||
// 受控
|
// 受控
|
||||||
const r = await app.call({
|
const r = await app.call({
|
||||||
path: 'micro-app',
|
path: 'micro-app',
|
||||||
@@ -115,6 +156,8 @@ router.post('/api/micro-app/upload', async (req, res) => {
|
|||||||
}
|
}
|
||||||
res.end(JSON.stringify(data));
|
res.end(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pipeBusboy(req, res, busboy);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useFileStore } from '@kevisual/use-config/file-store';
|
import { useFileStore } from '@kevisual/use-config/file-store';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { IncomingForm } from 'formidable';
|
import Busboy from 'busboy';
|
||||||
import { app, minioClient } from '@/app.ts';
|
import { app, minioClient } from '@/app.ts';
|
||||||
|
|
||||||
import { bucketName } from '@/modules/minio.ts';
|
import { bucketName } from '@/modules/minio.ts';
|
||||||
@@ -11,6 +11,9 @@ import { getContainerById } from '@/routes/container/module/get-container-file.t
|
|||||||
import { router, error, checkAuth, writeEvents } from './router.ts';
|
import { router, error, checkAuth, writeEvents } from './router.ts';
|
||||||
import './index.ts';
|
import './index.ts';
|
||||||
import { handleRequest as PageProxy } from './page-proxy.ts';
|
import { handleRequest as PageProxy } from './page-proxy.ts';
|
||||||
|
import path from 'path';
|
||||||
|
import { createWriteStream } from 'fs';
|
||||||
|
import { pipeBusboy } from '@/modules/fm-manager/pipe-busboy.ts';
|
||||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
||||||
|
|
||||||
router.get('/api/app/upload', async (req, res) => {
|
router.get('/api/app/upload', async (req, res) => {
|
||||||
@@ -23,17 +26,28 @@ router.post('/api/app/upload', async (req, res) => {
|
|||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
const { tokenUser, token } = await checkAuth(req, res);
|
const { tokenUser, token } = await checkAuth(req, res);
|
||||||
if (!tokenUser) return;
|
if (!tokenUser) return;
|
||||||
// 使用 formidable 解析 multipart/form-data
|
|
||||||
const form = new IncomingForm({
|
// 使用 busboy 解析 multipart/form-data
|
||||||
multiples: true, // 支持多文件上传
|
const busboy = Busboy({ headers: req.headers });
|
||||||
uploadDir: cacheFilePath, // 上传文件存储目录
|
const fields: any = {};
|
||||||
allowEmptyFiles: true, // 允许空
|
const files: any = [];
|
||||||
minFileSize: 0, // 最小文件大小
|
const filePromises: Promise<void>[] = [];
|
||||||
createDirsFromUploads: false, // 根据上传的文件夹结构创建目录
|
let bytesReceived = 0;
|
||||||
keepExtensions: true, // 保留文件
|
let bytesExpected = parseInt(req.headers['content-length'] || '0');
|
||||||
hashAlgorithm: 'md5', // 文件哈希算法
|
|
||||||
|
busboy.on('field', (fieldname, value) => {
|
||||||
|
fields[fieldname] = value;
|
||||||
});
|
});
|
||||||
form.on('progress', (bytesReceived, bytesExpected) => {
|
|
||||||
|
busboy.on('file', (fieldname, fileStream, info) => {
|
||||||
|
const { filename, encoding, mimeType } = info;
|
||||||
|
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}-${filename}`);
|
||||||
|
const writeStream = createWriteStream(tempPath);
|
||||||
|
|
||||||
|
const filePromise = new Promise<void>((resolve, reject) => {
|
||||||
|
fileStream.on('data', (chunk) => {
|
||||||
|
bytesReceived += chunk.length;
|
||||||
|
if (bytesExpected > 0) {
|
||||||
const progress = (bytesReceived / bytesExpected) * 100;
|
const progress = (bytesReceived / bytesExpected) * 100;
|
||||||
console.log(`Upload progress: ${progress.toFixed(2)}%`);
|
console.log(`Upload progress: ${progress.toFixed(2)}%`);
|
||||||
const data = {
|
const data = {
|
||||||
@@ -41,23 +55,51 @@ router.post('/api/app/upload', async (req, res) => {
|
|||||||
message: `Upload progress: ${progress.toFixed(2)}%`,
|
message: `Upload progress: ${progress.toFixed(2)}%`,
|
||||||
};
|
};
|
||||||
writeEvents(req, data);
|
writeEvents(req, data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// 解析上传的文件
|
|
||||||
form.parse(req, async (err, fields, files) => {
|
fileStream.pipe(writeStream);
|
||||||
if (err) {
|
|
||||||
res.end(error(`Upload error: ${err.message}`));
|
writeStream.on('finish', () => {
|
||||||
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
files.push({
|
||||||
uploadedFiles.forEach((file) => {
|
filepath: tempPath,
|
||||||
fs.unlinkSync(file.filepath);
|
originalFilename: filename,
|
||||||
|
mimetype: mimeType,
|
||||||
});
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
filePromises.push(filePromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on('finish', async () => {
|
||||||
|
// 等待所有文件写入完成
|
||||||
|
try {
|
||||||
|
await Promise.all(filePromises);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`File write error: ${err.message}`);
|
||||||
|
res.end(error(`File write error: ${err.message}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const clearFiles = () => {
|
const clearFiles = () => {
|
||||||
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
files.forEach((file: any) => {
|
||||||
uploadedFiles.forEach((file) => {
|
if (file?.filepath && fs.existsSync(file.filepath)) {
|
||||||
fs.unlinkSync(file.filepath);
|
fs.unlinkSync(file.filepath);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 检查是否有文件上传
|
||||||
|
if (files.length === 0) {
|
||||||
|
res.end(error('files is required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let appKey,
|
let appKey,
|
||||||
version,
|
version,
|
||||||
username = '';
|
username = '';
|
||||||
@@ -99,11 +141,9 @@ router.post('/api/app/upload', async (req, res) => {
|
|||||||
console.log('Appkey', appKey, version);
|
console.log('Appkey', appKey, version);
|
||||||
|
|
||||||
// 逐个处理每个上传的文件
|
// 逐个处理每个上传的文件
|
||||||
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
|
||||||
const uploadResults = [];
|
const uploadResults = [];
|
||||||
for (let i = 0; i < uploadedFiles.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = uploadedFiles[i];
|
const file = files[i];
|
||||||
// @ts-ignore
|
|
||||||
const tempPath = file.filepath; // 文件上传时的临时路径
|
const tempPath = file.filepath; // 文件上传时的临时路径
|
||||||
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
|
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
|
||||||
// 比如 child2/b.txt
|
// 比如 child2/b.txt
|
||||||
@@ -144,6 +184,8 @@ router.post('/api/app/upload', async (req, res) => {
|
|||||||
}
|
}
|
||||||
res.end(JSON.stringify(data));
|
res.end(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pipeBusboy(req, res, busboy);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/api/container/file/:id', async (req, res) => {
|
router.get('/api/container/file/:id', async (req, res) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useFileStore } from '@kevisual/use-config/file-store';
|
import { useFileStore } from '@kevisual/use-config/file-store';
|
||||||
import { checkAuth, error, router, writeEvents, getKey, getTaskId } from '../router.ts';
|
import { checkAuth, error, router, writeEvents, getKey, getTaskId } from '../router.ts';
|
||||||
import { IncomingForm } from 'formidable';
|
import Busboy from 'busboy';
|
||||||
import { app, oss } from '@/app.ts';
|
import { app, oss } from '@/app.ts';
|
||||||
|
|
||||||
import { getContentType } from '@/utils/get-content-type.ts';
|
import { getContentType } from '@/utils/get-content-type.ts';
|
||||||
@@ -8,6 +8,9 @@ import { User } from '@/models/user.ts';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { ConfigModel } from '@/routes/config/models/model.ts';
|
import { ConfigModel } from '@/routes/config/models/model.ts';
|
||||||
import { validateDirectory } from './util.ts';
|
import { validateDirectory } from './util.ts';
|
||||||
|
import path from 'path';
|
||||||
|
import { createWriteStream } from 'fs';
|
||||||
|
import { pipeBusboy } from '@/modules/fm-manager/index.ts';
|
||||||
|
|
||||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
||||||
|
|
||||||
@@ -23,35 +26,70 @@ router.post('/api/s1/resources/upload/chunk', async (req, res) => {
|
|||||||
const url = new URL(req.url || '', 'http://localhost');
|
const url = new URL(req.url || '', 'http://localhost');
|
||||||
const share = !!url.searchParams.get('public');
|
const share = !!url.searchParams.get('public');
|
||||||
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
|
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
|
||||||
// 使用 formidable 解析 multipart/form-data
|
|
||||||
const form = new IncomingForm({
|
|
||||||
multiples: false, // 改为单文件上传
|
|
||||||
uploadDir: cacheFilePath, // 上传文件存储目录
|
|
||||||
allowEmptyFiles: true, // 允许空
|
|
||||||
minFileSize: 0, // 最小文件大小
|
|
||||||
createDirsFromUploads: false, // 根据上传的文件夹结构创建目录
|
|
||||||
keepExtensions: true, // 保留文件拓展名
|
|
||||||
hashAlgorithm: 'md5', // 文件哈希算法
|
|
||||||
});
|
|
||||||
const taskId = getTaskId(req);
|
const taskId = getTaskId(req);
|
||||||
const finalFilePath = `${cacheFilePath}/${taskId}`;
|
const finalFilePath = `${cacheFilePath}/${taskId}`;
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
res.end(error('taskId is required'));
|
res.end(error('taskId is required'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 解析上传的文件
|
|
||||||
form.parse(req, async (err, fields, files) => {
|
// 使用 busboy 解析 multipart/form-data
|
||||||
const file = Array.isArray(files.file) ? files.file[0] : files.file;
|
const busboy = Busboy({ headers: req.headers });
|
||||||
|
const fields: any = {};
|
||||||
|
let file: any = null;
|
||||||
|
let tempPath = '';
|
||||||
|
let filePromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
busboy.on('field', (fieldname, value) => {
|
||||||
|
fields[fieldname] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on('file', (fieldname, fileStream, info) => {
|
||||||
|
const { filename, encoding, mimeType } = info;
|
||||||
|
tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}-${filename}`);
|
||||||
|
const writeStream = createWriteStream(tempPath);
|
||||||
|
|
||||||
|
filePromise = new Promise<void>((resolve, reject) => {
|
||||||
|
fileStream.pipe(writeStream);
|
||||||
|
|
||||||
|
writeStream.on('finish', () => {
|
||||||
|
file = {
|
||||||
|
filepath: tempPath,
|
||||||
|
originalFilename: filename,
|
||||||
|
mimetype: mimeType,
|
||||||
|
};
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on('finish', async () => {
|
||||||
|
// 等待文件写入完成
|
||||||
|
if (filePromise) {
|
||||||
|
try {
|
||||||
|
await filePromise;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`File write error: ${err.message}`);
|
||||||
|
res.end(error(`File write error: ${err.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const clearFiles = () => {
|
const clearFiles = () => {
|
||||||
if (file) {
|
if (tempPath && fs.existsSync(tempPath)) {
|
||||||
fs.unlinkSync(file.filepath);
|
fs.unlinkSync(tempPath);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(finalFilePath)) {
|
||||||
fs.unlinkSync(finalFilePath);
|
fs.unlinkSync(finalFilePath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (err) {
|
if (!file) {
|
||||||
res.end(error(`Upload error: ${err.message}`));
|
res.end(error('No file uploaded'));
|
||||||
clearFiles();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +107,7 @@ router.post('/api/s1/resources/upload/chunk', async (req, res) => {
|
|||||||
clearFiles();
|
clearFiles();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tempPath = file.filepath;
|
|
||||||
const relativePath = file.originalFilename;
|
const relativePath = file.originalFilename;
|
||||||
// Append chunk to the final file
|
|
||||||
|
|
||||||
const writeStream = fs.createWriteStream(finalFilePath, { flags: 'a' });
|
const writeStream = fs.createWriteStream(finalFilePath, { flags: 'a' });
|
||||||
const readStream = fs.createReadStream(tempPath);
|
const readStream = fs.createReadStream(tempPath);
|
||||||
@@ -195,4 +231,6 @@ router.post('/api/s1/resources/upload/chunk', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pipeBusboy(req, res, busboy);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { useFileStore } from '@kevisual/use-config/file-store';
|
import { useFileStore } from '@kevisual/use-config/file-store';
|
||||||
import { checkAuth, error, router, writeEvents, getKey } from '../router.ts';
|
import { checkAuth, error, router, writeEvents, getKey } from '../router.ts';
|
||||||
import { IncomingForm } from 'formidable';
|
import Busboy from 'busboy';
|
||||||
import { app, minioClient } from '@/app.ts';
|
import { app, minioClient } from '@/app.ts';
|
||||||
|
|
||||||
import { bucketName } from '@/modules/minio.ts';
|
import { bucketName } from '@/modules/minio.ts';
|
||||||
import { getContentType } from '@/utils/get-content-type.ts';
|
import { getContentType } from '@/utils/get-content-type.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { createWriteStream } from 'fs';
|
||||||
|
import { pipeBusboy } from '@/modules/fm-manager/pipe-busboy.ts';
|
||||||
import { ConfigModel } from '@/routes/config/models/model.ts';
|
import { ConfigModel } from '@/routes/config/models/model.ts';
|
||||||
import { validateDirectory } from './util.ts';
|
import { validateDirectory } from './util.ts';
|
||||||
import { pick } from 'lodash-es';
|
import { pick } from 'lodash-es';
|
||||||
@@ -103,17 +106,27 @@ router.post('/api/s1/resources/upload', async (req, res) => {
|
|||||||
const share = !!url.searchParams.get('public');
|
const share = !!url.searchParams.get('public');
|
||||||
const meta = parseIfJson(url.searchParams.get('meta'));
|
const meta = parseIfJson(url.searchParams.get('meta'));
|
||||||
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
|
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
|
||||||
// 使用 formi dable 解析 multipart/form-data
|
// 使用 busboy 解析 multipart/form-data
|
||||||
const form = new IncomingForm({
|
const busboy = Busboy({ headers: req.headers });
|
||||||
multiples: true, // 支持多文件上传
|
const fields: any = {};
|
||||||
uploadDir: cacheFilePath, // 上传文件存储目录
|
const files: any[] = [];
|
||||||
allowEmptyFiles: true, // 允许空
|
const filePromises: Promise<void>[] = [];
|
||||||
minFileSize: 0, // 最小文件大小
|
let bytesReceived = 0;
|
||||||
createDirsFromUploads: false, // 根据上传的文件夹结构创建目录
|
let bytesExpected = parseInt(req.headers['content-length'] || '0');
|
||||||
keepExtensions: true, // 保留文件拓展名
|
|
||||||
hashAlgorithm: 'md5', // 文件哈希算法
|
busboy.on('field', (fieldname, value) => {
|
||||||
|
fields[fieldname] = value;
|
||||||
});
|
});
|
||||||
form.on('progress', (bytesReceived, bytesExpected) => {
|
|
||||||
|
busboy.on('file', (fieldname, fileStream, info) => {
|
||||||
|
const { filename, encoding, mimeType } = info;
|
||||||
|
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}-${filename}`);
|
||||||
|
const writeStream = createWriteStream(tempPath);
|
||||||
|
|
||||||
|
const filePromise = new Promise<void>((resolve, reject) => {
|
||||||
|
fileStream.on('data', (chunk) => {
|
||||||
|
bytesReceived += chunk.length;
|
||||||
|
if (bytesExpected > 0) {
|
||||||
const progress = (bytesReceived / bytesExpected) * 100;
|
const progress = (bytesReceived / bytesExpected) * 100;
|
||||||
const data = {
|
const data = {
|
||||||
progress: progress.toFixed(2),
|
progress: progress.toFixed(2),
|
||||||
@@ -121,23 +134,51 @@ router.post('/api/s1/resources/upload', async (req, res) => {
|
|||||||
};
|
};
|
||||||
console.log('progress-upload', data);
|
console.log('progress-upload', data);
|
||||||
writeEvents(req, data);
|
writeEvents(req, data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// 解析上传的文件
|
|
||||||
form.parse(req, async (err, fields, files) => {
|
fileStream.pipe(writeStream);
|
||||||
|
|
||||||
|
writeStream.on('finish', () => {
|
||||||
|
files.push({
|
||||||
|
filepath: tempPath,
|
||||||
|
originalFilename: filename,
|
||||||
|
mimetype: mimeType,
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
filePromises.push(filePromise);
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on('finish', async () => {
|
||||||
|
// 等待所有文件写入完成
|
||||||
|
try {
|
||||||
|
await Promise.all(filePromises);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`File write error: ${err.message}`);
|
||||||
|
res.end(error(`File write error: ${err.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const clearFiles = () => {
|
const clearFiles = () => {
|
||||||
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
files.forEach((file) => {
|
||||||
uploadedFiles.forEach((file) => {
|
|
||||||
if (file?.filepath && fs.existsSync(file.filepath)) {
|
if (file?.filepath && fs.existsSync(file.filepath)) {
|
||||||
fs.unlinkSync(file.filepath);
|
fs.unlinkSync(file.filepath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
if (err) {
|
|
||||||
logger.error(`Upload error: ${err.message}`);
|
// 检查是否有文件上传
|
||||||
res.end(error(`Upload error: ${err.message}`));
|
if (files.length === 0) {
|
||||||
clearFiles();
|
res.end(error('files is required'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { appKey, version, username, directory, description } = getKey(fields, ['appKey', 'version', 'username', 'directory', 'description']);
|
let { appKey, version, username, directory, description } = getKey(fields, ['appKey', 'version', 'username', 'directory', 'description']);
|
||||||
let uid = tokenUser.id;
|
let uid = tokenUser.id;
|
||||||
if (username) {
|
if (username) {
|
||||||
@@ -170,7 +211,7 @@ router.post('/api/s1/resources/upload', async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 逐个处理每个上传的文件
|
// 逐个处理每个上传的文件
|
||||||
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
const uploadedFiles = files;
|
||||||
logger.info(
|
logger.info(
|
||||||
'upload files',
|
'upload files',
|
||||||
uploadedFiles.map((item) => {
|
uploadedFiles.map((item) => {
|
||||||
@@ -244,4 +285,6 @@ router.post('/api/s1/resources/upload', async (req, res) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pipeBusboy(req, res, busboy);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { router } from '@/app.ts';
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { checkAuth, error } from './middleware/auth.ts';
|
import { checkAuth, error } from './middleware/auth.ts';
|
||||||
import formidable from 'formidable';
|
|
||||||
export { router, checkAuth, error };
|
export { router, checkAuth, error };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,7 +67,7 @@ export const deleteOldClients = () => {
|
|||||||
* @param parseKeys 需要解析的键
|
* @param parseKeys 需要解析的键
|
||||||
* @returns 解析后的数据
|
* @returns 解析后的数据
|
||||||
*/
|
*/
|
||||||
export const getKey = (fields: formidable.Fields<string>, parseKeys: string[]) => {
|
export const getKey = (fields: Record<string, any>, parseKeys: string[]) => {
|
||||||
let value: Record<string, any> = {};
|
let value: Record<string, any> = {};
|
||||||
for (const key of parseKeys) {
|
for (const key of parseKeys) {
|
||||||
const v = fields[key];
|
const v = fields[key];
|
||||||
|
|||||||
Reference in New Issue
Block a user