diff --git a/src/lib/upload.ts b/src/lib/upload.ts index 7368e5c..56e0476 100644 --- a/src/lib/upload.ts +++ b/src/lib/upload.ts @@ -5,6 +5,10 @@ import path from 'path'; import { IncomingForm } from 'formidable'; import { checkToken } from '@abearxiong/auth'; import { useConfig } from '@abearxiong/use-config'; +import { minioClient } from '@/app.ts'; +import { bucketName } from '@/modules/minio.ts'; +import { getContentType } from '@/utils/get-content-type.ts'; +import { User } from '@/models/user.ts'; const { tokenSecret } = useConfig<{ tokenSecret: string }>(); const filePath = useFileStore('upload'); // curl -X POST http://localhost:4000/api/upload -F "file=@readme.md" @@ -15,12 +19,12 @@ const filePath = useFileStore('upload'); // -F "username=testuser" export const uploadMiddleware = async (req: http.IncomingMessage, res: http.ServerResponse) => { - if (req.method === 'GET' && req.url === '/api/upload') { + if (req.method === 'GET' && req.url === '/api/app/upload') { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Upload API is ready'); return; } - if (false && req.method === 'POST' && req.url === '/api/upload') { + if (false && req.method === 'POST' && req.url === '/api/app/upload') { res.writeHead(200, { 'Content-Type': 'text/plain' }); // 检查 Content-Type 是否为 multipart/form-data @@ -78,61 +82,70 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv 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 (req.method === 'POST' && req.url === '/api/app/upload') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + const authroization = req.headers?.['authorization'] as string; + const error = (msg: string) => { + return JSON.stringify({ code: 500, message: msg }); + }; if (!authroization) { res.statusCode = 401; - res.end('Invalid authorization'); + res.end(error('Invalid authorization')); return; } const token = authroization.split(' ')[1]; - const tokenUser = await checkToken(token, tokenSecret); - if (!tokenUser) { + let tokenUser; + try { + tokenUser = await User.verifyToken(token); + } catch (e) { res.statusCode = 401; - res.end('Invalid token'); + res.end(error('Invalid token')); return; } - // + // // 使用 formidable 解析 multipart/form-data const form = new IncomingForm({ multiples: true, // 支持多文件上传 uploadDir: filePath, // 上传文件存储目录 }); // 解析上传的文件 - form.parse(req, (err, fields, files) => { + form.parse(req, async (err, fields, files) => { if (err) { - res.end(`Upload error: ${err.message}`); + res.end(error(`Upload error: ${err.message}`)); return; } console.log('fields', fields); + const { appKey, version } = fields; // 逐个处理每个上传的文件 const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file]; const uploadResults = []; - - uploadedFiles.forEach((file) => { + for (let i = 0; i < uploadedFiles.length; i++) { + const file = uploadedFiles[i]; // @ts-ignore const tempPath = file.filepath; // 文件上传时的临时路径 const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构) - uploadResults.push(`File ${relativePath} uploaded successfully. ${tempPath}`); + // 比如 child2/b.txt + const minioPath = `${tokenUser.username}/${appKey}/${version}/${relativePath}`; // 上传到 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')); + 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); // 删除临时文件 + } + // 修改header + // res.writeHead(200, { 'Content-Type': 'text/plain' }); + const data = { + code: 200, + data: uploadResults, + }; + res.end(JSON.stringify(data)); }); } }; diff --git a/src/routes/app-manager/list.ts b/src/routes/app-manager/list.ts index aa6d1e0..3da0370 100644 --- a/src/routes/app-manager/list.ts +++ b/src/routes/app-manager/list.ts @@ -10,10 +10,15 @@ app }) .define(async (ctx) => { const tokenUser = ctx.state.tokenUser; + const data = ctx.query.data || {}; + if (!data.key) { + throw new CustomError('key is required'); + } const list = await AppListModel.findAll({ order: [['updatedAt', 'DESC']], where: { uid: tokenUser.id, + key: data.key, }, }); ctx.body = list; @@ -61,6 +66,10 @@ app } return; } + + if (!rest.key) { + throw new CustomError('key is required'); + } const app = await AppListModel.create({ data, ...rest, uid: tokenUser.id }); ctx.body = app; return ctx; @@ -82,7 +91,9 @@ app if (!app) { throw new CustomError('app not found'); } - await app.destroy(); + await app.destroy({ + force: true, + }); ctx.body = 'success'; return ctx; }) diff --git a/src/routes/app-manager/module/app-list.ts b/src/routes/app-manager/module/app-list.ts index 60429f5..2fd46b3 100644 --- a/src/routes/app-manager/module/app-list.ts +++ b/src/routes/app-manager/module/app-list.ts @@ -11,8 +11,7 @@ export class AppListModel extends Model { declare id: string; declare data: AppData; declare version: string; - declare appType: AppType; - declare type: string; + declare key: string; declare uid: string; } @@ -32,13 +31,8 @@ AppListModel.init( type: DataTypes.STRING, defaultValue: '', }, - appType: { + key: { type: DataTypes.STRING, - defaultValue: '', - }, - type: { - type: DataTypes.STRING, - defaultValue: '', }, uid: { type: DataTypes.UUID, diff --git a/src/routes/app-manager/module/app.ts b/src/routes/app-manager/module/app.ts index 1667846..3d30c37 100644 --- a/src/routes/app-manager/module/app.ts +++ b/src/routes/app-manager/module/app.ts @@ -14,6 +14,8 @@ export type App = Partial>; export class AppModel extends Model { declare id: string; declare data: AppData; + declare title: string; + declare description: string; declare version: string; declare domain: string; declare appType: string; @@ -21,6 +23,7 @@ export class AppModel extends Model { declare type: string; declare uid: string; declare user: string; + declare status: string; } AppModel.init( { @@ -30,6 +33,14 @@ AppModel.init( defaultValue: DataTypes.UUIDV4, comment: 'id', }, + title: { + type: DataTypes.STRING, + defaultValue: '', + }, + description: { + type: DataTypes.STRING, + defaultValue: '', + }, data: { type: DataTypes.JSON, defaultValue: {}, @@ -48,7 +59,7 @@ AppModel.init( }, key: { type: DataTypes.STRING, - unique: true, + // 和 uid 组合唯一 }, type: { type: DataTypes.STRING, @@ -58,11 +69,25 @@ AppModel.init( type: DataTypes.UUID, allowNull: true, }, + user: { + type: DataTypes.STRING, + allowNull: true, + }, + status: { + type: DataTypes.STRING, + defaultValue: 'running', // stop, running + }, }, { sequelize, tableName: 'kv_app', paranoid: true, + indexes: [ + { + unique: true, + fields: ['key', 'uid'], + }, + ], }, ); diff --git a/src/routes/app-manager/user-app.ts b/src/routes/app-manager/user-app.ts index d9641a1..ae98535 100644 --- a/src/routes/app-manager/user-app.ts +++ b/src/routes/app-manager/user-app.ts @@ -48,6 +48,8 @@ app middleware: ['auth'], }) .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const { data, id, ...rest } = ctx.query.data; if (id) { const app = await AppModel.findByPk(id); @@ -60,8 +62,19 @@ app } return; } - const tokenUser = ctx.state.tokenUser; - const app = await AppModel.create({ data, ...rest, uid: tokenUser.id }); + if (!rest.key) { + throw new CustomError('key is required'); + } + const findApp = await AppModel.findOne({ where: { key: rest.key, uid: tokenUser.id } }); + if (findApp) { + throw new CustomError('key already exists'); + } + const app = await AppModel.create({ + data: { files: [] }, + ...rest, + uid: tokenUser.id, + user: tokenUser.username, + }); ctx.body = app; return ctx; }) diff --git a/src/routes/file/list.ts b/src/routes/file/list.ts index ef0494d..c68dac7 100644 --- a/src/routes/file/list.ts +++ b/src/routes/file/list.ts @@ -1,8 +1,25 @@ import { app } from '@/app.ts'; -import { getMinioList } from './module/get-minio-list.ts'; +import { getFileStat, getMinioList } from './module/get-minio-list.ts'; import path from 'path'; import { CustomError } from '@abearxiong/router'; +import { get } from 'http'; +const handlePrefix = (prefix: string) => { + // 清理所有的 '..' + if (!prefix) return ''; + if (prefix.includes('..')) { + throw new CustomError('invalid prefix'); + } + return prefix; +}; +const getPrefixByUser = (data: { prefix: string }, tokenUser: { username: string }) => { + const prefixBase = '/' + tokenUser.username; + const _prefix = handlePrefix(data.prefix); + return { + len: prefixBase.length, + prefix: path.join(prefixBase, './', _prefix), + }; +}; app .route({ path: 'file', @@ -12,19 +29,37 @@ app .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 { len, prefix } = getPrefixByUser(data, tokenUser); const recursive = data.recursive; const list = await getMinioList({ prefix: prefix.slice(1), recursive: recursive }); - ctx.body = list; + + ctx.body = list.map((item) => { + if ('prefix' in item) { + return { + ...item, + prefix: item.prefix.slice(len), + }; + } else { + return { ...item, name: item.name.slice(len) }; + } + }); + return ctx; + }) + .addTo(app); + +app + .route({ + path: 'file', + key: 'stat', + middleware: ['auth'], + }) + .define(async (ctx) => { + const tokenUser = ctx.state.tokenUser; + const data = ctx.query.data || {}; + const { prefix } = getPrefixByUser(data, tokenUser); + console.log('prefix', prefix); + const stat = await getFileStat(prefix.slice(1)); + ctx.body = stat; return ctx; }) .addTo(app); diff --git a/src/routes/file/module/get-minio-list.ts b/src/routes/file/module/get-minio-list.ts index 2241b18..5441f6d 100644 --- a/src/routes/file/module/get-minio-list.ts +++ b/src/routes/file/module/get-minio-list.ts @@ -5,17 +5,17 @@ type MinioListOpt = { prefix: string; recursive?: boolean; }; -type MinioFile = { +export type MinioFile = { name: string; size: number; lastModified: Date; etag: string; }; -type MinioDirectory = { +export type MinioDirectory = { prefix: string; size: number; }; -type MinioList = (MinioFile | MinioDirectory)[]; +export type MinioList = (MinioFile | MinioDirectory)[]; export const getMinioList = async (opts: MinioListOpt): Promise => { const prefix = opts.prefix; const recursive = opts.recursive ?? false; @@ -41,3 +41,15 @@ export const getMinioList = async (opts: MinioListOpt): Promise => { }); }); }; +export const getFileStat = async (prefix: string): Promise => { + try { + const obj = await minioClient.statObject(bucketName, prefix); + return obj; + } catch (e) { + if (e.code === 'NotFound') { + return null; + } + console.error('get File Stat Error not handle', e); + return null; + } +}; diff --git a/src/scripts/get-minio-list.ts b/src/scripts/get-minio-list.ts index c28537c..fb192ef 100644 --- a/src/scripts/get-minio-list.ts +++ b/src/scripts/get-minio-list.ts @@ -1,5 +1,5 @@ import { bucketName, minioClient } from '@/modules/minio.ts'; - +import { S3Error } from 'minio'; const main = async () => { const res = await new Promise((resolve, reject) => { let res: any[] = []; @@ -24,4 +24,17 @@ const main = async () => { }); console.log(res); }; -main(); +// main(); + +const main2 = async () => { + try { + const obj = await minioClient.statObject(bucketName, 'root/codeflow/0.0.1/README.md'); + + console.log(obj); + } catch (e) { + console.log('', e.message, '\n\r', e.code); + // console.error(e); + } +}; + +main2(); diff --git a/src/utils/get-content-type.ts b/src/utils/get-content-type.ts new file mode 100644 index 0000000..d592ba3 --- /dev/null +++ b/src/utils/get-content-type.ts @@ -0,0 +1,18 @@ +import path from 'path'; +// 获取文件的 content-type +export const getContentType = (filePath: string) => { + const extname = path.extname(filePath); + const contentType = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.wav': 'audio/wav', + '.mp4': 'video/mp4', + }; + return contentType[extname] || 'application/octet-stream'; +}; diff --git a/upload/6c885eb32f2698efeb5720102 b/upload/6c885eb32f2698efeb5720102 deleted file mode 100644 index 8ac69e4..0000000 --- a/upload/6c885eb32f2698efeb5720102 +++ /dev/null @@ -1,2 +0,0 @@ -code的flow流程成图 - diff --git a/upload/6c885eb32f2698efeb5720103 b/upload/6c885eb32f2698efeb5720103 deleted file mode 100644 index 157478f..0000000 --- a/upload/6c885eb32f2698efeb5720103 +++ /dev/null @@ -1,41 +0,0 @@ -// 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 {};