├── .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 | Build Status 11 | 12 | 13 | Coverage Status 14 | 15 |

16 |

17 | 18 | Go Report Card 19 | 20 | 21 | 22 | 23 | 24 | GoDoc 25 | 26 | 27 | Open Source Helpers 28 | 29 | 30 | Join the chat 31 | 32 | 33 | Latest Stable Version 34 | 35 | 36 | License 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 | --------------------------------------------------------------------------------