[add] studio api server
This commit is contained in:
parent
3f0869c11e
commit
d74f74b5d9
7 changed files with 511 additions and 20 deletions
|
|
@ -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
41
studio/middleware.go
Normal 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
183
studio/router.go
Normal 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()
|
||||
}
|
||||
147
studio/studio.go
147
studio/studio.go
|
|
@ -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
78
studio/studio_test.go
Normal 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{})))
|
||||
|
||||
}
|
||||
|
|
@ -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
33
tests/studio/table.js
Normal 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";
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue