[add] studio api server

This commit is contained in:
Max 2022-10-13 19:51:07 +08:00
parent 3f0869c11e
commit d74f74b5d9
7 changed files with 511 additions and 20 deletions

View file

@ -2,20 +2,26 @@ package config
// Config 象传应用引擎配置
type Config struct {
Mode string `json:"mode,omitempty" env:"YAO_ENV" envDefault:"production"` // 象传引擎启动模式 production/development
Root string `json:"root,omitempty" env:"YAO_ROOT" envDefault:"."` // 应用根目录
DataRoot string `json:"data_root,omitempty" env:"YAO_DATA_ROOT" envDefault:""` // DATA PATH
Host string `json:"host,omitempty" env:"YAO_HOST" envDefault:"0.0.0.0"` // 服务监听地址
Port int `json:"port,omitempty" env:"YAO_PORT" envDefault:"5099"` // 服务监听端口
Cert string `json:"cert,omitempty" env:"YAO_CERT"` // HTTPS 证书文件地址
Key string `json:"key,omitempty" env:"YAO_KEY"` // HTTPS 证书密钥地址
Log string `json:"log,omitempty" env:"YAO_LOG"` // 服务日志地址
LogMode string `json:"log_mode,omitempty" env:"YAO_LOG_MODE" envDefault:"TEXT"` // 服务日志模式 JSON|TEXT
// Session string `json:"session,omitempty" env:"YAO_SESSION" envDefault:"memory"` // 用户会话模式 memory|redis|database
JWTSecret string `json:"jwt_secret,omitempty" env:"YAO_JWT_SECRET"` // JWT 密钥
DB DBConfig `json:"db,omitempty"` // 数据库配置
AllowFrom []string `json:"allowfrom,omitempty" envSeparator:"|" env:"YAO_ALLOW_FROM" ` // Domain list the separator is |
Session SessionConfig `json:"session,omitempty"`
Mode string `json:"mode,omitempty" env:"YAO_ENV" envDefault:"production"` // 象传引擎启动模式 production/development
Root string `json:"root,omitempty" env:"YAO_ROOT" envDefault:"."` // 应用根目录
DataRoot string `json:"data_root,omitempty" env:"YAO_DATA_ROOT" envDefault:""` // DATA PATH
Host string `json:"host,omitempty" env:"YAO_HOST" envDefault:"0.0.0.0"` // 服务监听地址
Port int `json:"port,omitempty" env:"YAO_PORT" envDefault:"5099"` // 服务监听端口
Cert string `json:"cert,omitempty" env:"YAO_CERT"` // HTTPS 证书文件地址
Key string `json:"key,omitempty" env:"YAO_KEY"` // HTTPS 证书密钥地址
Log string `json:"log,omitempty" env:"YAO_LOG"` // 服务日志地址
LogMode string `json:"log_mode,omitempty" env:"YAO_LOG_MODE" envDefault:"TEXT"` // 服务日志模式 JSON|TEXT
JWTSecret string `json:"jwt_secret,omitempty" env:"YAO_JWT_SECRET"` // JWT 密钥
DB DBConfig `json:"db,omitempty"` // 数据库配置
AllowFrom []string `json:"allowfrom,omitempty" envSeparator:"|" env:"YAO_ALLOW_FROM"` // Domain list the separator is |
Session SessionConfig `json:"session,omitempty"` // Session Config
Studio StudioConfig `json:"studio,omitempty"` // Studio config
}
// StudioConfig the studio config
type StudioConfig struct {
Port int `json:"studio_port,omitempty" env:"YAO_STUDIO_PORT" envDefault:"5077"` // Studio port
Secret int `json:"studio_secret,omitempty" env:"YAO_STUDIO_SECRET"` // Studio Secret, if does not set, auto-generate a secret
}
// DBConfig 数据库配置

41
studio/middleware.go Normal file
View file

@ -0,0 +1,41 @@
package studio
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/xun"
)
func hdRecovered(c *gin.Context, recovered interface{}) {
var code = http.StatusInternalServerError
if err, ok := recovered.(string); ok {
c.JSON(code, xun.R{
"code": code,
"message": fmt.Sprintf("%s", err),
})
} else if err, ok := recovered.(exception.Exception); ok {
code = err.Code
c.JSON(code, xun.R{
"code": code,
"message": err.Message,
})
} else if err, ok := recovered.(*exception.Exception); ok {
code = err.Code
c.JSON(code, xun.R{
"code": code,
"message": err.Message,
})
} else {
c.JSON(code, xun.R{
"code": code,
"message": fmt.Sprintf("%v", recovered),
})
}
c.AbortWithStatus(code)
}

183
studio/router.go Normal file
View file

