diff --git a/.example/other/test.go b/.example/other/test.go index 774c42627..92e1cfb76 100644 --- a/.example/other/test.go +++ b/.example/other/test.go @@ -1,37 +1,10 @@ package main import ( - "net/http" - - "github.com/gogf/gf/frame/g" - "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/container/garray" ) -func MiddlewareAuth(r *ghttp.Request) { - token := r.Get("token") - if token == "123456" { - r.Response.Writeln("auth") - r.Middleware.Next() - } else { - r.Response.WriteStatus(http.StatusForbidden) - } -} - -func MiddlewareCORS(r *ghttp.Request) { - r.Response.Writeln("cors") - r.Response.CORSDefault() - r.Middleware.Next() -} - func main() { - s := g.Server() - s.Use(MiddlewareCORS) - s.Group("/api.v2", func(group *ghttp.RouterGroup) { - group.Middleware(MiddlewareAuth) - group.ALL("/user/list", func(r *ghttp.Request) { - r.Response.Writeln("list") - }) - }) - s.SetPort(8199) - s.Run() + arr := garray.NewStrArray(false) + arr.Unique() } diff --git a/DONATOR.MD b/DONATOR.MD index 418951da5..d5327b5a9 100644 --- a/DONATOR.MD +++ b/DONATOR.MD @@ -15,7 +15,7 @@ We currently accept donation by Alipay/WechatPay, please note your github/gitee |[zhuhuan12](https://gitee.com/zhuhuan12)|gitee|¥50.00 | |[zfan_codes](https://gitee.com/zfan_codes)|gitee|¥10.00 | |[arden](https://github.com/arden)|alipay|¥10.00 | -|[macnie](https://www.macnie.com)|wechat|¥100.00 | +|[macnie](https://www.macnie.com)|wechat|¥110.00 | |lah|wechat|¥100.00 | |x*z|wechat|¥20.00 | |潘兄|wechat|¥100.00 | diff --git a/container/garray/garray_sorted_any.go b/container/garray/garray_sorted_any.go index dd6c1690b..32439e8f8 100644 --- a/container/garray/garray_sorted_any.go +++ b/container/garray/garray_sorted_any.go @@ -439,6 +439,9 @@ func (a *SortedArray) SetUnique(unique bool) *SortedArray { func (a *SortedArray) Unique() *SortedArray { a.mu.Lock() defer a.mu.Unlock() + if len(a.array) == 0 { + return a + } i := 0 for { if i == len(a.array)-1 { diff --git a/container/garray/garray_sorted_int.go b/container/garray/garray_sorted_int.go index 4d469aa51..fc92567dc 100644 --- a/container/garray/garray_sorted_int.go +++ b/container/garray/garray_sorted_int.go @@ -429,6 +429,10 @@ func (a *SortedIntArray) SetUnique(unique bool) *SortedIntArray { // Unique uniques the array, clear repeated items. func (a *SortedIntArray) Unique() *SortedIntArray { a.mu.Lock() + defer a.mu.Unlock() + if len(a.array) == 0 { + return a + } i := 0 for { if i == len(a.array)-1 { @@ -440,7 +444,6 @@ func (a *SortedIntArray) Unique() *SortedIntArray { i++ } } - a.mu.Unlock() return a } diff --git a/container/garray/garray_sorted_str.go b/container/garray/garray_sorted_str.go index 1b8054c09..c7c9f03aa 100644 --- a/container/garray/garray_sorted_str.go +++ b/container/garray/garray_sorted_str.go @@ -414,6 +414,10 @@ func (a *SortedStrArray) SetUnique(unique bool) *SortedStrArray { // Unique uniques the array, clear repeated items. func (a *SortedStrArray) Unique() *SortedStrArray { a.mu.Lock() + defer a.mu.Unlock() + if len(a.array) == 0 { + return a + } i := 0 for { if i == len(a.array)-1 { @@ -425,7 +429,6 @@ func (a *SortedStrArray) Unique() *SortedStrArray { i++ } } - a.mu.Unlock() return a } diff --git a/net/ghttp/ghttp_func.go b/net/ghttp/ghttp_func.go index 4f32302f4..e2d46a8ef 100644 --- a/net/ghttp/ghttp_func.go +++ b/net/ghttp/ghttp_func.go @@ -50,9 +50,7 @@ func niceCallFunc(f func()) { defer func() { if err := recover(); err != nil { switch err { - case gEXCEPTION_EXIT: - fallthrough - case gEXCEPTION_EXIT_ALL: + case gEXCEPTION_EXIT, gEXCEPTION_EXIT_ALL: return default: panic(err) diff --git a/net/ghttp/ghttp_request_param_file.go b/net/ghttp/ghttp_request_param_file.go index 6e54725b2..e91f5b3e7 100644 --- a/net/ghttp/ghttp_request_param_file.go +++ b/net/ghttp/ghttp_request_param_file.go @@ -8,12 +8,12 @@ package ghttp import ( "errors" + "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/os/gfile" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/util/grand" "io" "mime/multipart" - "os" "strconv" "strings" ) @@ -45,22 +45,21 @@ func (f *UploadFile) Save(path string, randomlyRename ...bool) error { } defer file.Close() - var newFile *os.File + filePath := path if gfile.IsDir(path) { filename := gfile.Basename(f.Filename) if len(randomlyRename) > 0 && randomlyRename[0] { filename = strings.ToLower(strconv.FormatInt(gtime.TimestampNano(), 36) + grand.S(6)) filename = filename + gfile.Ext(f.Filename) } - newFile, err = gfile.Create(gfile.Join(path, filename)) - } else { - newFile, err = gfile.Create(path) + filePath = gfile.Join(path, filename) } + newFile, err := gfile.Create(filePath) if err != nil { return err } defer newFile.Close() - + intlog.Printf(`save upload file: %s`, filePath) if _, err := io.Copy(newFile, file); err != nil { return err } diff --git a/net/ghttp/ghttp_response_cors.go b/net/ghttp/ghttp_response_cors.go index 1853239a7..835a1c8e2 100644 --- a/net/ghttp/ghttp_response_cors.go +++ b/net/ghttp/ghttp_response_cors.go @@ -27,6 +27,20 @@ type CORSOptions struct { AllowHeaders string // Access-Control-Allow-Headers } +var ( + // defaultAllowHeaders is the default allowed headers for CORS. + // It's defined as map for better header key searching performance. + defaultAllowHeaders = map[string]struct{}{ + "Origin": {}, + "Accept": {}, + "Cookie": {}, + "Authorization": {}, + "X-Auth-Token": {}, + "X-Requested-With": {}, + "Content-Type": {}, + } +) + // DefaultCORSOptions returns the default CORS options, // which allows any cross-domain request. func (r *Response) DefaultCORSOptions() CORSOptions { @@ -34,9 +48,24 @@ func (r *Response) DefaultCORSOptions() CORSOptions { AllowOrigin: "*", AllowMethods: HTTP_METHODS, AllowCredentials: "true", - AllowHeaders: "Origin,Content-Type,Accept,User-Agent,Cookie,Authorization,X-Auth-Token,X-Requested-With", MaxAge: 3628800, } + // Allow all client's custom headers in default. + if headers := r.Request.Header.Get("Access-Control-Request-Headers"); headers != "" { + array := gstr.SplitAndTrim(headers, ",") + for _, header := range array { + if _, ok := defaultAllowHeaders[header]; !ok { + options.AllowHeaders += header + "," + } + } + for header, _ := range defaultAllowHeaders { + if len(options.AllowHeaders) > 0 { + options.AllowHeaders += "," + } + options.AllowHeaders += header + } + } + // Allow all anywhere origin in default. if origin := r.Request.Header.Get("Origin"); origin != "" { options.AllowOrigin = origin } else if referer := r.Request.Referer(); referer != "" { diff --git a/net/ghttp/ghttp_server.go b/net/ghttp/ghttp_server.go index b6126fac6..d79aa80c7 100644 --- a/net/ghttp/ghttp_server.go +++ b/net/ghttp/ghttp_server.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "github.com/gogf/gf/debug/gdebug" + "github.com/gogf/gf/internal/intlog" "net/http" "os" "reflect" @@ -78,7 +79,6 @@ type ( // handlerItem is the registered handler for route handling, // including middleware and hook functions. handlerItem struct { - itemId int // Unique ID mark. itemName string // Handler name, which is automatically retrieved from runtime stack when registered. itemType int // Handler type: object/handler/controller/middleware/hook. itemFunc HandlerFunc // Handler address. @@ -143,7 +143,7 @@ var ( // it is used for quick HTTP method searching using map. methodsMap = make(map[string]struct{}) - // serverMapping stores more than one server instances. + // serverMapping stores more than one server instances for current process. // The key is the name of the server, and the value is its instance. serverMapping = gmap.NewStrAnyMap(true) @@ -444,14 +444,20 @@ func (s *Server) GetRouterArray() []RouterItem { } // Run starts server listening in blocking way. +// It's commonly used for single server situation. func (s *Server) Run() { if err := s.Start(); err != nil { s.Logger().Fatal(err) } - // Blocking using channel. <-s.closeChan - + // Remove plugins. + if len(s.plugins) > 0 { + for _, p := range s.plugins { + intlog.Printf(`remove plugin: %s`, p.Name()) + p.Remove() + } + } s.Logger().Printf("[ghttp] %d: all servers shutdown", gproc.Pid()) } @@ -459,7 +465,17 @@ func (s *Server) Run() { // It's commonly used in multiple servers situation. func Wait() { <-allDoneChan - + // Remove plugins. + serverMapping.Iterator(func(k string, v interface{}) bool { + s := v.(*Server) + if len(s.plugins) > 0 { + for _, p := range s.plugins { + intlog.Printf(`remove plugin: %s`, p.Name()) + p.Remove() + } + } + return true + }) glog.Printf("[ghttp] %d: all servers shutdown", gproc.Pid()) } diff --git a/net/ghttp/ghttp_server_plugin.go b/net/ghttp/ghttp_server_plugin.go index ff7b0ad69..d8d47aaf3 100644 --- a/net/ghttp/ghttp_server_plugin.go +++ b/net/ghttp/ghttp_server_plugin.go @@ -10,7 +10,7 @@ package ghttp type Plugin interface { Name() string // Name returns the name of the plugin. Author() string // Author returns the author of the plugin. - Version() string // Version returns the version of the plugin. + Version() string // Version returns the version of the plugin, like "v1.0.0". Description() string // Description returns the description of the plugin. Install(s *Server) error // Install installs the plugin before server starts. Remove() error // Remove removes the plugin. diff --git a/net/ghttp/ghttp_server_router.go b/net/ghttp/ghttp_server_router.go index 83bc9ef37..7e7c2f353 100644 --- a/net/ghttp/ghttp_server_router.go +++ b/net/ghttp/ghttp_server_router.go @@ -9,7 +9,7 @@ package ghttp import ( "errors" "fmt" - "github.com/gogf/gf/container/gtype" + "github.com/gogf/gf/util/gutil" "strings" "github.com/gogf/gf/debug/gdebug" @@ -23,12 +23,12 @@ const ( gFILTER_KEY = "/net/ghttp/ghttp" ) -var ( - // 用于服务函数的ID生成变量 - handlerIdGenerator = gtype.NewInt() -) +// handlerKey creates and returns an unique router key for given parameters. +func (s *Server) handlerKey(hook, method, path, domain string) string { + return hook + "%" + s.serveHandlerKey(method, path, domain) +} -// 解析pattern +// parsePattern parses the given pattern to domain, method and path variable. func (s *Server) parsePattern(pattern string) (domain, method, path string, err error) { path = strings.TrimSpace(pattern) domain = gDEFAULT_DOMAIN @@ -48,18 +48,17 @@ func (s *Server) parsePattern(pattern string) (domain, method, path string, err if path == "" { err = errors.New("invalid pattern: URI should not be empty") } - // 去掉末尾的"/"符号,与路由匹配时处理一致 if path != "/" { path = strings.TrimRight(path, "/") } return } -// 路由注册处理方法。 -// 非叶节点为哈希表检索节点,按照URI注册的层级进行高效检索,直至到叶子链表节点; -// 叶子节点是链表,按照优先级进行排序,优先级高的排前面,按照遍历检索,按照哈希表层级检索后的叶子链表数据量不会很大,所以效率比较高; +// setHandler creates router item with given handler and pattern and registers the handler to the router tree. +// The router tree can be treated as a multilayer hash table, please refer to the comment in following codes. +// This function is called during server starts up, which cares little about the performance. What really cares +// is the well designed router storage structure for router searching when the request is under serving. func (s *Server) setHandler(pattern string, handler *handlerItem) { - handler.itemId = handlerIdGenerator.Add(1) domain, method, uri, err := s.parsePattern(pattern) if err != nil { s.Logger().Fatal("invalid pattern:", pattern, err) @@ -69,7 +68,8 @@ func (s *Server) setHandler(pattern string, handler *handlerItem) { s.Logger().Fatal("invalid pattern:", pattern, "URI should lead with '/'") return } - // 注册地址记录及重复注册判断 + + // Repeated router checks, this feature can be disabled by server configuration. regKey := s.handlerKey(handler.hookName, method, uri, domain) if !s.config.RouteOverWrite { switch handler.itemType { @@ -80,11 +80,11 @@ func (s *Server) setHandler(pattern string, handler *handlerItem) { } } } - // 注册的路由信息对象 + // Create a new router by given parameter. handler.router = &Router{ Uri: uri, Domain: domain, - Method: method, + Method: strings.ToUpper(method), Priority: strings.Count(uri[1:], "/"), } handler.router.RegRule, handler.router.RegNames = s.patternToRegRule(uri) @@ -92,7 +92,8 @@ func (s *Server) setHandler(pattern string, handler *handlerItem) { if _, ok := s.serveTree[domain]; !ok { s.serveTree[domain] = make(map[string]interface{}) } - // 当前节点的规则链表 + // List array, very important for router register. + // There may be multiple lists adding into this array when searching from root to leaf. lists := make([]*glist.List, 0) array := ([]string)(nil) if strings.EqualFold("/", uri) { @@ -100,9 +101,57 @@ func (s *Server) setHandler(pattern string, handler *handlerItem) { } else { array = strings.Split(uri[1:], "/") } - // 键名"*fuzz"代表当前节点为模糊匹配节点,该节点也会有一个*list链表; - // 键名"*list"代表链表,叶子节点和模糊匹配节点都有该属性,优先级越高越排前; + // Multilayer hash table: + // 1. Each node of the table is separated by URI path which is split by char '/'. + // 2. The key "*fuzz" specifies this node is a fuzzy node, which has no certain name. + // 3. The key "*list" is the list item of the node, MOST OF THE NODES HAVE THIS ITEM, + // especially the fuzzy node. NOTE THAT the fuzzy node must have the "*list" item, + // and the leaf node also has "*list" item. If the node is not a fuzzy node either + // a leaf, it neither has "*list" item. + // 2. The "*list" item is a list containing registered router items ordered by their + // priorities from high to low. + // 3. There may be repeated router items in the router lists. The lists' priorities + // from root to leaf are from low to high. p := s.serveTree[domain] + for i, part := range array { + // Ignore empty URI part, like: /user//index + if part == "" { + continue + } + // Check if it's a fuzzy node. + if gregex.IsMatchString(`^[:\*]|\{[\w\.\-]+\}|\*`, part) { + part = "*fuzz" + // If it's a fuzzy node, it creates a "*list" item - which is a list - in the hash map. + // All the sub router items from this fuzzy node will also be added to its "*list" item. + if v, ok := p.(map[string]interface{})["*list"]; !ok { + newListForFuzzy := glist.New() + p.(map[string]interface{})["*list"] = newListForFuzzy + lists = append(lists, newListForFuzzy) + } else { + lists = append(lists, v.(*glist.List)) + } + } + // Make a new bucket for current node. + if _, ok := p.(map[string]interface{})[part]; !ok { + p.(map[string]interface{})[part] = make(map[string]interface{}) + } + // Loop to next bucket. + p = p.(map[string]interface{})[part] + // The leaf is a hash map and must have an item named "*list", which contains the router item. + // The leaf can be furthermore extended by adding more ket-value pairs into its map. + // Note that the `v != "*fuzz"` comparison is required as the list might be added in the former + // fuzzy checks. + if i == len(array)-1 && part != "*fuzz" { + if v, ok := p.(map[string]interface{})["*list"]; !ok { + list := glist.New() + p.(map[string]interface{})["*list"] = list + lists = append(lists, list) + } else { + lists = append(lists, v.(*glist.List)) + } + } + } + for k, v := range array { if len(v) == 0 { continue @@ -135,8 +184,8 @@ func (s *Server) setHandler(pattern string, handler *handlerItem) { } } - // 上面循环后得到的lists是该路由规则一路匹配下来相关的模糊匹配链表(注意不是这棵树所有的链表)。 - // 下面从头开始遍历每个节点的模糊匹配链表,将该路由项插入进去(按照优先级高的放在lists链表的前面) + // It iterates the list array of , compares priorities and inserts the new router item in + // the proper position of each list. The priority of the list is ordered from high to low. item := (*handlerItem)(nil) for _, l := range lists { pushed := false @@ -173,6 +222,7 @@ func (s *Server) setHandler(pattern string, handler *handlerItem) { // Append the route. s.routesMap[regKey] = append(s.routesMap[regKey], routeItem) } + gutil.Dump(s.serveTree) } // 对比两个handlerItem的优先级,需要非常注意的是,注意新老对比项的参数先后顺序。 @@ -312,7 +362,7 @@ func (s *Server) patternToRegRule(rule string) (regrule string, names []string) regrule += `/{0,1}.*` } default: - // 特殊字符替换 + // Special chars replacement. v = gstr.ReplaceByMap(v, map[string]string{ `.`: `\.`, `+`: `\+`, diff --git a/net/ghttp/ghttp_server_router_hook.go b/net/ghttp/ghttp_server_router_hook.go index fcce0aa66..63d170dba 100644 --- a/net/ghttp/ghttp_server_router_hook.go +++ b/net/ghttp/ghttp_server_router_hook.go @@ -66,8 +66,3 @@ func (s *Server) niceCallHookHandler(f HandlerFunc, r *Request) (err interface{} f(r) return } - -// 生成hook key,如果是hook key,那么使用'%'符号分隔 -func (s *Server) handlerKey(hook, method, path, domain string) string { - return hook + "%" + s.serveHandlerKey(method, path, domain) -} diff --git a/net/ghttp/ghttp_server_router_serve.go b/net/ghttp/ghttp_server_router_serve.go index a3b728ff9..7306a5614 100644 --- a/net/ghttp/ghttp_server_router_serve.go +++ b/net/ghttp/ghttp_server_router_serve.go @@ -15,15 +15,14 @@ import ( "github.com/gogf/gf/text/gregex" ) -// 缓存数据项 +// handlerCacheItem is a item for router cache. type handlerCacheItem struct { parsedItems []*handlerParsedItem hasHook bool hasServe bool } -// 查询请求处理方法. -// 内部带锁机制,可以并发读,但是不能并发写;并且有缓存机制,按照Host、Method、Path进行缓存. +// getHandlersWithCache searches the router item with cache feature for given request. func (s *Server) getHandlersWithCache(r *Request) (parsedItems []*handlerParsedItem, hasHook, hasServe bool) { value := s.serveCache.GetOrSetFunc(s.serveHandlerKey(r.Method, r.URL.Path, r.GetHost()), func() interface{} { parsedItems, hasHook, hasServe = s.searchHandlers(r.Method, r.URL.Path, r.GetHost()) @@ -39,18 +38,19 @@ func (s *Server) getHandlersWithCache(r *Request) (parsedItems []*handlerParsedI return } -// 路由注册方法检索,返回所有该路由的注册函数,构造成数组返回 +// searchHandlers retrieves and returns the routers with given parameters. +// Note that the returned routers contain serving handler, middleware handlers and hook handlers. func (s *Server) searchHandlers(method, path, domain string) (parsedItems []*handlerParsedItem, hasHook, hasServe bool) { if len(path) == 0 { return nil, false, false } - // 遍历检索的域名列表,优先遍历默认域名 + // Default domain has the most priority when iteration. domains := []string{gDEFAULT_DOMAIN} if !strings.EqualFold(gDEFAULT_DOMAIN, domain) { domains = append(domains, domain) } - // URL.Path层级拆分 - array := ([]string)(nil) + // Split the URL.path to separate parts. + var array []string if strings.EqualFold("/", path) { array = []string{"/"} } else { @@ -58,42 +58,40 @@ func (s *Server) searchHandlers(method, path, domain string) (parsedItems []*han } parsedItemList := glist.New() lastMiddlewareElem := (*glist.Element)(nil) - repeatHandlerCheckMap := make(map[int]struct{}) for _, domain := range domains { p, ok := s.serveTree[domain] if !ok { continue } - // 多层链表(每个节点都有一个*list链表)的目的是当叶子节点未有任何规则匹配时,让父级模糊匹配规则继续处理 lists := make([]*glist.List, 0, 16) - for k, v := range array { + for i, part := range array { // In case of double '/' URI, eg: /user//index - if v == "" { + if part == "" { continue } - if _, ok := p.(map[string]interface{})["*list"]; ok { - lists = append(lists, p.(map[string]interface{})["*list"].(*glist.List)) + if v, ok := p.(map[string]interface{})["*list"]; ok { + lists = append(lists, v.(*glist.List)) } - if _, ok := p.(map[string]interface{})[v]; ok { - p = p.(map[string]interface{})[v] - if k == len(array)-1 { - if _, ok := p.(map[string]interface{})["*list"]; ok { - lists = append(lists, p.(map[string]interface{})["*list"].(*glist.List)) + if _, ok := p.(map[string]interface{})[part]; ok { + p = p.(map[string]interface{})[part] + if i == len(array)-1 { + if v, ok := p.(map[string]interface{})["*list"]; ok { + lists = append(lists, v.(*glist.List)) break } } } else { - if _, ok := p.(map[string]interface{})["*fuzz"]; ok { - p = p.(map[string]interface{})["*fuzz"] + if v, ok := p.(map[string]interface{})["*fuzz"]; ok { + p = v } } // 如果是叶子节点,同时判断当前层级的"*fuzz"键名,解决例如:/user/*action 匹配 /user 的规则 - if k == len(array)-1 { - if _, ok := p.(map[string]interface{})["*fuzz"]; ok { - p = p.(map[string]interface{})["*fuzz"] + if i == len(array)-1 { + if v, ok := p.(map[string]interface{})["*fuzz"]; ok { + p = v } - if _, ok := p.(map[string]interface{})["*list"]; ok { - lists = append(lists, p.(map[string]interface{})["*list"].(*glist.List)) + if v, ok := p.(map[string]interface{})["*list"]; ok { + lists = append(lists, v.(*glist.List)) } } } @@ -102,12 +100,6 @@ func (s *Server) searchHandlers(method, path, domain string) (parsedItems []*han for i := len(lists) - 1; i >= 0; i-- { for e := lists[i].Front(); e != nil; e = e.Next() { item := e.Value.(*handlerItem) - // 主要是用于路由注册函数的重复添加判断(特别是中间件和钩子函数) - if _, ok := repeatHandlerCheckMap[item.itemId]; ok { - continue - } else { - repeatHandlerCheckMap[item.itemId] = struct{}{} - } // 服务路由函数只能添加一次,将重复判断放在这里提高检索效率 if hasServe { switch item.itemType { @@ -115,8 +107,7 @@ func (s *Server) searchHandlers(method, path, domain string) (parsedItems []*han continue } } - // 动态匹配规则带有gDEFAULT_METHOD的情况,不会像静态规则那样直接解析为所有的HTTP METHOD存储 - if strings.EqualFold(item.router.Method, gDEFAULT_METHOD) || strings.EqualFold(item.router.Method, method) { + if item.router.Method == gDEFAULT_METHOD || item.router.Method == method { // 注意当不带任何动态路由规则时,len(match) == 1 if match, err := gregex.MatchString(item.router.RegRule, path); err == nil && len(match) > 0 { parsedItem := &handlerParsedItem{item, nil} @@ -207,7 +198,7 @@ func (item *handlerParsedItem) MarshalJSON() ([]byte, error) { return json.Marshal(item.handler) } -// 生成回调方法查询的Key +// serveHandlerKey creates and returns a cache key for router. func (s *Server) serveHandlerKey(method, path, domain string) string { if len(domain) > 0 { domain = "@" + domain diff --git a/net/ghttp/ghttp_unit_router_controller_rest_test.go b/net/ghttp/ghttp_unit_router_controller_rest_test.go index a5b7e3cd1..149e7d11f 100644 --- a/net/ghttp/ghttp_unit_router_controller_rest_test.go +++ b/net/ghttp/ghttp_unit_router_controller_rest_test.go @@ -46,14 +46,6 @@ func (c *ControllerRest) Delete() { c.Response.Write("Controller Delete") } -func (c *ControllerRest) Patch() { - c.Response.Write("Controller Patch") -} - -func (c *ControllerRest) Options() { - c.Response.Write("Controller Options") -} - func (c *ControllerRest) Head() { c.Response.Header().Set("head-ok", "1") } @@ -78,8 +70,6 @@ func Test_Router_ControllerRest(t *testing.T) { gtest.Assert(client.PutContent("/"), "1Controller Put2") gtest.Assert(client.PostContent("/"), "1Controller Post2") gtest.Assert(client.DeleteContent("/"), "1Controller Delete2") - gtest.Assert(client.PatchContent("/"), "1Controller Patch2") - gtest.Assert(client.OptionsContent("/"), "1Controller Options2") resp1, err := client.Head("/") if err == nil { defer resp1.Close() @@ -91,8 +81,6 @@ func Test_Router_ControllerRest(t *testing.T) { gtest.Assert(client.PutContent("/controller-rest/put"), "1Controller Put2") gtest.Assert(client.PostContent("/controller-rest/post"), "1Controller Post2") gtest.Assert(client.DeleteContent("/controller-rest/delete"), "1Controller Delete2") - gtest.Assert(client.PatchContent("/controller-rest/patch"), "1Controller Patch2") - gtest.Assert(client.OptionsContent("/controller-rest/options"), "1Controller Options2") resp2, err := client.Head("/controller-rest/head") if err == nil { defer resp2.Close() diff --git a/net/ghttp/ghttp_unit_router_hook_test.go b/net/ghttp/ghttp_unit_router_hook_test.go index e7a2daca2..1edc604fe 100644 --- a/net/ghttp/ghttp_unit_router_hook_test.go +++ b/net/ghttp/ghttp_unit_router_hook_test.go @@ -50,6 +50,7 @@ func Test_Router_Hook_Fuzzy_Router(t *testing.T) { pattern1 := "/:name/info" s.BindHookHandlerByMap(pattern1, map[string]ghttp.HandlerFunc{ ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) { + fmt.Println("called") r.SetParam("uid", i) i++ }, @@ -58,19 +59,19 @@ func Test_Router_Hook_Fuzzy_Router(t *testing.T) { r.Response.Write(r.Get("uid")) }) - pattern2 := "/{object}/list/{page}.java" - s.BindHookHandlerByMap(pattern2, map[string]ghttp.HandlerFunc{ - ghttp.HOOK_BEFORE_OUTPUT: func(r *ghttp.Request) { - r.Response.SetBuffer([]byte( - fmt.Sprint(r.Get("object"), "&", r.Get("page"), "&", i), - )) - }, - }) - s.BindHandler(pattern2, func(r *ghttp.Request) { - r.Response.Write(r.Router.Uri) - }) + //pattern2 := "/{object}/list/{page}.java" + //s.BindHookHandlerByMap(pattern2, map[string]ghttp.HandlerFunc{ + // ghttp.HOOK_BEFORE_OUTPUT: func(r *ghttp.Request) { + // r.Response.SetBuffer([]byte( + // fmt.Sprint(r.Get("object"), "&", r.Get("page"), "&", i), + // )) + // }, + //}) + //s.BindHandler(pattern2, func(r *ghttp.Request) { + // r.Response.Write(r.Router.Uri) + //}) s.SetPort(p) - s.SetDumpRouterMap(false) + //s.SetDumpRouterMap(false) s.Start() defer s.Shutdown() diff --git a/os/gfile/gfile.go b/os/gfile/gfile.go index 94c222819..5c101e632 100644 --- a/os/gfile/gfile.go +++ b/os/gfile/gfile.go @@ -10,6 +10,7 @@ package gfile import ( "bytes" "errors" + "github.com/gogf/gf/text/gstr" "os" "os/exec" "os/user" @@ -58,7 +59,9 @@ func Mkdir(path string) error { func Create(path string) (*os.File, error) { dir := Dir(path) if !Exists(dir) { - Mkdir(dir) + if err := Mkdir(dir); err != nil { + return nil, err + } } return os.Create(path) } @@ -93,7 +96,14 @@ func OpenWithFlagPerm(path string, flag int, perm os.FileMode) (*os.File, error) // Join joins string array paths with file separator of current system. func Join(paths ...string) string { - return strings.Join(paths, Separator) + var s string + for _, path := range paths { + if s != "" { + s += Separator + } + s += gstr.TrimRight(path, Separator) + } + return s } // Exists checks whether given exist. diff --git a/text/gregex/gregex_cache.go b/text/gregex/gregex_cache.go index c87168e63..979ec19cd 100644 --- a/text/gregex/gregex_cache.go +++ b/text/gregex/gregex_cache.go @@ -14,7 +14,7 @@ import ( var ( regexMu = sync.RWMutex{} // Cache for regex object. - // TODO There's no expiring logic for this map. + // Note that there's no expiring logic for this map. regexMap = make(map[string]*regexp.Regexp) ) @@ -22,29 +22,25 @@ var ( // It uses cache to enhance the performance for compiling regular expression pattern, // which means, it will return the same *regexp.Regexp object with the same regular // expression pattern. -func getRegexp(pattern string) (*regexp.Regexp, error) { - if r := getCache(pattern); r != nil { - return r, nil - } - if r, err := regexp.Compile(pattern); err == nil { - setCache(pattern, r) - return r, nil - } else { - return nil, err - } -} - -// getCache returns *regexp.Regexp object from cache by given , for internal usage. -func getCache(pattern string) (regex *regexp.Regexp) { +// +// It is concurrent-safe for multiple goroutines. +func getRegexp(pattern string) (regex *regexp.Regexp, err error) { + // Retrieve the regular expression object using reading lock. regexMu.RLock() regex = regexMap[pattern] regexMu.RUnlock() - return -} - -// setCache stores *regexp.Regexp object into cache, for internal usage. -func setCache(pattern string, regex *regexp.Regexp) { + if regex != nil { + return + } + // If it does not exist in the cache, + // it compiles the pattern and creates one. + regex, err = regexp.Compile(pattern) + if err != nil { + return + } + // Cache the result object using writing lock. regexMu.Lock() regexMap[pattern] = regex regexMu.Unlock() + return } diff --git a/util/gpage/gpage.go b/util/gpage/gpage.go index a231c74bc..01b4e7459 100644 --- a/util/gpage/gpage.go +++ b/util/gpage/gpage.go @@ -10,7 +10,7 @@ package gpage import ( "fmt" "math" - url2 "net/url" + "net/url" "strings" "github.com/gogf/gf/net/ghttp" @@ -19,30 +19,27 @@ import ( "github.com/gogf/gf/util/gconv" ) -// 分页对象 +// Page is the pagination implementer. type Page struct { - Url *url2.URL // 当前页面的URL对象 - Router *ghttp.Router // 当前页面的路由对象(与gf框架耦合,在静态分页下有效) - UrlTemplate string // URL生成规则,内部可使用{.page}变量指定页码 - TotalSize int // 总共数据条数 - TotalPage int // 总页数 - CurrentPage int // 当前页码 - PageName string // 分页参数名称(GET参数) - NextPageTag string // 下一页标签 - PrevPageTag string // 上一页标签 - FirstPageTag string // 首页标签 - LastPageTag string // 尾页标签 - PrevBar string // 上一分页条 - NextBar string // 下一分页条 - PageBarNum int // 控制分页条的数量 - AjaxActionName string // AJAX方法名,当该属性有值时,表示使用AJAX分页 + UrlTemplate string // Custom url template for page url producing. + TotalSize int // Total size. + TotalPage int // Total page, which is automatically calculated. + CurrentPage int // Current page number >= 1. + PageName string // Page variable name. It's "page" in default. + NextPageTag string // Tag name for next p. + PrevPageTag string // Tag name for prev p. + FirstPageTag string // Tag name for first p. + LastPageTag string // Tag name for last p. + PrevBar string // Tag string for prev bar. + NextBar string // Tag string for next bar. + PageBarNum int // Page bar number for displaying. + AjaxActionName string // Ajax function name. Ajax is enabled if this attribute is not empty. } // 创建一个分页对象,输入参数分别为: // 总数量、每页数量、当前页码、当前的URL(URI+QUERY)、(可选)路由规则(例如: /user/list/:page、/order/list/*page、/order/list/{page}.html) -func New(TotalSize, perPage int, CurrentPage interface{}, url string, router ...*ghttp.Router) *Page { - u, _ := url2.Parse(url) - page := &Page{ +func New(totalSize, pageSize, currentPage int, urlTemplate string) *Page { + p := &Page{ PageName: "page", PrevPageTag: "<", NextPageTag: ">", @@ -50,34 +47,20 @@ func New(TotalSize, perPage int, CurrentPage interface{}, url string, router ... LastPageTag: ">|", PrevBar: "<<", NextBar: ">>", - TotalSize: TotalSize, - TotalPage: int(math.Ceil(float64(TotalSize) / float64(perPage))), + TotalSize: totalSize, + TotalPage: int(math.Ceil(float64(totalSize) / float64(pageSize))), CurrentPage: 1, PageBarNum: 10, - Url: u, + UrlTemplate: urlTemplate, } - curPage := gconv.Int(CurrentPage) - if curPage > 0 { - page.CurrentPage = curPage + if currentPage > 0 { + p.CurrentPage = currentPage } - if len(router) > 0 { - page.Router = router[0] - } - return page -} - -// 启用AJAX分页 -func (page *Page) EnableAjax(actionName string) { - page.AjaxActionName = actionName -} - -// 设置URL生成规则模板,模板中可使用{.page}变量指定页码位置 -func (page *Page) SetUrlTemplate(template string) { - page.UrlTemplate = template + return p } // 获取显示"下一页"的内容. -func (page *Page) NextPage(styles ...string) string { +func (p *Page) NextPage(styles ...string) string { var curStyle, style string if len(styles) > 0 { curStyle = styles[0] @@ -85,14 +68,14 @@ func (page *Page) NextPage(styles ...string) string { if len(styles) > 1 { style = styles[0] } - if page.CurrentPage < page.TotalPage { - return page.GetLink(page.GetUrl(page.CurrentPage+1), page.NextPageTag, "下一页", style) + if p.CurrentPage < p.TotalPage { + return p.GetLink(p.GetUrl(p.CurrentPage+1), p.NextPageTag, "下一页", style) } - return fmt.Sprintf(`%s`, curStyle, page.NextPageTag) + return fmt.Sprintf(`%s`, curStyle, p.NextPageTag) } // 获取显示“上一页”的内容 -func (page *Page) PrevPage(styles ...string) string { +func (p *Page) PrevPage(styles ...string) string { var curStyle, style string if len(styles) > 0 { curStyle = styles[0] @@ -100,14 +83,14 @@ func (page *Page) PrevPage(styles ...string) string { if len(styles) > 1 { style = styles[0] } - if page.CurrentPage > 1 { - return page.GetLink(page.GetUrl(page.CurrentPage-1), page.PrevPageTag, "上一页", style) + if p.CurrentPage > 1 { + return p.GetLink(p.GetUrl(p.CurrentPage-1), p.PrevPageTag, "上一页", style) } - return fmt.Sprintf(`%s`, curStyle, page.PrevPageTag) + return fmt.Sprintf(`%s`, curStyle, p.PrevPageTag) } // 获取显示“首页”的代码 -func (page *Page) FirstPage(styles ...string) string { +func (p *Page) FirstPage(styles ...string) string { var curStyle, style string if len(styles) > 0 { curStyle = styles[0] @@ -115,14 +98,14 @@ func (page *Page) FirstPage(styles ...string) string { if len(styles) > 1 { style = styles[0] } - if page.CurrentPage == 1 { - return fmt.Sprintf(`%s`, curStyle, page.FirstPageTag) + if p.CurrentPage == 1 { + return fmt.Sprintf(`%s`, curStyle, p.FirstPageTag) } - return page.GetLink(page.GetUrl(1), page.FirstPageTag, "第一页", style) + return p.GetLink(p.GetUrl(1), p.FirstPageTag, "第一页", style) } // 获取显示“尾页”的内容 -func (page *Page) LastPage(styles ...string) string { +func (p *Page) LastPage(styles ...string) string { var curStyle, style string if len(styles) > 0 { curStyle = styles[0] @@ -130,14 +113,14 @@ func (page *Page) LastPage(styles ...string) string { if len(styles) > 1 { style = styles[0] } - if page.CurrentPage == page.TotalPage { - return fmt.Sprintf(`%s`, curStyle, page.LastPageTag) + if p.CurrentPage == p.TotalPage { + return fmt.Sprintf(`%s`, curStyle, p.LastPageTag) } - return page.GetLink(page.GetUrl(page.TotalPage), page.LastPageTag, "最后页", style) + return p.GetLink(p.GetUrl(p.TotalPage), p.LastPageTag, "最后页", style) } // 获得分页条列表内容 -func (page *Page) PageBar(styles ...string) string { +func (p *Page) PageBar(styles ...string) string { var curStyle, style string if len(styles) > 0 { curStyle = styles[0] @@ -145,19 +128,19 @@ func (page *Page) PageBar(styles ...string) string { if len(styles) > 1 { style = styles[0] } - plus := int(math.Ceil(float64(page.PageBarNum / 2))) - if page.PageBarNum-plus+page.CurrentPage > page.TotalPage { - plus = page.PageBarNum - page.TotalPage + page.CurrentPage + plus := int(math.Ceil(float64(p.PageBarNum / 2))) + if p.PageBarNum-plus+p.CurrentPage > p.TotalPage { + plus = p.PageBarNum - p.TotalPage + p.CurrentPage } - begin := page.CurrentPage - plus + 1 + begin := p.CurrentPage - plus + 1 if begin < 1 { begin = 1 } ret := "" - for i := begin; i < begin+page.PageBarNum; i++ { - if i <= page.TotalPage { - if i != page.CurrentPage { - ret += page.GetLink(page.GetUrl(i), gconv.String(i), style, "") + for i := begin; i < begin+p.PageBarNum; i++ { + if i <= p.TotalPage { + if i != p.CurrentPage { + ret += p.GetLink(p.GetUrl(i), gconv.String(i), style, "") } else { ret += fmt.Sprintf(`%d`, curStyle, i) } @@ -169,13 +152,13 @@ func (page *Page) PageBar(styles ...string) string { } // 获取基于select标签的显示跳转按钮的代码 -func (page *Page) SelectBar() string { - ret := `` + for i := 1; i <= p.TotalPage; i++ { + if i == p.CurrentPage { + ret += fmt.Sprintf(``, p.GetUrl(i), i) } else { - ret += fmt.Sprintf(``, page.GetUrl(i), i) + ret += fmt.Sprintf(``, p.GetUrl(i), i) } } ret += "" @@ -183,117 +166,87 @@ func (page *Page) SelectBar() string { } // 预定义的分页显示风格内容 -func (page *Page) GetContent(mode int) string { +func (p *Page) GetContent(mode int) string { switch mode { case 1: - page.NextPageTag = "下一页" - page.PrevPageTag = "上一页" + p.NextPageTag = "下一页" + p.PrevPageTag = "上一页" return fmt.Sprintf( `%s %d %s`, - page.PrevPage(), - page.CurrentPage, - page.NextPage(), + p.PrevPage(), + p.CurrentPage, + p.NextPage(), ) case 2: - page.NextPageTag = "下一页>>" - page.PrevPageTag = "<<上一页" - page.FirstPageTag = "首页" - page.LastPageTag = "尾页" + p.NextPageTag = "下一页>>" + p.PrevPageTag = "<<上一页" + p.FirstPageTag = "首页" + p.LastPageTag = "尾页" return fmt.Sprintf( `%s%s[第%d页]%s%s第%s页`, - page.FirstPage(), - page.PrevPage(), - page.CurrentPage, - page.NextPage(), - page.LastPage(), - page.SelectBar(), + p.FirstPage(), + p.PrevPage(), + p.CurrentPage, + p.NextPage(), + p.LastPage(), + p.SelectBar(), ) case 3: - page.NextPageTag = "下一页" - page.PrevPageTag = "上一页" - page.FirstPageTag = "首页" - page.LastPageTag = "尾页" - pageStr := page.FirstPage() - pageStr += page.PrevPage() - pageStr += page.PageBar("current") - pageStr += page.NextPage() - pageStr += page.LastPage() + p.NextPageTag = "下一页" + p.PrevPageTag = "上一页" + p.FirstPageTag = "首页" + p.LastPageTag = "尾页" + pageStr := p.FirstPage() + pageStr += p.PrevPage() + pageStr += p.PageBar("current") + pageStr += p.NextPage() + pageStr += p.LastPage() pageStr += fmt.Sprintf( `当前页%d/%d 共%d条`, - page.CurrentPage, - page.TotalPage, - page.TotalSize, + p.CurrentPage, + p.TotalPage, + p.TotalSize, ) return pageStr case 4: - page.NextPageTag = "下一页" - page.PrevPageTag = "上一页" - page.FirstPageTag = "首页" - page.LastPageTag = "尾页" - pageStr := page.FirstPage() - pageStr += page.PrevPage() - pageStr += page.PageBar("current") - pageStr += page.NextPage() - pageStr += page.LastPage() + p.NextPageTag = "下一页" + p.PrevPageTag = "上一页" + p.FirstPageTag = "首页" + p.LastPageTag = "尾页" + pageStr := p.FirstPage() + pageStr += p.PrevPage() + pageStr += p.PageBar("current") + pageStr += p.NextPage() + pageStr += p.LastPage() return pageStr } return "" } // 为指定的页面返回地址值 -func (page *Page) GetUrl(pageNo int) string { - // 复制一个URL对象 - url := *page.Url - if len(page.UrlTemplate) == 0 && page.Router != nil { - page.UrlTemplate = page.makeUrlTemplate(url.Path, page.Router) - } - if len(page.UrlTemplate) > 0 { - // 指定URL生成模板 - url.Path = gstr.Replace(page.UrlTemplate, "{.page}", gconv.String(pageNo)) +func (p *Page) GetUrl(pageNo int) string { + pattern := fmt.Sprintf(`(:%s|\*%s|\.%s)`, p.PageName, p.PageName, p.PageName) + result, _ := gregex.ReplaceString(pattern, pageNo, p.UrlTemplate) + url.Path = gstr.Replace(p.UrlTemplate, "{.page}", gconv.String(pageNo)) return url.String() } - values := page.Url.Query() - values.Set(page.PageName, gconv.String(pageNo)) + values := p.Url.Query() + values.Set(p.PageName, gconv.String(pageNo)) url.RawQuery = values.Encode() return url.String() } -// 根据当前URL以及注册路由信息计算出对应的URL模板 -func (page *Page) makeUrlTemplate(url string, router *ghttp.Router) (tpl string) { - if page.Router != nil && len(router.RegNames) > 0 { - if match, err := gregex.MatchString(router.RegRule, url); err == nil && len(match) > 0 { - if len(match) > len(router.RegNames) { - tpl = router.Uri - hasPageName := false - for i, name := range router.RegNames { - rule := fmt.Sprintf(`[:\*]%s|\{%s\}`, name, name) - if !hasPageName && strings.Compare(name, page.PageName) == 0 { - hasPageName = true - tpl, _ = gregex.ReplaceString(rule, `{.page}`, tpl) - } else { - tpl, _ = gregex.ReplaceString(rule, match[i+1], tpl) - } - } - if !hasPageName { - tpl = "" - } - } - } - } - return -} - // 获取链接地址 -func (page *Page) GetLink(url, text, title, style string) string { +func (p *Page) GetLink(url, text, title, style string) string { if len(style) > 0 { style = fmt.Sprintf(`class="%s" `, style) } - if len(page.AjaxActionName) > 0 { - return fmt.Sprintf(`%s`, style, page.AjaxActionName, url, text) + if len(p.AjaxActionName) > 0 { + return fmt.Sprintf(`%s`, style, p.AjaxActionName, url, text) } else { return fmt.Sprintf(`%s`, style, url, title, text) }