diff --git a/package.json b/package.json index 02eedfa..985a4ec 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "dependencies": { "@kevisual/ai": "^0.0.24", "@kevisual/auth": "^2.0.3", - "@kevisual/query": "^0.0.38", + "@kevisual/query": "^0.0.39", "@types/busboy": "^1.5.4", "@types/send": "^1.2.1", "@types/ws": "^8.18.1", @@ -77,16 +77,16 @@ "@kevisual/file-listener": "^0.0.2", "@kevisual/local-app-manager": "0.1.32", "@kevisual/logger": "^0.0.4", - "@kevisual/oss": "0.0.18", - "@kevisual/permission": "^0.0.3", - "@kevisual/router": "0.0.65", + "@kevisual/oss": "0.0.19", + "@kevisual/permission": "^0.0.4", + "@kevisual/router": "0.0.66", "@kevisual/types": "^0.0.12", - "@kevisual/use-config": "^1.0.28", + "@kevisual/use-config": "^1.0.30", "@types/archiver": "^7.0.0", "@types/bun": "^1.3.8", "@types/crypto-js": "^4.2.2", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^25.1.0", + "@types/node": "^25.2.0", "@types/pg": "^8.16.0", "@types/semver": "^7.7.1", "@types/xml2js": "^0.4.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ceb311f..c90aa7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,8 @@ importers: specifier: ^2.0.3 version: 2.0.3 '@kevisual/query': - specifier: ^0.0.38 - version: 0.0.38 + specifier: ^0.0.39 + version: 0.0.39 '@types/busboy': specifier: ^1.5.4 version: 1.5.4 @@ -98,20 +98,20 @@ importers: specifier: ^0.0.4 version: 0.0.4 '@kevisual/oss': - specifier: 0.0.18 - version: 0.0.18 + specifier: 0.0.19 + version: 0.0.19 '@kevisual/permission': - specifier: ^0.0.3 - version: 0.0.3 + specifier: ^0.0.4 + version: 0.0.4 '@kevisual/router': - specifier: 0.0.65 - version: 0.0.65(typescript@5.9.3) + specifier: 0.0.66 + version: 0.0.66(typescript@5.9.3) '@kevisual/types': specifier: ^0.0.12 version: 0.0.12 '@kevisual/use-config': - specifier: ^1.0.28 - version: 1.0.28(dotenv@17.2.3) + specifier: ^1.0.30 + version: 1.0.30(dotenv@17.2.3) '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -125,8 +125,8 @@ importers: specifier: ^9.0.10 version: 9.0.10 '@types/node': - specifier: ^25.1.0 - version: 25.1.0 + specifier: ^25.2.0 + version: 25.2.0 '@types/pg': specifier: ^8.16.0 version: 8.16.0 @@ -876,15 +876,21 @@ packages: '@kevisual/logger@0.0.4': resolution: {integrity: sha512-+fpr92eokSxoGOW1SIRl/27lPuO+zyY+feR5o2Q4YCNlAdt2x64NwC/w8r/3NEC5QenLgd4K0azyKTI2mHbARw==} - '@kevisual/oss@0.0.18': - resolution: {integrity: sha512-vTdXe41inq4oc+bfYIR3xMDm8GZyOAaWq3DBh+Eur9uNOJcIUdgZBVPOm2uSigmjl3PvqekUw8bE/vbWWJAY7w==} + '@kevisual/oss@0.0.19': + resolution: {integrity: sha512-4Y5krJTqLQOsEwJf7K7a/88t9YHm8PQNuZ5SJDTMopYDOflJlwVjvqiu0lapQ0UrpI+wG6FdfmdmnWpXdQsa1Q==} '@kevisual/permission@0.0.3': resolution: {integrity: sha512-8JsA/5O5Ax/z+M+MYpFYdlioHE6jNmWMuFSokBWYs9CCAHNiSKMR01YLkoVDoPvncfH/Y8F5K/IEXRCbptuMNA==} + '@kevisual/permission@0.0.4': + resolution: {integrity: sha512-zwBYPnT/z21W4q2wkklJrxvoYBYWG/+a3iXFDKqXQAnDOcxm/SU1f1N6FQb9KxGKl36/fclVlhxlxqszvKCenQ==} + '@kevisual/query@0.0.38': resolution: {integrity: sha512-bfvbSodsZyMfwY+1T2SvDeOCKsT/AaIxlVe0+B1R/fNhlg2MDq2CP0L9HKiFkEm+OXrvXcYDMKPUituVUM5J6Q==} + '@kevisual/query@0.0.39': + resolution: {integrity: sha512-3UEPBIvtdykNkrby3hvrgrHdgd17Uq+Pnr4zs+JBzATkU2eKaOqtTUJqdyIEwuySCwzGTxrnlUzWP4tziDQDLQ==} + '@kevisual/router@0.0.21': resolution: {integrity: sha512-XKTxbNO924cT18UOAGplWErZ+hMze8Y53F2jYCk18v4jsdsvjRho5uXXjJb6HSVsuITMtQR4R3rG0IcM3jkDKQ==} @@ -897,14 +903,14 @@ packages: '@kevisual/router@0.0.60': resolution: {integrity: sha512-2v/ZzUstsaq+Uqo+tZX9ys5E+/2erPggCtljv9jTb3NA88ZdHsYUAsd5wUFvLtf9QucpJCzyWEt+InDV/98FKw==} - '@kevisual/router@0.0.65': - resolution: {integrity: sha512-UiGqjLWheDbWOhEBBOSggCnafYFz3tCjLZYDp44ahiyeC2APwFRozz7UYbEq7+amH4Ex1wdqk1AlKmuP7w04og==} + '@kevisual/router@0.0.66': + resolution: {integrity: sha512-yoiCfKJ8yxrXToh8ud1+/JFqlRexrZmJ0PhofQX3jyfmmyEBQQJFL+2UYewm4FxbG3l7ndBC/NIhu1v5CdwxiQ==} '@kevisual/types@0.0.12': resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==} - '@kevisual/use-config@1.0.28': - resolution: {integrity: sha512-ngF+LDbjxpXWrZNmnShIKF/jPpAa+ezV+DcgoZIIzHlRnIjE+rr9sLkN/B7WJbiH9C/j1tQXOILY8ujBqILrow==} + '@kevisual/use-config@1.0.30': + resolution: {integrity: sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw==} peerDependencies: dotenv: ^17 @@ -1408,8 +1414,8 @@ packages: '@types/node@25.0.10': resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==} - '@types/node@25.1.0': - resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==} + '@types/node@25.2.0': + resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} '@types/pg@8.16.0': resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} @@ -3872,7 +3878,7 @@ snapshots: dependencies: '@kevisual/auth': 1.0.5 '@kevisual/router': 0.0.21 - '@kevisual/use-config': 1.0.28(dotenv@17.2.3) + '@kevisual/use-config': 1.0.30(dotenv@17.2.3) ioredis: 5.9.2 nanoid: 5.1.6 pg: 8.18.0 @@ -3898,7 +3904,7 @@ snapshots: dependencies: '@kevisual/auth': 1.0.5 '@kevisual/router': 0.0.23 - '@kevisual/use-config': 1.0.28(dotenv@17.2.3) + '@kevisual/use-config': 1.0.30(dotenv@17.2.3) ioredis: 5.9.2 nanoid: 5.1.6 pg: 8.18.0 @@ -3937,7 +3943,7 @@ snapshots: dependencies: '@kevisual/code-center-module': 0.0.20(dotenv@17.2.3) '@kevisual/router': 0.0.22 - '@kevisual/use-config': 1.0.28(dotenv@17.2.3) + '@kevisual/use-config': 1.0.30(dotenv@17.2.3) cookie: 1.1.1 dayjs: 1.11.19 formidable: 3.5.4 @@ -3971,14 +3977,20 @@ snapshots: '@kevisual/logger@0.0.4': {} - '@kevisual/oss@0.0.18': {} + '@kevisual/oss@0.0.19': {} '@kevisual/permission@0.0.3': {} + '@kevisual/permission@0.0.4': {} + '@kevisual/query@0.0.38': dependencies: tslib: 2.8.1 + '@kevisual/query@0.0.39': + dependencies: + tslib: 2.8.1 + '@kevisual/router@0.0.21': dependencies: path-to-regexp: 8.3.0 @@ -4001,7 +4013,7 @@ snapshots: dependencies: hono: 4.11.5 - '@kevisual/router@0.0.65(typescript@5.9.3)': + '@kevisual/router@0.0.66(typescript@5.9.3)': dependencies: '@kevisual/dts': 0.0.3(typescript@5.9.3) hono: 4.11.7 @@ -4010,7 +4022,7 @@ snapshots: '@kevisual/types@0.0.12': {} - '@kevisual/use-config@1.0.28(dotenv@17.2.3)': + '@kevisual/use-config@1.0.30(dotenv@17.2.3)': dependencies: '@kevisual/load': 0.0.6 dotenv: 17.2.3 @@ -4562,13 +4574,13 @@ snapshots: '@types/busboy@1.5.4': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.2.0 '@types/cookie@0.4.1': {} '@types/cors@2.8.17': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.2.0 '@types/crypto-js@4.2.2': {} @@ -4581,31 +4593,31 @@ snapshots: '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 0.7.34 - '@types/node': 25.1.0 + '@types/node': 25.2.0 '@types/ms@0.7.34': {} '@types/node-forge@1.3.11': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.2.0 '@types/node@25.0.10': dependencies: undici-types: 7.16.0 - '@types/node@25.1.0': + '@types/node@25.2.0': dependencies: undici-types: 7.16.0 '@types/pg@8.16.0': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.2.0 pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.2.0 '@types/resolve@1.20.2': {} @@ -4613,13 +4625,13 @@ snapshots: '@types/send@1.2.1': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.2.0 '@types/validator@13.12.2': {} '@types/ws@8.18.1': dependencies: - '@types/node': 25.1.0 + '@types/node': 25.2.0 '@types/xml2js@0.4.14': dependencies: @@ -4789,7 +4801,7 @@ snapshots: bun-types@1.3.8: dependencies: - '@types/node': 25.1.0 + '@types/node': 25.2.0 busboy@1.6.0: dependencies: @@ -4999,7 +5011,7 @@ snapshots: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.17 - '@types/node': 25.1.0 + '@types/node': 25.2.0 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 @@ -6291,7 +6303,7 @@ snapshots: wkx@0.5.0: dependencies: - '@types/node': 25.1.0 + '@types/node': 25.2.0 wrap-ansi@7.0.0: dependencies: diff --git a/src/modules/fm-manager/proxy/ai-proxy.ts b/src/modules/fm-manager/proxy/ai-proxy.ts index fc2e7a8..960bc46 100644 --- a/src/modules/fm-manager/proxy/ai-proxy.ts +++ b/src/modules/fm-manager/proxy/ai-proxy.ts @@ -98,10 +98,19 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy return true; } const stat = await oss.statObject(objectName); - if (!stat) { - createNotFoundPage('Invalid proxy url'); + if (!stat && isOwner) { + // createNotFoundPage('文件不存在'); + res.writeHead(200, { 'content-type': 'application/json' }); + res.end( + JSON.stringify({ + code: 404, + message: 'object not found', + }), + ); logger.debug('no stat', objectName, owner, req.url); return true; + } else if (!stat && !isOwner) { + return createNotFoundPage('Invalid ai proxy url'); } const permissionInstance = new UserPermission({ permission: stat.metaData as Permission, owner: owner }); const checkPermission = permissionInstance.checkPermissionSuccess({ @@ -112,6 +121,7 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy logger.info('no permission', checkPermission, loginUser, owner); return createNotFoundPage('no permission'); } + if (showStat) { res.writeHead(200, { 'content-type': 'application/json' }); res.end( diff --git a/src/routes-simple/code/upload.ts b/src/routes-simple/code/upload.ts deleted file mode 100644 index b3ad6e2..0000000 --- a/src/routes-simple/code/upload.ts +++ /dev/null @@ -1,171 +0,0 @@ -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'; -import { app, oss } from '@/app.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, defCharset: 'utf-8' }); - 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; - // 处理 UTF-8 文件名编码 - const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename; - 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: decodedFilename, - 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 oss.fPutObject(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; - } -} diff --git a/src/routes-simple/event.ts b/src/routes-simple/event.ts deleted file mode 100644 index 12dec63..0000000 --- a/src/routes-simple/event.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { router, error, checkAuth, clients, getTaskId, writeEvents, deleteOldClients } from './router.ts'; - -router.get('/api/events', async (req, res) => { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - }); - const taskId = getTaskId(req); - if (!taskId) { - res.end(error('task-id is required')); - return; - } - // 将客户端连接推送到 clients 数组 - clients.set(taskId, { client: res, createTime: Date.now() }); - // 移除客户端连接 - req.on('close', () => { - clients.delete(taskId); - }); -}); - -router.get('/api/s1/events', async (req, res) => { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - }); - const taskId = getTaskId(req); - if (!taskId) { - res.end(error('task-id is required')); - return; - } - // 将客户端连接推送到 clients 数组 - clients.set(taskId, { client: res, createTime: Date.now() }); - writeEvents(req, { progress: 0, message: 'start' }); - // 不自动关闭连接 - // res.end('ok'); -}); - -router.get('/api/s1/events/close', async (req, res) => { - const taskId = getTaskId(req); - if (!taskId) { - res.end(error('task-id is required')); - return; - } - deleteOldClients(); - clients.delete(taskId); - res.end('ok'); -}); diff --git a/src/routes-simple/handle-request.ts b/src/routes-simple/handle-request.ts index 4e851bb..fbb38fb 100644 --- a/src/routes-simple/handle-request.ts +++ b/src/routes-simple/handle-request.ts @@ -1,20 +1,9 @@ -import { useFileStore } from '@kevisual/use-config'; import http from 'node:http'; -import fs from 'fs'; -import Busboy from 'busboy'; -import { app, oss } from '@/app.ts'; - -import { getContentType } from '@/utils/get-content-type.ts'; -import { User } from '@/models/user.ts'; -import { router, error, checkAuth, writeEvents } from './router.ts'; +import { router } from './router.ts'; import './index.ts'; import { handleRequest as PageProxy } from './page-proxy.ts'; const simpleAppsPrefixs = [ - "/api/micro-app/", - "/api/events", - "/api/s1/", - "/api/resource/", "/api/wxmsg" ]; diff --git a/src/routes-simple/index.ts b/src/routes-simple/index.ts index 29d91ab..e69de29 100644 --- a/src/routes-simple/index.ts +++ b/src/routes-simple/index.ts @@ -1,6 +0,0 @@ -// import './code/upload.ts'; -import './event.ts'; - -import './resources/upload.ts'; -import './resources/chunk.ts'; -// import './resources/get-resources.ts'; diff --git a/src/routes-simple/middleware/auth.ts b/src/routes-simple/middleware/auth.ts deleted file mode 100644 index 32cee75..0000000 --- a/src/routes-simple/middleware/auth.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { User } from '@/models/user.ts'; -import http from 'http'; -import { parse } from '@kevisual/router/src/server/cookie.ts'; -export const error = (msg: string, code = 500) => { - return JSON.stringify({ code, message: msg }); -}; -export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse) => { - let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || ''; - const url = new URL(req.url || '', 'http://localhost'); - const resNoPermission = () => { - res.statusCode = 401; - res.end(error('Invalid authorization')); - return { tokenUser: null, token: null }; - }; - if (!token) { - token = url.searchParams.get('token') || ''; - } - if (!token) { - const parsedCookies = parse(req.headers.cookie || ''); - token = parsedCookies.token || ''; - } - if (!token) { - return resNoPermission(); - } - if (token) { - token = token.replace('Bearer ', ''); - } - let tokenUser; - try { - tokenUser = await User.verifyToken(token); - } catch (e) { - console.log('checkAuth error', e); - res.statusCode = 401; - res.end(error('Invalid token')); - return { tokenUser: null, token: null }; - } - return { tokenUser, token }; -}; - -export const getLoginUser = async (req: http.IncomingMessage) => { - let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || ''; - const url = new URL(req.url || '', 'http://localhost'); - if (!token) { - token = url.searchParams.get('token') || ''; - } - if (!token) { - const parsedCookies = parse(req.headers.cookie || ''); - token = parsedCookies.token || ''; - } - - if (token) { - token = token.replace('Bearer ', ''); - } - let tokenUser; - try { - tokenUser = await User.verifyToken(token); - return { tokenUser, token }; - } catch (e) { - return null; - } -}; diff --git a/src/routes-simple/middleware/index.ts b/src/routes-simple/middleware/index.ts deleted file mode 100644 index 2d17511..0000000 --- a/src/routes-simple/middleware/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './auth.ts' \ No newline at end of file diff --git a/src/routes-simple/resources/chunk.ts b/src/routes-simple/resources/chunk.ts deleted file mode 100644 index a1a0ec4..0000000 --- a/src/routes-simple/resources/chunk.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { useFileStore } from '@kevisual/use-config'; -import { checkAuth, error, router, writeEvents, getKey, getTaskId } from '../router.ts'; -import Busboy from 'busboy'; -import { app, oss } from '@/app.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'; -import { validateDirectory } from './util.ts'; -import path from 'path'; -import { createWriteStream } from 'fs'; -import { pipeBusboy } from '@/modules/fm-manager/index.ts'; - -const cacheFilePath = useFileStore('cache-file', { needExists: true }); - -router.get('/api/s1/resources/upload/chunk', 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/chunk', async (req, res) => { - const { tokenUser, token } = await checkAuth(req, res); - if (!tokenUser) return; - const url = new URL(req.url || '', 'http://localhost'); - const share = !!url.searchParams.get('public'); - const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles'); - - const taskId = getTaskId(req); - const finalFilePath = `${cacheFilePath}/${taskId}`; - if (!taskId) { - res.end(error('taskId is required')); - return; - } - - // 使用 busboy 解析 multipart/form-data - const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' }); - const fields: any = {}; - let file: any = null; - let tempPath = ''; - let filePromise: Promise | null = null; - - busboy.on('field', (fieldname, value) => { - fields[fieldname] = value; - }); - - busboy.on('file', (fieldname, fileStream, info) => { - const { filename, encoding, mimeType } = info; - // 处理 UTF-8 文件名编码 - const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename; - tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`); - const writeStream = createWriteStream(tempPath); - - filePromise = new Promise((resolve, reject) => { - fileStream.pipe(writeStream); - - writeStream.on('finish', () => { - file = { - filepath: tempPath, - originalFilename: decodedFilename, - mimetype: mimeType, - }; - 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 (tempPath && fs.existsSync(tempPath)) { - fs.unlinkSync(tempPath); - } - if (fs.existsSync(finalFilePath)) { - fs.unlinkSync(finalFilePath); - } - }; - - if (!file) { - res.end(error('No file uploaded')); - return; - } - - // Handle chunked upload logic here - let { chunkIndex, totalChunks, appKey, version, username, directory } = getKey(fields, [ - 'chunkIndex', - 'totalChunks', - 'appKey', - 'version', - 'username', - 'directory', - ]); - if (!chunkIndex || !totalChunks) { - res.end(error('chunkIndex, totalChunks is required')); - clearFiles(); - return; - } - const relativePath = file.originalFilename; - - const writeStream = fs.createWriteStream(finalFilePath, { flags: 'a' }); - const readStream = fs.createReadStream(tempPath); - readStream.pipe(writeStream); - - writeStream.on('finish', async () => { - fs.unlinkSync(tempPath); // 删除临时文件 - - // Write event for progress tracking - const progress = ((parseInt(chunkIndex) + 1) / parseInt(totalChunks)) * 100; - writeEvents(req, { - progress, - message: `Upload progress: ${progress}%`, - }); - - if (parseInt(chunkIndex) + 1 === parseInt(totalChunks)) { - 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 { code, message } = validateDirectory(directory); - if (code !== 200) { - res.end(error(message)); - clearFiles(); - return; - } - const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`; - const metadata: any = {}; - if (share) { - metadata.share = 'public'; - } - // All chunks uploaded, now upload to MinIO - await oss.fPutObject(minioPath, finalFilePath, { - 'Content-Type': getContentType(relativePath), - 'app-source': 'user-app', - 'Cache-Control': relativePath.endsWith('.html') ? 'no-cache' : 'max-age=31536000, immutable', - ...metadata, - }); - - // Clean up the final file - fs.unlinkSync(finalFilePath); - const downloadBase = '/api/s1/share'; - - const uploadResult = { - name: relativePath, - path: `${downloadBase}/${minioPath}`, - appKey, - version, - username, - }; - if (!noCheckAppFiles) { - // Notify the app - const r = await app.call({ - path: 'app', - key: 'detectVersionList', - payload: { - token: token, - data: { - appKey, - version, - username, - }, - }, - }); - const data: any = { - code: r.code, - data: { - app: r.body, - upload: [uploadResult], - }, - }; - if (r.message) { - data.message = r.message; - } - console.log('upload data', data); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(data)); - } else { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - code: 200, - message: 'Chunk uploaded successfully', - data: { chunkIndex, totalChunks, upload: [uploadResult] }, - }), - ); - } - } else { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - code: 200, - message: 'Chunk uploaded successfully', - data: { - chunkIndex, - totalChunks, - }, - }), - ); - } - }); - }); - - pipeBusboy(req, res, busboy); -}); diff --git a/src/routes-simple/resources/upload.ts b/src/routes-simple/resources/upload.ts deleted file mode 100644 index fca2495..0000000 --- a/src/routes-simple/resources/upload.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { useFileStore } from '@kevisual/use-config'; -import { checkAuth, error, router, writeEvents, getKey } from '../router.ts'; -import Busboy from 'busboy'; -import { app } from '@/app.ts'; - -import { getContentType } from '@/utils/get-content-type.ts'; -import { User } from '@/models/user.ts'; -import fs from 'fs'; -import path from 'path'; -import { createWriteStream } from 'fs'; -import { pipeBusboy } from '@/modules/fm-manager/pipe-busboy.ts'; -import { ConfigModel } from '@/routes/config/models/model.ts'; -import { validateDirectory } from './util.ts'; -import { pick } from 'es-toolkit'; -import { getFileStat } from '@/routes/file/index.ts'; -import { logger } from '@/modules/logger.ts'; -import { oss } from '@/modules/s3.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'); -}); -export const parseIfJson = (data = '{}') => { - try { - const _data = JSON.parse(data); - if (typeof _data === 'object') return _data; - return {}; - } catch (error) { - return {}; - } -}; -router.post('/api/s1/resources/upload/check', async (req, res) => { - const { tokenUser, token } = await checkAuth(req, res); - if (!tokenUser) { - res.end(error('Token is invalid.')); - return; - } - console.log('data', req.url); - res.writeHead(200, { 'Content-Type': 'application/json' }); - const data = await router.getBody(req); - type Data = { - appKey: string; - version: string; - username: string; - directory: string; - files: { path: string; hash: string }[]; - }; - let { appKey, version, username, directory, files } = pick(data, ['appKey', 'version', 'username', 'directory', 'files']) as Data; - 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')); - return; - } - const _user = await User.findOne({ where: { username } }); - uid = _user?.id || ''; - } - if (!appKey || !version) { - res.end(error('appKey and version is required')); - } - - const { code, message } = validateDirectory(directory); - if (code !== 200) { - res.end(error(message)); - return; - } - type CheckResult = { - path: string; - stat: any; - resourcePath: string; - hash: string; - uploadHash: string; - isUpload?: boolean; - }; - const checkResult: CheckResult[] = []; - for (let i = 0; i < files.length; i++) { - const file = files[i]; - const relativePath = file.path; - const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`; - let stat = await getFileStat(minioPath, true); - const statHash = stat?.etag || ''; - checkResult.push({ - path: relativePath, - uploadHash: file.hash, - resourcePath: minioPath, - isUpload: statHash === file.hash, - stat, - hash: statHash, - }); - } - res.end(JSON.stringify({ code: 200, data: checkResult })); -}); - -// /api/s1/resources/upload -router.post('/api/s1/resources/upload', async (req, res) => { - const { tokenUser, token } = await checkAuth(req, res); - if (!tokenUser) { - res.end(error('Token is invalid.')); - return; - } - const url = new URL(req.url || '', 'http://localhost'); - const share = !!url.searchParams.get('public'); - const meta = parseIfJson(url.searchParams.get('meta')); - const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles'); - // 使用 busboy 解析 multipart/form-data - const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' }); - const fields: any = {}; - const files: any[] = []; - const filePromises: Promise[] = []; - 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; - // 处理 UTF-8 文件名编码(busboy 可能返回 Latin-1 编码的缓冲区) - const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename; - const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`); - const writeStream = createWriteStream(tempPath); - const filePromise = new Promise((resolve, reject) => { - fileStream.on('data', (chunk) => { - bytesReceived += chunk.length; - if (bytesExpected > 0) { - const progress = (bytesReceived / bytesExpected) * 100; - const data = { - progress: progress.toFixed(2), - message: `Upload progress: ${progress.toFixed(2)}%`, - }; - console.log('progress-upload', data); - writeEvents(req, data); - } - }); - - fileStream.pipe(writeStream); - - writeStream.on('finish', () => { - files.push({ - filepath: tempPath, - originalFilename: decodedFilename, - mimetype: mimeType, - }); - resolve(); - }); - - writeStream.on('error', (err) => { - reject(err); - }); - }); - - filePromises.push(filePromise); - }); - - busboy.on('finish', async () => { - // 等待所有文件写入完成 - try { - await Promise.all(filePromises); - } catch (err) { - logger.error(`File write error: ${err.message}`); - res.end(error(`File write error: ${err.message}`)); - return; - } - const clearFiles = () => { - files.forEach((file) => { - if (file?.filepath && fs.existsSync(file.filepath)) { - fs.unlinkSync(file.filepath); - } - }); - }; - - // 检查是否有文件上传 - if (files.length === 0) { - res.end(error('files is required')); - return; - } - - let { appKey, version, username, directory, description } = getKey(fields, ['appKey', 'version', 'username', 'directory', 'description']); - 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 { code, message } = validateDirectory(directory); - if (code !== 200) { - res.end(error(message)); - clearFiles(); - return; - } - // 逐个处理每个上传的文件 - const uploadedFiles = files; - logger.info( - 'upload files', - uploadedFiles.map((item) => { - return pick(item, ['filepath', 'originalFilename']); - }), - ); - 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}${directory ? `/${directory}` : ''}/${relativePath}`; - // 上传到 MinIO 并保留文件夹结构 - const isHTML = relativePath.endsWith('.html'); - const metadata: any = {}; - if (share) { - metadata.share = 'public'; - } - Object.assign(metadata, meta); - await oss.fPutObject(minioPath, tempPath, { - 'Content-Type': getContentType(relativePath), - 'app-source': 'user-app', - 'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年 - ...metadata, - }); - uploadResults.push({ - name: relativePath, - path: minioPath, - }); - fs.unlinkSync(tempPath); // 删除临时文件 - } - if (!noCheckAppFiles) { - const _data = { appKey, version, username, files: uploadResults, description, } - if (_data.description) { - delete _data.description; - } - // 受控 - const r = await app.call({ - path: 'app', - key: 'uploadFiles', - payload: { - token: token, - data: _data, - }, - }); - const data: any = { - code: r.code, - data: { - app: r.body, - upload: uploadResults, - }, - }; - if (r.message) { - data.message = r.message; - } - console.log('upload data', data); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(data)); - } else { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - code: 200, - data: { - detect: [], - upload: uploadResults, - }, - }), - ); - } - }); - - pipeBusboy(req, res, busboy); -}); diff --git a/src/routes-simple/resources/util.ts b/src/routes-simple/resources/util.ts deleted file mode 100644 index 9de03d2..0000000 --- a/src/routes-simple/resources/util.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 校验directory是否合法, 合法返回200, 不合法返回500 - * - * directory 不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。 - * 把directory的/替换掉后,只能包含数字、字母、下划线、中划线 - * @param directory 目录 - * @returns - */ -export const validateDirectory = (directory?: string) => { - // 对directory进行校验,不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。 - if (directory && (directory.startsWith('/') || directory.endsWith('/') || directory.startsWith('..') || directory.endsWith('..'))) { - return { - code: 500, - message: 'directory is invalid', - }; - } - // 把directory的/替换掉后,只能包含数字、字母、下划线、中划线 - // 可以包含. - let _directory = directory?.replace(/\//g, ''); - if (_directory && !/^[a-zA-Z0-9_.-]+$/.test(_directory)) { - return { - code: 500, - message: 'directory is invalid, only number, letter, underline and hyphen are allowed', - }; - } - return { - code: 200, - message: 'directory is valid', - }; -}; diff --git a/src/routes-simple/router.ts b/src/routes-simple/router.ts index 3de694d..eec065f 100644 --- a/src/routes-simple/router.ts +++ b/src/routes-simple/router.ts @@ -1,14 +1,13 @@ import { router } from '@/app.ts'; import http from 'http'; import { useContextKey } from '@kevisual/context'; -import { checkAuth, error } from './middleware/auth.ts'; -export { router, checkAuth, error }; +export { router, }; /** * 事件客户端 */ const eventClientsInit = () => { - const clients = new Map(); + const clients = new Map(); return clients; }; export const clients = useContextKey('event-clients', () => eventClientsInit()); diff --git a/src/routes/app-manager/list.ts b/src/routes/app-manager/list.ts index 2678e47..55ef75d 100644 --- a/src/routes/app-manager/list.ts +++ b/src/routes/app-manager/list.ts @@ -43,7 +43,7 @@ app console.log('get app manager called'); const tokenUser = ctx.state.tokenUser; const id = ctx.query.id; - const { key, version } = ctx.query?.data || {}; + const { key, version, create = false } = ctx.query?.data || {}; if (!id && (!key || !version)) { throw new CustomError('id is required'); } @@ -59,8 +59,27 @@ app }, }); } + if (!am && create) { + am = await AppListModel.create({ + key, + version, + uid: tokenUser.id, + data: {}, + }); + const res = await app.run({ path: 'app', key: "detectVersionList", payload: { data: { appKey: key, version, username: tokenUser.username }, token: ctx.query.token } }); + if (res.code !== 200) { + ctx.throw(res.message || 'detect version list error'); + } + am = await AppListModel.findOne({ + where: { + key, + version, + uid: tokenUser.id, + }, + }); + } if (!am) { - throw new CustomError('app not found'); + ctx.throw('app not found'); } console.log('get app', am.id, am.key, am.version); ctx.body = prefixFix(am, tokenUser.username);