@ -0,0 +1,183 @@
package studio
import (
"fmt"
"io"
"os"
"strings"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou"
)
// Serve start the api server
func setRouter(router *gin.Engine) {
router.Use(gin.CustomRecovery(hdRecovered))
// DSL ReadDir, ReadFile
router.GET("/dsl/:method", func(c *gin.Context) {
method := strings.ToLower(c.Param("method"))
switch method {
case "readfile":
name := c.Query("name")
if name == "" {
throw(c, 400, "file name is required")
return
}
data, err := dfs.ReadFile(name)
if err != nil {
throw(c, 500, err.Error())
return
}
res := map[string]interface{}{}
err = jsoniter.Unmarshal(data, &res)
if err != nil {
throw(c, 500, err.Error())
return
}
c.JSON(200, res)
c.Done()
break
case "readdir":
name := c.Query("name")
if name == "" {
throw(c, 400, "dir name is required")
return
}
recursive := false
if c.Query("recursive") == "1" || strings.ToLower(c.Query("recursive")) == "true" {
recursive = true
}
data, err := dfs.ReadDir(name, recursive)
if err != nil {
throw(c, 500, err.Error())
return
}
c.JSON(200, data)
c.Done()
break
}
})
// DSL WriteFile, Mkdir, MkdirAll ...
router.POST("/dsl/:method", func(c *gin.Context) {
method := strings.ToLower(c.Param("method"))
switch method {
case "writefile":
name := c.Query("name")
if name == "" {
throw(c, 400, "dir name is required")
return
}
payload, err := io.ReadAll(c.Request.Body)
if err != nil {
throw(c, 500, err.Error())
return
}
if payload == nil || len(payload) == 0 {
throw(c, 500, "file content is required")
return
}
length, err := dfs.WriteFile(name, payload, 0644)
if err != nil {
throw(c, 500, err.Error())
return
}
c.JSON(200, length)
c.Done()
break
case "mkdir":
name := c.Query("name")
if name == "" {
throw(c, 400, "dir name is required")
return
}
err := dfs.Mkdir(name, int(os.ModePerm))
if err != nil {
throw(c, 500, err.Error())
return
}
c.Status(200)
c.Done()
break
case "mkdirall":
name := c.Query("name")
if name == "" {
throw(c, 400, "dir name is required")
return
}
err := dfs.MkdirAll(name, int(os.ModePerm))
if err != nil {
throw(c, 500, err.Error())
return
}
c.Status(200)
c.Done()
break
}
})
// Cloud Functions
router.POST("/service/:name", func(c *gin.Context) {
name := c.Param("name")
if name == "" {
throw(c, 400, "service name is required")
return
}
service := fmt.Sprintf("__yao.studio.%s", c.Param("name"))
payload, err := io.ReadAll(c.Request.Body)
if err != nil {
throw(c, 500, err.Error())
return
}
if payload == nil || len(payload) > 0 {
throw(c, 400, "file content is required")
return
}
var call cloudCall
err = jsoniter.Unmarshal(payload, &call)
if err != nil {
throw(c, 500, err.Error())
return
}
res, err := gou.Yao.Engine.Call(map[string]interface{}{}, service, call.Method, call.Args...)
if err != nil {
throw(c, 500, err.Error())
return
}
c.JSON(200, res)
c.Done()
})
}
func throw(c *gin.Context, code int, message string) {
c.JSON(code, map[string]interface{}{
"message": message,
"code": code,
})
c.Done()
}

View file

@ -1,3 +1,148 @@
package studio
// Yao Studio
import (
"fmt"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"github.com/gin-gonic/gin"
"github.com/yaoapp/gou"
"github.com/yaoapp/gou/fs"
"github.com/yaoapp/gou/fs/dsl"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/share"
)
var shutdownSignal = make(chan bool, 1)
var dfs fs.FileSystem
var scripts = map[string][]byte{}
type cloudCall struct {
Method string `json:"method"`
Args []interface{} `json:"args,omitempty"`
}
// Start start the studio api server
func Start(cfg config.Config) (err error) {
// recive interrupt signal
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
errCh := make(chan error, 1)
// Set router
router := gin.Default()
setRouter(router)
// Server setting
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Studio.Port)
srv := &http.Server{
Addr: addr,
Handler: router,
}
// Listen
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer func() {
log.Info("[Studio] %s Close Serve", addr)
err = srv.Close()
if err != nil {
log.Error("[Studio] Close Serve Error (%v)", err)
}
}()
// start serve
go func() {
log.Info("[Studio] Starting: %s", addr)
if err := srv.Serve(l); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
select {
case <-shutdownSignal:
log.Info("[Studio] %s Exit (Manual)", addr)
return err
case <-interrupt:
log.Info("[Studio] %s Exit (Interrupt) ", addr)
return err
case err := <-errCh:
log.Error("[Studio] %s Error (%v)", addr, err)
return err
}
}
// Stop stop the studio api server
func Stop() {
shutdownSignal <- true
}
// Load studio config
func Load(cfg config.Config) error {
err := loadDSL(cfg)
if err != nil {
return err
}
return loadScripts(cfg)
}
func loadDSL(cfg config.Config) error {
root, err := filepath.Abs(cfg.Root)
if err != nil {
return err
}
scriptRoot := filepath.Join(root, "scripts")
dataRoot := filepath.Join(root, "data")
dslDenyList := []string{scriptRoot, dataRoot}
dfs = dsl.New(root).DenyAbs(dslDenyList...)
return nil
}
func loadScripts(cfg config.Config) error {
root, err := filepath.Abs(cfg.Root)
if err != nil {
return err
}
studioRoot := filepath.Join(root, "studio")
return loadScriptFrom(studioRoot)
}
// Load script From dir
func loadScriptFrom(dir string) error {
if share.DirNotExists(dir) {
log.Warn("[Studio] Load %s does not exists", dir)
return nil
}
messages := []string{}
err := share.Walk(dir, ".js", func(root, filename string) {
name := fmt.Sprintf("__yao.studio.%s", share.SpecName(root, filename))
err := gou.Yao.Load(filename, name)
if err != nil {
messages = append(messages, err.Error())
}
})
if len(messages) > 0 {
return fmt.Errorf("[Studio] Load %s", strings.Join(messages, ";"))
}
return err
}

