├── .github └── workflows │ └── go.yml ├── .travis.yml ├── LICENSE ├── README.md ├── bind.go ├── bind_test.go ├── cache ├── cache.go ├── cache_test.go ├── redis │ ├── cache_redis.go │ └── cache_redis_test.go └── runtime │ ├── cache_runtime.go │ └── cache_runtime_test.go ├── config ├── config_json.go ├── config_xml.go ├── config_yaml.go ├── configs.go ├── configs_test.go ├── configset.go ├── defaults.go └── testdata │ ├── dotweb.conf │ ├── dotweb.json │ └── dotweb.yaml ├── consts.go ├── context.go ├── context_test.go ├── core ├── concurrenceMap.go ├── concurrenceMap_test.go ├── hideReaddirFS.go ├── hideReaddirFS_test.go ├── htmlx.go ├── state.go └── state_test.go ├── docs └── GUIDE.md ├── dotweb.go ├── dotweb_sysgroup.go ├── dotweb_test.go ├── example ├── README.md ├── bind │ └── main.go ├── config │ ├── dotweb.conf │ ├── dotweb.json │ ├── dotweb.yaml │ ├── main.go │ └── userconf.xml ├── main.go ├── middleware │ └── main.go ├── mock │ └── main.go └── router │ └── main.go ├── framework ├── convert │ ├── convert.go │ └── convert_test.go ├── crypto │ ├── cryptos.go │ ├── cryptos_test.go │ ├── des │ │ ├── des.go │ │ └── des_test.go │ └── uuid │ │ ├── uuid.go │ │ └── uuid_test.go ├── encodes │ ├── base64x │ │ ├── base64util.go │ │ └── base64util_test.go │ └── gob │ │ ├── gobutil.go │ │ └── gobutil_test.go ├── exception │ ├── exception.go │ └── exception_test.go ├── file │ ├── file.go │ ├── file_test.go │ ├── path.go │ ├── path_test.go │ └── testdata │ │ └── file.test ├── hystrix │ ├── counter.go │ └── hystrix.go ├── json │ ├── jsonutil.go │ └── jsonutil_test.go ├── redis │ ├── redisutil.go │ └── redisutil_test.go ├── reflects │ ├── reflects.go │ └── reflects_test.go ├── stringx │ ├── stringx.go │ └── stringx_test.go └── sysx │ └── sysx.go ├── go.mod ├── go.sum ├── group.go ├── logger ├── logger.go ├── logger_test.go ├── xlog.go └── xlog_test.go ├── logo.png ├── middleware.go ├── middleware_test.go ├── mock.go ├── plugin.go ├── plugin_test.go ├── render.go ├── render_test.go ├── request.go ├── response.go ├── response_test.go ├── router.go ├── router_test.go ├── scripts └── ut.sh ├── server.go ├── server_test.go ├── session ├── session.go ├── session_test.go ├── sessionstate.go ├── sessionstate_test.go ├── store_redis.go ├── store_redis_test.go ├── store_runtime.go └── store_runtime_test.go ├── test ├── assert.go └── util.go ├── tools.go ├── tools_test.go ├── tree.go ├── tree_test.go ├── uploadfile.go ├── uploadfile_test.go ├── utils_test.go └── version.MD /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.13 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.13 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | if [ -f Gopkg.toml ]; then 23 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 24 | dep ensure 25 | fi 26 | 27 | - name: Build 28 | run: go build -v . 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: required 3 | 4 | go: 5 | - 1.9 6 | 7 | env: 8 | - GO15VENDOREXPERIMENT="1" 9 | 10 | before_install: 11 | - Project=devfeel 12 | 13 | script: 14 | - go build -o "$Project" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 devfeel pzrr@qq.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bind.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "errors" 7 | "strings" 8 | 9 | "github.com/devfeel/dotweb/framework/reflects" 10 | ) 11 | 12 | const ( 13 | defaultTagName = "form" 14 | jsonTagName = "json" 15 | ) 16 | 17 | type ( 18 | // Binder is the interface that wraps the Bind method. 19 | Binder interface { 20 | Bind(interface{}, Context) error 21 | BindJsonBody(interface{}, Context) error 22 | } 23 | 24 | binder struct{} 25 | ) 26 | 27 | // Bind decode req.Body or form-value to struct 28 | func (b *binder) Bind(i interface{}, ctx Context) (err error) { 29 | req := ctx.Request() 30 | ctype := req.Header.Get(HeaderContentType) 31 | if req.Body == nil { 32 | err = errors.New("request body can't be empty") 33 | return err 34 | } 35 | err = errors.New("request unsupported MediaType -> " + ctype) 36 | switch { 37 | case strings.HasPrefix(ctype, MIMEApplicationJSON): 38 | err = json.Unmarshal(ctx.Request().PostBody(), i) 39 | case strings.HasPrefix(ctype, MIMEApplicationXML): 40 | err = xml.Unmarshal(ctx.Request().PostBody(), i) 41 | // case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm), 42 | // strings.HasPrefix(ctype, MIMETextHTML): 43 | // err = reflects.ConvertMapToStruct(defaultTagName, i, ctx.FormValues()) 44 | default: 45 | // check is use json tag, fixed for issue #91 46 | tagName := defaultTagName 47 | if ctx.HttpServer().ServerConfig().EnabledBindUseJsonTag { 48 | tagName = jsonTagName 49 | } 50 | // no check content type for fixed issue #6 51 | err = reflects.ConvertMapToStruct(tagName, i, ctx.Request().FormValues()) 52 | } 53 | return err 54 | } 55 | 56 | // BindJsonBody default use json decode req.Body to struct 57 | func (b *binder) BindJsonBody(i interface{}, ctx Context) (err error) { 58 | if ctx.Request().PostBody() == nil { 59 | err = errors.New("request body can't be empty") 60 | return err 61 | } 62 | err = json.Unmarshal(ctx.Request().PostBody(), i) 63 | return err 64 | } 65 | 66 | func newBinder() *binder { 67 | return &binder{} 68 | } 69 | -------------------------------------------------------------------------------- /bind_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "github.com/devfeel/dotweb/test" 5 | 6 | "testing" 7 | ) 8 | 9 | type Person struct { 10 | Hair string 11 | HasGlass bool 12 | Age int 13 | Legs []string 14 | } 15 | 16 | // json 17 | func TestBinder_Bind_json(t *testing.T) { 18 | 19 | binder := newBinder() 20 | 21 | if binder == nil { 22 | t.Error("binder can not be nil!") 23 | } 24 | 25 | // init DotServer 26 | app := New() 27 | 28 | if app == nil { 29 | t.Error("app can not be nil!") 30 | } 31 | 32 | // expected 33 | expected := &Person{ 34 | Hair: "Brown", 35 | HasGlass: true, 36 | Age: 10, 37 | Legs: []string{"Left", "Right"}, 38 | } 39 | 40 | // init param 41 | param := &InitContextParam{ 42 | t, 43 | expected, 44 | "application/json", 45 | test.ToJson, 46 | } 47 | 48 | // init param 49 | context := initContext(param) 50 | // actual 51 | person := &Person{} 52 | 53 | err := binder.Bind(person, context) 54 | 55 | // check error must nil 56 | test.Nil(t, err) 57 | 58 | // check expected 59 | test.Equal(t, expected, person) 60 | 61 | t.Log("person:", person) 62 | t.Log("expected:", expected) 63 | 64 | } 65 | 66 | // json 67 | func TestBinder_Bind_json_error(t *testing.T) { 68 | 69 | binder := newBinder() 70 | 71 | if binder == nil { 72 | t.Error("binder can not be nil!") 73 | } 74 | 75 | // init DotServer 76 | app := New() 77 | 78 | if app == nil { 79 | t.Error("app can not be nil!") 80 | } 81 | 82 | // expected 83 | expected := &Person{ 84 | Hair: "Brown", 85 | HasGlass: true, 86 | Age: 10, 87 | Legs: []string{"Left", "Right"}, 88 | } 89 | 90 | // init param 91 | param := &InitContextParam{ 92 | t, 93 | expected, 94 | "application/xml", 95 | test.ToJson, 96 | } 97 | 98 | // init param 99 | context := initContext(param) 100 | // actual 101 | person := &Person{} 102 | 103 | err := binder.Bind(person, context) 104 | 105 | // check error must not nil 106 | test.NotNil(t, err) 107 | } 108 | 109 | // xml 110 | func TestBinder_Bind_xml(t *testing.T) { 111 | 112 | binder := newBinder() 113 | 114 | if binder == nil { 115 | t.Error("binder can not be nil!") 116 | } 117 | 118 | // init DotServer 119 | app := New() 120 | 121 | if app == nil { 122 | t.Error("app can not be nil!") 123 | } 124 | 125 | // expected 126 | expected := &Person{ 127 | Hair: "Brown", 128 | HasGlass: true, 129 | Age: 10, 130 | Legs: []string{"Left", "Right"}, 131 | } 132 | param := &InitContextParam{ 133 | t, 134 | expected, 135 | "application/xml", 136 | test.ToXML, 137 | } 138 | 139 | // init param 140 | context := initContext(param) 141 | // actual 142 | person := &Person{} 143 | 144 | err := binder.Bind(person, context) 145 | 146 | // check error must nil 147 | test.Nil(t, err) 148 | 149 | // check expected 150 | test.Equal(t, expected, person) 151 | 152 | t.Log("person:", person) 153 | t.Log("expected:", expected) 154 | 155 | } 156 | 157 | // xml 158 | func TestBinder_Bind_xml_error(t *testing.T) { 159 | 160 | binder := newBinder() 161 | 162 | if binder == nil { 163 | t.Error("binder can not be nil!") 164 | } 165 | 166 | // init DotServer 167 | app := New() 168 | 169 | if app == nil { 170 | t.Error("app can not be nil!") 171 | } 172 | 173 | // expected 174 | expected := &Person{ 175 | Hair: "Brown", 176 | HasGlass: true, 177 | Age: 10, 178 | Legs: []string{"Left", "Right"}, 179 | } 180 | param := &InitContextParam{ 181 | t, 182 | expected, 183 | "application/json", 184 | test.ToXML, 185 | } 186 | 187 | // init param 188 | context := initContext(param) 189 | // actual 190 | person := &Person{} 191 | 192 | err := binder.Bind(person, context) 193 | 194 | // check error must not nil 195 | test.NotNil(t, err) 196 | } 197 | 198 | // else 199 | func TestBinder_Bind_default(t *testing.T) { 200 | 201 | binder := newBinder() 202 | 203 | if binder == nil { 204 | t.Error("binder can not be nil!") 205 | } 206 | 207 | // init DotServer 208 | app := New() 209 | 210 | if app == nil { 211 | t.Error("app can not be nil!") 212 | } 213 | 214 | // expected 215 | expected := &Person{ 216 | Hair: "Brown", 217 | HasGlass: true, 218 | Age: 10, 219 | Legs: []string{"Left", "Right"}, 220 | } 221 | param := &InitContextParam{ 222 | t, 223 | expected, 224 | "", 225 | test.ToDefault, 226 | } 227 | 228 | // init param 229 | context := initContext(param) 230 | 231 | form := make(map[string][]string) 232 | form["Hair"] = []string{"Brown"} 233 | form["HasGlass"] = []string{"true"} 234 | form["Age"] = []string{"10"} 235 | form["Legs"] = []string{"Left", "Right"} 236 | 237 | context.request.Form = form 238 | // actual 239 | person := &Person{} 240 | 241 | err := binder.Bind(person, context) 242 | 243 | // check error must nil 244 | test.Nil(t, err) 245 | 246 | // check expected 247 | test.Equal(t, expected, person) 248 | 249 | t.Log("person:", person) 250 | t.Log("expected:", expected) 251 | 252 | } 253 | 254 | // else 255 | func TestBinder_Bind_default_error(t *testing.T) { 256 | 257 | binder := newBinder() 258 | 259 | if binder == nil { 260 | t.Error("binder can not be nil!") 261 | } 262 | 263 | // init DotServer 264 | app := New() 265 | 266 | if app == nil { 267 | t.Error("app can not be nil!") 268 | } 269 | 270 | // expected 271 | expected := &Person{ 272 | Hair: "Brown", 273 | HasGlass: true, 274 | Age: 10, 275 | Legs: []string{"Left", "Right"}, 276 | } 277 | param := &InitContextParam{ 278 | t, 279 | expected, 280 | "application/xml", 281 | test.ToDefault, 282 | } 283 | 284 | // init param 285 | context := initContext(param) 286 | 287 | form := make(map[string][]string) 288 | form["Hair"] = []string{"Brown"} 289 | form["HasGlass"] = []string{"true"} 290 | form["Age"] = []string{"10"} 291 | form["Legs"] = []string{"Left", "Right"} 292 | 293 | context.request.Form = form 294 | // actual 295 | person := &Person{} 296 | 297 | err := binder.Bind(person, context) 298 | 299 | // check error must not nil 300 | test.NotNil(t, err) 301 | 302 | } 303 | 304 | // default 305 | // TODO:content type is null but body not null,is it right?? 306 | func TestBinder_Bind_ContentTypeNull(t *testing.T) { 307 | 308 | binder := newBinder() 309 | 310 | if binder == nil { 311 | t.Error("binder can not be nil!") 312 | } 313 | 314 | // init DotServer 315 | app := New() 316 | 317 | if app == nil { 318 | t.Error("app can not be nil!") 319 | } 320 | 321 | // expected 322 | expected := &Person{ 323 | Hair: "Brown", 324 | HasGlass: true, 325 | Age: 10, 326 | Legs: []string{"Left", "Right"}, 327 | } 328 | param := &InitContextParam{ 329 | t, 330 | expected, 331 | "", 332 | test.ToXML, 333 | } 334 | 335 | // init param 336 | context := initContext(param) 337 | // actual 338 | person := &Person{} 339 | 340 | err := binder.Bind(person, context) 341 | 342 | // check error must nil? 343 | test.Nil(t, err) 344 | } 345 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/devfeel/dotweb/cache/redis" 5 | "github.com/devfeel/dotweb/cache/runtime" 6 | ) 7 | 8 | type Cache interface { 9 | // Exist return true if value cached by given key 10 | Exists(key string) (bool, error) 11 | // Get returns value by given key 12 | Get(key string) (interface{}, error) 13 | // GetString returns value string format by given key 14 | GetString(key string) (string, error) 15 | // GetInt returns value int format by given key 16 | GetInt(key string) (int, error) 17 | // GetInt64 returns value int64 format by given key 18 | GetInt64(key string) (int64, error) 19 | // Set cache value by given key 20 | Set(key string, v interface{}, ttl int64) error 21 | // Incr increases int64-type value by given key as a counter 22 | // if key not exist, before increase set value with zero 23 | Incr(key string) (int64, error) 24 | // Decr decreases int64-type value by given key as a counter 25 | // if key not exist, before increase set value with zero 26 | Decr(key string) (int64, error) 27 | // Delete delete cache item by given key 28 | Delete(key string) error 29 | // ClearAll clear all cache items 30 | ClearAll() error 31 | } 32 | 33 | // NewRuntimeCache new runtime cache 34 | func NewRuntimeCache() Cache { 35 | return runtime.NewRuntimeCache() 36 | } 37 | 38 | // NewRedisCache create new redis cache 39 | // must set serverURL like "redis://:password@10.0.1.11:6379/0" 40 | func NewRedisCache(serverURL string) Cache { 41 | return redis.NewRedisCache(serverURL) 42 | } 43 | -------------------------------------------------------------------------------- /cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | var runtimeCache Cache 6 | var key string 7 | var val []byte 8 | 9 | func init() { 10 | runtimeCache = NewRuntimeCache() 11 | key = "abc" 12 | val = []byte("def") 13 | } 14 | 15 | func DoSet(cache Cache) { 16 | expire := 60 // expire in 60 seconds 17 | cache.Set(key, val, int64(expire)) 18 | } 19 | 20 | func DoGet(cache Cache) { 21 | cache.Get(key) 22 | } 23 | 24 | func BenchmarkTestSet(b *testing.B) { 25 | for i := 0; i < b.N; i++ { 26 | DoSet(runtimeCache) 27 | } 28 | } 29 | 30 | func BenchmarkTestGet(b *testing.B) { 31 | DoSet(runtimeCache) 32 | for i := 0; i < b.N; i++ { 33 | DoGet(runtimeCache) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cache/redis/cache_redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/devfeel/dotweb/framework/redis" 7 | ) 8 | 9 | var ( 10 | ZeroInt64 int64 = 0 11 | ) 12 | 13 | // RedisCache is redis cache adapter. 14 | // it contains serverIp for redis conn. 15 | type RedisCache struct { 16 | serverURL string // connection string, like "redis://:password@10.0.1.11:6379/0" 17 | } 18 | 19 | // NewRedisCache returns a new *RedisCache. 20 | func NewRedisCache(serverURL string) *RedisCache { 21 | cache := RedisCache{serverURL: serverURL} 22 | return &cache 23 | } 24 | 25 | // Exists check item exist in redis cache. 26 | func (ca *RedisCache) Exists(key string) (bool, error) { 27 | redisClient := redisutil.GetDefaultRedisClient(ca.serverURL) 28 | exists, err := redisClient.Exists(key) 29 | return exists, err 30 | } 31 | 32 | // Incr increase int64 counter in redis cache. 33 | func (ca *RedisCache) Incr(key string) (int64, error) { 34 | redisClient := redisutil.GetDefaultRedisClient(ca.serverURL) 35 | val, err := redisClient.INCR(key) 36 | if err != nil { 37 | return 0, err 38 | } 39 | return int64(val), nil 40 | } 41 | 42 | // Decr decrease counter in redis cache. 43 | func (ca *RedisCache) Decr(key string) (int64, error) { 44 | redisClient := redisutil.GetDefaultRedisClient(ca.serverURL) 45 | val, err := redisClient.DECR(key) 46 | if err != nil { 47 | return 0, err 48 | } 49 | return int64(val), nil 50 | } 51 | 52 | // Get cache from redis cache. 53 | // if non-existed or expired, return nil. 54 | func (ca *RedisCache) Get(key string) (interface{}, error) { 55 | redisClient := redisutil.GetDefaultRedisClient(ca.serverURL) 56 | reply, err := redisClient.GetObj(key) 57 | return reply, err 58 | } 59 | 60 | // returns value string format by given key 61 | // if non-existed or expired, return "". 62 | func (ca *RedisCache) GetString(key string) (string, error) { 63 | redisClient := redisutil.GetDefaultRedisClient(ca.serverURL) 64 | reply, err := redisClient.Get(key) 65 | return reply, err 66 | } 67 | 68 | // returns value int format by given key 69 | // if non-existed or expired, return nil. 70 | func (ca *RedisCache) GetInt(key string) (int, error) { 71 | v, err := ca.GetString(key) 72 | if err != nil || v == "" { 73 | return 0, err 74 | } else { 75 | i, e := strconv.Atoi(v) 76 | if e != nil { 77 | return 0, err 78 | } else { 79 | return i, nil 80 | } 81 | } 82 | } 83 | 84 | // returns value int64 format by given key 85 | // if non-existed or expired, return nil. 86 | func (ca *RedisCache) GetInt64(key string) (int64, error) { 87 | v, err := ca.GetString(key) 88 | if err != nil || v == "" { 89 | return ZeroInt64, err 90 | } else { 91 | i, e := strconv.ParseInt(v, 10, 64) 92 | if e != nil { 93 | return ZeroInt64, err 94 | } else { 95 | return i, nil 96 | } 97 | } 98 | } 99 | 100 | // Set cache to redis. 101 | // ttl is second, if ttl is 0, it will be forever. 102 | func (ca *RedisCache) Set(key string, value interface{}, ttl int64) error { 103 | redisClient := redisutil.GetDefaultRedisClient(ca.serverURL) 104 | var err error 105 | if ttl <= 0 { 106 | _, err = redisClient.Set(key, value) 107 | } else { 108 | _, err = redisClient.SetWithExpire(key, value, ttl) 109 | } 110 | return err 111 | } 112 | 113 | // Delete item in redis cacha. 114 | // if not exists, we think it's success 115 | func (ca *RedisCache) Delete(key string) error { 116 | redisClient := redisutil.GetDefaultRedisClient(ca.serverURL) 117 | _, err := redisClient.Del(key) 118 | return err 119 | } 120 | 121 | // ClearAll will delete all item in redis cache. 122 | // never error 123 | func (ca *RedisCache) ClearAll() error { 124 | redisClient := redisutil.GetDefaultRedisClient(ca.serverURL) 125 | redisClient.FlushDB() 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /cache/redis/cache_redis_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | -------------------------------------------------------------------------------- /cache/runtime/cache_runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | var ( 12 | // DefaultGCInterval means gc interval. 13 | DefaultGCInterval = 60 * time.Second // 1 minute 14 | ZeroInt64 int64 = 0 15 | ) 16 | 17 | // RuntimeItem store runtime cache item. 18 | type RuntimeItem struct { 19 | value interface{} 20 | createTime time.Time 21 | ttl time.Duration 22 | } 23 | 24 | // check item is expire 25 | func (mi *RuntimeItem) isExpire() bool { 26 | // 0 means forever 27 | if mi.ttl == 0 { 28 | return false 29 | } 30 | return time.Now().Sub(mi.createTime) > mi.ttl 31 | } 32 | 33 | // RuntimeCache is runtime cache adapter. 34 | // it contains a RW locker for safe map storage. 35 | type RuntimeCache struct { 36 | sync.RWMutex //only used with Incr\Decr 37 | gcInterval time.Duration 38 | items *sync.Map 39 | //itemsMap map[string]*RuntimeItem 40 | } 41 | 42 | // NewRuntimeCache returns a new *RuntimeCache. 43 | func NewRuntimeCache() *RuntimeCache { 44 | cache := RuntimeCache{items: new(sync.Map), gcInterval: DefaultGCInterval} 45 | go cache.gc() 46 | return &cache 47 | } 48 | 49 | // Get cache from runtime cache. 50 | // if non-existed or expired, return nil. 51 | func (ca *RuntimeCache) Get(key string) (interface{}, error) { 52 | if itemObj, ok := ca.items.Load(key); ok { 53 | item := itemObj.(*RuntimeItem) 54 | if item.isExpire() { 55 | return nil, nil 56 | } 57 | return item.value, nil 58 | } 59 | return nil, nil 60 | } 61 | 62 | 63 | // returns value string format by given key 64 | // if non-existed or expired, return "". 65 | func (ca *RuntimeCache) GetString(key string) (string, error) { 66 | v, err := ca.Get(key) 67 | if err != nil || v == nil { 68 | return "", err 69 | } else { 70 | return fmt.Sprint(v), nil 71 | } 72 | } 73 | 74 | // returns value int format by given key 75 | // if non-existed or expired, return 0. 76 | func (ca *RuntimeCache) GetInt(key string) (int, error) { 77 | v, err := ca.GetString(key) 78 | if err != nil || v == "" { 79 | return 0, err 80 | } else { 81 | i, e := strconv.Atoi(v) 82 | if e != nil { 83 | return 0, e 84 | } else { 85 | return i, nil 86 | } 87 | } 88 | } 89 | 90 | // returns value int64 format by given key 91 | // if non-existed or expired, return 0. 92 | func (ca *RuntimeCache) GetInt64(key string) (int64, error) { 93 | v, err := ca.GetString(key) 94 | if err != nil || v == "" { 95 | return ZeroInt64, nil 96 | } else { 97 | i, e := strconv.ParseInt(v, 10, 64) 98 | if e != nil { 99 | return ZeroInt64, e 100 | } else { 101 | return i, nil 102 | } 103 | } 104 | } 105 | 106 | // Set cache to runtime. 107 | // ttl is second, if ttl is 0, it will be forever till restart. 108 | func (ca *RuntimeCache) Set(key string, value interface{}, ttl int64) error { 109 | ca.initValue(key, value, ttl) 110 | return nil 111 | } 112 | 113 | func (ca *RuntimeCache) initValue(key string, value interface{}, ttl int64) error { 114 | ca.items.Store(key, &RuntimeItem{ 115 | value: value, 116 | createTime: time.Now(), 117 | ttl: time.Duration(ttl) * time.Second, 118 | }) 119 | return nil 120 | } 121 | 122 | // Incr increase int64 counter in runtime cache. 123 | func (ca *RuntimeCache) Incr(key string) (int64, error) { 124 | ca.Lock() 125 | itemObj, ok := ca.items.Load(key) 126 | if !ok { 127 | // if not exists, auto set new with 0 128 | ca.initValue(key, ZeroInt64, 0) 129 | // reload 130 | itemObj, _ = ca.items.Load(key) 131 | } 132 | 133 | item := itemObj.(*RuntimeItem) 134 | switch item.value.(type) { 135 | case int: 136 | item.value = item.value.(int) + 1 137 | case int32: 138 | item.value = item.value.(int32) + 1 139 | case int64: 140 | item.value = item.value.(int64) + 1 141 | case uint: 142 | item.value = item.value.(uint) + 1 143 | case uint32: 144 | item.value = item.value.(uint32) + 1 145 | case uint64: 146 | item.value = item.value.(uint64) + 1 147 | default: 148 | return 0, errors.New("item val is not (u)int (u)int32 (u)int64") 149 | } 150 | 151 | ca.Unlock() 152 | 153 | val, _ := strconv.ParseInt(fmt.Sprint(item.value), 10, 64) 154 | return val, nil 155 | } 156 | 157 | // Decr decrease counter in runtime cache. 158 | func (ca *RuntimeCache) Decr(key string) (int64, error) { 159 | ca.Lock() 160 | itemObj, ok := ca.items.Load(key) 161 | if !ok { 162 | // if not exists, auto set new with 0 163 | ca.initValue(key, ZeroInt64, 0) 164 | // reload 165 | itemObj, _ = ca.items.Load(key) 166 | } 167 | 168 | item := itemObj.(*RuntimeItem) 169 | switch item.value.(type) { 170 | case int: 171 | item.value = item.value.(int) - 1 172 | case int64: 173 | item.value = item.value.(int64) - 1 174 | case int32: 175 | item.value = item.value.(int32) - 1 176 | case uint: 177 | if item.value.(uint) > 0 { 178 | item.value = item.value.(uint) - 1 179 | } else { 180 | return 0, errors.New("item val is less than 0") 181 | } 182 | case uint32: 183 | if item.value.(uint32) > 0 { 184 | item.value = item.value.(uint32) - 1 185 | } else { 186 | return 0, errors.New("item val is less than 0") 187 | } 188 | case uint64: 189 | if item.value.(uint64) > 0 { 190 | item.value = item.value.(uint64) - 1 191 | } else { 192 | return 0, errors.New("item val is less than 0") 193 | } 194 | default: 195 | return 0, errors.New("item val is not int int64 int32") 196 | } 197 | ca.Unlock() 198 | 199 | val, _ := strconv.ParseInt(fmt.Sprint(item.value), 10, 64) 200 | return val, nil 201 | } 202 | 203 | // Exist check item exist in runtime cache. 204 | func (ca *RuntimeCache) Exists(key string) (bool, error) { 205 | if itemObj, ok := ca.items.Load(key); ok { 206 | item := itemObj.(*RuntimeItem) 207 | return !item.isExpire(), nil 208 | } 209 | return false, nil 210 | } 211 | 212 | // Delete item in runtime cacha. 213 | // if not exists, we think it's success 214 | func (ca *RuntimeCache) Delete(key string) error { 215 | ca.items.Delete(key) 216 | return nil 217 | } 218 | 219 | // ClearAll will delete all item in runtime cache. 220 | func (ca *RuntimeCache) ClearAll() error { 221 | ca.Lock() 222 | defer ca.Unlock() 223 | ca.items = new(sync.Map) 224 | return nil 225 | } 226 | 227 | func (ca *RuntimeCache) gc() { 228 | for { 229 | <-time.After(ca.gcInterval) 230 | if ca.items == nil { 231 | return 232 | } 233 | ca.items.Range(func(key interface{}, v interface{}) bool { 234 | ca.itemExpired(fmt.Sprint(key)) 235 | return true 236 | }) 237 | } 238 | } 239 | 240 | // itemExpired returns true if an item is expired. 241 | func (ca *RuntimeCache) itemExpired(name string) bool { 242 | itemObj, ok := ca.items.Load(name) 243 | if !ok { 244 | return true 245 | } 246 | item := itemObj.(*RuntimeItem) 247 | if item.isExpire() { 248 | ca.items.Delete(name) 249 | return true 250 | } 251 | return false 252 | } 253 | -------------------------------------------------------------------------------- /cache/runtime/cache_runtime_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/devfeel/dotweb/test" 10 | ) 11 | 12 | const ( 13 | 14 | // DefaultTestGCInterval 15 | DefaultTestGCInterval = 2 16 | 17 | // cache key 18 | TESTCacheKey = "joe" 19 | // cache value 20 | TESTCacheValue = "zou" 21 | // int value 22 | TESTCacheIntValue = 1 23 | 24 | // int64 value 25 | TESTCacheInt64Value = int64(1) 26 | ) 27 | 28 | func TestRuntimeCache_Get(t *testing.T) { 29 | cache := NewRuntimeCache() 30 | cache.Set(TESTCacheKey, TESTCacheValue, 2) 31 | var wg sync.WaitGroup 32 | 33 | // check value 34 | wg.Add(1) 35 | go func(cache *RuntimeCache, t *testing.T) { 36 | time.Sleep(1 * time.Second) 37 | value, err := cache.Get(TESTCacheKey) 38 | 39 | test.Nil(t, err) 40 | test.Equal(t, TESTCacheValue, value) 41 | wg.Done() 42 | }(cache, t) 43 | 44 | // check expired 45 | wg.Add(1) 46 | go func(cache *RuntimeCache, t *testing.T) { 47 | time.Sleep(2 * time.Second) 48 | value, err := cache.Exists(TESTCacheKey) 49 | 50 | test.Nil(t, err) 51 | test.Equal(t, false, value) 52 | wg.Done() 53 | }(cache, t) 54 | 55 | wg.Wait() 56 | } 57 | 58 | func TestRuntimeCache_GetInt(t *testing.T) { 59 | testRuntimeCache(t, TESTCacheIntValue, func(cache *RuntimeCache, key string) (interface{}, error) { 60 | return cache.GetInt(key) 61 | }) 62 | } 63 | 64 | func TestRuntimeCache_GetInt64(t *testing.T) { 65 | testRuntimeCache(t, TESTCacheInt64Value, func(cache *RuntimeCache, key string) (interface{}, error) { 66 | return cache.GetInt64(key) 67 | }) 68 | } 69 | 70 | func TestRuntimeCache_GetString(t *testing.T) { 71 | testRuntimeCache(t, TESTCacheValue, func(cache *RuntimeCache, key string) (interface{}, error) { 72 | return cache.GetString(key) 73 | }) 74 | } 75 | 76 | func testRuntimeCache(t *testing.T, insertValue interface{}, f func(cache *RuntimeCache, key string) (interface{}, error)) { 77 | cache := NewRuntimeCache() 78 | cache.Set(TESTCacheKey, insertValue, 2) 79 | var wg sync.WaitGroup 80 | 81 | // check value 82 | wg.Add(1) 83 | go func(cache *RuntimeCache, t *testing.T) { 84 | time.Sleep(1 * time.Second) 85 | value, err := f(cache, TESTCacheKey) 86 | 87 | test.Nil(t, err) 88 | test.Equal(t, insertValue, value) 89 | wg.Done() 90 | }(cache, t) 91 | time.Sleep(2 * time.Second) 92 | wg.Wait() 93 | } 94 | 95 | func TestRuntimeCache_Delete(t *testing.T) { 96 | cache := NewRuntimeCache() 97 | cache.Set(TESTCacheKey, TESTCacheValue, 2) 98 | 99 | value, e := cache.Get(TESTCacheKey) 100 | 101 | test.Nil(t, e) 102 | test.Equal(t, TESTCacheValue, value) 103 | 104 | cache.Delete(TESTCacheKey) 105 | 106 | value, e = cache.Get(TESTCacheKey) 107 | test.Nil(t, e) 108 | test.Nil(t, value) 109 | } 110 | 111 | func TestRuntimeCache_ClearAll(t *testing.T) { 112 | cache := NewRuntimeCache() 113 | cache.Set(TESTCacheKey, TESTCacheValue, 2) 114 | cache.Set("2", TESTCacheValue, 2) 115 | cache.Set("3", TESTCacheValue, 2) 116 | 117 | val2, err := cache.GetString("2") 118 | if err != nil { 119 | t.Error(err) 120 | } 121 | test.Equal(t, TESTCacheValue, val2) 122 | 123 | cache.ClearAll() 124 | exists2, err := cache.Exists("2") 125 | if err != nil { 126 | t.Error(err) 127 | } 128 | if exists2 { 129 | t.Error("exists 2 but need not exists") 130 | } 131 | } 132 | 133 | func TestRuntimeCache_Incr(t *testing.T) { 134 | cache := NewRuntimeCache() 135 | var wg sync.WaitGroup 136 | wg.Add(2) 137 | 138 | go func(cache *RuntimeCache) { 139 | for i := 0; i < 50; i++ { 140 | cache.Incr(TESTCacheKey) 141 | } 142 | 143 | wg.Add(-1) 144 | }(cache) 145 | 146 | go func(cache *RuntimeCache) { 147 | for i := 0; i < 50; i++ { 148 | cache.Incr(TESTCacheKey) 149 | } 150 | wg.Add(-1) 151 | }(cache) 152 | 153 | wg.Wait() 154 | 155 | value, e := cache.GetInt(TESTCacheKey) 156 | test.Nil(t, e) 157 | 158 | test.Equal(t, 100, value) 159 | } 160 | 161 | func TestRuntimeCache_Decr(t *testing.T) { 162 | cache := NewRuntimeCache() 163 | var wg sync.WaitGroup 164 | wg.Add(2) 165 | 166 | go func(cache *RuntimeCache) { 167 | for i := 0; i < 50; i++ { 168 | cache.Decr(TESTCacheKey) 169 | } 170 | 171 | wg.Add(-1) 172 | }(cache) 173 | 174 | go func(cache *RuntimeCache) { 175 | for i := 0; i < 50; i++ { 176 | cache.Decr(TESTCacheKey) 177 | } 178 | wg.Add(-1) 179 | }(cache) 180 | 181 | wg.Wait() 182 | 183 | value, e := cache.GetInt(TESTCacheKey) 184 | test.Nil(t, e) 185 | 186 | test.Equal(t, -100, value) 187 | } 188 | 189 | func BenchmarkTestRuntimeCache_Get(b *testing.B) { 190 | cache := NewRuntimeCache() 191 | cache.Set(TESTCacheKey, TESTCacheValue, 200000) 192 | for i := 0; i < b.N; i++ { 193 | cache.Get(TESTCacheKey) 194 | } 195 | } 196 | 197 | func BenchmarkTestRuntimeCache_Set(b *testing.B) { 198 | cache := NewRuntimeCache() 199 | for i := 0; i < b.N; i++ { 200 | cache.Set(TESTCacheKey+strconv.Itoa(i), TESTCacheValue, 0) 201 | } 202 | } 203 | 204 | func TestRuntimeCache_ConcurrentGetSetError(t *testing.T) { 205 | cache := NewRuntimeCache() 206 | cache.Set(TESTCacheKey, TESTCacheValue, 200000) 207 | 208 | var wg sync.WaitGroup 209 | wg.Add(2 * 10000) 210 | 211 | for i := 0; i < 10000; i++ { 212 | go func() { 213 | cache.Get(TESTCacheKey) 214 | wg.Done() 215 | }() 216 | } 217 | 218 | for i := 0; i < 10000; i++ { 219 | go func() { 220 | cache.Set(TESTCacheKey+strconv.Itoa(i), TESTCacheValue, 0) 221 | wg.Done() 222 | }() 223 | } 224 | wg.Wait() 225 | } 226 | 227 | func TestRuntimeCache_ConcurrentIncrDecrError(t *testing.T) { 228 | cache := NewRuntimeCache() 229 | cache.Set(TESTCacheKey, TESTCacheValue, 200000) 230 | 231 | var wg sync.WaitGroup 232 | wg.Add(2 * 10000) 233 | 234 | for i := 0; i < 10000; i++ { 235 | go func() { 236 | cache.Incr(TESTCacheKey + strconv.Itoa(i)) 237 | wg.Done() 238 | }() 239 | } 240 | 241 | for i := 0; i < 10000; i++ { 242 | go func() { 243 | cache.Decr(TESTCacheKey + strconv.Itoa(i)) 244 | wg.Done() 245 | }() 246 | } 247 | wg.Wait() 248 | } 249 | -------------------------------------------------------------------------------- /config/config_json.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "encoding/json" 4 | 5 | // UnmarshalJSON parses the JSON-encoded data and stores the result 6 | // in the value pointed to by v. 7 | func UnmarshalJSON(content []byte, v interface{}) error { 8 | return json.Unmarshal(content, v) 9 | } 10 | 11 | // MarshalJSON returns the JSON encoding of v. 12 | func MarshalJSON(v interface{}) (out []byte, err error) { 13 | return json.Marshal(v) 14 | } 15 | 16 | // MarshalJSONString returns the JSON encoding string format of v. 17 | func MarshalJSONString(v interface{}) (out string) { 18 | marshal, err := json.Marshal(v) 19 | if err != nil { 20 | return "" 21 | } 22 | return string(marshal) 23 | } 24 | -------------------------------------------------------------------------------- /config/config_xml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/xml" 5 | ) 6 | 7 | // UnmarshalXML parses the XML-encoded data and stores the result in 8 | // the value pointed to by v, which must be an arbitrary struct, 9 | // slice, or string. Well-formed data that does not fit into v is 10 | // discarded. 11 | func UnmarshalXML(content []byte, v interface{}) error { 12 | return xml.Unmarshal(content, v) 13 | } 14 | 15 | // MarshalXML returns the XML encoding of v. 16 | func MarshalXML(v interface{}) (out []byte, err error) { 17 | return xml.Marshal(v) 18 | } 19 | 20 | // MarshalXMLString returns the XML encoding string format of v. 21 | func MarshalXMLString(v interface{}) (out string) { 22 | marshal, err := xml.Marshal(v) 23 | if err != nil { 24 | return "" 25 | } 26 | return string(marshal) 27 | } 28 | -------------------------------------------------------------------------------- /config/config_yaml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "gopkg.in/yaml.v2" 5 | ) 6 | 7 | // UnmarshalYaml decodes the first document found within the in byte slice 8 | // and assigns decoded values into the out value. 9 | // For example: 10 | // 11 | // type T struct { 12 | // F int `yaml:"a,omitempty"` 13 | // B int 14 | // } 15 | // var t T 16 | // yaml.Unmarshal([]byte("a: 1\nb: 2"), &t) 17 | func UnmarshalYaml(content []byte, v interface{}) error { 18 | return yaml.Unmarshal(content, v) 19 | } 20 | 21 | // MarshalYaml Marshal serializes the value provided into a YAML document. 22 | // For example: 23 | // 24 | // type T struct { 25 | // F int "a,omitempty" 26 | // B int 27 | // } 28 | // yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" 29 | // yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" 30 | func MarshalYaml(v interface{}) (out []byte, err error) { 31 | return yaml.Marshal(v) 32 | } 33 | 34 | // MarshalYamlString returns the Ymal encoding string format of v. 35 | func MarshalYamlString(v interface{}) (out string) { 36 | marshal, err := yaml.Marshal(v) 37 | if err != nil { 38 | return "" 39 | } 40 | return string(marshal) 41 | } 42 | -------------------------------------------------------------------------------- /config/configs_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/devfeel/dotweb/test" 7 | ) 8 | 9 | func TestInitConfig(t *testing.T) { 10 | conf, err := InitConfig("testdata/dotweb.json", "json") 11 | 12 | test.Nil(t, err) 13 | test.NotNil(t, conf) 14 | test.NotNil(t, conf.App) 15 | test.NotNil(t, conf.App.LogPath) 16 | test.NotNil(t, conf.ConfigSet) 17 | } 18 | 19 | func TestInitConfigWithXml(t *testing.T) { 20 | conf, err := InitConfig("testdata/dotweb.conf", "xml") 21 | 22 | test.Nil(t, err) 23 | test.NotNil(t, conf) 24 | test.NotNil(t, conf.App) 25 | test.NotNil(t, conf.App.LogPath) 26 | test.NotNil(t, conf.ConfigSet) 27 | // test.Equal(t, 4, conf.ConfigSet.Len()) 28 | } 29 | -------------------------------------------------------------------------------- /config/configset.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "io/ioutil" 7 | 8 | "github.com/devfeel/dotweb/core" 9 | ) 10 | 11 | type ( 12 | // ConfigSet set of config nodes 13 | ConfigSet struct { 14 | XMLName xml.Name `xml:"config" json:"-" yaml:"-"` 15 | Name string `xml:"name,attr"` 16 | ConfigSetNodes []*ConfigSetNode `xml:"set"` 17 | } 18 | 19 | // ConfigSetNode update for issue #16 config file 20 | ConfigSetNode struct { 21 | Key string `xml:"key,attr"` 22 | Value string `xml:"value,attr"` 23 | } 24 | ) 25 | 26 | // ParseConfigSetXML include ConfigSet xml file 27 | func ParseConfigSetXML(configFile string) (core.ConcurrenceMap, error) { 28 | return parseConfigSetFile(configFile, ConfigType_XML) 29 | } 30 | 31 | // ParseConfigSetJSON include ConfigSet json file 32 | func ParseConfigSetJSON(configFile string) (core.ConcurrenceMap, error) { 33 | return parseConfigSetFile(configFile, ConfigType_JSON) 34 | } 35 | 36 | // ParseConfigSetYaml include ConfigSet yaml file 37 | func ParseConfigSetYaml(configFile string) (core.ConcurrenceMap, error) { 38 | return parseConfigSetFile(configFile, ConfigType_Yaml) 39 | } 40 | 41 | func parseConfigSetFile(configFile string, confType string) (core.ConcurrenceMap, error) { 42 | content, err := ioutil.ReadFile(configFile) 43 | if err != nil { 44 | return nil, errors.New("DotWeb:Config:parseConfigSetFile 配置文件[" + configFile + ", " + confType + "]无法解析 - " + err.Error()) 45 | } 46 | set := new(ConfigSet) 47 | if confType == ConfigType_XML { 48 | err = UnmarshalXML(content, set) 49 | } 50 | if confType == ConfigType_JSON { 51 | err = UnmarshalJSON(content, set) 52 | } 53 | if confType == ConfigType_Yaml { 54 | err = UnmarshalYaml(content, set) 55 | } 56 | if err != nil { 57 | return nil, errors.New("DotWeb:Config:parseConfigSetFile config file[" + configFile + ", " + confType + "]cannot be parsed - " + err.Error()) 58 | } 59 | item := core.NewConcurrenceMap() 60 | for _, s := range set.ConfigSetNodes { 61 | item.Set(s.Key, s.Value) 62 | } 63 | return item, nil 64 | } 65 | -------------------------------------------------------------------------------- /config/defaults.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | 5 | // default timeout Millisecond for per request handler 6 | DefaultRequestTimeOut = 30000 7 | ) 8 | -------------------------------------------------------------------------------- /config/testdata/dotweb.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |   -------------------------------------------------------------------------------- /config/testdata/dotweb.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "LogPath": "d:/gotmp/", 4 | "EnabledLog": true, 5 | "RunMode": "development", 6 | "PProfPort": 0, 7 | "EnabledPProf": false 8 | }, 9 | "AppSets": [{ 10 | "Key": "set1", 11 | "Value": "1" 12 | }, { 13 | "Key": "set2", 14 | "Value": "2" 15 | }, { 16 | "Key": "set3", 17 | "Value": "3" 18 | }, { 19 | "Key": "set4", 20 | "Value": "4" 21 | }], 22 | "Offline": { 23 | "Offline": false, 24 | "OfflineText": "", 25 | "OfflineUrl": "" 26 | }, 27 | "Server": { 28 | "EnabledListDir": false, 29 | "EnabledRequestID": false, 30 | "EnabledGzip": false, 31 | "EnabledAutoHEAD": true, 32 | "EnabledAutoCORS": false, 33 | "EnabledIgnoreFavicon": false, 34 | "EnabledBindUseJsonTag": false, 35 | "Port": 8080, 36 | "EnabledTLS": false, 37 | "TLSCertFile": "", 38 | "TLSKeyFile": "", 39 | "IndexPage": "index.html", 40 | "EnabledDetailRequestData": false 41 | }, 42 | "Session": { 43 | "EnabledSession": true, 44 | "SessionMode": "runtime", 45 | "Timeout": 20, 46 | "ServerIP": "", 47 | "UserName": "", 48 | "Password": "" 49 | }, 50 | "Routers": [{ 51 | "Method": "GET", 52 | "Path": "/index", 53 | "HandlerName": "Index", 54 | "Middlewares": [{ 55 | "Name": "urllog", 56 | "IsUse": true 57 | }], 58 | "IsUse": true 59 | }, { 60 | "Method": "GET", 61 | "Path": "/index2", 62 | "HandlerName": "Index", 63 | "Middlewares": [{ 64 | "Name": "urllog", 65 | "IsUse": true 66 | }], 67 | "IsUse": true 68 | }, { 69 | "Method": "GET", 70 | "Path": "/index3", 71 | "HandlerName": "Index", 72 | "Middlewares": [{ 73 | "Name": "urllog", 74 | "IsUse": true 75 | }], 76 | "IsUse": true 77 | }, { 78 | "Method": "GET", 79 | "Path": "/redirect", 80 | "HandlerName": "Redirect", 81 | "Middlewares": null, 82 | "IsUse": true 83 | }, { 84 | "Method": "GET", 85 | "Path": "/error", 86 | "HandlerName": "Error", 87 | "Middlewares": null, 88 | "IsUse": true 89 | }, { 90 | "Method": "GET", 91 | "Path": "/panic", 92 | "HandlerName": "Panic", 93 | "Middlewares": null, 94 | "IsUse": true 95 | }, { 96 | "Method": "GET", 97 | "Path": "/appset", 98 | "HandlerName": "appset", 99 | "Middlewares": null, 100 | "IsUse": true 101 | }], 102 | "Groups": [{ 103 | "Path": "/admin", 104 | "Routers": [{ 105 | "Method": "GET", 106 | "Path": "/login", 107 | "HandlerName": "Login", 108 | "Middlewares": [{ 109 | "Name": "urllog", 110 | "IsUse": true 111 | }], 112 | "IsUse": true 113 | }, { 114 | "Method": "GET", 115 | "Path": "/login3", 116 | "HandlerName": "Login", 117 | "Middlewares": null, 118 | "IsUse": true 119 | }, { 120 | "Method": "GET", 121 | "Path": "/logout", 122 | "HandlerName": "Logout", 123 | "Middlewares": null, 124 | "IsUse": true 125 | }, { 126 | "Method": "GET", 127 | "Path": "/login2", 128 | "HandlerName": "Login", 129 | "Middlewares": null, 130 | "IsUse": true 131 | }], 132 | "Middlewares": [{ 133 | "Name": "grouplog", 134 | "IsUse": true 135 | }, { 136 | "Name": "simpleauth", 137 | "IsUse": true 138 | }], 139 | "IsUse": true 140 | }], 141 | "Middlewares": [{ 142 | "Name": "applog", 143 | "IsUse": true 144 | }] 145 | } -------------------------------------------------------------------------------- /config/testdata/dotweb.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | logpath: d:/gotmp/ 3 | enabledlog: true 4 | runmode: development 5 | pprofport: 0 6 | enabledpprof: false 7 | appsets: 8 | - key: set1 9 | value: "1" 10 | - key: set2 11 | value: "2" 12 | - key: set3 13 | value: "3" 14 | - key: set4 15 | value: "4" 16 | offline: 17 | offline: false 18 | offlinetext: "" 19 | offlineurl: "" 20 | server: 21 | enabledlistdir: false 22 | enabledrequestid: false 23 | enabledgzip: false 24 | enabledautohead: true 25 | enabledautocors: false 26 | enabledignorefavicon: false 27 | enabledbindusejsontag: false 28 | port: 8080 29 | enabledtls: false 30 | tlscertfile: "" 31 | tlskeyfile: "" 32 | indexpage: index.html 33 | enableddetailrequestdata: false 34 | session: 35 | enabledsession: true 36 | sessionmode: runtime 37 | timeout: 20 38 | serverip: "" 39 | username: "" 40 | password: "" 41 | routers: 42 | - method: GET 43 | path: /index 44 | handlername: Index 45 | middlewares: 46 | - name: urllog 47 | isuse: true 48 | isuse: true 49 | - method: GET 50 | path: /index2 51 | handlername: Index 52 | middlewares: 53 | - name: urllog 54 | isuse: true 55 | isuse: true 56 | - method: GET 57 | path: /index3 58 | handlername: Index 59 | middlewares: 60 | - name: urllog 61 | isuse: true 62 | isuse: true 63 | - method: GET 64 | path: /redirect 65 | handlername: Redirect 66 | middlewares: [] 67 | isuse: true 68 | - method: GET 69 | path: /error 70 | handlername: Error 71 | middlewares: [] 72 | isuse: true 73 | - method: GET 74 | path: /panic 75 | handlername: Panic 76 | middlewares: [] 77 | isuse: true 78 | - method: GET 79 | path: /appset 80 | handlername: appset 81 | middlewares: [] 82 | isuse: true 83 | groups: 84 | - path: /admin 85 | routers: 86 | - method: GET 87 | path: /login 88 | handlername: Login 89 | middlewares: 90 | - name: urllog 91 | isuse: true 92 | isuse: true 93 | - method: GET 94 | path: /login3 95 | handlername: Login 96 | middlewares: [] 97 | isuse: true 98 | - method: GET 99 | path: /logout 100 | handlername: Logout 101 | middlewares: [] 102 | isuse: true 103 | - method: GET 104 | path: /login2 105 | handlername: Login 106 | middlewares: [] 107 | isuse: true 108 | middlewares: 109 | - name: grouplog 110 | isuse: true 111 | - name: simpleauth 112 | isuse: true 113 | isuse: true 114 | middlewares: 115 | - name: applog 116 | isuse: true -------------------------------------------------------------------------------- /consts.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | // Global define 4 | const ( 5 | // Version current version 6 | Version = "1.7.22" 7 | ) 8 | 9 | // Log define 10 | const ( 11 | LogTarget_Default = "dotweb_default" 12 | LogTarget_HttpRequest = "dotweb_request" 13 | LogTarget_HttpServer = "dotweb_server" 14 | LogTarget_RequestTimeout = "dotweb_req_timeout" 15 | 16 | LogLevel_Debug = "debug" 17 | LogLevel_Info = "info" 18 | LogLevel_Warn = "warn" 19 | LogLevel_Error = "error" 20 | ) 21 | 22 | // Http define 23 | const ( 24 | CharsetUTF8 = "charset=utf-8" 25 | DefaultServerName = "dotweb" 26 | ) 27 | 28 | const ( 29 | Windows = "windows" 30 | Linux = "linux" 31 | ) 32 | 33 | // MIME types 34 | const ( 35 | MIMEApplicationJSON = "application/json" 36 | MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + CharsetUTF8 37 | MIMEApplicationJavaScript = "application/javascript" 38 | MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + CharsetUTF8 39 | MIMEApplicationXML = "application/xml" 40 | MIMEApplicationXMLCharsetUTF8 = MIMEApplicationXML + "; " + CharsetUTF8 41 | MIMEApplicationForm = "application/x-www-form-urlencoded" 42 | MIMEApplicationProtobuf = "application/protobuf" 43 | MIMEApplicationMsgpack = "application/msgpack" 44 | MIMETextHTML = "text/html" 45 | MIMETextHTMLCharsetUTF8 = MIMETextHTML + "; " + CharsetUTF8 46 | MIMETextPlain = "text/plain" 47 | MIMETextPlainCharsetUTF8 = MIMETextPlain + "; " + CharsetUTF8 48 | MIMEMultipartForm = "multipart/form-data" 49 | MIMEOctetStream = "application/octet-stream" 50 | ) 51 | 52 | // Headers 53 | const ( 54 | HeaderAcceptEncoding = "Accept-Encoding" 55 | HeaderAllow = "Allow" 56 | HeaderAuthorization = "Authorization" 57 | HeaderContentDisposition = "Content-Disposition" 58 | HeaderContentEncoding = "Content-Encoding" 59 | HeaderContentLength = "Content-Length" 60 | HeaderContentType = "Content-Type" 61 | HeaderCookie = "Cookie" 62 | HeaderSetCookie = "Set-Cookie" 63 | HeaderIfModifiedSince = "If-Modified-Since" 64 | HeaderLastModified = "Last-Modified" 65 | HeaderLocation = "Location" 66 | HeaderUpgrade = "Upgrade" 67 | HeaderVary = "Vary" 68 | HeaderWWWAuthenticate = "WWW-Authenticate" 69 | HeaderXRequestedWith = "X-Requested-With" 70 | HeaderXForwardedProto = "X-Forwarded-Proto" 71 | HeaderXHTTPMethodOverride = "X-HTTP-Method-Override" 72 | HeaderXForwardedFor = "X-Forwarded-For" 73 | HeaderXRealIP = "X-Real-IP" 74 | HeaderServer = "Server" 75 | HeaderOrigin = "Origin" 76 | HeaderAccessControlRequestMethod = "Access-Control-Request-Method" 77 | HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" 78 | HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" 79 | HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" 80 | HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" 81 | HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" 82 | HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" 83 | HeaderAccessControlMaxAge = "Access-Control-Max-Age" 84 | HeaderP3P = "P3P" 85 | HeaderCacheControl = "Cache-control" 86 | 87 | // Security 88 | HeaderStrictTransportSecurity = "Strict-Transport-Security" 89 | HeaderXContentTypeOptions = "X-Content-Type-Options" 90 | HeaderXXSSProtection = "X-XSS-Protection" 91 | HeaderXFrameOptions = "X-Frame-Options" 92 | HeaderContentSecurityPolicy = "Content-Security-Policy" 93 | HeaderXCSRFToken = "X-CSRF-Token" 94 | ) 95 | 96 | const ( 97 | HeaderRequestID = "d_request_id" 98 | HeaderResponseTime = "d_response_time" 99 | ) 100 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/devfeel/dotweb/test" 10 | ) 11 | 12 | type Animal struct { 13 | Hair string 14 | HasMouth bool 15 | } 16 | 17 | // normal write 18 | func TestWrite(t *testing.T) { 19 | param := &InitContextParam{ 20 | t, 21 | &Animal{}, 22 | "", 23 | test.ToDefault, 24 | } 25 | 26 | // init param 27 | context := initResponseContext(param) 28 | 29 | exceptedObject := &Animal{ 30 | "Black", 31 | true, 32 | } 33 | 34 | animalJson, err := json.Marshal(exceptedObject) 35 | test.Nil(t, err) 36 | 37 | // call function 38 | status := http.StatusNotFound 39 | _, contextErr := context.Write(status, animalJson) 40 | test.Nil(t, contextErr) 41 | 42 | // check result 43 | 44 | // header 45 | contentType := context.response.header.Get(HeaderContentType) 46 | 47 | // check the default value 48 | test.Contains(t, CharsetUTF8, contentType) 49 | test.Equal(t, status, context.response.Status) 50 | 51 | // body 52 | body := string(context.response.body) 53 | 54 | test.Equal(t, string(animalJson), body) 55 | } 56 | 57 | // normal write string 58 | func TestWriteString(t *testing.T) { 59 | param := &InitContextParam{ 60 | t, 61 | &Animal{}, 62 | "", 63 | test.ToDefault, 64 | } 65 | 66 | // init param 67 | context := initResponseContext(param) 68 | 69 | exceptedObject := &Animal{ 70 | "Black", 71 | true, 72 | } 73 | 74 | animalJson, err := json.Marshal(exceptedObject) 75 | test.Nil(t, err) 76 | 77 | // call function 78 | // 这里是一个interface数组,用例需要小心. 79 | contextErr := context.WriteString(string(animalJson)) 80 | test.Nil(t, contextErr) 81 | 82 | // header 83 | contentType := context.response.header.Get(HeaderContentType) 84 | // 因writer中的header方法调用过http.Header默认设置 85 | test.Contains(t, CharsetUTF8, contentType) 86 | test.Equal(t, defaultHttpCode, context.response.Status) 87 | 88 | // body 89 | body := string(context.response.body) 90 | 91 | test.Equal(t, string(animalJson), body) 92 | } 93 | 94 | func TestWriteJson(t *testing.T) { 95 | param := &InitContextParam{ 96 | t, 97 | &Animal{}, 98 | "", 99 | test.ToDefault, 100 | } 101 | 102 | // init param 103 | context := initResponseContext(param) 104 | 105 | exceptedObject := &Animal{ 106 | "Black", 107 | true, 108 | } 109 | 110 | animalJson, err := json.Marshal(exceptedObject) 111 | test.Nil(t, err) 112 | 113 | // call function 114 | contextErr := context.WriteJson(exceptedObject) 115 | test.Nil(t, contextErr) 116 | 117 | // header 118 | contentType := context.response.header.Get(HeaderContentType) 119 | // 因writer中的header方法调用过http.Header默认设置 120 | test.Equal(t, MIMEApplicationJSONCharsetUTF8, contentType) 121 | test.Equal(t, defaultHttpCode, context.response.Status) 122 | 123 | // body 124 | body := string(context.response.body) 125 | 126 | test.Equal(t, string(animalJson), body) 127 | } 128 | 129 | // normal jsonp 130 | func TestWriteJsonp(t *testing.T) { 131 | param := &InitContextParam{ 132 | t, 133 | &Animal{}, 134 | "", 135 | test.ToDefault, 136 | } 137 | 138 | // init param 139 | context := initResponseContext(param) 140 | 141 | exceptedObject := &Animal{ 142 | "Black", 143 | true, 144 | } 145 | 146 | callback := "jsonCallBack" 147 | 148 | // call function 149 | err := context.WriteJsonp(callback, exceptedObject) 150 | test.Nil(t, err) 151 | 152 | // check result 153 | 154 | // header 155 | contentType := context.response.header.Get(HeaderContentType) 156 | test.Equal(t, MIMEApplicationJavaScriptCharsetUTF8, contentType) 157 | test.Equal(t, defaultHttpCode, context.response.Status) 158 | 159 | // body 160 | body := string(context.response.body) 161 | 162 | animalJson, err := json.Marshal(exceptedObject) 163 | test.Nil(t, err) 164 | 165 | excepted := fmt.Sprint(callback, "(", string(animalJson), ");") 166 | 167 | test.Equal(t, excepted, body) 168 | } 169 | -------------------------------------------------------------------------------- /core/concurrenceMap.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type ( 10 | // ReadonlyMap only support readonly method for map 11 | ReadonlyMap interface { 12 | Get(key string) (value interface{}, exists bool) 13 | GetString(key string) string 14 | GetTimeDuration(key string) time.Duration 15 | GetInt(key string) int 16 | GetUInt64(key string) uint64 17 | Exists(key string) bool 18 | Len() int 19 | } 20 | 21 | // ReadonlyMap support concurrence for map 22 | ConcurrenceMap interface { 23 | Get(key string) (value interface{}, exists bool) 24 | GetString(key string) string 25 | GetTimeDuration(key string) time.Duration 26 | GetInt(key string) int 27 | GetUInt64(key string) uint64 28 | Exists(key string) bool 29 | GetCurrentMap() map[string]interface{} 30 | Len() int 31 | Set(key string, value interface{}) 32 | Remove(key string) 33 | Once(key string) (value interface{}, exists bool) 34 | } 35 | ) 36 | 37 | // ItemMap concurrence map 38 | type ItemMap struct { 39 | innerMap map[string]interface{} 40 | *sync.RWMutex 41 | } 42 | 43 | // NewItemMap create new ItemMap 44 | func NewItemMap() *ItemMap { 45 | return &ItemMap{ 46 | innerMap: make(map[string]interface{}), 47 | RWMutex: new(sync.RWMutex), 48 | } 49 | } 50 | 51 | // NewConcurrenceMap create new ConcurrenceMap 52 | func NewConcurrenceMap() ConcurrenceMap { 53 | return &ItemMap{ 54 | innerMap: make(map[string]interface{}), 55 | RWMutex: new(sync.RWMutex), 56 | } 57 | } 58 | 59 | // NewReadonlyMap create new ReadonlyMap 60 | func NewReadonlyMap() ReadonlyMap { 61 | return &ItemMap{ 62 | innerMap: make(map[string]interface{}), 63 | RWMutex: new(sync.RWMutex), 64 | } 65 | } 66 | 67 | // Set put key, value into ItemMap 68 | func (ctx *ItemMap) Set(key string, value interface{}) { 69 | ctx.Lock() 70 | ctx.innerMap[key] = value 71 | ctx.Unlock() 72 | } 73 | 74 | // Get returns value of specified key 75 | func (ctx *ItemMap) Get(key string) (value interface{}, exists bool) { 76 | ctx.RLock() 77 | value, exists = ctx.innerMap[key] 78 | ctx.RUnlock() 79 | return value, exists 80 | } 81 | 82 | // Remove remove item by gived key 83 | // if not exists key, do nothing... 84 | func (ctx *ItemMap) Remove(key string) { 85 | ctx.Lock() 86 | delete(ctx.innerMap, key) 87 | ctx.Unlock() 88 | } 89 | 90 | // Once get item by gived key, and remove it 91 | // only can be read once, it will be locked 92 | func (ctx *ItemMap) Once(key string) (value interface{}, exists bool) { 93 | ctx.Lock() 94 | defer ctx.Unlock() 95 | value, exists = ctx.innerMap[key] 96 | if exists { 97 | delete(ctx.innerMap, key) 98 | } 99 | return value, exists 100 | } 101 | 102 | // GetString returns value as string specified by key 103 | // return empty string if key not exists 104 | func (ctx *ItemMap) GetString(key string) string { 105 | value, exists := ctx.Get(key) 106 | if !exists { 107 | return "" 108 | } 109 | return fmt.Sprint(value) 110 | } 111 | 112 | // GetInt returns value as int specified by key 113 | // return 0 if key not exists 114 | func (ctx *ItemMap) GetInt(key string) int { 115 | value, exists := ctx.Get(key) 116 | if !exists { 117 | return 0 118 | } 119 | return value.(int) 120 | } 121 | 122 | // GetUInt64 returns value as uint64 specified by key 123 | // return 0 if key not exists or value cannot be converted to int64 124 | func (ctx *ItemMap) GetUInt64(key string) uint64 { 125 | value, exists := ctx.Get(key) 126 | if !exists { 127 | return 0 128 | } 129 | return value.(uint64) 130 | } 131 | 132 | // GetTimeDuration returns value as time.Duration specified by key 133 | // return 0 if key not exists or value cannot be converted to time.Duration 134 | func (ctx *ItemMap) GetTimeDuration(key string) time.Duration { 135 | timeDuration, err := time.ParseDuration(ctx.GetString(key)) 136 | if err != nil { 137 | return 0 138 | } 139 | return timeDuration 140 | } 141 | 142 | // Exists check exists key 143 | func (ctx *ItemMap) Exists(key string) bool { 144 | _, exists := ctx.innerMap[key] 145 | return exists 146 | } 147 | 148 | // GetCurrentMap get current map, returns map[string]interface{} 149 | func (ctx *ItemMap) GetCurrentMap() map[string]interface{} { 150 | return ctx.innerMap 151 | } 152 | 153 | // Len get context length 154 | func (ctx *ItemMap) Len() int { 155 | return len(ctx.innerMap) 156 | } 157 | -------------------------------------------------------------------------------- /core/concurrenceMap_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | var ic ConcurrenceMap 12 | var keys []string 13 | 14 | func init() { 15 | ic = NewConcurrenceMap() 16 | for i := 0; i < 10000000; i++ { 17 | keys = append(keys, time.Now().String()) 18 | } 19 | fmt.Println("len of keys ", len(keys)) 20 | 21 | } 22 | 23 | func TestItemContext_Get_Set(t *testing.T) { 24 | 25 | ic.Set("foo", "bar") 26 | t.Log(ic.Get("foo")) 27 | t.Log(ic.Exists("foo")) 28 | 29 | t.Log(ic.Get("none")) 30 | t.Log(ic.Exists("none")) 31 | } 32 | 33 | func TestItemContext_Get_Once(t *testing.T) { 34 | ic.Set("foo", "bar") 35 | t.Log(ic.Once("foo")) 36 | t.Log(ic.Get("foo")) 37 | } 38 | 39 | func TestItemContext_Remove(t *testing.T) { 40 | 41 | ic.Set("foo", "bar") 42 | ic.Set("foo1", "bar1") 43 | t.Log(len(ic.GetCurrentMap())) 44 | ic.Remove("foo") 45 | t.Log(ic.GetString("foo")) 46 | } 47 | 48 | func TestItemContext_Current(t *testing.T) { 49 | lock := &sync.Mutex{} 50 | j := 0 51 | for i := 0; i < 9; i++ { 52 | go func() { 53 | lock.Lock() 54 | fmt.Println("go", j) 55 | j++ 56 | v := "bar" + strconv.Itoa(j) 57 | fmt.Println(v) 58 | ic.Set(strconv.Itoa(j), v) 59 | lock.Unlock() 60 | }() 61 | } 62 | 63 | time.Sleep(3 * time.Second) 64 | 65 | t.Log(ic.GetCurrentMap()) 66 | 67 | } 68 | 69 | // 性能测试 70 | 71 | // 基准测试 72 | func BenchmarkItemContext_Set_1(b *testing.B) { 73 | var num uint64 = 1 74 | for i := 0; i < b.N; i++ { 75 | ic.Set(string(num), num) 76 | } 77 | } 78 | 79 | // 并发效率 80 | func BenchmarkItemContext_Set_Parallel(b *testing.B) { 81 | b.RunParallel(func(pb *testing.PB) { 82 | var num uint64 = 1 83 | for pb.Next() { 84 | ic.Set(string(num), num) 85 | } 86 | }) 87 | } 88 | 89 | // 基准测试 90 | func BenchmarkItemContext_Get_1(b *testing.B) { 91 | ic.Set("foo", "bar") 92 | for i := 0; i < b.N; i++ { 93 | ic.Get("foo") 94 | } 95 | } 96 | 97 | // 并发效率 98 | func BenchmarkItemContext_Get_Parallel(b *testing.B) { 99 | ic.Set("foo", "bar") 100 | b.RunParallel(func(pb *testing.PB) { 101 | for pb.Next() { 102 | ic.Get("foo") 103 | } 104 | }) 105 | 106 | } 107 | -------------------------------------------------------------------------------- /core/hideReaddirFS.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | ) 7 | 8 | // FileSystem with hide Readdir 9 | type HideReaddirFS struct { 10 | FileSystem http.FileSystem 11 | } 12 | 13 | // File with hide Readdir 14 | type hideReaddirFile struct { 15 | http.File 16 | } 17 | 18 | // Conforms to http.Filesystem 19 | func (fs HideReaddirFS) Open(name string) (http.File, error) { 20 | f, err := fs.FileSystem.Open(name) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return hideReaddirFile{File: f}, nil 25 | } 26 | 27 | // Overrides the http.File:Readdir default implementation 28 | func (f hideReaddirFile) Readdir(count int) ([]os.FileInfo, error) { 29 | // this disables directory listing 30 | return nil, nil 31 | } 32 | -------------------------------------------------------------------------------- /core/hideReaddirFS_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | -------------------------------------------------------------------------------- /core/htmlx.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "strings" 4 | 5 | var defaultCol = ` 6 | 7 | 8 | ` 9 | var tableHtml = ` 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Dotweb 20 | 71 | 112 | 113 | 114 |
115 | {{tableBody}} 116 |
117 | 118 | 119 | ` 120 | 121 | // CreateTablePart create a table part html by replacing flags 122 | func CreateTablePart(col, title, header, body string) string { 123 | template := `
124 | {{col}} 125 | 126 | 127 | {{header}} 128 | 129 | {{body}} 130 |
{{title}}
` 131 | if col == "" { 132 | col = defaultCol 133 | } 134 | data := strings.Replace(template, "{{col}}", col, -1) 135 | data = strings.Replace(data, "{{title}}", title, -1) 136 | data = strings.Replace(data, "{{header}}", header, -1) 137 | data = strings.Replace(data, "{{body}}", body, -1) 138 | return data 139 | } 140 | 141 | // CreateTableHtml create a complete page html by replacing {{tableBody}} and table part html 142 | func CreateTableHtml(col, title, header, body string) string { 143 | template := `
144 | {{col}} 145 | 146 | 147 | {{header}} 148 | 149 | {{body}} 150 |
{{title}}
` 151 | 152 | if col == "" { 153 | col = defaultCol 154 | } 155 | data := strings.Replace(template, "{{col}}", col, -1) 156 | data = strings.Replace(data, "{{title}}", title, -1) 157 | data = strings.Replace(data, "{{header}}", header, -1) 158 | data = strings.Replace(data, "{{body}}", body, -1) 159 | return CreateHtml(data) 160 | } 161 | 162 | // CreateHtml create a complete page html by replacing {{tableBody}} 163 | func CreateHtml(tableBody string) string { 164 | return strings.Replace(tableHtml, "{{tableBody}}", tableBody, -1) 165 | } 166 | -------------------------------------------------------------------------------- /core/state_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "testing" 7 | "time" 8 | 9 | "github.com/devfeel/dotweb/test" 10 | ) 11 | 12 | // function tests 13 | 14 | func Test_AddRequestCount_1(t *testing.T) { 15 | var wg sync.WaitGroup 16 | wg.Add(2) 17 | 18 | go addRequestCount(&wg, 50) 19 | 20 | go addRequestCount(&wg, 60) 21 | 22 | wg.Wait() 23 | 24 | // wait for the handler to consume all the info 25 | time.Sleep(1 * time.Second) 26 | test.Equal(t, uint64(110), GlobalState.TotalRequestCount) 27 | } 28 | 29 | func addRequestCount(wg *sync.WaitGroup, count int) { 30 | for i := 0; i < count; i++ { 31 | GlobalState.AddRequestCount("test", 200, 1) 32 | } 33 | wg.Add(-1) 34 | } 35 | 36 | func Test_CurrentRequestCount(t *testing.T) { 37 | //var num uint64 = 1 38 | GlobalState.AddCurrentRequest(1000465) 39 | t.Log(GlobalState.CurrentRequestCount) 40 | GlobalState.SubCurrentRequest(2561) 41 | t.Log(GlobalState.CurrentRequestCount) 42 | } 43 | 44 | func Test_AddRequestCount_2(t *testing.T) { 45 | var num uint64 = 1 46 | for i := 0; i < 100; i++ { 47 | GlobalState.AddRequestCount("test", 200, num) 48 | num++ 49 | } 50 | } 51 | 52 | func Test_AddErrorCount_1(t *testing.T) { 53 | var wg sync.WaitGroup 54 | wg.Add(2) 55 | 56 | go addErrorCount(&wg, 50) 57 | 58 | go addErrorCount(&wg, 60) 59 | 60 | wg.Wait() 61 | 62 | test.Equal(t, uint64(110), GlobalState.TotalErrorCount) 63 | } 64 | 65 | func Test_AddErrorCount_2(t *testing.T) { 66 | var num, count uint64 67 | for i := 0; i < 100; i++ { 68 | count = GlobalState.AddErrorCount("test", errors.New("test error"), num) 69 | num++ 70 | } 71 | t.Log("TotalErrorCount:", count) 72 | } 73 | 74 | func addErrorCount(wg *sync.WaitGroup, count int) { 75 | for i := 0; i < count; i++ { 76 | GlobalState.AddErrorCount("test", errors.New("test error"), 1) 77 | } 78 | wg.Add(-1) 79 | } 80 | 81 | // performance tests 82 | 83 | func Benchmark_AddErrorCount_1(b *testing.B) { 84 | var num uint64 = 1 85 | for i := 0; i < b.N; i++ { 86 | GlobalState.AddErrorCount("test", errors.New("test error"), num) 87 | } 88 | } 89 | 90 | func Benchmark_AddErrorCount_Parallel(b *testing.B) { 91 | b.RunParallel(func(pb *testing.PB) { 92 | var num uint64 = 1 93 | for pb.Next() { 94 | GlobalState.AddErrorCount("test", errors.New("test error"), num) 95 | } 96 | }) 97 | } 98 | 99 | func Benchmark_AddRequestCount_1(b *testing.B) { 100 | var num uint64 = 1 101 | for i := 0; i < b.N; i++ { 102 | GlobalState.AddRequestCount("test", 200, num) 103 | } 104 | } 105 | 106 | func Benchmark_AddCurrentRequestCount_1(b *testing.B) { 107 | var num uint64 = 1 108 | for i := 0; i < b.N; i++ { 109 | GlobalState.AddCurrentRequest(num) 110 | } 111 | } 112 | 113 | func Benchmark_AddRequestCount_Parallel(b *testing.B) { 114 | b.RunParallel(func(pb *testing.PB) { 115 | var num uint64 = 1 116 | for pb.Next() { 117 | GlobalState.AddRequestCount("test", 200, num) 118 | } 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /dotweb_sysgroup.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "fmt" 5 | "github.com/devfeel/dotweb/core" 6 | jsonutil "github.com/devfeel/dotweb/framework/json" 7 | "runtime" 8 | "runtime/debug" 9 | "runtime/pprof" 10 | "strings" 11 | ) 12 | 13 | // initDotwebGroup init Dotweb route group which start with /dotweb/ 14 | func initDotwebGroup(server *HttpServer) { 15 | gInner := server.Group("/dotweb") 16 | gInner.GET("/debug/pprof/:key", showPProf) 17 | gInner.GET("/debug/freemem", freeMemory) 18 | gInner.GET("/state", showServerState) 19 | gInner.GET("/state/interval", showIntervalData) 20 | gInner.GET("/query/:key", showQuery) 21 | gInner.GET("/routers", showRouters) 22 | } 23 | 24 | // query pprof debug info 25 | // key:heap goroutine threadcreate block 26 | func showPProf(ctx Context) error { 27 | querykey := ctx.GetRouterName("key") 28 | runtime.GC() 29 | return pprof.Lookup(querykey).WriteTo(ctx.Response().Writer(), 1) 30 | } 31 | 32 | func freeMemory(ctx Context) error { 33 | debug.FreeOSMemory() 34 | return nil 35 | } 36 | 37 | func showIntervalData(ctx Context) error { 38 | if ctx.Request().ExistsQueryKey("pretty") { 39 | return showIntervalDataPretty(ctx) 40 | } else { 41 | return showIntervalDataJson(ctx) 42 | } 43 | } 44 | 45 | func showIntervalDataJson(ctx Context) error { 46 | type data struct { 47 | Time string 48 | RequestCount uint64 49 | ErrorCount uint64 50 | } 51 | queryKey := ctx.QueryString("querykey") 52 | 53 | d := new(data) 54 | d.Time = queryKey 55 | d.RequestCount = ctx.HttpServer().StateInfo().QueryIntervalRequestData(queryKey) 56 | d.ErrorCount = ctx.HttpServer().StateInfo().QueryIntervalErrorData(queryKey) 57 | return ctx.WriteJson(d) 58 | } 59 | 60 | func showIntervalDataPretty(ctx Context) error { 61 | type data struct { 62 | Time string 63 | RequestCount uint64 64 | ErrorCount uint64 65 | } 66 | queryKey := ctx.QueryString("querykey") 67 | d := new(data) 68 | d.Time = queryKey 69 | d.RequestCount = ctx.HttpServer().StateInfo().QueryIntervalRequestData(queryKey) 70 | d.ErrorCount = ctx.HttpServer().StateInfo().QueryIntervalErrorData(queryKey) 71 | tableData := "" + d.Time + "" + fmt.Sprint(d.RequestCount) + "" + fmt.Sprint(d.ErrorCount) + "" 72 | col := ` 73 | 74 | 75 | 76 | ` 77 | header := ` 78 | Time 79 | RequestCount 80 | ErrorCount 81 | ` 82 | html := core.CreateTableHtml(col, "IntervalData", header, tableData) 83 | return ctx.WriteHtml(html) 84 | } 85 | 86 | // snow server status 87 | func showServerState(ctx Context) error { 88 | return ctx.WriteHtml(ctx.HttpServer().StateInfo().ShowHtmlTableData(Version, ctx.HttpServer().DotApp.GlobalUniqueID())) 89 | } 90 | 91 | // query server information 92 | func showQuery(ctx Context) error { 93 | querykey := ctx.GetRouterName("key") 94 | switch querykey { 95 | case "state": 96 | return ctx.WriteString(jsonutil.GetJsonString(ctx.HttpServer().StateInfo())) 97 | case "": 98 | return ctx.WriteString("please input key") 99 | default: 100 | return ctx.WriteString("not support key => " + querykey) 101 | } 102 | } 103 | 104 | func showRouters(ctx Context) error { 105 | data := "" 106 | routerCount := len(ctx.HttpServer().router.GetAllRouterExpress()) 107 | for k, _ := range ctx.HttpServer().router.GetAllRouterExpress() { 108 | method := strings.Split(k, routerExpressSplit)[0] 109 | router := strings.Split(k, routerExpressSplit)[1] 110 | data += "" + method + "" + router + "" 111 | } 112 | col := ` 113 | 114 | 115 | ` 116 | header := ` 117 | Method 118 | Router 119 | ` 120 | html := core.CreateTableHtml(col, "Routers:"+fmt.Sprint(routerCount), header, data) 121 | 122 | return ctx.WriteHtml(html) 123 | } 124 | -------------------------------------------------------------------------------- /dotweb_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/devfeel/dotweb/config" 8 | "github.com/devfeel/dotweb/test" 9 | ) 10 | 11 | // 以下为功能测试 12 | 13 | // 测试RunMode函数无配置文件时的返回值 14 | func Test_RunMode_1(t *testing.T) { 15 | app := New() 16 | runMode := app.RunMode() 17 | t.Log("RunMode:", runMode) 18 | } 19 | 20 | // 测试RunMode函数有配置文件时的返回值 21 | func Test_RunMode_2(t *testing.T) { 22 | runModes := []string{"dev", "development", "prod", "production"} 23 | 24 | app := New() 25 | for _, value := range runModes { 26 | app.Config.App.RunMode = value 27 | runMode := app.RunMode() 28 | t.Log("runModes value:", value, "RunMode:", runMode) 29 | } 30 | } 31 | 32 | //测试IsDevelopmentMode函数 33 | func Test_IsDevelopmentMode_1(t *testing.T) { 34 | app := New() 35 | app.Config.App.RunMode = "development" 36 | b := app.IsDevelopmentMode() 37 | test.Equal(t, true, b) 38 | t.Log("Run IsDevelopmentMode :", b) 39 | } 40 | 41 | func Test_IsDevelopmentMode_2(t *testing.T) { 42 | app := New() 43 | app.Config.App.RunMode = "production" 44 | b := app.IsDevelopmentMode() 45 | t.Log("Run IsDevelopmentMode :", b) 46 | } 47 | 48 | func TestDotWeb_UsePlugin(t *testing.T) { 49 | app := newConfigDotWeb() 50 | app.UsePlugin(new(testPlugin)) 51 | app.UsePlugin(NewDefaultNotifyPlugin(app)) 52 | fmt.Println(app.pluginMap) 53 | app.StartServer(8081) 54 | } 55 | 56 | func newConfigDotWeb() *DotWeb { 57 | app := New() 58 | appConfig, err := config.InitConfig("config/testdata/dotweb.conf", "xml") 59 | if err != nil { 60 | fmt.Println("dotweb.InitConfig error => " + fmt.Sprint(err)) 61 | return nil 62 | } 63 | app.Logger().SetEnabledConsole(true) 64 | app.SetConfig(appConfig) 65 | return app 66 | } 67 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # DotWeb 2 | Simple and easy go web micro framework 3 | 4 | More examples: https://github.com/devfeel/dotweb-example 5 | 6 | ## Contact Us 7 | #### QQ-Group:193409346 - Golang-Devfeel 8 | #### Gitter:[![Gitter](https://badges.gitter.im/devfeel/dotweb.svg)](https://gitter.im/devfeel-dotweb/wechat) 9 | -------------------------------------------------------------------------------- /example/bind/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "errors" 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/devfeel/dotweb" 12 | "github.com/devfeel/dotweb/framework/file" 13 | "github.com/devfeel/dotweb/framework/reflects" 14 | ) 15 | 16 | func main() { 17 | //初始化DotServer 18 | app := dotweb.New() 19 | 20 | //设置dotserver日志目录 21 | app.SetLogPath(file.GetCurrentDirectory()) 22 | 23 | //这里仅为示例,默认情况下,开启的模式就是development模式 24 | app.SetDevelopmentMode() 25 | 26 | //使用json标签 27 | app.HttpServer.SetEnabledBindUseJsonTag(true) 28 | //设置gzip开关 29 | //app.HttpServer.SetEnabledGzip(true) 30 | 31 | //设置自定义绑定器 32 | app.HttpServer.SetBinder(newUserBinder()) 33 | 34 | //设置路由 35 | InitRoute(app.HttpServer) 36 | 37 | //启动 监控服务 38 | //app.SetPProfConfig(true, 8081) 39 | 40 | // 开始服务 41 | port := 8080 42 | fmt.Println("dotweb.StartServer => " + strconv.Itoa(port)) 43 | err := app.StartServer(port) 44 | fmt.Println("dotweb.StartServer error => ", err) 45 | } 46 | 47 | func TestBind(ctx dotweb.Context) error { 48 | type UserInfo struct { 49 | UserName string 50 | Sex int 51 | } 52 | user := new(UserInfo) 53 | errstr := "no error" 54 | if err := ctx.Bind(user); err != nil { 55 | errstr = err.Error() 56 | } else { 57 | 58 | } 59 | 60 | return ctx.WriteString("TestBind [" + errstr + "] " + fmt.Sprint(user)) 61 | } 62 | 63 | func GetBind(ctx dotweb.Context) error { 64 | //type UserInfo struct { 65 | // UserName string `form:"user"` 66 | // Sex int `form:"sex"` 67 | //} 68 | type UserInfo struct { 69 | UserName string `json:"user"` 70 | Sex int `json:"sex"` 71 | } 72 | user := new(UserInfo) 73 | errstr := "no error" 74 | if err := ctx.Bind(user); err != nil { 75 | errstr = err.Error() 76 | } else { 77 | 78 | } 79 | 80 | return ctx.WriteString("GetBind [" + errstr + "] " + fmt.Sprint(user)) 81 | } 82 | 83 | func PostJsonBind(ctx dotweb.Context) error { 84 | type UserInfo struct { 85 | UserName string `json:"user"` 86 | Sex int `json:"sex"` 87 | } 88 | user := new(UserInfo) 89 | errstr := "no error" 90 | if err := ctx.BindJsonBody(user); err != nil { 91 | errstr = err.Error() 92 | } else { 93 | 94 | } 95 | 96 | return ctx.WriteString("PostBind [" + errstr + "] " + fmt.Sprint(user)) 97 | } 98 | 99 | func InitRoute(server *dotweb.HttpServer) { 100 | server.Router().POST("/", TestBind) 101 | server.Router().GET("/getbind", GetBind) 102 | server.Router().POST("/jsonbind", PostJsonBind) 103 | } 104 | 105 | type userBinder struct { 106 | } 107 | 108 | //Bind decode req.Body or form-value to struct 109 | func (b *userBinder) Bind(i interface{}, ctx dotweb.Context) (err error) { 110 | fmt.Println("UserBind.Bind") 111 | req := ctx.Request() 112 | ctype := req.Header.Get(dotweb.HeaderContentType) 113 | if req.Body == nil { 114 | err = errors.New("request body can't be empty") 115 | return err 116 | } 117 | err = errors.New("request unsupported MediaType -> " + ctype) 118 | switch { 119 | case strings.HasPrefix(ctype, dotweb.MIMEApplicationJSON): 120 | err = json.Unmarshal(ctx.Request().PostBody(), i) 121 | case strings.HasPrefix(ctype, dotweb.MIMEApplicationXML): 122 | err = xml.Unmarshal(ctx.Request().PostBody(), i) 123 | //case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm), 124 | // strings.HasPrefix(ctype, MIMETextHTML): 125 | // err = reflects.ConvertMapToStruct(defaultTagName, i, ctx.FormValues()) 126 | default: 127 | //check is use json tag, fixed for issue #91 128 | tagName := "form" 129 | if ctx.HttpServer().ServerConfig().EnabledBindUseJsonTag { 130 | tagName = "json" 131 | } 132 | //no check content type for fixed issue #6 133 | err = reflects.ConvertMapToStruct(tagName, i, ctx.Request().FormValues()) 134 | } 135 | return err 136 | } 137 | 138 | //BindJsonBody default use json decode req.Body to struct 139 | func (b *userBinder) BindJsonBody(i interface{}, ctx dotweb.Context) (err error) { 140 | fmt.Println("UserBind.BindJsonBody") 141 | if ctx.Request().PostBody() == nil { 142 | err = errors.New("request body can't be empty") 143 | return err 144 | } 145 | err = json.Unmarshal(ctx.Request().PostBody(), i) 146 | return err 147 | } 148 | 149 | func newUserBinder() *userBinder { 150 | return &userBinder{} 151 | } 152 | -------------------------------------------------------------------------------- /example/config/dotweb.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |   -------------------------------------------------------------------------------- /example/config/dotweb.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "LogPath": "d:/gotmp/", 4 | "EnabledLog": true, 5 | "RunMode": "development", 6 | "PProfPort": 0, 7 | "EnabledPProf": false 8 | }, 9 | "AppSets": [{ 10 | "Key": "set1", 11 | "Value": "1" 12 | }, { 13 | "Key": "set2", 14 | "Value": "2" 15 | }, { 16 | "Key": "set3", 17 | "Value": "3" 18 | }, { 19 | "Key": "set4", 20 | "Value": "4" 21 | }], 22 | "Offline": { 23 | "Offline": false, 24 | "OfflineText": "", 25 | "OfflineUrl": "" 26 | }, 27 | "Server": { 28 | "EnabledListDir": false, 29 | "EnabledRequestID": false, 30 | "EnabledGzip": false, 31 | "EnabledAutoHEAD": true, 32 | "EnabledAutoCORS": false, 33 | "EnabledIgnoreFavicon": false, 34 | "EnabledBindUseJsonTag": false, 35 | "Port": 8080, 36 | "EnabledTLS": false, 37 | "TLSCertFile": "", 38 | "TLSKeyFile": "", 39 | "IndexPage": "index.html", 40 | "EnabledDetailRequestData": false 41 | }, 42 | "Session": { 43 | "EnabledSession": true, 44 | "SessionMode": "runtime", 45 | "Timeout": 20, 46 | "ServerIP": "", 47 | "UserName": "", 48 | "Password": "" 49 | }, 50 | "Routers": [{ 51 | "Method": "GET", 52 | "Path": "/index", 53 | "HandlerName": "Index", 54 | "Middlewares": [{ 55 | "Name": "urllog", 56 | "IsUse": true 57 | }], 58 | "IsUse": true 59 | }, { 60 | "Method": "GET", 61 | "Path": "/index2", 62 | "HandlerName": "Index", 63 | "Middlewares": [{ 64 | "Name": "urllog", 65 | "IsUse": true 66 | }], 67 | "IsUse": true 68 | }, { 69 | "Method": "GET", 70 | "Path": "/index3", 71 | "HandlerName": "Index", 72 | "Middlewares": [{ 73 | "Name": "urllog", 74 | "IsUse": true 75 | }], 76 | "IsUse": true 77 | }, { 78 | "Method": "GET", 79 | "Path": "/redirect", 80 | "HandlerName": "Redirect", 81 | "Middlewares": null, 82 | "IsUse": true 83 | }, { 84 | "Method": "GET", 85 | "Path": "/error", 86 | "HandlerName": "Error", 87 | "Middlewares": null, 88 | "IsUse": true 89 | }, { 90 | "Method": "GET", 91 | "Path": "/panic", 92 | "HandlerName": "Panic", 93 | "Middlewares": null, 94 | "IsUse": true 95 | }, { 96 | "Method": "GET", 97 | "Path": "/appset", 98 | "HandlerName": "appset", 99 | "Middlewares": null, 100 | "IsUse": true 101 | }], 102 | "Groups": [{ 103 | "Path": "/admin", 104 | "Routers": [{ 105 | "Method": "GET", 106 | "Path": "/login", 107 | "HandlerName": "Login", 108 | "Middlewares": [{ 109 | "Name": "urllog", 110 | "IsUse": true 111 | }], 112 | "IsUse": true 113 | }, { 114 | "Method": "GET", 115 | "Path": "/login3", 116 | "HandlerName": "Login", 117 | "Middlewares": null, 118 | "IsUse": true 119 | }, { 120 | "Method": "GET", 121 | "Path": "/logout", 122 | "HandlerName": "Logout", 123 | "Middlewares": null, 124 | "IsUse": true 125 | }, { 126 | "Method": "GET", 127 | "Path": "/login2", 128 | "HandlerName": "Login", 129 | "Middlewares": null, 130 | "IsUse": true 131 | }], 132 | "Middlewares": [{ 133 | "Name": "grouplog", 134 | "IsUse": true 135 | }, { 136 | "Name": "simpleauth", 137 | "IsUse": true 138 | }], 139 | "IsUse": true 140 | }], 141 | "Middlewares": [{ 142 | "Name": "applog", 143 | "IsUse": true 144 | }] 145 | } -------------------------------------------------------------------------------- /example/config/dotweb.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | logpath: d:/gotmp/ 3 | enabledlog: true 4 | runmode: development 5 | pprofport: 0 6 | enabledpprof: false 7 | appsets: 8 | - key: set1 9 | value: "1" 10 | - key: set2 11 | value: "2" 12 | - key: set3 13 | value: "3" 14 | - key: set4 15 | value: "4" 16 | offline: 17 | offline: false 18 | offlinetext: "" 19 | offlineurl: "" 20 | server: 21 | enabledlistdir: false 22 | enabledrequestid: false 23 | enabledgzip: false 24 | enabledautohead: true 25 | enabledautocors: false 26 | enabledignorefavicon: false 27 | enabledbindusejsontag: false 28 | port: 8080 29 | enabledtls: false 30 | tlscertfile: "" 31 | tlskeyfile: "" 32 | indexpage: index.html 33 | enableddetailrequestdata: false 34 | session: 35 | enabledsession: true 36 | sessionmode: runtime 37 | timeout: 20 38 | serverip: "" 39 | username: "" 40 | password: "" 41 | routers: 42 | - method: GET 43 | path: /index 44 | handlername: Index 45 | middlewares: 46 | - name: urllog 47 | isuse: true 48 | isuse: true 49 | - method: GET 50 | path: /index2 51 | handlername: Index 52 | middlewares: 53 | - name: urllog 54 | isuse: true 55 | isuse: true 56 | - method: GET 57 | path: /index3 58 | handlername: Index 59 | middlewares: 60 | - name: urllog 61 | isuse: true 62 | isuse: true 63 | - method: GET 64 | path: /redirect 65 | handlername: Redirect 66 | middlewares: [] 67 | isuse: true 68 | - method: GET 69 | path: /error 70 | handlername: Error 71 | middlewares: [] 72 | isuse: true 73 | - method: GET 74 | path: /panic 75 | handlername: Panic 76 | middlewares: [] 77 | isuse: true 78 | - method: GET 79 | path: /appset 80 | handlername: appset 81 | middlewares: [] 82 | isuse: true 83 | groups: 84 | - path: /admin 85 | routers: 86 | - method: GET 87 | path: /login 88 | handlername: Login 89 | middlewares: 90 | - name: urllog 91 | isuse: true 92 | isuse: true 93 | - method: GET 94 | path: /login3 95 | handlername: Login 96 | middlewares: [] 97 | isuse: true 98 | - method: GET 99 | path: /logout 100 | handlername: Logout 101 | middlewares: [] 102 | isuse: true 103 | - method: GET 104 | path: /login2 105 | handlername: Login 106 | middlewares: [] 107 | isuse: true 108 | middlewares: 109 | - name: grouplog 110 | isuse: true 111 | - name: simpleauth 112 | isuse: true 113 | isuse: true 114 | middlewares: 115 | - name: applog 116 | isuse: true -------------------------------------------------------------------------------- /example/config/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/devfeel/dotweb" 6 | "github.com/devfeel/dotweb/config" 7 | "github.com/devfeel/dotweb/framework/json" 8 | ) 9 | 10 | func main() { 11 | //初始化DotServer 12 | app := dotweb.New() 13 | 14 | //注册HttpHandler 15 | RegisterHandler(app.HttpServer) 16 | 17 | //xml config 18 | //appConfig, err := config.InitConfig("d:/gotmp/dotweb.conf") 19 | //json config 20 | //appConfig, err := config.InitConfig("d:/gotmp/dotweb.json", "json") 21 | //yaml config 22 | appConfig, err := config.InitConfig("d:/gotmp/dotweb.yaml", "yaml") 23 | if err != nil { 24 | fmt.Println("dotweb.InitConfig error => " + fmt.Sprint(err)) 25 | return 26 | } 27 | fmt.Println(jsonutil.GetJsonString(appConfig)) 28 | 29 | //引入自定义ConfigSet 30 | err = app.Config.IncludeConfigSet("d:/gotmp/userconf.xml", config.ConfigType_XML) 31 | if err != nil { 32 | fmt.Println(err.Error()) 33 | return 34 | } 35 | 36 | app.SetConfig(appConfig) 37 | 38 | fmt.Println("dotweb.StartServer => " + fmt.Sprint(appConfig)) 39 | err = app.Start() 40 | fmt.Println("dotweb.StartServer error => ", err) 41 | } 42 | 43 | func Index(ctx dotweb.Context) error { 44 | ctx.Response().Header().Set("Content-Type", "text/html; charset=utf-8") 45 | return ctx.WriteString("index => ", fmt.Sprint(ctx.RouterNode().Middlewares())) 46 | } 47 | 48 | func GetAppSet(ctx dotweb.Context) error { 49 | key := ctx.QueryString("key") 50 | return ctx.WriteString(ctx.Request().Url(), " => key = ", ctx.ConfigSet().GetString(key)) 51 | } 52 | 53 | // ConfigSet 54 | func ConfigSet(ctx dotweb.Context) error { 55 | vkey1 := ctx.ConfigSet().GetString("set1") 56 | vkey2 := ctx.ConfigSet().GetString("set2") 57 | return ctx.WriteString(ctx.Request().Path(), "key1=", vkey1, "key2=", vkey2) 58 | } 59 | 60 | func RegisterHandler(server *dotweb.HttpServer) { 61 | server.Router().RegisterHandler("Index", Index) 62 | server.Router().RegisterHandler("appset", GetAppSet) 63 | server.GET("/configser", ConfigSet) 64 | } 65 | -------------------------------------------------------------------------------- /example/config/userconf.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/devfeel/dotweb" 11 | "github.com/devfeel/dotweb/framework/exception" 12 | "github.com/devfeel/dotweb/session" 13 | ) 14 | 15 | func main() { 16 | 17 | defer func() { 18 | var errmsg string 19 | if err := recover(); err != nil { 20 | errmsg = exception.CatchError("main", dotweb.LogTarget_HttpServer, err) 21 | fmt.Println("main error : ", errmsg) 22 | } 23 | }() 24 | 25 | //初始化DotServer 26 | app := dotweb.Classic("c:/gotmp") 27 | 28 | //设置dotserver日志目录 29 | //如果不设置,默认不启用,且默认为当前目录 30 | app.SetEnabledLog(true) 31 | 32 | app.HttpServer.SetEnabledRequestID(true) 33 | // 设置解析请求体大小为 10MB 34 | app.HttpServer.SetMaxBodySize(-1) 35 | 36 | //开启development模式 37 | app.SetDevelopmentMode() 38 | app.SetProductionMode() 39 | 40 | //设置自定义Context 41 | app.HttpServer.SetContextCreater(testContextCreater) 42 | 43 | //设置gzip开关 44 | //app.HttpServer.SetEnabledGzip(true) 45 | 46 | //设置Session开关 47 | app.HttpServer.SetEnabledSession(true) 48 | 49 | app.HttpServer.SetEnabledGzip(true) 50 | 51 | //set virtual path 52 | app.HttpServer.SetVirtualPath("/1") 53 | 54 | //1.use default config 55 | //app.HttpServer.Features.SetEnabledCROS() 56 | //2.use user config 57 | //app.HttpServer.Features.SetEnabledCROS(true).SetOrigin("*").SetMethod("GET") 58 | 59 | //设置Session配置 60 | //runtime mode 61 | app.HttpServer.SetSessionConfig(session.NewDefaultRuntimeConfig()) 62 | //redis no auth mode 63 | //app.HttpServer.SetSessionConfig(session.NewDefaultRedisConfig("redis://192.168.8.175:6379/0")) 64 | //redis auth mode 65 | //app.HttpServer.SetSessionConfig(session.NewDefaultRedisConfig("redis://:password@192.168.8.175:6379/0")) 66 | 67 | app.HttpServer.SetEnabledDetailRequestData(true) 68 | 69 | //设置路由 70 | InitRoute(app.HttpServer) 71 | 72 | //自定义404输出 73 | app.SetNotFoundHandle(func(ctx dotweb.Context) { 74 | ctx.Response().Write(http.StatusNotFound, []byte("is't app's not found!")) 75 | }) 76 | 77 | app.SetExceptionHandle(func(ctx dotweb.Context, err error) { 78 | ctx.Response().SetContentType(dotweb.MIMEApplicationJSONCharsetUTF8) 79 | ctx.WriteJsonC(http.StatusInternalServerError, err.Error()) 80 | }) 81 | 82 | //设置超时钩子事件,当请求超过指定时间阀值,会自动调用传入的函数 83 | //不会终止请求,只作为旁路执行 84 | app.UseTimeoutHook(dotweb.DefaultTimeoutHookHandler, time.Second*2) 85 | 86 | //设置HttpModule 87 | //InitModule(app) 88 | 89 | //启动 监控服务 90 | app.SetPProfConfig(true, 8081) 91 | 92 | //全局容器 93 | app.Items.Set("gstring", "gvalue") 94 | app.Items.Set("gint", 1) 95 | 96 | // 开始服务 97 | port := 8080 98 | fmt.Println("dotweb.StartServer => " + strconv.Itoa(port)) 99 | err := app.StartServer(port) 100 | fmt.Println("dotweb.StartServer error => ", err) 101 | } 102 | 103 | func Index(ctx dotweb.Context) error { 104 | ctx.Response().Header().Set("Content-Type", "text/html; charset=utf-8") 105 | ctx.Write(200, []byte(ctx.Request().RemoteIP()+" "+ctx.(*testContext).TestInfo)) 106 | //_, err := ctx.WriteStringC(201, "index => ", ctx.RemoteIP(), "我是首页") 107 | return nil 108 | } 109 | 110 | func Time(ctx dotweb.Context) error { 111 | minuteTimeLayout := "200601021504" 112 | if t, err := time.Parse(minuteTimeLayout, "201709251541"); err != nil { 113 | ctx.WriteString(err.Error()) 114 | } else { 115 | now, _ := time.Parse(minuteTimeLayout, time.Now().Format(minuteTimeLayout)) 116 | ctx.WriteString(t) 117 | ctx.WriteString(now) 118 | ctx.WriteString(t.Sub(now)) 119 | //ctx.WriteString(t.Sub(time.Now()) > 5*time.Minute) 120 | } 121 | return nil 122 | } 123 | 124 | func LogOut(ctx dotweb.Context) error { 125 | err := ctx.DestorySession() 126 | if err != nil { 127 | ctx.WriteString(err) 128 | return err 129 | } 130 | err = ctx.Redirect(http.StatusMovedPermanently, "index?fromlogout") 131 | if err != nil { 132 | ctx.WriteString(err) 133 | } 134 | return err 135 | } 136 | 137 | func OutputTestInfo(ctx dotweb.Context) error { 138 | return ctx.WriteString(ctx.(*testContext).TestInfo) 139 | } 140 | 141 | func IndexReg(ctx dotweb.Context) error { 142 | ctx.HttpServer().Logger().Info(dotweb.LogTarget_Default, "test log") 143 | ctx.Response().Header().Set("Content-Type", "text/html; charset=utf-8") 144 | return ctx.WriteString("welcome to dotweb") 145 | } 146 | 147 | func IndexPretty(ctx dotweb.Context) error { 148 | type Result struct { 149 | Code int 150 | Message string 151 | } 152 | result := Result{ 153 | Code: 200, 154 | Message: "Test", 155 | } 156 | return ctx.WriteString(ctx.Tools().PrettyJson(result)) 157 | } 158 | 159 | func ReadPost(ctx dotweb.Context) error { 160 | return ctx.WriteString(ctx.Request().PostBody()) 161 | } 162 | 163 | func IndexParam(ctx dotweb.Context) error { 164 | ctx.Response().Header().Set("Content-Type", "text/html; charset=utf-8") 165 | return ctx.WriteString("IndexParam", ctx.GetRouterName("id")) 166 | } 167 | 168 | func KeyPost(ctx dotweb.Context) error { 169 | username1 := ctx.PostFormValue("username") 170 | username2 := ctx.FormValue("username") 171 | username3 := ctx.PostFormValue("username") 172 | return ctx.WriteString("username:" + username1 + " - " + username2 + " - " + username3) 173 | } 174 | 175 | func JsonPost(ctx dotweb.Context) error { 176 | return ctx.WriteString("body:" + string(ctx.Request().PostBody())) 177 | } 178 | 179 | func DefaultError(ctx dotweb.Context) error { 180 | //panic("my panic error!") 181 | i := 0 182 | b := 2 / i 183 | return ctx.WriteString(b) 184 | } 185 | 186 | func Redirect(ctx dotweb.Context) error { 187 | err := ctx.Redirect(http.StatusMovedPermanently, "http://www.baidu.com") 188 | if err != nil { 189 | ctx.WriteString(err) 190 | } 191 | return err 192 | } 193 | 194 | func ReturnError(ctx dotweb.Context) error { 195 | return errors.New("return error") 196 | } 197 | 198 | func InitRoute(server *dotweb.HttpServer) { 199 | server.GET("/", Index) 200 | server.GET("/time", Time) 201 | server.GET("/index", Index) 202 | server.GET("/id/:id", IndexParam) 203 | server.POST("/keypost", KeyPost) 204 | server.POST("/jsonpost", JsonPost) 205 | server.GET("/error", DefaultError) 206 | server.GET("/returnerr", ReturnError) 207 | server.GET("/redirect", Redirect) 208 | server.POST("/readpost", ReadPost) 209 | server.GET("/pretty", IndexPretty) 210 | server.GET("/logout", LogOut) 211 | //server.Router().RegisterRoute(dotweb.RouteMethod_GET, "/index", IndexReg) 212 | } 213 | 214 | type testContext struct { 215 | dotweb.HttpContext 216 | TestInfo string 217 | } 218 | 219 | func testContextCreater() dotweb.Context { 220 | return &testContext{TestInfo: "Test"} 221 | } 222 | -------------------------------------------------------------------------------- /example/middleware/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/devfeel/dotweb" 10 | ) 11 | 12 | func main() { 13 | //初始化DotServer 14 | app := dotweb.New() 15 | 16 | //设置dotserver日志目录 17 | //如果不设置,默认不启用,且默认为当前目录 18 | app.SetEnabledLog(true) 19 | 20 | //开启development模式 21 | app.SetDevelopmentMode() 22 | 23 | app.UseTimeoutHook(dotweb.DefaultTimeoutHookHandler, time.Second*10) 24 | 25 | exAccessFmtLog := NewAccessFmtLog("appex") 26 | exAccessFmtLog.Exclude("/index") 27 | exAccessFmtLog.Exclude("/v1/machines/queryIP/:IP") 28 | app.Use(exAccessFmtLog) 29 | 30 | app.ExcludeUse(NewAccessFmtLog("appex1"), "/") 31 | app.Use( 32 | NewAccessFmtLog("app"), 33 | ) 34 | //设置路由 35 | InitRoute(app.HttpServer) 36 | 37 | //启动 监控服务 38 | app.SetPProfConfig(true, 8081) 39 | 40 | // 开始服务 41 | port := 8080 42 | fmt.Println("dotweb.StartServer => " + strconv.Itoa(port)) 43 | err := app.StartServer(port) 44 | fmt.Println("dotweb.StartServer error => ", err) 45 | } 46 | 47 | func Index(ctx dotweb.Context) error { 48 | ctx.Response().Header().Set("Content-Type", "text/html; charset=utf-8") 49 | //fmt.Println(time.Now(), "Index Handler") 50 | err := ctx.WriteString("index => ", ctx.Request().Url()) 51 | fmt.Println(ctx.RouterNode().GroupMiddlewares()) 52 | return err 53 | } 54 | 55 | func ShowMiddlewares(ctx dotweb.Context) error { 56 | err := ctx.WriteString("ShowMiddlewares => ", ctx.RouterNode().GroupMiddlewares()) 57 | return err 58 | } 59 | 60 | func InitRoute(server *dotweb.HttpServer) { 61 | server.Router().GET("/", Index) 62 | server.Router().GET("/index", Index) 63 | server.Router().GET("/v1/machines/queryIP/:IP", Index) 64 | server.Router().GET("/v1/machines/queryIP2", Index) 65 | server.Router().GET("/use", Index).Use(NewAccessFmtLog("Router-use")) 66 | 67 | /*g := server.Group("/group").Use(NewAccessFmtLog("group")).Use(NewSimpleAuth("admin")) 68 | g.GET("/", Index) 69 | g.GET("/use", Index).Use(NewAccessFmtLog("group-use"))*/ 70 | 71 | g := server.Group("/A").Use(NewAGroup()) 72 | g.GET("/", ShowMiddlewares) 73 | g1 := g.Group("/B").Use(NewBGroup()) 74 | g1.GET("/", ShowMiddlewares) 75 | g2 := g.Group("/C").Use(NewCGroup()) 76 | g2.GET("/", ShowMiddlewares) 77 | 78 | g = server.Group("/B").Use(NewBGroup()) 79 | g.GET("/", ShowMiddlewares) 80 | 81 | } 82 | 83 | func InitModule(dotserver *dotweb.HttpServer) { 84 | dotserver.RegisterModule(&dotweb.HttpModule{ 85 | OnBeginRequest: func(ctx dotweb.Context) { 86 | fmt.Println(time.Now(), "HttpModule BeginRequest1:", ctx.Request().RequestURI) 87 | }, 88 | OnEndRequest: func(ctx dotweb.Context) { 89 | fmt.Println(time.Now(), "HttpModule EndRequest1:", ctx.Request().RequestURI) 90 | }, 91 | }) 92 | 93 | dotserver.RegisterModule(&dotweb.HttpModule{ 94 | OnBeginRequest: func(ctx dotweb.Context) { 95 | fmt.Println(time.Now(), "HttpModule BeginRequest2:", ctx.Request().RequestURI) 96 | }, 97 | }) 98 | dotserver.RegisterModule(&dotweb.HttpModule{ 99 | OnEndRequest: func(ctx dotweb.Context) { 100 | fmt.Println(time.Now(), "HttpModule EndRequest3:", ctx.Request().RequestURI) 101 | }, 102 | }) 103 | } 104 | 105 | type AccessFmtLog struct { 106 | dotweb.BaseMiddleware 107 | Index string 108 | } 109 | 110 | func (m *AccessFmtLog) Handle(ctx dotweb.Context) error { 111 | fmt.Println(time.Now(), "[AccessFmtLog ", m.Index, "] begin request -> ", ctx.Request().RequestURI) 112 | err := m.Next(ctx) 113 | fmt.Println(time.Now(), "[AccessFmtLog ", m.Index, "] finish request ", err, " -> ", ctx.Request().RequestURI) 114 | return err 115 | } 116 | 117 | func NewAccessFmtLog(index string) *AccessFmtLog { 118 | return &AccessFmtLog{Index: index} 119 | } 120 | 121 | type SimpleAuth struct { 122 | dotweb.BaseMiddleware 123 | exactToken string 124 | } 125 | 126 | func (m *SimpleAuth) Handle(ctx dotweb.Context) error { 127 | fmt.Println(time.Now(), "[SimpleAuth] begin request -> ", ctx.Request().RequestURI) 128 | var err error 129 | if ctx.QueryString("token") != m.exactToken { 130 | ctx.Write(http.StatusUnauthorized, []byte("sorry, Unauthorized")) 131 | } else { 132 | err = m.Next(ctx) 133 | } 134 | fmt.Println(time.Now(), "[SimpleAuth] finish request ", err, " -> ", ctx.Request().RequestURI) 135 | return err 136 | } 137 | 138 | func NewSimpleAuth(exactToken string) *SimpleAuth { 139 | return &SimpleAuth{exactToken: exactToken} 140 | } 141 | 142 | type AGroup struct { 143 | dotweb.BaseMiddleware 144 | } 145 | 146 | func (m *AGroup) Handle(ctx dotweb.Context) error { 147 | fmt.Println(time.Now(), "[AGroup] request)") 148 | err := m.Next(ctx) 149 | return err 150 | } 151 | 152 | func NewAGroup() *AGroup { 153 | return &AGroup{} 154 | } 155 | 156 | type BGroup struct { 157 | dotweb.BaseMiddleware 158 | } 159 | 160 | func (m *BGroup) Handle(ctx dotweb.Context) error { 161 | fmt.Println(time.Now(), "[BGroup] request)") 162 | err := m.Next(ctx) 163 | return err 164 | } 165 | 166 | func NewBGroup() *BGroup { 167 | return &BGroup{} 168 | } 169 | 170 | type CGroup struct { 171 | dotweb.BaseMiddleware 172 | } 173 | 174 | func (m *CGroup) Handle(ctx dotweb.Context) error { 175 | fmt.Println(time.Now(), "[CGroup] request)") 176 | err := m.Next(ctx) 177 | return err 178 | } 179 | 180 | func NewCGroup() *CGroup { 181 | return &CGroup{} 182 | } 183 | -------------------------------------------------------------------------------- /example/mock/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/devfeel/dotweb" 8 | ) 9 | 10 | func main() { 11 | //初始化DotServer 12 | app := dotweb.New() 13 | 14 | //设置dotserver日志目录 15 | //如果不设置,默认不启用,且默认为当前目录 16 | app.SetEnabledLog(true) 17 | 18 | //开启development模式 19 | app.SetDevelopmentMode() 20 | 21 | //设置Mock逻辑 22 | app.SetMock(AppMock()) 23 | 24 | //设置路由 25 | InitRoute(app.HttpServer) 26 | 27 | // 开始服务 28 | port := 8080 29 | fmt.Println("dotweb.StartServer => " + strconv.Itoa(port)) 30 | err := app.StartServer(port) 31 | fmt.Println("dotweb.StartServer error => ", err) 32 | } 33 | 34 | // Index index handler 35 | func Index(ctx dotweb.Context) error { 36 | ctx.Response().Header().Set("Content-Type", "text/html; charset=utf-8") 37 | err := ctx.WriteString("index => ", ctx.Request().Url()) 38 | return err 39 | } 40 | 41 | // InitRoute init app's route 42 | func InitRoute(server *dotweb.HttpServer) { 43 | server.Router().GET("/", Index) 44 | } 45 | 46 | // AppMock create app Mock 47 | func AppMock() dotweb.Mock { 48 | m := dotweb.NewStandardMock() 49 | m.RegisterString("/", "mock data") 50 | return m 51 | } 52 | -------------------------------------------------------------------------------- /example/router/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/devfeel/dotweb" 9 | "github.com/devfeel/dotweb/framework/file" 10 | ) 11 | 12 | func main() { 13 | //初始化DotServer 14 | app := dotweb.Classic(file.GetCurrentDirectory()) 15 | 16 | app.SetDevelopmentMode() 17 | 18 | app.HttpServer.SetEnabledAutoHEAD(true) 19 | //app.HttpServer.SetEnabledAutoOPTIONS(true) 20 | 21 | app.SetMethodNotAllowedHandle(func(ctx dotweb.Context) { 22 | ctx.Redirect(301, "/") 23 | }) 24 | 25 | //设置路由 26 | InitRoute(app.HttpServer) 27 | 28 | //启动 监控服务 29 | //app.SetPProfConfig(true, 8081) 30 | 31 | // 开始服务 32 | port := 8080 33 | fmt.Println("dotweb.StartServer => " + strconv.Itoa(port)) 34 | err := app.StartServer(port) 35 | fmt.Println("dotweb.StartServer error => ", err) 36 | } 37 | 38 | func Index(ctx dotweb.Context) error { 39 | ctx.Response().Header().Set("Content-Type", "text/html; charset=utf-8") 40 | flag := ctx.HttpServer().Router().MatchPath(ctx, "/d/:x/y") 41 | return ctx.WriteString("index - " + ctx.Request().Method + " - " + ctx.RouterNode().Path() + " - " + fmt.Sprint(flag)) 42 | } 43 | 44 | func Any(ctx dotweb.Context) error { 45 | ctx.Response().Header().Set("Content-Type", "text/html; charset=utf-8") 46 | return ctx.WriteString("any - " + ctx.Request().Method + " - " + ctx.RouterNode().Path()) 47 | } 48 | 49 | func HandlerFunc(w http.ResponseWriter, r *http.Request) { 50 | w.Write([]byte("go raw http func")) 51 | } 52 | 53 | func InitRoute(server *dotweb.HttpServer) { 54 | server.GET("/", Index) 55 | server.GET("/d/:x/y", Index) 56 | server.GET("/x/:y", Index) 57 | server.GET("/x/", Index) 58 | 59 | server.POST("/post", Index) 60 | 61 | server.Any("/any", Any) 62 | server.RegisterHandlerFunc("GET", "/h/func", HandlerFunc) 63 | } 64 | -------------------------------------------------------------------------------- /framework/convert/convert.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "errors" 5 | "math/big" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | // String2Bytes convert string to []byte 11 | func String2Bytes(val string) []byte { 12 | return []byte(val) 13 | } 14 | 15 | // String2Int convert string to int 16 | func String2Int(val string) (int, error) { 17 | return strconv.Atoi(val) 18 | } 19 | 20 | // Int2String convert int to string 21 | func Int2String(val int) string { 22 | return strconv.Itoa(val) 23 | } 24 | 25 | // String2Int64 convert string to int64 26 | func String2Int64(val string) (int64, error) { 27 | return strconv.ParseInt(val, 10, 64) 28 | } 29 | 30 | // Int642String convert int64 to string 31 | func Int642String(val int64) string { 32 | return strconv.FormatInt(val, 10) 33 | } 34 | 35 | // String2UInt64 convert string to uint64 36 | func String2UInt64(val string) (uint64, error) { 37 | return strconv.ParseUint(val, 10, 64) 38 | } 39 | 40 | // UInt642String convert uint64 to string 41 | func UInt642String(val uint64) string { 42 | return strconv.FormatUint(val, 10) 43 | } 44 | 45 | // NSToTime convert ns to time.Time 46 | func NSToTime(ns int64) (time.Time, error) { 47 | if ns <= 0 { 48 | return time.Time{}, errors.New("ns is err") 49 | } 50 | bigNS := big.NewInt(ns) 51 | return time.Unix(ns/1e9, int64(bigNS.Mod(bigNS, big.NewInt(1e9)).Uint64())), nil 52 | } 53 | -------------------------------------------------------------------------------- /framework/convert/convert_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/devfeel/dotweb/test" 8 | ) 9 | 10 | //功能测试 11 | 12 | func Test_String2Bytes_1(t *testing.T) { 13 | str := "0123456789" 14 | b := String2Bytes(str) 15 | t.Log(str, " String to Byte: ", b) 16 | excepted := []byte{48, 49, 50, 51, 52, 53, 54, 55, 56, 57} 17 | test.Equal(t, excepted, b) 18 | } 19 | 20 | func Test_String2Int_1(t *testing.T) { 21 | str := "1234567890" 22 | b, e := String2Int(str) 23 | 24 | t.Log(str, " String to Int: ", b) 25 | test.Nil(t, e) 26 | test.Equal(t, 1234567890, b) 27 | } 28 | 29 | func Test_String2Int_2(t *testing.T) { 30 | str := "1234567890ssss" 31 | b, e := String2Int(str) 32 | 33 | t.Log(str, " String to Int: ", b) 34 | test.NotNil(t, e) 35 | test.Equal(t, 0, b) 36 | } 37 | 38 | func Test_Int2String_1(t *testing.T) { 39 | vint := 9876543210 40 | s := Int2String(vint) 41 | t.Log(vint, "Int to String: ", s) 42 | test.Equal(t, "9876543210", s) 43 | } 44 | 45 | //String2Int64 46 | func Test_String2Int64_1(t *testing.T) { 47 | str := "0200000010" 48 | b, e := String2Int64(str) 49 | 50 | t.Log(str, "String to Int64: ", b) 51 | test.Nil(t, e) 52 | test.Equal(t, int64(200000010), b) 53 | } 54 | 55 | //String2Int64 56 | func Test_String2Int64_2(t *testing.T) { 57 | str := "a0200000010" 58 | b, e := String2Int64(str) 59 | 60 | t.Log(str, "String to Int64: ", b) 61 | test.NotNil(t, e) 62 | test.Equal(t, int64(0), b) 63 | } 64 | 65 | //Int642String 66 | func Test_Int642String_1(t *testing.T) { 67 | var vint int64 = 1 << 62 68 | s := Int642String(vint) 69 | t.Log(vint, "Int64 to String: ", s) 70 | test.Equal(t, "4611686018427387904", s) 71 | } 72 | 73 | func Test_Int642String_2(t *testing.T) { 74 | var vint int64 = 1 << 62 >> 4 75 | s := Int642String(vint) 76 | t.Log(vint, "Int64 to String: ", s) 77 | 78 | test.Equal(t, "288230376151711744", s) 79 | } 80 | 81 | //NSToTime 82 | func Test_NSToTime_1(t *testing.T) { 83 | now := time.Now().UnixNano() 84 | b, e := NSToTime(now) 85 | test.Nil(t, e) 86 | t.Log(now, "NSToTime: ", b) 87 | } 88 | 89 | //NSToTime 90 | func Test_NSToTime_2(t *testing.T) { 91 | now := time.Now().Unix() 92 | b, e := NSToTime(now) 93 | test.Nil(t, e) 94 | t.Log(now, "NSToTime: ", b) 95 | } 96 | -------------------------------------------------------------------------------- /framework/crypto/cryptos.go: -------------------------------------------------------------------------------- 1 | package cryptos 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "crypto/rand" 7 | "encoding/hex" 8 | "math/big" 9 | ) 10 | 11 | // GetMd5String compute the md5 sum as string 12 | func GetMd5String(s string) string { 13 | h := md5.New() 14 | h.Write([]byte(s)) 15 | return hex.EncodeToString(h.Sum(nil)) 16 | } 17 | 18 | // GetRandString returns randominzed string with given length 19 | func GetRandString(length int) string { 20 | var container string 21 | var str = "0123456789abcdefghijklmnopqrstuvwxyz" 22 | b := bytes.NewBufferString(str) 23 | len := b.Len() 24 | bigInt := big.NewInt(int64(len)) 25 | for i := 0; i < length; i++ { 26 | randomInt, _ := rand.Int(rand.Reader, bigInt) 27 | container += string(str[randomInt.Int64()]) 28 | } 29 | return container 30 | } 31 | -------------------------------------------------------------------------------- /framework/crypto/cryptos_test.go: -------------------------------------------------------------------------------- 1 | package cryptos 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/devfeel/dotweb/test" 7 | ) 8 | 9 | // 10 | 11 | func Test_GetMd5String_1(t *testing.T) { 12 | str := "123456789" 13 | md5str := GetMd5String(str) 14 | t.Log("GetMd5String:", md5str) 15 | test.Equal(t, "25f9e794323b453885f5181f1b624d0b", md5str) 16 | } 17 | 18 | func Test_GetRandString(t *testing.T) { 19 | randStr := GetRandString(12) 20 | rand1 := GetRandString(12) 21 | rand2 := GetRandString(12) 22 | rand3 := GetRandString(12) 23 | if rand1 == rand2 || rand2 == rand3 || rand1 == rand3 { 24 | t.Error("rand result is same") 25 | } else { 26 | t.Log("GetRandString:", randStr) 27 | test.Equal(t, 12, len(randStr)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /framework/crypto/des/des.go: -------------------------------------------------------------------------------- 1 | package des 2 | 3 | import ( 4 | "bytes" 5 | "crypto/des" 6 | "errors" 7 | ) 8 | 9 | // ECB PKCS5Padding 10 | func PKCS5Padding(ciphertext []byte, blockSize int) []byte { 11 | padding := blockSize - len(ciphertext)%blockSize 12 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 13 | return append(ciphertext, padtext...) 14 | } 15 | 16 | // ECB PKCS5UnPadding 17 | func PKCS5UnPadding(origData []byte) []byte { 18 | length := len(origData) 19 | unpadding := int(origData[length-1]) 20 | return origData[:(length - unpadding)] 21 | } 22 | 23 | // ECB Des encrypt 24 | func ECBEncrypt(origData, key []byte) ([]byte, error) { 25 | block, err := des.NewCipher(key) 26 | if err != nil { 27 | return nil, err 28 | } 29 | bs := block.BlockSize() 30 | origData = PKCS5Padding(origData, bs) 31 | if len(origData)%bs != 0 { 32 | return nil, errors.New("Need a multiple of the blocksize") 33 | } 34 | out := make([]byte, len(origData)) 35 | dst := out 36 | for len(origData) > 0 { 37 | block.Encrypt(dst, origData[:bs]) 38 | origData = origData[bs:] 39 | dst = dst[bs:] 40 | } 41 | return out, nil 42 | } 43 | 44 | // ECB Des decrypt 45 | func ECBDecrypt(crypted, key []byte) ([]byte, error) { 46 | if len(crypted) < 1 || len(key) < 1 { 47 | return nil, errors.New("wrong data or key") 48 | } 49 | block, err := des.NewCipher(key) 50 | if err != nil { 51 | return nil, err 52 | } 53 | bs := block.BlockSize() 54 | if len(crypted)%bs != 0 { 55 | return nil, errors.New("DecryptDES crypto/cipher: input not full blocks") 56 | } 57 | out := make([]byte, len(crypted)) 58 | dst := out 59 | for len(crypted) > 0 { 60 | block.Decrypt(dst, crypted[:bs]) 61 | crypted = crypted[bs:] 62 | dst = dst[bs:] 63 | } 64 | out = PKCS5UnPadding(out) 65 | return out, nil 66 | } 67 | 68 | // [golang ECB 3DES Encrypt] 69 | func TripleEcbDesEncrypt(origData, key []byte) ([]byte, error) { 70 | tkey := make([]byte, 24, 24) 71 | copy(tkey, key) 72 | k1 := tkey[:8] 73 | k2 := tkey[8:16] 74 | k3 := tkey[16:] 75 | 76 | block, err := des.NewCipher(k1) 77 | if err != nil { 78 | return nil, err 79 | } 80 | bs := block.BlockSize() 81 | origData = PKCS5Padding(origData, bs) 82 | 83 | buf1, err := ECBEncrypt(origData, k1) 84 | if err != nil { 85 | return nil, err 86 | } 87 | buf2, err := ECBDecrypt(buf1, k2) 88 | if err != nil { 89 | return nil, err 90 | } 91 | out, err := ECBEncrypt(buf2, k3) 92 | if err != nil { 93 | return nil, err 94 | } 95 | return out, nil 96 | } 97 | 98 | // [golang ECB 3DES Decrypt] 99 | func TripleEcbDesDecrypt(crypted, key []byte) ([]byte, error) { 100 | tkey := make([]byte, 24, 24) 101 | copy(tkey, key) 102 | k1 := tkey[:8] 103 | k2 := tkey[8:16] 104 | k3 := tkey[16:] 105 | buf1, err := ECBDecrypt(crypted, k3) 106 | if err != nil { 107 | return nil, err 108 | } 109 | buf2, err := ECBEncrypt(buf1, k2) 110 | if err != nil { 111 | return nil, err 112 | } 113 | out, err := ECBDecrypt(buf2, k1) 114 | if err != nil { 115 | return nil, err 116 | } 117 | out = PKCS5UnPadding(out) 118 | return out, nil 119 | } 120 | -------------------------------------------------------------------------------- /framework/crypto/des/des_test.go: -------------------------------------------------------------------------------- 1 | package des 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/devfeel/dotweb/test" 8 | ) 9 | 10 | // 11 | 12 | func Test_ECBEncrypt_1(t *testing.T) { 13 | key := []byte("01234567") 14 | origData := []byte("dotweb@devfeel") 15 | b, e := ECBEncrypt(origData, key) 16 | if e != nil { 17 | t.Error(e) 18 | } else { 19 | t.Logf("%x\n", b) 20 | } 21 | 22 | test.Equal(t, "72f9f187eafe43478f9eb3dd49ef7b43", fmt.Sprintf("%x", b)) 23 | } 24 | 25 | func Test_ECBDecrypt_1(t *testing.T) { 26 | key := []byte("01234567") 27 | origData := []byte("dotweb@devfeel") 28 | b1, e1 := ECBEncrypt(origData, key) 29 | if e1 != nil { 30 | t.Error(e1) 31 | } 32 | b, e := ECBDecrypt(b1, key) 33 | if e != nil { 34 | t.Error(e) 35 | } else { 36 | t.Logf("%x\n", b) 37 | } 38 | 39 | test.Equal(t, "dotweb@devfeel", string(b)) 40 | } 41 | 42 | func Test_PKCS5Padding_1(t *testing.T) {} 43 | 44 | func Test_PKCS5UnPadding_1(t *testing.T) {} 45 | 46 | func Test_TripleEcbDesDecrypt_1(t *testing.T) {} 47 | 48 | func Test_TripleEcbDesEncrypt_1(t *testing.T) {} 49 | -------------------------------------------------------------------------------- /framework/crypto/uuid/uuid_test.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/devfeel/dotweb/test" 7 | ) 8 | 9 | // Test_GetUUID_V1_32 test uuid with v1 and return 32 len string 10 | func Test_GetUUID_V1_32(t *testing.T) { 11 | uuid := NewV1().String32() 12 | t.Log("GetUUID:", uuid) 13 | test.Equal(t, 32, len(uuid)) 14 | } 15 | 16 | // Test_GetUUID_V1 test uuid with v1 and return 36 len string 17 | func Test_GetUUID_V1(t *testing.T) { 18 | uuid := NewV1().String() 19 | t.Log("GetUUID:", uuid) 20 | test.Equal(t, 36, len(uuid)) 21 | } 22 | 23 | func Benchmark_GetUUID_V1_32(b *testing.B) { 24 | for i := 0; i < b.N; i++ { 25 | NewV1().String32() 26 | } 27 | } 28 | 29 | // Test_GetUUID_V4_32 test uuid with v1 and return 32 len string 30 | func Test_GetUUID_V4_32(t *testing.T) { 31 | uuid := NewV4().String32() 32 | t.Log("GetUUID:", uuid) 33 | test.Equal(t, 32, len(uuid)) 34 | } 35 | 36 | // Test_GetUUID_V4 test uuid with v1 and return 36 len string 37 | func Test_GetUUID_V4(t *testing.T) { 38 | uuid := NewV4().String() 39 | t.Log("GetUUID:", uuid) 40 | test.Equal(t, 36, len(uuid)) 41 | } 42 | func Benchmark_GetUUID_V4_32(b *testing.B) { 43 | for i := 0; i < b.N; i++ { 44 | NewV4().String32() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /framework/encodes/base64x/base64util.go: -------------------------------------------------------------------------------- 1 | package base64x 2 | 3 | import "encoding/base64" 4 | 5 | // EncodeString encode string use base64 StdEncoding 6 | func EncodeString(source string) string { 7 | return base64.StdEncoding.EncodeToString([]byte(source)) 8 | } 9 | 10 | // DecodeString deencode string use base64 StdEncoding 11 | func DecodeString(source string) (string, error) { 12 | dst, err := base64.StdEncoding.DecodeString(source) 13 | if err != nil { 14 | return "", err 15 | } else { 16 | return string(dst), nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /framework/encodes/base64x/base64util_test.go: -------------------------------------------------------------------------------- 1 | package base64x 2 | 3 | import "testing" 4 | 5 | func TestEncodeString(t *testing.T) { 6 | source := "welcome to dotweb!" 7 | t.Log(EncodeString(source)) 8 | } 9 | 10 | func TestDecodeString(t *testing.T) { 11 | source := "welcome to dotweb!" 12 | encode := EncodeString(source) 13 | 14 | dst, err := DecodeString(encode) 15 | if err != nil { 16 | t.Error("TestDecodeString error", err) 17 | } else { 18 | t.Log("TestDecodeString success", dst, source) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /framework/encodes/gob/gobutil.go: -------------------------------------------------------------------------------- 1 | package gob 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | ) 7 | 8 | func init() { 9 | gob.Register([]interface{}{}) 10 | gob.Register(map[int]interface{}{}) 11 | gob.Register(map[string]interface{}{}) 12 | gob.Register(map[interface{}]interface{}{}) 13 | gob.Register(map[string]string{}) 14 | gob.Register(map[int]string{}) 15 | gob.Register(map[int]int{}) 16 | gob.Register(map[int]int64{}) 17 | } 18 | 19 | // EncodeMap encode the map to gob 20 | func EncodeMap(obj map[interface{}]interface{}) ([]byte, error) { 21 | buf := bytes.NewBuffer(nil) 22 | enc := gob.NewEncoder(buf) 23 | err := enc.Encode(obj) 24 | if err != nil { 25 | return []byte(""), err 26 | } 27 | return buf.Bytes(), nil 28 | } 29 | 30 | // DecodeMap decode data to map 31 | func DecodeMap(encoded []byte) (map[interface{}]interface{}, error) { 32 | buf := bytes.NewBuffer(encoded) 33 | dec := gob.NewDecoder(buf) 34 | var out map[interface{}]interface{} 35 | err := dec.Decode(&out) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return out, nil 40 | } 41 | -------------------------------------------------------------------------------- /framework/encodes/gob/gobutil_test.go: -------------------------------------------------------------------------------- 1 | package gob 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // 8 | 9 | func Test_EncodeMap_1(t *testing.T) { 10 | // 11 | } 12 | -------------------------------------------------------------------------------- /framework/exception/exception.go: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime/debug" 7 | ) 8 | 9 | // CatchError is the unified exception handler 10 | func CatchError(title string, logtarget string, err interface{}) (errmsg string) { 11 | errmsg = fmt.Sprintln(err) 12 | stack := string(debug.Stack()) 13 | os.Stdout.Write([]byte(title + " error! => " + errmsg + " => " + stack)) 14 | return title + " error! => " + errmsg + " => " + stack 15 | } 16 | -------------------------------------------------------------------------------- /framework/exception/exception_test.go: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | // func Test_CatchError_1(t *testing.T) { 4 | // err := errors.New("runtime error: slice bounds out of range.") 5 | // errMsg := CatchError("httpserver::RouterHandle", dotweb.LogTarget_HttpServer, err) 6 | // t.Log(errMsg) 7 | // } 8 | -------------------------------------------------------------------------------- /framework/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | func GetCurrentDirectory() string { 11 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 12 | if err != nil { 13 | log.Fatalln(err) 14 | } 15 | return strings.Replace(dir, "\\", "/", -1) 16 | } 17 | 18 | // check filename exists 19 | func Exist(filename string) bool { 20 | _, err := os.Stat(filename) 21 | return err == nil || os.IsExist(err) 22 | } 23 | -------------------------------------------------------------------------------- /framework/file/file_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | ) 7 | 8 | // 以下是功能测试 9 | 10 | func Test_GetCurrentDirectory_1(t *testing.T) { 11 | thisDir := GetCurrentDirectory() 12 | t.Log(thisDir) 13 | } 14 | 15 | func Test_GetFileExt_1(t *testing.T) { 16 | fn := "/download/vagrant_1.9.2.dmg" 17 | fileExt := filepath.Ext(fn) 18 | if len(fileExt) < 1 { 19 | t.Error("fileExt null!") 20 | } else { 21 | t.Log(fileExt) 22 | } 23 | } 24 | 25 | func Test_GetFileExt_2(t *testing.T) { 26 | fn := "/download/vagrant_1.abc" 27 | fileExt := filepath.Ext(fn) 28 | if len(fileExt) < 1 { 29 | t.Error("fileExt null!") 30 | } else { 31 | t.Log(fileExt) 32 | } 33 | } 34 | 35 | func Test_Exist_1(t *testing.T) { 36 | fn := "testdata/file.test" 37 | // fn := "/Users/kevin/Downloads/commdownload.dmg" 38 | isExist := Exist(fn) 39 | if isExist { 40 | t.Log(isExist) 41 | } else { 42 | t.Log("请修改测试代码中文件的路径!") 43 | } 44 | } 45 | 46 | // 以下是性能测试 47 | -------------------------------------------------------------------------------- /framework/file/path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Based on the path package, Copyright 2009 The Go Authors. 3 | // Use of this source code is governed by a BSD-style license that can be found 4 | // in the LICENSE file. 5 | 6 | package file 7 | 8 | // CleanPath is the URL version of path.Clean, it returns a canonical URL path 9 | // for p, eliminating . and .. elements. 10 | // 11 | // The following rules are applied iteratively until no further processing can 12 | // be done: 13 | // 1. Replace multiple slashes with a single slash. 14 | // 2. Eliminate each . path name element (the current directory). 15 | // 3. Eliminate each inner .. path name element (the parent directory) 16 | // along with the non-.. element that precedes it. 17 | // 4. Eliminate .. elements that begin a rooted path: 18 | // that is, replace "/.." by "/" at the beginning of a path. 19 | // 20 | // If the result of this process is an empty string, "/" is returned 21 | func CleanPath(p string) string { 22 | // Turn empty string into "/" 23 | if p == "" { 24 | return "/" 25 | } 26 | 27 | n := len(p) 28 | var buf []byte 29 | 30 | // Invariants: 31 | // reading from path; r is index of next byte to process. 32 | // writing to buf; w is index of next byte to write. 33 | 34 | // path must start with '/' 35 | r := 1 36 | w := 1 37 | 38 | if p[0] != '/' { 39 | r = 0 40 | buf = make([]byte, n+1) 41 | buf[0] = '/' 42 | } 43 | 44 | trailing := n > 2 && p[n-1] == '/' 45 | 46 | // A bit more clunky without a 'lazybuf' like the path package, but the loop 47 | // gets completely inlined (bufApp). So in contrast to the path package this 48 | // loop has no expensive function calls (except 1x make) 49 | 50 | for r < n { 51 | switch { 52 | case p[r] == '/': 53 | // empty path element, trailing slash is added after the end 54 | r++ 55 | 56 | case p[r] == '.' && r+1 == n: 57 | trailing = true 58 | r++ 59 | 60 | case p[r] == '.' && p[r+1] == '/': 61 | // . element 62 | r++ 63 | 64 | case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): 65 | // .. element: remove to last / 66 | r += 2 67 | 68 | if w > 1 { 69 | // can backtrack 70 | w-- 71 | 72 | if buf == nil { 73 | for w > 1 && p[w] != '/' { 74 | w-- 75 | } 76 | } else { 77 | for w > 1 && buf[w] != '/' { 78 | w-- 79 | } 80 | } 81 | } 82 | 83 | default: 84 | // real path element. 85 | // add slash if needed 86 | if w > 1 { 87 | bufApp(&buf, p, w, '/') 88 | w++ 89 | } 90 | 91 | // copy element 92 | for r < n && p[r] != '/' { 93 | bufApp(&buf, p, w, p[r]) 94 | w++ 95 | r++ 96 | } 97 | } 98 | } 99 | 100 | // re-append trailing slash 101 | if trailing && w > 1 { 102 | bufApp(&buf, p, w, '/') 103 | w++ 104 | } 105 | 106 | if buf == nil { 107 | return p[:w] 108 | } 109 | return string(buf[:w]) 110 | } 111 | 112 | // internal helper to lazily create a buffer if necessary 113 | func bufApp(buf *[]byte, s string, w int, c byte) { 114 | if *buf == nil { 115 | if s[w] == c { 116 | return 117 | } 118 | 119 | *buf = make([]byte, len(s)) 120 | copy(*buf, s[:w]) 121 | } 122 | (*buf)[w] = c 123 | } 124 | -------------------------------------------------------------------------------- /framework/file/path_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | -------------------------------------------------------------------------------- /framework/file/testdata/file.test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfeel/dotweb/d50e13e812dfddda82c6f976ef1cc9007ea06e7c/framework/file/testdata/file.test -------------------------------------------------------------------------------- /framework/hystrix/counter.go: -------------------------------------------------------------------------------- 1 | package hystrix 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | const ( 8 | minuteTimeLayout = "200601021504" 9 | ) 10 | 11 | // Counter incremented and decremented base on int64 value. 12 | type Counter interface { 13 | Clear() 14 | Count() int64 15 | Dec(int64) 16 | Inc(int64) 17 | } 18 | 19 | // NewCounter constructs a new StandardCounter. 20 | func NewCounter() Counter { 21 | return &StandardCounter{} 22 | } 23 | 24 | // StandardCounter is the standard implementation of a Counter 25 | type StandardCounter struct { 26 | count int64 27 | } 28 | 29 | // Clear sets the counter to zero. 30 | func (c *StandardCounter) Clear() { 31 | atomic.StoreInt64(&c.count, 0) 32 | } 33 | 34 | // Count returns the current count. 35 | func (c *StandardCounter) Count() int64 { 36 | return atomic.LoadInt64(&c.count) 37 | } 38 | 39 | // Dec decrements the counter by the given amount. 40 | func (c *StandardCounter) Dec(i int64) { 41 | atomic.AddInt64(&c.count, -i) 42 | } 43 | 44 | // Inc increments the counter by the given amount. 45 | func (c *StandardCounter) Inc(i int64) { 46 | atomic.AddInt64(&c.count, i) 47 | } 48 | -------------------------------------------------------------------------------- /framework/hystrix/hystrix.go: -------------------------------------------------------------------------------- 1 | package hystrix 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | const ( 9 | status_Hystrix = 1 10 | status_Alive = 2 11 | DefaultCheckHystrixInterval = 10 // unit is Second 12 | DefaultCheckAliveInterval = 60 // unit is Second 13 | DefaultCleanHistoryInterval = 60 * 5 // unit is Second 14 | DefaultMaxFailedNumber = 100 15 | DefaultReserveMinutes = 30 16 | ) 17 | 18 | type Hystrix interface { 19 | // Do begin do check 20 | Do() 21 | // RegisterAliveCheck register check Alive func 22 | RegisterAliveCheck(CheckFunc) 23 | // RegisterHystrixCheck register check Hystrix func 24 | RegisterHystrixCheck(CheckFunc) 25 | // IsHystrix return is Hystrix status 26 | IsHystrix() bool 27 | // TriggerHystrix trigger Hystrix status 28 | TriggerHystrix() 29 | // TriggerAlive trigger Alive status 30 | TriggerAlive() 31 | // SetCheckInterval set interval for doCheckHystric and doCheckAlive, unit is Second 32 | SetCheckInterval(int, int) 33 | 34 | // GetCounter get lasted Counter with time key 35 | GetCounter() Counter 36 | 37 | // SetMaxFailed set max failed count for hystrix default counter 38 | SetMaxFailedNumber(int64) 39 | } 40 | 41 | type CheckFunc func() bool 42 | 43 | type StandHystrix struct { 44 | status int 45 | checkHystrixFunc CheckFunc 46 | checkHystrixInterval int 47 | checkAliveFunc CheckFunc 48 | checkAliveInterval int 49 | 50 | maxFailedNumber int64 51 | counters *sync.Map 52 | } 53 | 54 | // NewHystrix create new Hystrix, config with CheckAliveFunc and checkAliveInterval, unit is Minute 55 | func NewHystrix(checkAlive CheckFunc, checkHysrix CheckFunc) Hystrix { 56 | h := &StandHystrix{ 57 | counters: new(sync.Map), 58 | status: status_Alive, 59 | checkAliveFunc: checkAlive, 60 | checkHystrixFunc: checkHysrix, 61 | checkAliveInterval: DefaultCheckAliveInterval, 62 | checkHystrixInterval: DefaultCheckHystrixInterval, 63 | maxFailedNumber: DefaultMaxFailedNumber, 64 | } 65 | if h.checkHystrixFunc == nil { 66 | h.checkHystrixFunc = h.defaultCheckHystrix 67 | } 68 | return h 69 | } 70 | 71 | func (h *StandHystrix) Do() { 72 | go h.doCheck() 73 | go h.doCleanHistoryCounter() 74 | } 75 | 76 | func (h *StandHystrix) SetCheckInterval(hystrixInterval, aliveInterval int) { 77 | h.checkAliveInterval = aliveInterval 78 | h.checkHystrixInterval = hystrixInterval 79 | } 80 | 81 | // SetMaxFailed set max failed count for hystrix default counter 82 | func (h *StandHystrix) SetMaxFailedNumber(number int64) { 83 | h.maxFailedNumber = number 84 | } 85 | 86 | // GetCounter get lasted Counter with time key 87 | func (h *StandHystrix) GetCounter() Counter { 88 | key := getLastedTimeKey() 89 | var counter Counter 90 | loadCounter, exists := h.counters.Load(key) 91 | if !exists { 92 | counter = NewCounter() 93 | h.counters.Store(key, counter) 94 | } else { 95 | counter = loadCounter.(Counter) 96 | } 97 | return counter 98 | } 99 | 100 | func (h *StandHystrix) IsHystrix() bool { 101 | return h.status == status_Hystrix 102 | } 103 | 104 | func (h *StandHystrix) RegisterAliveCheck(check CheckFunc) { 105 | h.checkAliveFunc = check 106 | } 107 | 108 | func (h *StandHystrix) RegisterHystrixCheck(check CheckFunc) { 109 | h.checkHystrixFunc = check 110 | } 111 | 112 | func (h *StandHystrix) TriggerHystrix() { 113 | h.status = status_Hystrix 114 | } 115 | 116 | func (h *StandHystrix) TriggerAlive() { 117 | h.status = status_Alive 118 | } 119 | 120 | // doCheck do checkAlive when status is Hystrix or checkHytrix when status is Alive 121 | func (h *StandHystrix) doCheck() { 122 | if h.checkAliveFunc == nil || h.checkHystrixFunc == nil { 123 | return 124 | } 125 | if h.IsHystrix() { 126 | isAlive := h.checkAliveFunc() 127 | if isAlive { 128 | h.TriggerAlive() 129 | h.GetCounter().Clear() 130 | time.AfterFunc(time.Duration(h.checkHystrixInterval)*time.Second, h.doCheck) 131 | } else { 132 | time.AfterFunc(time.Duration(h.checkAliveInterval)*time.Second, h.doCheck) 133 | } 134 | } else { 135 | isHystrix := h.checkHystrixFunc() 136 | if isHystrix { 137 | h.TriggerHystrix() 138 | time.AfterFunc(time.Duration(h.checkAliveInterval)*time.Second, h.doCheck) 139 | } else { 140 | time.AfterFunc(time.Duration(h.checkHystrixInterval)*time.Second, h.doCheck) 141 | } 142 | } 143 | } 144 | 145 | func (h *StandHystrix) doCleanHistoryCounter() { 146 | var needRemoveKey []string 147 | now, _ := time.Parse(minuteTimeLayout, time.Now().Format(minuteTimeLayout)) 148 | h.counters.Range(func(k, v interface{}) bool { 149 | key := k.(string) 150 | if t, err := time.Parse(minuteTimeLayout, key); err != nil { 151 | needRemoveKey = append(needRemoveKey, key) 152 | } else { 153 | if now.Sub(t) > (DefaultReserveMinutes * time.Minute) { 154 | needRemoveKey = append(needRemoveKey, key) 155 | } 156 | } 157 | return true 158 | }) 159 | for _, k := range needRemoveKey { 160 | // fmt.Println(time.Now(), "hystrix doCleanHistoryCounter remove key",k) 161 | h.counters.Delete(k) 162 | } 163 | time.AfterFunc(time.Duration(DefaultCleanHistoryInterval)*time.Second, h.doCleanHistoryCounter) 164 | } 165 | 166 | func (h *StandHystrix) defaultCheckHystrix() bool { 167 | count := h.GetCounter().Count() 168 | if count > h.maxFailedNumber { 169 | return true 170 | } else { 171 | return false 172 | } 173 | } 174 | 175 | func getLastedTimeKey() string { 176 | key := time.Now().Format(minuteTimeLayout) 177 | if time.Now().Minute()/2 != 0 { 178 | key = time.Now().Add(time.Duration(-1 * time.Minute)).Format(minuteTimeLayout) 179 | } 180 | return key 181 | } 182 | -------------------------------------------------------------------------------- /framework/json/jsonutil.go: -------------------------------------------------------------------------------- 1 | package jsonutil 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // GetJsonString marshals the object as string 8 | func GetJsonString(obj interface{}) string { 9 | resByte, err := json.Marshal(obj) 10 | if err != nil { 11 | return "" 12 | } 13 | return string(resByte) 14 | } 15 | 16 | // Marshal marshals the value as string 17 | func Marshal(v interface{}) (string, error) { 18 | resByte, err := json.Marshal(v) 19 | if err != nil { 20 | return "", err 21 | } else { 22 | return string(resByte), nil 23 | } 24 | } 25 | 26 | // Unmarshal converts the jsonstring into value 27 | func Unmarshal(jsonstring string, v interface{}) error { 28 | return json.Unmarshal([]byte(jsonstring), v) 29 | } 30 | -------------------------------------------------------------------------------- /framework/json/jsonutil_test.go: -------------------------------------------------------------------------------- 1 | package jsonutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // 以下是功能测试 8 | 9 | type ( 10 | colorGroup struct { 11 | ID int 12 | Name string 13 | Colors []string 14 | } 15 | mymap map[string]interface{} 16 | ) 17 | 18 | func Test_GetJsonString_1(t *testing.T) { 19 | g := newGroup() 20 | json := GetJsonString(g) 21 | t.Log(json) 22 | } 23 | 24 | func Test_Marshal_1(t *testing.T) { 25 | g := newGroup() 26 | json, err := Marshal(g) 27 | if err != nil { 28 | t.Error(err) 29 | } else { 30 | t.Log(json) 31 | } 32 | 33 | } 34 | 35 | func Test_Unmarshal_1(t *testing.T) { 36 | var group colorGroup 37 | g := newGroup() 38 | json := GetJsonString(g) 39 | err := Unmarshal(json, &group) 40 | if err != nil { 41 | t.Error(err) 42 | } else { 43 | t.Log(group) 44 | } 45 | 46 | } 47 | 48 | func newGroup() colorGroup { 49 | group := colorGroup{ 50 | ID: 1, 51 | Name: "Reds", 52 | Colors: []string{"Crimson", "Red", "Ruby", "Maroon"}, 53 | } 54 | return group 55 | } 56 | -------------------------------------------------------------------------------- /framework/redis/redisutil_test.go: -------------------------------------------------------------------------------- 1 | package redisutil 2 | 3 | /* 4 | import ( 5 | "testing" 6 | ) 7 | 8 | const redisServerURL = "redis://:123456@192.168.8.175:7001/0" 9 | 10 | func TestRedisClient_Ping(t *testing.T) { 11 | redisClient := GetRedisClient(redisServerURL) 12 | val, err := redisClient.Ping() 13 | if err != nil { 14 | t.Error(err) 15 | } else { 16 | t.Log(val) 17 | } 18 | } 19 | */ 20 | -------------------------------------------------------------------------------- /framework/reflects/reflects.go: -------------------------------------------------------------------------------- 1 | package reflects 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | // convert map to struct 10 | func ConvertMapToStruct(tagName string, ptr interface{}, form map[string][]string) error { 11 | typ := reflect.TypeOf(ptr).Elem() 12 | val := reflect.ValueOf(ptr).Elem() 13 | 14 | for i := 0; i < typ.NumField(); i++ { 15 | typeField := typ.Field(i) 16 | structField := val.Field(i) 17 | if !structField.CanSet() { 18 | continue 19 | } 20 | structFieldKind := structField.Kind() 21 | inputFieldName := typeField.Tag.Get(tagName) 22 | 23 | if inputFieldName == "" { 24 | inputFieldName = typeField.Name 25 | // If "form" tag is nil, we inspect if the field is a struct. 26 | if structFieldKind == reflect.Struct { 27 | err := ConvertMapToStruct(tagName, structField.Addr().Interface(), form) 28 | if err != nil { 29 | return err 30 | } 31 | continue 32 | } 33 | } 34 | inputValue, exists := form[inputFieldName] 35 | if !exists { 36 | continue 37 | } 38 | 39 | numElems := len(inputValue) 40 | if structFieldKind == reflect.Slice && numElems > 0 { 41 | sliceOf := structField.Type().Elem().Kind() 42 | slice := reflect.MakeSlice(structField.Type(), numElems, numElems) 43 | for i := 0; i < numElems; i++ { 44 | if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { 45 | return err 46 | } 47 | } 48 | val.Field(i).Set(slice) 49 | } else { 50 | if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { 51 | return err 52 | } 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { 59 | switch valueKind { 60 | case reflect.Int: 61 | return setIntField(val, 0, structField) 62 | case reflect.Int8: 63 | return setIntField(val, 8, structField) 64 | case reflect.Int16: 65 | return setIntField(val, 16, structField) 66 | case reflect.Int32: 67 | return setIntField(val, 32, structField) 68 | case reflect.Int64: 69 | return setIntField(val, 64, structField) 70 | case reflect.Uint: 71 | return setUintField(val, 0, structField) 72 | case reflect.Uint8: 73 | return setUintField(val, 8, structField) 74 | case reflect.Uint16: 75 | return setUintField(val, 16, structField) 76 | case reflect.Uint32: 77 | return setUintField(val, 32, structField) 78 | case reflect.Uint64: 79 | return setUintField(val, 64, structField) 80 | case reflect.Bool: 81 | return setBoolField(val, structField) 82 | case reflect.Float32: 83 | return setFloatField(val, 32, structField) 84 | case reflect.Float64: 85 | return setFloatField(val, 64, structField) 86 | case reflect.String: 87 | structField.SetString(val) 88 | default: 89 | return errors.New("unknown type") 90 | } 91 | return nil 92 | } 93 | 94 | func setIntField(value string, bitSize int, field reflect.Value) error { 95 | if value == "" { 96 | value = "0" 97 | } 98 | intVal, err := strconv.ParseInt(value, 10, bitSize) 99 | if err == nil { 100 | field.SetInt(intVal) 101 | } 102 | return err 103 | } 104 | 105 | func setUintField(value string, bitSize int, field reflect.Value) error { 106 | if value == "" { 107 | value = "0" 108 | } 109 | uintVal, err := strconv.ParseUint(value, 10, bitSize) 110 | if err == nil { 111 | field.SetUint(uintVal) 112 | } 113 | return err 114 | } 115 | 116 | func setBoolField(value string, field reflect.Value) error { 117 | if value == "" { 118 | value = "false" 119 | } 120 | boolVal, err := strconv.ParseBool(value) 121 | if err == nil { 122 | field.SetBool(boolVal) 123 | } 124 | return err 125 | } 126 | 127 | func setFloatField(value string, bitSize int, field reflect.Value) error { 128 | if value == "" { 129 | value = "0.0" 130 | } 131 | floatVal, err := strconv.ParseFloat(value, bitSize) 132 | if err == nil { 133 | field.SetFloat(floatVal) 134 | } 135 | return err 136 | } 137 | -------------------------------------------------------------------------------- /framework/reflects/reflects_test.go: -------------------------------------------------------------------------------- 1 | package reflects 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // 以下是功能测试 8 | 9 | // convert map to struct 10 | // ConvertMapToStruct(tagName string, ptr interface{}, form map[string][]string) error 11 | func Test_ConvertMapToStruct_1(t *testing.T) { 12 | t.Log("ok") 13 | } 14 | 15 | // setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error 16 | 17 | // setIntField(value string, bitSize int, field reflect.Value) error 18 | 19 | // setUintField(value string, bitSize int, field reflect.Value) error 20 | 21 | // setBoolField(value string, field reflect.Value) error 22 | 23 | // setFloatField(value string, bitSize int, field reflect.Value) error 24 | -------------------------------------------------------------------------------- /framework/stringx/stringx.go: -------------------------------------------------------------------------------- 1 | package stringx 2 | 3 | // CompletionRight Completion content with flag on right 4 | func CompletionRight(content, flag string, length int) string { 5 | if length <= 0 { 6 | return "" 7 | } 8 | if len(content) >= length { 9 | return string(content[0:length]) 10 | } 11 | 12 | flagsLegth := length - len(content) 13 | flags := flag 14 | for { 15 | if len(flags) == flagsLegth { 16 | break 17 | } else if len(flags) > flagsLegth { 18 | flags = string(flags[0:flagsLegth]) 19 | break 20 | } else { 21 | flags = flags + flag 22 | } 23 | } 24 | return content + flags 25 | } 26 | 27 | // CompletionLeft Completion content with flag on left 28 | func CompletionLeft(content, flag string, length int) string { 29 | if length <= 0 { 30 | return "" 31 | } 32 | if len(content) >= length { 33 | return string(content[0:length]) 34 | } 35 | flagsLegth := length - len(content) 36 | flags := flag 37 | for { 38 | if len(flags) == flagsLegth { 39 | break 40 | } else if len(flags) > flagsLegth { 41 | flags = string(flags[0:flagsLegth]) 42 | break 43 | } else { 44 | flags = flags + flag 45 | } 46 | } 47 | return flags + content 48 | } 49 | -------------------------------------------------------------------------------- /framework/stringx/stringx_test.go: -------------------------------------------------------------------------------- 1 | package stringx 2 | 3 | import ( 4 | "github.com/devfeel/dotweb/test" 5 | "testing" 6 | ) 7 | 8 | func TestCompletionRight(t *testing.T) { 9 | content := "ab" 10 | flag := "cbc" 11 | length := 6 12 | wantResult := "abcbcc" 13 | test.Equal(t, wantResult, CompletionRight(content, flag, length)) 14 | } 15 | 16 | func TestCompletionLeft(t *testing.T) { 17 | content := "ab" 18 | flag := "cbc" 19 | length := 6 20 | wantResult := "cbccab" 21 | test.Equal(t, wantResult, CompletionLeft(content, flag, length)) 22 | } 23 | -------------------------------------------------------------------------------- /framework/sysx/sysx.go: -------------------------------------------------------------------------------- 1 | package sysx 2 | 3 | import "os" 4 | 5 | // GetHostName get host name 6 | func GetHostName() string { 7 | host, _ := os.Hostname() 8 | return host 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/devfeel/dotweb 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/garyburd/redigo v1.6.0 7 | golang.org/x/net v0.0.0-20190606173856-1492cefac77f 8 | gopkg.in/yaml.v2 v2.2.2 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= 2 | github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= 3 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 4 | golang.org/x/net v0.0.0-20190606173856-1492cefac77f h1:IWHgpgFqnL5AhBUBZSgBdjl2vkQUEzcY+JNKWfcgAU0= 5 | golang.org/x/net v0.0.0-20190606173856-1492cefac77f/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 6 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 7 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 10 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 11 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import "reflect" 4 | 5 | type ( 6 | Group interface { 7 | Use(m ...Middleware) Group 8 | Group(prefix string, m ...Middleware) Group 9 | DELETE(path string, h HttpHandle) RouterNode 10 | GET(path string, h HttpHandle) RouterNode 11 | HEAD(path string, h HttpHandle) RouterNode 12 | OPTIONS(path string, h HttpHandle) RouterNode 13 | PATCH(path string, h HttpHandle) RouterNode 14 | POST(path string, h HttpHandle) RouterNode 15 | PUT(path string, h HttpHandle) RouterNode 16 | ServerFile(path string, fileroot string) RouterNode 17 | RegisterRoute(method, path string, h HttpHandle) RouterNode 18 | } 19 | xGroup struct { 20 | prefix string 21 | middlewares []Middleware 22 | allRouterExpress map[string]struct{} 23 | server *HttpServer 24 | } 25 | ) 26 | 27 | func NewGroup(prefix string, server *HttpServer) Group { 28 | g := &xGroup{prefix: prefix, server: server, allRouterExpress: make(map[string]struct{})} 29 | server.groups = append(server.groups, g) 30 | server.Logger().Debug("DotWeb:Group NewGroup ["+prefix+"]", LogTarget_HttpServer) 31 | return g 32 | } 33 | 34 | // Use implements `Router#Use()` for sub-routes within the Group. 35 | func (g *xGroup) Use(ms ...Middleware) Group { 36 | if len(ms) <= 0 { 37 | return g 38 | } 39 | 40 | // deepcopy middleware structs to avoid middleware chain misbehaving 41 | m := []Middleware{} 42 | for _, om := range ms { 43 | //newM := reflect.New(reflect.ValueOf(om).Elem().Type()).Interface().(Middleware) 44 | newElem := reflect.New(reflect.TypeOf(om).Elem()) 45 | newElem.Elem().Set(reflect.ValueOf(om).Elem()) 46 | newM := newElem.Interface().(Middleware) 47 | 48 | newM.SetNext(nil) 49 | m = append(m, newM) 50 | } 51 | step := len(g.middlewares) - 1 52 | for i := range m { 53 | if m[i] != nil { 54 | if step >= 0 { 55 | g.middlewares[step].SetNext(m[i]) 56 | } 57 | g.middlewares = append(g.middlewares, m[i]) 58 | step++ 59 | } 60 | } 61 | return g 62 | } 63 | 64 | // DELETE implements `Router#DELETE()` for sub-routes within the Group. 65 | func (g *xGroup) DELETE(path string, h HttpHandle) RouterNode { 66 | return g.add(RouteMethod_DELETE, path, h) 67 | } 68 | 69 | // GET implements `Router#GET()` for sub-routes within the Group. 70 | func (g *xGroup) GET(path string, h HttpHandle) RouterNode { 71 | return g.add(RouteMethod_GET, path, h) 72 | } 73 | 74 | // HEAD implements `Router#HEAD()` for sub-routes within the Group. 75 | func (g *xGroup) HEAD(path string, h HttpHandle) RouterNode { 76 | return g.add(RouteMethod_HEAD, path, h) 77 | } 78 | 79 | // OPTIONS implements `Router#OPTIONS()` for sub-routes within the Group. 80 | func (g *xGroup) OPTIONS(path string, h HttpHandle) RouterNode { 81 | return g.add(RouteMethod_OPTIONS, path, h) 82 | } 83 | 84 | // PATCH implements `Router#PATCH()` for sub-routes within the Group. 85 | func (g *xGroup) PATCH(path string, h HttpHandle) RouterNode { 86 | return g.add(RouteMethod_PATCH, path, h) 87 | } 88 | 89 | // POST implements `Router#POST()` for sub-routes within the Group. 90 | func (g *xGroup) POST(path string, h HttpHandle) RouterNode { 91 | return g.add(RouteMethod_POST, path, h) 92 | } 93 | 94 | // PUT implements `Router#PUT()` for sub-routes within the Group. 95 | func (g *xGroup) PUT(path string, h HttpHandle) RouterNode { 96 | return g.add(RouteMethod_PUT, path, h) 97 | } 98 | 99 | // PUT implements `Router#PUT()` for sub-routes within the Group. 100 | func (g *xGroup) ServerFile(path string, fileroot string) RouterNode { 101 | g.allRouterExpress[RouteMethod_GET+routerExpressSplit+g.prefix+path] = struct{}{} 102 | node := g.server.Router().ServerFile(g.prefix+path, fileroot) 103 | node.Node().groupMiddlewares = g.middlewares 104 | return node 105 | } 106 | 107 | // Group creates a new sub-group with prefix and optional sub-group-level middleware. 108 | func (g *xGroup) Group(prefix string, m ...Middleware) Group { 109 | return NewGroup(g.prefix+prefix, g.server).Use(g.middlewares...).Use(m...) 110 | } 111 | 112 | func (g *xGroup) RegisterRoute(method, path string, handler HttpHandle) RouterNode { 113 | return g.add(method, path, handler) 114 | } 115 | 116 | func (g *xGroup) add(method, path string, handler HttpHandle) RouterNode { 117 | node := g.server.Router().RegisterRoute(method, g.prefix+path, handler) 118 | g.allRouterExpress[method+routerExpressSplit+g.prefix+path] = struct{}{} 119 | node.Node().groupMiddlewares = g.middlewares 120 | return node 121 | } 122 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "errors" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/devfeel/dotweb/framework/file" 10 | ) 11 | 12 | const ( 13 | // LogLevelDebug raw log level 14 | LogLevelRaw = "RAW" 15 | // LogLevelDebug debug log level 16 | LogLevelDebug = "DEBUG" 17 | // LogLevelInfo info log level 18 | LogLevelInfo = "INFO" 19 | // LogLevelWarn warn log level 20 | LogLevelWarn = "WARN" 21 | // LogLevelError error log level 22 | LogLevelError = "ERROR" 23 | ) 24 | 25 | type AppLog interface { 26 | SetLogPath(logPath string) 27 | SetEnabledConsole(enabled bool) 28 | SetEnabledLog(enabledLog bool) 29 | IsEnabledLog() bool 30 | Print(log string, logTarget string) 31 | Raw(log string, logTarget string) 32 | Debug(log string, logTarget string) 33 | Info(log string, logTarget string) 34 | Warn(log string, logTarget string) 35 | Error(log string, logTarget string) 36 | } 37 | 38 | var ( 39 | DefaultLogPath string 40 | DefaultEnabledLog bool = false 41 | DefaultEnabledConsole bool = false 42 | ) 43 | 44 | func NewAppLog() AppLog { 45 | if DefaultLogPath == "" { 46 | DefaultLogPath = file.GetCurrentDirectory() + "/logs" 47 | } 48 | appLog := NewXLog() 49 | appLog.SetLogPath(DefaultLogPath) // set default log path 50 | appLog.SetEnabledLog(DefaultEnabledLog) // set default enabled log 51 | appLog.SetEnabledConsole(DefaultEnabledConsole) // set default enabled console output 52 | return appLog 53 | } 54 | 55 | // Log content 56 | // fileName source file name 57 | // line line number in source file 58 | // fullPath full path of source file 59 | // funcName function name of caller 60 | type logContext struct { 61 | fileName string 62 | line int 63 | fullPath string 64 | funcName string 65 | } 66 | 67 | // priting 68 | // skip=0 runtime.Caller 69 | // skip=1 runtime/proc.c: runtime.main 70 | // skip=2 runtime/proc.c: runtime.goexit 71 | // 72 | // Process startup procedure of a go program: 73 | // 1.runtime.goexit is the actual entry point(NOT main.main) 74 | // 2.then runtime.goexit calls runtime.main 75 | // 3.finally runtime.main calls user defined main.main 76 | func callerInfo(skip int) (ctx *logContext, err error) { 77 | pc, file, line, ok := runtime.Caller(skip) 78 | if !ok { 79 | return nil, errors.New("error during runtime.Callers") 80 | } 81 | 82 | funcInfo := runtime.FuncForPC(pc) 83 | if funcInfo == nil { 84 | return nil, errors.New("error during runtime.FuncForPC") 85 | } 86 | 87 | funcName := funcInfo.Name() 88 | if strings.HasPrefix(funcName, ".") { 89 | funcName = funcName[strings.Index(funcName, "."):] 90 | } 91 | 92 | ctx = &logContext{ 93 | funcName: filepath.Base(funcName), 94 | line: line, 95 | fullPath: file, 96 | fileName: filepath.Base(file), 97 | } 98 | 99 | return ctx, nil 100 | 101 | } 102 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | -------------------------------------------------------------------------------- /logger/xlog.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/devfeel/dotweb/framework/file" 12 | ) 13 | 14 | type chanLog struct { 15 | Content string 16 | LogTarget string 17 | LogLevel string 18 | isRaw bool 19 | logCtx *logContext 20 | } 21 | 22 | type xLog struct { 23 | logRootPath string 24 | logChan_Custom chan chanLog 25 | enabledLog bool 26 | enabledConsole bool 27 | } 28 | 29 | // NewXLog create new xLog 30 | func NewXLog() *xLog { 31 | l := &xLog{logChan_Custom: make(chan chanLog, 10000)} 32 | go l.handleCustom() 33 | return l 34 | } 35 | 36 | const ( 37 | defaultDateFormatForFileName = "2006_01_02" 38 | defaultDateLayout = "2006-01-02" 39 | defaultFullTimeLayout = "2006-01-02 15:04:05.9999" 40 | defaultTimeLayout = "2006-01-02 15:04:05" 41 | ) 42 | 43 | // Print debug log with no format 44 | func (l *xLog) Print(log string, logTarget string) { 45 | l.log(log, logTarget, LogLevelDebug, true) 46 | } 47 | 48 | func (l *xLog) Raw(log string, logTarget string) { 49 | l.log(log, logTarget, LogLevelRaw, true) 50 | } 51 | 52 | // Debug debug log with default format 53 | func (l *xLog) Debug(log string, logTarget string) { 54 | l.log(log, logTarget, LogLevelDebug, false) 55 | } 56 | 57 | // Info info log with default format 58 | func (l *xLog) Info(log string, logTarget string) { 59 | l.log(log, logTarget, LogLevelInfo, false) 60 | } 61 | 62 | // Warn warn log with default format 63 | func (l *xLog) Warn(log string, logTarget string) { 64 | l.log(log, logTarget, LogLevelWarn, false) 65 | } 66 | 67 | // Error error log with default format 68 | func (l *xLog) Error(log string, logTarget string) { 69 | l.log(log, logTarget, LogLevelError, false) 70 | } 71 | 72 | // log push log into chan 73 | func (l *xLog) log(log string, logTarget string, logLevel string, isRaw bool) { 74 | if l.enabledLog { 75 | skip := 3 76 | logCtx, err := callerInfo(skip) 77 | if err != nil { 78 | fmt.Println("log println err! " + time.Now().Format("2006-01-02 15:04:05") + " Error: " + err.Error()) 79 | logCtx = &logContext{} 80 | } 81 | if logLevel != LogLevelRaw { 82 | logTarget = logTarget + "_" + logLevel 83 | } 84 | chanLog := chanLog{ 85 | LogTarget: logTarget, 86 | Content: log, 87 | LogLevel: logLevel, 88 | isRaw: isRaw, 89 | logCtx: logCtx, 90 | } 91 | l.logChan_Custom <- chanLog 92 | } 93 | } 94 | 95 | // SetLogPath set log path 96 | func (l *xLog) SetLogPath(rootPath string) { 97 | // set root path of the log file 98 | l.logRootPath = rootPath 99 | if !strings.HasSuffix(l.logRootPath, "/") { 100 | l.logRootPath = l.logRootPath + "/" 101 | } 102 | } 103 | 104 | // SetEnabledLog set enabled log 105 | func (l *xLog) SetEnabledLog(enabledLog bool) { 106 | l.enabledLog = enabledLog 107 | } 108 | 109 | // IsEnabledLog return enabled log flag 110 | func (l *xLog) IsEnabledLog() bool { 111 | return l.enabledLog 112 | } 113 | 114 | // SetEnabledConsole set enabled Console output 115 | func (l *xLog) SetEnabledConsole(enabled bool) { 116 | l.enabledConsole = enabled 117 | } 118 | 119 | // custom handling of the log 120 | func (l *xLog) handleCustom() { 121 | for { 122 | log := <-l.logChan_Custom 123 | l.writeLog(log, "custom") 124 | } 125 | } 126 | 127 | func (l *xLog) writeLog(chanLog chanLog, level string) { 128 | filePath := l.logRootPath + chanLog.LogTarget 129 | switch level { 130 | case "custom": 131 | filePath = filePath + "_" + time.Now().Format(defaultDateFormatForFileName) + ".log" 132 | break 133 | } 134 | log := chanLog.Content 135 | if !chanLog.isRaw { 136 | log = fmt.Sprintf("%s [%s] [%s:%v] %s", time.Now().Format(defaultFullTimeLayout), chanLog.LogLevel, chanLog.logCtx.fileName, chanLog.logCtx.line, chanLog.Content) 137 | } 138 | if l.enabledConsole { 139 | fmt.Println(log) 140 | } 141 | writeFile(filePath, log) 142 | } 143 | 144 | func writeFile(logFile string, log string) { 145 | pathDir := filepath.Dir(logFile) 146 | if !file.Exist(pathDir) { 147 | // create path 148 | err := os.MkdirAll(pathDir, 0777) 149 | if err != nil { 150 | fmt.Println("xlog.writeFile create path error ", err) 151 | return 152 | } 153 | } 154 | 155 | var mode os.FileMode 156 | flag := syscall.O_RDWR | syscall.O_APPEND | syscall.O_CREAT 157 | mode = 0666 158 | logstr := log + "\r\n" 159 | file, err := os.OpenFile(logFile, flag, mode) 160 | defer file.Close() 161 | if err != nil { 162 | fmt.Println(logFile, err) 163 | return 164 | } 165 | file.WriteString(logstr) 166 | } 167 | -------------------------------------------------------------------------------- /logger/xlog_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfeel/dotweb/d50e13e812dfddda82c6f976ef1cc9007ea06e7c/logo.png -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/devfeel/dotweb/framework/convert" 7 | ) 8 | 9 | const ( 10 | middleware_App = "app" 11 | middleware_Group = "group" 12 | middleware_Router = "router" 13 | ) 14 | 15 | type ( 16 | // HttpModule global module in http server 17 | // it will be no effect when websocket request or use offline mode 18 | HttpModule struct { 19 | Name string 20 | // OnBeginRequest is the first event in the execution chain 21 | OnBeginRequest func(Context) 22 | // OnEndRequest is the last event in the execution chain 23 | OnEndRequest func(Context) 24 | } 25 | 26 | MiddlewareFunc func() Middleware 27 | 28 | // middleware execution priority: 29 | // app > group > router 30 | // Middleware middleware interface 31 | Middleware interface { 32 | Handle(ctx Context) error 33 | SetNext(m Middleware) 34 | Next(ctx Context) error 35 | Exclude(routers ...string) 36 | HasExclude() bool 37 | ExistsExcludeRouter(router string) bool 38 | } 39 | 40 | // BaseMiddlware is a shortcut for BaseMiddleware 41 | // Deprecated: 由于该struct命名有误,将在2.0版本弃用,请大家尽快修改自己的middleware 42 | BaseMiddlware struct { 43 | BaseMiddleware 44 | } 45 | 46 | // BaseMiddleware is the base struct, user defined middleware should extend this 47 | BaseMiddleware struct { 48 | next Middleware 49 | excludeRouters map[string]struct{} 50 | } 51 | 52 | xMiddleware struct { 53 | BaseMiddleware 54 | IsEnd bool 55 | } 56 | ) 57 | 58 | func (bm *BaseMiddleware) SetNext(m Middleware) { 59 | bm.next = m 60 | } 61 | 62 | func (bm *BaseMiddleware) Next(ctx Context) error { 63 | if ctx.getMiddlewareStep() == "" { 64 | ctx.setMiddlewareStep(middleware_App) 65 | } 66 | if bm.next == nil { 67 | if ctx.getMiddlewareStep() == middleware_App { 68 | ctx.setMiddlewareStep(middleware_Group) 69 | if len(ctx.RouterNode().GroupMiddlewares()) > 0 { 70 | return ctx.RouterNode().GroupMiddlewares()[0].Handle(ctx) 71 | } 72 | } 73 | if ctx.getMiddlewareStep() == middleware_Group { 74 | ctx.setMiddlewareStep(middleware_Router) 75 | if len(ctx.RouterNode().Middlewares()) > 0 { 76 | return ctx.RouterNode().Middlewares()[0].Handle(ctx) 77 | } 78 | } 79 | 80 | if ctx.getMiddlewareStep() == middleware_Router { 81 | return ctx.Handler()(ctx) 82 | } 83 | } else { 84 | // check exclude config 85 | if ctx.RouterNode().Node().hasExcludeMiddleware && bm.next.HasExclude() { 86 | if bm.next.ExistsExcludeRouter(ctx.RouterNode().Node().fullPath) { 87 | return bm.next.Next(ctx) 88 | } 89 | } 90 | return bm.next.Handle(ctx) 91 | } 92 | return nil 93 | } 94 | 95 | // Exclude Exclude this middleware with router 96 | func (bm *BaseMiddleware) Exclude(routers ...string) { 97 | if bm.excludeRouters == nil { 98 | bm.excludeRouters = make(map[string]struct{}) 99 | } 100 | for _, v := range routers { 101 | bm.excludeRouters[v] = struct{}{} 102 | } 103 | } 104 | 105 | // HasExclude check has set exclude router 106 | func (bm *BaseMiddleware) HasExclude() bool { 107 | if bm.excludeRouters == nil { 108 | return false 109 | } 110 | if len(bm.excludeRouters) > 0 { 111 | return true 112 | } else { 113 | return false 114 | } 115 | } 116 | 117 | // ExistsExcludeRouter check is exists router in exclude map 118 | func (bm *BaseMiddleware) ExistsExcludeRouter(router string) bool { 119 | if bm.excludeRouters == nil { 120 | return false 121 | } 122 | _, exists := bm.excludeRouters[router] 123 | return exists 124 | } 125 | 126 | func getIgnoreFaviconModule() *HttpModule { 127 | return &HttpModule{ 128 | Name: "IgnoreFavicon", 129 | OnBeginRequest: func(ctx Context) { 130 | if ctx.Request().Path() == "/favicon.ico" { 131 | ctx.End() 132 | } 133 | }, 134 | } 135 | } 136 | 137 | func (x *xMiddleware) Handle(ctx Context) error { 138 | if ctx.getMiddlewareStep() == "" { 139 | ctx.setMiddlewareStep(middleware_App) 140 | } 141 | if x.IsEnd { 142 | return ctx.Handler()(ctx) 143 | } 144 | return x.Next(ctx) 145 | } 146 | 147 | type RequestLogMiddleware struct { 148 | BaseMiddleware 149 | } 150 | 151 | func (m *RequestLogMiddleware) Handle(ctx Context) error { 152 | var timeDuration time.Duration 153 | var timeTaken uint64 154 | err := m.Next(ctx) 155 | if ctx.Items().Exists(ItemKeyHandleDuration) { 156 | var errParse error 157 | timeDuration, errParse = time.ParseDuration(ctx.Items().GetString(ItemKeyHandleDuration)) 158 | if errParse != nil { 159 | timeTaken = 0 160 | } else { 161 | timeTaken = uint64(timeDuration / time.Millisecond) 162 | } 163 | } else { 164 | var begin time.Time 165 | beginVal, exists := ctx.Items().Get(ItemKeyHandleStartTime) 166 | if !exists { 167 | begin = time.Now() 168 | } else { 169 | begin = beginVal.(time.Time) 170 | } 171 | timeTaken = uint64(time.Now().Sub(begin) / time.Millisecond) 172 | } 173 | log := ctx.Request().Url() + " " + logContext(ctx, timeTaken) 174 | ctx.HttpServer().Logger().Debug(log, LogTarget_HttpRequest) 175 | return err 176 | } 177 | 178 | // get default log string 179 | func logContext(ctx Context, timetaken uint64) string { 180 | var reqbytelen, resbytelen, method, proto, status, userip string 181 | if ctx != nil { 182 | reqbytelen = convert.Int642String(ctx.Request().ContentLength) 183 | resbytelen = convert.Int642String(ctx.Response().Size) 184 | method = ctx.Request().Method 185 | proto = ctx.Request().Proto 186 | status = convert.Int2String(ctx.Response().Status) 187 | userip = ctx.RemoteIP() 188 | } 189 | 190 | log := method + " " 191 | log += userip + " " 192 | log += proto + " " 193 | log += status + " " 194 | log += reqbytelen + " " 195 | log += resbytelen + " " 196 | log += convert.UInt642String(timetaken) 197 | 198 | return log 199 | } 200 | 201 | type TimeoutHookMiddleware struct { 202 | BaseMiddleware 203 | HookHandle StandardHandle 204 | TimeoutDuration time.Duration 205 | } 206 | 207 | func (m *TimeoutHookMiddleware) Handle(ctx Context) error { 208 | var begin time.Time 209 | if m.HookHandle != nil { 210 | beginVal, exists := ctx.Items().Get(ItemKeyHandleStartTime) 211 | if !exists { 212 | begin = time.Now() 213 | } else { 214 | begin = beginVal.(time.Time) 215 | } 216 | } 217 | // Do next 218 | err := m.Next(ctx) 219 | if m.HookHandle != nil { 220 | realDuration := time.Now().Sub(begin) 221 | ctx.Items().Set(ItemKeyHandleDuration, realDuration) 222 | if realDuration > m.TimeoutDuration { 223 | m.HookHandle(ctx) 224 | } 225 | } 226 | return err 227 | } 228 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | -------------------------------------------------------------------------------- /mock.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | const ( 4 | requestHeaderUseMockKey = "dotweb_req_mock" 5 | requestHeaderUseMockFlag = "true" 6 | ) 7 | 8 | // MockHandle the handle define on mock module 9 | type MockHandle func(ctx Context) 10 | 11 | // Mock the define Mock module 12 | type Mock interface { 13 | // Register register MockHandle on route 14 | Register(route string, handler MockHandle) 15 | // RegisterString register return mock string on route 16 | RegisterString(route string, resData interface{}) 17 | // RegisterJSON register return mock json on route 18 | RegisterJSON(route string, resData interface{}) 19 | // CheckNeedMock check is need do mock logic 20 | CheckNeedMock(Context) bool 21 | // Do do mock logic 22 | Do(Context) 23 | } 24 | 25 | // StandardMock standard mock implement for Mock interface 26 | type StandardMock struct { 27 | routeMap map[string]MockHandle 28 | } 29 | 30 | // NewStandardMock create new StandardMock 31 | func NewStandardMock() *StandardMock { 32 | return &StandardMock{routeMap: make(map[string]MockHandle)} 33 | } 34 | 35 | // CheckNeedMock check is need do mock logic 36 | func (m *StandardMock) CheckNeedMock(ctx Context) bool { 37 | if ctx.Request().QueryHeader(requestHeaderUseMockKey) == requestHeaderUseMockFlag { 38 | return true 39 | } 40 | return false 41 | } 42 | 43 | // Do do mock logic 44 | func (m *StandardMock) Do(ctx Context) { 45 | handler, exists := m.routeMap[ctx.RouterNode().Node().fullPath] 46 | if exists { 47 | handler(ctx) 48 | } 49 | } 50 | 51 | // Register register MockHandle on route 52 | func (m *StandardMock) Register(route string, handler MockHandle) { 53 | m.routeMap[route] = handler 54 | } 55 | 56 | // RegisterString register return mock string on route 57 | func (m *StandardMock) RegisterString(route string, resData interface{}) { 58 | m.routeMap[route] = func(ctx Context) { 59 | ctx.Response().SetHeader(requestHeaderUseMockKey, requestHeaderUseMockFlag) 60 | ctx.WriteString(resData) 61 | ctx.End() 62 | } 63 | } 64 | 65 | // RegisterJSON register return mock json on route 66 | func (m *StandardMock) RegisterJSON(route string, resData interface{}) { 67 | m.routeMap[route] = func(ctx Context) { 68 | ctx.Response().SetHeader(requestHeaderUseMockKey, requestHeaderUseMockFlag) 69 | ctx.WriteJson(resData) 70 | ctx.End() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "fmt" 5 | "github.com/devfeel/dotweb/config" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | ) 10 | 11 | // Plugin a interface for app's global plugin 12 | type Plugin interface { 13 | Name() string 14 | Run() error 15 | IsValidate() bool 16 | } 17 | 18 | // NewDefaultNotifyPlugin return new NotifyPlugin with default config 19 | func NewDefaultNotifyPlugin(app *DotWeb) *NotifyPlugin { 20 | p := new(NotifyPlugin) 21 | p.app = app 22 | p.LoopTime = notifyPlugin_LoopTime 23 | p.Root = app.Config.ConfigFilePath 24 | p.suffix = make(map[string]bool) 25 | p.ModTimes = make(map[string]time.Time) 26 | return p 27 | } 28 | 29 | // NewNotifyPlugin return new NotifyPlugin with fileRoot & loopTime & suffix 30 | // if suffix is nil or suffix[0] == "*", will visit all files in fileRoot 31 | /*func NewNotifyPlugin(app *DotWeb, fileRoot string, loopTime int, suffix []string) *NotifyPlugin{ 32 | p := new(NotifyPlugin) 33 | p.app = app 34 | p.LoopTime = loopTime 35 | p.Root = fileRoot 36 | Suffix := make(map[string]bool) 37 | if len(suffix) > 0 && suffix[0] != "*" { 38 | for _, v := range suffix { 39 | Suffix[v] = true 40 | } 41 | } 42 | p.suffix = Suffix 43 | p.ModTimes = make(map[string]time.Time) 44 | return p 45 | }*/ 46 | 47 | const notifyPlugin_LoopTime = 500 //ms 48 | 49 | type NotifyPlugin struct { 50 | app *DotWeb 51 | Root string 52 | suffix map[string]bool 53 | LoopTime int 54 | ModTimes map[string]time.Time 55 | } 56 | 57 | func (p *NotifyPlugin) Name() string { 58 | return "NotifyPlugin" 59 | } 60 | 61 | func (p *NotifyPlugin) IsValidate() bool { 62 | return true 63 | } 64 | 65 | func (p *NotifyPlugin) Run() error { 66 | return p.start() 67 | } 68 | 69 | func (p *NotifyPlugin) visit(path string, fileinfo os.FileInfo, err error) error { 70 | if err != nil { 71 | return fmt.Errorf("访问文件失败%s", err) 72 | } 73 | ext := filepath.Ext(path) 74 | if !fileinfo.IsDir() && (p.suffix[ext] || len(p.suffix) == 0) { 75 | modTime := fileinfo.ModTime() 76 | if oldModTime, ok := p.ModTimes[path]; !ok { 77 | p.ModTimes[path] = modTime 78 | } else { 79 | if oldModTime.Before(modTime) { 80 | p.app.Logger().Info("NotifyPlugin Reload "+path, LogTarget_HttpServer) 81 | appConfig, err := config.InitConfig(p.app.Config.ConfigFilePath, p.app.Config.ConfigType) 82 | if err != nil { 83 | p.app.Logger().Error("NotifyPlugin Reload "+path+" error => "+fmt.Sprint(err), LogTarget_HttpServer) 84 | } 85 | p.app.ReSetConfig(appConfig) 86 | p.ModTimes[path] = modTime 87 | } 88 | } 89 | } 90 | return nil 91 | } 92 | 93 | func (p *NotifyPlugin) start() error { 94 | for { 95 | filepath.Walk(p.Root, p.visit) 96 | time.Sleep(time.Duration(p.LoopTime) * time.Millisecond) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /plugin_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "fmt" 5 | "github.com/devfeel/dotweb/test" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type testPlugin struct { 11 | } 12 | 13 | func (p *testPlugin) Name() string { 14 | return "test" 15 | } 16 | func (p *testPlugin) Run() error { 17 | fmt.Println(p.Name(), "runing") 18 | //panic("error test run") 19 | return nil 20 | } 21 | func (p *testPlugin) IsValidate() bool { 22 | return true 23 | } 24 | 25 | func TestNotifyPlugin_Name(t *testing.T) { 26 | app := newConfigDotWeb() 27 | //fmt.Println(app.Config.ConfigFilePath) 28 | p := NewDefaultNotifyPlugin(app) 29 | needShow := "NotifyPlugin" 30 | test.Equal(t, needShow, p.Name()) 31 | } 32 | 33 | func TestNotifyPlugin_IsValidate(t *testing.T) { 34 | app := newConfigDotWeb() 35 | p := NewDefaultNotifyPlugin(app) 36 | needShow := true 37 | test.Equal(t, needShow, p.IsValidate()) 38 | } 39 | 40 | func TestNotifyPlugin_Run(t *testing.T) { 41 | app := newConfigDotWeb() 42 | p := NewDefaultNotifyPlugin(app) 43 | go func() { 44 | for { 45 | fmt.Println(p.ModTimes[app.Config.ConfigFilePath]) 46 | time.Sleep(time.Duration(600 * time.Millisecond)) 47 | } 48 | }() 49 | p.Run() 50 | } 51 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "errors" 5 | "html/template" 6 | "io" 7 | "path" 8 | "path/filepath" 9 | "sync" 10 | 11 | "github.com/devfeel/dotweb/framework/file" 12 | ) 13 | 14 | // Renderer is the interface that wraps the render method. 15 | type Renderer interface { 16 | SetTemplatePath(path string) 17 | Render(io.Writer, interface{}, Context, ...string) error 18 | RegisterTemplateFunc(string, interface{}) 19 | } 20 | 21 | type innerRenderer struct { 22 | templatePath string 23 | // Template cache (for FromCache()) 24 | enabledCache bool 25 | templateCache map[string]*template.Template 26 | templateCacheMutex sync.RWMutex 27 | 28 | // used to manager template func 29 | templateFuncs map[string]interface{} 30 | templateFuncsMutex *sync.RWMutex 31 | } 32 | 33 | // Render render view use http/template 34 | func (r *innerRenderer) Render(w io.Writer, data interface{}, ctx Context, tpl ...string) error { 35 | if len(tpl) <= 0 { 36 | return errors.New("no enough render template files") 37 | } 38 | t, err := r.parseFiles(tpl...) 39 | if err != nil { 40 | return err 41 | } 42 | return t.Execute(w, data) 43 | } 44 | 45 | // SetTemplatePath set default template path 46 | func (r *innerRenderer) SetTemplatePath(path string) { 47 | r.templatePath = path 48 | } 49 | 50 | // RegisterTemplateFunc used to register template func in renderer 51 | func (r *innerRenderer) RegisterTemplateFunc(funcName string, funcHandler interface{}) { 52 | r.templateFuncsMutex.Lock() 53 | r.templateFuncs[funcName] = funcHandler 54 | r.templateFuncsMutex.Unlock() 55 | } 56 | 57 | // unescaped inner template func used to encapsulates a known safe HTML document fragment 58 | func unescaped(x string) interface{} { return template.HTML(x) } 59 | 60 | // return http/template by gived file name 61 | func (r *innerRenderer) parseFiles(fileNames ...string) (*template.Template, error) { 62 | var realFileNames []string 63 | var filesCacheKey string 64 | var err error 65 | for _, v := range fileNames { 66 | if !file.Exist(v) { 67 | v = path.Join(r.templatePath, v) 68 | } 69 | realFileNames = append(realFileNames, v) 70 | filesCacheKey = filesCacheKey + v 71 | } 72 | 73 | var t *template.Template 74 | var exists bool 75 | if r.enabledCache { 76 | // check from chach 77 | t, exists = r.parseFilesFromCache(filesCacheKey) 78 | } 79 | if !exists { 80 | name := filepath.Base(fileNames[0]) 81 | t = template.New(name) 82 | if len(r.templateFuncs) > 0 { 83 | t = t.Funcs(r.templateFuncs) 84 | } 85 | t, err = t.ParseFiles(realFileNames...) 86 | if err != nil { 87 | return nil, err 88 | } 89 | r.templateCacheMutex.Lock() 90 | defer r.templateCacheMutex.Unlock() 91 | r.templateCache[filesCacheKey] = t 92 | } 93 | 94 | return t, nil 95 | } 96 | 97 | func (r *innerRenderer) parseFilesFromCache(filesCacheKey string) (*template.Template, bool) { 98 | r.templateCacheMutex.RLock() 99 | defer r.templateCacheMutex.RUnlock() 100 | t, exists := r.templateCache[filesCacheKey] 101 | return t, exists 102 | } 103 | 104 | // registeInnerTemplateFunc registe default support funcs 105 | func registeInnerTemplateFunc(funcMap map[string]interface{}) { 106 | funcMap["unescaped"] = unescaped 107 | } 108 | 109 | // NewInnerRenderer create a inner renderer instance 110 | func NewInnerRenderer() Renderer { 111 | r := new(innerRenderer) 112 | r.enabledCache = true 113 | r.templateCache = make(map[string]*template.Template) 114 | r.templateFuncs = make(map[string]interface{}) 115 | r.templateFuncsMutex = new(sync.RWMutex) 116 | registeInnerTemplateFunc(r.templateFuncs) 117 | return r 118 | } 119 | 120 | // NewInnerRendererNoCache create a inner renderer instance with no cache mode 121 | func NewInnerRendererNoCache() Renderer { 122 | r := NewInnerRenderer().(*innerRenderer) 123 | r.enabledCache = false 124 | return r 125 | } 126 | -------------------------------------------------------------------------------- /render_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "io/ioutil" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | var maxBodySize int64 = 32 << 20 // 32 MB 12 | 13 | type Request struct { 14 | *http.Request 15 | httpCtx Context 16 | postBody []byte 17 | realUrl string 18 | isReadBody bool 19 | requestID string 20 | } 21 | 22 | // reset response attr 23 | func (req *Request) reset(r *http.Request, ctx Context) { 24 | req.httpCtx = ctx 25 | req.Request = r 26 | req.isReadBody = false 27 | if ctx.HttpServer().ServerConfig().EnabledRequestID { 28 | req.requestID = ctx.HttpServer().DotApp.IDGenerater() 29 | ctx.Response().SetHeader(HeaderRequestID, req.requestID) 30 | } else { 31 | req.requestID = "" 32 | } 33 | } 34 | 35 | func (req *Request) release() { 36 | req.Request = nil 37 | req.isReadBody = false 38 | req.postBody = nil 39 | req.requestID = "" 40 | req.realUrl = "" 41 | } 42 | 43 | func (req *Request) httpServer() *HttpServer { 44 | return req.httpCtx.HttpServer() 45 | } 46 | 47 | func (req *Request) httpApp() *DotWeb { 48 | return req.httpCtx.HttpServer().DotApp 49 | } 50 | 51 | // RequestID get unique ID with current request 52 | // must HttpServer.SetEnabledRequestID(true) 53 | // default is empty string 54 | func (req *Request) RequestID() string { 55 | return req.requestID 56 | } 57 | 58 | // QueryStrings parses RawQuery and returns the corresponding values. 59 | func (req *Request) QueryStrings() url.Values { 60 | return req.URL.Query() 61 | } 62 | 63 | // RawQuery returns the original query string 64 | func (req *Request) RawQuery() string { 65 | return req.URL.RawQuery 66 | } 67 | 68 | // QueryString returns the first value associated with the given key. 69 | func (req *Request) QueryString(key string) string { 70 | return req.URL.Query().Get(key) 71 | } 72 | 73 | // ExistsQueryKey check is exists from query params with the given key. 74 | func (req *Request) ExistsQueryKey(key string) bool { 75 | _, isExists := req.URL.Query()[key] 76 | return isExists 77 | } 78 | 79 | // FormFile get file by form key 80 | func (req *Request) FormFile(key string) (*UploadFile, error) { 81 | file, header, err := req.Request.FormFile(key) 82 | if err != nil { 83 | return nil, err 84 | } else { 85 | return NewUploadFile(file, header), nil 86 | } 87 | } 88 | 89 | // FormFiles get multi files 90 | // fixed #92 91 | func (req *Request) FormFiles() (map[string]*UploadFile, error) { 92 | files := make(map[string]*UploadFile) 93 | req.parseForm() 94 | if req.Request.MultipartForm == nil || req.Request.MultipartForm.File == nil { 95 | return nil, http.ErrMissingFile 96 | } 97 | for key, fileMap := range req.Request.MultipartForm.File { 98 | if len(fileMap) > 0 { 99 | file, err := fileMap[0].Open() 100 | if err == nil { 101 | files[key] = NewUploadFile(file, fileMap[0]) 102 | } 103 | } 104 | } 105 | return files, nil 106 | } 107 | 108 | // FormValues including both the URL field's query parameters and the POST or PUT form data 109 | func (req *Request) FormValues() map[string][]string { 110 | req.parseForm() 111 | return map[string][]string(req.Form) 112 | } 113 | 114 | // PostValues contains the parsed form data from POST, PATCH, or PUT body parameters 115 | func (req *Request) PostValues() map[string][]string { 116 | req.parseForm() 117 | return map[string][]string(req.PostForm) 118 | } 119 | 120 | func (req *Request) parseForm() error { 121 | if strings.HasPrefix(req.QueryHeader(HeaderContentType), MIMEMultipartForm) { 122 | if err := req.ParseMultipartForm(defaultMemory); err != nil { 123 | return err 124 | } 125 | } else { 126 | if err := req.ParseForm(); err != nil { 127 | return err 128 | } 129 | } 130 | return nil 131 | } 132 | 133 | // ContentType get ContentType 134 | func (req *Request) ContentType() string { 135 | return req.Header.Get(HeaderContentType) 136 | } 137 | 138 | // QueryHeader query header value by key 139 | func (req *Request) QueryHeader(key string) string { 140 | return req.Header.Get(key) 141 | } 142 | 143 | // PostString returns the first value for the named component of the POST 144 | // or PUT request body. URL query parameters are ignored. 145 | // Deprecated: Use the PostFormValue instead 146 | func (req *Request) PostString(key string) string { 147 | return req.PostFormValue(key) 148 | } 149 | 150 | // PostBody returns data from the POST or PUT request body 151 | func (req *Request) PostBody() []byte { 152 | if !req.isReadBody { 153 | if req.httpCtx != nil { 154 | switch req.httpCtx.HttpServer().DotApp.Config.Server.MaxBodySize { 155 | case -1: 156 | break 157 | case 0: 158 | req.Body = http.MaxBytesReader(req.httpCtx.Response().Writer(), req.Body, maxBodySize) 159 | break 160 | default: 161 | req.Body = http.MaxBytesReader(req.httpCtx.Response().Writer(), req.Body, req.httpApp().Config.Server.MaxBodySize) 162 | break 163 | } 164 | } 165 | bts, err := ioutil.ReadAll(req.Body) 166 | if err != nil { 167 | //if err, panic it 168 | panic(err) 169 | } else { 170 | req.isReadBody = true 171 | req.postBody = bts 172 | } 173 | } 174 | return req.postBody 175 | } 176 | 177 | // RemoteIP RemoteAddr to an "IP" address 178 | func (req *Request) RemoteIP() string { 179 | host, _, _ := net.SplitHostPort(req.RemoteAddr) 180 | return host 181 | } 182 | 183 | // RealIP returns the first ip from 'X-Forwarded-For' or 'X-Real-IP' header key 184 | // if not exists data, returns request.RemoteAddr 185 | // fixed for #164 186 | func (req *Request) RealIP() string { 187 | if ip := req.Header.Get(HeaderXForwardedFor); ip != "" { 188 | return strings.Split(ip, ", ")[0] 189 | } 190 | if ip := req.Header.Get(HeaderXRealIP); ip != "" { 191 | return ip 192 | } 193 | host, _, _ := net.SplitHostPort(req.RemoteAddr) 194 | return host 195 | } 196 | 197 | // FullRemoteIP RemoteAddr to an "IP:port" address 198 | func (req *Request) FullRemoteIP() string { 199 | fullIp := req.Request.RemoteAddr 200 | return fullIp 201 | } 202 | 203 | // Path returns requested path. 204 | // 205 | // The path is valid until returning from RequestHandler. 206 | func (req *Request) Path() string { 207 | return req.URL.Path 208 | } 209 | 210 | // IsAJAX returns if it is a ajax request 211 | func (req *Request) IsAJAX() bool { 212 | return strings.Contains(req.Header.Get(HeaderXRequestedWith), "XMLHttpRequest") 213 | } 214 | 215 | // Url get request url 216 | func (req *Request) Url() string { 217 | if req.realUrl != "" { 218 | return req.realUrl 219 | } else { 220 | return req.URL.String() 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "bufio" 5 | "compress/gzip" 6 | "errors" 7 | "io" 8 | "net" 9 | "net/http" 10 | ) 11 | 12 | type ( 13 | Response struct { 14 | writer http.ResponseWriter 15 | Status int 16 | Size int64 17 | body []byte 18 | committed bool 19 | header http.Header 20 | isEnd bool 21 | } 22 | 23 | gzipResponseWriter struct { 24 | io.Writer 25 | http.ResponseWriter 26 | } 27 | ) 28 | 29 | func NewResponse(w http.ResponseWriter) (r *Response) { 30 | return &Response{writer: w, 31 | header: w.Header()} 32 | } 33 | 34 | func (r *Response) Header() http.Header { 35 | return r.header 36 | } 37 | 38 | func (r *Response) QueryHeader(key string) string { 39 | return r.Header().Get(key) 40 | } 41 | 42 | func (r *Response) Redirect(code int, targetUrl string) error { 43 | r.Header().Set(HeaderCacheControl, "no-cache") 44 | r.Header().Set(HeaderLocation, targetUrl) 45 | return r.WriteHeader(code) 46 | } 47 | 48 | func (r *Response) Writer() http.ResponseWriter { 49 | return r.writer 50 | } 51 | 52 | func (r *Response) SetWriter(w http.ResponseWriter) *Response { 53 | r.writer = w 54 | return r 55 | } 56 | 57 | // HttpCode return http code format int 58 | func (r *Response) HttpCode() int { 59 | return r.Status 60 | } 61 | 62 | func (r *Response) Body() []byte { 63 | return r.body 64 | } 65 | 66 | func (r *Response) BodyString() string { 67 | return string(r.body) 68 | } 69 | 70 | func (r *Response) SetHeader(key, val string) { 71 | r.Header().Set(key, val) 72 | } 73 | 74 | func (r *Response) SetContentType(contenttype string) { 75 | r.SetHeader(HeaderContentType, contenttype) 76 | } 77 | 78 | func (r *Response) SetStatusCode(code int) error { 79 | return r.WriteHeader(code) 80 | } 81 | 82 | // WriteHeader sends an HTTP response header with status code. If WriteHeader is 83 | // not called explicitly, the first call to Write will trigger an implicit 84 | // WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly 85 | // used to send error codes. 86 | func (r *Response) WriteHeader(code int) error { 87 | if r.committed { 88 | return errors.New("response already set status") 89 | } 90 | r.Status = code 91 | r.writer.WriteHeader(code) 92 | r.committed = true 93 | return nil 94 | } 95 | 96 | // Write writes the data to the connection as part of an HTTP reply. 97 | func (r *Response) Write(code int, b []byte) (n int, err error) { 98 | if !r.committed { 99 | r.WriteHeader(code) 100 | } 101 | n, err = r.writer.Write(b) 102 | r.Size += int64(n) 103 | r.body = append(r.body, b[0:]...) 104 | return 105 | } 106 | 107 | // End stop current response 108 | func (r *Response) End() { 109 | r.isEnd = true 110 | } 111 | 112 | // Flush implements the http.Flusher interface to allow an HTTP handler to flush 113 | // buffered data to the client. 114 | // See [http.Flusher](https://golang.org/pkg/net/http/#Flusher) 115 | func (r *Response) Flush() { 116 | r.writer.(http.Flusher).Flush() 117 | } 118 | 119 | // Hijack implements the http.Hijacker interface to allow an HTTP handler to 120 | // take over the connection. 121 | // See https://golang.org/pkg/net/http/#Hijacker 122 | func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { 123 | return r.writer.(http.Hijacker).Hijack() 124 | } 125 | 126 | // reset response attr 127 | func (r *Response) reset(w http.ResponseWriter) { 128 | r.writer = w 129 | r.header = w.Header() 130 | r.Status = http.StatusOK 131 | r.Size = 0 132 | r.body = nil 133 | r.committed = false 134 | } 135 | 136 | // reset response attr 137 | func (r *Response) release() { 138 | r.writer = nil 139 | r.header = nil 140 | r.Status = http.StatusOK 141 | r.Size = 0 142 | r.body = nil 143 | r.committed = false 144 | } 145 | 146 | // WriteHeader sends an HTTP response header with the provided 147 | // status code. 148 | func (w *gzipResponseWriter) WriteHeader(code int) { 149 | if code == http.StatusNoContent { // Issue #489 150 | w.ResponseWriter.Header().Del(HeaderContentEncoding) 151 | } 152 | w.ResponseWriter.WriteHeader(code) 153 | } 154 | 155 | // Write do write data 156 | func (w *gzipResponseWriter) Write(b []byte) (int, error) { 157 | if w.Header().Get(HeaderContentType) == "" { 158 | w.Header().Set(HeaderContentType, http.DetectContentType(b)) 159 | } 160 | return w.Writer.Write(b) 161 | } 162 | 163 | // Flush do flush 164 | func (w *gzipResponseWriter) Flush() { 165 | w.Writer.(*gzip.Writer).Flush() 166 | } 167 | 168 | // Hijack do hijack 169 | func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 170 | return w.ResponseWriter.(http.Hijacker).Hijack() 171 | } 172 | 173 | 174 | // Push support http2 Push 175 | func (r *Response) Push(target string, opts *http.PushOptions) error { 176 | return r.writer.(http.Pusher).Push(target, opts) 177 | } -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/devfeel/dotweb/session" 8 | "github.com/devfeel/dotweb/test" 9 | ) 10 | 11 | func TestRouter_ServeHTTP(t *testing.T) { 12 | param := &InitContextParam{ 13 | t, 14 | "", 15 | "", 16 | test.ToDefault, 17 | } 18 | 19 | context := initAllContext(param) 20 | 21 | app := New() 22 | server := app.HttpServer 23 | r := NewRouter(server) 24 | 25 | r.ServeHTTP(context) 26 | } 27 | 28 | func TestWrapRouterHandle(t *testing.T) { 29 | param := &InitContextParam{ 30 | t, 31 | "", 32 | "", 33 | test.ToDefault, 34 | } 35 | 36 | context := initAllContext(param) 37 | 38 | app := New() 39 | server := app.HttpServer 40 | router := server.Router().(*router) 41 | // use default config 42 | server.SetSessionConfig(session.NewDefaultRuntimeConfig()) 43 | handle := router.wrapRouterHandle(Index, false) 44 | 45 | handle(context) 46 | } 47 | 48 | func TestLogWebsocketContext(t *testing.T) { 49 | param := &InitContextParam{ 50 | t, 51 | "", 52 | "", 53 | test.ToDefault, 54 | } 55 | 56 | context := initAllContext(param) 57 | 58 | log := logWebsocketContext(context, time.Now().Unix()) 59 | t.Log("logContext:", log) 60 | // test.NotNil(t,log) 61 | test.Equal(t, "", "") 62 | } 63 | 64 | func BenchmarkRouter_MatchPath(b *testing.B) { 65 | app := New() 66 | server := app.HttpServer 67 | r := NewRouter(server) 68 | r.GET("/1", func(ctx Context) error { 69 | ctx.WriteString("1") 70 | return nil 71 | }) 72 | r.GET("/2", func(ctx Context) error { 73 | ctx.WriteString("2") 74 | return nil 75 | }) 76 | r.POST("/p1", func(ctx Context) error { 77 | ctx.WriteString("1") 78 | return nil 79 | }) 80 | r.POST("/p2", func(ctx Context) error { 81 | ctx.WriteString("2") 82 | return nil 83 | }) 84 | 85 | for i := 0; i < b.N; i++ { 86 | if root := r.Nodes["GET"]; root != nil { 87 | root.getNode("/1?q=1") 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /scripts/ut.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | export COVERAGE_PATH=$(pwd) 4 | rm -rf "${COVERAGE_PATH}/scripts/coverage.out" 5 | for d in $(go list ./... | grep -v vendor); do 6 | cd "$GOPATH/src/$d" 7 | if [ $(ls | grep _test.go | wc -l) -gt 0 ]; then 8 | go test -cover -covermode atomic -coverprofile coverage.out 9 | if [ -f coverage.out ]; then 10 | sed '1d;$d' coverage.out >> "${COVERAGE_PATH}/scripts/coverage.out" 11 | rm -f coverage.out 12 | fi 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/devfeel/dotweb/session" 7 | "github.com/devfeel/dotweb/test" 8 | ) 9 | 10 | // check httpServer 11 | func TestNewHttpServer(t *testing.T) { 12 | server := NewHttpServer() 13 | 14 | test.NotNil(t, server.router) 15 | test.NotNil(t, server.stdServer) 16 | test.NotNil(t, server.ServerConfig) 17 | test.NotNil(t, server.SessionConfig) 18 | test.NotNil(t, server.lock_session) 19 | test.NotNil(t, server.binder) 20 | test.NotNil(t, server.pool) 21 | test.NotNil(t, server.pool.context) 22 | test.NotNil(t, server.pool.request) 23 | test.NotNil(t, server.pool.response) 24 | test.Equal(t, false, server.IsOffline()) 25 | 26 | // t.Log("is offline:",server.IsOffline()) 27 | } 28 | 29 | func TestSesionConfig(t *testing.T) { 30 | server := NewHttpServer() 31 | server.DotApp = New() 32 | // use default config 33 | server.SetSessionConfig(session.NewDefaultRuntimeConfig()) 34 | 35 | // init 36 | server.InitSessionManager() 37 | 38 | // get session 39 | sessionManager := server.GetSessionManager() 40 | 41 | // EnabledSession flag is false 42 | test.Nil(t, sessionManager) 43 | 44 | // switch EnabledSession flag 45 | server.SessionConfig().EnabledSession = true 46 | sessionManager = server.GetSessionManager() 47 | 48 | test.NotNil(t, sessionManager) 49 | test.Equal(t, server.sessionManager.StoreConfig().CookieName, session.DefaultSessionCookieName) 50 | test.Equal(t, server.sessionManager.GCLifetime, int64(session.DefaultSessionGCLifeTime)) 51 | } 52 | 53 | func Index(ctx Context) error { 54 | ctx.Response().Header().Set("Content-Type", "text/html; charset=utf-8") 55 | err := ctx.WriteStringC(201, "index => ", ctx.RemoteIP(), "我是首页") 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "github.com/devfeel/dotweb/logger" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/devfeel/dotweb/framework/crypto" 12 | ) 13 | 14 | const ( 15 | DefaultSessionGCLifeTime = 60 // second 16 | DefaultSessionMaxLifeTime = 20 * 60 // second 17 | DefaultSessionCookieName = "dotweb_sessionId" 18 | DefaultSessionLength = 20 19 | SessionMode_Runtime = "runtime" 20 | SessionMode_Redis = "redis" 21 | 22 | LogTarget_Session = "dotweb_session" 23 | ) 24 | 25 | type ( 26 | SessionStore interface { 27 | SessionRead(sessionId string) (*SessionState, error) 28 | SessionExist(sessionId string) bool 29 | SessionUpdate(state *SessionState) error 30 | SessionRemove(sessionId string) error 31 | SessionCount() int // get all active session length 32 | SessionGC() int // gc session and return out of date state num 33 | } 34 | 35 | // session config info 36 | StoreConfig struct { 37 | StoreName string 38 | Maxlifetime int64 // session life time, with second 39 | CookieName string // custom cookie name which sessionid store 40 | ServerIP string // if use redis, connection string, like "redis://:password@10.0.1.11:6379/0" 41 | BackupServerUrl string // if use redis, if ServerIP is down, use this server, like "redis://:password@10.0.1.11:6379/0" 42 | StoreKeyPre string // if use redis, set custom redis key-pre; default is dotweb:session: 43 | MaxIdle int // if use redis, set MaxIdle; default is 10 44 | MaxActive int // if use redis, set MaxActive; default is 50 45 | } 46 | 47 | SessionManager struct { 48 | GCLifetime int64 `json:"gclifetime"` 49 | 50 | appLog logger.AppLog 51 | store SessionStore 52 | storeConfig *StoreConfig 53 | } 54 | ) 55 | 56 | // GetSessionStore create new session store with store config 57 | func GetSessionStore(config *StoreConfig) SessionStore { 58 | switch config.StoreName { 59 | case SessionMode_Runtime: 60 | return NewRuntimeStore(config) 61 | case SessionMode_Redis: 62 | store, err := NewRedisStore(config) 63 | if err != nil { 64 | panic(fmt.Sprintf("redis session [%v] ping error -> %v", config.StoreName, err.Error())) 65 | } else { 66 | return store 67 | } 68 | default: 69 | panic("not support session store -> " + config.StoreName) 70 | } 71 | return nil 72 | } 73 | 74 | // NewDefaultRuntimeConfig create new store with default config and use runtime store 75 | func NewDefaultRuntimeConfig() *StoreConfig { 76 | return NewStoreConfig(SessionMode_Runtime, DefaultSessionMaxLifeTime, "", "", 0, 0) 77 | } 78 | 79 | // NewDefaultRedisConfig create new store with default config and use redis store 80 | func NewDefaultRedisConfig(serverIp string) *StoreConfig { 81 | return NewRedisConfig(serverIp, DefaultSessionMaxLifeTime, "", 0, 0) 82 | } 83 | 84 | // NewRedisConfig create new store with config and use redis store 85 | // must set serverIp and storeKeyPre 86 | func NewRedisConfig(serverIp string, maxlifetime int64, storeKeyPre string, maxIdle int, maxActive int) *StoreConfig { 87 | return NewStoreConfig(SessionMode_Redis, maxlifetime, serverIp, storeKeyPre, maxIdle, maxActive) 88 | } 89 | 90 | // NewStoreConfig create new store config 91 | func NewStoreConfig(storeName string, maxlifetime int64, serverIp string, storeKeyPre string, maxIdle int, maxActive int) *StoreConfig { 92 | return &StoreConfig{ 93 | StoreName: storeName, 94 | Maxlifetime: maxlifetime, 95 | ServerIP: serverIp, 96 | StoreKeyPre: storeKeyPre, 97 | MaxIdle: maxIdle, 98 | MaxActive: maxActive, 99 | } 100 | } 101 | 102 | // NewDefaultSessionManager create new session manager with default config info 103 | func NewDefaultSessionManager(appLog logger.AppLog, config *StoreConfig) (*SessionManager, error) { 104 | return NewSessionManager(DefaultSessionGCLifeTime, appLog, config) 105 | } 106 | 107 | // NewSessionManager create new seesion manager 108 | func NewSessionManager(gcLifetime int64, appLog logger.AppLog, config *StoreConfig) (*SessionManager, error) { 109 | if gcLifetime <= 0 { 110 | gcLifetime = DefaultSessionGCLifeTime 111 | } 112 | if config.CookieName == "" { 113 | config.CookieName = DefaultSessionCookieName 114 | } 115 | manager := &SessionManager{ 116 | store: GetSessionStore(config), 117 | appLog: appLog, 118 | GCLifetime: gcLifetime, 119 | storeConfig: config, 120 | } 121 | // enable GC 122 | go func() { 123 | time.AfterFunc(time.Duration(manager.GCLifetime)*time.Second, func() { manager.GC() }) 124 | }() 125 | return manager, nil 126 | } 127 | 128 | // NewSessionID create new session id with DefaultSessionLength 129 | func (manager *SessionManager) NewSessionID() string { 130 | val := cryptos.GetRandString(DefaultSessionLength) 131 | return val 132 | } 133 | 134 | // StoreConfig return store config 135 | func (manager *SessionManager) StoreConfig() *StoreConfig { 136 | return manager.storeConfig 137 | } 138 | 139 | // GetClientSessionID get session id from client 140 | // default mode is from cookie 141 | func (manager *SessionManager) GetClientSessionID(req *http.Request) (string, error) { 142 | cookie, err := req.Cookie(manager.storeConfig.CookieName) 143 | if err != nil { 144 | return "", err 145 | } 146 | if cookie.Value == "" { 147 | return "", nil 148 | } 149 | // TODO: check client validity 150 | // check ip & agent 151 | return url.QueryUnescape(cookie.Value) 152 | } 153 | 154 | func (manager *SessionManager) GetSessionState(sessionId string) (session *SessionState, err error) { 155 | session, err = manager.store.SessionRead(sessionId) 156 | if err != nil { 157 | session = NewSessionState(manager.store, sessionId, make(map[interface{}]interface{})) 158 | } 159 | return session, nil 160 | } 161 | 162 | // RemoveSessionState delete the session state associated with a specific session ID 163 | func (manager *SessionManager) RemoveSessionState(sessionId string) error { 164 | return manager.store.SessionRemove(sessionId) 165 | } 166 | 167 | // GC loop gc session data 168 | func (manager *SessionManager) GC() { 169 | num := manager.store.SessionGC() 170 | if num > 0 { 171 | manager.appLog.Debug("SessionManger.GC => "+strconv.Itoa(num), LogTarget_Session) 172 | } 173 | time.AfterFunc(time.Duration(manager.GCLifetime)*time.Second, func() { manager.GC() }) 174 | } 175 | -------------------------------------------------------------------------------- /session/session_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | const ( 4 | IP = "0.0.0.0" 5 | ) 6 | 7 | /* 8 | func TestGetSessionStore(t *testing.T) { 9 | defaultConfig := NewDefaultRuntimeConfig() 10 | 11 | defaultSessionStore := GetSessionStore(defaultConfig) 12 | 13 | test.Equal(t, SessionMode_Runtime, defaultConfig.StoreName) 14 | test.Equal(t, int64(DefaultSessionMaxLifeTime), defaultConfig.Maxlifetime) 15 | test.Equal(t, "", defaultConfig.ServerIP) 16 | 17 | test.NotNil(t, defaultSessionStore) 18 | 19 | defaultRedisConfig := NewDefaultRedisConfig(IP) 20 | 21 | defaultRedisSessionStore := GetSessionStore(defaultRedisConfig) 22 | 23 | test.Equal(t, SessionMode_Redis, defaultRedisConfig.StoreName) 24 | test.Equal(t, int64(DefaultSessionMaxLifeTime), defaultRedisConfig.Maxlifetime) 25 | test.Equal(t, IP, defaultRedisConfig.ServerIP) 26 | 27 | test.NotNil(t, defaultRedisSessionStore) 28 | } 29 | 30 | func TestNewDefaultSessionManager(t *testing.T) { 31 | defaultRedisConfig := NewDefaultRedisConfig(IP) 32 | manager, err := NewDefaultSessionManager(defaultRedisConfig) 33 | 34 | test.Nil(t, err) 35 | test.NotNil(t, manager) 36 | 37 | test.NotNil(t, manager.store) 38 | test.Equal(t, int64(DefaultSessionGCLifeTime), manager.GCLifetime) 39 | test.Equal(t, DefaultSessionCookieName, manager.storeConfig.CookieName) 40 | test.Equal(t, defaultRedisConfig, manager.storeConfig) 41 | 42 | sessionId := manager.NewSessionID() 43 | 44 | test.Equal(t, 32, len(sessionId)) 45 | 46 | sessionState, err := manager.GetSessionState(sessionId) 47 | test.Nil(t, err) 48 | test.NotNil(t, sessionState) 49 | test.Equal(t, sessionId, sessionState.sessionId) 50 | } 51 | */ 52 | -------------------------------------------------------------------------------- /session/sessionstate.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | var sessionStatePool sync.Pool 11 | 12 | func init() { 13 | sessionStatePool = sync.Pool{ 14 | New: func() interface{} { 15 | return &SessionState{} 16 | }, 17 | } 18 | } 19 | 20 | // session state 21 | type SessionState struct { 22 | sessionId string // session id 23 | timeAccessed time.Time // last access time 24 | values map[interface{}]interface{} // session store 25 | lock *sync.RWMutex 26 | store SessionStore 27 | } 28 | 29 | func NewSessionState(store SessionStore, sessionId string, values map[interface{}]interface{}) *SessionState { 30 | state := sessionStatePool.Get().(*SessionState) 31 | state.reset(store, sessionId, values, time.Now()) 32 | return state 33 | } 34 | 35 | // Set key-value to current state 36 | func (state *SessionState) reset(store SessionStore, sessionId string, values map[interface{}]interface{}, accessTime time.Time) { 37 | state.values = values 38 | state.sessionId = sessionId 39 | state.timeAccessed = accessTime 40 | state.store = store 41 | state.lock = new(sync.RWMutex) 42 | } 43 | 44 | // Set key-value to current state 45 | func (state *SessionState) Set(key, value interface{}) error { 46 | state.lock.Lock() 47 | defer state.lock.Unlock() 48 | state.values[key] = value 49 | return state.store.SessionUpdate(state) 50 | 51 | } 52 | 53 | // Get value by key in current state 54 | func (state *SessionState) Get(key interface{}) interface{} { 55 | state.lock.RLock() 56 | defer state.lock.RUnlock() 57 | if v, ok := state.values[key]; ok { 58 | return v 59 | } 60 | return nil 61 | } 62 | 63 | // GetString Get value as string by key in current state 64 | func (state *SessionState) GetString(key interface{}) string { 65 | v := state.Get(key) 66 | return fmt.Sprint(v) 67 | } 68 | 69 | // GetInt Get value as int by key in current state 70 | func (state *SessionState) GetInt(key interface{}) int { 71 | v, _ := strconv.Atoi(state.GetString(key)) 72 | return v 73 | } 74 | 75 | // GetInt64 Get value as int64 by key in current state 76 | func (state *SessionState) GetInt64(key interface{}) int64 { 77 | v, _ := strconv.ParseInt(state.GetString(key), 10, 64) 78 | return v 79 | } 80 | 81 | // Remove value by key in current state 82 | func (state *SessionState) Remove(key interface{}) error { 83 | state.lock.Lock() 84 | defer state.lock.Unlock() 85 | delete(state.values, key) 86 | return nil 87 | } 88 | 89 | // Clear delete all values in current store 90 | func (state *SessionState) Clear() error { 91 | state.lock.Lock() 92 | defer state.lock.Unlock() 93 | state.values = make(map[interface{}]interface{}) 94 | return nil 95 | } 96 | 97 | // SessionID get this id in current state 98 | func (state *SessionState) SessionID() string { 99 | return state.sessionId 100 | } 101 | 102 | // Count get all item's count in current state 103 | func (state *SessionState) Count() int { 104 | return len(state.values) 105 | } 106 | -------------------------------------------------------------------------------- /session/sessionstate_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | -------------------------------------------------------------------------------- /session/store_redis.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/devfeel/dotweb/framework/encodes/gob" 9 | "github.com/devfeel/dotweb/framework/hystrix" 10 | "github.com/devfeel/dotweb/framework/redis" 11 | ) 12 | 13 | const ( 14 | defaultRedisKeyPre = "dotweb:session:" 15 | HystrixErrorCount = 20 16 | ) 17 | 18 | // RedisStore Implement the SessionStore interface 19 | type RedisStore struct { 20 | hystrix hystrix.Hystrix 21 | lock *sync.RWMutex // locker 22 | maxlifetime int64 23 | serverIp string // connection string, like "redis://:password@10.0.1.11:6379/0" 24 | backupServerUrl string // backup connection string, like "redis://:password@10.0.1.11:6379/0" 25 | storeKeyPre string // set custom redis key-pre; default is dotweb:session: 26 | maxIdle int // set MaxIdle; default is 10 27 | maxActive int // set MaxActive; default is 20 28 | } 29 | 30 | // create new redis store 31 | func NewRedisStore(config *StoreConfig) (*RedisStore, error) { 32 | store := &RedisStore{ 33 | lock: new(sync.RWMutex), 34 | serverIp: config.ServerIP, 35 | backupServerUrl: config.BackupServerUrl, 36 | maxlifetime: config.Maxlifetime, 37 | maxIdle: config.MaxIdle, 38 | maxActive: config.MaxActive, 39 | } 40 | store.hystrix = hystrix.NewHystrix(store.checkRedisAlive, nil) 41 | store.hystrix.SetMaxFailedNumber(HystrixErrorCount) 42 | store.hystrix.Do() 43 | // init redis key-pre 44 | if config.StoreKeyPre == "" { 45 | store.storeKeyPre = defaultRedisKeyPre 46 | } else { 47 | store.storeKeyPre = config.StoreKeyPre 48 | } 49 | redisClient := store.getRedisClient() 50 | _, err := redisClient.Ping() 51 | if store.checkConnErrorAndNeedRetry(err) { 52 | store.hystrix.TriggerHystrix() 53 | redisClient = store.getBackupRedis() 54 | _, err = redisClient.Ping() 55 | } 56 | return store, err 57 | } 58 | 59 | func (store *RedisStore) getRedisKey(key string) string { 60 | return store.storeKeyPre + key 61 | } 62 | 63 | // SessionRead get session state by sessionId 64 | func (store *RedisStore) SessionRead(sessionId string) (*SessionState, error) { 65 | redisClient := store.getRedisClient() 66 | key := store.getRedisKey(sessionId) 67 | kvs, err := redisClient.Get(key) 68 | if store.checkConnErrorAndNeedRetry(err) { 69 | redisClient = store.getBackupRedis() 70 | kvs, err = redisClient.Get(key) 71 | } 72 | if err != nil { 73 | return nil, err 74 | } 75 | var kv map[interface{}]interface{} 76 | if len(kvs) == 0 { 77 | kv = make(map[interface{}]interface{}) 78 | } else { 79 | kv, err = gob.DecodeMap([]byte(kvs)) 80 | if err != nil { 81 | return nil, err 82 | } 83 | } 84 | state := NewSessionState(store, sessionId, kv) 85 | go store.sessionReExpire(state) 86 | return state, nil 87 | } 88 | 89 | // SessionExist check session state exist by sessionId 90 | func (store *RedisStore) SessionExist(sessionId string) bool { 91 | redisClient := store.getRedisClient() 92 | key := store.getRedisKey(sessionId) 93 | exists, err := redisClient.Exists(key) 94 | if store.checkConnErrorAndNeedRetry(err) { 95 | redisClient = store.getBackupRedis() 96 | exists, err = redisClient.Exists(key) 97 | } 98 | if err != nil { 99 | return false 100 | } 101 | return exists 102 | } 103 | 104 | // sessionReExpire reset expire session key 105 | func (store *RedisStore) sessionReExpire(state *SessionState) error { 106 | redisClient := store.getRedisClient() 107 | key := store.getRedisKey(state.SessionID()) 108 | _, err := redisClient.Expire(key, store.maxlifetime) 109 | if store.checkConnErrorAndNeedRetry(err) { 110 | redisClient = store.getBackupRedis() 111 | _, err = redisClient.Expire(key, store.maxlifetime) 112 | } 113 | return err 114 | } 115 | 116 | // SessionUpdate update session state in store 117 | func (store *RedisStore) SessionUpdate(state *SessionState) error { 118 | defer func() { 119 | // ignore error 120 | if err := recover(); err != nil { 121 | fmt.Println("SessionUpdate-Redis error", err) 122 | // TODO deal panic err 123 | } 124 | }() 125 | redisClient := store.getRedisClient() 126 | bytes, err := gob.EncodeMap(state.values) 127 | if err != nil { 128 | return err 129 | } 130 | key := store.getRedisKey(state.SessionID()) 131 | _, err = redisClient.SetWithExpire(key, string(bytes), store.maxlifetime) 132 | if store.checkConnErrorAndNeedRetry(err) { 133 | redisClient = store.getBackupRedis() 134 | _, err = redisClient.SetWithExpire(key, string(bytes), store.maxlifetime) 135 | } 136 | return err 137 | } 138 | 139 | // SessionRemove delete session state in store 140 | func (store *RedisStore) SessionRemove(sessionId string) error { 141 | redisClient := redisutil.GetRedisClient(store.serverIp, store.maxIdle, store.maxActive) 142 | key := store.getRedisKey(sessionId) 143 | _, err := redisClient.Del(key) 144 | if store.checkConnErrorAndNeedRetry(err) { 145 | redisClient = store.getBackupRedis() 146 | _, err = redisClient.Del(key) 147 | } 148 | return err 149 | } 150 | 151 | // SessionGC clean expired session states 152 | // in redis store,not use 153 | func (store *RedisStore) SessionGC() int { 154 | return 0 155 | } 156 | 157 | // SessionAll get count number 158 | func (store *RedisStore) SessionCount() int { 159 | return 0 160 | } 161 | 162 | // getRedisClient get alive redis client 163 | func (store *RedisStore) getRedisClient() *redisutil.RedisClient { 164 | if store.hystrix.IsHystrix() { 165 | if store.backupServerUrl != "" { 166 | return store.getBackupRedis() 167 | } 168 | } 169 | return store.getDefaultRedis() 170 | } 171 | 172 | func (store *RedisStore) getDefaultRedis() *redisutil.RedisClient { 173 | return redisutil.GetRedisClient(store.serverIp, store.maxIdle, store.maxActive) 174 | } 175 | 176 | func (store *RedisStore) getBackupRedis() *redisutil.RedisClient { 177 | return redisutil.GetRedisClient(store.backupServerUrl, store.maxIdle, store.maxActive) 178 | } 179 | 180 | // checkConnErrorAndNeedRetry check err is Conn error and is need to retry 181 | func (store *RedisStore) checkConnErrorAndNeedRetry(err error) bool { 182 | if err == nil { 183 | return false 184 | } 185 | if strings.Index(err.Error(), "no such host") >= 0 || 186 | strings.Index(err.Error(), "No connection could be made because the target machine actively refused it") >= 0 || 187 | strings.Index(err.Error(), "A connection attempt failed because the connected party did not properly respond after a period of time") >= 0 { 188 | store.hystrix.GetCounter().Inc(1) 189 | // if is hystrix, not to retry, because in getReadRedisClient already use backUp redis 190 | if store.hystrix.IsHystrix() { 191 | return false 192 | } 193 | if store.backupServerUrl == "" { 194 | return false 195 | } 196 | return true 197 | } 198 | return false 199 | } 200 | 201 | // checkRedisAlive check redis is alive use ping 202 | // if set readonly redis, check readonly redis 203 | // if not set readonly redis, check default redis 204 | func (store *RedisStore) checkRedisAlive() bool { 205 | isAlive := false 206 | var redisClient *redisutil.RedisClient 207 | redisClient = store.getDefaultRedis() 208 | for i := 0; i <= 5; i++ { 209 | reply, err := redisClient.Ping() 210 | if err != nil { 211 | isAlive = false 212 | break 213 | } 214 | if reply != "PONG" { 215 | isAlive = false 216 | break 217 | } 218 | isAlive = true 219 | continue 220 | } 221 | return isAlive 222 | } 223 | -------------------------------------------------------------------------------- /session/store_redis_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | -------------------------------------------------------------------------------- /session/store_runtime.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // MemProvider Implement the provider interface 10 | type RuntimeStore struct { 11 | lock *sync.RWMutex // locker 12 | sessions map[string]*list.Element // map in memory 13 | list *list.List // for gc 14 | maxlifetime int64 // session life time, with second 15 | } 16 | 17 | func NewRuntimeStore(config *StoreConfig) *RuntimeStore { 18 | return &RuntimeStore{ 19 | lock: new(sync.RWMutex), 20 | sessions: make(map[string]*list.Element), 21 | list: new(list.List), 22 | maxlifetime: config.Maxlifetime, 23 | } 24 | } 25 | 26 | // SessionRead get session state by sessionId 27 | func (store *RuntimeStore) SessionRead(sessionId string) (*SessionState, error) { 28 | store.lock.RLock() 29 | if element, ok := store.sessions[sessionId]; ok { 30 | go store.SessionAccess(sessionId) 31 | store.lock.RUnlock() 32 | return element.Value.(*SessionState), nil 33 | } 34 | store.lock.RUnlock() 35 | 36 | // if sessionId of state not exist, create a new state 37 | state := NewSessionState(store, sessionId, make(map[interface{}]interface{})) 38 | store.lock.Lock() 39 | element := store.list.PushFront(state) 40 | store.sessions[sessionId] = element 41 | store.lock.Unlock() 42 | return state, nil 43 | } 44 | 45 | // SessionExist check session state exist by sessionId 46 | func (store *RuntimeStore) SessionExist(sessionId string) bool { 47 | store.lock.RLock() 48 | defer store.lock.RUnlock() 49 | if _, ok := store.sessions[sessionId]; ok { 50 | return true 51 | } 52 | return false 53 | } 54 | 55 | // SessionUpdate update session state in store 56 | func (store *RuntimeStore) SessionUpdate(state *SessionState) error { 57 | store.lock.RLock() 58 | if element, ok := store.sessions[state.sessionId]; ok { // state has exist 59 | go store.SessionAccess(state.sessionId) 60 | store.lock.RUnlock() 61 | element.Value.(*SessionState).values = state.values // only assist update whole session state 62 | return nil 63 | } 64 | store.lock.RUnlock() 65 | 66 | // if sessionId of state not exist, create a new state 67 | new_state := NewSessionState(store, state.sessionId, state.values) 68 | store.lock.Lock() 69 | new_element := store.list.PushFront(new_state) 70 | store.sessions[state.sessionId] = new_element 71 | store.lock.Unlock() 72 | return nil 73 | } 74 | 75 | // SessionRemove delete session state in store 76 | func (store *RuntimeStore) SessionRemove(sessionId string) error { 77 | store.lock.Lock() 78 | defer store.lock.Unlock() 79 | if element, ok := store.sessions[sessionId]; ok { 80 | delete(store.sessions, sessionId) 81 | store.list.Remove(element) 82 | return nil 83 | } 84 | return nil 85 | } 86 | 87 | // SessionGC clean expired session stores in memory session 88 | func (store *RuntimeStore) SessionGC() int { 89 | num := 0 90 | store.lock.RLock() 91 | for { 92 | element := store.list.Back() 93 | if element == nil { 94 | break 95 | } 96 | if (element.Value.(*SessionState).timeAccessed.Unix() + store.maxlifetime) < time.Now().Unix() { 97 | store.lock.RUnlock() 98 | store.lock.Lock() 99 | store.list.Remove(element) 100 | delete(store.sessions, element.Value.(*SessionState).SessionID()) 101 | num += 1 102 | store.lock.Unlock() 103 | store.lock.RLock() 104 | } else { 105 | break 106 | } 107 | } 108 | store.lock.RUnlock() 109 | return num 110 | } 111 | 112 | // SessionAll get count number of memory session 113 | func (store *RuntimeStore) SessionCount() int { 114 | return store.list.Len() 115 | } 116 | 117 | // SessionAccess expand time of session store by id in memory session 118 | func (store *RuntimeStore) SessionAccess(sessionId string) error { 119 | store.lock.Lock() 120 | defer store.lock.Unlock() 121 | if element, ok := store.sessions[sessionId]; ok { 122 | element.Value.(*SessionState).timeAccessed = time.Now() 123 | store.list.MoveToFront(element) 124 | return nil 125 | } 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /session/store_runtime_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | var conf *StoreConfig 12 | var runtime_store *RuntimeStore 13 | var session_state *SessionState 14 | var session_states []*SessionState 15 | 16 | func init() { 17 | //log.Println("初始化") 18 | value := make(map[interface{}]interface{}) 19 | value["foo"] = "bar" 20 | value["kak"] = "lal" 21 | 22 | conf = NewDefaultRuntimeConfig() 23 | runtime_store = NewRuntimeStore(conf) 24 | runtime_store.list.Init() 25 | 26 | session_state = NewSessionState(nil, "session_read", value) 27 | for i := 0; i < 1000000; i++ { 28 | session_states = append(session_states, NewSessionState(nil, "session_read"+strconv.Itoa(i), value)) 29 | //runtime_store.SessionUpdate(NewSessionState(nil,"session_read"+strconv.FormatInt(time.Now().UnixNano(),10),value)) 30 | } 31 | 32 | runtime_store.SessionUpdate(session_state) 33 | runtime_store.SessionUpdate(NewSessionState(nil, "session_read_1", value)) 34 | } 35 | 36 | func TestRuntimeStore_SessionUpdate(t *testing.T) { 37 | //log.Println("开始 写测试") 38 | fmt.Println("-------------before update session state------------") 39 | state, _ := runtime_store.SessionRead("session_read") 40 | fmt.Printf("session state session_read: %+v \n ", state) 41 | session_state.values["foo"] = "newbar" 42 | runtime_store.SessionUpdate(session_state) 43 | state, _ = runtime_store.SessionRead("session_read") 44 | fmt.Println("-------------after update session state------------") 45 | fmt.Printf("session state session_read: %+v \n ", state) 46 | 47 | } 48 | func TestNewRuntimeStore_SessionUpdate_StateNotExist(t *testing.T) { 49 | fmt.Println("-------------before update session state------------") 50 | state, _ := runtime_store.SessionRead("session_read_2") 51 | fmt.Printf("session state session_read: %+v \n ", state) 52 | state.values["make"] = "new" 53 | runtime_store.SessionUpdate(state) 54 | state, _ = runtime_store.SessionRead("session_read") 55 | fmt.Println("-------------after update session state------------") 56 | fmt.Printf("session state session_read: %+v \n ", state) 57 | } 58 | 59 | func TestRuntimeStore_SessionRead(t *testing.T) { 60 | //log.Println("开始读测试") 61 | fmt.Printf("runtime_store: %+v \n", *runtime_store) 62 | read, _ := runtime_store.SessionRead("session_read") 63 | if read == nil { 64 | fmt.Println("cannot find sessionId") 65 | return 66 | } 67 | fmt.Println("start read : ") 68 | fmt.Printf("sessionid : %v , values : %v \n", read.SessionID(), read.values) 69 | } 70 | 71 | func TestRuntimeStore_SessionExist(t *testing.T) { 72 | //log.Println("测试 session 存在") 73 | fmt.Println("is session exist: ", runtime_store.SessionExist("session_read")) 74 | 75 | } 76 | 77 | func TestRuntimeStore_SessionRemove(t *testing.T) { 78 | log.Println("session 删除测试") 79 | fmt.Println("------------------------") 80 | fmt.Println("before remove : ") 81 | read, err := runtime_store.SessionRead("session_read") 82 | if err != nil { 83 | panic(err) 84 | } 85 | fmt.Println("read : ") 86 | fmt.Printf("sessionid : %s , values : %v \n", read.SessionID(), read.values) 87 | 88 | err = runtime_store.SessionRemove("session_read") 89 | if err != nil { 90 | fmt.Println(err.Error()) 91 | } 92 | 93 | fmt.Println("------------------------") 94 | fmt.Println("after remove : ") 95 | read, err = runtime_store.SessionRead("session_read") 96 | if err != nil { 97 | panic(err) 98 | } 99 | fmt.Println("read : ") 100 | fmt.Printf("sessionid : %s , values : %v \n", read.SessionID(), read.values) 101 | } 102 | 103 | func TestRuntimeStore_SessionGC(t *testing.T) { 104 | 105 | } 106 | 107 | func TestRuntimeStore_SessionCount(t *testing.T) { 108 | fmt.Println(runtime_store.SessionCount()) 109 | } 110 | 111 | func TestRuntimeStore_SessionAccess(t *testing.T) { 112 | state, _ := runtime_store.SessionRead("session_read") 113 | fmt.Println("------------------") 114 | fmt.Println("before session access") 115 | fmt.Println(state.timeAccessed.String()) 116 | fmt.Println("------------------") 117 | fmt.Println("after session access") 118 | time.Sleep(10 * time.Second) 119 | runtime_store.SessionAccess("session_read") 120 | fmt.Println(state.timeAccessed.String()) 121 | 122 | } 123 | 124 | /** 125 | 性能测试 | 基准测试 126 | */ 127 | 128 | func BenchmarkRuntimeStore_SessionRead_1(b *testing.B) { 129 | for i := 0; i < b.N; i++ { 130 | runtime_store.SessionRead("session_read") 131 | } 132 | b.ReportAllocs() 133 | } 134 | func BenchmarkRuntimeStore_SessionRead_Parallel(b *testing.B) { 135 | b.RunParallel(func(pb *testing.PB) { 136 | for pb.Next() { 137 | runtime_store.SessionRead("session_read") 138 | } 139 | }) 140 | b.ReportAllocs() 141 | 142 | } 143 | 144 | func BenchmarkRuntimeStore_SessionCount_1(b *testing.B) { 145 | for i := 0; i < b.N; i++ { 146 | runtime_store.SessionCount() 147 | } 148 | } 149 | 150 | func BenchmarkRuntimeStore_SessionCount_Parallel(b *testing.B) { 151 | b.RunParallel(func(pb *testing.PB) { 152 | for pb.Next() { 153 | runtime_store.SessionCount() 154 | } 155 | }) 156 | b.ReportAllocs() 157 | 158 | } 159 | 160 | func BenchmarkRuntimeStore_SessionRemove_1(b *testing.B) { 161 | 162 | } 163 | 164 | func BenchmarkRuntimeStore_SessionUpdate_1(b *testing.B) { 165 | for i := 0; i < b.N; i++ { 166 | runtime_store.SessionUpdate(session_states[i]) 167 | } 168 | b.ReportAllocs() 169 | } 170 | 171 | func BenchmarkRuntimeStore_SessionUpdate_Parallel(b *testing.B) { 172 | b.RunParallel(func(pb *testing.PB) { 173 | for pb.Next() { 174 | runtime_store.SessionUpdate(session_state) 175 | } 176 | }) 177 | b.ReportAllocs() 178 | fmt.Println(len(runtime_store.sessions)) 179 | } 180 | -------------------------------------------------------------------------------- /test/assert.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "path/filepath" 5 | "reflect" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func Contains(t *testing.T, expected, actual interface{}) { 12 | if reflect.TypeOf(expected) != reflect.TypeOf(actual) { 13 | formattedLog(t, "value type not match %T (expected)\n\n\t != %T (actual)", expected, actual) 14 | } 15 | switch e := expected.(type) { 16 | case string: 17 | a := actual.(string) 18 | if !strings.Contains(a, e) { 19 | formattedLog(t, " %v (expected)\n\n\t!= %v (actual)", 20 | expected, actual) 21 | t.FailNow() 22 | } 23 | default: 24 | formattedLog(t, "unsupported type %T(expected)", expected) 25 | t.FailNow() 26 | } 27 | } 28 | 29 | func Equal(t *testing.T, expected, actual interface{}) { 30 | if !reflect.DeepEqual(expected, actual) { 31 | formattedLog(t, " %v (expected)\n\n\t!= %v (actual)", 32 | expected, actual) 33 | t.FailNow() 34 | } 35 | } 36 | 37 | func NotEqual(t *testing.T, expected, actual interface{}) { 38 | if reflect.DeepEqual(expected, actual) { 39 | formattedLog(t, "value should not equal %#v", actual) 40 | t.FailNow() 41 | } 42 | } 43 | 44 | func Nil(t *testing.T, object interface{}) { 45 | if !isNil(object) { 46 | formattedLog(t, " (expected)\n\n\t!= %#v (actual)", object) 47 | t.FailNow() 48 | } 49 | } 50 | 51 | func NotNil(t *testing.T, object interface{}) { 52 | if isNil(object) { 53 | formattedLog(t, "Expected value not to be ", object) 54 | t.FailNow() 55 | } 56 | } 57 | 58 | func isNil(object interface{}) bool { 59 | if object == nil { 60 | return true 61 | } 62 | 63 | value := reflect.ValueOf(object) 64 | kind := value.Kind() 65 | if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { 66 | return true 67 | } 68 | 69 | return false 70 | } 71 | 72 | func formattedLog(t *testing.T, fmt string, args ...interface{}) { 73 | _, file, line, _ := runtime.Caller(2) 74 | file = filepath.Base(file) 75 | targs := make([]interface{}, len(args)+2) 76 | targs[0] = file 77 | targs[1] = line 78 | copy(targs[2:], args) 79 | t.Logf("\033[31m%s:%d:\n\n\t"+fmt+"\033[39m\n\n", targs...) 80 | } 81 | -------------------------------------------------------------------------------- /test/util.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "testing" 7 | ) 8 | 9 | // ToJson 10 | func ToJson(t *testing.T, v interface{}) string { 11 | b, err := json.Marshal(v) 12 | Nil(t, err) 13 | return string(b) 14 | } 15 | 16 | // ToXML 17 | func ToXML(t *testing.T, v interface{}) string { 18 | b, err := xml.Marshal(v) 19 | Nil(t, err) 20 | //t.Log("xml:",string(b)) 21 | return string(b) 22 | } 23 | 24 | //ToDefault 25 | func ToDefault(t *testing.T, v interface{}) string { 26 | return "" 27 | } 28 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type Tools struct { 9 | } 10 | 11 | func (t *Tools) PrettyJson(data interface{}) string { 12 | by, err := json.MarshalIndent(data, "", "\t") 13 | if err != nil { 14 | return fmt.Sprint(data) 15 | } 16 | return string(by) 17 | } 18 | -------------------------------------------------------------------------------- /tools_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | -------------------------------------------------------------------------------- /uploadfile.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "mime/multipart" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/devfeel/dotweb/framework/crypto" 12 | ) 13 | 14 | const randFileNameLength = 12 15 | 16 | type UploadFile struct { 17 | File multipart.File 18 | Header *multipart.FileHeader 19 | fileExt string // file extensions 20 | fileName string 21 | randomFileName string 22 | fileSize int64 23 | } 24 | 25 | func NewUploadFile(file multipart.File, header *multipart.FileHeader) *UploadFile { 26 | return &UploadFile{ 27 | File: file, 28 | Header: header, 29 | fileName: header.Filename, 30 | randomFileName: cryptos.GetRandString(randFileNameLength) + filepath.Ext(header.Filename), 31 | fileExt: filepath.Ext(header.Filename), // update for issue #99 32 | } 33 | } 34 | 35 | // FileName get upload file client-local name 36 | func (f *UploadFile) FileName() string { 37 | return f.fileName 38 | } 39 | 40 | // RandomFileName get upload file random name with uuid 41 | func (f *UploadFile) RandomFileName() string { 42 | return f.randomFileName 43 | } 44 | 45 | // Size get upload file size 46 | func (f *UploadFile) Size() int64 { 47 | return f.Header.Size 48 | } 49 | 50 | // SaveFile save file in server-local with filename 51 | // special: 52 | // if you SaveFile, it's will cause empty data when use ReadBytes 53 | func (f *UploadFile) SaveFile(fileName string) (size int64, err error) { 54 | size = 0 55 | if fileName == "" { 56 | return size, errors.New("filename not allow empty") 57 | } 58 | 59 | fileWriter, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0666) 60 | if err != nil { 61 | return size, err 62 | } 63 | defer fileWriter.Close() 64 | size, err = io.Copy(fileWriter, f.File) 65 | return size, err 66 | } 67 | 68 | // GetFileExt get upload file extensions 69 | func (f *UploadFile) GetFileExt() string { 70 | return f.fileExt 71 | } 72 | 73 | // ReadBytes Bytes returns a slice of byte hoding the UploadFile.File 74 | // special: 75 | // if you read bytes, it's will cause empty data in UploadFile.File, so you use SaveFile will no any data to save 76 | func (f *UploadFile) ReadBytes() []byte { 77 | buf := new(bytes.Buffer) 78 | buf.ReadFrom(f.File) 79 | return buf.Bytes() 80 | } 81 | -------------------------------------------------------------------------------- /uploadfile_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_NewUploadFile_1(t *testing.T) { 8 | } 9 | 10 | func Test_FileName_1(t *testing.T) { 11 | // 12 | } 13 | 14 | func Test_Size_1(t *testing.T) { 15 | // 16 | } 17 | 18 | func Test_SaveFile_1(t *testing.T) { 19 | // 20 | } 21 | 22 | // GetFileExt 23 | func Test_GetFileExt_1(t *testing.T) { 24 | // 25 | } 26 | 27 | func Test_Request_1(t *testing.T) { 28 | // 29 | } 30 | 31 | func Test_SendMessage_1(t *testing.T) { 32 | // 33 | } 34 | 35 | func Test_ReadMessage_1(t *testing.T) { 36 | // 37 | } 38 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package dotweb 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | // common init context 15 | func initContext(param *InitContextParam) *HttpContext { 16 | httpRequest := &http.Request{} 17 | context := &HttpContext{ 18 | request: &Request{ 19 | Request: httpRequest, 20 | }, 21 | httpServer: &HttpServer{ 22 | DotApp: New(), 23 | }, 24 | } 25 | header := make(map[string][]string) 26 | header["Accept-Encoding"] = []string{"gzip, deflate"} 27 | header["Accept-Language"] = []string{"en-us"} 28 | header["Foo"] = []string{"Bar", "two"} 29 | // specify json 30 | header["Content-Type"] = []string{param.contentType} 31 | context.request.Header = header 32 | 33 | jsonStr := param.convertHandler(param.t, param.v) 34 | body := format(jsonStr) 35 | context.request.Request.Body = body 36 | 37 | return context 38 | } 39 | 40 | // init response context 41 | func initResponseContext(param *InitContextParam) *HttpContext { 42 | context := &HttpContext{ 43 | response: &Response{}, 44 | } 45 | 46 | var buf1 bytes.Buffer 47 | w := io.MultiWriter(&buf1) 48 | 49 | writer := &gzipResponseWriter{ 50 | ResponseWriter: &httpWriter{}, 51 | Writer: w, 52 | } 53 | 54 | context.response = NewResponse(writer) 55 | 56 | return context 57 | } 58 | 59 | // init request and response context 60 | func initAllContext(param *InitContextParam) *HttpContext { 61 | context := &HttpContext{ 62 | response: &Response{}, 63 | request: &Request{ 64 | Request: &http.Request{}, 65 | }, 66 | httpServer: &HttpServer{ 67 | DotApp: New(), 68 | }, 69 | routerNode: &Node{}, 70 | } 71 | 72 | header := make(map[string][]string) 73 | header["Accept-Encoding"] = []string{"gzip, deflate"} 74 | header["Accept-Language"] = []string{"en-us"} 75 | header["Foo"] = []string{"Bar", "two"} 76 | // specify json 77 | header["Content-Type"] = []string{param.contentType} 78 | context.request.Header = header 79 | 80 | u := &url.URL{ 81 | Path: "/index", 82 | } 83 | 84 | context.request.URL = u 85 | context.request.Method = "POST" 86 | 87 | jsonStr := param.convertHandler(param.t, param.v) 88 | body := format(jsonStr) 89 | context.request.Request.Body = body 90 | 91 | w := &httpWriter{} 92 | 93 | context.response = NewResponse(w) 94 | 95 | return context 96 | } 97 | 98 | type httpWriter http.Header 99 | 100 | func (ho httpWriter) Header() http.Header { 101 | return http.Header(ho) 102 | } 103 | 104 | func (ho httpWriter) Write(byte []byte) (int, error) { 105 | fmt.Println("string:", string(byte)) 106 | return 0, nil 107 | } 108 | 109 | func (ho httpWriter) WriteHeader(code int) { 110 | fmt.Println("code:", code) 111 | } 112 | 113 | func format(b string) io.ReadCloser { 114 | s := strings.NewReader(b) 115 | r := ioutil.NopCloser(s) 116 | r.Close() 117 | return r 118 | } 119 | 120 | type InitContextParam struct { 121 | t *testing.T 122 | v interface{} 123 | contentType string 124 | convertHandler func(t *testing.T, v interface{}) string 125 | } 126 | --------------------------------------------------------------------------------