From a1f1d74f01586898ff8b473f2c1b5d0a1ea49e34 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Nov 2022 12:36:17 +0800 Subject: [PATCH 1/2] [add] table.Download api & process --- widgets/table/action.go | 10 ++++++++ widgets/table/api.go | 18 +++++++++++++ widgets/table/process.go | 48 +++++++++++++++++++++++++++++++++++ widgets/table/process_test.go | 42 ++++++++++++++++++++++++------ widgets/table/table.go | 6 ++++- widgets/table/types.go | 1 + 6 files changed, 116 insertions(+), 9 deletions(-) diff --git a/widgets/table/action.go b/widgets/table/action.go index a26e34f8..7b47065e 100644 --- a/widgets/table/action.go +++ b/widgets/table/action.go @@ -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"]). diff --git a/widgets/table/api.go b/widgets/table/api.go index e09e6c66..bc4e6439 100644 --- a/widgets/table/api.go +++ b/widgets/table/api.go @@ -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", diff --git a/widgets/table/process.go b/widgets/table/process.go index c952b72c..4382c9f4 100644 --- a/widgets/table/process.go +++ b/widgets/table/process.go @@ -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) diff --git a/widgets/table/process_test.go b/widgets/table/process_test.go index 214dc870..b5e8eb51 100644 --- a/widgets/table/process_test.go +++ b/widgets/table/process_test.go @@ -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) { diff --git a/widgets/table/table.go b/widgets/table/table.go index 555643f2..dce00bcb 100644 --- a/widgets/table/table.go +++ b/widgets/table/table.go @@ -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 diff --git a/widgets/table/types.go b/widgets/table/types.go index 3fe2a5a9..41c089ce 100644 --- a/widgets/table/types.go +++ b/widgets/table/types.go @@ -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"` From ae6c234dd10bd193bb6dece434e2ba51d5fa3e9d Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 16 Nov 2022 13:39:17 +0800 Subject: [PATCH 2/2] [add] upload & download compute --- widgets/component/compute.go | 1 + widgets/component/handlers.go | 153 ++++++++++++++++++++++++++++++++++ widgets/compute/compute.go | 2 + 3 files changed, 156 insertions(+) diff --git a/widgets/component/compute.go b/widgets/component/compute.go index 785397f0..bf6636c2 100644 --- a/widgets/component/compute.go +++ b/widgets/component/compute.go @@ -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 diff --git a/widgets/component/handlers.go b/widgets/component/handlers.go index 7695c592..70d8a60d 100644 --- a/widgets/component/handlers.go +++ b/widgets/component/handlers.go @@ -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 +} diff --git a/widgets/compute/compute.go b/widgets/compute/compute.go index 5e672f50..901d7a60 100644 --- a/widgets/compute/compute.go +++ b/widgets/compute/compute.go @@ -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 {