78
studio/studio_test.go Normal file
View file

@ -0,0 +1,78 @@
package studio
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/network"
)
func TestLoad(t *testing.T) {
err := Load(config.Conf)
if err != nil {
t.Fatal(err)
}
assert.NotNil(t, dfs)
res, err := gou.Yao.Engine.Call(map[string]interface{}{}, "__yao.studio.table", "Ping")
assert.Nil(t, err)
assert.Equal(t, "PONG", res)
}
func TestStartStop(t *testing.T) {
var err error
go func() { err = Start(config.Conf) }()
if err != nil {
t.Fatal(err)
}
time.Sleep(500 * time.Millisecond)
Stop()
time.Sleep(100 * time.Millisecond)
}
func TestStartStopError(t *testing.T) {
var err error
go func() { err = Start(config.Conf) }()
if err != nil {
t.Fatal(err)
}
time.Sleep(500 * time.Millisecond)
go func() { err = Start(config.Conf) }()
time.Sleep(100 * time.Millisecond)
assert.NotNil(t, err)
Stop()
time.Sleep(100 * time.Millisecond)
}
func TestGetAPI(t *testing.T) {
Load(config.Conf)
var err error
go func() { err = Start(config.Conf) }()
if err != nil {
t.Fatal(err)
}
defer Stop()
time.Sleep(500 * time.Millisecond)
url := fmt.Sprintf("http://127.0.0.1:%d/dsl/readfile?name=/models/user.json", config.Conf.Studio.Port)
res := network.RequestGet(url, nil, nil)
assert.Equal(t, "用户", res.Data.(map[string]interface{})["name"])
url = fmt.Sprintf("http://127.0.0.1:%d/dsl/readdir?name=/models", config.Conf.Studio.Port)
res = network.RequestGet(url, nil, nil)
assert.Equal(t, 11, len(res.Data.([]interface{})))
url = fmt.Sprintf("http://127.0.0.1:%d/dsl/readdir?name=/models&recursive=1", config.Conf.Studio.Port)
res = network.RequestGet(url, nil, nil)
assert.Equal(t, 12, len(res.Data.([]interface{})))
}

View file

@ -385,17 +385,22 @@ func ProcessExport(process *gou.Process) interface{} {
WithSID(process.Sid).
Run()
// After Hook
response = table.After(table.Hooks.AfterSearch, response, []interface{}{param, page, pagesize}, process.Sid)
if debug {
bytes, _ := jsoniter.Marshal(response)
log.Info("[Export] %s %d %d Prepare: %s", filename, page, pagesize, string(bytes))
}
res, ok := response.(map[string]interface{})
// After Hook
respAfterHook := table.After(table.Hooks.AfterSearch, response, []interface{}{param, page, pagesize}, process.Sid)
if debug {
bytes, _ := jsoniter.Marshal(respAfterHook)
log.Info("[Export] %s %d %d Prepare After: %s", filename, page, pagesize, string(bytes))
}
res, ok := respAfterHook.(map[string]interface{})
if !ok {
res, ok = response.(maps.MapStrAny)
res, ok = respAfterHook.(maps.MapStrAny)
if !ok {
page = -1
continue

33
tests/studio/table.js Normal file
View file

@ -0,0 +1,33 @@
/**
* Create table
* @param {*} name
*/
function Make(name) {
var fs = new FS("dsl");
var model = GetModel(name);
var table = JSON.stringify({
name: `${model.name} Admin`,
actions: { bind: { model: name } },
});
fs.WriteFile(`/tables/auto/${name}.tab.json`, table);
}
/**
* Get Model
* @param {*} name
* @returns
*/
function Model(name) {
var fs = new FS("dsl");
var file = `/models/${name}.mod.json`;
var data = fs.ReadFile(file);
return JSON.parse(data);
}
/**
* for unit tests
* @returns
*/
function Ping() {
return "PONG";
}