chore: src/modules/plugin/service/info.ts
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
import {
|
||||
BaseService,
|
||||
CoolCommException,
|
||||
CoolEventManager,
|
||||
} from '@cool-midway/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Equal, In, Not, Repository } from 'typeorm';
|
||||
import { PluginInfoEntity } from '../entity/info';
|
||||
import {
|
||||
App,
|
||||
Config,
|
||||
ILogger,
|
||||
IMidwayApplication,
|
||||
IMidwayContext,
|
||||
Inject,
|
||||
InjectClient,
|
||||
Logger,
|
||||
Provide,
|
||||
} from '@midwayjs/core';
|
||||
import * as _ from 'lodash';
|
||||
import { PluginInfo } from '../interface';
|
||||
import { PluginCenterService } from './center';
|
||||
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
|
||||
import {
|
||||
GLOBAL_EVENT_PLUGIN_INIT,
|
||||
GLOBAL_EVENT_PLUGIN_REMOVE,
|
||||
} from '../event/init';
|
||||
import { PluginMap, AnyString } from '../../../../typings/plugin';
|
||||
import { PluginTypesService } from './types';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { pPluginPath } from '../../../comm/path';
|
||||
/**
|
||||
* 插件信息
|
||||
*/
|
||||
@Provide()
|
||||
export class PluginService extends BaseService {
|
||||
@InjectEntityModel(PluginInfoEntity)
|
||||
pluginInfoEntity: Repository<PluginInfoEntity>;
|
||||
|
||||
@Inject()
|
||||
ctx: IMidwayContext;
|
||||
|
||||
@App()
|
||||
app: IMidwayApplication;
|
||||
|
||||
@Inject()
|
||||
pluginCenterService: PluginCenterService;
|
||||
|
||||
@Config('module.plugin.hooks')
|
||||
hooksConfig;
|
||||
|
||||
@InjectClient(CachingFactory, 'default')
|
||||
midwayCache: MidwayCache;
|
||||
|
||||
@Inject()
|
||||
coolEventManager: CoolEventManager;
|
||||
|
||||
@Inject()
|
||||
pluginTypesService: PluginTypesService;
|
||||
|
||||
@Logger()
|
||||
logger: ILogger;
|
||||
|
||||
/**
|
||||
* 新增或更新
|
||||
* @param param
|
||||
* @param type
|
||||
*/
|
||||
async addOrUpdate(param: any, type?: 'add' | 'update') {
|
||||
await super.addOrUpdate(param, type);
|
||||
const info = await this.pluginInfoEntity
|
||||
.createQueryBuilder('a')
|
||||
.select(['a.id', 'a.keyName', 'a.status', 'a.hook'])
|
||||
.where({
|
||||
id: Equal(param.id),
|
||||
})
|
||||
.getOne();
|
||||
if (info.status == 1) {
|
||||
await this.reInit(info.keyName);
|
||||
} else {
|
||||
await this.remove(info.keyName, !!info.hook);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新初始化插件
|
||||
*/
|
||||
async reInit(keyName: string) {
|
||||
// 多进程发送全局事件,pm2下生效,本地开发则通过普通事件
|
||||
this.coolEventManager.globalEmit(GLOBAL_EVENT_PLUGIN_INIT, false, keyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除插件
|
||||
* @param keyName
|
||||
* @param isHook
|
||||
*/
|
||||
async remove(keyName: string, isHook = false) {
|
||||
// 多进程发送全局事件,pm2下生效
|
||||
this.coolEventManager.globalEmit(
|
||||
GLOBAL_EVENT_PLUGIN_REMOVE,
|
||||
false,
|
||||
keyName,
|
||||
isHook
|
||||
);
|
||||
this.pluginTypesService.deleteDtsFile(keyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除不经过回收站
|
||||
* @param ids
|
||||
*/
|
||||
async delete(ids: any) {
|
||||
const list = await this.pluginInfoEntity.findBy({ id: In(ids) });
|
||||
for (const item of list) {
|
||||
await this.remove(item.keyName, !!item.hook);
|
||||
// 删除文件
|
||||
await this.deleteData(item.keyName);
|
||||
}
|
||||
await this.pluginInfoEntity.delete(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param param
|
||||
*/
|
||||
async update(param: any) {
|
||||
const old = await this.pluginInfoEntity.findOne({
|
||||
where: { id: param.id },
|
||||
select: ['id', 'status', 'hook'],
|
||||
});
|
||||
// 启用插件,禁用同名插件
|
||||
if (old.hook && param.status == 1 && old.status != param.status) {
|
||||
await this.pluginInfoEntity.update(
|
||||
{ hook: old.hook, status: 1, id: Not(old.id) },
|
||||
{ status: 0 }
|
||||
);
|
||||
}
|
||||
await super.update(param);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得插件配置
|
||||
* @param key
|
||||
*/
|
||||
async getConfig(key: string) {
|
||||
return this.pluginCenterService.pluginInfos.get(key)?.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用插件
|
||||
* @param key 插件key
|
||||
* @param method 方法
|
||||
* @param params 参数
|
||||
* @returns
|
||||
*/
|
||||
async invoke<K extends keyof PluginMap>(
|
||||
key: K | AnyString,
|
||||
method: string,
|
||||
...params
|
||||
) {
|
||||
// 实例
|
||||
const instance: any = await this.getInstance(key);
|
||||
return await instance[method](...params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得插件实例
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
async getInstance<K extends keyof PluginMap>(
|
||||
key: K | AnyString
|
||||
): Promise<K extends keyof PluginMap ? PluginMap[K] : any> {
|
||||
const check = await this.checkStatus(key);
|
||||
if (!check) throw new CoolCommException(`插件[${key}]不存在或已禁用`);
|
||||
let instance;
|
||||
const pluginInfo = this.pluginCenterService.pluginInfos.get(key);
|
||||
if (pluginInfo.singleton) {
|
||||
instance = this.pluginCenterService.plugins.get(key);
|
||||
} else {
|
||||
instance = new (await this.pluginCenterService.plugins.get(key))();
|
||||
await instance.init(pluginInfo, this.ctx, this.app, {
|
||||
cache: this.midwayCache,
|
||||
pluginService: this,
|
||||
});
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查状态
|
||||
* @param key
|
||||
*/
|
||||
async checkStatus(key: string) {
|
||||
if (Object.keys(this.hooksConfig).includes(key)) {
|
||||
return true;
|
||||
}
|
||||
const info = await this.pluginInfoEntity
|
||||
.createQueryBuilder('a')
|
||||
.select(['a.id', 'a.status'])
|
||||
.where({ status: 1, keyName: Equal(key) })
|
||||
.getOne();
|
||||
|
||||
return !!info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查
|
||||
* @param filePath
|
||||
*/
|
||||
async check(filePath: string) {
|
||||
let data;
|
||||
try {
|
||||
data = await this.data(filePath);
|
||||
} catch (e) {
|
||||
return {
|
||||
type: 0,
|
||||
message: `插件信息不完整,请检查${data?.errorData || ''}`,
|
||||
};
|
||||
}
|
||||
const check = await this.pluginInfoEntity.findOne({
|
||||
where: { keyName: Equal(data.pluginJson.key) },
|
||||
select: ['id', 'hook', 'status'],
|
||||
});
|
||||
if (check && !check.hook) {
|
||||
return {
|
||||
type: 1,
|
||||
message: '插件已存在,继续安装将覆盖',
|
||||
};
|
||||
}
|
||||
if (check && check.hook && check.status == 1) {
|
||||
return {
|
||||
type: 2,
|
||||
message:
|
||||
'已存在同名Hook插件,你可以继续安装,但是多个相同的Hook插件只能同时开启一个',
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 3,
|
||||
message: '检查通过',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得插件数据
|
||||
* @param filePath
|
||||
*/
|
||||
async data(filePath: string): Promise<{
|
||||
pluginJson: any;
|
||||
readme: string;
|
||||
logo: string;
|
||||
content: string;
|
||||
tsContent: string;
|
||||
errorData: string;
|
||||
}> {
|
||||
const AdmZip = require('adm-zip');
|
||||
const zip = new AdmZip(filePath);
|
||||
const files = zip.getEntries();
|
||||
let errorData;
|
||||
let pluginJson: PluginInfo,
|
||||
readme: string,
|
||||
logo: string,
|
||||
content: string,
|
||||
tsContent: string;
|
||||
|
||||
try {
|
||||
// 通用方法获取文件内容
|
||||
const getFileContent = (
|
||||
entryName: string,
|
||||
encoding: 'utf-8' | 'base64' = 'utf-8'
|
||||
) => {
|
||||
const file = _.find(files, { entryName });
|
||||
if (!file) {
|
||||
throw new Error(`File ${entryName} not found`);
|
||||
}
|
||||
return file?.getData()?.toString(encoding);
|
||||
};
|
||||
|
||||
errorData = 'plugin.json';
|
||||
pluginJson = JSON.parse(getFileContent('plugin.json'));
|
||||
|
||||
errorData = 'readme';
|
||||
readme = getFileContent(pluginJson.readme);
|
||||
|
||||
errorData = 'logo';
|
||||
logo = getFileContent(pluginJson.logo, 'base64');
|
||||
|
||||
errorData = 'content';
|
||||
content = getFileContent('src/index.js');
|
||||
|
||||
tsContent = getFileContent('source/index.ts');
|
||||
} catch (e) {
|
||||
throw new CoolCommException('插件信息不完整');
|
||||
}
|
||||
return {
|
||||
pluginJson,
|
||||
readme,
|
||||
logo,
|
||||
content,
|
||||
tsContent,
|
||||
errorData,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
* @param file 文件
|
||||
* @param force 是否强制安装
|
||||
*/
|
||||
async install(filePath: string, force = false) {
|
||||
const forceBool = typeof force === 'string' ? force === 'true' : force;
|
||||
const checkResult = await this.check(filePath);
|
||||
if (checkResult.type != 3 && !forceBool) {
|
||||
return checkResult;
|
||||
}
|
||||
const { pluginJson, readme, logo, content, tsContent } = await this.data(
|
||||
filePath
|
||||
);
|
||||
if (pluginJson.key == 'plugin') {
|
||||
throw new CoolCommException('插件key不能为plugin,请更换其他key');
|
||||
}
|
||||
const check = await this.pluginInfoEntity.findOne({
|
||||
where: { keyName: Equal(pluginJson.key) },
|
||||
select: ['id', 'status', 'config'],
|
||||
});
|
||||
const data = {
|
||||
name: pluginJson.name,
|
||||
keyName: pluginJson.key,
|
||||
version: pluginJson.version,
|
||||
author: pluginJson.author,
|
||||
hook: pluginJson.hook,
|
||||
readme,
|
||||
logo,
|
||||
content: {
|
||||
type: 'comm',
|
||||
data: content,
|
||||
},
|
||||
tsContent: {
|
||||
type: 'ts',
|
||||
data: tsContent,
|
||||
},
|
||||
description: pluginJson.description,
|
||||
pluginJson,
|
||||
config: pluginJson.config,
|
||||
status: 1,
|
||||
} as PluginInfoEntity;
|
||||
// 存在同名插件,更新,保留配置
|
||||
if (check) {
|
||||
await this.pluginInfoEntity.update(check.id, {
|
||||
...data,
|
||||
status: check.status,
|
||||
config: {
|
||||
...pluginJson.config,
|
||||
...check.config,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 全新安装
|
||||
await this.pluginInfoEntity.insert(data);
|
||||
}
|
||||
// 保存插件内容
|
||||
await this.saveData(
|
||||
{
|
||||
content: {
|
||||
type: 'comm',
|
||||
data: content,
|
||||
},
|
||||
tsContent: {
|
||||
type: 'ts',
|
||||
data: tsContent,
|
||||
},
|
||||
},
|
||||
pluginJson.key
|
||||
);
|
||||
this.pluginTypesService.generateDtsFile(pluginJson.key, tsContent);
|
||||
// 初始化插件
|
||||
await this.reInit(pluginJson.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将插件内容保存到文件
|
||||
* @param content 内容
|
||||
* @param keyName 插件key
|
||||
*/
|
||||
async saveData(
|
||||
data: {
|
||||
content: {
|
||||
type: 'comm' | 'module';
|
||||
data: string;
|
||||
};
|
||||
tsContent: {
|
||||
type: 'ts';
|
||||
data: string;
|
||||
};
|
||||
},
|
||||
keyName: string
|
||||
) {
|
||||
const filePath = this.pluginPath(keyName);
|
||||
// 确保目录存在
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
// 写入文件,如果存在则覆盖
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 0), { flag: 'w' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得插件数据
|
||||
* @param keyName
|
||||
* @returns
|
||||
*/
|
||||
async getData(keyName: string): Promise<{
|
||||
content: {
|
||||
type: 'comm' | 'module';
|
||||
data: string;
|
||||
};
|
||||
tsContent: {
|
||||
type: 'ts';
|
||||
data: string;
|
||||
};
|
||||
}> {
|
||||
const filePath = this.pluginPath(keyName);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
// 尝试从数据库中获取
|
||||
const info = await this.pluginInfoEntity.findOne({
|
||||
where: { keyName: Equal(keyName) },
|
||||
select: ['content', 'tsContent'],
|
||||
});
|
||||
if (info) {
|
||||
// 保存插件到文件
|
||||
this.saveData(
|
||||
{
|
||||
content: info.content,
|
||||
tsContent: info.tsContent,
|
||||
},
|
||||
keyName
|
||||
);
|
||||
return {
|
||||
content: info.content,
|
||||
tsContent: info.tsContent,
|
||||
};
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`插件[${keyName}]文件不存在,请卸载后重新安装: ${filePath}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return JSON.parse(await fs.promises.readFile(filePath, 'utf-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件
|
||||
* @param keyName
|
||||
*/
|
||||
async deleteData(keyName: string) {
|
||||
const filePath = this.pluginPath(keyName);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得插件路径
|
||||
* @param keyName
|
||||
* @returns
|
||||
*/
|
||||
pluginPath(keyName: string) {
|
||||
return path.join(pPluginPath(), `${keyName}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user