354 lines
8.5 KiB
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)
|
|
}
|