171 lines
5.0 KiB
TypeScript
171 lines
5.0 KiB
TypeScript
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<void> | 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<void>((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;
|
|
}
|
|
}
|