Merge pull request #261 from trheyi/main

[add] table.Download api & process &compute
This commit is contained in:
Max 2022-11-16 14:46:18 +08:00 committed by GitHub
commit 350c9fedc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 272 additions and 9 deletions

View file

@ -16,6 +16,7 @@ var defaults = []CArg{
{IsExp: true, key: "props", value: nil},
{IsExp: true, key: "type", value: nil},
{IsExp: true, key: "id", value: nil},
{IsExp: true, key: "path", value: nil},
}
// NewExp create a new exp CArg

View file

@ -2,6 +2,7 @@ package component
import (
"fmt"
"net/url"
"strings"
)
@ -10,6 +11,8 @@ var hanlders = map[string]ComputeHanlder{
"Trim": Trim,
"Hide": Hide,
"Concat": Concat,
"Download": Download,
"Upload": Upload,
"QueryString": Trim,
"ImagesView": Trim,
"ImagesEdit": Trim,
@ -61,3 +64,153 @@ func Get(args ...interface{}) (interface{}, error) {
func Hide(args ...interface{}) (interface{}, error) {
return nil, nil
}
// Upload return the file download path
func Upload(args ...interface{}) (interface{}, error) {
if len(args) < 5 {
return nil, fmt.Errorf("Upload args[0]~args[4] is required")
}
if args[0] == nil {
return "", nil
}
files := []string{}
switch values := args[0].(type) {
case []interface{}:
for i := range values {
file := fmt.Sprintf("%v", values[i])
if file != "" {
files = append(files, fmt.Sprintf("%v", file))
}
}
break
case []string:
for _, file := range values {
if file != "" {
files = append(files, fmt.Sprintf("%v", file))
}
}
break
case string:
if values != "" {
files = append(files, fmt.Sprintf("%v", values))
}
break
case map[string]interface{}:
for name := range values {
file := fmt.Sprintf("%v", values[name])
if file != "" {
files = append(files, fmt.Sprintf("%v", file))
}
}
break
}
id, ok := args[3].(string)
if !ok {
return nil, fmt.Errorf("Upload args[3] is not string")
}
path, ok := args[4].(string)
if !ok {
return nil, fmt.Errorf("Upload args[4] is not string")
}
preifx := fmt.Sprintf("/api/__yao/table/%s/download/%s?name=", id, url.QueryEscape(path))
res := []string{}
for _, file := range files {
file = strings.TrimSpace(file)
if strings.HasPrefix(file, "http") {
res = append(res, file)
continue
}
res = append(res, strings.TrimPrefix(file, preifx))
}
if len(res) == 0 {
return nil, nil
}
return res, nil
}
// Download return the file download path
func Download(args ...interface{}) (interface{}, error) {
if len(args) < 5 {
return nil, fmt.Errorf("Download args[0]~args[4] is required")
}
if args[0] == nil {
return "", nil
}
files := []string{}
switch values := args[0].(type) {
case []interface{}:
for i := range values {
file := fmt.Sprintf("%v", values[i])
if file != "" {
files = append(files, fmt.Sprintf("%v", file))
}
}
break
case []string:
for _, file := range values {
if file != "" {
files = append(files, fmt.Sprintf("%v", file))
}
}
break
case string:
if values != "" {
files = append(files, fmt.Sprintf("%v", values))
}
break
case map[string]interface{}:
for name := range values {
file := fmt.Sprintf("%v", values[name])
if file != "" {
files = append(files, fmt.Sprintf("%v", file))
}
}
break
}
id, ok := args[3].(string)
if !ok {
return nil, fmt.Errorf("Download args[3] is not string")
}
path, ok := args[4].(string)
if !ok {
return nil, fmt.Errorf("Download args[4] is not string")
}
res := []string{}
for _, file := range files {
file = strings.TrimSpace(file)
if strings.HasPrefix(file, "http") {
res = append(res, file)
continue
}
file = fmt.Sprintf("/api/__yao/table/%s/download/%s?name=%s", id, url.QueryEscape(path), file)
res = append(res, file)
}
if len(res) == 0 {
return nil, nil
}
return res, nil
}

View file

@ -102,6 +102,7 @@ func (c *Computable) editRow(process *gou.Process, res map[string]interface{}, g
data.Set("id", id)
data.Set("value", res[key])
data.Set("path", fmt.Sprintf("%s.%s", path, unit.Name))
data.Merge(any.MapOf(field.Edit.Map()).MapStrAny.Dot())
new, err := field.Edit.Compute.Value(data, process.Sid, process.Global)
if err != nil {
@ -243,6 +244,7 @@ func (c *Computable) viewRow(name string, process *gou.Process, res map[string]i
data.Set("value", res[key])
data.Set("id", id)
data.Set("path", fmt.Sprintf("%s.%s", path, unit.Name))
data.Merge(any.MapOf(field.View.Map()).MapStrAny.Dot())
new, err := field.View.Compute.Value(data, process.Sid, process.Global)
if err != nil {

View file

@ -26,6 +26,12 @@ var processActionDefaults = map[string]*action.Process{
Guard: "bearer-jwt",
Default: []interface{}{nil, nil, nil},
},
"Download": {
Name: "yao.table.Download",
Guard: "-",
Process: "fs.system.Download",
Default: []interface{}{nil},
},
"Search": {
Name: "yao.table.Search",
Guard: "bearer-jwt",
@ -103,6 +109,10 @@ func (act *ActionDSL) SetDefaultProcess() {
Merge(processActionDefaults["Upload"]).
SetHandler(processHandler)
act.Download = action.ProcessOf(act.Download).
Merge(processActionDefaults["Download"]).
SetHandler(processHandler)
act.Search = action.ProcessOf(act.Search).
WithBefore(act.BeforeSearch).WithAfter(act.AfterSearch).
Merge(processActionDefaults["Search"]).

View file

@ -53,6 +53,8 @@ func (table *DSL) getAction(path string) (*action.Process, error) {
return table.Action.Component, nil
case "/api/__yao/table/:id/upload/:xpath/:method":
return table.Action.Upload, nil
case "/api/__yao/table/:id/download/:field":
return table.Action.Download, nil
case "/api/__yao/table/:id/search":
return table.Action.Search, nil
case "/api/__yao/table/:id/get":
@ -166,6 +168,22 @@ func exportAPI() error {
}
http.Paths = append(http.Paths, path)
// GET /api/__yao/table/:id/download/:field -> Default process: yao.table.Download $param.id $param.xpath $param.field $query.name $query.token
path = gou.Path{
Label: "Download",
Description: "Download",
Path: "/:id/download/:field",
Method: "GET",
Process: "yao.table.Download",
In: []string{"$param.id", "$param.field", "$query.name", "$query.token"},
Out: gou.Out{
Status: 200,
Body: "{{content}}",
Headers: map[string]string{"Content-Type": "{{type}}"},
},
}
http.Paths = append(http.Paths, path)
// POST /api/__yao/table/:id/save -> Default process: yao.table.Save $param.id :payload
path = gou.Path{
Label: "Save",

View file

@ -5,7 +5,10 @@ import (
"strings"
"github.com/yaoapp/gou"
"github.com/yaoapp/gou/fs"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/helper"
)
// Export process
@ -15,6 +18,7 @@ func exportProcess() {
gou.RegisterProcessHandler("yao.table.xgen", processXgen)
gou.RegisterProcessHandler("yao.table.component", processComponent)
gou.RegisterProcessHandler("yao.table.upload", processUpload)
gou.RegisterProcessHandler("yao.table.download", processDownload)
gou.RegisterProcessHandler("yao.table.search", processSearch)
gou.RegisterProcessHandler("yao.table.get", processGet)
gou.RegisterProcessHandler("yao.table.find", processFind)
@ -40,6 +44,50 @@ func processXgen(process *gou.Process) interface{} {
return setting
}
func processDownload(process *gou.Process) interface{} {
process.ValidateArgNums(4)
tab := MustGet(process)
field := process.ArgsString(1)
file := process.ArgsString(2)
tokenString := process.ArgsString(3)
// checking
ext := fs.ExtName(file)
if _, has := fs.DownloadWhitelist[ext]; !has {
exception.New("%s.%s .%s file does not allow", 403, tab.ID, field, ext).Throw()
}
// Auth
tokenString = strings.TrimSpace(strings.TrimPrefix(tokenString, "Bearer "))
if tokenString == "" {
exception.New("%s.%s No permission", 403, tab.ID, field).Throw()
}
claims := helper.JwtValidate(tokenString)
// Get Process name
name := "fs.system.Download"
if tab.Action.Download.Process != "" {
name = tab.Action.Download.Process
}
// Create process
p, err := gou.ProcessOf(name, file)
if err != nil {
log.Error("[downalod] %s.%s %s", tab.ID, field, err.Error())
exception.New("[downalod] %s.%s %s", 400, tab.ID, field, err.Error()).Throw()
}
// Excute process
res, err := p.WithGlobal(process.Global).WithSID(claims.SID).Exec()
if err != nil {
log.Error("[downalod] %s.%s %s", tab.ID, field, err.Error())
exception.New("[downalod] %s.%s %s", 500, tab.ID, field, err.Error()).Throw()
}
return res
}
func processUpload(process *gou.Process) interface{} {
process.ValidateArgNums(4)

View file

@ -8,8 +8,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou"
"github.com/yaoapp/gou/fs"
"github.com/yaoapp/kun/any"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/helper"
q "github.com/yaoapp/yao/query"
)
@ -332,6 +334,20 @@ func TestProcessComponent(t *testing.T) {
assert.Equal(t, "checked", pets[1]["value"])
}
func TestProcessComponentError(t *testing.T) {
load(t)
clear(t)
testData(t)
args := []interface{}{
"pet",
"fields.filter.edit.props.状态.::not-exist",
"remote",
map[string]interface{}{"select": []string{"name", "status"}, "limit": 2},
}
_, err := gou.NewProcess("yao.table.Component", args...).Exec()
assert.Contains(t, err.Error(), "fields.filter.edit.props.状态.::not-exist")
}
func TestProcessUpload(t *testing.T) {
load(t)
clear(t)
@ -353,18 +369,28 @@ func TestProcessUpload(t *testing.T) {
assert.NotEmpty(t, file)
}
func TestProcessComponentError(t *testing.T) {
func TestProcessDownload(t *testing.T) {
load(t)
clear(t)
testData(t)
args := []interface{}{
"pet",
"fields.filter.edit.props.状态.::not-exist",
"remote",
map[string]interface{}{"select": []string{"name", "status"}, "limit": 2},
jwt := helper.JwtMake(1, map[string]interface{}{"id": 1}, map[string]interface{}{"sid": 1})
fs := fs.MustGet("system")
_, err := fs.WriteFile("/text.txt", []byte("Hello"), uint32(os.ModePerm))
if err != nil {
t.Fatal(err)
}
_, err := gou.NewProcess("yao.table.Component", args...).Exec()
assert.Contains(t, err.Error(), "fields.filter.edit.props.状态.::not-exist")
args := []interface{}{"pet", "images", "/text.txt", jwt.Token}
res, err := gou.NewProcess("yao.table.Download", args...).Exec()
if err != nil {
t.Fatal(err)
}
body, ok := res.(map[string]interface{})
assert.True(t, ok)
assert.Equal(t, []byte("Hello"), body["content"])
assert.Equal(t, "text/plain; charset=utf-8", body["type"])
}
func TestProcessSetting(t *testing.T) {

View file

@ -23,6 +23,8 @@ import (
// GET /api/__yao/table/:id/get -> Default process: yao.table.Get $param.id :query
// GET /api/__yao/table/:id/find/:primary -> Default process: yao.table.Find $param.id $param.primary :query
// GET /api/__yao/table/:id/component/:xpath/:method -> Default process: yao.table.Component $param.id $param.xpath $param.method :query
// GET /api/__yao/table/:id/upload/:xpath/:method -> Default process: yao.table.Upload $param.id $param.xpath $param.method $file.file
// GET /api/__yao/table/:id/download/:field -> Default process: yao.table.Download $param.id $param.field $query.name $query.token
// POST /api/__yao/table/:id/save -> Default process: yao.table.Save $param.id :payload
// POST /api/__yao/table/:id/create -> Default process: yao.table.Create $param.id :payload
// POST /api/__yao/table/:id/insert -> Default process: yao.table.Insert :payload
@ -39,7 +41,9 @@ import (
// yao.table.Search Return the records with pagination
// yao.table.Get Return the records without pagination
// yao.table.Find Return the record via the given primary key
// yao.table.Component Return the result defined in props.xProps
// yao.table.Component Return the result defined in props
// yao.table.Upload Upload file defined in props
// yao.table.Download Download file defined in props
// yao.table.Save Save a record, if given a primary key update, else insert
// yao.table.Create Create a record
// yao.table.Insert Insert records

View file

@ -27,6 +27,7 @@ type ActionDSL struct {
Setting *action.Process `json:"setting,omitempty"`
Component *action.Process `json:"component,omitempty"`
Upload *action.Process `json:"upload,omitempty"`
Download *action.Process `json:"download,omitempty"`
Search *action.Process `json:"search,omitempty"`
Get *action.Process `json:"get,omitempty"`
Find *action.Process `json:"find,omitempty"`