This commit is contained in:
xion 2025-03-14 01:41:53 +08:00
parent efef48a1b0
commit d947043a16
12 changed files with 284 additions and 65 deletions

View File

@ -4,8 +4,11 @@ import * as redisLib from './modules/redis.ts';
import * as minioLib from './modules/minio.ts'; import * as minioLib from './modules/minio.ts';
import * as sequelizeLib from './modules/sequelize.ts'; import * as sequelizeLib from './modules/sequelize.ts';
import { useContextKey, useContext } from '@kevisual/use-config/context'; import { useContextKey, useContext } from '@kevisual/use-config/context';
import { SimpleRouter } from '@kevisual/router/simple';
useConfig(); useConfig();
export const router = useContextKey('router', () => new SimpleRouter());
export const redis = useContextKey('redis', () => redisLib.redis); export const redis = useContextKey('redis', () => redisLib.redis);
export const redisPublisher = useContextKey('redisPublisher', () => redisLib.redisPublisher); export const redisPublisher = useContextKey('redisPublisher', () => redisLib.redisPublisher);
export const redisSubscriber = useContextKey('redisSubscriber', () => redisLib.redisSubscriber); export const redisSubscriber = useContextKey('redisSubscriber', () => redisLib.redisSubscriber);

View File

@ -1,9 +1,8 @@
import { IncomingForm } from 'formidable'; import { IncomingForm } from 'formidable';
import { checkAuth } from '../middleware/auth.ts'; import { checkAuth } from '../middleware/auth.ts';
import { router } from '../router.ts'; import { router, clients, writeEvents } from '../router.ts';
import { error } from '../middleware/auth.ts'; import { error } from '../middleware/auth.ts';
import fs from 'fs'; import fs from 'fs';
import { clients } from '../upload.ts';
import { useFileStore } from '@kevisual/use-config/file-store'; import { useFileStore } from '@kevisual/use-config/file-store';
import { app, minioClient } from '@/app.ts'; import { app, minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts'; import { bucketName } from '@/modules/minio.ts';
@ -29,7 +28,6 @@ router.post('/api/micro-app/upload', async (req, res) => {
keepExtensions: true, // 保留文件 keepExtensions: true, // 保留文件
hashAlgorithm: 'md5', // 文件哈希算法 hashAlgorithm: 'md5', // 文件哈希算法
}); });
const taskId = req.headers['task-id'] as string;
form.on('progress', (bytesReceived, bytesExpected) => { form.on('progress', (bytesReceived, bytesExpected) => {
const progress = (bytesReceived / bytesExpected) * 100; const progress = (bytesReceived / bytesExpected) * 100;
console.log(`Upload progress: ${progress.toFixed(2)}%`); console.log(`Upload progress: ${progress.toFixed(2)}%`);
@ -37,7 +35,7 @@ router.post('/api/micro-app/upload', async (req, res) => {
progress: progress.toFixed(2), progress: progress.toFixed(2),
message: `Upload progress: ${progress.toFixed(2)}%`, message: `Upload progress: ${progress.toFixed(2)}%`,
}; };
clients.get(taskId)?.client?.write?.(`${JSON.stringify(data)}\n`); writeEvents(req, data);
}); });
// 解析上传的文件 // 解析上传的文件
form.parse(req, async (err, fields, files) => { form.parse(req, async (err, fields, files) => {

View File

@ -0,0 +1,22 @@
import { router, error, checkAuth, clients, getTaskId } 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 tokenUser = await checkAuth(req, res);
if (!tokenUser) return;
const taskId = getTaskId(req);
if (!taskId) {
res.end(error('task-id is required'));
return;
}
// 将客户端连接推送到 clients 数组
clients.set(taskId, { client: res, tokenUser });
// 移除客户端连接
req.on('close', () => {
clients.delete(taskId);
});
});

View File

@ -1,24 +1,25 @@
import { User } from '@/models/user.ts'; import { User } from '@/models/user.ts';
import http from 'http'; import http from 'http';
import cookie from 'cookie';
export const error = (msg: string, code = 500) => { export const error = (msg: string, code = 500) => {
return JSON.stringify({ code, message: msg }); return JSON.stringify({ code, message: msg });
}; };
export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse) => { export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse) => {
let token = ''; let token = (req.headers?.['authorization'] as string) || '';
const authroization = req.headers?.['authorization'] as string;
const url = new URL(req.url || '', 'http://localhost'); const url = new URL(req.url || '', 'http://localhost');
const resNoPermission = () => { const resNoPermission = () => {
res.statusCode = 401; res.statusCode = 401;
res.end(error('Invalid authorization')); res.end(error('Invalid authorization'));
return { tokenUser: null, token: null }; return { tokenUser: null, token: null };
}; };
if (authroization) { if (!token) {
// return resNoPermission();
token = authroization.split(' ')[1];
} else if (url.searchParams.get('token')) {
token = url.searchParams.get('token') || ''; token = url.searchParams.get('token') || '';
} else { }
if (!token) {
const parsedCookies = cookie.parse(req.headers.cookie || '');
token = parsedCookies.token || '';
}
if (!token) {
return resNoPermission(); return resNoPermission();
} }
let tokenUser; let tokenUser;

View File

@ -1,3 +1,31 @@
import { SimpleRouter } from '@kevisual/router/simple'; import { router } from '@/app.ts';
import http from 'http';
import { useContextKey } from '@kevisual/use-config/context'; import { useContextKey } from '@kevisual/use-config/context';
export const router = useContextKey('router', () => new SimpleRouter()); import { checkAuth, error } from './middleware/auth.ts';
export { router, checkAuth, error };
/**
*
*/
const eventClientsInit = () => {
const clients = new Map<string, { client?: http.ServerResponse; [key: string]: any }>();
return clients;
};
export const clients = useContextKey('event-clients', () => eventClientsInit());
/**
* task-id
* @param req
* @returns
*/
export const getTaskId = (req: http.IncomingMessage) => {
return req.headers['task-id'] as string;
};
/**
*
* @param req
* @param data
*/
export const writeEvents = (req: http.IncomingMessage, data: any) => {
const taskId = getTaskId(req);
taskId && clients.get(taskId)?.client?.write?.(`${JSON.stringify(data)}\n`);
};

View File

@ -9,7 +9,7 @@ import { bucketName } from '@/modules/minio.ts';
import { getContentType } from '@/utils/get-content-type.ts'; import { getContentType } from '@/utils/get-content-type.ts';
import { User } from '@/models/user.ts'; import { User } from '@/models/user.ts';
import { getContainerById } from '@/routes/container/module/get-container-file.ts'; import { getContainerById } from '@/routes/container/module/get-container-file.ts';
import { router } from './router.ts'; import { router, error, checkAuth, clients, writeEvents } from './router.ts';
import './index.ts'; import './index.ts';
const filePath = useFileStore('upload', { needExists: true }); const filePath = useFileStore('upload', { needExists: true });
@ -21,29 +21,6 @@ const cacheFilePath = useFileStore('cache-file', { needExists: true });
// -F "description=This is a test upload" \ // -F "description=This is a test upload" \
// -F "username=testuser" // -F "username=testuser"
export const clients = new Map<string, { client?: http.ServerResponse; [key: string]: any }>();
const error = (msg: string, code = 500) => {
return JSON.stringify({ code, message: msg });
};
const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse) => {
const authroization = req.headers?.['authorization'] as string;
if (!authroization) {
res.statusCode = 401;
res.end(error('Invalid authorization'));
return { tokenUser: null, token: null };
}
const token = authroization.split(' ')[1];
let tokenUser;
try {
tokenUser = await User.verifyToken(token);
} catch (e) {
res.statusCode = 401;
res.end(error('Invalid token'));
return { tokenUser: null, token: null };
}
return { tokenUser, token };
};
router.get('/api/app/upload', async (req, res) => { router.get('/api/app/upload', async (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' }); res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Upload API is ready'); res.end('Upload API is ready');
@ -59,6 +36,16 @@ router.post('/api/upload', async (req, res) => {
uploadDir: filePath, // 上传文件存储目录 uploadDir: filePath, // 上传文件存储目录
allowEmptyFiles: true, // 允许空文件 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) => { form.parse(req, async (err, fields, files) => {
if (err) { if (err) {
@ -101,7 +88,6 @@ router.post('/api/app/upload', async (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
const { tokenUser, token } = await checkAuth(req, res); const { tokenUser, token } = await checkAuth(req, res);
if (!tokenUser) return; if (!tokenUser) return;
//
// 使用 formidable 解析 multipart/form-data // 使用 formidable 解析 multipart/form-data
const form = new IncomingForm({ const form = new IncomingForm({
multiples: true, // 支持多文件上传 multiples: true, // 支持多文件上传
@ -119,8 +105,7 @@ router.post('/api/app/upload', async (req, res) => {
progress: progress.toFixed(2), progress: progress.toFixed(2),
message: `Upload progress: ${progress.toFixed(2)}%`, message: `Upload progress: ${progress.toFixed(2)}%`,
}; };
// 向所有连接的客户端推送进度信息 writeEvents(req, data);
clients.forEach((client) => client.write(`${JSON.stringify(data)}\n`));
}); });
// 解析上传的文件 // 解析上传的文件
form.parse(req, async (err, fields, files) => { form.parse(req, async (err, fields, files) => {
@ -225,24 +210,7 @@ router.post('/api/app/upload', async (req, res) => {
res.end(JSON.stringify(data)); res.end(JSON.stringify(data));
}); });
}); });
router.get('/api/events', async (req, res) => {
if (req.url === '/api/events') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const tokenUser = await checkAuth(req, res);
if (!tokenUser) return;
const taskId = req.headers['task-id'] as string;
// 将客户端连接推送到 clients 数组
clients.set(taskId, { client: res, tokenUser });
// 移除客户端连接
req.on('close', () => {
clients.delete(taskId);
});
}
});
router.get('/api/container/file/:id', async (req, res) => { router.get('/api/container/file/:id', async (req, res) => {
const id = req.params.id; const id = req.params.id;
if (!id) { if (!id) {

View File

@ -0,0 +1,2 @@
import './list.ts';
import './upload-config.ts';

21
src/routes/config/list.ts Normal file
View File

@ -0,0 +1,21 @@
import { app } from '@/app.ts';
import { ConfigModel } from './models/model.ts';
app
.route({
path: 'config',
key: 'list',
middleware: ['auth'],
})
.define(async (ctx) => {
const { id } = ctx.state.tokenUser;
const config = await ConfigModel.findAll({
where: {
uid: id,
},
});
ctx.body = {
list: config,
};
})
.addTo(app);

View File

@ -0,0 +1,140 @@
import { useContextKey } from '@kevisual/use-config/context';
import { sequelize } from '../../../modules/sequelize.ts';
import { DataTypes, Model } from 'sequelize';
export interface ConfigData {
key?: string;
version?: string;
}
export type Config = Partial<InstanceType<typeof ConfigModel>>;
/**
*
*/
export class ConfigModel extends Model {
declare id: string;
declare title: string;
declare description: string;
declare tags: string[];
declare key: string;
declare data: ConfigData; // files
declare uid: string;
/**
*
* @param key key
* @param opts
* @param opts.uid id
* @param opts.defaultData
* @returns
*/
static async getConfig(key: string, opts: { uid: string; defaultData?: any }) {
const [config, isNew] = await ConfigModel.findOrCreate({
where: { key, uid: opts.uid },
defaults: {
key,
title: key,
uid: opts.uid,
data: opts?.defaultData || {},
},
});
return {
config: config,
isNew,
};
}
static async setConfig(key: string, opts: { uid: string; data: any }) {
let config = await ConfigModel.findOne({
where: { key, uid: opts.uid },
});
if (config) {
config.data = { ...config.data, ...opts.data };
await config.save();
} else {
config = await ConfigModel.create({
title: key,
key,
uid: opts.uid,
data: opts.data,
});
}
return config;
}
/**
*
* @param key key
* @param opts
* @param opts.uid id
* @returns
*/
static async getUploadConfig(opts: { uid: string }) {
const defaultConfig = {
key: 'upload',
type: 'upload',
version: '1.0.0',
};
const config = await ConfigModel.getConfig('upload', {
uid: opts.uid,
defaultData: defaultConfig,
});
const data = config.config.data;
const prefix = `/${data.key}/${data.version}`;
return {
config: config.config,
isNew: config.isNew,
prefix,
};
}
static async setUploadConfig(opts: { uid: string; data: any }) {
const config = await ConfigModel.setConfig('upload', {
uid: opts.uid,
data: opts.data,
});
return config;
}
}
ConfigModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
comment: 'id',
},
title: {
type: DataTypes.TEXT,
defaultValue: '',
},
key: {
type: DataTypes.TEXT,
defaultValue: '',
},
description: {
type: DataTypes.TEXT,
defaultValue: '',
},
tags: {
type: DataTypes.JSONB,
defaultValue: [],
},
data: {
type: DataTypes.JSONB,
defaultValue: {},
},
uid: {
type: DataTypes.UUID,
allowNull: true,
},
},
{
sequelize,
tableName: 'kv_config',
paranoid: true,
},
);
ConfigModel.sync({ alter: true, logging: false }).catch((e) => {
console.error('ConfigModel sync', e);
});
useContextKey('ConfigModel', () => ConfigModel);

View File

@ -0,0 +1,34 @@
import { app } from '../../app.ts';
import { ConfigModel } from './models/model.ts';
app
.route({
path: 'config',
key: 'getUploadConfig',
middleware: ['auth'],
})
.define(async (ctx) => {
const { id } = ctx.state.tokenUser;
const config = await ConfigModel.getUploadConfig({
uid: id,
});
ctx.body = config;
})
.addTo(app);
app
.route({
path: 'config',
key: 'setUploadConfig',
middleware: ['auth'],
})
.define(async (ctx) => {
const { id } = ctx.state.tokenUser;
const data = ctx.query.data || {};
const config = await ConfigModel.setUploadConfig({
uid: id,
data,
});
ctx.body = config;
})
.addTo(app);

View File

@ -15,3 +15,5 @@ import './file/index.ts';
// import './packages/index.ts'; // import './packages/index.ts';
import './micro-app/index.ts'; import './micro-app/index.ts';
import './config/index.ts';

View File

@ -1,6 +1,5 @@
import { ResourceData, ResourceModel } from './models/index.ts'; import { ResourceModel } from './models/index.ts';
import { app } from '../../app.ts'; import { app } from '../../app.ts';
import { CustomError } from '@kevisual/router';
app app
.route({ .route({
@ -29,11 +28,11 @@ app
.define(async (ctx) => { .define(async (ctx) => {
const id = ctx.query.id; const id = ctx.query.id;
if (!id) { if (!id) {
throw new CustomError('id is required'); ctx.throw('id is required');
} }
const rm = await ResourceModel.findByPk(id); const rm = await ResourceModel.findByPk(id);
if (!rm) { if (!rm) {
throw new CustomError('resource not found'); ctx.throw('resource not found');
} }
ctx.body = rm; ctx.body = rm;
return ctx; return ctx;
@ -61,15 +60,16 @@ app
.route({ .route({
path: 'resource', path: 'resource',
key: 'delete', key: 'delete',
middleware: ['auth'],
}) })
.define(async (ctx) => { .define(async (ctx) => {
const id = ctx.query.id; const id = ctx.query.id;
if (!id) { if (!id) {
throw new CustomError('id is required'); ctx.throw('id is required');
} }
const resource = await ResourceModel.findByPk(id); const resource = await ResourceModel.findByPk(id);
if (!resource) { if (!resource) {
throw new CustomError('resource not found'); ctx.throw('resource not found');
} }
await resource.destroy(); await resource.destroy();
ctx.body = 'success'; ctx.body = 'success';