diff --git a/src/routes-simple/index.ts b/src/routes-simple/index.ts index d742761..ce1b11c 100644 --- a/src/routes-simple/index.ts +++ b/src/routes-simple/index.ts @@ -1 +1,2 @@ import './code/upload.ts'; +import './resources/upload.ts'; diff --git a/src/routes-simple/resources/upload.ts b/src/routes-simple/resources/upload.ts new file mode 100644 index 0000000..1307d31 --- /dev/null +++ b/src/routes-simple/resources/upload.ts @@ -0,0 +1,125 @@ +import { useFileStore } from '@kevisual/use-config/file-store'; +import { checkAuth, error, router, writeEvents, getKey } from '../router.ts'; +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 fs from 'fs'; +import { ConfigModel } from '@/routes/config/models/model.ts'; + +const cacheFilePath = useFileStore('cache-file', { needExists: true }); + +router.get('/api/s1/resources/upload', async (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Upload API is ready'); +}); + +// /api/s1/resources/upload +router.post('/api/s1/resources/upload', async (req, res) => { + 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; + const data = { + progress: progress.toFixed(2), + message: `Upload progress: ${progress.toFixed(2)}%`, + }; + writeEvents(req, data); + }); + // 解析上传的文件 + form.parse(req, async (err, fields, files) => { + const clearFiles = () => { + const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file]; + uploadedFiles.forEach((file) => { + fs.unlinkSync(file.filepath); + }); + }; + if (err) { + res.end(error(`Upload error: ${err.message}`)); + clearFiles(); + return; + } + let { appKey, version, username } = getKey(fields, ['appKey', 'version', 'username']); + let uid = tokenUser.id; + 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; + } + const _user = await User.findOne({ where: { username } }); + uid = _user?.id || ''; + } + if (!appKey || !version) { + const config = await ConfigModel.getUploadConfig({ uid }); + if (config) { + appKey = config.config?.data?.key || ''; + version = config.config?.data?.version || ''; + } + } + if (!appKey || !version) { + res.end(error('appKey or version is not found, please check the upload config.')); + clearFiles(); + 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 = `${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)); + }); +}); diff --git a/src/routes-simple/router.ts b/src/routes-simple/router.ts index 7c5d637..7540b1b 100644 --- a/src/routes-simple/router.ts +++ b/src/routes-simple/router.ts @@ -2,6 +2,7 @@ import { router } from '@/app.ts'; import http from 'http'; import { useContextKey } from '@kevisual/use-config/context'; import { checkAuth, error } from './middleware/auth.ts'; +import formidable from 'formidable'; export { router, checkAuth, error }; /** @@ -29,3 +30,22 @@ export const writeEvents = (req: http.IncomingMessage, data: any) => { const taskId = getTaskId(req); taskId && clients.get(taskId)?.client?.write?.(`${JSON.stringify(data)}\n`); }; + +/** + * 解析表单数据, 如果表单数据是数组, 则取第一个,appKey, version, username 等 + * @param fields 表单数据 + * @param parseKeys 需要解析的键 + * @returns 解析后的数据 + */ +export const getKey = (fields: formidable.Fields, parseKeys: string[]) => { + let value: Record = {}; + for (const key of parseKeys) { + const v = fields[key]; + if (Array.isArray(v)) { + value[key] = v[0]; + } else { + value[key] = v; + } + } + return value; +}; diff --git a/src/routes/app-manager/list.ts b/src/routes/app-manager/list.ts index 0f1b60a..57a250d 100644 --- a/src/routes/app-manager/list.ts +++ b/src/routes/app-manager/list.ts @@ -167,21 +167,23 @@ app } } let am = await AppModel.findOne({ where: { key: appKey, uid } }); + let appIsNew = false; if (!am) { + appIsNew = true; am = await AppModel.create({ user: userPrefix, key: appKey, uid, - version: '0.0.0', + version: version || '0.0.0', title: appKey, + proxy: true, data: { - files: [], + files: files || [], }, }); } let app = await AppListModel.findOne({ where: { version: version, key: appKey, uid: uid } }); if (!app) { - // throw new CustomError('app not found'); app = await AppListModel.create({ key: appKey, version, @@ -194,6 +196,9 @@ app const dataFiles = app.data.files || []; const newFiles = _.uniqBy([...dataFiles, ...files], 'name'); const res = await app.update({ data: { ...app.data, files: newFiles } }); + if (version === am.version && !appIsNew) { + await am.update({ data: { ...am.data, files: newFiles } }); + } setExpire(app.id, 'test'); ctx.body = prefixFix(res, userPrefix); } catch (e) {