From 59a6cd927e5c0b19a0dbe827d89e646548cf4839 Mon Sep 17 00:00:00 2001 From: arraykeys Date: Fri, 12 Jan 2024 17:28:08 +0800 Subject: [PATCH] ghttpserver --- core/http.go | 5 ++ http/server/api.go | 10 +++ http/server/api_test.go | 18 +++++ http/server/server.go | 43 +++++++++++ http/server/server_test.go | 26 +++++++ http/template/helper.go | 57 ++++++++++++++ http/template/helper_test.go | 13 ++++ http/template/testdata/e.txt | 1 + http/template/testdata/f/f.txt | 1 + http/template/testdata/fs.go | 6 ++ http/template/testdata/g.html | 1 + http/template/testdata/tpl/a.html | 1 + http/template/testdata/tpl/a/aa.html | 1 + http/template/testdata/tpl/a/c/c.html | 1 + http/template/testdata/tpl/b/b.html | 1 + http/template/testdata/tpl/d.txt | 1 + util/json/json.go | 107 +++++++++++++++----------- util/json/json_test.go | 27 +------ 18 files changed, 253 insertions(+), 67 deletions(-) create mode 100644 http/template/testdata/e.txt create mode 100644 http/template/testdata/f/f.txt create mode 100644 http/template/testdata/fs.go create mode 100644 http/template/testdata/g.html create mode 100644 http/template/testdata/tpl/a.html create mode 100644 http/template/testdata/tpl/a/aa.html create mode 100644 http/template/testdata/tpl/a/c/c.html create mode 100644 http/template/testdata/tpl/b/b.html create mode 100644 http/template/testdata/tpl/d.txt diff --git a/core/http.go b/core/http.go index eb74bffc3..9817a51c6 100644 --- a/core/http.go +++ b/core/http.go @@ -8,6 +8,7 @@ package gcore import ( "bufio" "context" + "embed" "io" "net" "net/http" @@ -173,6 +174,8 @@ type APIServer interface { Ctx() Ctx ListenerFactory() func(addr string) (net.Listener, error) SetListenerFactory(listenerFactory func(addr string) (net.Listener, error)) + ServeEmbedFS(fs embed.FS, urlPath string) + ServeFiles(rootPath, urlPath string) } type HTTPServer interface { @@ -206,6 +209,8 @@ type HTTPServer interface { Listen() (err error) ListenTLS() (err error) SetCtx(ctx Ctx) + ServeEmbedFS(fs embed.FS, urlPath string) + ServeFiles(rootPath, urlPath string) } type Controller interface { diff --git a/http/server/api.go b/http/server/api.go index 8b047ec5c..6c0d63364 100644 --- a/http/server/api.go +++ b/http/server/api.go @@ -9,10 +9,12 @@ import ( "context" "crypto/tls" "crypto/x509" + "embed" "fmt" gcore "github.com/snail007/gmc/core" ghttputil "github.com/snail007/gmc/internal/util/http" "github.com/snail007/gmc/module/log" + gfile "github.com/snail007/gmc/util/file" "io" "io/ioutil" "log" @@ -409,3 +411,11 @@ func (this *APIServer) Listeners() []net.Listener { func (this *APIServer) Listener() net.Listener { return this.listener } + +func (s *APIServer) ServeEmbedFS(fs embed.FS, urlPath string) { + serveEmbedFS(s.router, fs, urlPath) +} + +func (s *APIServer) ServeFiles(rootPath, urlPath string) { + serveFiles(s.router, gfile.Abs(rootPath), urlPath) +} diff --git a/http/server/api_test.go b/http/server/api_test.go index e71b6a06f..39fa3f178 100644 --- a/http/server/api_test.go +++ b/http/server/api_test.go @@ -7,6 +7,7 @@ package ghttpserver import ( gcore "github.com/snail007/gmc/core" + "github.com/snail007/gmc/http/template/testdata" "net" "testing" @@ -180,3 +181,20 @@ func TestAPIServer_createListener(t *testing.T) { assert.Nil(e) assert.IsType((*MyListener)(nil), api.listener) } + +func TestAPIServer_ServeFiles(t *testing.T) { + assert := assert.New(t) + api := NewAPIServer(gcore.ProviderCtx()(), ":") + api.ServeEmbedFS(testdata.TplFS, "/tpls/") + api.ServeFiles("tests", "/files/") + + w, r := mockRequest("/tpls/f/f.txt") + api.ServeHTTP(w, r) + str, _ := result(w) + assert.Equal("f", str) + + w, r = mockRequest("/files/d.txt") + api.ServeHTTP(w, r) + str, _ = result(w) + assert.Equal("d", str) +} diff --git a/http/server/server.go b/http/server/server.go index e0d47391b..53536926d 100644 --- a/http/server/server.go +++ b/http/server/server.go @@ -6,21 +6,25 @@ package ghttpserver import ( + "bytes" "compress/gzip" "context" "crypto/tls" "crypto/x509" + "embed" "encoding/base64" "fmt" gcore "github.com/snail007/gmc/core" ghttputil "github.com/snail007/gmc/internal/util/http" "github.com/snail007/gmc/module/log" + gfile "github.com/snail007/gmc/util/file" "io" "io/ioutil" "log" "mime" "net" "net/http" + "os" "path/filepath" "strings" "sync" @@ -576,6 +580,7 @@ func (s *HTTPServer) SetLog(l gcore.Logger) { s.logger = l return } + func (s *HTTPServer) callMiddleware(ctx gcore.Ctx, middleware []gcore.Middleware) (isStop bool) { for _, fn := range middleware { func() { @@ -591,3 +596,41 @@ func (s *HTTPServer) callMiddleware(ctx gcore.Ctx, middleware []gcore.Middleware } return } + +func (s *HTTPServer) ServeEmbedFS(fs embed.FS, urlPath string) { + serveEmbedFS(s.router, fs, urlPath) +} + +func (s *HTTPServer) ServeFiles(rootPath, urlPath string) { + serveFiles(s.router, gfile.Abs(rootPath), urlPath) +} + +func serveEmbedFS(router gcore.HTTPRouter, fs embed.FS, urlPath string) { + urlPath = strings.TrimSuffix(urlPath, "/") + bindPath := urlPath + "/*filepath" + router.HandlerFuncAny(bindPath, func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, urlPath+"/") + b, err := fs.ReadFile(path) + if err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Not Found")) + return + } + http.ServeContent(w, r, filepath.Base(path), time.Time{}, bytes.NewReader(b)) + }) +} + +func serveFiles(router gcore.HTTPRouter, root, urlPath string) { + urlPath = strings.TrimSuffix(urlPath, "/") + bindPath := urlPath + "/*filepath" + router.HandlerFuncAny(bindPath, func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, urlPath+"/") + b, err := os.ReadFile(filepath.Join(root, path)) + if err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Not Found")) + return + } + http.ServeContent(w, r, filepath.Base(path), time.Time{}, bytes.NewReader(b)) + }) +} diff --git a/http/server/server_test.go b/http/server/server_test.go index 1b630ce16..0026db73e 100644 --- a/http/server/server_test.go +++ b/http/server/server_test.go @@ -13,6 +13,7 @@ import ( "github.com/snail007/gmc/core" gcontroller "github.com/snail007/gmc/http/controller" gsession "github.com/snail007/gmc/http/session" + "github.com/snail007/gmc/http/template/testdata" gctx "github.com/snail007/gmc/module/ctx" ghttppprof "github.com/snail007/gmc/util/pprof" "io/ioutil" @@ -660,3 +661,28 @@ func TestSetBinBytes(t *testing.T) { }) assert.Equal(t, defaultBinData["test/a"], []byte("aa")) } + +func TestHTTPServer_ServeEmbedFS(t *testing.T) { + assert := assert.New(t) + s := mockHTTPServer() + s.ServeEmbedFS(testdata.TplFS, "/tpls/") + s.ServeFiles("tests", "/files/") + + w, r := mockRequest("/tpls/f/f.txt") + s.ServeHTTP(w, r) + str, _ := result(w) + assert.Equal("f", str) + + w, r = mockRequest("/files/d.txt") + s.ServeHTTP(w, r) + str, _ = result(w) + assert.Equal("d", str) + + w, r = mockRequest("/tpls/none.txt") + s.ServeHTTP(w, r) + assert.Equal(404, w.Result().StatusCode) + + w, r = mockRequest("/files/none.txt") + s.ServeHTTP(w, r) + assert.Equal(404, w.Result().StatusCode) +} diff --git a/http/template/helper.go b/http/template/helper.go index 90490deba..0100db584 100644 --- a/http/template/helper.go +++ b/http/template/helper.go @@ -6,9 +6,16 @@ package gtemplate import ( + "embed" + "encoding/base64" + "errors" gcore "github.com/snail007/gmc/core" gctx "github.com/snail007/gmc/module/ctx" glog "github.com/snail007/gmc/module/log" + gcond "github.com/snail007/gmc/util/cond" + "io/fs" + "path/filepath" + "strings" ) func Init(ctx gcore.Ctx) (tpl gcore.Template, err error) { @@ -58,3 +65,53 @@ func RenderStringWithFunc(tpl string, data map[string]interface{}, funcMap map[s r, e := RenderBytesWithFunc([]byte(tpl), data, funcMap) return string(r), e } + +type EmbedTemplateFS struct { + root string + ext string + fs embed.FS + tpl *Template +} + +// NewEmbedTemplateFS parse template files from fs embed.FS to tpl *Template, +// it should be called before tpl.Parse(), if the tpl parsed already , nil returned. +func NewEmbedTemplateFS(tpl *Template, fs embed.FS, rootDir string) *EmbedTemplateFS { + return &EmbedTemplateFS{ + fs: fs, + tpl: tpl, + root: rootDir, + ext: ".html", + } +} + +func (s *EmbedTemplateFS) SetExt(ext string) *EmbedTemplateFS { + s.ext = ext + return s +} + +func (s *EmbedTemplateFS) Parse() (err error) { + if s.tpl.parsed { + return errors.New("tpl parsed already") + } + bindData := map[string]string{} + err = fs.WalkDir(s.fs, s.root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && filepath.Ext(d.Name()) == s.ext { + b, _ := s.fs.ReadFile(path) + key := strings.TrimSuffix(path, s.ext) + trimPrefix := gcond.Cond(s.root == "", "", strings.TrimRight(s.root, "/")+"/").String() + if len(trimPrefix) > 0 { + key = strings.TrimPrefix(key, trimPrefix) + } + bindData[key] = base64.StdEncoding.EncodeToString(b) + } + return nil + }) + if err != nil { + return err + } + s.tpl.SetBinBase64(bindData) + return nil +} diff --git a/http/template/helper_test.go b/http/template/helper_test.go index 19bd3f7a0..ae077cf6c 100644 --- a/http/template/helper_test.go +++ b/http/template/helper_test.go @@ -1,6 +1,8 @@ package gtemplate import ( + "github.com/snail007/gmc/http/template/testdata" + gctx "github.com/snail007/gmc/module/ctx" gmap "github.com/snail007/gmc/util/map" "github.com/stretchr/testify/assert" "testing" @@ -23,3 +25,14 @@ func TestRender(t *testing.T) { //fmt.Println(err.Error()) assert.Equal(t, "abbc", d) } + +func TestNewEmbedTemplateFS(t *testing.T) { + tpl, _ := NewTemplate(gctx.NewCtx(), "") + efs := NewEmbedTemplateFS(tpl, testdata.TplFS, "tpl").SetExt(".html") + efs.Parse() + assert.Len(t, efs.tpl.binData, 4) + + //err + tpl.Parse() + assert.Error(t, NewEmbedTemplateFS(tpl, testdata.TplFS, ".").Parse()) +} diff --git a/http/template/testdata/e.txt b/http/template/testdata/e.txt new file mode 100644 index 000000000..9cbe6ea56 --- /dev/null +++ b/http/template/testdata/e.txt @@ -0,0 +1 @@ +e \ No newline at end of file diff --git a/http/template/testdata/f/f.txt b/http/template/testdata/f/f.txt new file mode 100644 index 000000000..4d1ae35ba --- /dev/null +++ b/http/template/testdata/f/f.txt @@ -0,0 +1 @@ +f \ No newline at end of file diff --git a/http/template/testdata/fs.go b/http/template/testdata/fs.go new file mode 100644 index 000000000..fbd8be5b7 --- /dev/null +++ b/http/template/testdata/fs.go @@ -0,0 +1,6 @@ +package testdata + +import "embed" + +//go:embed * +var TplFS embed.FS diff --git a/http/template/testdata/g.html b/http/template/testdata/g.html new file mode 100644 index 000000000..7937c68fb --- /dev/null +++ b/http/template/testdata/g.html @@ -0,0 +1 @@ +g \ No newline at end of file diff --git a/http/template/testdata/tpl/a.html b/http/template/testdata/tpl/a.html new file mode 100644 index 000000000..2e65efe2a --- /dev/null +++ b/http/template/testdata/tpl/a.html @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/http/template/testdata/tpl/a/aa.html b/http/template/testdata/tpl/a/aa.html new file mode 100644 index 000000000..7ec9a4b77 --- /dev/null +++ b/http/template/testdata/tpl/a/aa.html @@ -0,0 +1 @@ +aa \ No newline at end of file diff --git a/http/template/testdata/tpl/a/c/c.html b/http/template/testdata/tpl/a/c/c.html new file mode 100644 index 000000000..3410062ba --- /dev/null +++ b/http/template/testdata/tpl/a/c/c.html @@ -0,0 +1 @@ +c \ No newline at end of file diff --git a/http/template/testdata/tpl/b/b.html b/http/template/testdata/tpl/b/b.html new file mode 100644 index 000000000..63d8dbd40 --- /dev/null +++ b/http/template/testdata/tpl/b/b.html @@ -0,0 +1 @@ +b \ No newline at end of file diff --git a/http/template/testdata/tpl/d.txt b/http/template/testdata/tpl/d.txt new file mode 100644 index 000000000..c59d9b634 --- /dev/null +++ b/http/template/testdata/tpl/d.txt @@ -0,0 +1 @@ +d \ No newline at end of file diff --git a/util/json/json.go b/util/json/json.go index 1804f16ff..d1fe673c6 100644 --- a/util/json/json.go +++ b/util/json/json.go @@ -15,14 +15,64 @@ import ( ) var ( - AddModifier = gjson.AddModifier + // AddModifier binds a custom modifier command to the GJSON syntax. + // This operation is not thread safe and should be executed prior to + // using all other gjson function. + AddModifier = gjson.AddModifier + + // ModifierExists returns true when the specified modifier exists. ModifierExists = gjson.ModifierExists - Escape = gjson.Escape - ForEachLine = gjson.ForEachLine - Parse = gjson.Parse - ParseBytes = gjson.ParseBytes - Valid = gjson.Valid - ValidBytes = gjson.ValidBytes + + // Escape returns an escaped path component. + // + // json := `{ + // "user":{ + // "first.name": "Janet", + // "last.name": "Prichard" + // } + // }` + // user := gjson.Get(json, "user") + // println(user.Get(gjson.Escape("first.name")) + // println(user.Get(gjson.Escape("last.name")) + // // Output: + // // Janet + // // Prichard + Escape = gjson.Escape + + // ForEachLine iterates through lines of JSON as specified by the JSON Lines + // format (http://jsonlines.org/). + // Each line is returned as a GJSON Result. + ForEachLine = gjson.ForEachLine + + // Parse parses the json and returns a result. + // + // This function expects that the json is well-formed, and does not validate. + // Invalid json will not panic, but it may return back unexpected results. + // If you are consuming JSON from an unpredictable source then you may want to + // use the Valid function first. + Parse = gjson.Parse + + // ParseBytes parses the json and returns a result. + // If working with bytes, this method preferred over Parse(string(data)) + ParseBytes = gjson.ParseBytes + + // Valid returns true if the input is valid json. + // + // if !gjson.Valid(json) { + // return errors.New("invalid json") + // } + // value := gjson.Get(json, "name.last") + Valid = gjson.Valid + + // ValidBytes returns true if the input is valid json. + // + // if !gjson.Valid(json) { + // return errors.New("invalid json") + // } + // value := gjson.Get(json, "name.last") + // + // If working with bytes, this method preferred over ValidBytes(string(data)) + ValidBytes = gjson.ValidBytes ) type Options = sjson.Options @@ -41,16 +91,12 @@ func (s Result) Paths() []string { return s.paths } -func (s Result) AsJSONObject() *JSONObject { - obj := NewJSONObject(nil) - obj.json = s.Raw - return obj +func (s Result) ToJSONObject() *JSONObject { + return NewJSONObject(s.Raw) } -func (s Result) AsJSONArray() *JSONArray { - obj := NewJSONArray(nil) - obj.json = s.Raw - return obj +func (s Result) ToJSONArray() *JSONArray { + return NewJSONArray(s.Raw) } type Builder struct { @@ -110,19 +156,6 @@ func (s *Builder) Set(path string, value interface{}) error { return err } -// SetOptions sets a json value for the specified path with options. -// A path is in dot syntax, such as "name.last" or "age". -// This function expects that the json is well-formed, and does not validate. -// Invalid json will not panic, but it may return back unexpected results. -// An error is returned if the path is not valid. -func (s *Builder) SetOptions(path string, value interface{}, opts *Options) error { - j, err := sjson.SetOptions(s.json, path, value, opts) - if err == nil { - s.json = j - } - return err -} - // SetRaw sets a raw json value for the specified path. // This function works the same as Set except that the value is set as a // raw block of json. This allows for setting premarshalled json objects. @@ -137,20 +170,6 @@ func (s *Builder) SetRaw(path, value string) error { return err } -// SetRawOptions sets a raw json value for the specified path with options. -// This furnction works the same as SetOptions except that the value is set -// as a raw block of json. This allows for setting premarshalled json objects. -func (s *Builder) SetRawOptions(path, value string, opts *Options) error { - j, err := sjson.SetRawOptions(s.json, path, value, opts) - if err == nil { - if !Valid(j) { - return errors.New("invalid json value: " + value) - } - s.json = j - } - return err -} - // Get searches json for the specified path. // A path is in dot syntax, such as "name.last" or "age". // When the value is found it's returned immediately. @@ -223,7 +242,9 @@ func (s *Builder) JSONArray() *JSONArray { return NewJSONArray(s.json) } -// GetMany batch of Get +// GetMany searches json for the multiple paths. +// The return value is a Result array where the number of items +// will be equal to the number of input paths. func (s *Builder) GetMany(path ...string) []Result { rs1 := gjson.GetMany(s.json, path...) var rs []Result diff --git a/util/json/json_test.go b/util/json/json_test.go index e3b056074..04fe70c5c 100644 --- a/util/json/json_test.go +++ b/util/json/json_test.go @@ -203,27 +203,6 @@ func TestBuilderOperations(t *testing.T) { assert.Nil(t, result.Paths()) } -func TestBuilderAdditionalOperations(t *testing.T) { - builder := NewBuilder(`{"name": "John", "age": 30, "city": "New York"}`) - - opts := &Options{Optimistic: false} - err := builder.SetOptions("address", "123 Main St", opts) - if err != nil { - t.Errorf("SetOptions method failed: %v", err) - } - - rawOpts := &Options{Optimistic: false} - err = builder.SetRawOptions("info", `{"key": "value"}`, rawOpts) - if err != nil { - t.Errorf("SetRawOptions method failed: %v", err) - } - - err = builder.SetRawOptions("info", `abc`, rawOpts) - if err == nil { - t.Errorf("SetRawOptions method failed: %v", err) - } -} - func TestJSONArray_Append(t *testing.T) { arr := NewJSONArray("[123]") assert.Equal(t, "123", arr.Get("0").String()) @@ -231,7 +210,7 @@ func TestJSONArray_Append(t *testing.T) { obj := NewJSONObject(map[string]string{"name": "456"}) arr.Append(obj) assert.Equal(t, "456", arr.Get("1.name").String()) - assert.Equal(t, "456", arr.Get("1").AsJSONObject().Get("name").String()) + assert.Equal(t, "456", arr.Get("1").ToJSONObject().Get("name").String()) obj = NewJSONObject(nil) obj.Set("name", "789") @@ -243,7 +222,7 @@ func TestJSONArray_Append(t *testing.T) { arr.Append(obja) assert.Equal(t, "000", arr.Get("3.0").String()) assert.Equal(t, "111", arr.Get("3.1").String()) - assert.Equal(t, "000", arr.Get("3").AsJSONArray().Get("0").String()) + assert.Equal(t, "000", arr.Get("3").ToJSONArray().Get("0").String()) assert.Equal(t, int64(4), arr.Len()) @@ -251,7 +230,7 @@ func TestJSONArray_Append(t *testing.T) { arr.Append(*obja) assert.Equal(t, "0000", arr.Get("4.0").String()) assert.Equal(t, "1111", arr.Get("4.1").String()) - assert.Equal(t, "0000", arr.Get("4").AsJSONArray().Get("0").String()) + assert.Equal(t, "0000", arr.Get("4").ToJSONArray().Get("0").String()) obj = NewJSONObject(`{"name":"111"}`) arr.Append(obj)