kra/internal/biz/system/auto_code_plugin.go

354 lines
8.5 KiB
Go

package system
import (
"bytes"
"context"
"go/parser"
"go/printer"
"go/token"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"github.com/go-kratos/kratos/v2/log"
"github.com/mholt/archives"
cp "github.com/otiai10/copy"
"github.com/pkg/errors"
"kra/internal/conf"
"kra/pkg/utils"
)
// AutoCodePluginRepo 插件仓储接口
type AutoCodePluginRepo interface {
GetMenusByIDs(ctx context.Context, ids []uint) ([]SysBaseMenu, error)
GetApisByIDs(ctx context.Context, ids []uint) ([]SysApi, error)
}
// AutoCodePluginUsecase 插件用例
type AutoCodePluginUsecase struct {
repo AutoCodePluginRepo
config *conf.AutoCodeConfig
log *log.Helper
}
// NewAutoCodePluginUsecase 创建插件用例
func NewAutoCodePluginUsecase(
repo AutoCodePluginRepo,
config *conf.AutoCodeConfig,
logger log.Logger,
) *AutoCodePluginUsecase {
return &AutoCodePluginUsecase{
repo: repo,
config: config,
log: log.NewHelper(logger),
}
}
// Install 插件安装
func (uc *AutoCodePluginUsecase) Install(file *multipart.FileHeader) (web, server int, err error) {
const PLUGINPATH = "./kra-plug-temp/"
defer os.RemoveAll(PLUGINPATH)
if _, err = os.Stat(PLUGINPATH); os.IsNotExist(err) {
os.Mkdir(PLUGINPATH, os.ModePerm)
}
src, err := file.Open()
if err != nil {
return -1, -1, err
}
defer src.Close()
out, err := os.Create(PLUGINPATH + file.Filename)
if err != nil {
return -1, -1, err
}
defer out.Close()
_, err = io.Copy(out, src)
if err != nil {
return -1, -1, err
}
paths, err := utils.Unzip(PLUGINPATH+file.Filename, PLUGINPATH)
if err != nil {
return -1, -1, err
}
paths = uc.filterFile(paths)
var webIndex = -1
var serverIndex = -1
webPlugin := ""
serverPlugin := ""
for i := range paths {
paths[i] = filepath.ToSlash(paths[i])
pathArr := strings.Split(paths[i], "/")
ln := len(pathArr)
if ln < 4 {
continue
}
if pathArr[2]+"/"+pathArr[3] == `server/plugin` && len(serverPlugin) == 0 {
serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
}
if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 {
webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
}
}
if len(serverPlugin) == 0 && len(webPlugin) == 0 {
uc.log.Error("非标准插件,请按照文档自动迁移使用")
return webIndex, serverIndex, errors.New("非标准插件,请按照文档自动迁移使用")
}
if len(serverPlugin) != 0 {
err = uc.installation(serverPlugin, uc.config.Server, uc.config.Server)
if err != nil {
return webIndex, serverIndex, err
}
}
if len(webPlugin) != 0 {
err = uc.installation(webPlugin, uc.config.Server, uc.config.Web)
if err != nil {
return webIndex, serverIndex, err
}
}
return 1, 1, err
}
// installation 安装插件
func (uc *AutoCodePluginUsecase) installation(path string, formPath string, toPath string) error {
arr := strings.Split(filepath.ToSlash(path), "/")
ln := len(arr)
if ln < 3 {
return errors.New("路径格式错误")
}
name := arr[ln-3]
form := filepath.Join(uc.config.Root, formPath, path)
to := filepath.Join(uc.config.Root, toPath, "plugin")
if _, err := os.Stat(to + name); err == nil {
uc.log.Error("已存在同名插件,请自行手动安装", "to", to)
return errors.New(toPath + "已存在同名插件,请自行手动安装")
}
return cp.Copy(form, to, cp.Options{Skip: uc.skipMacSpecialDocument})
}
// filterFile 过滤文件
func (uc *AutoCodePluginUsecase) filterFile(paths []string) []string {
np := make([]string, 0, len(paths))
for _, path := range paths {
if ok, _ := uc.skipMacSpecialDocument(nil, path, ""); ok {
continue
}
np = append(np, path)
}
return np
}
// skipMacSpecialDocument 跳过Mac特殊文件
func (uc *AutoCodePluginUsecase) skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) {
if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") {
return true, nil
}
return false, nil
}
// PubPlug 发布插件
func (uc *AutoCodePluginUsecase) PubPlug(plugName string) (zipPath string, err error) {
if plugName == "" {
return "", errors.New("插件名称不能为空")
}
// 防止路径穿越
plugName = filepath.Clean(plugName)
webPath := filepath.Join(uc.config.Root, uc.config.Web, "plugin", plugName)
serverPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", plugName)
// 判断目录是否存在
if _, err = os.Stat(webPath); err != nil {
return "", errors.New("web路径不存在")
}
if _, err = os.Stat(serverPath); err != nil {
return "", errors.New("server路径不存在")
}
fileName := plugName + ".zip"
// 创建zip文件
files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{
webPath: plugName + "/web/plugin/" + plugName,
serverPath: plugName + "/server/plugin/" + plugName,
})
if err != nil {
return "", err
}
out, err := os.Create(fileName)
if err != nil {
return "", err
}
defer out.Close()
format := archives.CompressedArchive{
Archival: archives.Zip{},
}
if err = format.Archive(context.Background(), out, files); err != nil {
return "", err
}
return filepath.Join(uc.config.Root, uc.config.Server, fileName), nil
}
// InitMenuRequest 初始化菜单请求
type InitMenuRequest struct {
PlugName string `json:"plugName"`
ParentMenu string `json:"parentMenu"`
Menus []uint `json:"menus"`
}
// InitMenu 初始化菜单
func (uc *AutoCodePluginUsecase) InitMenu(ctx context.Context, menuInfo InitMenuRequest) error {
menuPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go")
src, err := os.ReadFile(menuPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
if err != nil {
return err
}
arrayAst := utils.FindArray(astFile, "model", "SysBaseMenu")
if arrayAst == nil {
return errors.New("未找到菜单数组")
}
// 查询菜单
menus, err := uc.repo.GetMenusByIDs(ctx, menuInfo.Menus)
if err != nil {
return err
}
// 添加父菜单
parentMenu := SysBaseMenu{
ParentId: 0,
Path: menuInfo.PlugName + "Menu",
Name: menuInfo.PlugName + "Menu",
Hidden: false,
Component: "view/routerHolder.vue",
Sort: 0,
Meta: Meta{
Title: menuInfo.ParentMenu,
Icon: "school",
},
}
allMenus := append([]SysBaseMenu{parentMenu}, menus...)
// 转换为utils.SysBaseMenu类型
utilsMenus := make([]utils.SysBaseMenu, len(allMenus))
for i, m := range allMenus {
utilsMenus[i] = utils.SysBaseMenu{
ParentId: m.ParentId,
Path: m.Path,
Name: m.Name,
Hidden: m.Hidden,
Component: m.Component,
Sort: m.Sort,
Title: m.Meta.Title,
Icon: m.Meta.Icon,
}
// 转换参数
for _, p := range m.Parameters {
utilsMenus[i].Parameters = append(utilsMenus[i].Parameters, utils.SysBaseMenuParameter{
Type: p.Type,
Key: p.Key,
Value: p.Value,
})
}
// 转换按钮
for _, b := range m.MenuBtn {
utilsMenus[i].MenuBtn = append(utilsMenus[i].MenuBtn, utils.SysBaseMenuBtn{
Name: b.Name,
Desc: b.Desc,
})
}
}
menuExpr := utils.CreateMenuStructAst(utilsMenus)
arrayAst.Elts = *menuExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
return os.WriteFile(menuPath, bf.Bytes(), 0666)
}
// InitApiRequest 初始化API请求
type InitApiRequest struct {
PlugName string `json:"plugName"`
APIs []uint `json:"apis"`
}
// InitAPI 初始化API
func (uc *AutoCodePluginUsecase) InitAPI(ctx context.Context, apiInfo InitApiRequest) error {
apiPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", apiInfo.PlugName, "initialize", "api.go")
src, err := os.ReadFile(apiPath)
if err != nil {
return err
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
if err != nil {
return err
}
arrayAst := utils.FindArray(astFile, "model", "SysApi")
if arrayAst == nil {
return errors.New("未找到API数组")
}
// 查询API
apis, err := uc.repo.GetApisByIDs(ctx, apiInfo.APIs)
if err != nil {
return err
}
// 转换为utils.SysApi类型
utilsApis := make([]utils.SysApi, len(apis))
for i, a := range apis {
utilsApis[i] = utils.SysApi{
Path: a.Path,
Description: a.Description,
ApiGroup: a.ApiGroup,
Method: a.Method,
}
}
apisExpr := utils.CreateApiStructAst(utilsApis)
arrayAst.Elts = *apisExpr
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
return os.WriteFile(apiPath, bf.Bytes(), 0666)
}