├── .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 | {{title}}
126 |
127 | {{header}}
128 |
129 | {{body}}
130 |
`
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 | {{title}}
146 |
147 | {{header}}
148 |
149 | {{body}}
150 |
`
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 -
8 | #### Gitter:[](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 |
--------------------------------------------------------------------------------