improve router feature for ghttp.Server

This commit is contained in:
John
2020-03-04 17:29:23 +08:00
parent b0ef63fc9d
commit d8a7e36478
18 changed files with 309 additions and 301 deletions

View File

@ -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()
}

View File

@ -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 |

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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 != "" {

View File

@ -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())
}

View File

@ -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.

View File

@ -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 <lists>, 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{
`.`: `\.`,
`+`: `\+`,

View File

@ -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)
}

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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 <path> exist.

View File

@ -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 <pattern>, 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
}

View File

@ -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(`<span class="%s">%s</span>`, curStyle, page.NextPageTag)
return fmt.Sprintf(`<span class="%s">%s</span>`, 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(`<span class="%s">%s</span>`, curStyle, page.PrevPageTag)
return fmt.Sprintf(`<span class="%s">%s</span>`, 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(`<span class="%s">%s</span>`, curStyle, page.FirstPageTag)
if p.CurrentPage == 1 {
return fmt.Sprintf(`<span class="%s">%s</span>`, 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(`<span class="%s">%s</span>`, curStyle, page.LastPageTag)
if p.CurrentPage == p.TotalPage {
return fmt.Sprintf(`<span class="%s">%s</span>`, 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(`<span class="%s">%d</span>`, curStyle, i)
}
@ -169,13 +152,13 @@ func (page *Page) PageBar(styles ...string) string {
}
// 获取基于select标签的显示跳转按钮的代码
func (page *Page) SelectBar() string {
ret := `<select name="gpage_select" onchange="window.location.href=this.value">`
for i := 1; i <= page.TotalPage; i++ {
if i == page.CurrentPage {
ret += fmt.Sprintf(`<option value="%s" selected>%d</option>`, page.GetUrl(i), i)
func (p *Page) SelectBar() string {
ret := `<select name="GPageSelect" onchange="window.location.href=this.value">`
for i := 1; i <= p.TotalPage; i++ {
if i == p.CurrentPage {
ret += fmt.Sprintf(`<option value="%s" selected>%d</option>`, p.GetUrl(i), i)
} else {
ret += fmt.Sprintf(`<option value="%s">%d</option>`, page.GetUrl(i), i)
ret += fmt.Sprintf(`<option value="%s">%d</option>`, p.GetUrl(i), i)
}
}
ret += "</select>"
@ -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 <span class="current">%d</span> %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<span class="current">[第%d页]</span>%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(
`<span>当前页%d/%d</span> <span>共%d条</span>`,
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(`<a %shref='#' onclick="%s('%s')">%s</a>`, style, page.AjaxActionName, url, text)
if len(p.AjaxActionName) > 0 {
return fmt.Sprintf(`<a %shref='#' onclick="%s('%s')">%s</a>`, style, p.AjaxActionName, url, text)
} else {
return fmt.Sprintf(`<a %shref="%s" title="%s">%s</a>`, style, url, title, text)
}