import Busboy from 'busboy'; import { checkAuth } from '../middleware/auth.ts'; import { router, clients, writeEvents } from '../router.ts'; import { error } from '../middleware/auth.ts'; import fs from 'fs'; import { useFileStore } from '@kevisual/use-config/file-store'; import { app, minioClient } from '@/app.ts'; import { bucketName } from '@/modules/minio.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 }); router.post('/api/micro-app/upload', async (req, res) => { if (res.headersSent) return; // 如果响应已发送,不再处理 res.writeHead(200, { 'Content-Type': 'application/json' }); const { tokenUser, token } = await checkAuth(req, res); if (!tokenUser) return; // 使用 busboy 解析 multipart/form-data const busboy = Busboy({ headers: req.headers, preservePath: true }); const fields: any = {}; let file: any = null; let filePromise: Promise | null = null; let bytesReceived = 0; let bytesExpected = parseInt(req.headers['content-length'] || '0'); busboy.on('field', (fieldname, value) => { fields[fieldname] = value; }); busboy.on('file', (fieldname, fileStream, info) => { const { filename, encoding, mimeType } = info; const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`); const writeStream = createWriteStream(tempPath); const hash = crypto.createHash('md5'); let size = 0; filePromise = new Promise((resolve, reject) => { fileStream.on('data', (chunk) => { bytesReceived += chunk.length; size += chunk.length; hash.update(chunk); if (bytesExpected > 0) { const progress = (bytesReceived / bytesExpected) * 100; console.log(`Upload progress: ${progress.toFixed(2)}%`); const data = { progress: progress.toFixed(2), message: `Upload progress: ${progress.toFixed(2)}%`, }; writeEvents(req, data); } }); fileStream.pipe(writeStream); writeStream.on('finish', () => { file = { filepath: tempPath, 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; } } const clearFiles = () => { if (file?.filepath && fs.existsSync(file.filepath)) { fs.unlinkSync(file.filepath); } }; if (!file) { res.end(error('No file uploaded')); return; } let appKey, collection; const { appKey: _appKey, collection: _collecion } = fields; if (Array.isArray(_appKey)) { appKey = _appKey?.[0]; } else { appKey = _appKey; } if (Array.isArray(_collecion)) { collection = _collecion?.[0]; } else { collection = _collecion; } collection = parseIfJson(collection); appKey = appKey || 'micro-app'; console.log('Appkey', appKey); console.log('collection', collection); // 处理上传的文件 const uploadResults = []; const tempPath = file.filepath; // 文件上传时的临时路径 const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构) // 比如 child2/b.txt const minioPath = `private/${tokenUser.username}/${appKey}/${relativePath}`; // 上传到 MinIO 并保留文件夹结构 const isHTML = relativePath.endsWith('.html'); await minioClient.fPutObject(bucketName, minioPath, tempPath, { 'Content-Type': getContentType(relativePath), 'app-source': 'user-micro-app', 'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年 }); uploadResults.push({ name: relativePath, path: minioPath, hash: file.hash, size: file.size, }); fs.unlinkSync(tempPath); // 删除临时文件 // 受控 const r = await app.call({ path: 'micro-app', key: 'upload', payload: { token: token, data: { appKey, collection, files: uploadResults, }, }, }); const data: any = { code: r.code, data: r.body, }; if (r.message) { data.message = r.message; } res.end(JSON.stringify(data)); }); pipeBusboy(req, res, busboy); }); function parseIfJson(collection: any): any { try { return JSON.parse(collection); } catch (e) { return collection; } }