252 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			252 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { useFileStore } from '@kevisual/use-config/file-store';
 | |
| import http from 'http';
 | |
| import fs, { rm } from 'fs';
 | |
| import path from 'path';
 | |
| import { IncomingForm } from 'formidable';
 | |
| import { app, minioClient } from '@/app.ts';
 | |
| 
 | |
| 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, error, checkAuth, clients, writeEvents } 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"
 | |
| // curl -X POST http://localhost:4000/api/upload \
 | |
| //   -F "file=@readme.md" \
 | |
| //   -F "file=@types/index.d.ts" \
 | |
| //   -F "description=This is a test upload" \
 | |
| //   -F "username=testuser"
 | |
| 
 | |
| router.get('/api/app/upload', async (req, res) => {
 | |
|   res.writeHead(200, { 'Content-Type': 'text/plain' });
 | |
|   res.end('Upload API is ready');
 | |
| });
 | |
| router.post('/api/upload', async (req, res) => {
 | |
|   if (res.headersSent) return; // 如果响应已发送,不再处理
 | |
|   res.writeHead(200, { 'Content-Type': 'application/json' });
 | |
|   const { tokenUser } = await checkAuth(req, res);
 | |
|   if (!tokenUser) return;
 | |
|   // 使用 formidable 解析 multipart/form-data
 | |
|   const form = new IncomingForm({
 | |
|     multiples: true, // 支持多文件上传
 | |
|     uploadDir: filePath, // 上传文件存储目录
 | |
|     allowEmptyFiles: true, // 允许空文件
 | |
|   });
 | |
|   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)}%`,
 | |
|     };
 | |
|     writeEvents(req, data);
 | |
|   });
 | |
| 
 | |
|   // 解析上传的文件
 | |
|   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 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 = `${tokenUser.username}/${relativePath}`;
 | |
|       // 上传到 MinIO 并保留文件夹结构
 | |
|       const isHTML = relativePath.endsWith('.html');
 | |
|       await minioClient.fPutObject(bucketName, minioPath, tempPath, {
 | |
|         'Content-Type': getContentType(relativePath),
 | |
|         'app-source': 'user-files',
 | |
|         'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
 | |
|       });
 | |
|       uploadResults.push({
 | |
|         name: relativePath,
 | |
|         path: minioPath,
 | |
|       });
 | |
|       fs.unlinkSync(tempPath); // 删除临时文件
 | |
|     }
 | |
|     res.end(JSON.stringify({ code: 200, data: uploadResults }));
 | |
|   });
 | |
| });
 | |
| router.post('/api/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', // 文件哈希算法
 | |
|   });
 | |
|   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)}%`,
 | |
|     };
 | |
|     writeEvents(req, data);
 | |
|   });
 | |
|   // 解析上传的文件
 | |
|   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,
 | |
|       username = '';
 | |
|     const { appKey: _appKey, version: _version, username: _username } = fields;
 | |
|     if (Array.isArray(_appKey)) {
 | |
|       appKey = _appKey?.[0];
 | |
|     } else {
 | |
|       appKey = _appKey;
 | |
|     }
 | |
|     if (Array.isArray(_version)) {
 | |
|       version = _version?.[0];
 | |
|     } else {
 | |
|       version = _version;
 | |
|     }
 | |
|     if (Array.isArray(_username)) {
 | |
|       username = _username?.[0];
 | |
|     } else if (_username) {
 | |
|       username = _username;
 | |
|     }
 | |
|     if (username) {
 | |
|       const user = await User.getUserByToken(token);
 | |
|       const has = await user.hasUser(username, true);
 | |
|       if (!has) {
 | |
|         res.end(error('username is not found'));
 | |
|         clearFiles();
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
|     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 = `${username || tokenUser.username}/${appKey}/${version}/${relativePath}`;
 | |
|       // 上传到 MinIO 并保留文件夹结构
 | |
|       const isHTML = relativePath.endsWith('.html');
 | |
|       await minioClient.fPutObject(bucketName, minioPath, tempPath, {
 | |
|         'Content-Type': getContentType(relativePath),
 | |
|         'app-source': 'user-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: 'app',
 | |
|       key: 'uploadFiles',
 | |
|       payload: {
 | |
|         token: token,
 | |
|         data: {
 | |
|           appKey,
 | |
|           version,
 | |
|           username,
 | |
|           files: uploadResults,
 | |
|         },
 | |
|       },
 | |
|     });
 | |
|     const data: any = {
 | |
|       code: r.code,
 | |
|       data: r.body,
 | |
|     };
 | |
|     if (r.message) {
 | |
|       data.message = r.message;
 | |
|     }
 | |
|     res.end(JSON.stringify(data));
 | |
|   });
 | |
| });
 | |
| 
 | |
| router.get('/api/container/file/:id', async (req, res) => {
 | |
|   const id = req.params.id;
 | |
|   if (!id) {
 | |
|     res.end(error('id is required'));
 | |
|     return;
 | |
|   }
 | |
|   const container = await getContainerById(id);
 | |
|   if (container.id) {
 | |
|     const code = container.code;
 | |
|     res.writeHead(200, {
 | |
|       'Content-Type': 'application/javascript; charset=utf-8',
 | |
|       'container-id': container.id,
 | |
|     });
 | |
|     res.end(code);
 | |
|   } else {
 | |
|     res.end(error('Container not found'));
 | |
|   }
 | |
| 
 | |
|   res.writeHead(200, {
 | |
|     'Content-Type': 'application/json',
 | |
|   });
 | |
|   res.end(JSON.stringify(container));
 | |
| });
 | |
| 
 | |
| router.get('/api/code/version', async (req, res) => {
 | |
|   const version = VERSION;
 | |
|   res.writeHead(200, {
 | |
|     'Content-Type': 'application/json',
 | |
|   });
 | |
|   res.end(JSON.stringify({ code: 200, data: { version } }));
 | |
| });
 | |
| 
 | |
| export const uploadMiddleware = async (req: http.IncomingMessage, res: http.ServerResponse) => {
 | |
|   if (req.url?.startsWith('/api/router')) {
 | |
|     return;
 | |
|   }
 | |
|   return router.parse(req, res);
 | |
| };
 |