diff --git a/package.json b/package.json index 95c6161..a8658e5 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/semver": "^7.5.8", "dayjs": "^1.11.13", "dts-bundle-generator": "^9.5.1", + "formidable": "^3.5.1", "ioredis": "^5.4.1", "json5": "^2.2.3", "jsonwebtoken": "^9.0.2", @@ -61,7 +62,9 @@ "zod": "^3.23.8" }, "devDependencies": { + "@abearxiong/use-file-store": "^0.0.1", "@types/crypto-js": "^4.2.2", + "@types/formidable": "^3.4.5", "@types/jsonwebtoken": "^9.0.7", "@types/lodash-es": "^4.17.12", "@types/node": "^22.7.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a580083..74c6c1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,9 @@ importers: dts-bundle-generator: specifier: ^9.5.1 version: 9.5.1 + formidable: + specifier: ^3.5.1 + version: 3.5.1 ioredis: specifier: ^5.4.1 version: 5.4.1 @@ -98,9 +101,15 @@ importers: specifier: ^3.23.8 version: 3.23.8 devDependencies: + '@abearxiong/use-file-store': + specifier: ^0.0.1 + version: 0.0.1(typescript@5.6.2)(webpack-cli@5.1.4(webpack@5.95.0)) '@types/crypto-js': specifier: ^4.2.2 version: 4.2.2 + '@types/formidable': + specifier: ^3.4.5 + version: 3.4.5 '@types/jsonwebtoken': specifier: ^9.0.7 version: 9.0.7 @@ -239,6 +248,9 @@ packages: '@abearxiong/use-config@0.0.2': resolution: {integrity: sha512-IBOmeP46ykbDlkplFS65UsAHjyPDKnvS2oqbkpLWhbSwDbF5zhBnD4ibsFZKPCyc3lMlPeRqYva4x6puX3E/qQ==, tarball: https://npm.pkg.github.com/download/@abearxiong/use-config/0.0.2/59fbeec8c8e086ec48e55024fe39020b079e6fa5} + '@abearxiong/use-file-store@0.0.1': + resolution: {integrity: sha512-65ZQBHxwr76sAFG+Xd4IQstx8dERhkaX5MLqtqJ0f9m+2NnS/klNe0t4q9tgjMWAEWQxHjnPShpHWzkCENaDnQ==, tarball: https://npm.pkg.github.com/download/@abearxiong/use-file-store/0.0.1/f171e398c078d4940c1ddedf5ad529d17b0eec32} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -1247,6 +1259,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/formidable@3.4.5': + resolution: {integrity: sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1469,6 +1484,9 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -1736,6 +1754,9 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dotenv@4.0.0: resolution: {integrity: sha512-XcaMACOr3JMVcEv0Y/iUM2XaOsATRZ3U1In41/1jjK6vJZ2PZbQ1bzCG8uvaByfaBpl9gqc9QWJovpUGBXLLYQ==} engines: {node: '>=4.6.0'} @@ -1954,6 +1975,9 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + formidable@3.5.1: + resolution: {integrity: sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==} + fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -2071,6 +2095,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hexoid@1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -2562,6 +2590,9 @@ packages: ollama@0.5.9: resolution: {integrity: sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} @@ -3328,6 +3359,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.17.1: resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} @@ -3414,6 +3448,18 @@ snapshots: '@abearxiong/use-config@0.0.2': {} + '@abearxiong/use-file-store@0.0.1(typescript@5.6.2)(webpack-cli@5.1.4(webpack@5.95.0))': + dependencies: + json5: 2.2.3 + ts-loader: 9.5.1(typescript@5.6.2)(webpack@5.95.0(webpack-cli@5.1.4)) + webpack: 5.95.0(webpack-cli@5.1.4) + transitivePeerDependencies: + - '@swc/core' + - esbuild + - typescript + - uglify-js + - webpack-cli + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -4527,6 +4573,10 @@ snapshots: '@types/estree@1.0.6': {} + '@types/formidable@3.4.5': + dependencies: + '@types/node': 22.7.4 + '@types/json-schema@7.0.15': {} '@types/jsonwebtoken@9.0.7': @@ -4769,6 +4819,8 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 + asap@2.0.6: {} + async@3.2.6: {} asynckit@0.4.0: {} @@ -5056,6 +5108,11 @@ snapshots: denque@2.1.0: {} + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dotenv@4.0.0: {} dotignore@0.1.2: @@ -5347,6 +5404,12 @@ snapshots: dependencies: fetch-blob: 3.2.0 + formidable@3.5.1: + dependencies: + dezalgo: 1.0.4 + hexoid: 1.0.0 + once: 1.4.0 + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -5468,6 +5531,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hexoid@1.0.0: {} + humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -5939,6 +6004,10 @@ snapshots: dependencies: whatwg-fetch: 3.6.20 + once@1.4.0: + dependencies: + wrappy: 1.0.2 + open@7.4.2: dependencies: is-docker: 2.2.1 @@ -6776,6 +6845,8 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrappy@1.0.2: {} + ws@8.17.1: {} ws@8.18.0: {} diff --git a/src/index.ts b/src/index.ts index 50a43af..4a3780f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,9 +3,11 @@ import { app } from './app.ts'; import './route.ts'; const config = useConfig(); import { app as aiApp } from '@kevisual/ai-lang/src/index.ts'; +import { uploadMiddleware } from './lib/upload.ts'; // export { aiApp }; export { app }; app.listen(config.port, () => { console.log(`server is running at http://localhost:${config.port}`); }); +app.server.on(uploadMiddleware); diff --git a/src/lib/upload.ts b/src/lib/upload.ts new file mode 100644 index 0000000..7368e5c --- /dev/null +++ b/src/lib/upload.ts @@ -0,0 +1,179 @@ +import { useFileStore } from '@abearxiong/use-file-store'; +import http from 'http'; +import fs from 'fs'; +import path from 'path'; +import { IncomingForm } from 'formidable'; +import { checkToken } from '@abearxiong/auth'; +import { useConfig } from '@abearxiong/use-config'; +const { tokenSecret } = useConfig<{ tokenSecret: string }>(); +const filePath = useFileStore('upload'); +// 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" + +export const uploadMiddleware = async (req: http.IncomingMessage, res: http.ServerResponse) => { + if (req.method === 'GET' && req.url === '/api/upload') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Upload API is ready'); + return; + } + if (false && req.method === 'POST' && req.url === '/api/upload') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + + // 检查 Content-Type 是否为 multipart/form-data + const contentType = req.headers['content-type']; + if (!contentType || !contentType.startsWith('multipart/form-data')) { + res.end('Invalid content type, expecting multipart/form-data'); + return; + } + // 提取 boundary (边界) 标识 + const boundary = contentType.split('boundary=')[1]; + if (!boundary) { + res.end('Invalid multipart/form-data format'); + return; + } + + // 将接收到的所有数据存入临时数组中 + let rawData = Buffer.alloc(0); + req.on('data', (chunk) => { + rawData = Buffer.concat([rawData, chunk]); + }); + + req.on('end', () => { + // 解析所有文件部分 + const parts = parseMultipartData(rawData, boundary); + + // 存储上传文件结果 + const uploadResults = []; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part.filename) { + if (!fs.existsSync(filePath)) { + fs.mkdirSync(filePath, { recursive: true }); + } + const tempFilePath = path.join(filePath, part.filename); + fs.writeFileSync(tempFilePath, part.data); + uploadResults.push(`File ${part.filename} uploaded successfully.`); + // 上传到 MinIO + // minioClient.fPutObject(bucketName, part.filename, tempFilePath, {}, (err, etag) => { + // fs.unlinkSync(tempFilePath); // 删除临时文件 + // if (err) { + // uploadResults.push(`Upload error for ${part.filename}: ${err.message}`); + // } else { + // uploadResults.push(`File ${part.filename} uploaded successfully. ETag: ${etag}`); + // } + + // // 如果所有文件都处理完毕,返回结果 + // if (uploadResults.length === parts.length) { + // res.writeHead(200, { 'Content-Type': 'text/plain' }); + // res.end(uploadResults.join('\n')); + // } + // }); + } + } + res.end(uploadResults.join('\n')); + }); + } + if (req.method === 'POST' && req.url === '/api/upload') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + const authroization = req.headers?.['Authorization'] as string; + if (!authroization) { + res.statusCode = 401; + res.end('Invalid authorization'); + return; + } + const token = authroization.split(' ')[1]; + const tokenUser = await checkToken(token, tokenSecret); + if (!tokenUser) { + res.statusCode = 401; + res.end('Invalid token'); + return; + } + // + // 使用 formidable 解析 multipart/form-data + const form = new IncomingForm({ + multiples: true, // 支持多文件上传 + uploadDir: filePath, // 上传文件存储目录 + }); + // 解析上传的文件 + form.parse(req, (err, fields, files) => { + if (err) { + res.end(`Upload error: ${err.message}`); + return; + } + console.log('fields', fields); + // 逐个处理每个上传的文件 + const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file]; + const uploadResults = []; + + uploadedFiles.forEach((file) => { + // @ts-ignore + const tempPath = file.filepath; // 文件上传时的临时路径 + const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构) + uploadResults.push(`File ${relativePath} uploaded successfully. ${tempPath}`); + // 上传到 MinIO 并保留文件夹结构 + // minioClient.fPutObject(bucketName, relativePath, tempPath, {}, (err, etag) => { + // fs.unlinkSync(tempPath); // 删除临时文件 + + // if (err) { + // uploadResults.push(`Upload error for ${relativePath}: ${err.message}`); + // } else { + // uploadResults.push(`File ${relativePath} uploaded successfully. ETag: ${etag}`); + // } + + // // 如果所有文件都处理完毕,返回结果 + // if (uploadResults.length === uploadedFiles.length) { + // res.writeHead(200, { 'Content-Type': 'text/plain' }); + // res.end(uploadResults.join('\n')); + // } + // }); + }); + res.end(uploadResults.join('\n')); + }); + } +}; + +/** + * 解析 multipart/form-data 格式数据,提取各个字段和文件内容 + * @param {Buffer} buffer - 完整的 HTTP 请求体数据 + * @param {string} boundary - multipart/form-data 的 boundary 标识符 + * @returns {Array} 返回包含各个部分数据的数组 + */ +function parseMultipartData(buffer, boundary) { + const parts = []; + const boundaryBuffer = Buffer.from(`--${boundary}`, 'utf-8'); + let start = buffer.indexOf(boundaryBuffer) + boundaryBuffer.length + 2; // Skip first boundary and \r\n + + while (start < buffer.length) { + // 查找下一个 boundary 的位置 + const end = buffer.indexOf(boundaryBuffer, start) - 2; // Subtract 2 to remove trailing \r\n + if (end <= start) break; + + // 提取单个 part 数据 + const part = buffer.slice(start, end); + start = end + boundaryBuffer.length + 2; // Move start to next part + + // 分割 part 头和内容 + const headerEndIndex = part.indexOf('\r\n\r\n'); + const headers = part.slice(0, headerEndIndex).toString(); + const content = part.slice(headerEndIndex + 4); // Skip \r\n\r\n + + // 解析 headers 以获取字段名称和文件信息 + const nameMatch = headers.match(/name="([^"]+)"/); + const filenameMatch = headers.match(/filename="([^"]+)"/); + + const partData = { + name: nameMatch ? nameMatch[1] : null, + filename: filenameMatch ? filenameMatch[1] : null, + data: content, + }; + + parts.push(partData); + } + + return parts; +} diff --git a/src/route.ts b/src/route.ts index bbe8f74..7c46c7a 100644 --- a/src/route.ts +++ b/src/route.ts @@ -10,3 +10,4 @@ createAuthRoute({ app, secret: config.tokenSecret, }); + diff --git a/src/routes/app-manager/index.ts b/src/routes/app-manager/index.ts new file mode 100644 index 0000000..0868247 --- /dev/null +++ b/src/routes/app-manager/index.ts @@ -0,0 +1,2 @@ +import './list.ts'; +import './user-app.ts'; diff --git a/src/routes/app-manager/list.ts b/src/routes/app-manager/list.ts new file mode 100644 index 0000000..aa6d1e0 --- /dev/null +++ b/src/routes/app-manager/list.ts @@ -0,0 +1,89 @@ +import { CustomError } from '@abearxiong/router'; +import { AppModel, AppListModel } from './module/index.ts'; +import { app } from '@/app.ts'; + +app + .route({ + path: 'app', + key: 'list', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const list = await AppListModel.findAll({ + order: [['updatedAt', 'DESC']], + where: { + uid: tokenUser.id, + }, + }); + ctx.body = list; + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'app', + key: 'get', + middleware: ['auth'], + }) + .define(async (ctx) => { + const id = ctx.query.id; + if (!id) { + throw new CustomError('id is required'); + } + const am = await AppListModel.findByPk(id); + if (!am) { + throw new CustomError('app not found'); + } + ctx.body = am; + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'app', + key: 'update', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { data, id, ...rest } = ctx.query.data; + if (id) { + const app = await AppListModel.findByPk(id); + if (app) { + const newData = { ...app.data, ...data }; + const newApp = await app.update({ data: newData, ...rest }); + ctx.body = newApp; + } else { + throw new CustomError('app not found'); + } + return; + } + const app = await AppListModel.create({ data, ...rest, uid: tokenUser.id }); + ctx.body = app; + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'app', + key: 'delete', + middleware: ['auth'], + }) + .define(async (ctx) => { + const id = ctx.query.id; + if (!id) { + throw new CustomError('id is required'); + } + const app = await AppListModel.findByPk(id); + if (!app) { + throw new CustomError('app not found'); + } + await app.destroy(); + ctx.body = 'success'; + return ctx; + }) + .addTo(app); diff --git a/src/routes/app-manager/module/app-list.ts b/src/routes/app-manager/module/app-list.ts new file mode 100644 index 0000000..60429f5 --- /dev/null +++ b/src/routes/app-manager/module/app-list.ts @@ -0,0 +1,57 @@ +import { sequelize } from '../../../modules/sequelize.ts'; +import { DataTypes, Model } from 'sequelize'; +import { AppData, AppType } from './app.ts'; + +export type AppList = Partial>; + +/** + * APP List 管理 + */ +export class AppListModel extends Model { + declare id: string; + declare data: AppData; + declare version: string; + declare appType: AppType; + declare type: string; + declare uid: string; +} + +AppListModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + comment: 'id', + }, + data: { + type: DataTypes.JSON, + defaultValue: {}, + }, + version: { + type: DataTypes.STRING, + defaultValue: '', + }, + appType: { + type: DataTypes.STRING, + defaultValue: '', + }, + type: { + type: DataTypes.STRING, + defaultValue: '', + }, + uid: { + type: DataTypes.UUID, + allowNull: true, + }, + }, + { + sequelize, + tableName: 'kv_app_list', + paranoid: true, + }, +); + +AppListModel.sync({ alter: true, logging: false }).catch((e) => { + console.error('AppListModel sync', e); +}); diff --git a/src/routes/app-manager/module/app.ts b/src/routes/app-manager/module/app.ts new file mode 100644 index 0000000..1667846 --- /dev/null +++ b/src/routes/app-manager/module/app.ts @@ -0,0 +1,71 @@ +import { sequelize } from '../../../modules/sequelize.ts'; +import { DataTypes, Model } from 'sequelize'; + +export interface AppData { + files: { name: string; path: string }[]; +} +export type AppType = 'web-single' | 'web-module'; + +export type App = Partial>; + +/** + * APP 管理 + */ +export class AppModel extends Model { + declare id: string; + declare data: AppData; + declare version: string; + declare domain: string; + declare appType: string; + declare key: string; + declare type: string; + declare uid: string; + declare user: string; +} +AppModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + comment: 'id', + }, + data: { + type: DataTypes.JSON, + defaultValue: {}, + }, + version: { + type: DataTypes.STRING, + defaultValue: '', + }, + domain: { + type: DataTypes.STRING, + defaultValue: '', + }, + appType: { + type: DataTypes.STRING, + defaultValue: '', + }, + key: { + type: DataTypes.STRING, + unique: true, + }, + type: { + type: DataTypes.STRING, + defaultValue: '', + }, + uid: { + type: DataTypes.UUID, + allowNull: true, + }, + }, + { + sequelize, + tableName: 'kv_app', + paranoid: true, + }, +); + +AppModel.sync({ alter: true, logging: false }).catch((e) => { + console.error('AppModel sync', e); +}); diff --git a/src/routes/app-manager/module/index.ts b/src/routes/app-manager/module/index.ts new file mode 100644 index 0000000..11d6baa --- /dev/null +++ b/src/routes/app-manager/module/index.ts @@ -0,0 +1,2 @@ +export * from './app-list.ts'; +export * from './app.ts'; diff --git a/src/routes/app-manager/user-app.ts b/src/routes/app-manager/user-app.ts new file mode 100644 index 0000000..d9641a1 --- /dev/null +++ b/src/routes/app-manager/user-app.ts @@ -0,0 +1,88 @@ +import { CustomError } from '@abearxiong/router'; +import { AppModel, AppListModel } from './module/index.ts'; +import { app } from '@/app.ts'; + +app + .route({ + path: 'user-app', + key: 'list', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const list = await AppModel.findAll({ + order: [['updatedAt', 'DESC']], + where: { + uid: tokenUser.id, + }, + }); + ctx.body = list; + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'user-app', + key: 'get', + middleware: ['auth'], + }) + .define(async (ctx) => { + const id = ctx.query.id; + if (!id) { + throw new CustomError('id is required'); + } + const am = await AppModel.findByPk(id); + if (!am) { + throw new CustomError('app not found'); + } + ctx.body = am; + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'user-app', + key: 'update', + middleware: ['auth'], + }) + .define(async (ctx) => { + const { data, id, ...rest } = ctx.query.data; + if (id) { + const app = await AppModel.findByPk(id); + if (app) { + const newData = { ...app.data, ...data }; + const newApp = await app.update({ data: newData, ...rest }); + ctx.body = newApp; + } else { + throw new CustomError('app not found'); + } + return; + } + const tokenUser = ctx.state.tokenUser; + const app = await AppModel.create({ data, ...rest, uid: tokenUser.id }); + ctx.body = app; + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'user-app', + key: 'delete', + middleware: ['auth'], + }) + .define(async (ctx) => { + const id = ctx.query.id; + if (!id) { + throw new CustomError('id is required'); + } + const am = await AppModel.findByPk(id); + if (!am) { + throw new CustomError('app not found'); + } + await am.destroy(); + return ctx; + }) + .addTo(app); diff --git a/src/routes/file/index.ts b/src/routes/file/index.ts new file mode 100644 index 0000000..83ec5cd --- /dev/null +++ b/src/routes/file/index.ts @@ -0,0 +1 @@ +import './list.ts'; diff --git a/src/routes/file/list.ts b/src/routes/file/list.ts new file mode 100644 index 0000000..ef0494d --- /dev/null +++ b/src/routes/file/list.ts @@ -0,0 +1,30 @@ +import { app } from '@/app.ts'; +import { getMinioList } from './module/get-minio-list.ts'; +import path from 'path'; +import { CustomError } from '@abearxiong/router'; + +app + .route({ + path: 'file', + key: 'list', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const data = ctx.query.data || {}; + const prefixBase = '/' + tokenUser.username; + const handlePrefix = (prefix: string) => { + // 清理所有的 '..' + if (prefix.includes('..')) { + throw new CustomError('invalid prefix'); + } + return prefix; + }; + const _prefix = handlePrefix(data.prefix); + const prefix = path.join(prefixBase, './', _prefix); + const recursive = data.recursive; + const list = await getMinioList({ prefix: prefix.slice(1), recursive: recursive }); + ctx.body = list; + return ctx; + }) + .addTo(app); diff --git a/src/routes/file/module/get-minio-list.ts b/src/routes/file/module/get-minio-list.ts new file mode 100644 index 0000000..2241b18 --- /dev/null +++ b/src/routes/file/module/get-minio-list.ts @@ -0,0 +1,43 @@ +import { minioClient } from '@/app.ts'; +import { bucketName } from '@/modules/minio.ts'; + +type MinioListOpt = { + prefix: string; + recursive?: boolean; +}; +type MinioFile = { + name: string; + size: number; + lastModified: Date; + etag: string; +}; +type MinioDirectory = { + prefix: string; + size: number; +}; +type MinioList = (MinioFile | MinioDirectory)[]; +export const getMinioList = async (opts: MinioListOpt): Promise => { + const prefix = opts.prefix; + const recursive = opts.recursive ?? false; + return await new Promise((resolve, reject) => { + let res: any[] = []; + let hasError = false; + minioClient + .listObjectsV2(bucketName, prefix, recursive) + .on('data', (data) => { + res.push(data); + }) + .on('error', (err) => { + console.error('minio error', opts.prefix, err); + hasError = true; + }) + .on('end', () => { + if (hasError) { + reject(); + return; + } else { + resolve(res); + } + }); + }); +}; diff --git a/src/routes/index.ts b/src/routes/index.ts index b68d478..7087c5b 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -15,3 +15,7 @@ import './chat-prompt/index.ts'; import './chat-history/index.ts'; import './github/index.ts'; + +import './app-manager/index.ts'; + +import './file/index.ts'; diff --git a/src/scripts/get-minio-list.ts b/src/scripts/get-minio-list.ts new file mode 100644 index 0000000..c28537c --- /dev/null +++ b/src/scripts/get-minio-list.ts @@ -0,0 +1,27 @@ +import { bucketName, minioClient } from '@/modules/minio.ts'; + +const main = async () => { + const res = await new Promise((resolve, reject) => { + let res: any[] = []; + let hasError = false; + minioClient + .listObjectsV2(bucketName, 'root/codeflow/0.0.1/') + .on('data', (data) => { + res.push(data); + }) + .on('error', (err) => { + console.error('error', err); + hasError = true; + }) + .on('end', () => { + if (hasError) { + reject(); + return; + } else { + resolve(res); + } + }); + }); + console.log(res); +}; +main(); diff --git a/upload/6c885eb32f2698efeb5720102 b/upload/6c885eb32f2698efeb5720102 new file mode 100644 index 0000000..8ac69e4 --- /dev/null +++ b/upload/6c885eb32f2698efeb5720102 @@ -0,0 +1,2 @@ +code的flow流程成图 + diff --git a/upload/6c885eb32f2698efeb5720103 b/upload/6c885eb32f2698efeb5720103 new file mode 100644 index 0000000..157478f --- /dev/null +++ b/upload/6c885eb32f2698efeb5720103 @@ -0,0 +1,41 @@ +// Generated by dts-bundle-generator v9.5.1 + +export type RouterCode = { + id: string; + path: string; + key: string; + active: boolean; + project: string; + code: string; + exec: string; + type: RouterCodeType; + middleware: string[]; + next: string; + data: any; + validator: any; +}; +declare enum RouterCodeType { + route = "route", + middleware = "middleware" +} +declare enum CodeStatus { + running = "running", + stop = "stop", + fail = "fail" +} +export type CodeManager = { + fn?: any; + status?: CodeStatus; + errorMsg?: string; + lock?: boolean; +} & Partial; +export interface ContainerData { + style?: { + [key: string]: string; + }; + className?: string; + showChild?: boolean; + shadowRoot?: boolean; +} + +export {};