├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── cache
├── README.md
├── cache.go
├── cache_test.go
├── memory_store.go
├── memory_store_test.go
├── redis_store.go
├── redis_store_test.go
├── repository.go
├── store.go
├── tag_set.go
└── utils.go
├── config
├── app.go
├── config.go
├── cookie.go
├── database.go
├── route.go
├── session.go
└── view.go
├── context
├── cookie.go
├── file.go
├── request.go
├── response.go
└── session.go
├── filesystem
├── fs.go
└── utils.go
├── go.mod
├── go.sum
├── helper
├── helper.go
└── utils.go
├── log
├── formatter
│ ├── formatter.go
│ ├── json_formatter.go
│ └── line_formatter.go
├── handler
│ ├── console.go
│ ├── file.go
│ ├── file_test.go
│ ├── handler.go
│ ├── rotate.go
│ └── rotate_test.go
├── log.go
├── log_test.go
├── logger.go
└── record
│ └── record.go
├── pipeline.go
├── router
├── metch.go
├── middleware.go
├── parameter.go
├── pipeline.go
├── resource_controller.go
├── route.go
├── router.go
├── rule.go
├── static.go
└── utils.go
├── session
├── cookie-handler.go
├── file-handler.go
├── handler.go
├── http.go
├── manager.go
├── redis-handler.go
└── store.go
├── think.go
├── think
├── app.go
├── handler.go
├── recover_handler.go
├── response.go
├── route_hendler.go
├── session_handler.go
└── type.go
├── think_test.go
└── view
├── html
├── layout.html
└── tpl.html
├── view.go
└── view_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | .idea
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | sudo: false
3 | go:
4 | - tip
5 |
6 | services:
7 | - redis-server
8 |
9 | before_install:
10 | - go get github.com/mattn/goveralls
11 |
12 | script:
13 | - $GOPATH/bin/goveralls -service=travis-ci
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ThinkGo
3 |
4 |
5 |
6 | ThinkGo is a lightweight MVC framework written in Go (Golang).
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 | ## Installation
42 |
43 | The only requirement is the [Go Programming Language](https://golang.org/dl/)
44 |
45 | ```
46 | go get -u github.com/forgoer/thinkgo
47 | ```
48 |
49 | ## Quick start
50 |
51 | ```go
52 | package main
53 |
54 | import (
55 | "fmt"
56 |
57 | "github.com/forgoer/thinkgo"
58 | "github.com/forgoer/thinkgo/think"
59 | )
60 |
61 | func main() {
62 | th := thinkgo.New()
63 | th.RegisterRoute(func(route *think.Route) {
64 |
65 | route.Get("/", func(req *think.Req) *think.Res {
66 | return think.Text("Hello ThinkGo !")
67 | })
68 |
69 | route.Get("/ping", func(req *think.Req) *think.Res {
70 | return think.Json(map[string]string{
71 | "message": "pong",
72 | })
73 | })
74 |
75 | // Dependency injection
76 | route.Get("/user/{name}", func(req *think.Req, name string) *think.Res {
77 | return think.Text(fmt.Sprintf("Hello %s !", name))
78 | })
79 | })
80 | // listen and serve on 0.0.0.0:9011
81 | th.Run()
82 | }
83 | ```
84 |
85 | ## Features
86 |
87 | - [Routing](#routing)
88 | - [Middleware](#middleware)
89 | - [Controller](#controller)
90 | - [Request](#http-request)
91 | - [Response](#http-response)
92 | - [View](#view)
93 | - [HTTP Session](#http-session)
94 | - [Logging](#logging)
95 | - [Cache](#cache)
96 | - [ORM](#orm)
97 |
98 | ## Routing
99 |
100 | #### Basic Routing
101 |
102 | The most basic routes accept a URI and a Closure, providing a very simple and expressive method of defining routes:
103 |
104 | ```go
105 | think.RegisterRoute(func(route *router.Route) {
106 | route.Get("/foo", func(req *context.Request) *context.Response {
107 | return thinkgo.Text("Hello ThinkGo !")
108 | })
109 | })
110 | ```
111 |
112 | #### Available Router Methods
113 |
114 | The router allows you to register routes that respond to any HTTP verb:
115 |
116 | ```go
117 | route.Get("/someGet", getting)
118 | route.Post("/somePost", posting)
119 | route.Put("/somePut", putting)
120 | route.Delete("/someDelete", deleting)
121 | route.Patch("/somePatch", patching)
122 | route.Options("/someOptions", options)
123 | ```
124 |
125 | Sometimes you may need to register a route that responds to multiple HTTP verbs. You may even register a route that responds to all HTTP verbs using the `Any` method:
126 |
127 | ```go
128 | route.Any("/someAny", any)
129 | ```
130 |
131 | #### Parameters in path
132 |
133 | Of course, sometimes you will need to capture segments of the URI within your route. For example, you may need to capture a user's ID from the URL. You may do so by defining route parameters:
134 |
135 | ```go
136 | route.Get("/user/{id}", func(req *context.Request, id string) *context.Response {
137 | return thinkgo.Text(fmt.Sprintf("User %s", id))
138 | })
139 | ```
140 |
141 | You may define as many route parameters as required by your route:
142 |
143 | ```go
144 | route.Get("/posts/{post}/comments/{comment}", func(req *context.Request, postId, commentId string) *context.Response {
145 | //
146 | })
147 | ```
148 |
149 | #### Route Prefixes
150 |
151 | The prefix method may be used to prefix each route in the group with a given URI. For example, you may want to prefix all route URIs within the group with `admin`:
152 |
153 | ```go
154 | route.Prefix("/admin").Group(func(group *router.Route) {
155 | group.Prefix("user").Group(func(group *router.Route) {
156 | // ...
157 | })
158 | group.Prefix("posts").Group(func(group *router.Route) {
159 | // ...
160 | })
161 | })
162 | ```
163 |
164 | #### Route Groups
165 |
166 | Route groups allow you to share route attributes, such as middleware or prefix, across a large number of routes without needing to define those attributes on each individual route.
167 |
168 | ```go
169 | route.Prefix("/admin").Group(func(group *router.Route) {
170 | group.Prefix("user").Group(func(group *router.Route) {
171 | group.Get("", func(request *context.Request) *context.Response {
172 | return thinkgo.Text("admin user !")
173 | }).Middleware(func(request *context.Request, next router.Closure) interface{} {
174 | if _, err := request.Input("id"); err != nil {
175 | return thinkgo.Text("Invalid parameters")
176 | }
177 | return next(request)
178 | })
179 | group.Get("edit", func(request *context.Request) *context.Response {
180 | return thinkgo.Text("admin user edit !")
181 | })
182 | }).Middleware(func(request *context.Request, next router.Closure) interface{} {
183 | if _, err := request.Input("user"); err != nil {
184 | return thinkgo.Text("Invalid parameters")
185 | }
186 | return next(request)
187 | })
188 | }).Middleware(func(request *context.Request, next router.Closure) interface{} {
189 | if _, err := request.Input("token"); err != nil {
190 | return thinkgo.Text("Invalid parameters")
191 | }
192 | return next(request)
193 | })
194 | ```
195 |
196 | ## Middleware
197 |
198 | Middleware provide a convenient mechanism for filtering HTTP requests entering your application. You only need to implement the `Middleware` interface.
199 |
200 | ```go
201 | route.Get("/foo", func(request *context.Request) *context.Response {
202 | return thinkgo.Text("Hello ThinkGo !")
203 | }).Middleware(func(request *context.Request, next router.Closure) interface{} {
204 | if _, err := request.Input("name"); err != nil {
205 | return thinkgo.Text("Invalid parameters")
206 | }
207 | return next(request)
208 | })
209 | ```
210 |
211 | #### Before Middleware
212 |
213 | Whether a middleware runs before or after a request depends on the middleware itself. For example, the following middleware would perform some task `before` the request is handled by the application:
214 |
215 | ```go
216 | func(request *context.Request, next router.Closure) interface{} {
217 |
218 | // Perform action
219 | // ...
220 |
221 | return next(request)
222 | }
223 | ```
224 |
225 | #### After Middleware
226 |
227 | However, this middleware would perform its task `after` the request is handled by the application:
228 |
229 | ```go
230 | func(request *context.Request, next router.Closure) interface{} {
231 |
232 | response := next(request)
233 |
234 | // Perform action
235 | // ...
236 |
237 | return response
238 | }
239 | ```
240 |
241 | ## Controller
242 |
243 | #### Basic Controller
244 |
245 | Below is an example of a basic controller class.
246 |
247 | ```go
248 | package controller
249 |
250 | import (
251 | "github.com/forgoer/thinkgo"
252 | "github.com/forgoer/thinkgo/context"
253 | )
254 |
255 | func Index(req *context.Request) *context.Response {
256 | return thinkgo.Text("Hello ThinkGo !")
257 | }
258 |
259 | ```
260 |
261 | You can define a route to this controller like so:
262 |
263 | ```go
264 | route.Get("/", controller.Index)
265 | ```
266 |
267 | #### Resource Controller
268 |
269 | This feature will be supported in a future release.
270 |
271 | ## HTTP Request
272 |
273 | #### Accessing The Request
274 |
275 | To obtain an instance of the current HTTP request via dependency injection
276 |
277 | ```go
278 | func Handler(req *context.Request) *context.Response {
279 | name := req.Input("name")
280 | }
281 | ```
282 |
283 | #### Dependency Injection & Route Parameters
284 |
285 | If your controller method is also expecting input from a route parameter you should list your route parameters after the request dependencies. For example, you can access your route parameter `name` like so:
286 |
287 | ```go
288 | route.Put("/user/{name}", func(req *context.Request, name string) *context.Response {
289 | //
290 | })
291 | ```
292 |
293 | #### Request Path & Method
294 |
295 | The `path` method returns the request's path information. So, if the incoming request is targeted at `http://domain.com/foo/bar`, the `path` method will return `foo/bar`:
296 |
297 | ```go
298 | uri := req.GetPath()
299 | ```
300 |
301 | The `method` method will return the HTTP verb for the request.
302 |
303 | ```go
304 | method := req.GetMethod();
305 | ```
306 |
307 | #### Retrieving Cookies From Requests
308 |
309 | ```go
310 | name, _ := request.Cookie("name")
311 | ```
312 |
313 | ## HTTP Response
314 |
315 | an HTTP Response Must implement the `*context.Response` interface
316 |
317 | #### Creating Responses
318 |
319 | a simple strings or json Response:
320 |
321 | ```go
322 | thinkgo.Text("Hello ThinkGo !")
323 |
324 | thinkgo.Json(map[string]string{
325 | "message": "pong",
326 | })
327 | ```
328 |
329 | #### Attaching Cookies To Responses
330 |
331 | ```go
332 | response.Cookie("name", "alice")
333 | ```
334 |
335 | #### Redirects
336 |
337 | ```go
338 | route.Get("/redirect", func(request *context.Request) *context.Response {
339 | return context.Redirect("https://www.google.com")
340 | })
341 | ```
342 |
343 | ## View
344 |
345 | Specify the `views` directory before running the app:
346 |
347 | ```go
348 | view.ParseGlob("/path/to/views/*")
349 | ```
350 |
351 | views are stored in the `views` directory, A simple view might look something like this:
352 |
353 | `views/layout.html` like this:
354 |
355 | ```html
356 | {{ define "layout" }}
357 |
358 |
359 |
360 |
361 | {{ .Title }}
362 |
363 |
364 | {{ template "content" .}}
365 |
366 |
367 | {{ end }}
368 | ```
369 |
370 | `views/tpl.html` like this:
371 |
372 | ```html
373 | {{ define "content" }}
374 | {{ .Message }}
375 | {{ end }}
376 | {{ template "layout" . }}
377 | ```
378 |
379 | we may return it using the `Render` function like so:
380 |
381 | ```go
382 | route.Get("/tpl", func(request *context.Request) *context.Response {
383 | data := map[string]interface{}{"Title": "ThinkGo", "Message": "Hello ThinkGo !"}
384 | return view.Render("tpl.html", data)
385 | })
386 | ```
387 |
388 | ## HTTP Session
389 |
390 | When the app starts, you need to register the session handler.
391 |
392 | ```go
393 | think.RegisterHandler(app.NewSessionHandler)
394 | ```
395 |
396 | `ThinkGo` ships with several great drivers out of the box:
397 |
398 | - cookie - sessions are stored in cookies
399 | - file - sessions are stored in files.
400 |
401 | #### Using The Session
402 |
403 | retrieving Data like this:
404 |
405 | ```go
406 | request.Session().Get("user")
407 | ```
408 |
409 | storing Data like this:
410 |
411 | ```go
412 | request.Session().Set("user", "alice")
413 | ```
414 |
415 | #### Adding Custom Session Drivers
416 |
417 | Your custom session driver should implement the `Handler`.
418 |
419 | ```go
420 | type Handler interface {
421 | Read(id string) string
422 | Write(id string, data string)
423 | }
424 | ```
425 |
426 | Once your driver has been implemented, you are ready to register it:
427 |
428 | ```go
429 | import "github.com/forgoer/thinkgo/session"
430 |
431 | session.Extend("my_session", MySessionHandler)
432 | ```
433 |
434 | ## Logging
435 |
436 | The logger provides the eight logging levels defined in [RFC 5424]( https://tools.ietf.org/html/rfc5424 ): **emergency**, **alert**, **critical**, **error**, **warning**, **notice**, **info** and **debug**.
437 |
438 | #### Basic Usage
439 |
440 | ```go
441 | import "github.com/forgoer/thinkgo/log"
442 |
443 | log.Debug("log with Debug")
444 | log.Info("log with Info")
445 | log.Notice("log with Notice")
446 | log.Warn("log with Warn")
447 | log.Error("log with Error")
448 | log.Crit("log with Crit")
449 | log.Alert("log with Alert")
450 | log.Emerg("log with Emerg")
451 | ```
452 |
453 | #### Log Storage
454 |
455 | Out of the box, ThinkGo supports writing log information to `daily` files, the `console`.
456 |
457 | For example, if you wish to use `daily` log files, you can do this:
458 |
459 | ```go
460 | import (
461 | "github.com/forgoer/thinkgo/log"
462 | "github.com/forgoer/thinkgo/log/handler"
463 | "github.com/forgoer/thinkgo/log/record"
464 | )
465 |
466 | fh := handler.NewFileHandler("path/to/thinkgo.log", record.INFO)
467 |
468 | log.GetLogger().PushHandler(fh)
469 | ```
470 |
471 | ## Cache
472 |
473 | ThinkGo Cache Currently supports redis, memory, and can customize the store adapter.
474 |
475 | #### Basic Usage
476 |
477 | ```go
478 | import (
479 | "github.com/forgoer/thinkgo/cache"
480 | "time"
481 | )
482 |
483 |
484 | var foo string
485 |
486 | // Create a cache with memory store
487 | c, _ := cache.Cache(cache.NewMemoryStore("thinkgo"))
488 |
489 | // Set the value
490 | c.Put("foo", "thinkgo", 10 * time.Minute)
491 |
492 | // Get the string associated with the key "foo" from the cache
493 | c.Get("foo", &foo)
494 |
495 | ```
496 |
497 | #### Retrieve & Store
498 |
499 | Sometimes you may wish to retrieve an item from the cache, but also store a default value if the requested item doesn't exist. For example, you may wish to retrieve all users from the cache or, if they don't exist, retrieve them from the callback and add them to the cache. You may do this using the `Remember` method:
500 |
501 | ```go
502 | var foo int
503 |
504 | cache.Remember("foo", &a, 1*time.Minute, func() interface{} {
505 | return "thinkgo"
506 | })
507 | ```
508 |
509 | refer to [ThinkGo Cache]( https://github.com/forgoer/thinkgo/tree/master/cache )
510 |
511 | ## ORM
512 |
513 | refer to [ThinkORM]( https://github.com/forgoer/thinkorm )
514 |
515 | ## License
516 |
517 | This project is licensed under the [Apache 2.0 license](LICENSE).
518 |
519 | ## Contact
520 |
521 | If you have any issues or feature requests, please contact us. PR is welcomed.
522 | - https://github.com/forgoer/thinkgo/issues
523 | - techqiang@gmail.com
524 |
--------------------------------------------------------------------------------
/cache/README.md:
--------------------------------------------------------------------------------
1 | # ThinkGo-Cache
2 |
3 | `ThinkGo-Cache` is a cache library for Golang,it currently supports redis, memory, and can customize the store adapter.
4 |
5 | ## Installation
6 |
7 | ```
8 | go get github.com/forgoer/thinkgo/cache
9 | ```
10 |
11 | ## Usage
12 |
13 | #### Basic Usage
14 |
15 | ```go
16 | import (
17 | "github.com/forgoer/thinkgo/cache"
18 | "time"
19 | )
20 |
21 |
22 | var foo string
23 |
24 | // Create a cache with memory store
25 | c, _ := cache.Cache(cache.NewMemoryStore("thinkgo"))
26 |
27 | // Set the value
28 | c.Put("foo", "thinkgo", 10 * time.Minute)
29 |
30 | // Get the string associated with the key "foo" from the cache
31 | c.Get("foo", &foo)
32 |
33 | ```
34 |
35 | #### Retrieve & Store
36 |
37 | Sometimes you may wish to retrieve an item from the cache, but also store a default value if the requested item doesn't exist. For example, you may wish to retrieve all users from the cache or, if they don't exist, retrieve them from the callback and add them to the cache. You may do this using the `Remember` method:
38 |
39 | ```go
40 | var foo int
41 |
42 | cache.Remember("foo", &a, 1*time.Minute, func() interface{} {
43 | return "thinkgo"
44 | })
45 | ```
46 |
47 | ## License
48 |
49 | This project is licensed under the `Apache 2.0 license`.
--------------------------------------------------------------------------------
/cache/cache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | )
7 |
8 | var adapters = make(map[string]Store)
9 |
10 | // Register Register a cache adapter available by the adapter name.
11 | func Register(name string, adapter Store) error {
12 | if adapter == nil {
13 | return errors.New("cache: Register adapter is nil")
14 | }
15 | if _, ok := adapters[name]; ok {
16 | return errors.New("cache: Register called twice for adapter " + name)
17 | }
18 | adapters[name] = adapter
19 | return nil
20 | }
21 |
22 | // NewCache Create a new cache by adapter name.
23 | func Cache(adapter interface{}) (*Repository, error) {
24 | var store Store
25 | switch adapter.(type) {
26 | case string:
27 | var ok bool
28 | store, ok = adapters[adapter.(string)]
29 | if !ok {
30 | err := fmt.Errorf("cache: unknown adapter name %q (forgot to import?)", adapter.(string))
31 | return nil, err
32 | }
33 | case Store:
34 | store = adapter.(Store)
35 | }
36 |
37 | return NewRepository(store), nil
38 | }
39 |
--------------------------------------------------------------------------------
/cache/cache_test.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 |
8 | "github.com/gomodule/redigo/redis"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | type Foo struct {
13 | Name string `json:"name"`
14 | Age int `json:"age"`
15 | }
16 |
17 | func testCache(t *testing.T, cache *Repository) {
18 | var a int
19 | var b string
20 | var c Foo
21 |
22 | cache.Clear()
23 |
24 | assert.Error(t, cache.Get("a", &a))
25 | assert.Error(t, cache.Get("b", &b))
26 |
27 | assert.NoError(t, cache.Put("a", 1, 10*time.Minute))
28 | assert.NoError(t, cache.Put("b", "thinkgo", 10*time.Minute))
29 |
30 | assert.True(t, cache.Has("a"))
31 | assert.True(t, cache.Has("b"))
32 |
33 | assert.NoError(t, cache.Get("a", &a))
34 | assert.Equal(t, a, 1)
35 | assert.NoError(t, cache.Get("b", &b))
36 | assert.Equal(t, b, "thinkgo")
37 |
38 | assert.NoError(t, cache.Pull("b", &b))
39 | assert.Equal(t, b, "thinkgo")
40 | assert.False(t, cache.Has("b"))
41 |
42 | assert.NoError(t, cache.Set("b", "think go", 10*time.Minute))
43 | assert.Error(t, cache.Add("b", "think go", 10*time.Minute))
44 |
45 | assert.True(t, cache.Has("b"))
46 | assert.NoError(t, cache.Forget("b"))
47 | assert.False(t, cache.Has("b"))
48 |
49 | assert.NoError(t, cache.Put("c", Foo{
50 | Name: "thinkgo",
51 | Age:100,
52 | }, 10*time.Minute))
53 | assert.NoError(t,cache.Get("c", &c))
54 | fmt.Println(c)
55 | assert.Equal(t, c.Name , "thinkgo")
56 | assert.Equal(t, c.Age , 100)
57 | assert.NoError(t, cache.Delete("c"))
58 | assert.False(t, cache.Has("c"))
59 |
60 | _, ok := cache.GetStore().(Store)
61 | assert.True(t, ok)
62 |
63 | assert.NoError(t, cache.Clear())
64 | assert.False(t, cache.Has("a"))
65 | assert.False(t, cache.Has("b"))
66 |
67 | assert.NoError(t, cache.Remember("a", &a, 1*time.Minute, func() interface{} {
68 | return 1000
69 | }))
70 |
71 | assert.Equal(t, a, 1000)
72 |
73 | assert.NoError(t,cache.Remember("b", &b, 1*time.Minute, func() interface{} {
74 | return "hello thinkgo"
75 | }))
76 |
77 | assert.Equal(t, b, "hello thinkgo")
78 | }
79 |
80 | func TestMemoryCache(t *testing.T) {
81 | Register("memory", NewMemoryStore("thinkgo"))
82 |
83 | cache, err := Cache("memory")
84 |
85 | if err != nil {
86 | t.Error(err)
87 | }
88 | testCache(t, cache)
89 | }
90 |
91 | func TestRedisCache(t *testing.T) {
92 | pool := &redis.Pool{
93 | MaxIdle: 5,
94 | MaxActive: 1000,
95 | IdleTimeout: 300 * time.Second,
96 | Wait: true,
97 | // Other pool configuration not shown in this example.
98 | Dial: func() (redis.Conn, error) {
99 | c, err := redis.Dial("tcp", "127.0.0.1:6379")
100 | if err != nil {
101 | return nil, err
102 | }
103 | // if _, err := c.Do("AUTH", "123456"); err != nil {
104 | // c.Close()
105 | // return nil, err
106 | // }
107 | return c, nil
108 | },
109 | }
110 |
111 | cache, err := Cache(NewRedisStore(pool, "thinkgo"))
112 | if err != nil {
113 | t.Error(err)
114 | }
115 | testCache(t, cache)
116 | }
--------------------------------------------------------------------------------
/cache/memory_store.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "reflect"
7 | "sync"
8 | "time"
9 | )
10 |
11 | type item struct {
12 | Object interface{}
13 | Expiration int64
14 | }
15 |
16 | // Expired Returns true if the item has expired.
17 | func (item item) Expired() bool {
18 | if item.Expiration < 0 {
19 | return false
20 | }
21 | return time.Now().UnixNano() > item.Expiration
22 | }
23 |
24 | type MemoryStore struct {
25 | prefix string
26 | items map[string]item
27 | mu sync.RWMutex
28 | cleanupTimer *time.Timer
29 | }
30 |
31 | // NewStore Create a memory cache store
32 | func NewMemoryStore(prefix string) *MemoryStore {
33 | s := &MemoryStore{
34 | items: make(map[string]item),
35 | }
36 | return s.SetPrefix(prefix)
37 | }
38 |
39 | // Get get cached value by key.
40 | // func (s *Store) Get(key string) (interface{}, error) {
41 | func (s *MemoryStore) Get(key string, val interface{}) error {
42 | s.mu.RLock()
43 | defer s.mu.RUnlock()
44 |
45 | item, ok := s.items[s.prefix+key]
46 | if !ok {
47 | return errors.New("not found")
48 | }
49 |
50 | if item.Expired() {
51 | return errors.New("expired")
52 | }
53 |
54 | rv := reflect.ValueOf(val)
55 | if rv.Kind() != reflect.Ptr || rv.IsNil() {
56 | return errors.New("invalid unmarshal")
57 | }
58 |
59 | rv = rv.Elem()
60 |
61 | rv.Set(reflect.ValueOf(item.Object))
62 |
63 | return nil
64 | }
65 |
66 | // Put set cached value with key and expire time.
67 | func (s *MemoryStore) Put(key string, val interface{}, timeout time.Duration) error {
68 | var e int64 = -1
69 | if timeout >= 0 {
70 | e = time.Now().Add(timeout).UnixNano()
71 | }
72 |
73 | s.mu.RLock()
74 | defer s.mu.RUnlock()
75 |
76 | s.items[s.prefix+key] = item{
77 | Object: val,
78 | Expiration: e,
79 | }
80 |
81 | if e >= 0 {
82 | s.DeleteExpired()
83 | }
84 |
85 | return nil
86 | }
87 |
88 | // Increment the value of an item in the cache.
89 | func (s *MemoryStore) Increment(key string, value ...int) (int, error) {
90 | s.mu.RLock()
91 | defer s.mu.RUnlock()
92 |
93 | var by = 1
94 | if len(value) > 0 {
95 | by = value[0]
96 | }
97 |
98 | exist, ok := s.items[s.prefix+key]
99 | if !ok {
100 | s.items[s.prefix+key] = item{
101 | Object: 1 + by,
102 | }
103 | } else {
104 | by = exist.Object.(int) + by
105 | exist.Object = by
106 | s.items[s.prefix+key] = exist
107 | }
108 |
109 | return by, nil
110 | }
111 |
112 | // Decrement the value of an item in the cache.
113 | func (s *MemoryStore) Decrement(key string, value ...int) (int, error) {
114 | s.mu.RLock()
115 | defer s.mu.RUnlock()
116 |
117 | var by = 1
118 | if len(value) > 0 {
119 | by = value[0]
120 | }
121 |
122 | exist, ok := s.items[s.prefix+key]
123 | if !ok {
124 | s.items[s.prefix+key] = item{
125 | Object: 0 - by,
126 | }
127 | } else {
128 | by = exist.Object.(int) - by
129 | exist.Object = by
130 | s.items[s.prefix+key] = exist
131 | }
132 |
133 | return by, nil
134 | }
135 |
136 | // Forever Store an item in the cache indefinitely.
137 | func (s *MemoryStore) Forever(key string, val interface{}) error {
138 | return s.Put(key, val, 0)
139 | }
140 |
141 | // Exist check cache's existence in memory.
142 | func (s *MemoryStore) Exist(key string) bool {
143 | s.mu.RLock()
144 | defer s.mu.RUnlock()
145 |
146 | item, ok := s.items[s.prefix+key]
147 |
148 | if item.Expired() {
149 | return false
150 | }
151 |
152 | return ok
153 | }
154 |
155 | // Expire set value expire time.
156 | func (s *MemoryStore) Expire(key string, timeout time.Duration) error {
157 | var e int64 = -1
158 | if timeout >= 0 {
159 | e = time.Now().Add(timeout).UnixNano()
160 | }
161 |
162 | s.mu.RLock()
163 | defer s.mu.RUnlock()
164 |
165 | if !s.Exist(key) {
166 | return errors.New("key not exist")
167 | }
168 |
169 | item := s.items[s.prefix+key]
170 | item.Expiration = e
171 | s.items[s.prefix+key] = item
172 |
173 | if e >= 0 {
174 | s.DeleteExpired()
175 | }
176 |
177 | return nil
178 | }
179 |
180 | // Forget Remove an item from the cache.
181 | func (s *MemoryStore) Forget(key string) error {
182 | delete(s.items, s.prefix+key)
183 | return nil
184 | }
185 |
186 | // Remove all items from the cache.
187 | func (s *MemoryStore) Flush() error {
188 | s.mu.RLock()
189 | defer s.mu.RUnlock()
190 |
191 | s.items = map[string]item{}
192 |
193 | return nil
194 | }
195 |
196 | func (s *MemoryStore) Tags(names ...string) Store {
197 | // tags not be supported
198 | return s
199 | }
200 |
201 | func (s *MemoryStore) TTL(key string) (int64, error) {
202 | s.mu.RLock()
203 | defer s.mu.RUnlock()
204 |
205 | item, ok := s.items[s.prefix+key]
206 | if !ok {
207 | return 0, errors.New("not found")
208 | }
209 |
210 | if item.Expired() {
211 | return 0, errors.New("not found")
212 | }
213 |
214 | return item.Expiration - time.Now().UnixNano(), nil
215 | }
216 |
217 | // GetPrefix Get the cache key prefix.
218 | func (s *MemoryStore) GetPrefix() string {
219 | return s.prefix
220 | }
221 |
222 | // SetPrefix Set the cache key prefix.
223 | func (s *MemoryStore) SetPrefix(prefix string) *MemoryStore {
224 | if len(prefix) != 0 {
225 | s.prefix = fmt.Sprintf("%s:", prefix)
226 | } else {
227 | s.prefix = ""
228 | }
229 | return s
230 | }
231 |
232 | // Delete all expired items from the cache.
233 | func (s *MemoryStore) DeleteExpired() {
234 | s.mu.RLock()
235 | defer s.mu.RUnlock()
236 |
237 | if s.cleanupTimer != nil {
238 | s.cleanupTimer.Stop()
239 | }
240 |
241 | smallestDuration := 0 * time.Nanosecond
242 | for key, item := range s.items {
243 | if item.Expiration < 0 {
244 | continue
245 | }
246 | // "Inlining" of expired
247 | if item.Expired() {
248 | delete(s.items, key)
249 | } else {
250 | // Find the item chronologically closest to its end-of-lifespan.
251 | sub := item.Expiration - time.Now().UnixNano()
252 |
253 | if smallestDuration == 0 {
254 | smallestDuration = time.Duration(sub) * time.Nanosecond
255 | } else {
256 | if time.Duration(sub)*time.Nanosecond < smallestDuration {
257 | smallestDuration = time.Duration(sub) * time.Nanosecond
258 | }
259 | }
260 | }
261 | }
262 |
263 | if smallestDuration > 0 {
264 | s.cleanupTimer = time.AfterFunc(smallestDuration, func() {
265 | go s.DeleteExpired()
266 | })
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/cache/memory_store_test.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "errors"
5 | "testing"
6 | "time"
7 | )
8 |
9 | type CacheUser struct {
10 | Name string
11 | Age int
12 | }
13 |
14 | func getMemoryStore() *MemoryStore {
15 | return NewMemoryStore("cache")
16 | }
17 |
18 | func TestMemoryStore(t *testing.T) {
19 | s := getMemoryStore()
20 | var a int
21 | var b string
22 | var c CacheUser
23 |
24 | err := s.Get("a", &a)
25 | if err == nil {
26 | t.Error("Getting A found value that shouldn't exist:", a)
27 | }
28 |
29 | err = s.Get("b", &b)
30 | if err == nil {
31 | t.Error("Getting B found value that shouldn't exist:", b)
32 | }
33 |
34 | s.Put("a", 1, 10*time.Minute)
35 | s.Put("b", "thinkgo", 2*time.Minute)
36 |
37 | err = s.Get("a", &a)
38 | if err != nil {
39 | t.Error(err)
40 | }
41 |
42 | if a != 1 {
43 | t.Error("Expect: ", 1)
44 | }
45 |
46 | err = s.Get("b", &b)
47 | if err != nil {
48 | t.Error(err)
49 | }
50 |
51 | if b != "thinkgo" {
52 | t.Error("Expect: ", "thinkgo")
53 | }
54 |
55 | err = s.Put(
56 | "user", CacheUser{
57 | Name: "alice",
58 | Age: 16,
59 | },
60 | 10*time.Minute,
61 | )
62 | if err != nil {
63 | t.Error(err)
64 | }
65 |
66 | err = s.Get("user", &c)
67 | if err != nil {
68 | t.Error(err)
69 | }
70 |
71 | t.Logf("user:name=%s,age=%d", c.Name, c.Age)
72 | }
73 |
74 | func TestMemoryStoreDuration(t *testing.T) {
75 | s := getMemoryStore()
76 | var a int
77 |
78 | s.Put("a", 3, 20*time.Millisecond)
79 |
80 | <-time.After(21 * time.Millisecond)
81 | err := s.Get("a", &a)
82 | if err == nil {
83 | t.Error("Found a when it should have been automatically deleted")
84 | }
85 | }
86 |
87 | func TestMemoryStoreForgetAndExist(t *testing.T) {
88 | s := getMemoryStore()
89 | err := s.Put("forget", "Forget me", 10*time.Minute)
90 | if err != nil {
91 | t.Error(err)
92 | }
93 |
94 | exist := s.Exist("forget")
95 | if exist != true {
96 | t.Error(errors.New("Expect true"))
97 | }
98 |
99 | err = s.Forget("forget")
100 | if err != nil {
101 | t.Error(err)
102 | }
103 |
104 | exist = s.Exist("forget")
105 | if exist == true {
106 | t.Error(errors.New("Expect false"))
107 | }
108 | }
109 |
110 | func TestMemoryStoreFlush(t *testing.T) {
111 | s := getMemoryStore()
112 | err := s.Put("Flush", "Flush all", 10*time.Minute)
113 | if err != nil {
114 | t.Error(err)
115 | }
116 |
117 | exist := s.Exist("Flush")
118 | if exist != true {
119 | t.Error(errors.New("Expect true"))
120 | }
121 |
122 | err = s.Flush()
123 | if err != nil {
124 | t.Error(err)
125 | }
126 |
127 | exist = s.Exist("Flush")
128 | if exist == true {
129 | t.Error(errors.New("Expect false"))
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/cache/redis_store.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | "github.com/gomodule/redigo/redis"
10 | )
11 |
12 | // ReferenceKeyForever Forever reference key.
13 | const ReferenceKeyForever = "forever_ref"
14 |
15 | // ReferenceKeyStandard Standard reference key.
16 | const ReferenceKeyStandard = "standard_ref"
17 |
18 | type RedisStore struct {
19 | pool *redis.Pool // redis connection pool
20 | tagSet *TagSet
21 | prefix string
22 | }
23 |
24 | // NewRedisStore Create a redis cache store
25 | func NewRedisStore(pool *redis.Pool, prefix string) *RedisStore {
26 | s := RedisStore{}
27 | return s.SetPool(pool).SetPrefix(prefix)
28 | }
29 |
30 | // Get get cached value by key.
31 | func (s *RedisStore) Get(key string, val interface{}) error {
32 | c := s.pool.Get()
33 | defer c.Close()
34 |
35 | b, err := redis.Bytes(c.Do("GET", s.prefix+key))
36 | if err != nil {
37 | return err
38 | }
39 |
40 | return json.Unmarshal(b, val)
41 | }
42 |
43 | // Put set cached value with key and expire time.
44 | func (s *RedisStore) Put(key string, val interface{}, timeout time.Duration) error {
45 | b, err := json.Marshal(val)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | err = s.pushStandardKeys(key)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | c := s.pool.Get()
56 | defer c.Close()
57 | _, err = c.Do("SETEX", s.prefix+key, int64(timeout/time.Second), string(b))
58 | return err
59 | }
60 |
61 | // Increment the value of an item in the cache.
62 | func (s *RedisStore) Increment(key string, value ...int) (int, error) {
63 | err := s.pushStandardKeys(key)
64 | if err != nil {
65 | return 0, err
66 | }
67 |
68 | c := s.pool.Get()
69 | defer c.Close()
70 |
71 | var by = 1
72 | if len(value) > 0 {
73 | by = value[0]
74 | }
75 |
76 | return redis.Int(c.Do("INCRBY", s.prefix+key, by))
77 | }
78 |
79 | // Decrement the value of an item in the cache.
80 | func (s *RedisStore) Decrement(key string, value ...int) (int, error) {
81 | err := s.pushStandardKeys(key)
82 | if err != nil {
83 | return 0, err
84 | }
85 |
86 | c := s.pool.Get()
87 | defer c.Close()
88 |
89 | var by = 1
90 | if len(value) > 0 {
91 | by = value[0]
92 | }
93 |
94 | return redis.Int(c.Do("DECRBY", s.prefix+key, by))
95 | }
96 |
97 | // Forever Store an item in the cache indefinitely.
98 | func (s *RedisStore) Forever(key string, val interface{}) error {
99 | b, err := json.Marshal(val)
100 | if err != nil {
101 | return err
102 | }
103 |
104 | err = s.pushForeverKeys(key)
105 | if err != nil {
106 | return err
107 | }
108 |
109 | c := s.pool.Get()
110 | defer c.Close()
111 | _, err = c.Do("SET", s.prefix+key, string(b))
112 | return err
113 | }
114 |
115 | // Exist check cache's existence in redis.
116 | func (s *RedisStore) Exist(key string) bool {
117 | c := s.pool.Get()
118 | defer c.Close()
119 | v, err := redis.Bool(c.Do("EXISTS", s.prefix+key))
120 | if err != nil {
121 | return false
122 | }
123 | return v
124 | }
125 |
126 | // Expire set value expire time.
127 | func (s *RedisStore) Expire(key string, timeout time.Duration) error {
128 | c := s.pool.Get()
129 | defer c.Close()
130 | _, err := c.Do("EXPIRE", s.prefix+key, int64(timeout/time.Second))
131 |
132 | return err
133 | }
134 |
135 | // Forget Remove an item from the cache.
136 | func (s *RedisStore) Forget(key string) error {
137 | c := s.pool.Get()
138 | defer c.Close()
139 | _, err := c.Do("DEL", s.prefix+key)
140 | return err
141 | }
142 |
143 | // Remove all items from the cache.
144 | func (s *RedisStore) Flush() error {
145 | if s.tagSet != nil {
146 | err := s.deleteForeverKeys()
147 | if err != nil {
148 | return err
149 | }
150 | err = s.deleteStandardKeys()
151 | if err != nil {
152 | return err
153 | }
154 | err = s.tagSet.Reset()
155 | if err != nil {
156 | return err
157 | }
158 | return nil
159 | }
160 |
161 | return s.FlushByPrefix("")
162 | }
163 |
164 | func (s *RedisStore) FlushByPrefix(prefix string) error {
165 | c := s.pool.Get()
166 | defer c.Close()
167 |
168 | var err error
169 | iter := 0
170 | keys := []string{}
171 | pattern := s.prefix + prefix + "*"
172 | for {
173 | arr, err := redis.Values(c.Do("SCAN", iter, "MATCH", pattern))
174 | if err != nil {
175 | return err
176 | }
177 |
178 | iter, _ = redis.Int(arr[0], nil)
179 | k, _ := redis.Strings(arr[1], nil)
180 | keys = append(keys, k...)
181 |
182 | if iter == 0 {
183 | break
184 | }
185 | }
186 |
187 | length := len(keys)
188 | if length == 0 {
189 | return nil
190 | }
191 |
192 | var keysChunk []interface{}
193 | for i, key := range keys {
194 | keysChunk = append(keysChunk, key)
195 | if i == length-1 || len(keysChunk) == 1000 {
196 | _, err = c.Do("DEL", keysChunk...)
197 | if err != nil {
198 | return err
199 | }
200 | keysChunk = nil
201 | }
202 | }
203 |
204 | return nil
205 | }
206 |
207 | func (s *RedisStore) Tags(names ...string) Store {
208 | if len(names) == 0 {
209 | return s
210 | }
211 | ss := s.clone()
212 | ss.tagSet = NewTagSet(s, names)
213 |
214 | return ss
215 | }
216 |
217 | func (s *RedisStore) TTL(key string) (int64, error) {
218 | c := s.pool.Get()
219 | defer c.Close()
220 |
221 | return redis.Int64(c.Do("TTL", s.prefix+key))
222 | }
223 |
224 | // SetPool Get the redis pool.
225 | func (s *RedisStore) SetPool(pool *redis.Pool) *RedisStore {
226 | s.pool = pool
227 | return s
228 | }
229 |
230 | // GetPrefix Get the cache key prefix.
231 | func (s *RedisStore) GetPrefix() string {
232 | return s.prefix
233 | }
234 |
235 | // SetPrefix Set the cache key prefix.
236 | func (s *RedisStore) SetPrefix(prefix string) *RedisStore {
237 | if len(prefix) != 0 {
238 | s.prefix = fmt.Sprintf("%s:", prefix)
239 | } else {
240 | s.prefix = ""
241 | }
242 | return s
243 | }
244 |
245 | func (s *RedisStore) clone() *RedisStore {
246 | return &RedisStore{
247 | pool: s.pool,
248 | prefix: s.prefix,
249 | }
250 | }
251 |
252 | func (s *RedisStore) pushStandardKeys(key string) error {
253 | return s.pushKeys(key, ReferenceKeyStandard)
254 | }
255 |
256 | func (s *RedisStore) pushForeverKeys(key string) error {
257 | return s.pushKeys(key, ReferenceKeyForever)
258 | }
259 |
260 | func (s *RedisStore) pushKeys(key, reference string) error {
261 | if s.tagSet == nil {
262 | return nil
263 | }
264 |
265 | namespace, err := s.tagSet.GetNamespace()
266 | if err != nil {
267 | return err
268 | }
269 |
270 | fullKey := s.prefix + key
271 | segments := strings.Split(namespace, "|")
272 |
273 | c := s.pool.Get()
274 | defer c.Close()
275 | for _, segment := range segments {
276 | _, err = c.Do("SADD", s.referenceKey(segment, reference), fullKey)
277 | if err != nil {
278 | return err
279 | }
280 | }
281 | return nil
282 | }
283 |
284 | func (s *RedisStore) deleteStandardKeys() error {
285 | return s.deleteKeysByReference(ReferenceKeyStandard)
286 | }
287 |
288 | func (s *RedisStore) deleteForeverKeys() error {
289 | return s.deleteKeysByReference(ReferenceKeyForever)
290 | }
291 |
292 | func (s *RedisStore) deleteKeysByReference(reference string) error {
293 | if s.tagSet == nil {
294 | return nil
295 | }
296 |
297 | namespace, err := s.tagSet.GetNamespace()
298 | if err != nil {
299 | return err
300 | }
301 | segments := strings.Split(namespace, "|")
302 | c := s.pool.Get()
303 | defer c.Close()
304 |
305 | for _, segment := range segments {
306 | segment = s.referenceKey(segment, reference)
307 | err = s.deleteKeys(segment)
308 | if err != nil {
309 | return err
310 | }
311 | _, err = c.Do("DEL", segment)
312 | if err != nil {
313 | return err
314 | }
315 | }
316 |
317 | return nil
318 | }
319 |
320 | func (s *RedisStore) deleteKeys(referenceKey string) error {
321 | c := s.pool.Get()
322 | defer c.Close()
323 | keys, err := redis.Strings(c.Do("SMEMBERS", referenceKey))
324 | if err != nil {
325 | return err
326 | }
327 | var length = len(keys)
328 | if length == 0 {
329 | return nil
330 | }
331 |
332 | var keysChunk []interface{}
333 | for i, key := range keys {
334 | keysChunk = append(keysChunk, key)
335 | if i == length-1 || len(keysChunk) == 1000 {
336 | _, err = c.Do("DEL", keysChunk...)
337 | if err != nil {
338 | return err
339 | }
340 | keysChunk = nil
341 | }
342 | }
343 |
344 | return nil
345 | }
346 |
347 | func (s *RedisStore) referenceKey(segment, suffix string) string {
348 | return s.prefix + segment + ":" + suffix
349 | }
350 |
--------------------------------------------------------------------------------
/cache/redis_store_test.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "errors"
5 | "testing"
6 | "time"
7 |
8 | "github.com/gomodule/redigo/redis"
9 | )
10 |
11 | func GetPool() *redis.Pool {
12 | return &redis.Pool{
13 | MaxIdle: 5,
14 | MaxActive: 1000,
15 | IdleTimeout: 300 * time.Second,
16 | Wait: true,
17 | // Other pool configuration not shown in this example.
18 | Dial: func() (redis.Conn, error) {
19 | c, err := redis.Dial("tcp", "10.0.41.242:6379")
20 | if err != nil {
21 | return nil, err
22 | }
23 | if _, err := c.Do("AUTH", "abc-123"); err != nil {
24 | c.Close()
25 | return nil, err
26 | }
27 | if _, err := c.Do("SELECT", 0); err != nil {
28 | c.Close()
29 | return nil, err
30 | }
31 | return c, nil
32 | },
33 | }
34 | }
35 |
36 | func getRedisStore() *RedisStore {
37 | pool := GetPool()
38 | return NewRedisStore(pool, "cache")
39 | }
40 |
41 | func TestRedisStoreInt(t *testing.T) {
42 | s := getRedisStore()
43 | err := s.Put("int", 9811, 10*time.Minute)
44 | if err != nil {
45 | t.Error(err)
46 | }
47 |
48 | var v int
49 |
50 | err = s.Get("int", &v)
51 | if err != nil {
52 | t.Error(err)
53 | }
54 |
55 | t.Logf("int:%d", v)
56 | }
57 |
58 | func TestRedisStoreString(t *testing.T) {
59 | s := getRedisStore()
60 | err := s.Put("str", "this is a string", 10*time.Minute)
61 | if err != nil {
62 | t.Error(err)
63 | }
64 |
65 | var str string
66 |
67 | err = s.Get("str", &str)
68 | if err != nil {
69 | t.Error(err)
70 | }
71 |
72 | t.Logf("str:%s", str)
73 | }
74 |
75 | func TestStoreStruct(t *testing.T) {
76 | s := getRedisStore()
77 | err := s.Put(
78 | "user", CacheUser{
79 | Name: "alice",
80 | Age: 16,
81 | },
82 | 10*time.Minute,
83 | )
84 | if err != nil {
85 | t.Error(err)
86 | }
87 |
88 | user := &CacheUser{}
89 |
90 | err = s.Get("user", user)
91 | if err != nil {
92 | t.Error(err)
93 | }
94 |
95 | t.Logf("user:name=%s,age=%d", user.Name, user.Age)
96 | }
97 |
98 | func TestRedisStoreForgetAndExist(t *testing.T) {
99 | s := getRedisStore()
100 | err := s.Put("forget", "Forget me", 10*time.Minute)
101 | if err != nil {
102 | t.Error(err)
103 | }
104 |
105 | exist := s.Exist("forget")
106 | if exist != true {
107 | t.Error(errors.New("Expect true"))
108 | }
109 |
110 | err = s.Forget("forget")
111 | if err != nil {
112 | t.Error(err)
113 | }
114 |
115 | exist = s.Exist("forget")
116 | if exist == true {
117 | t.Error(errors.New("Expect false"))
118 | }
119 | }
120 |
121 | func TestRedisStoreFlush(t *testing.T) {
122 | s := getRedisStore()
123 | err := s.Put("Flush", "Flush all", 10*time.Minute)
124 | if err != nil {
125 | t.Error(err)
126 | }
127 |
128 | exist := s.Exist("Flush")
129 | if exist != true {
130 | t.Error(errors.New("Expect true"))
131 | }
132 |
133 | err = s.Flush()
134 | if err != nil {
135 | t.Error(err)
136 | }
137 |
138 | exist = s.Exist("Flush")
139 | if exist == true {
140 | t.Error(errors.New("Expect false"))
141 | }
142 | }
143 |
144 | func TestRedisStore_Tags(t *testing.T) {
145 | cache := getRedisStore()
146 | key := "john"
147 | value := "LosAngeles"
148 | err := cache.Tags("people", "artists").Put(key, value, time.Hour)
149 | if err != nil {
150 | t.Fatal(err)
151 | }
152 | var val1 string
153 | cache.Get(key, &val1)
154 | if value != val1 {
155 | t.Errorf("%s != %s", value, val1)
156 | }
157 |
158 | var val2 string
159 | cache.Tags("people").Get(key, &val2)
160 | if value != val2 {
161 | t.Errorf("%s != %s", value, val2)
162 | }
163 |
164 | var val3 string
165 | cache.Tags("artists").Get(key, &val3)
166 | if value != val3 {
167 | t.Errorf("%s != %s", value, val3)
168 | }
169 |
170 | cache.Tags("people").Put("bob", "NewYork", time.Hour)
171 |
172 | err = cache.Tags("artists").Flush()
173 | if err != nil {
174 | t.Fatal(err)
175 | }
176 |
177 | err = cache.Tags("artists").Get(key, &val1)
178 | if err == nil {
179 | t.Fatal("err should not be nil")
180 | }
181 |
182 | cache.Tags("people").Get("bob", &val2)
183 | if "NewYork" != val2 {
184 | t.Errorf("%s != %s", "NewYork", val2)
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/cache/repository.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "time"
5 |
6 | "errors"
7 | )
8 |
9 | type Repository struct {
10 | store Store
11 | }
12 |
13 | func NewRepository(store Store) *Repository {
14 | s := &Repository{
15 | store: store,
16 | }
17 | return s
18 | }
19 |
20 | // Has Determine if an item exists in the cache.
21 | func (r *Repository) Has(key string) bool {
22 | return r.store.Exist(key)
23 | }
24 |
25 | // Get Retrieve an item from the cache by key.
26 | func (r *Repository) Get(key string, val interface{}) error {
27 | err := r.store.Get(key, val)
28 | return err
29 | }
30 |
31 | // Pull Retrieve an item from the cache and delete it.
32 | func (r *Repository) Pull(key string, val interface{}) error {
33 | err := r.store.Get(key, val)
34 | if err != nil {
35 | return err
36 | }
37 | r.store.Forget(key)
38 | return nil
39 | }
40 |
41 | // Put Store an item in the cache.
42 | func (r *Repository) Put(key string, val interface{}, timeout time.Duration) error {
43 | return r.store.Put(key, val, timeout)
44 | }
45 |
46 | // Set Store an item in the cache.
47 | func (r *Repository) Set(key string, val interface{}, timeout time.Duration) error {
48 | return r.Put(key, val, timeout)
49 | }
50 |
51 | // Add Store an item in the cache if the key does not exist.
52 | func (r *Repository) Add(key string, val interface{}, timeout time.Duration) error {
53 | if r.store.Exist(key) {
54 | return errors.New("the key already exists:" + key)
55 | }
56 | return r.store.Put(key, val, timeout)
57 | }
58 |
59 | // Increment the value of an item in the cache.
60 | func (r *Repository) Increment(key string, value ...int) (int, error) {
61 | return r.store.Increment(key, value...)
62 | }
63 |
64 | // Decrement the value of an item in the cache.
65 | func (r *Repository) Decrement(key string, value ...int) (int, error) {
66 | return r.store.Decrement(key, value...)
67 | }
68 |
69 | // Expire set value expire time.
70 | func (r *Repository) Expire(key string, timeout time.Duration) error {
71 | return r.store.Expire(key, timeout)
72 | }
73 |
74 | // Remember Get an item from the cache, or store the default value.
75 | func (r *Repository) Remember(key string, val interface{}, timeout time.Duration, callback func() interface{}) error {
76 | err := r.Get(key, val)
77 | if err == nil {
78 | return nil
79 | }
80 |
81 | value := callback()
82 | if err, ok := value.(error); ok {
83 | return err
84 | }
85 |
86 | r.Put(key, value, timeout)
87 |
88 | return r.Get(key, val)
89 | }
90 |
91 | // Forget Remove an item from the cache.
92 | func (r *Repository) Forget(key string) error {
93 | return r.store.Forget(key)
94 | }
95 |
96 | // Delete Alias for the "Delete" method.
97 | func (r *Repository) Delete(key string) error {
98 | return r.Forget(key)
99 | }
100 |
101 | // Clear Remove all items from the cache.
102 | func (r *Repository) Clear() error {
103 | return r.store.Flush()
104 | }
105 |
106 | // Tags Begin executing a new tags operation if the store supports it.
107 | func (r *Repository) Tags(names ...string) *Repository {
108 | return NewRepository(r.store.Tags(names...))
109 | }
110 |
111 | // TTL get the ttl of the key.
112 | func (r *Repository) TTL(key string) (int64, error) {
113 | return r.store.TTL(key)
114 | }
115 |
116 | // GetStore Get the cache store implementation.
117 | func (r *Repository) GetStore() Store {
118 | return r.store
119 | }
120 |
--------------------------------------------------------------------------------
/cache/store.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Store interface {
8 | // Get get cached value by key.
9 | Get(key string, val interface{}) error
10 |
11 | // Put set cached value with key and expire time.
12 | Put(key string, val interface{}, timeout time.Duration) error
13 |
14 | // Increment the value of an item in the cache.
15 | Increment(key string, value ...int) (int, error)
16 |
17 | // Decrement the value of an item in the cache.
18 | Decrement(key string, value ...int) (int, error)
19 |
20 | // Forever Store an item in the cache indefinitely.
21 | Forever(key string, val interface{}) error
22 |
23 | // Exist check cache's existence in redis.
24 | Exist(key string) bool
25 |
26 | // Expire set value expire time.
27 | Expire(key string, timeout time.Duration) error
28 |
29 | // Forget Remove an item from the cache.
30 | Forget(key string) error
31 |
32 | // Flush Remove all items from the cache.
33 | Flush() error
34 |
35 | Tags(names ...string) Store
36 |
37 | // TTL get the ttl of the key.
38 | TTL(key string) (int64, error)
39 | }
40 |
--------------------------------------------------------------------------------
/cache/tag_set.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | type TagSet struct {
8 | names []string
9 | store Store
10 | }
11 |
12 | func NewTagSet(store Store, names []string) *TagSet {
13 | return &TagSet{names: names, store: store}
14 | }
15 |
16 | func (t *TagSet) Reset() error {
17 | for _, name := range t.names {
18 | _, err := t.ResetTag(name)
19 | if err != nil {
20 | return err
21 | }
22 | }
23 |
24 | return nil
25 | }
26 |
27 | func (t *TagSet) ResetTag(name string) (string, error) {
28 | id := Sha1(name)
29 |
30 | err := t.store.Forever(t.TagKey(name), id)
31 |
32 | return id, err
33 | }
34 |
35 | func (t *TagSet) GetNamespace() (string, error) {
36 | var err error
37 | var names = make([]string, len(t.names))
38 | for i, name := range t.names {
39 | name, err = t.TagId(name)
40 | if err != nil {
41 | return "", err
42 | }
43 | names[i] = name
44 | }
45 | return strings.Join(names, "|"), nil
46 | }
47 |
48 | func (t *TagSet) TagId(name string) (string, error) {
49 | var id string
50 | tagKey := t.TagKey(name)
51 | err := t.store.Get(tagKey, &id)
52 | if err != nil {
53 | return t.ResetTag(name)
54 | }
55 |
56 | return id, nil
57 | }
58 |
59 | func (t *TagSet) TagKey(name string) string {
60 | return "tag:" + name + ":key"
61 | }
62 |
--------------------------------------------------------------------------------
/cache/utils.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "crypto/sha1"
5 | "fmt"
6 | )
7 |
8 | // Sha1 Calculate the sha1 hash of a string
9 | func Sha1(str string) string {
10 | h := sha1.New()
11 | _, _ = h.Write([]byte(str))
12 | return fmt.Sprintf("%x", h.Sum(nil))
13 | }
14 |
--------------------------------------------------------------------------------
/config/app.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type AppConfig struct {
4 | Name string
5 | Env string
6 | Debug bool
7 | }
8 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import "time"
4 |
5 | var App *AppConfig
6 | var Route *RouteConfig
7 | var View *ViewConfig
8 |
9 | //var Database *DatabaseConfig
10 | var Cookie *CookieConfig
11 | var Session *SessionConfig
12 |
13 | func init() {
14 | loadAppConfig()
15 | loadViewConfig()
16 | loadRouteConfig()
17 | loadCookieConfig()
18 | loadSessionConfig()
19 | }
20 |
21 | func loadAppConfig() {
22 | App = &AppConfig{
23 | Name: "ThinkGo",
24 | Env: "production",
25 | Debug: false,
26 | }
27 | }
28 |
29 | func loadRouteConfig() {
30 | Route = &RouteConfig{
31 | Static: map[string]string{
32 | "static": "public",
33 | "upload": "public",
34 | },
35 | }
36 | }
37 |
38 | func loadViewConfig() {
39 | View = &ViewConfig{
40 | Path: "view",
41 | }
42 | }
43 |
44 | func loadCookieConfig() {
45 | Cookie = &CookieConfig{
46 | Prefix: "",
47 | Expires: time.Hour * 4,
48 | Path: "/",
49 | Domain: "",
50 | MaxAge: 0,
51 | Secure: false,
52 | HttpOnly: true,
53 | }
54 | }
55 |
56 | func loadSessionConfig() {
57 | Session = &SessionConfig{
58 | Driver: "file",
59 | Lifetime: time.Hour * 4,
60 | Encrypt: false,
61 | Files: "temp/sessions",
62 | CookieName: "thinkgo_sessions",
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/config/cookie.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import "time"
4 |
5 | type CookieConfig struct {
6 | Prefix string
7 | Expires time.Duration
8 | Path string
9 | Domain string
10 | MaxAge int
11 | Secure bool
12 | HttpOnly bool
13 | }
14 |
--------------------------------------------------------------------------------
/config/database.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type DatabaseConfig struct {
4 | Connection string
5 | Connections map[string]Connection
6 | }
7 |
8 | type Connection struct {
9 | Driver string
10 | Host string
11 | Port string
12 | Database string
13 | Username string
14 | Password string
15 | Charset string
16 | Prefix string
17 | Engine string
18 | }
19 |
--------------------------------------------------------------------------------
/config/route.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type RouteConfig struct {
4 | Static map[string]string
5 | }
6 |
--------------------------------------------------------------------------------
/config/session.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import "time"
4 |
5 | type SessionConfig struct {
6 | //Default Session Driver
7 | Driver string
8 |
9 | //Session Cookie Name
10 | CookieName string
11 |
12 | //Session Lifetime
13 | Lifetime time.Duration
14 |
15 | //Session Encryption
16 | Encrypt bool
17 |
18 | //Session File Location
19 | Files string
20 | }
21 |
--------------------------------------------------------------------------------
/config/view.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type ViewConfig struct {
4 | Path string
5 | }
6 |
--------------------------------------------------------------------------------
/context/cookie.go:
--------------------------------------------------------------------------------
1 | package context
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "net/url"
7 | "time"
8 |
9 | "github.com/forgoer/thinkgo/config"
10 | )
11 |
12 | type CookieConfig struct {
13 | Prefix string
14 |
15 | Path string // optional
16 | Domain string // optional
17 | Expires time.Time // optional
18 | RawExpires string // for reading cookies only
19 |
20 | // MaxAge=0 means no 'Max-Age' attribute specified.
21 | // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
22 | // MaxAge>0 means Max-Age attribute present and given in seconds
23 | MaxAge int
24 | Secure bool
25 | HttpOnly bool
26 | Raw string
27 | Unparsed []string // Raw text of unparsed attribute-value pairs
28 | }
29 |
30 | type Cookie struct {
31 | Config *CookieConfig
32 | }
33 |
34 | func (c *Cookie) Set(name interface{}, params ...interface{}) (*http.Cookie, error) {
35 | var cookie *http.Cookie
36 |
37 | switch name.(type) {
38 | case *http.Cookie:
39 | cookie = name.(*http.Cookie)
40 | case string:
41 | if len(params) == 0 {
42 | return nil, errors.New("Invalid parameters for Cookie.")
43 | }
44 |
45 | value := params[0]
46 | if _, ok := value.(string); !ok {
47 | return nil, errors.New("Invalid parameters for Cookie.")
48 | }
49 | cookie = &http.Cookie{
50 | Name: c.Config.Prefix + name.(string),
51 | Value: url.QueryEscape(value.(string)),
52 | Path: c.Config.Path,
53 | Domain: c.Config.Domain,
54 | Expires: c.Config.Expires,
55 | MaxAge: c.Config.MaxAge,
56 | Secure: c.Config.Secure,
57 | HttpOnly: c.Config.HttpOnly,
58 | }
59 |
60 | if len(params) > 1 {
61 | maxAge := params[1]
62 | if _, ok := maxAge.(int); !ok {
63 | return nil, errors.New("Invalid parameters for Cookie.")
64 | }
65 | cookie.MaxAge = maxAge.(int)
66 | }
67 |
68 | if len(params) > 2 {
69 | path := params[2]
70 | if _, ok := path.(string); !ok {
71 | return nil, errors.New("Invalid parameters for Cookie.")
72 | }
73 | cookie.Path = path.(string)
74 | }
75 |
76 | if len(params) > 3 {
77 | domain := params[3]
78 | if _, ok := domain.(string); !ok {
79 | return nil, errors.New("Invalid parameters for Cookie.")
80 | }
81 | cookie.Domain = domain.(string)
82 | }
83 |
84 | if len(params) > 4 {
85 | secure := params[4]
86 | if _, ok := secure.(bool); !ok {
87 | return nil, errors.New("Invalid parameters for Cookie.")
88 | }
89 | cookie.Secure = secure.(bool)
90 | }
91 | default:
92 | return nil, errors.New("Invalid parameters for Cookie.")
93 | }
94 | return cookie, nil
95 | }
96 |
97 | func ParseCookieHandler() *Cookie {
98 | return &Cookie{
99 | Config: &CookieConfig{
100 | Prefix: config.Cookie.Prefix,
101 | Path: config.Cookie.Path,
102 | Domain: config.Cookie.Domain,
103 | Expires: time.Now().Add(config.Cookie.Expires),
104 | MaxAge: config.Cookie.MaxAge,
105 | Secure: config.Cookie.Secure,
106 | HttpOnly: config.Cookie.HttpOnly,
107 | },
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/context/file.go:
--------------------------------------------------------------------------------
1 | package context
2 |
3 | import (
4 | "io"
5 | "mime/multipart"
6 | "os"
7 | "path"
8 | )
9 |
10 | type File struct {
11 | FileHeader *multipart.FileHeader
12 | }
13 |
14 | func (f *File) Move(directory string, name ...string) (bool, error) {
15 | src, err := f.FileHeader.Open()
16 | if err != nil {
17 | return false, err
18 | }
19 | defer src.Close()
20 |
21 | fname := f.FileHeader.Filename
22 |
23 | if len(name) > 0 {
24 | fname = name[0]
25 | }
26 |
27 | dst := path.Join(directory, fname)
28 |
29 | out, err := os.Create(dst)
30 | if err != nil {
31 | return false, err
32 | }
33 | defer out.Close()
34 |
35 | io.Copy(out, src)
36 |
37 | return true, nil
38 | }
39 |
--------------------------------------------------------------------------------
/context/request.go:
--------------------------------------------------------------------------------
1 | package context
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "io/ioutil"
7 | "net/http"
8 | "net/url"
9 | "strings"
10 | )
11 |
12 | //Request HTTP request
13 | type Request struct {
14 | Request *http.Request
15 | method string
16 | path string
17 | query map[string]string
18 | post map[string]string
19 | files map[string]*File
20 | session Session
21 | CookieHandler *Cookie
22 | }
23 |
24 | // NewRequest create a new HTTP request from *http.Request
25 | func NewRequest(req *http.Request) *Request {
26 |
27 | return &Request{
28 | Request: req,
29 | method: req.Method,
30 | path: req.URL.Path,
31 | query: parseQuery(req.URL.Query()),
32 | post: parsePost(req),
33 | }
34 | }
35 |
36 | // GetMethod get the request method.
37 | func (r *Request) GetMethod() string {
38 | return r.method
39 | }
40 |
41 | // GetPath get the request path.
42 | func (r *Request) GetPath() string {
43 | return r.path
44 | }
45 |
46 | // GetHttpRequest get Current *http.Request
47 | func (r *Request) GetHttpRequest() *http.Request {
48 | return r.Request
49 | }
50 |
51 | //IsMethod checks if the request method is of specified type.
52 | func (r *Request) IsMethod(m string) bool {
53 | return strings.ToUpper(m) == r.GetMethod()
54 | }
55 |
56 | // Query returns a query string item from the request.
57 | func (r *Request) Query(key string, value ...string) (string, error) {
58 | if v, ok := r.query[key]; ok {
59 | return v, nil
60 | }
61 | if len(value) > 0 {
62 | return value[0], nil
63 | }
64 | return "", errors.New("named query not present")
65 | }
66 |
67 | // Input returns a input item from the request.
68 | func (r *Request) Input(key string, value ...string) (string, error) {
69 | if v, ok := r.post[key]; ok {
70 | return v, nil
71 | }
72 |
73 | if v, ok := r.query[key]; ok {
74 | return v, nil
75 | }
76 |
77 | if len(value) > 0 {
78 | return value[0], nil
79 | }
80 | return "", errors.New("named input not present")
81 | }
82 |
83 | // Input returns a post item from the request.
84 | func (r *Request) Post(key string, value ...string) (string, error) {
85 | if v, ok := r.post[key]; ok {
86 | return v, nil
87 | }
88 |
89 | if len(value) > 0 {
90 | return value[0], nil
91 | }
92 | return "", errors.New("named post not present")
93 | }
94 |
95 | //Cookie Retrieve a cookie from the request.
96 | func (r *Request) Cookie(key string, value ...string) (string, error) {
97 | var err error
98 | key = r.CookieHandler.Config.Prefix + key
99 | cookie, err := r.Request.Cookie(key)
100 | if err == nil {
101 | c, _ := url.QueryUnescape(cookie.Value)
102 | return c, err
103 | }
104 | if len(value) > 0 {
105 | return value[0], nil
106 | }
107 | return "", err
108 | }
109 |
110 | // File returns a file from the request.
111 | func (r *Request) File(key string) (*File, error) {
112 | if f, ok := r.files[key]; ok {
113 | return f, nil
114 | }
115 |
116 | _, fh, err := r.Request.FormFile(key)
117 | if err != nil {
118 | return nil, err
119 | }
120 | r.files[key] = &File{fh}
121 |
122 | return r.files[key], nil
123 | }
124 |
125 | // AllFiles returns all files from the request.
126 | func (r *Request) AllFiles() (map[string]*File, error) {
127 | err := r.Request.ParseMultipartForm(32 << 20)
128 | if err != nil {
129 | return nil, err
130 | }
131 | if r.Request.MultipartForm != nil || r.Request.MultipartForm.File != nil {
132 | for key, fh := range r.Request.MultipartForm.File {
133 | r.files[key] = &File{fh[0]}
134 | }
135 | }
136 | return r.files, nil
137 | }
138 |
139 | // All get all of the input and query for the request.
140 | func (r *Request) All(keys ...string) map[string]string {
141 | all := mergeForm(r.query, r.post)
142 |
143 | if len(keys) == 0 {
144 | return all
145 | }
146 |
147 | result := make(map[string]string)
148 |
149 | for _, key := range keys {
150 | if v, ok := all[key]; ok {
151 | result[key] = v
152 | } else {
153 | result[key] = ""
154 | }
155 | }
156 |
157 | return result
158 | }
159 |
160 | //Only get a subset of the items from the input data.
161 | func (r *Request) Only(keys ...string) map[string]string {
162 | all := r.All()
163 |
164 | result := make(map[string]string)
165 |
166 | for _, key := range keys {
167 | if v, ok := all[key]; ok {
168 | result[key] = v
169 | }
170 | }
171 |
172 | return result
173 | }
174 |
175 | //Except Get all of the input except for a specified array of items.
176 | func (r *Request) Except(keys ...string) map[string]string {
177 | all := r.All()
178 |
179 | for _, key := range keys {
180 | if _, ok := all[key]; ok {
181 | delete(all, key)
182 | }
183 | }
184 |
185 | return all
186 | }
187 |
188 | // Has Determine if the request contains a given input item key.
189 | func (r *Request) Exists(keys ...string) bool {
190 | all := r.All()
191 |
192 | for _, key := range keys {
193 | if _, ok := all[key]; !ok {
194 | return false
195 | }
196 | }
197 |
198 | return true
199 | }
200 |
201 | // Filled Determine if the request contains a non-empty value for an input item.
202 | func (r *Request) Has(keys ...string) bool {
203 | all := r.All()
204 |
205 | for _, key := range keys {
206 | if _, ok := all[key]; !ok {
207 | return false
208 | }
209 | if len(all[key]) == 0 {
210 | return false
211 | }
212 | }
213 |
214 | return true
215 | }
216 |
217 | //Url get the URL (no query string) for the request.
218 | func (r *Request) Url() string {
219 | return r.Request.URL.Path
220 | }
221 |
222 | // FullUrl get the full URL for the request.
223 | func (r *Request) FullUrl() string {
224 | return r.Url() + "?" + r.Request.URL.RawQuery
225 | }
226 |
227 | // Path get the current path info for the request.
228 | func (r *Request) Path() string {
229 | return r.path
230 | }
231 |
232 | // Method get the current method for the request.
233 | func (r *Request) Method() string {
234 | return r.method
235 | }
236 |
237 | // GetContent Returns the request body content.
238 | func (r *Request) GetContent() ([]byte, error) {
239 | var body []byte
240 |
241 | if body == nil {
242 | body, err := ioutil.ReadAll(r.Request.Body)
243 | if err != nil {
244 | return nil, err
245 | }
246 | r.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
247 | }
248 |
249 | return body, nil
250 | }
251 |
252 | // Session get the session associated with the request.
253 | func (r *Request) Session() Session {
254 | return r.session
255 | }
256 |
257 | // Session set the session associated with the request.
258 | func (r *Request) SetSession(s Session) {
259 | r.session = s
260 | }
261 |
262 | func parseQuery(q url.Values) map[string]string {
263 | query := make(map[string]string)
264 | for k, v := range q {
265 | query[k] = v[0]
266 | }
267 | return query
268 | }
269 |
270 | func parsePost(r *http.Request) map[string]string {
271 | post := make(map[string]string)
272 |
273 | r.ParseForm()
274 | for k, v := range r.PostForm {
275 | post[k] = v[0]
276 | }
277 |
278 | r.ParseMultipartForm(32 << 20)
279 | if r.MultipartForm != nil {
280 | for k, v := range r.MultipartForm.Value {
281 | post[k] = v[0]
282 | }
283 | }
284 |
285 | return post
286 | }
287 |
288 | func mergeForm(slices ...map[string]string) map[string]string {
289 | r := make(map[string]string)
290 |
291 | for _, slice := range slices {
292 | for k, v := range slice {
293 | r[k] = v
294 | }
295 | }
296 |
297 | return r
298 | }
299 |
--------------------------------------------------------------------------------
/context/response.go:
--------------------------------------------------------------------------------
1 | package context
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | type Response struct {
8 | // Writer context.ResponseWriter
9 | contentType string
10 | charset string
11 | code int
12 | content string
13 | cookies map[string]*http.Cookie
14 | CookieHandler *Cookie
15 | Header *http.Header
16 | }
17 |
18 | // GetContentType sets the Content-Type on the response.
19 | func (r *Response) SetContentType(val string) *Response {
20 | r.contentType = val
21 | return r
22 | }
23 |
24 | // GetContentType sets the Charset on the response.
25 | func (r *Response) SetCharset(val string) *Response {
26 | r.charset = val
27 | return r
28 | }
29 |
30 | // SetCode sets the status code on the response.
31 | func (r *Response) SetCode(val int) *Response {
32 | r.code = val
33 | return r
34 | }
35 |
36 | // SetContent sets the content on the response.
37 | func (r *Response) SetContent(val string) *Response {
38 | r.content = val
39 | return r
40 | }
41 |
42 | // GetContentType get the Content-Type on the response.
43 | func (r *Response) GetContentType() string {
44 | return r.contentType
45 | }
46 |
47 | // GetContentType get the Charset on the response.
48 | func (r *Response) GetCharset() string {
49 | return r.charset
50 | }
51 |
52 | // GetCode get the response status code.
53 | func (r *Response) GetCode() int {
54 | return r.code
55 | }
56 |
57 | // GetCode get the response content.
58 | func (r *Response) GetContent() string {
59 | return r.content
60 | }
61 |
62 | // Cookie Add a cookie to the response.
63 | func (r *Response) Cookie(name interface{}, params ...interface{}) error {
64 | cookie, err := r.CookieHandler.Set(name, params...)
65 |
66 | if err != nil {
67 | if r.cookies == nil {
68 | r.cookies = make(map[string]*http.Cookie)
69 | }
70 | r.cookies[cookie.Name] = cookie
71 | }
72 |
73 | return err
74 | }
75 |
76 | // Send Sends HTTP headers and content.
77 | func (r *Response) Send(w http.ResponseWriter) {
78 | for _, cookie := range r.cookies {
79 | http.SetCookie(w, cookie)
80 | }
81 | for key, value := range *r.Header {
82 | for _, val := range value {
83 | w.Header().Add(key, val)
84 | }
85 | }
86 | w.Header().Set("Content-Type", r.GetContentType()+";"+" charset="+r.GetCharset())
87 | // r.Header.Write(w)
88 | w.WriteHeader(r.GetCode())
89 | w.Write([]byte(r.GetContent()))
90 | }
91 |
92 | // NewResponse Create a new HTTP Response
93 | func NewResponse() *Response {
94 | r := &Response{
95 | Header: &http.Header{},
96 | }
97 | r.SetCode(http.StatusOK)
98 | r.SetContentType("text/html")
99 | r.SetCharset("utf-8")
100 | r.CookieHandler = ParseCookieHandler()
101 | return r
102 | }
103 |
104 | // NotFoundResponse Create a new HTTP NotFoundResponse
105 | func NotFoundResponse() *Response {
106 | return NewResponse().SetCode(http.StatusNotFound).SetContent("Not Found")
107 | }
108 |
109 | // NotFoundResponse Create a new HTTP Error Response
110 | func ErrorResponse() *Response {
111 | return NewResponse().SetCode(http.StatusInternalServerError).SetContent("Server Error")
112 | }
113 |
114 | // Redirect Create a new HTTP Redirect Response
115 | func Redirect(to string) *Response {
116 | r := NewResponse().SetCode(http.StatusMovedPermanently)
117 | r.Header.Set("Location", to)
118 | return r
119 | }
120 |
--------------------------------------------------------------------------------
/context/session.go:
--------------------------------------------------------------------------------
1 | package context
2 |
3 | type Session interface {
4 | Get(name string, value ...interface{}) interface{}
5 |
6 | Set(name string, value interface{})
7 |
8 | All() map[string]interface{}
9 |
10 | Remove(name string) interface{}
11 |
12 | Forget(names ...string)
13 |
14 | Clear()
15 |
16 | Save()
17 | }
18 |
--------------------------------------------------------------------------------
/filesystem/fs.go:
--------------------------------------------------------------------------------
1 | package filesystem
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "os"
7 | )
8 |
9 | type JustFileSystem struct {
10 | fs http.FileSystem
11 | readDirBatchSize int
12 | }
13 |
14 | func NewFileFileSystem(root string, listDirectory bool) http.FileSystem {
15 | fs := http.Dir(root)
16 | if listDirectory {
17 | return fs
18 | }
19 | return &JustFileSystem{fs, 2}
20 | }
21 |
22 | func (fs JustFileSystem) Open(name string) (http.File, error) {
23 | f, err := fs.fs.Open(name)
24 | if err != nil {
25 | return nil, err
26 | }
27 | return neuteredStatFile{
28 | File: f,
29 | readDirBatchSize: fs.readDirBatchSize,
30 | }, nil
31 | }
32 |
33 | type neuteredStatFile struct {
34 | http.File
35 | readDirBatchSize int
36 | }
37 |
38 | func (e neuteredStatFile) Stat() (os.FileInfo, error) {
39 | s, err := e.File.Stat()
40 | if err != nil {
41 | return nil, err
42 | }
43 | if s.IsDir() {
44 | LOOP:
45 | for {
46 | fl, err := e.File.Readdir(e.readDirBatchSize)
47 | switch err {
48 | case io.EOF:
49 | break LOOP
50 | case nil:
51 | for _, f := range fl {
52 | if f.Name() == "index.html" {
53 | return s, err
54 | }
55 | }
56 | default:
57 | return nil, err
58 | }
59 | }
60 | return nil, os.ErrNotExist
61 | }
62 | return s, err
63 | }
64 |
--------------------------------------------------------------------------------
/filesystem/utils.go:
--------------------------------------------------------------------------------
1 | package filesystem
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "path"
7 | "path/filepath"
8 | "sync"
9 | "time"
10 | )
11 |
12 | var lock sync.RWMutex
13 |
14 | func Exists(p ...string) (bool, error) {
15 | _, err := os.Stat(path.Join(p...))
16 | if err == nil {
17 | return true, nil
18 | }
19 | if os.IsNotExist(err) {
20 | return false, nil
21 | }
22 | return false, err
23 | }
24 |
25 | func Get(path string) ([]byte, error) {
26 | lock.Lock()
27 | defer lock.Unlock()
28 |
29 | var b []byte
30 |
31 | f, err := os.OpenFile(path, os.O_RDWR, 0600)
32 | defer f.Close()
33 |
34 | if err != nil {
35 | return b, err
36 | }
37 |
38 | b, err = ioutil.ReadAll(f)
39 | if err != nil {
40 | return b, err
41 | }
42 | return b, nil
43 | }
44 |
45 | func Put(path string, data string) error {
46 | lock.Lock()
47 | defer lock.Unlock()
48 |
49 | dir := filepath.Dir(path)
50 | if ok, _ := Exists(dir); !ok {
51 | err := os.MkdirAll(dir, 0600)
52 | if err != nil {
53 | return err
54 | }
55 | }
56 |
57 | f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0600)
58 | defer f.Close()
59 |
60 | if err != nil {
61 | return err
62 | }
63 | _, err = f.WriteString(data)
64 | return err
65 | }
66 |
67 | func ModTime(path string) (time.Time, error) {
68 | var modTime time.Time
69 | fileInfo, err := os.Stat(path)
70 | if err != nil {
71 | return modTime, err
72 | }
73 | return fileInfo.ModTime(), nil
74 | }
75 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/forgoer/thinkgo
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/gomodule/redigo v1.8.8
7 | github.com/stretchr/testify v1.7.0
8 | )
9 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/gomodule/redigo v1.8.8 h1:f6cXq6RRfiyrOJEV7p3JhLDlmawGBVBBP1MggY8Mo4E=
4 | github.com/gomodule/redigo v1.8.8/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
8 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
9 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
14 |
--------------------------------------------------------------------------------
/helper/helper.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 | "strings"
9 | )
10 |
11 | func ThinkGoPath(args ...string) string {
12 | _, file, _, ok := runtime.Caller(0)
13 | if !ok {
14 | panic(errors.New("Can not load ThinkGo path info"))
15 | }
16 |
17 | dir := filepath.Dir(filepath.Dir(file)) + "/"
18 |
19 | if 1 == len(args) {
20 | dir = dir + strings.TrimLeft(args[0], "/")
21 | }
22 |
23 | return dir
24 | }
25 |
26 | func WorkPath(args ...string) string {
27 | dir, err := os.Getwd()
28 | if err != nil {
29 | panic(err)
30 | }
31 |
32 | if 1 == len(args) {
33 | dir = filepath.Join(dir, strings.TrimLeft(args[0], "/"))
34 | }
35 |
36 | return dir
37 | }
38 |
39 | func AppPath(args ...string) string {
40 |
41 | dir := WorkPath("app")
42 |
43 | if 1 == len(args) {
44 | dir = filepath.Join(dir, strings.TrimLeft(args[0], "/"))
45 | }
46 |
47 | return dir
48 | }
49 |
50 | func ConfigPath(args ...string) string {
51 | dir := WorkPath("config")
52 |
53 | if 1 == len(args) {
54 | dir = filepath.Join(dir, strings.TrimLeft(args[0], "/"))
55 | }
56 |
57 | return dir
58 | }
59 |
--------------------------------------------------------------------------------
/helper/utils.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "os"
5 | "strings"
6 | )
7 |
8 | func ParseAddr(addrs ...string) string {
9 | var addr = "0.0.0.0"
10 | var port = "9011"
11 | switch len(addrs) {
12 | case 0:
13 | if a := os.Getenv("THINKGO_ADDR"); a != "" {
14 | addr = a
15 | }
16 | if p := os.Getenv("THINKGO_PORT"); p != "" {
17 | port = p
18 | }
19 | case 1:
20 | strs := strings.Split(addrs[0], ":")
21 | if len(strs) > 0 && strs[0] != "" {
22 | addr = strs[0]
23 | }
24 | if len(strs) > 1 && strs[1] != "" {
25 | port = strs[1]
26 | }
27 | default:
28 |
29 | }
30 | return addr + ":" + port
31 | }
32 |
--------------------------------------------------------------------------------
/log/formatter/formatter.go:
--------------------------------------------------------------------------------
1 | package formatter
2 |
3 | import "github.com/forgoer/thinkgo/log/record"
4 |
5 | type Formatter interface {
6 | Format(r record.Record) string
7 | FormatBatch(rs []record.Record) string
8 | }
9 |
--------------------------------------------------------------------------------
/log/formatter/json_formatter.go:
--------------------------------------------------------------------------------
1 | package formatter
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/forgoer/thinkgo/log/record"
6 | )
7 |
8 | type JsonFormatter struct {
9 | }
10 |
11 | func NewJsonFormatter() *JsonFormatter {
12 | j := &JsonFormatter{}
13 | return j
14 | }
15 |
16 | func (f *JsonFormatter) Format(r record.Record) string {
17 | normalized := make(map[string]interface{})
18 |
19 | normalized["message"] = r.Message
20 | normalized["level"] = r.Level
21 | normalized["level_name"] = r.LevelName
22 | normalized["channel"] = r.Channel
23 | normalized["datetime"] = r.Datetime.Local().Format("2006-01-02 15:04:05.000")
24 |
25 | output, _ := json.Marshal(normalized)
26 | return string(output) + "\n"
27 | }
28 |
29 | func (f *JsonFormatter) FormatBatch(rs []record.Record) string {
30 | message := ""
31 | for _, r := range rs {
32 | message = message + f.Format(r)
33 | }
34 | return message
35 | }
36 |
--------------------------------------------------------------------------------
/log/formatter/line_formatter.go:
--------------------------------------------------------------------------------
1 | package formatter
2 |
3 | import "github.com/forgoer/thinkgo/log/record"
4 |
5 | type LineFormatter struct {
6 | }
7 |
8 | func NewLineFormatter() *LineFormatter {
9 | f := &LineFormatter{}
10 | return f
11 | }
12 |
13 | func (f *LineFormatter) Format(r record.Record) string {
14 | return "[" + r.Datetime.Local().Format("2006-01-02 15:04:05.000") + "]" + r.Channel + "." + r.LevelName + ": " + r.Message + "\n"
15 | }
16 |
17 | func (f *LineFormatter) FormatBatch(rs []record.Record) string {
18 | message := ""
19 | for _, r := range rs {
20 | message = message + f.Format(r)
21 | }
22 | return message
23 | }
24 |
--------------------------------------------------------------------------------
/log/handler/console.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "os"
5 | "sync"
6 |
7 | "github.com/forgoer/thinkgo/log/record"
8 | )
9 |
10 | type brush func(string) string
11 |
12 | func newBrush(color string) brush {
13 | pre := "\033["
14 | reset := "\033[0m"
15 | return func(text string) string {
16 | return pre + color + "m" + text + reset
17 | }
18 | }
19 |
20 | var colors = map[record.Level]brush{
21 | record.EMERGENCY: newBrush("1;41"), // Emergency Red background
22 | record.ALERT: newBrush("1;35"), // Alert purple
23 | record.CRITICAL: newBrush("1;34"), // Critical blue
24 | record.ERROR: newBrush("1;31"), // Error red
25 | record.WARNING: newBrush("1;33"), // Warn yellow
26 | record.INFO: newBrush("1;36"), // Informational sky blue
27 | record.DEBUG: newBrush("1;32"), // Debug green
28 | record.NOTICE: newBrush("1;32"), // Trace green
29 | }
30 |
31 | type ConsoleHandler struct {
32 | Handler
33 | sync.Mutex
34 | level record.Level
35 |
36 | bubble bool
37 | }
38 |
39 | func NewConsoleHandler(level record.Level) *ConsoleHandler {
40 | return &ConsoleHandler{
41 | level: level,
42 | bubble: true,
43 | }
44 | }
45 |
46 | // IsHandling Checks whether the given record will be handled by this handler.
47 | func (h *ConsoleHandler) IsHandling(r record.Record) bool {
48 | return r.Level >= h.level
49 | }
50 |
51 | // Handle Handles a record.
52 | func (h *ConsoleHandler) Handle(r record.Record) bool {
53 | if !h.IsHandling(r) {
54 | return false
55 | }
56 |
57 | r.Formatted = h.GetFormatter().Format(r)
58 |
59 | h.write(r)
60 |
61 | return false == h.bubble
62 | }
63 |
64 | func (h *ConsoleHandler) write(r record.Record) {
65 | h.Lock()
66 | defer h.Unlock()
67 | message := colors[r.Level](r.Formatted)
68 | os.Stdout.Write(append([]byte(message)))
69 | }
70 |
--------------------------------------------------------------------------------
/log/handler/file.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "os"
5 | "path"
6 | "strings"
7 | "sync"
8 | "time"
9 |
10 | "github.com/forgoer/thinkgo/log/record"
11 | )
12 |
13 | type FileHandler struct {
14 | Handler
15 | sync.Mutex
16 | level record.Level
17 | bubble bool
18 | filename string
19 |
20 | filenameFormat string
21 | dateFormat string
22 | timedFilename string
23 |
24 | rotate bool
25 | }
26 |
27 | func NewFileHandler(filename string, level record.Level) *FileHandler {
28 | h := &FileHandler{
29 | level: level,
30 | bubble: true,
31 | filename: filename,
32 | filenameFormat: "{filename}-{date}",
33 | dateFormat: "2006-01-02",
34 | }
35 | // h.timedFilename = h.GetTimedFilename()
36 | return h
37 | }
38 |
39 | // IsHandling Checks whether the given record will be handled by this handler.
40 | func (h *FileHandler) IsHandling(r record.Record) bool {
41 | return r.Level >= h.level
42 | }
43 |
44 | // Handle Handles a record.
45 | func (h *FileHandler) Handle(r record.Record) bool {
46 | if !h.IsHandling(r) {
47 | return false
48 | }
49 |
50 | r.Formatted = h.GetFormatter().Format(r)
51 |
52 | h.write(r)
53 |
54 | return false == h.bubble
55 | }
56 |
57 | // SetLevel Sets minimum logging level at which this handler will be triggered.
58 | func (h *FileHandler) SetLevel(level record.Level) {
59 | h.level = level
60 | }
61 |
62 | func (h *FileHandler) write(r record.Record) {
63 | h.Lock()
64 | defer h.Unlock()
65 | file, _ := os.OpenFile(h.GetFilename(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
66 | defer file.Close()
67 | file.Write([]byte(r.Formatted))
68 | }
69 |
70 | // GetFilename Gets the filename.
71 | func (h *FileHandler) GetFilename() string {
72 | if !h.rotate {
73 | return h.filename
74 | }
75 |
76 | return h.GetTimedFilename()
77 | }
78 |
79 | // GetTimedFilename Gets the timed filename.
80 | func (h *FileHandler) GetTimedFilename() string {
81 | dirname := path.Dir(h.filename)
82 | filename := path.Base(h.filename)
83 | fileExt := path.Ext(h.filename)
84 | filename = strings.TrimSuffix(filename, fileExt)
85 |
86 | timedFilename := strings.Replace(path.Join(dirname, h.filenameFormat), "{filename}", filename, -1)
87 | timedFilename = strings.Replace(timedFilename, "{date}", time.Now().Local().Format(h.dateFormat), -1)
88 |
89 | if len(fileExt) > 0 {
90 | timedFilename = timedFilename + fileExt
91 | }
92 |
93 | return timedFilename
94 | }
95 |
--------------------------------------------------------------------------------
/log/handler/file_test.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "path"
7 | "strings"
8 | "testing"
9 | "time"
10 |
11 | "github.com/forgoer/thinkgo/log/record"
12 | )
13 |
14 | func TestNewFileHandler(t *testing.T) {
15 | filename := path.Join(os.TempDir(), "thinkgo.log")
16 |
17 | h := NewFileHandler(filename, record.DEBUG)
18 | filename = h.GetFilename()
19 |
20 | os.Remove(filename)
21 |
22 | message := "Log write to file"
23 | r := getRecord(message)
24 | h.Handle(r)
25 |
26 | b, err := ioutil.ReadFile(filename)
27 | if err != nil {
28 | t.Error(err)
29 | }
30 | content := string(b)
31 |
32 | if !strings.Contains(content, message) {
33 | t.Error("test FileHandler error")
34 | }
35 | t.Log(filename, content)
36 | }
37 |
38 | func getRecord(message string) record.Record {
39 | return record.Record{
40 | Level: 200,
41 | Message: message,
42 | LevelName: "INFO",
43 | Channel: "testing",
44 | Datetime: time.Now(),
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/log/handler/handler.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/log/formatter"
5 | "github.com/forgoer/thinkgo/log/record"
6 | )
7 |
8 | type Handler struct {
9 | formatter formatter.Formatter
10 | level record.Level
11 | }
12 |
13 | // GetFormatter Gets the formatter.
14 | func (h *Handler) GetFormatter() formatter.Formatter {
15 | if h.formatter == nil {
16 | h.formatter = h.getDefaultFormatter()
17 | }
18 | return h.formatter
19 | }
20 |
21 | // SetFormatter Sets the formatter.
22 | func (h *Handler) SetFormatter(f formatter.Formatter) *Handler {
23 | h.formatter = f
24 | return h
25 | }
26 |
27 | func (h *Handler) getDefaultFormatter() formatter.Formatter {
28 | return formatter.NewLineFormatter()
29 | }
30 |
--------------------------------------------------------------------------------
/log/handler/rotate.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/log/record"
5 | )
6 |
7 | func NewRotateHandler(filename string, level record.Level) *FileHandler {
8 | // h.timedFilename = h.GetTimedFilename()
9 | fileHandler := NewFileHandler(filename, level)
10 | fileHandler.rotate = true
11 |
12 | return fileHandler
13 | }
14 |
--------------------------------------------------------------------------------
/log/handler/rotate_test.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/log/record"
5 | "io/ioutil"
6 | "os"
7 | "path"
8 | "strings"
9 | "testing"
10 | )
11 |
12 | func TestRotateHandler(t *testing.T) {
13 | filename := path.Join(os.TempDir(), "thinkgo.log")
14 |
15 | h := NewRotateHandler(filename, record.DEBUG)
16 | filename = h.GetFilename()
17 |
18 | os.Remove(filename)
19 |
20 | message := "Log write to file"
21 | r := getRecord(message)
22 | h.Handle(r)
23 |
24 | b, err := ioutil.ReadFile(filename)
25 | if err != nil {
26 | t.Error(err)
27 | }
28 | content := string(b)
29 |
30 | if !strings.Contains(content, message) {
31 | t.Error("test FileHandler error")
32 | }
33 | t.Log(filename, content)
34 | }
35 |
--------------------------------------------------------------------------------
/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/log/record"
5 | )
6 |
7 | var logger *Logger
8 |
9 | func init() {
10 | logger = NewLogger("develop", record.DEBUG)
11 | }
12 |
13 | // Debug Adds a log record at the DEBUG level.
14 | func Debug(format string, v ...interface{}) (bool, error) {
15 | return logger.Debug(format, v...)
16 | }
17 |
18 | // Info Adds a log record at the INFO level.
19 | func Info(format string, v ...interface{}) (bool, error) {
20 | return logger.Info(format, v...)
21 | }
22 |
23 | // Notice Adds a log record at the NOTICE level.
24 | func Notice(format string, v ...interface{}) (bool, error) {
25 | return logger.Notice(format, v...)
26 | }
27 |
28 | // Warn Adds a log record at the WARNING level.
29 | func Warn(format string, v ...interface{}) (bool, error) {
30 | return logger.Warn(format, v...)
31 | }
32 |
33 | // Error Adds a log record at the ERROR level.
34 | func Error(format string, v ...interface{}) (bool, error) {
35 | return logger.Error(format, v...)
36 | }
37 |
38 | // Crit Adds a log record at the CRITICAL level.
39 | func Crit(format string, v ...interface{}) (bool, error) {
40 | return logger.Crit(format, v...)
41 | }
42 |
43 | // Alert Adds a log record at the ALERT level.
44 | func Alert(format string, v ...interface{}) (bool, error) {
45 | return logger.Alert(format, v...)
46 | }
47 |
48 | // Emerg Adds a log record at the EMERGENCY level.
49 | func Emerg(format string, v ...interface{}) (bool, error) {
50 | return logger.Emerg(format, v...)
51 | }
52 |
53 | // GetLogger Get the default Logger
54 | func GetLogger() *Logger {
55 | return logger
56 | }
57 |
58 | // SetLogger Set the default Logger
59 | func SetLogger(l *Logger) {
60 | logger = l
61 | }
62 |
--------------------------------------------------------------------------------
/log/log_test.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 | "os"
7 | "path"
8 | "strings"
9 | "testing"
10 |
11 | "github.com/forgoer/thinkgo/log/handler"
12 | "github.com/forgoer/thinkgo/log/record"
13 | )
14 |
15 | func TestLog(t *testing.T) {
16 | Debug("log with Debug")
17 | Info("log with Info")
18 | Notice("log with Notice")
19 | Warn("log with Warn")
20 | Error("log with Error")
21 | Crit("log with Crit")
22 | Alert("log with Alert")
23 | Emerg("log with Emerg")
24 | }
25 |
26 | func TestLogWithFileHandler(t *testing.T) {
27 |
28 | filename := path.Join(os.TempDir(), "thinkgo.log")
29 |
30 | h := handler.NewFileHandler(filename, record.INFO)
31 |
32 | l := NewLogger("testing", record.INFO)
33 | l.PushHandler(h)
34 |
35 | filename = h.GetTimedFilename()
36 |
37 | os.Remove(filename)
38 |
39 | message := "Log write to file"
40 |
41 | l.Debug(message)
42 |
43 | _, err := ioutil.ReadFile(filename)
44 | if err == nil {
45 | t.Error(errors.New("test FileHandler error"))
46 | }
47 |
48 |
49 | h.SetLevel(record.DEBUG)
50 | l = NewLogger("testing", record.DEBUG)
51 | l.PushHandler(h)
52 | l.Debug(message)
53 |
54 | b, err := ioutil.ReadFile(filename)
55 | if err != nil {
56 | t.Error(errors.New("test FileHandler error"))
57 | }
58 | content := string(b)
59 |
60 | if !strings.Contains(content, message) {
61 | t.Error("test FileHandler error")
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/log/logger.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "container/list"
5 | "errors"
6 | "fmt"
7 | "time"
8 |
9 | "github.com/forgoer/thinkgo/log/handler"
10 | "github.com/forgoer/thinkgo/log/record"
11 | )
12 |
13 | type Handler interface {
14 | IsHandling(r record.Record) bool
15 | Handle(r record.Record) bool
16 | }
17 |
18 | type Logger struct {
19 | name string
20 | level record.Level
21 | handlers *list.List
22 | }
23 |
24 | // NewLogger New a Logger instance
25 | func NewLogger(name string, level record.Level) *Logger {
26 | logger := &Logger{
27 | name: name,
28 | handlers: list.New(),
29 | level: level,
30 | }
31 | return logger
32 | }
33 |
34 | // GetName Gets the name
35 | func (logger *Logger) GetName(name string) string {
36 | return logger.name
37 | }
38 |
39 | // SetName Sets the name
40 | func (logger *Logger) SetName(name string) *Logger {
41 | logger.name = name
42 | return logger
43 | }
44 |
45 | // PushHandler Pushes a handler on to the stack.
46 | func (logger *Logger) PushHandler(handler Handler) *Logger {
47 | logger.handlers.PushFront(handler)
48 | return logger
49 | }
50 |
51 | // PopHandler Pops a handler from the stack
52 | func (logger *Logger) PopHandler() {
53 | front := logger.handlers.Front()
54 | if front != nil {
55 | logger.handlers.Remove(front)
56 | }
57 | }
58 |
59 | // SetHandlers Set handlers, replacing all existing ones.
60 | func (logger *Logger) SetHandlers(handlers []Handler) *Logger {
61 | count := len(handlers)
62 | for i := count - 1; i >= 0; i = i - 1 {
63 | logger.PushHandler(handlers[i])
64 | }
65 | return logger
66 | }
67 |
68 | // GetHandlers Returns a Handler slice
69 | func (logger *Logger) GetHandlers() []Handler {
70 | var handler []Handler
71 | for e := logger.handlers.Front(); e != nil; e = e.Next() {
72 | handler = append(handler, e.Value.(Handler))
73 | }
74 | return handler
75 | }
76 |
77 | // AddRecord Adds a log record.
78 | func (logger *Logger) AddRecord(level record.Level, format string, v ...interface{}) (bool, error) {
79 | if logger.handlers.Len() == 0 {
80 | logger.PushHandler(handler.NewConsoleHandler(logger.level))
81 | }
82 |
83 | levelName, err := GetLevelName(level)
84 | if err != nil {
85 | return false, err
86 | }
87 |
88 | handlerKey := false
89 | for e := logger.handlers.Front(); e != nil; e = e.Next() {
90 | h := e.Value.(Handler)
91 | if h.IsHandling(record.Record{Level: level}) {
92 | handlerKey = true
93 | break
94 | }
95 | }
96 | if !handlerKey {
97 | return false, nil
98 | }
99 |
100 | if len(v) > 0 {
101 | format = fmt.Sprintf(format, v...)
102 | }
103 |
104 | r := record.Record{
105 | Level: level,
106 | Message: format,
107 | LevelName: levelName,
108 | Channel: logger.name,
109 | Datetime: time.Now(),
110 | }
111 |
112 | for e := logger.handlers.Front(); e != nil; e = e.Next() {
113 | h := e.Value.(Handler)
114 | if h.Handle(r) {
115 | break
116 | }
117 | }
118 |
119 | return true, nil
120 | }
121 |
122 | // Debug Adds a log record at the DEBUG level.
123 | func (logger *Logger) Debug(format string, v ...interface{}) (bool, error) {
124 | return logger.AddRecord(record.DEBUG, format, v...)
125 | }
126 |
127 | // Info Adds a log record at the INFO level.
128 | func (logger *Logger) Info(format string, v ...interface{}) (bool, error) {
129 | return logger.AddRecord(record.INFO, format, v...)
130 | }
131 |
132 | // Notice Adds a log record at the NOTICE level.
133 | func (logger *Logger) Notice(format string, v ...interface{}) (bool, error) {
134 | return logger.AddRecord(record.NOTICE, format, v...)
135 | }
136 |
137 | // Warn Adds a log record at the WARNING level.
138 | func (logger *Logger) Warn(format string, v ...interface{}) (bool, error) {
139 | return logger.AddRecord(record.WARNING, format, v...)
140 | }
141 |
142 | // Error Adds a log record at the ERROR level.
143 | func (logger *Logger) Error(format string, v ...interface{}) (bool, error) {
144 | return logger.AddRecord(record.ERROR, format, v...)
145 | }
146 |
147 | // Crit Adds a log record at the CRITICAL level.
148 | func (logger *Logger) Crit(format string, v ...interface{}) (bool, error) {
149 | return logger.AddRecord(record.CRITICAL, format, v...)
150 | }
151 |
152 | // Alert Adds a log record at the ALERT level.
153 | func (logger *Logger) Alert(format string, v ...interface{}) (bool, error) {
154 | return logger.AddRecord(record.ALERT, format, v...)
155 | }
156 |
157 | // Emerg Adds a log record at the EMERGENCY level.
158 | func (logger *Logger) Emerg(format string, v ...interface{}) (bool, error) {
159 | return logger.AddRecord(record.EMERGENCY, format, v...)
160 | }
161 |
162 | // Gets the name of the logging level.
163 | func GetLevelName(level record.Level) (string, error) {
164 | levels := record.GetLevels()
165 | l, ok := levels[level]
166 | if !ok {
167 | return l, errors.New(fmt.Sprintf("Level %d is not defined", level))
168 | }
169 | return l, nil
170 | }
171 |
--------------------------------------------------------------------------------
/log/record/record.go:
--------------------------------------------------------------------------------
1 | package record
2 |
3 | import (
4 | "strings"
5 | "time"
6 | )
7 |
8 | type Level int
9 |
10 | const (
11 | // DEBUG Detailed debug information
12 | DEBUG Level = 100
13 | // INFO Interesting events
14 | INFO Level = 200
15 | // NOTICE Uncommon events
16 | NOTICE Level = 250
17 | // WARNING Exceptional occurrences that are not errors
18 | WARNING Level = 300
19 | // ERROR Runtime errors
20 | ERROR Level = 400
21 | // CRITICAL Critical conditions
22 | CRITICAL Level = 500
23 | // ALERT Action must be taken immediately
24 | ALERT Level = 550
25 | // EMERGENCY Urgent alert.
26 | EMERGENCY Level = 600
27 | )
28 |
29 | // Logging levels from syslog protocol defined in RFC 5424
30 | var levels = map[Level]string{
31 | DEBUG: "DEBUG",
32 | INFO: "INFO",
33 | NOTICE: "NOTICE",
34 | WARNING: "WARNING",
35 | ERROR: "ERROR",
36 | CRITICAL: "CRITICAL",
37 | ALERT: "ALERT",
38 | EMERGENCY: "EMERGENCY",
39 | }
40 |
41 | type Record struct {
42 | Level Level
43 | Message string
44 | LevelName string
45 | Channel string
46 | Datetime time.Time
47 | Formatted string
48 | }
49 |
50 | // GetLevels returns levels map
51 | func GetLevels() map[Level]string {
52 | return levels
53 | }
54 |
55 | // GetLevel Parse the string level into a Level constant.
56 | func GetLevel(levelKey string) Level {
57 | for level, s := range levels {
58 | if strings.ToUpper(levelKey) == s {
59 | return level
60 | }
61 | }
62 | return DEBUG
63 | }
64 |
--------------------------------------------------------------------------------
/pipeline.go:
--------------------------------------------------------------------------------
1 | package thinkgo
2 |
3 | import (
4 | "container/list"
5 | "html/template"
6 | "net/http"
7 |
8 | "github.com/forgoer/thinkgo/context"
9 | "github.com/forgoer/thinkgo/router"
10 | "github.com/forgoer/thinkgo/think"
11 | )
12 |
13 | type Pipeline struct {
14 | handlers []think.Handler
15 | pipeline *list.List
16 | passable *context.Request
17 | }
18 |
19 | // Pipeline returns a new Pipeline
20 | func NewPipeline() *Pipeline {
21 | p := &Pipeline{
22 | pipeline: list.New(),
23 | }
24 | return p
25 | }
26 |
27 | // Pipe Push a Middleware Handler to the pipeline
28 | func (p *Pipeline) Pipe(m think.Handler) *Pipeline {
29 | p.pipeline.PushBack(m)
30 | return p
31 | }
32 |
33 | // Pipe Batch push Middleware Handlers to the pipeline
34 | func (p *Pipeline) Through(hls []think.Handler) *Pipeline {
35 | for _, hl := range hls {
36 | p.Pipe(hl)
37 | }
38 | return p
39 | }
40 |
41 | // Passable set the request being sent through the pipeline.
42 | func (p *Pipeline) Passable(passable *context.Request) *Pipeline {
43 | p.passable = passable
44 | return p
45 | }
46 |
47 | // Run run the pipeline
48 | func (p *Pipeline) Run() interface{} {
49 | var result interface{}
50 | e := p.pipeline.Front()
51 | if e != nil {
52 | result = p.handler(p.passable, e)
53 | }
54 | return result
55 | }
56 |
57 | // ServeHTTP
58 | func (p *Pipeline) ServeHTTP(w http.ResponseWriter, r *http.Request) {
59 | request := context.NewRequest(r)
60 | request.CookieHandler = context.ParseCookieHandler()
61 | p.Passable(request)
62 |
63 | result := p.Run()
64 |
65 | switch result.(type) {
66 | case router.Response:
67 | result.(router.Response).Send(w)
68 | break
69 | case template.HTML:
70 | think.Html(string(result.(template.HTML))).Send(w)
71 | break
72 | case http.Handler:
73 | result.(http.Handler).ServeHTTP(w, r)
74 | break
75 | default:
76 | think.Response(result).Send(w)
77 | break
78 | }
79 | }
80 |
81 | func (p *Pipeline) handler(passable *context.Request, e *list.Element) interface{} {
82 | if e == nil {
83 | return nil
84 | }
85 | hl := e.Value.(think.Handler)
86 | result := hl.Process(passable, p.closure(e))
87 | return result
88 | }
89 |
90 | func (p *Pipeline) closure(e *list.Element) think.Closure {
91 | return func(req *context.Request) interface{} {
92 | e = e.Next()
93 | return p.handler(req, e)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/router/metch.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "regexp"
5 | )
6 |
7 | func matchMethod(method, target string) bool {
8 | if "*" == target {
9 | return true
10 | }
11 |
12 | if target == method {
13 | return true
14 | }
15 |
16 | return false
17 | }
18 |
19 | func matchMethods(method string, target []string) bool {
20 | for _, v := range target {
21 | if matchMethod(method, v) {
22 | return true
23 | }
24 | }
25 | return false
26 | }
27 |
28 | func matchPath(path, target string) bool {
29 |
30 | res, err := regexp.MatchString(target, path)
31 |
32 | if err != nil {
33 | return false
34 | }
35 |
36 | return res
37 | }
38 |
--------------------------------------------------------------------------------
/router/middleware.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/forgoer/thinkgo/context"
7 | )
8 |
9 | // Response an HTTP response interface
10 | type Response interface {
11 | Send(w http.ResponseWriter)
12 | }
13 |
14 | // Closure Anonymous function, Used in Middleware Handler
15 | type Closure func(req *context.Request) interface {
16 | }
17 |
18 | // MiddlewareFunc Handle an incoming request.
19 | type Middleware func(request *context.Request, next Closure) interface{}
20 |
--------------------------------------------------------------------------------
/router/parameter.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | type parameter struct {
4 | name string
5 | value string
6 | }
7 |
--------------------------------------------------------------------------------
/router/pipeline.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "container/list"
5 |
6 | "github.com/forgoer/thinkgo/context"
7 | )
8 |
9 | type Pipeline struct {
10 | handlers []Middleware
11 | pipeline *list.List
12 | passable *context.Request
13 | }
14 |
15 | // Pipeline returns a new Pipeline
16 | func NewPipeline() *Pipeline {
17 | p := &Pipeline{
18 | pipeline: list.New(),
19 | }
20 | return p
21 | }
22 |
23 | // Pipe Push a Middleware Handler to the pipeline
24 | func (p *Pipeline) Pipe(m Middleware) *Pipeline {
25 | p.pipeline.PushBack(m)
26 | return p
27 | }
28 |
29 | // Pipe Batch push Middleware Handlers to the pipeline
30 | func (p *Pipeline) Through(hls []Middleware) *Pipeline {
31 | for _, hl := range hls {
32 | p.Pipe(hl)
33 | }
34 | return p
35 | }
36 |
37 | // Passable set the request being sent through the pipeline.
38 | func (p *Pipeline) Passable(passable *context.Request) *Pipeline {
39 | p.passable = passable
40 | return p
41 | }
42 |
43 | // Run run the pipeline
44 | func (p *Pipeline) Run(destination Middleware) interface{} {
45 | var result interface{}
46 |
47 | p.Pipe(destination)
48 |
49 | e := p.pipeline.Front()
50 | if e != nil {
51 | result = p.handler(p.passable, e)
52 | }
53 | return result
54 | }
55 |
56 | func (p *Pipeline) handler(passable *context.Request, e *list.Element) interface{} {
57 | if e == nil {
58 | return nil
59 | }
60 | middleware := e.Value.(Middleware)
61 | result := middleware(passable, p.closure(e))
62 | return result
63 | }
64 |
65 | func (p *Pipeline) closure(e *list.Element) Closure {
66 | return func(req *context.Request) interface{} {
67 | e = e.Next()
68 | return p.handler(req, e)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/router/resource_controller.go:
--------------------------------------------------------------------------------
1 | package router
2 |
--------------------------------------------------------------------------------
/router/route.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "path"
8 | "strings"
9 | )
10 |
11 | var verbs = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}
12 |
13 | type Request interface {
14 | GetMethod() string
15 | GetPath() string
16 | }
17 |
18 | type Route struct {
19 | inited bool
20 |
21 | method []string
22 | prefix string
23 | pattern string
24 | handler interface{}
25 | middlewares []Middleware
26 | group *Route
27 |
28 | collects []*Route
29 |
30 | rules map[string]map[string]*Rule
31 | allRules map[string]*Rule
32 | // Current *Rule
33 | }
34 |
35 | // New Create a new Route instance.
36 | func New() *Route {
37 | route := &Route{
38 | rules: make(map[string]map[string]*Rule),
39 | }
40 | return route
41 | }
42 |
43 | // Dispatch Dispatch the request
44 | func (r *Route) Dispatch(request Request) (*Rule, error) {
45 | rule, err := r.Match(request)
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | rule.Bind(request.GetPath())
51 |
52 | return rule, nil
53 | }
54 |
55 | // Match Find the first rule matching a given request.
56 | func (r *Route) Match(request Request) (*Rule, error) {
57 | for _, rule := range r.rules[request.GetMethod()] {
58 | if true == rule.Matches(request.GetMethod(), request.GetPath()) {
59 | return rule, nil
60 | }
61 | }
62 | return nil, errors.New("Not Found")
63 | }
64 |
65 | // AddRule Add a Rule to the Router.Rules
66 | func (r *Route) AddRule(rule *Rule) *Rule {
67 | domainAndUri := rule.pattern
68 | method := ""
69 | for _, method = range rule.method {
70 |
71 | if _, ok := r.rules[method]; !ok {
72 | r.rules[method] = map[string]*Rule{
73 | domainAndUri: rule,
74 | }
75 | } else {
76 | r.rules[method][domainAndUri] = rule
77 | }
78 |
79 | }
80 |
81 | if r.allRules == nil {
82 | r.allRules = map[string]*Rule{}
83 | }
84 | r.allRules[method+domainAndUri] = rule
85 |
86 | return rule
87 | }
88 |
89 | // Add Add a router
90 | func (r *Route) Add(method []string, pattern string, handler interface{}) *Route {
91 | route := r.initRoute()
92 | route.method = method
93 | route.pattern = r.getPrefix(pattern)
94 | route.handler = handler
95 | return route
96 | }
97 |
98 | // Get Register a new GET rule with the router.
99 | func (r *Route) Get(pattern string, handler interface{}) *Route {
100 | return r.Add(Method("GET", "HEAD"), pattern, handler)
101 | }
102 |
103 | // Head Register a new Head rule with the router.
104 | func (r *Route) Head(pattern string, handler interface{}) *Route {
105 | return r.Add(Method("HEAD"), pattern, handler)
106 | }
107 |
108 | // Post Register a new POST rule with the router.
109 | func (r *Route) Post(pattern string, handler interface{}) *Route {
110 | return r.Add(Method("POST"), pattern, handler)
111 | }
112 |
113 | // Put Register a new PUT rule with the router.
114 | func (r *Route) Put(pattern string, handler interface{}) *Route {
115 | return r.Add(Method("PUT"), pattern, handler)
116 | }
117 |
118 | // Patch Register a new PATCH rule with the router.
119 | func (r *Route) Patch(pattern string, handler interface{}) *Route {
120 | return r.Add(Method("PATCH"), pattern, handler)
121 | }
122 |
123 | // Delete Register a new DELETE rule with the router.
124 | func (r *Route) Delete(pattern string, handler interface{}) *Route {
125 | return r.Add(Method("DELETE"), pattern, handler)
126 | }
127 |
128 | // Options Register a new OPTIONS rule with the router.
129 | func (r *Route) Options(pattern string, handler interface{}) *Route {
130 | return r.Add(Method("OPTIONS"), pattern, handler)
131 | }
132 |
133 | // Any Register a new rule responding to all verbs.
134 | func (r *Route) Any(pattern string, handler interface{}) *Route {
135 | return r.Add(verbs, pattern, handler)
136 | }
137 |
138 | // Static Register a new Static rule.
139 | func (r *Route) Static(path, root string) {
140 | path = "/" + strings.Trim(path, "/") + "/*"
141 |
142 | h := NewStaticHandle(root)
143 |
144 | r.Get(path, h)
145 | r.Head(path, h)
146 | }
147 |
148 | // Statics Bulk register Static rule.
149 | func (r *Route) Statics(statics map[string]string) {
150 | for path, root := range statics {
151 | r.Static(path, root)
152 | }
153 | }
154 |
155 | // Prefix Add a prefix to the route URI.
156 | func (r *Route) Prefix(prefix string) *Route {
157 | route := r.initRoute()
158 | route.prefix = route.getPrefix(prefix)
159 | return route
160 | }
161 |
162 | // Group Create a route group
163 | func (r *Route) Group(callback func(group *Route)) *Route {
164 | route := r.initRoute()
165 | group := route.cloneRoute()
166 | group.Middleware(route.middlewares...)
167 | callback(group)
168 | return route
169 | }
170 |
171 | // Middleware Set the middleware attached to the route.
172 | func (r *Route) Middleware(middlewares ...Middleware) *Route {
173 | route := r.initRoute()
174 | for _, m := range middlewares {
175 | route.middlewares = append(route.middlewares, m)
176 | }
177 | return route
178 | }
179 |
180 | // Register Register route from the collect.
181 | func (r *Route) Register() {
182 | r.register(r)
183 | }
184 |
185 | func (r *Route) Dump() []byte {
186 | var b bytes.Buffer
187 | for _, rule := range r.allRules {
188 | fmt.Fprintf(&b, "%s %s %T \r\n", strings.Join(rule.method, "|"), rule.pattern, rule.handler)
189 | }
190 |
191 | return b.Bytes()
192 | }
193 |
194 | func (r *Route) register(root *Route) {
195 | for _, route := range r.collects {
196 | route.prefix = r.getPrefix(route.prefix)
197 |
198 | var middlewares []Middleware
199 | for _, m := range r.middlewares {
200 | middlewares = append(middlewares, m)
201 | }
202 | for _, m := range route.middlewares {
203 | middlewares = append(middlewares, m)
204 | }
205 | route.middlewares = middlewares
206 |
207 | route.register(root)
208 |
209 | if route.handler == nil {
210 | continue
211 | }
212 | rule := &Rule{
213 | method: route.method,
214 | pattern: route.getPrefix(route.pattern),
215 | handler: route.handler,
216 | middlewares: route.middlewares,
217 | }
218 |
219 | root.AddRule(rule)
220 | }
221 | r.collects = r.collects[0:0]
222 | }
223 |
224 | // initRoute Initialize a new Route if not initialized
225 | func (r *Route) initRoute() *Route {
226 | route := r
227 | if !r.inited {
228 | route = &Route{
229 | inited: true,
230 | rules: make(map[string]map[string]*Rule),
231 | // prefix: r.prefix,
232 | // middlewares: r.middlewares,
233 | }
234 | r.collects = append(r.collects, route)
235 | }
236 | return route
237 | }
238 |
239 | func (r *Route) cloneRoute() *Route {
240 | route := &Route{
241 | inited: false,
242 | rules: make(map[string]map[string]*Rule),
243 | // prefix: r.prefix,
244 | // middlewares: r.middlewares,
245 | }
246 | r.collects = append(r.collects, route)
247 |
248 | return route
249 | }
250 |
251 | func (r *Route) getPrefix(pattern string) string {
252 | return path.Join("/", r.prefix, pattern)
253 | }
254 |
--------------------------------------------------------------------------------
/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/context"
5 | )
6 |
7 | // RunRoute Return the response for the given rule.
8 | func RunRoute(request *context.Request, rule *Rule) interface{} {
9 | return PrepareResponse(
10 | request,
11 | rule,
12 | runMiddlewares(request, rule),
13 | )
14 | }
15 |
16 | // PrepareResponse Create a response instance from the given value.
17 | func PrepareResponse(request *context.Request, rule *Rule, result interface{}) interface{} {
18 | return result
19 | //var response Response
20 | //switch result.(type) {
21 | //case Response:
22 | // return result.(Response)
23 | //// case string:
24 | //case http.Handler:
25 | // return response
26 | //default:
27 | // response = context.NewResponse().SetContent(fmt.Sprint(result))
28 | //}
29 | //return response
30 | }
31 |
32 | // runMiddlewares Run the given route within Middlewares instance.
33 | func runMiddlewares(request *context.Request, rule *Rule) interface{} {
34 | pipeline := NewPipeline()
35 |
36 | for _, m := range rule.GatherRouteMiddleware() {
37 | pipeline.Pipe(m)
38 | }
39 | return pipeline.Passable(request).Run(func(request *context.Request, next Closure) interface{} {
40 | return PrepareResponse(
41 | request,
42 | rule,
43 | rule.Run(request),
44 | )
45 | })
46 | }
47 |
--------------------------------------------------------------------------------
/router/rule.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "reflect"
5 | "regexp"
6 | "strings"
7 |
8 | "github.com/forgoer/thinkgo/context"
9 | )
10 |
11 | // Rule Route rule
12 | type Rule struct {
13 | middlewares []Middleware
14 | method []string
15 | pattern string
16 | handler interface{}
17 | parameterNames []string
18 | parameters []*parameter
19 | Compiled *Compiled
20 | }
21 |
22 | type Compiled struct {
23 | Regex string
24 | }
25 |
26 | // Matches Determine if the rule matches given request.
27 | func (r *Rule) Matches(method, path string) bool {
28 | r.compile()
29 |
30 | if false == matchMethods(method, r.method) {
31 | return false
32 | }
33 |
34 | if false == matchPath(path, r.Compiled.Regex) {
35 | return false
36 | }
37 |
38 | return true
39 | }
40 |
41 | // Bind Bind the router to a given request for execution.
42 | func (r *Rule) Bind(path string) {
43 | r.compile()
44 |
45 | path = "/" + strings.TrimLeft(path, "/")
46 | reg := regexp.MustCompile(r.Compiled.Regex)
47 | matches := reg.FindStringSubmatch(path)[1:]
48 |
49 | parameterNames := r.getParameterNames()
50 |
51 | if len(parameterNames) == 0 {
52 | return
53 | }
54 |
55 | parameters := make([]*parameter, 0)
56 |
57 | for k, v := range parameterNames {
58 | parameters = append(parameters, ¶meter{
59 | name: v,
60 | value: matches[k],
61 | })
62 | }
63 |
64 | r.parameters = parameters
65 | }
66 |
67 | // Middleware Set the middleware attached to the rule.
68 | func (r *Rule) Middleware(middlewares ...Middleware) *Rule {
69 | for _, m := range middlewares {
70 | r.middlewares = append(r.middlewares, m)
71 | }
72 | return r
73 | }
74 |
75 | // GatherRouteMiddleware Get all middleware, including the ones from the controller.
76 | func (r *Rule) GatherRouteMiddleware() []Middleware {
77 | return r.middlewares
78 | }
79 |
80 | // Run Run the route action and return the response.
81 | func (r *Rule) Run(request *context.Request) (result interface{}) {
82 | if r.handler == nil {
83 | return
84 | }
85 |
86 | v := reflect.ValueOf(r.handler)
87 | switch v.Type().Kind() {
88 | case reflect.Func:
89 | in := parseParams(v, request, r.parameters)
90 | out := v.Call(in)
91 |
92 | if len(out) > 0 {
93 | result = out[0].Interface()
94 | }
95 | default:
96 | result = r.handler
97 | }
98 |
99 | return
100 | }
101 |
102 | // getParameterNames Get all of the parameter names for the rule.
103 | func (r *Rule) getParameterNames() []string {
104 | if r.parameterNames != nil {
105 | return r.parameterNames
106 | }
107 | r.parameterNames = r.compileParameterNames()
108 |
109 | return r.parameterNames
110 | }
111 |
112 | func (r *Rule) compile() {
113 | if r.Compiled != nil {
114 | return
115 | }
116 |
117 | r.pattern = strings.Replace(r.pattern, "/*", "/.*", -1)
118 |
119 | reg, _ := regexp.Compile(`\{\w+\}`)
120 | regex := reg.ReplaceAllString(r.pattern, "([^/]+)")
121 |
122 | r.Compiled = &Compiled{
123 | Regex: "^" + regex + "$",
124 | }
125 | }
126 |
127 | func (r *Rule) compileParameterNames() []string {
128 | reg := regexp.MustCompile(`\{(.*?)\}`)
129 | matches := reg.FindAllStringSubmatch(r.pattern, -1)
130 |
131 | var result []string
132 | for _, v := range matches {
133 | result = append(result, v[1])
134 | }
135 |
136 | return result
137 | }
138 |
139 | func parseParams(value reflect.Value, request *context.Request, parameters []*parameter) []reflect.Value {
140 | valueType := value.Type()
141 | needNum := valueType.NumIn()
142 | if needNum < 1 {
143 | return nil
144 | }
145 |
146 | in := make([]reflect.Value, 0, needNum)
147 | t := valueType.In(0)
148 | k := t.Kind()
149 | ptr := reflect.Ptr == k
150 | if ptr {
151 | k = t.Elem().Kind()
152 | }
153 | if k == reflect.ValueOf(request).Elem().Kind() {
154 | var v reflect.Value
155 | if ptr {
156 | v = reflect.ValueOf(request)
157 | } else {
158 | v = reflect.ValueOf(request).Elem()
159 | }
160 | in = append(in, v)
161 | needNum--
162 | }
163 |
164 | for _, p := range parameters {
165 | in = append(in, reflect.ValueOf(p.value))
166 | }
167 |
168 | return in
169 | }
170 |
--------------------------------------------------------------------------------
/router/static.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/forgoer/thinkgo/filesystem"
7 | )
8 |
9 | type staticHandle struct {
10 | fileServer http.Handler
11 | fs http.FileSystem
12 | }
13 |
14 | // NewStaticHandle A Handler responds to a Static HTTP request.
15 | func NewStaticHandle(root string) http.Handler {
16 | fs := filesystem.NewFileFileSystem(root, false)
17 | return &staticHandle{
18 | fileServer: http.FileServer(fs),
19 | fs: fs,
20 | }
21 | }
22 |
23 | // ServeHTTP responds to an Static HTTP request.
24 | func (s *staticHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
25 | s.fileServer.ServeHTTP(w, r)
26 | }
27 |
--------------------------------------------------------------------------------
/router/utils.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | // Method Convert multiple method strings to an slice
8 | func Method(method ...string) []string {
9 | var methods []string
10 | if len(method) == 0 {
11 | return methods
12 | }
13 | for _, m := range method {
14 | methods = append(methods, strings.ToUpper(m))
15 | }
16 | return methods
17 | }
18 |
--------------------------------------------------------------------------------
/session/cookie-handler.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | type CookieHandler struct {
4 | request Request
5 | response Response
6 | }
7 |
8 | func NewCookieHandler() *CookieHandler {
9 | return &CookieHandler{}
10 | }
11 |
12 | func (c *CookieHandler) SetRequest(req Request) {
13 | c.request = req
14 | }
15 |
16 | func (c *CookieHandler) SetResponse(res Response) {
17 | c.response = res
18 | }
19 |
20 | func (c *CookieHandler) Read(id string) string {
21 | value, _ := c.request.Cookie(id)
22 |
23 | return value
24 | }
25 |
26 | func (c *CookieHandler) Write(id string, data string) {
27 | c.response.Cookie(id, data)
28 | }
29 |
--------------------------------------------------------------------------------
/session/file-handler.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "path"
5 | "time"
6 |
7 | "github.com/forgoer/thinkgo/filesystem"
8 | )
9 |
10 | type FileHandler struct {
11 | Path string
12 | Lifetime time.Duration
13 | }
14 |
15 | func (c *FileHandler) Read(id string) string {
16 |
17 | savePath := path.Join(c.Path, id)
18 |
19 | if ok, _ := filesystem.Exists(savePath); ok {
20 | modTime, _ := filesystem.ModTime(savePath)
21 | if modTime.After(time.Now().Add(-c.Lifetime)) {
22 | data, err := filesystem.Get(savePath)
23 | if err != nil {
24 | panic(err)
25 | }
26 | return string(data)
27 | }
28 | }
29 | return ""
30 | }
31 |
32 | func (c *FileHandler) Write(id string, data string) {
33 | savePath := path.Join(c.Path, id)
34 |
35 | err := filesystem.Put(savePath, data)
36 |
37 | if err != nil {
38 | panic(err)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/session/handler.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | type Handler interface {
4 | Read(id string) string
5 | Write(id string, data string)
6 | }
7 |
--------------------------------------------------------------------------------
/session/http.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | type Request interface {
4 | Cookie(key string, value ...string) (string, error)
5 | }
6 |
7 | type Response interface {
8 | Cookie(name interface{}, params ...interface{}) error
9 | }
10 |
--------------------------------------------------------------------------------
/session/manager.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | var customHandlers map[string]Handler
8 |
9 | type Config struct {
10 | //Default Session Driver
11 | Driver string
12 |
13 | CookieName string
14 |
15 | //Session Lifetime
16 | Lifetime time.Duration
17 |
18 | //Session Encryption
19 | Encrypt bool
20 |
21 | //Session File Location
22 | Files string
23 | }
24 |
25 | type Manager struct {
26 | store *Store
27 | Config *Config
28 | }
29 |
30 | func NewManager(config *Config) *Manager {
31 | m := &Manager{
32 | Config: config,
33 | }
34 |
35 | return m
36 | }
37 |
38 | func (m *Manager) SessionStart(req Request) *Store {
39 | m.parseStore()
40 |
41 | if handler, ok := m.usingCookieSessions(); ok {
42 | handler.SetRequest(req)
43 | }
44 | name, _ := req.Cookie(m.store.GetName())
45 | m.store.SetId(name)
46 | m.store.Start()
47 | return m.store
48 | }
49 |
50 | func (m *Manager) SessionSave(res Response) *Store {
51 | if handler, ok := m.usingCookieSessions(); ok {
52 | handler.SetResponse(res)
53 | }
54 | res.Cookie(m.store.GetName(), m.store.GetId())
55 | m.store.Save()
56 | return m.store
57 | }
58 |
59 | func Extend(driver string, handler Handler) {
60 | if customHandlers == nil {
61 | customHandlers = make(map[string]Handler)
62 | }
63 | customHandlers[driver] = handler
64 | }
65 |
66 | func (m *Manager) buildSession(handler Handler) *Store {
67 | store := NewStore(m.Config.CookieName, handler)
68 | return store
69 | }
70 |
71 | func (m *Manager) usingCookieSessions() (handler *CookieHandler, ok bool) {
72 | handler, ok = m.store.GetHandler().(*CookieHandler)
73 | return
74 | }
75 |
76 | func (m *Manager) parseStore() {
77 | if m.store != nil {
78 | return
79 | }
80 |
81 | m.store = m.buildSession(
82 | m.parseStoreHandler(),
83 | )
84 | }
85 |
86 | func (m *Manager) parseStoreHandler() Handler {
87 | var storeHandler Handler
88 | switch m.Config.Driver {
89 | case "cookie":
90 | storeHandler = &CookieHandler{}
91 | case "file":
92 | storeHandler = &FileHandler{
93 | Path: m.Config.Files,
94 | Lifetime: m.Config.Lifetime,
95 | }
96 | default:
97 | var ok bool
98 | storeHandler, ok = customHandlers[m.Config.Driver]
99 | if !ok {
100 | panic("Unsupported session driver: " + m.Config.Driver)
101 | }
102 | }
103 |
104 | return storeHandler
105 | }
106 |
--------------------------------------------------------------------------------
/session/redis-handler.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/gomodule/redigo/redis"
6 | "time"
7 | )
8 |
9 | type RedisHandler struct {
10 | pool *redis.Pool // redis connection pool
11 | prefix string
12 | lifetime time.Duration
13 | }
14 |
15 | // NewRedisHandler Create a redis session handler
16 | func NewRedisHandler(pool *redis.Pool, prefix string, lifetime time.Duration) *RedisHandler {
17 | return &RedisHandler{pool, prefix, lifetime}
18 | }
19 |
20 | func (rh *RedisHandler) Read(id string) string {
21 | c := rh.pool.Get()
22 | defer c.Close()
23 |
24 | b, err := redis.Bytes(c.Do("GET", rh.prefix+":"+id))
25 | if err != nil {
26 | return ""
27 | }
28 |
29 | var value string
30 |
31 | json.Unmarshal(b, value)
32 |
33 | return value
34 | }
35 |
36 | func (rh *RedisHandler) Write(id string, data string) {
37 | c := rh.pool.Get()
38 | defer c.Close()
39 |
40 | c.Do("SETEX", rh.prefix+":"+id, int64(rh.lifetime), data)
41 | }
42 |
--------------------------------------------------------------------------------
/session/store.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/sha1"
6 | "encoding/base64"
7 | "encoding/json"
8 | "fmt"
9 | "io"
10 | "strconv"
11 | "time"
12 | )
13 |
14 | type Store struct {
15 | name string
16 | id string
17 | handler Handler
18 | attributes map[string]interface{}
19 | }
20 |
21 | func NewStore(name string, handler Handler) *Store {
22 | s := &Store{
23 | name: name,
24 | handler: handler,
25 | attributes: make(map[string]interface{}),
26 | }
27 | return s
28 | }
29 |
30 | func (s *Store) GetHandler() Handler {
31 | return s.handler
32 | }
33 |
34 | func (s *Store) GetId() string {
35 | return s.id
36 | }
37 |
38 | func (s *Store) SetId(id string) {
39 | if len(id) < 1 {
40 | id = generateSessionId()
41 | }
42 |
43 | s.id = id
44 | }
45 |
46 | func (s *Store) GetName() string {
47 | return s.name
48 | }
49 |
50 | func (s *Store) Start() {
51 | data := s.handler.Read(s.GetId())
52 |
53 | decodeData, _ := base64.StdEncoding.DecodeString(data)
54 |
55 | udata := make(map[string]interface{})
56 |
57 | json.Unmarshal(decodeData, &udata)
58 |
59 | for k, v := range udata {
60 | s.attributes[k] = v
61 | }
62 | }
63 |
64 | func (s *Store) Get(name string, value ...interface{}) interface{} {
65 | if v, ok := s.attributes[name]; ok {
66 | return v
67 | }
68 | if len(value) > 0 {
69 | return value[0]
70 | }
71 | return nil
72 | }
73 |
74 | func (s *Store) Set(name string, value interface{}) {
75 | s.attributes[name] = value
76 | }
77 |
78 | func (s *Store) All() map[string]interface{} {
79 | return s.attributes
80 | }
81 |
82 | func (s *Store) Remove(name string) interface{} {
83 | value := s.Get(name)
84 | delete(s.attributes, name)
85 | return value
86 | }
87 |
88 | func (s *Store) Forget(names ...string) {
89 | for _, name := range names {
90 | delete(s.attributes, name)
91 | }
92 | }
93 |
94 | func (s *Store) Clear() {
95 | s.attributes = nil
96 | }
97 |
98 | func (s *Store) Save() {
99 | data, _ := json.Marshal(s.attributes)
100 |
101 | encodeData := base64.StdEncoding.EncodeToString(data)
102 | s.handler.Write(s.GetId(), encodeData)
103 | }
104 |
105 | func generateSessionId() string {
106 | id := strconv.FormatInt(time.Now().UnixNano(), 10)
107 | b := make([]byte, 48)
108 | io.ReadFull(rand.Reader, b)
109 | id = id + base64.URLEncoding.EncodeToString(b)
110 |
111 | h := sha1.New()
112 | h.Write([]byte(id))
113 |
114 | return fmt.Sprintf("%x", h.Sum(nil))
115 | }
116 |
--------------------------------------------------------------------------------
/think.go:
--------------------------------------------------------------------------------
1 | package thinkgo
2 |
3 | import (
4 | "fmt"
5 | "github.com/forgoer/thinkgo/log"
6 | "github.com/forgoer/thinkgo/log/record"
7 | "net/http"
8 |
9 | "time"
10 |
11 | "github.com/forgoer/thinkgo/config"
12 | "github.com/forgoer/thinkgo/helper"
13 | "github.com/forgoer/thinkgo/router"
14 | "github.com/forgoer/thinkgo/think"
15 | )
16 |
17 | type registerRouteFunc func(route *router.Route)
18 |
19 | type registerConfigFunc func()
20 |
21 | type Think struct {
22 | App *think.Application
23 | handlers []think.HandlerFunc
24 | }
25 |
26 | // New Create The Application
27 | func New() *Think {
28 | application := think.NewApplication()
29 | application.Logger = log.NewLogger("develop", record.DEBUG)
30 | t := &Think{
31 | App: application,
32 | }
33 | //t.bootView()
34 | t.bootRoute()
35 | return t
36 | }
37 |
38 | // RegisterRoute Register Route
39 | func (th *Think) RegisterRoute(register registerRouteFunc) {
40 | route := th.App.GetRoute()
41 | defer route.Register()
42 | register(route)
43 | }
44 |
45 | // RegisterConfig Register Config
46 | func (th *Think) RegisterConfig(register registerConfigFunc) {
47 | register()
48 | }
49 |
50 | // RegisterConfig Register Config
51 | func (th *Think) RegisterHandler(handler think.HandlerFunc) {
52 | th.handlers = append(th.handlers, handler)
53 | }
54 |
55 | // Run thinkgo application.
56 | // Run() default run on HttpPort
57 | // Run("localhost")
58 | // Run(":9011")
59 | // Run("127.0.0.1:9011")
60 | func (th *Think) Run(params ...string) {
61 | var err error
62 | var endRunning = make(chan bool, 1)
63 |
64 | var addrs = helper.ParseAddr(params...)
65 |
66 | // register route handler
67 | th.RegisterHandler(think.NewRouteHandler)
68 |
69 | pipeline := NewPipeline()
70 | for _, h := range th.handlers {
71 | pipeline.Pipe(h(th.App))
72 | }
73 |
74 | th.App.Logger.Debug("\r\nLoaded routes:\r\n%s",string(th.App.GetRoute().Dump()))
75 |
76 | go func() {
77 | th.App.Logger.Debug("ThinkGo server running on http://%s", addrs)
78 |
79 | err = http.ListenAndServe(addrs, pipeline)
80 |
81 | if err != nil {
82 | fmt.Println(err.Error())
83 | time.Sleep(100 * time.Microsecond)
84 | endRunning <- true
85 | }
86 | }()
87 |
88 | <-endRunning
89 | }
90 |
91 | //func (th *Think) bootView() {
92 | // v := view.NewView()
93 | // v.SetPath(config.View.Path)
94 | // th.App.RegisterView(v)
95 | //}
96 |
97 | func (th *Think) bootRoute() {
98 | r := router.New()
99 | r.Statics(config.Route.Static)
100 | th.App.RegisterRoute(r)
101 | }
102 |
--------------------------------------------------------------------------------
/think/app.go:
--------------------------------------------------------------------------------
1 | package think
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/log"
5 | "github.com/forgoer/thinkgo/router"
6 | "github.com/forgoer/thinkgo/view"
7 | )
8 |
9 | // Application the ThinkGo Application
10 | type Application struct {
11 | Env string
12 | Debug bool
13 | Logger *log.Logger
14 | view *view.View
15 | route *router.Route
16 | }
17 |
18 | // NewApplication returns a new ThinkGo Application
19 | func NewApplication() *Application {
20 | return &Application{Env: "production"}
21 | }
22 |
23 | // RegisterRoute Register Route for Application
24 | func (a *Application) RegisterRoute(r *router.Route) {
25 | a.route = r
26 | }
27 |
28 | // RegisterView Register View for Application
29 | func (a *Application) RegisterView(v *view.View) {
30 | a.view = v
31 | }
32 |
33 | // GetRoute Get the router of the application
34 | func (a *Application) GetRoute() *router.Route {
35 | return a.route
36 | }
37 |
38 | // GetView Get the view of the application
39 | func (a *Application) GetView() *view.View {
40 | return a.view
41 | }
42 |
--------------------------------------------------------------------------------
/think/handler.go:
--------------------------------------------------------------------------------
1 | package think
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/context"
5 | )
6 |
7 | // HandlerFunc Handle the application.
8 | type HandlerFunc func(app *Application) Handler
9 |
10 | // Closure Anonymous function, Used in Middleware Handler
11 | type Closure func(req *context.Request) interface{}
12 |
13 | // Handler Middleware Handler interface
14 | type Handler interface {
15 | Process(request *context.Request, next Closure) interface{}
16 | }
17 |
--------------------------------------------------------------------------------
/think/recover_handler.go:
--------------------------------------------------------------------------------
1 | package think
2 |
3 | import (
4 | "fmt"
5 | "github.com/forgoer/thinkgo/context"
6 | "net/http/httputil"
7 | "runtime"
8 | "strings"
9 | )
10 |
11 | type RecoverHandler struct {
12 | app *Application
13 | }
14 |
15 | // NewRecoverHandler The default NewRecoverHandler
16 | func NewRecoverHandler(app *Application) Handler {
17 | return &RecoverHandler{
18 | app: app,
19 | }
20 | }
21 |
22 | // Process Process the request to a router and return the response.
23 | func (h *RecoverHandler) Process(req *context.Request, next Closure) (result interface{}) {
24 | defer func() {
25 | if err := recover(); err != nil {
26 | var stacktrace string
27 | for i := 1; ; i++ {
28 | _, f, l, got := runtime.Caller(i)
29 | if !got {
30 | break
31 | }
32 |
33 | stacktrace += fmt.Sprintf("%s:%d\n", f, l)
34 | }
35 |
36 | httpRequest, _ := httputil.DumpRequest(req.Request, false)
37 |
38 | headers := strings.Split(string(httpRequest), "\r\n")
39 | for idx, header := range headers {
40 | current := strings.Split(header, ":")
41 | if current[0] == "Authorization" {
42 | headers[idx] = current[0] + ": *"
43 | }
44 | }
45 |
46 | logMessage := fmt.Sprintf("Recovered at Request: %s\n", strings.Join(headers, "\r\n"))
47 | logMessage += fmt.Sprintf("Trace: %s\n", err)
48 | logMessage += fmt.Sprintf("\n%s", stacktrace)
49 |
50 | response := context.ErrorResponse()
51 | if h.app.Debug {
52 | response.SetContent(logMessage)
53 | }
54 | result = response
55 | }
56 | }()
57 |
58 | return next(req)
59 | }
60 |
--------------------------------------------------------------------------------
/think/response.go:
--------------------------------------------------------------------------------
1 | package think
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "reflect"
8 |
9 | "github.com/forgoer/thinkgo/context"
10 | )
11 |
12 | // Json Create a new HTTP Response with JSON data
13 | func Json(v interface{}) *context.Response {
14 | c, _ := json.Marshal(v)
15 | return context.NewResponse().SetContent(string(c)).SetContentType("application/json")
16 | }
17 |
18 | // Text Create a new HTTP Response with TEXT data
19 | func Text(s string) *context.Response {
20 | return context.NewResponse().SetContent(s).SetContentType("text/plain")
21 | }
22 |
23 | // Text Create a new HTTP Response with HTML data
24 | func Html(s string) *context.Response {
25 | return context.NewResponse().SetContent(s)
26 | }
27 |
28 | func Response(v interface{}) *context.Response {
29 | r := context.NewResponse()
30 | r.SetContentType("text/plain")
31 |
32 | var content string
33 | t := reflect.TypeOf(v)
34 | switch t.Kind() {
35 | case reflect.Bool:
36 | content = fmt.Sprintf("%t", v)
37 | case reflect.String:
38 | content = fmt.Sprintf("%s", v)
39 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
40 | reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
41 | content = fmt.Sprintf("%d", v)
42 | case reflect.Float32, reflect.Float64:
43 | content = fmt.Sprintf("%v", v)
44 | default:
45 | r.SetContentType("application/json")
46 | b, err := json.Marshal(v)
47 | if err != nil {
48 | panic(errors.New(`The Response content must be a string, numeric, boolean, slice, map or can be encoded as json a string, "` + t.Name() + `" given.`))
49 | }
50 | content = string(b)
51 | }
52 | r.SetContent(content)
53 |
54 | return r
55 | }
56 |
--------------------------------------------------------------------------------
/think/route_hendler.go:
--------------------------------------------------------------------------------
1 | package think
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/context"
5 | "github.com/forgoer/thinkgo/router"
6 | )
7 |
8 | type RouteHandler struct {
9 | Route *router.Route
10 | }
11 |
12 | // NewRouteHandler The default RouteHandler
13 | func NewRouteHandler(app *Application) Handler {
14 | return &RouteHandler{
15 | Route: app.GetRoute(),
16 | }
17 | }
18 |
19 | // Process Process the request to a router and return the response.
20 | func (h *RouteHandler) Process(request *context.Request, next Closure) interface{} {
21 | rule, err := h.Route.Dispatch(request)
22 |
23 | if err != nil {
24 | return context.NotFoundResponse()
25 | }
26 |
27 | return router.RunRoute(request, rule)
28 | }
29 |
--------------------------------------------------------------------------------
/think/session_handler.go:
--------------------------------------------------------------------------------
1 | package think
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/config"
5 | "github.com/forgoer/thinkgo/context"
6 | "github.com/forgoer/thinkgo/session"
7 | )
8 |
9 | type SessionHandler struct {
10 | Manager *session.Manager
11 | app *Application
12 | }
13 |
14 | // SessionHandler The default SessionHandler
15 | func NewSessionHandler(app *Application) Handler {
16 | handler := &SessionHandler{}
17 | handler.Manager = session.NewManager(&session.Config{
18 | Driver: config.Session.Driver,
19 | CookieName: config.Session.CookieName,
20 | Lifetime: config.Session.Lifetime,
21 | Encrypt: config.Session.Encrypt,
22 | Files: config.Session.Files,
23 | })
24 |
25 | handler.app = app
26 |
27 | return handler
28 | }
29 |
30 | func (h *SessionHandler) Process(req *context.Request, next Closure) interface{} {
31 | store := h.startSession(req)
32 |
33 | req.SetSession(store)
34 |
35 | result := next(req)
36 |
37 | if res, ok := result.(session.Response); ok {
38 | h.saveSession(res)
39 | }
40 |
41 | return result
42 | }
43 |
44 | func (h *SessionHandler) startSession(req *context.Request) *session.Store {
45 | return h.Manager.SessionStart(req)
46 | }
47 |
48 | func (h *SessionHandler) saveSession(res session.Response) {
49 | h.Manager.SessionSave(res)
50 | }
51 |
--------------------------------------------------------------------------------
/think/type.go:
--------------------------------------------------------------------------------
1 | package think
2 |
3 | import (
4 | "github.com/forgoer/thinkgo/context"
5 | "github.com/forgoer/thinkgo/router"
6 | )
7 |
8 | type (
9 | Req = context.Request
10 | Res = context.Response
11 |
12 | Route = router.Route
13 | )
14 |
--------------------------------------------------------------------------------
/think_test.go:
--------------------------------------------------------------------------------
1 | package thinkgo
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "github.com/forgoer/thinkgo/context"
7 | "io"
8 | "io/ioutil"
9 | "net/http"
10 | "net/url"
11 | "strings"
12 | "testing"
13 | "time"
14 |
15 | "github.com/stretchr/testify/assert"
16 | "github.com/forgoer/thinkgo/think"
17 | )
18 |
19 | func testRequest(t *testing.T, method, url string, data url.Values, res *think.Res) {
20 | var err error
21 | var resp *http.Response
22 |
23 | tr := &http.Transport{
24 | TLSClientConfig: &tls.Config{
25 | InsecureSkipVerify: true,
26 | },
27 | }
28 | client := &http.Client{Transport: tr}
29 |
30 | var body io.Reader
31 | if strings.ToUpper(method) == "GET" {
32 |
33 | } else {
34 |
35 | }
36 |
37 | contentType := "application/x-www-form-urlencoded"
38 |
39 | method = strings.ToUpper(method)
40 | switch method {
41 | case "GET":
42 | url = strings.TrimRight(url, "?") + "?" + data.Encode()
43 | case "POST", "PUT", "DELETE":
44 | if data != nil {
45 | body = strings.NewReader(data.Encode())
46 | }
47 | }
48 |
49 | req, err := http.NewRequest(method, url, body)
50 | assert.NoError(t, err)
51 |
52 | req.Header.Set("Content-Type", contentType)
53 | resp, err = client.Do(req)
54 | assert.NoError(t, err)
55 |
56 | defer resp.Body.Close()
57 |
58 | content, ioerr := ioutil.ReadAll(resp.Body)
59 | assert.NoError(t, ioerr)
60 |
61 | assert.Equal(t, res.GetCode(), resp.StatusCode)
62 | assert.Equal(t, res.GetContent(), string(content))
63 | }
64 |
65 | func TestRunWithPort(t *testing.T) {
66 | th := New()
67 |
68 | go func() {
69 | th.RegisterRoute(func(route *think.Route) {
70 | route.Get("/", func(req *think.Req) *think.Res {
71 | return think.Text("it worked")
72 | })
73 | })
74 | // listen and serve on 0.0.0.0:9011
75 | th.Run(":9012")
76 | }()
77 |
78 | time.Sleep(5 * time.Millisecond)
79 |
80 | testRequest(t, "get", "http://localhost:9012/", nil, context.NewResponse().SetContent("it worked"))
81 | }
82 |
83 | func TestThink_Run(t *testing.T) {
84 | th := New()
85 |
86 | go func() {
87 | th.RegisterRoute(func(route *think.Route) {
88 | route.Get("/", func(req *think.Req) interface{} {
89 | return "it worked"
90 | })
91 | route.Get("/user/{name}", func(req *think.Req, name string) interface{} {
92 | return fmt.Sprintf("Hello %s !", name)
93 | })
94 | route.Post("/user", func(req *think.Req) interface{} {
95 | name, err := req.Post("name")
96 | if err != nil {
97 | panic(err)
98 | }
99 | return fmt.Sprintf("Create %s", name)
100 | })
101 | route.Delete("/user/{name}", func(name string) interface{} {
102 | return fmt.Sprintf("Delete %s", name)
103 | })
104 | })
105 | // listen and serve on 0.0.0.0:9011
106 | th.Run()
107 | }()
108 |
109 | time.Sleep(5 * time.Millisecond)
110 |
111 | testRequest(t, "get", "http://localhost:9011/", nil, context.NewResponse().SetContent("it worked"))
112 | testRequest(t, "get", "http://localhost:9011/user/thinkgo", nil, context.NewResponse().SetContent(fmt.Sprintf("Hello %s !", "thinkgo")))
113 | testRequest(t, "post", "http://localhost:9011/user", url.Values{"name": {"thinkgo"}}, context.NewResponse().SetContent(fmt.Sprintf("Create %s", "thinkgo")))
114 | testRequest(t, "delete", "http://localhost:9011/user/thinkgo", url.Values{"name": {"thinkgo"}}, context.NewResponse().SetContent(fmt.Sprintf("Delete %s", "thinkgo")))
115 | }
116 |
--------------------------------------------------------------------------------
/view/html/layout.html:
--------------------------------------------------------------------------------
1 | {{ define "layout" }}
2 |
3 |
4 |
5 |
6 | {{ .Title }}
7 |
8 |
9 | {{ template "content" .}}
10 |
11 |
12 | {{ end }}
--------------------------------------------------------------------------------
/view/html/tpl.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 | {{ .Message }}
3 | {{ end }}
4 | {{ template "layout" . }}
--------------------------------------------------------------------------------
/view/view.go:
--------------------------------------------------------------------------------
1 | package view
2 |
3 | import (
4 | "bytes"
5 | "html/template"
6 | "path"
7 | )
8 |
9 | // the default View.
10 | var view = &View{}
11 |
12 | type View struct {
13 | tmpl *template.Template
14 | }
15 |
16 | func New() *View {
17 | v := &View{}
18 | return v
19 | }
20 |
21 | // ParseGlob creates a new Template and parses the template definitions from the
22 | // files identified by the pattern, which must match at least one file.
23 | func (v *View) ParseGlob(pattern string) {
24 | v.tmpl = template.Must(template.ParseGlob(pattern))
25 | }
26 |
27 | func (v *View) Render(name string, data interface{}) template.HTML {
28 | tmpl := v.tmpl
29 | if tmpl == nil {
30 | pattern := path.Join(path.Dir(name), "*")
31 | name = path.Base(name)
32 | tmpl = template.Must(template.ParseGlob(pattern))
33 | }
34 | var buf bytes.Buffer
35 | tmpl.ExecuteTemplate(&buf, name, data)
36 |
37 | return template.HTML(buf.String())
38 | }
39 |
40 | func Render(name string, data interface{}) template.HTML {
41 | return view.Render(name, data)
42 | }
43 |
44 | func ParseGlob(pattern string) {
45 | view.tmpl = template.Must(template.ParseGlob(pattern))
46 | }
47 |
--------------------------------------------------------------------------------
/view/view_test.go:
--------------------------------------------------------------------------------
1 | package view
2 |
3 | import (
4 | "github.com/stretchr/testify/assert"
5 | "html/template"
6 | "testing"
7 | )
8 |
9 | func TestView_Render(t *testing.T) {
10 | var v *View
11 | var content template.HTML
12 | v = New()
13 | content = v.Render("html/tpl.html", map[string]interface{}{
14 | "Title": "ThinkGo",
15 | "Message": "Hello ThinkGo !",
16 | })
17 |
18 | assert.Contains(t, content, "Hello ThinkGo !
")
19 | assert.Contains(t, content, "ThinkGo")
20 |
21 | v = New()
22 | v.ParseGlob("html/*")
23 | content = v.Render("tpl.html", map[string]interface{}{
24 | "Title": "ThinkGo",
25 | "Message": "Hello ThinkGo !",
26 | })
27 |
28 | assert.Contains(t, content, "Hello ThinkGo !
")
29 | assert.Contains(t, content, "ThinkGo")
30 | }
31 |
--------------------------------------------------------------------------------