From d71a57461379d9c2881b584eaa05e456f6bd4774 Mon Sep 17 00:00:00 2001 From: xion Date: Thu, 21 Nov 2024 23:42:07 +0800 Subject: [PATCH] feat: add routes-simple --- src/routes-simple/code/upload.ts | 124 ++++++++++++++++++++++++++ src/routes-simple/index.ts | 1 + src/routes-simple/middleware/index.ts | 1 + src/routes-simple/router.ts | 2 + src/routes-simple/upload.ts | 18 ++-- 5 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 src/routes-simple/code/upload.ts create mode 100644 src/routes-simple/index.ts create mode 100644 src/routes-simple/middleware/index.ts create mode 100644 src/routes-simple/router.ts diff --git a/src/routes-simple/code/upload.ts b/src/routes-simple/code/upload.ts new file mode 100644 index 0000000..2bbe31c --- /dev/null +++ b/src/routes-simple/code/upload.ts @@ -0,0 +1,124 @@ +import { IncomingForm } from 'formidable'; +import { checkAuth } from '../middleware/auth.ts'; +import { router } from '../router.ts'; +import { error } from '../middleware/auth.ts'; +import fs from 'fs'; +import { clients } from '../upload.ts'; +import { useFileStore } from '@abearxiong/use-file-store'; +import { app, minioClient } from '@/app.ts'; +import { bucketName } from '@/modules/minio.ts'; +import { getContentType } from '@/utils/get-content-type.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; + // + // 使用 formidable 解析 multipart/form-data + const form = new IncomingForm({ + multiples: true, // 支持多文件上传 + uploadDir: cacheFilePath, // 上传文件存储目录 + allowEmptyFiles: true, // 允许空 + minFileSize: 0, // 最小文件大小 + createDirsFromUploads: false, // 根据上传的文件夹结构创建目录 + keepExtensions: true, // 保留文件 + hashAlgorithm: 'md5', // 文件哈希算法 + }); + const taskId = req.headers['task-id'] as string; + form.on('progress', (bytesReceived, bytesExpected) => { + const progress = (bytesReceived / bytesExpected) * 100; + console.log(`Upload progress: ${progress.toFixed(2)}%`); + const data = { + progress: progress.toFixed(2), + message: `Upload progress: ${progress.toFixed(2)}%`, + }; + clients.get(taskId)?.client?.write?.(`${JSON.stringify(data)}\n`); + }); + // 解析上传的文件 + form.parse(req, async (err, fields, files) => { + if (err) { + res.end(error(`Upload error: ${err.message}`)); + const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file]; + uploadedFiles.forEach((file) => { + fs.unlinkSync(file.filepath); + }); + return; + } + const clearFiles = () => { + const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file]; + uploadedFiles.forEach((file) => { + fs.unlinkSync(file.filepath); + }); + }; + let appKey, version; + const { appKey: _appKey, version: _version } = fields; + if (Array.isArray(_appKey)) { + appKey = _appKey?.[0]; + } else { + appKey = _appKey; + } + if (Array.isArray(_version)) { + version = _version?.[0]; + } else { + version = _version; + } + appKey = appKey || 'micro-app'; + // if (!appKey) { + // res.end(error('appKey is required')); + // clearFiles(); + // return; + // } + // if (!version) { + // res.end(error('version is required')); + // clearFiles(); + // return; + // } + console.log('Appkey', appKey, version); + + // 逐个处理每个上传的文件 + const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file]; + const uploadResults = []; + for (let i = 0; i < uploadedFiles.length; i++) { + const file = uploadedFiles[i]; + // @ts-ignore + 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, + }); + fs.unlinkSync(tempPath); // 删除临时文件 + } + // 受控 + const r = await app.call({ + path: 'micro-app', + key: 'publish', + payload: { + token: token, + data: { + appKey, + files: uploadResults, + }, + }, + }); + const data: any = { + code: r.code, + data: r.body, + }; + if (r.message) { + data.message = r.message; + } + res.end(JSON.stringify(data)); + }); +}); diff --git a/src/routes-simple/index.ts b/src/routes-simple/index.ts new file mode 100644 index 0000000..d742761 --- /dev/null +++ b/src/routes-simple/index.ts @@ -0,0 +1 @@ +import './code/upload.ts'; diff --git a/src/routes-simple/middleware/index.ts b/src/routes-simple/middleware/index.ts new file mode 100644 index 0000000..2d17511 --- /dev/null +++ b/src/routes-simple/middleware/index.ts @@ -0,0 +1 @@ +export * from './auth.ts' \ No newline at end of file diff --git a/src/routes-simple/router.ts b/src/routes-simple/router.ts new file mode 100644 index 0000000..180e91e --- /dev/null +++ b/src/routes-simple/router.ts @@ -0,0 +1,2 @@ +import { SimpleRouter } from '@kevisual/router/simple'; +export const router = new SimpleRouter(); diff --git a/src/routes-simple/upload.ts b/src/routes-simple/upload.ts index c6314f7..3fad932 100644 --- a/src/routes-simple/upload.ts +++ b/src/routes-simple/upload.ts @@ -4,11 +4,14 @@ import fs, { rm } from 'fs'; import path from 'path'; import { IncomingForm } from 'formidable'; import { app, minioClient } from '@/app.ts'; -import { SimpleRouter } from '@kevisual/router/simple'; + import { bucketName } from '@/modules/minio.ts'; import { getContentType } from '@/utils/get-content-type.ts'; import { User } from '@/models/user.ts'; import { getContainerById } from '@/routes/container/module/get-container-file.ts'; +import { router } from './router.ts'; +import './index.ts'; + const filePath = useFileStore('upload', { needExists: true }); const cacheFilePath = useFileStore('cache-file', { needExists: true }); // curl -X POST http://localhost:4000/api/upload -F "file=@readme.md" @@ -18,9 +21,7 @@ const cacheFilePath = useFileStore('cache-file', { needExists: true }); // -F "description=This is a test upload" \ // -F "username=testuser" -let clients = []; - -const router = new SimpleRouter(); +export const clients = new Map(); const error = (msg: string, code = 500) => { return JSON.stringify({ code, message: msg }); @@ -214,11 +215,14 @@ router.get('/api/events', async (req, res) => { 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); - clients.push(res); - + const tokenUser = await checkAuth(req, res); + if (!tokenUser) return; + const taskId = req.headers['task-id'] as string; + // 将客户端连接推送到 clients 数组 + clients.set(taskId, { client: res, tokenUser }); // 移除客户端连接 req.on('close', () => { - clients = clients.filter((client) => client !== res); + clients.delete(taskId); }); } });