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"`