go vendor
This commit is contained in:
1 parent
2856da6888
commit
0fb92efbf3
670 files changed
+199010
No files matched your search
+6
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
# inetelli G idea.jet.brainz
|
||||
*.iml
|
||||
.idea/
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Modules Overview
|
||||
section: modules
|
||||
---
|
||||
|
||||
Revel Modules
|
||||
==================================
|
||||
|
||||
## Core Modules
|
||||
|
||||
These modules are part of the core revel and a dependancy
|
||||
|
||||
- [Static](static/) = Serves static files
|
||||
- [Jobs](jobs/) = The Cron Jobs runner
|
||||
- [TestRunner](testrunner/) = Test sweet
|
||||
|
||||
## Upcoming
|
||||
|
||||
- [csrf](csrf/)
|
||||
- [pprof](/pprof/)
|
||||
|
||||
|
||||
## Xperimental
|
||||
|
||||
- [Auth](/auth/)
|
||||
- [Db](/db/)
|
||||
- [Gorm ORM](/orm/gorm)
|
||||
|
||||
### Caution
|
||||
this is a work in progress
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
modules/auth
|
||||
===============
|
||||
|
||||
Basic user/auth module
|
||||
|
||||
This should be modeled after [flask-security](https://github.com/mattupstate/flask-security)
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
// "errors"
|
||||
)
|
||||
|
||||
var (
|
||||
Store StorageDriver
|
||||
)
|
||||
|
||||
// Store = gormauth.NewGormAuthDriver()
|
||||
|
||||
type UserAuth interface {
|
||||
// getters/setters implemented by the app-level model
|
||||
UserId() string
|
||||
Secret() string
|
||||
HashedSecret() string
|
||||
SetHashedSecret(string)
|
||||
|
||||
SecretDriver
|
||||
// // implemented by secret driver
|
||||
// Authenticate() (bool, error)
|
||||
}
|
||||
|
||||
type SecretDriver interface {
|
||||
Authenticate() (bool, error)
|
||||
HashSecret(args ...interface{}) (string, error)
|
||||
|
||||
// stuff for documentation
|
||||
// UserContext is expected in these?
|
||||
|
||||
// Secret expects 0 or non-0 arguments
|
||||
// When no parameter is passed, it acts as a getter
|
||||
// When one or more parameters are passed, it acts as a setter
|
||||
// A driver should specify the expected arguments and their meanings
|
||||
|
||||
// Register()
|
||||
// Login()
|
||||
// Logout()
|
||||
}
|
||||
|
||||
type StorageDriver interface {
|
||||
Save(user interface{}) error
|
||||
// Load should take a partially filled struct
|
||||
// (with values needed to look up)
|
||||
// and fills in the rest
|
||||
Load(user interface{}) error
|
||||
}
|
||||
|
||||
// func init() {
|
||||
// // auth.Store = gorm...
|
||||
// }
|
||||
|
||||
// func (c App) Login(email, password string) {
|
||||
|
||||
// u := User {
|
||||
// Email ...
|
||||
// }
|
||||
|
||||
// good, err := auth.Authenticate(email, password)
|
||||
|
||||
// user, err := user_info.GetUserByEmail(email)
|
||||
// }
|
||||
|
||||
// Bycrypt Authenticate() expects a single string argument of the plaintext password
|
||||
// It returns true on success and false if error or password mismatch
|
||||
// func Authenticate(attemptedUser UserAuth) (bool, error) {
|
||||
// // check user in Store
|
||||
// loadedUser, err := Store.Load(attemptedUser.UserId())
|
||||
// if err != nil {
|
||||
// return false, errors.New("User Not Found")
|
||||
// }
|
||||
|
||||
// loadedUser.Authenticate(attemptedUser.Secret())
|
||||
|
||||
// // successfully authenticated
|
||||
// return true, nil
|
||||
// }
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/revel/modules/auth"
|
||||
"github.com/revel/modules/auth/driver/secret"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
email string
|
||||
password string
|
||||
hashpass string
|
||||
|
||||
secret.BcryptAuth // SecurityDriver for testing
|
||||
}
|
||||
|
||||
func NewUser(email, pass string) *User {
|
||||
u := &User{
|
||||
email: email,
|
||||
password: pass,
|
||||
}
|
||||
u.UserContext = u
|
||||
return u
|
||||
}
|
||||
|
||||
func (self *User) UserId() string {
|
||||
return self.email
|
||||
}
|
||||
|
||||
func (self *User) Secret() string {
|
||||
return self.password
|
||||
}
|
||||
|
||||
func (self *User) HashedSecret() string {
|
||||
return self.hashpass
|
||||
}
|
||||
|
||||
func (self *User) SetHashedSecret(hpass string) {
|
||||
self.hashpass = hpass
|
||||
}
|
||||
|
||||
// func (self *User) Load() string
|
||||
|
||||
type TestStore struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (self *TestStore) Save(user interface{}) error {
|
||||
u, ok := user.(*User)
|
||||
if !ok {
|
||||
return errors.New("TestStore.Save() expected arg of type User")
|
||||
}
|
||||
|
||||
hPass, err := u.HashSecret(u.Secret())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self.data[u.UserId()] = hPass
|
||||
|
||||
return nil
|
||||
}
|
||||
func (self *TestStore) Load(user interface{}) error {
|
||||
u, ok := user.(*User)
|
||||
if !ok {
|
||||
return errors.New("TestStore.Load() expected arg of type User")
|
||||
}
|
||||
|
||||
hpass, ok := self.data[u.UserId()]
|
||||
if !ok {
|
||||
return errors.New("Record Not Found")
|
||||
}
|
||||
u.SetHashedSecret(hpass)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPasswordHash(t *testing.T) {
|
||||
auth.Store = &TestStore{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
u := NewUser("demo@domain.com", "demopass")
|
||||
fail := NewUser("demo@domain.com", "")
|
||||
|
||||
var err error
|
||||
u.hashpass, err = u.HashSecret(u.password)
|
||||
if err != nil {
|
||||
t.Errorf("Should have hashed password, get error: %v\n", err)
|
||||
}
|
||||
fail.hashpass, err = fail.HashSecret(fail.password)
|
||||
if err == nil {
|
||||
t.Errorf("Should have failed hashing\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
auth.Store = &TestStore{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
|
||||
// user registered a long time ago
|
||||
u := NewUser("demo@domain.com", "demopass")
|
||||
err := auth.Store.Save(u)
|
||||
if err != nil {
|
||||
t.Errorf("Should have saved user: %v", err)
|
||||
}
|
||||
|
||||
// users now logging in
|
||||
pass := NewUser("demo@domain.com", "demopass")
|
||||
fail := NewUser("demo@domain.com", "invalid")
|
||||
|
||||
// valid user is now trying to login
|
||||
// check user in DB
|
||||
err = auth.Store.Load(pass)
|
||||
if err != nil {
|
||||
t.Errorf("Should have loaded pass user: %v\n", err)
|
||||
}
|
||||
// check credentials
|
||||
ok, err := pass.Authenticate()
|
||||
if !ok || err != nil {
|
||||
t.Errorf("Should have authenticated user")
|
||||
}
|
||||
|
||||
// invalid user is now trying to login
|
||||
err = auth.Store.Load(fail)
|
||||
if err != nil {
|
||||
t.Errorf("Should have loaded fail user")
|
||||
}
|
||||
// this should fail
|
||||
ok, err = fail.Authenticate()
|
||||
if ok || err != nil {
|
||||
t.Errorf("Should have failed to authenticate user: %v\n", err)
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
modules/casbin
|
||||
======
|
||||
|
||||
Casbin authz is an authorization middleware for [Revel](https://github.com/revel/revel), it's based on [https://github.com/casbin/casbin](https://github.com/casbin/casbin).
|
||||
|
||||
## Simple Example
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/casbin/casbin"
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/modules/auth/casbin"
|
||||
)
|
||||
|
||||
var adapter = casbinauth.NewAdapter(params)
|
||||
var enforcer = casbin.NewEnforcer("authz_model.conf", adapter)
|
||||
var casbinModule = casbinauth.NewCasbinModule(enforcer)
|
||||
|
||||
var testFilters = []revel.Filter{
|
||||
casbinModule.AuthzFilter,
|
||||
func(c *revel.Controller, fc []revel.Filter) {
|
||||
c.RenderHTML("OK.")
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
r, _ := http.NewRequest("GET", "/dataset1/resource1", nil)
|
||||
r.SetBasicAuth("alice", "123")
|
||||
w := httptest.NewRecorder()
|
||||
context := revel.NewGoContext(nil)
|
||||
context.Request.SetRequest(r)
|
||||
context.Response.SetResponse(w)
|
||||
c := revel.NewController(context)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The authorization determines a request based on ``{subject, object, action}``, which means what ``subject`` can perform what ``action`` on what ``object``. In this plugin, the meanings are:
|
||||
|
||||
1. ``subject``: the logged-on user name
|
||||
2. ``object``: the URL path for the web resource like "dataset1/item1"
|
||||
3. ``action``: HTTP method like GET, POST, PUT, DELETE, or the high-level actions you defined like "read-file", "write-blog"
|
||||
|
||||
|
||||
For how to write authorization policy and other details, please refer to [the Casbin's documentation](https://github.com/casbin/casbin).
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package casbinauthz
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/casbin/casbin/model"
|
||||
"github.com/casbin/casbin/persist"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/revel/modules/orm/gorm/app"
|
||||
)
|
||||
|
||||
type Line struct {
|
||||
PType string `gorm:"size:100"`
|
||||
V0 string `gorm:"size:100"`
|
||||
V1 string `gorm:"size:100"`
|
||||
V2 string `gorm:"size:100"`
|
||||
V3 string `gorm:"size:100"`
|
||||
V4 string `gorm:"size:100"`
|
||||
V5 string `gorm:"size:100"`
|
||||
}
|
||||
|
||||
// Adapter represents the Gorm adapter for policy storage.
|
||||
type Adapter struct {
|
||||
driverName string
|
||||
dataSourceName string
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdapter is the constructor for Adapter.
|
||||
func NewAdapter(params gormdb.DbInfo) *Adapter {
|
||||
a := &Adapter{}
|
||||
gormdb.InitDBWithParameters(params)
|
||||
a.db = gormdb.DB
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Adapter) createTable() {
|
||||
if a.db.HasTable(&Line{}) {
|
||||
return
|
||||
}
|
||||
|
||||
err := a.db.CreateTable(&Line{}).Error
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) dropTable() {
|
||||
err := a.db.DropTable(&Line{}).Error
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadPolicyLine(line Line, model model.Model) {
|
||||
lineText := line.PType
|
||||
if line.V0 != "" {
|
||||
lineText += ", " + line.V0
|
||||
}
|
||||
if line.V1 != "" {
|
||||
lineText += ", " + line.V1
|
||||
}
|
||||
if line.V2 != "" {
|
||||
lineText += ", " + line.V2
|
||||
}
|
||||
if line.V3 != "" {
|
||||
lineText += ", " + line.V3
|
||||
}
|
||||
if line.V4 != "" {
|
||||
lineText += ", " + line.V4
|
||||
}
|
||||
if line.V5 != "" {
|
||||
lineText += ", " + line.V5
|
||||
}
|
||||
|
||||
persist.LoadPolicyLine(lineText, model)
|
||||
}
|
||||
|
||||
// LoadPolicy loads policy from database.
|
||||
func (a *Adapter) LoadPolicy(model model.Model) error {
|
||||
var lines []Line
|
||||
err := a.db.Find(&lines).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
loadPolicyLine(line, model)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func savePolicyLine(ptype string, rule []string) Line {
|
||||
line := Line{}
|
||||
|
||||
line.PType = ptype
|
||||
if len(rule) > 0 {
|
||||
line.V0 = rule[0]
|
||||
}
|
||||
if len(rule) > 1 {
|
||||
line.V1 = rule[1]
|
||||
}
|
||||
if len(rule) > 2 {
|
||||
line.V2 = rule[2]
|
||||
}
|
||||
if len(rule) > 3 {
|
||||
line.V3 = rule[3]
|
||||
}
|
||||
if len(rule) > 4 {
|
||||
line.V4 = rule[4]
|
||||
}
|
||||
if len(rule) > 5 {
|
||||
line.V5 = rule[5]
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
// SavePolicy saves policy to database.
|
||||
func (a *Adapter) SavePolicy(model model.Model) error {
|
||||
a.dropTable()
|
||||
a.createTable()
|
||||
|
||||
for ptype, ast := range model["p"] {
|
||||
for _, rule := range ast.Policy {
|
||||
line := savePolicyLine(ptype, rule)
|
||||
err := a.db.Create(&line).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ptype, ast := range model["g"] {
|
||||
for _, rule := range ast.Policy {
|
||||
line := savePolicyLine(ptype, rule)
|
||||
err := a.db.Create(&line).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPolicy adds a policy rule to the storage.
|
||||
func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
// RemovePolicy removes a policy rule from the storage.
|
||||
func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
|
||||
func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package casbinauthz
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/casbin/casbin"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
type CasbinModule struct {
|
||||
enforcer *casbin.Enforcer
|
||||
}
|
||||
|
||||
func NewCasbinModule(enforcer *casbin.Enforcer) *CasbinModule {
|
||||
cm := &CasbinModule{}
|
||||
cm.enforcer = enforcer
|
||||
return cm
|
||||
}
|
||||
|
||||
// AuthzFilter enables the authorization based on Casbin.
|
||||
//
|
||||
// Usage:
|
||||
// 1) Add `casbin.AuthzFilter` to the app's filters (it must come after the authentication).
|
||||
// 2) Init the Casbin enforcer.
|
||||
func (cm *CasbinModule) AuthzFilter(c *revel.Controller, fc []revel.Filter) {
|
||||
if !CheckPermission(cm.enforcer, c.Request) {
|
||||
c.Result = c.Forbidden("Access denied by the Authz plugin.")
|
||||
return
|
||||
} else {
|
||||
fc[0](c, fc[1:])
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserName gets the user name from the request.
|
||||
// Currently, only HTTP basic authentication is supported
|
||||
func GetUserName(r *revel.Request) string {
|
||||
req := r.In.GetRaw().(*http.Request)
|
||||
username, _, _ := req.BasicAuth()
|
||||
return username
|
||||
}
|
||||
|
||||
// CheckPermission checks the user/method/path combination from the request.
|
||||
// Returns true (permission granted) or false (permission forbidden)
|
||||
func CheckPermission(e *casbin.Enforcer, r *revel.Request) bool {
|
||||
user := GetUserName(r)
|
||||
method := r.Method
|
||||
path := r.URL.Path
|
||||
return e.Enforce(user, path, method)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
p, alice, /dataset1/*, GET
|
||||
p, alice, /dataset1/resource1, POST
|
||||
p, bob, /dataset2/resource1, *
|
||||
p, bob, /dataset2/resource2, GET
|
||||
p, bob, /dataset2/folder1/*, POST
|
||||
p, dataset1_admin, /dataset1/*, *
|
||||
g, cathy, dataset1_admin
|
||||
|
+129
@@ -0,0 +1,129 @@
|
||||
package casbinauthz
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/casbin/casbin"
|
||||
"github.com/casbin/casbin/util"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/modules/orm/gorm/app"
|
||||
)
|
||||
|
||||
var adapter = NewAdapter(DefaultDbParams())
|
||||
var enforcer = casbin.NewEnforcer("authz_model.conf", adapter)
|
||||
var casbinModule = NewCasbinModule(enforcer)
|
||||
|
||||
var testFilters = []revel.Filter{
|
||||
casbinModule.AuthzFilter,
|
||||
func(c *revel.Controller, fc []revel.Filter) {
|
||||
c.RenderHTML("OK.")
|
||||
},
|
||||
}
|
||||
|
||||
func DefaultDbParams() gormdb.DbInfo {
|
||||
params := gormdb.DbInfo{}
|
||||
params.DbDriver = "mysql"
|
||||
params.DbHost = "(localhost:3306)"
|
||||
params.DbUser = "root"
|
||||
params.DbPassword = ""
|
||||
params.DbName = "casbin"
|
||||
return params
|
||||
}
|
||||
|
||||
func testRequest(t *testing.T, user string, path string, method string, code int) {
|
||||
r, _ := http.NewRequest(method, path, nil)
|
||||
r.SetBasicAuth(user, "123")
|
||||
w := httptest.NewRecorder()
|
||||
context := revel.NewGoContext(nil)
|
||||
context.Request.SetRequest(r)
|
||||
context.Response.SetResponse(w)
|
||||
c := revel.NewController(context)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != code {
|
||||
t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, c.Response.Status, code)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetPolicy(t *testing.T, e *casbin.Enforcer, res [][]string) {
|
||||
myRes := e.GetPolicy()
|
||||
log.Print("Policy: ", myRes)
|
||||
|
||||
if !util.Array2DEquals(res, myRes) {
|
||||
t.Error("Policy: ", myRes, ", supposed to be ", res)
|
||||
}
|
||||
}
|
||||
|
||||
func initPolicy(t *testing.T) {
|
||||
// Because the DB is empty at first,
|
||||
// so we need to load the policy from the file adapter (.CSV) first.
|
||||
e := casbin.NewEnforcer("authz_model.conf", "authz_policy.csv")
|
||||
|
||||
a := NewAdapter(DefaultDbParams())
|
||||
// This is a trick to save the current policy to the DB.
|
||||
// We can't call e.SavePolicy() because the adapter in the enforcer is still the file adapter.
|
||||
// The current policy means the policy in the Casbin enforcer (aka in memory).
|
||||
err := a.SavePolicy(e.GetModel())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
// Initialize some policy in DB.
|
||||
initPolicy(t)
|
||||
// Note: you don't need to look at the above code
|
||||
// if you already have a working DB with policy inside.
|
||||
|
||||
// Now the DB has policy, so we can provide a normal use case.
|
||||
|
||||
testRequest(t, "alice", "/dataset1/resource1", "GET", 200)
|
||||
testRequest(t, "alice", "/dataset1/resource1", "POST", 200)
|
||||
testRequest(t, "alice", "/dataset1/resource2", "GET", 200)
|
||||
testRequest(t, "alice", "/dataset1/resource2", "POST", 403)
|
||||
}
|
||||
|
||||
func TestPathWildcard(t *testing.T) {
|
||||
// Initialize some policy in DB.
|
||||
initPolicy(t)
|
||||
// Note: you don't need to look at the above code
|
||||
// if you already have a working DB with policy inside.
|
||||
|
||||
// Now the DB has policy, so we can provide a normal use case.
|
||||
|
||||
testRequest(t, "bob", "/dataset2/resource1", "GET", 200)
|
||||
testRequest(t, "bob", "/dataset2/resource1", "POST", 200)
|
||||
testRequest(t, "bob", "/dataset2/resource1", "DELETE", 200)
|
||||
testRequest(t, "bob", "/dataset2/resource2", "GET", 200)
|
||||
testRequest(t, "bob", "/dataset2/resource2", "POST", 403)
|
||||
testRequest(t, "bob", "/dataset2/resource2", "DELETE", 403)
|
||||
|
||||
testRequest(t, "bob", "/dataset2/folder1/item1", "GET", 403)
|
||||
testRequest(t, "bob", "/dataset2/folder1/item1", "POST", 200)
|
||||
testRequest(t, "bob", "/dataset2/folder1/item1", "DELETE", 403)
|
||||
testRequest(t, "bob", "/dataset2/folder1/item2", "GET", 403)
|
||||
testRequest(t, "bob", "/dataset2/folder1/item2", "POST", 200)
|
||||
testRequest(t, "bob", "/dataset2/folder1/item2", "DELETE", 403)
|
||||
}
|
||||
|
||||
func TestRBAC(t *testing.T) {
|
||||
// Initialize some policy in DB.
|
||||
initPolicy(t)
|
||||
// Note: you don't need to look at the above code
|
||||
// if you already have a working DB with policy inside.
|
||||
|
||||
// Now the DB has policy, so we can provide a normal use case.
|
||||
|
||||
// cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role.
|
||||
testRequest(t, "cathy", "/dataset1/item", "GET", 200)
|
||||
testRequest(t, "cathy", "/dataset1/item", "POST", 200)
|
||||
testRequest(t, "cathy", "/dataset1/item", "DELETE", 200)
|
||||
testRequest(t, "cathy", "/dataset2/item", "GET", 403)
|
||||
testRequest(t, "cathy", "/dataset2/item", "POST", 403)
|
||||
testRequest(t, "cathy", "/dataset2/item", "DELETE", 403)
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/* A basic user authentication module for Revel
|
||||
|
||||
list of concerns:
|
||||
- Separating out the interface and driver
|
||||
- Removing DB/Storage dependency
|
||||
- UUID as default identifier?
|
||||
- how to deal with password/secret or generally, method of authorization
|
||||
- default {views,controllers,routes} for register/login/logut ?
|
||||
- reset password in most basic ?
|
||||
- activation (and other features) in a second / more sophisticated driver
|
||||
- filter for checking that user is authenticated
|
||||
|
||||
|
||||
I think a driver is made up of 2 parts
|
||||
data prep and data storage
|
||||
register and password reset are part of data prep
|
||||
as is the auth hash method
|
||||
they don't care how the data is stored
|
||||
|
||||
then there is the data store
|
||||
|
||||
perhaps each auth user model should instantiate 2 drivers instead of 1?
|
||||
one for data prep components and one for storage
|
||||
so the security driver and the storage driver
|
||||
|
||||
*/
|
||||
package auth
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/revel/modules/auth"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// example implementation of a Revel auth security driver
|
||||
// This driver should be embedded into your app-level User model
|
||||
// It expects your User model to have `Password` and `HashedPassword` string fields
|
||||
//
|
||||
// Your User model also needs to set itself as the UserContext for the BcryptAuth driver
|
||||
//
|
||||
// func NewUser(email, pass string) *User {
|
||||
// u := &User{
|
||||
// email: email,
|
||||
// password: pass,
|
||||
// }
|
||||
// u.UserContext = u
|
||||
// }
|
||||
//
|
||||
type BcryptAuth struct {
|
||||
UserContext auth.UserAuth
|
||||
}
|
||||
|
||||
// Bcrypt Secret() returns the hashed version of the password.
|
||||
// It expects an argument of type string, which is the plain text password
|
||||
func (self *BcryptAuth) HashSecret(args ...interface{}) (string, error) {
|
||||
if auth.Store == nil {
|
||||
return "", errors.New("Auth module StorageDriver not set")
|
||||
}
|
||||
argLen := len(args)
|
||||
if argLen == 0 {
|
||||
// we are getting
|
||||
return string(self.UserContext.HashedSecret()), nil
|
||||
}
|
||||
|
||||
if argLen == 1 {
|
||||
// we are setting
|
||||
password, ok := args[0].(string)
|
||||
if !ok {
|
||||
return "", errors.New("Wrong argument type provided, expected plaintext password as string")
|
||||
}
|
||||
hPass, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
self.UserContext.SetHashedSecret(string(hPass))
|
||||
return self.UserContext.HashedSecret(), nil
|
||||
}
|
||||
|
||||
// bad argument count
|
||||
return "", errors.New("Too many arguments provided, expected one")
|
||||
}
|
||||
|
||||
// Bycrypt Authenticate() expects a single string argument of the plaintext password
|
||||
// It returns true on success and false if error or password mismatch
|
||||
func (self *BcryptAuth) Authenticate() (bool, error) {
|
||||
// check password
|
||||
err := bcrypt.CompareHashAndPassword([]byte(self.UserContext.HashedSecret()), []byte(self.UserContext.Secret()))
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// successfully authenticated
|
||||
return true, nil
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Mysql Auth driver
|
||||
==================
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Postgresql Auth driver
|
||||
==================
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Sqlite Auth driver
|
||||
==================
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package auth
|
||||
|
||||
// var storageDriver auth.StorageDriver // postgres in example
|
||||
|
||||
// type AuthUserModel struct {
|
||||
// userId string
|
||||
// security *SecurityDriver // bcrypt in example
|
||||
|
||||
// }
|
||||
|
||||
// func (self *AuthUserModel) UserId() string {
|
||||
// return self.userId
|
||||
// }
|
||||
|
||||
// func (self *AuthUserModel) Secret() string {
|
||||
// return self.security.Secret()
|
||||
// }
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"html/template"
|
||||
"io"
|
||||
"math"
|
||||
"net/url"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
// allowMethods are HTTP methods that do NOT require a token
|
||||
var allowedMethods = map[string]bool{
|
||||
"GET": true,
|
||||
"HEAD": true,
|
||||
"OPTIONS": true,
|
||||
"TRACE": true,
|
||||
}
|
||||
|
||||
func RandomString(length int) (string, error) {
|
||||
buffer := make([]byte, int(math.Ceil(float64(length)/2)))
|
||||
if _, err := io.ReadFull(rand.Reader, buffer); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
str := hex.EncodeToString(buffer)
|
||||
return str[:length], nil
|
||||
}
|
||||
|
||||
func RefreshToken(c *revel.Controller) {
|
||||
token, err := RandomString(64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Session["csrf_token"] = token
|
||||
}
|
||||
|
||||
// CsrfFilter enables CSRF request token creation and verification.
|
||||
//
|
||||
// Usage:
|
||||
// 1) Add `csrf.CsrfFilter` to the app's filters (it must come after the revel.SessionFilter).
|
||||
// 2) Add CSRF fields to a form with the template tag `{{ csrftoken . }}`. The filter adds a function closure to the `ViewArgs` that can pull out the secret and make the token as-needed, caching the value in the request. Ajax support provided through the `X-CSRFToken` header.
|
||||
func CsrfFilter(c *revel.Controller, fc []revel.Filter) {
|
||||
token, foundToken := c.Session["csrf_token"]
|
||||
|
||||
if !foundToken {
|
||||
RefreshToken(c)
|
||||
}
|
||||
|
||||
referer, refErr := url.Parse(c.Request.Referer())
|
||||
if refErr != nil {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Unable to fetch referer")
|
||||
return
|
||||
}
|
||||
isSameOrigin := sameOrigin(c.Request.URL, referer)
|
||||
|
||||
// If the Request method isn't in the white listed methods
|
||||
if !allowedMethods[c.Request.Method] && !IsExempt(c) {
|
||||
// Token wasn't present at all
|
||||
if !foundToken {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Session token missing.")
|
||||
return
|
||||
}
|
||||
|
||||
// Referer header is invalid
|
||||
if refErr != nil {
|
||||
c.Result = c.Forbidden("REVEL CSRF: HTTP Referer malformed.")
|
||||
return
|
||||
}
|
||||
|
||||
// Same origin
|
||||
if !isSameOrigin {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Same origin mismatch.")
|
||||
return
|
||||
}
|
||||
|
||||
var requestToken string
|
||||
// First check for token in post data
|
||||
if c.Request.Method == "POST" {
|
||||
requestToken = c.Params.Get("csrftoken")
|
||||
}
|
||||
|
||||
// Then check for token in custom headers, as with AJAX
|
||||
if requestToken == "" {
|
||||
requestToken = c.Request.GetHttpHeader("X-CSRFToken")
|
||||
}
|
||||
|
||||
if requestToken == "" || !compareToken(requestToken, token) {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Invalid token.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fc[0](c, fc[1:])
|
||||
|
||||
// Only add token to ViewArgs if the request is: not AJAX, not missing referer header, and is same origin.
|
||||
if c.Request.GetHttpHeader("X-CSRFToken") == "" && isSameOrigin {
|
||||
c.ViewArgs["_csrftoken"] = token
|
||||
}
|
||||
}
|
||||
|
||||
func compareToken(requestToken, token string) bool {
|
||||
// ConstantTimeCompare will panic if the []byte aren't the same length
|
||||
if len(requestToken) != len(token) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(requestToken), []byte(token)) == 1
|
||||
}
|
||||
|
||||
// Validates same origin policy
|
||||
func sameOrigin(u1, u2 *url.URL) bool {
|
||||
return u1.Scheme == u2.Scheme && u1.Host == u2.Host
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.TemplateFuncs["csrftoken"] = func(viewArgs map[string]interface{}) template.HTML {
|
||||
if tokenFunc, ok := viewArgs["_csrftoken"]; !ok {
|
||||
panic("REVEL CSRF: _csrftoken missing from ViewArgs.")
|
||||
} else {
|
||||
return template.HTML(tokenFunc.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func NewTestController(w http.ResponseWriter, r *http.Request) *revel.Controller{
|
||||
context := revel.NewGoContext(nil)
|
||||
context.Request.SetRequest(r)
|
||||
context.Response.SetResponse(w)
|
||||
c := revel.NewController(context)
|
||||
return c
|
||||
}
|
||||
|
||||
var testFilters = []revel.Filter{
|
||||
revel.ParamsFilter,
|
||||
CsrfFilter,
|
||||
func(c *revel.Controller, fc []revel.Filter) {
|
||||
c.RenderHTML("{{ csrftoken . }}")
|
||||
},
|
||||
}
|
||||
|
||||
func TestTokenInSession(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
getRequest, _ := http.NewRequest("GET", "http://www.example.com/", nil)
|
||||
c := NewTestController(resp,getRequest)
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if _, ok := c.Session["csrf_token"]; !ok {
|
||||
t.Fatal("token should be present in session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostWithoutToken(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := NewTestController(resp,postRequest)
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != 403 {
|
||||
t.Fatal("post without token should be forbidden")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoReferrer(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
|
||||
c := NewTestController(resp,postRequest)
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
data := url.Values{}
|
||||
data.Set("csrftoken", token)
|
||||
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", bytes.NewBufferString(data.Encode()))
|
||||
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
|
||||
cnew := NewTestController(resp,formPostRequest)
|
||||
// and replace the old request
|
||||
c.Request = cnew.Request
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != 403 {
|
||||
t.Fatal("post without referer should be forbidden")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefererHttps(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := NewTestController(resp,postRequest)
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
data := url.Values{}
|
||||
data.Set("csrftoken", token)
|
||||
formPostRequest, _ := http.NewRequest("POST", "https://www.example.com/", bytes.NewBufferString(data.Encode()))
|
||||
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
formPostRequest.Header.Add("Referer", "http://www.example.com/")
|
||||
|
||||
cnew := NewTestController(resp,formPostRequest)
|
||||
// and replace the old request
|
||||
c.Request = cnew.Request
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != 403 {
|
||||
t.Fatal("posts to https should have an https referer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderWithToken(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := NewTestController(resp,postRequest)
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
formPostRequest.Header.Add("X-CSRFToken", token)
|
||||
formPostRequest.Header.Add("Referer", "http://www.example.com/")
|
||||
|
||||
cnew := NewTestController(resp,formPostRequest)
|
||||
// and replace the old request
|
||||
c.Request = cnew.Request
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post with http header token should be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormPostWithToken(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := NewTestController(resp,postRequest)
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
data := url.Values{}
|
||||
data.Set("csrftoken", token)
|
||||
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", bytes.NewBufferString(data.Encode()))
|
||||
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
formPostRequest.Header.Add("Referer", "http://www.example.com/")
|
||||
|
||||
cnew := NewTestController(resp,formPostRequest)
|
||||
// and replace the old request
|
||||
c.Request = cnew.Request
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("form post with token should be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoTokenInArgsWhenCORs(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
getRequest, _ := http.NewRequest("GET", "http://www.example1.com/", nil)
|
||||
getRequest.Header.Add("Referer", "http://www.example2.com/")
|
||||
|
||||
c := NewTestController(resp,getRequest)
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if _, ok := c.ViewArgs["_csrftoken"]; ok {
|
||||
t.Fatal("ViewArgs should not contain token when not same origin")
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var (
|
||||
exemptPath = make(map[string]bool)
|
||||
exemptAction = make(map[string]bool)
|
||||
)
|
||||
|
||||
func MarkExempt(route string) {
|
||||
if strings.HasPrefix(route, "/") {
|
||||
// e.g. "/controller/action"
|
||||
exemptPath[strings.ToLower(route)] = true
|
||||
} else if routeParts := strings.Split(route, "."); len(routeParts) == 2 {
|
||||
// e.g. "ControllerName.ActionName"
|
||||
exemptAction[route] = true
|
||||
} else {
|
||||
err := fmt.Sprintf("csrf.MarkExempt() received invalid argument \"%v\". Either provide a path prefixed with \"/\" or controller action in the form of \"ControllerName.ActionName\".", route)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func IsExempt(c *revel.Controller) bool {
|
||||
if _, ok := exemptPath[strings.ToLower(c.Request.GetPath())]; ok {
|
||||
return true
|
||||
} else if _, ok := exemptAction[c.Action]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func TestExemptPath(t *testing.T) {
|
||||
MarkExempt("/Controller/Action")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/Controller/Action", nil)
|
||||
c := NewTestController(resp, postRequest)
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post to csrf exempt action should pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExemptPathCaseInsensitive(t *testing.T) {
|
||||
MarkExempt("/Controller/Action")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/controller/action", nil)
|
||||
c := NewTestController(resp, postRequest)
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post to csrf exempt action should pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExemptAction(t *testing.T) {
|
||||
MarkExempt("Controller.Action")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/Controller/Action", nil)
|
||||
c := NewTestController(resp, postRequest)
|
||||
c.Session = make(revel.Session)
|
||||
c.Action = "Controller.Action"
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post to csrf exempt action should pass")
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
||||
// Revel Framework source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package db module configures a database connection for the application.
|
||||
//
|
||||
// Developers use this module by importing and calling db.Init().
|
||||
// A "Transactional" controller type is provided as a way to import interceptors
|
||||
// that manage the transaction
|
||||
//
|
||||
// In particular, a transaction is begun before each request and committed on
|
||||
// success. If a panic occurred during the request, the transaction is rolled
|
||||
// back. (The application may also roll the transaction back itself.)
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
// Database connection variables
|
||||
var (
|
||||
Db *sql.DB
|
||||
Driver string
|
||||
Spec string
|
||||
)
|
||||
|
||||
// Init method used to initialize DB module on `OnAppStart`
|
||||
func Init() {
|
||||
// Read configuration.
|
||||
var found bool
|
||||
if Driver, found = revel.Config.String("db.driver"); !found {
|
||||
revel.RevelLog.Fatal("db.driver not configured")
|
||||
}
|
||||
if Spec, found = revel.Config.String("db.spec"); !found {
|
||||
revel.RevelLog.Fatal("db.spec not configured")
|
||||
}
|
||||
|
||||
// Open a connection.
|
||||
var err error
|
||||
Db, err = sql.Open(Driver, Spec)
|
||||
if err != nil {
|
||||
revel.RevelLog.Fatal("Open database connection error", "error", err, "driver", Driver, "spec", Spec)
|
||||
}
|
||||
}
|
||||
|
||||
// Transactional definition for database transaction
|
||||
type Transactional struct {
|
||||
*revel.Controller
|
||||
Txn *sql.Tx
|
||||
}
|
||||
|
||||
// Begin a transaction
|
||||
func (c *Transactional) Begin() revel.Result {
|
||||
txn, err := Db.Begin()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Txn = txn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback if it's still going (must have panicked).
|
||||
func (c *Transactional) Rollback() revel.Result {
|
||||
if c.Txn != nil {
|
||||
if err := c.Txn.Rollback(); err != nil {
|
||||
if err != sql.ErrTxDone {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
c.Txn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit the transaction.
|
||||
func (c *Transactional) Commit() revel.Result {
|
||||
if c.Txn != nil {
|
||||
if err := c.Txn.Commit(); err != nil {
|
||||
if err != sql.ErrTxDone {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
c.Txn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.InterceptMethod((*Transactional).Begin, revel.BEFORE)
|
||||
revel.InterceptMethod((*Transactional).Commit, revel.AFTER)
|
||||
revel.InterceptMethod((*Transactional).Rollback, revel.FINALLY)
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
---
|
||||
title: Jobs
|
||||
github:
|
||||
labels:
|
||||
- topic-jobs
|
||||
- topic-runtime
|
||||
---
|
||||
|
||||
The [`Jobs`](https://godoc.org/github.com/revel/modules/jobs/app/jobs) framework for performing work asynchronously, outside of the
|
||||
request flow. This may take the form of [recurring tasks](#jobs) that updates cached data
|
||||
or [one-off tasks](#OneOff) such as sending emails.
|
||||
|
||||
## Activation
|
||||
|
||||
The [`Jobs`](https://godoc.org/github.com/revel/modules/jobs/app/jobs) framework is included as an optional [module](index.html), and is not enabled by default.
|
||||
To activate it, add `module.jobs` to the [app.conf](../manual/appconf.html) file:
|
||||
|
||||
```ini
|
||||
module.jobs = github.com/revel/modules/jobs
|
||||
```
|
||||
|
||||
Additionally, in order to access the job monitoring page, you will need to add
|
||||
this line to the `conf/routes` file, which will insert the `/@jobs` url:
|
||||
|
||||
module:jobs
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
There are some [configuration settings](../manual/appconf.html#jobs) that tell the framework what sort of limitations
|
||||
to place on the jobs that it runs. These are listed below with their default values;
|
||||
|
||||
- [`jobs.pool = 10`](appconf.html#jobspool) - The number of jobs allowed to run simultaneously
|
||||
- [`jobs.selfconcurrent = false`](appconf.html#jobsselfconcurrent) - Allow a job to run only if previous instances are done
|
||||
- [`jobs.acceptproxyaddress = false`](appconf#jobsacceptproxyaddress) - Accept `X-Forwarded-For` header value (which is spoofable) to allow or deny status page access
|
||||
|
||||
## Implementing Jobs
|
||||
|
||||
To create a Job, implement the [`cron.Job`](https://github.com/robfig/cron/) interface. The
|
||||
[`Job`](https://godoc.org/github.com/revel/modules/jobs/app/jobs#Job) interface has the following signature:
|
||||
|
||||
{% highlight go %}
|
||||
// https://github.com/robfig/cron/blob/master/cron.go
|
||||
type Job interface {
|
||||
Run()
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
For example:
|
||||
|
||||
{% highlight go %}
|
||||
type MyJob struct {}
|
||||
|
||||
func (j MyJob) Run() {
|
||||
// Do something
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
## Startup jobs
|
||||
|
||||
To run a task on application startup, use
|
||||
[`revel.OnAppStart()`](https://godoc.org/github.com/revel/revel#OnAppStart) to register a function.
|
||||
Revel runs these tasks serially, before starting the server. Note that this
|
||||
functionality does not actually use the jobs module, but it can be used to
|
||||
submit a job for execution that doesn't block server startup.
|
||||
|
||||
{% highlight go %}
|
||||
func init() {
|
||||
revel.OnAppStart(func() { jobs.Now(populateCache{}) })
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
<a name="RecurringJobs"></a>
|
||||
|
||||
## Recurring Jobs
|
||||
|
||||
Jobs may be scheduled to run on any schedule. There are two options for expressing the schedule:
|
||||
|
||||
1. A cron specification
|
||||
2. A fixed interval
|
||||
|
||||
Revel uses the [`cron library`](https://godoc.org/github.com/revel/cron) to parse the
|
||||
schedule and run the jobs. The library's
|
||||
[README](https://github.com/revel/cron/blob/master/README.md) provides a detailed
|
||||
description of the format accepted.
|
||||
|
||||
Jobs are generally registered using the
|
||||
[`revel.OnAppStart()`](https://godoc.org/github.com/revel/revel#OnAppStart) hook, but they may be
|
||||
registered at any later time as well.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
{% highlight go %}
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/modules/jobs/app/jobs"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ReminderEmails struct {
|
||||
// filtered
|
||||
}
|
||||
|
||||
func (e ReminderEmails) Run() {
|
||||
// Queries the DB
|
||||
// Sends some email
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.OnAppStart(func() {
|
||||
jobs.Schedule("0 0 0 * * ?", ReminderEmails{})
|
||||
jobs.Schedule("@midnight", ReminderEmails{})
|
||||
jobs.Schedule("@every 24h", ReminderEmails{})
|
||||
jobs.Every(24 * time.Hour, ReminderEmails{})
|
||||
})
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
<a name="NamedSchedules"></a>
|
||||
|
||||
## Named schedules
|
||||
|
||||
You can [configure schedules ](appconf.html#jobs) in the [`app.conf`](appconf.html) file and reference them anywhere.
|
||||
This provides an easy way to reuse, and a useful description for crontab specs.
|
||||
|
||||
Here is an example **named cron schedule**, in an [`app.conf`](appconf.html) file:
|
||||
|
||||
cron.workhours_15m = 0 */15 9-17 ? * MON-FRI
|
||||
|
||||
Use the named schedule by referencing it anywhere you would have used a cron spec.
|
||||
|
||||
{% highlight go %}
|
||||
func init() {
|
||||
revel.OnAppStart(func() {
|
||||
jobs.Schedule("cron.workhours_15m", ReminderEmails{})
|
||||
})
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<b>IMPORTANT</b>: The cron schedule's name must begin with <b>cron</b>.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<a name="OneOff"></a>
|
||||
|
||||
## One-off Jobs
|
||||
|
||||
Sometimes it is necessary to do something in response to a user action. In these
|
||||
cases, the jobs module allows you to submit a job to be run a single time.
|
||||
|
||||
The only control offered is how long to wait until the job should be run.
|
||||
|
||||
{% highlight go %}
|
||||
type AppController struct { *revel.Controller }
|
||||
|
||||
func (c AppController) Action() revel.Result {
|
||||
// Handle the request.
|
||||
...
|
||||
|
||||
// Send them email asynchronously, right now.
|
||||
jobs.Now(SendConfirmationEmail{})
|
||||
|
||||
// Or, send them email asynchronously after a minute.
|
||||
jobs.In(time.Minute, SendConfirmationEmail{})
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
## Registering functions
|
||||
|
||||
It is possible to register a `func()` as a job by wrapping it in the [`jobs.Func`](https://godoc.org/github.com/revel/modules/jobs/app/jobs#Func)
|
||||
type. For example:
|
||||
|
||||
{% highlight go %}
|
||||
func sendReminderEmails() {
|
||||
// Query the DB
|
||||
// Send some email
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.OnAppStart(func() {
|
||||
jobs.Schedule("@midnight", jobs.Func(sendReminderEmails))
|
||||
})
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
|
||||
## Job Status
|
||||
|
||||
The jobs module provides a status page (`/@jobs` url) that shows:
|
||||
|
||||
- a list of the scheduled jobs it knows about
|
||||
- the current status; **IDLE** or **RUNNING**
|
||||
- the previous and next run times
|
||||
|
||||
<div class="alert alert-info">For security purposes, the status page is restricted to requests that originate
|
||||
from 127.0.0.1.</div>
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Constrained pool size
|
||||
|
||||
It is possible to configure the job module to limit the number of jobs that are
|
||||
allowed to run at the same time. This allows the developer to restrict the
|
||||
resources that could be potentially in use by asynchronous jobs -- typically
|
||||
interactive responsiveness is valued above asynchronous processing. When a pool
|
||||
is full of running jobs, new jobs block to wait for running jobs to complete.
|
||||
|
||||
**Implementation Note**: The implementation blocks on a channel receive, which is
|
||||
implemented to be [FIFO](http://en.wikipedia.org/wiki/FIFO) for waiting goroutines (but not specified/required to be
|
||||
so). [See here for discussion](https://groups.google.com/forum/?fromgroups=#!topic/golang-nuts/CPwv8WlqKag).
|
||||
|
||||
## Future areas for development
|
||||
|
||||
* Allow access to the job status page with HTTP Basic Authentication credentials
|
||||
* Allow administrators to run scheduled jobs interactively from the status page
|
||||
* Provide more visibility into the job runner, e.g. the pool size, the job queue length, etc.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/revel/cron"
|
||||
"github.com/revel/modules/jobs/app/jobs"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
type Jobs struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
func (c Jobs) Status() revel.Result {
|
||||
remoteAddress := c.Request.RemoteAddr
|
||||
if revel.Config.BoolDefault("jobs.acceptproxyaddress", false) {
|
||||
if proxiedAddress := c.Request.GetHttpHeader("X-Forwarded-For"); proxiedAddress!="" {
|
||||
remoteAddress = proxiedAddress
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(remoteAddress, "127.0.0.1") &&
|
||||
!strings.HasPrefix(remoteAddress, "::1") &&
|
||||
!strings.HasPrefix(remoteAddress, "[::1]") {
|
||||
return c.Forbidden("%s is not local", remoteAddress)
|
||||
}
|
||||
entries := jobs.MainCron.Entries()
|
||||
return c.Render(entries)
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.TemplateFuncs["castjob"] = func(job cron.Job) *jobs.Job {
|
||||
return job.(*jobs.Job)
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var jobLog = revel.AppLog
|
||||
|
||||
func init() {
|
||||
revel.RegisterModuleInit(func(m *revel.Module){
|
||||
jobLog = m.Log
|
||||
})
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/revel/cron"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
Name string
|
||||
inner cron.Job
|
||||
status uint32
|
||||
running sync.Mutex
|
||||
}
|
||||
|
||||
const UnNamed = "(unnamed)"
|
||||
|
||||
func New(job cron.Job) *Job {
|
||||
name := reflect.TypeOf(job).Name()
|
||||
if name == "Func" {
|
||||
name = UnNamed
|
||||
}
|
||||
return &Job{
|
||||
Name: name,
|
||||
inner: job,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Job) Status() string {
|
||||
if atomic.LoadUint32(&j.status) > 0 {
|
||||
return "RUNNING"
|
||||
}
|
||||
return "IDLE"
|
||||
}
|
||||
|
||||
func (j *Job) Run() {
|
||||
// If the job panics, just print a stack trace.
|
||||
// Don't let the whole process die.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if revelError := revel.NewErrorFromPanic(err); revelError != nil {
|
||||
jobLog.Error("Job Recovery ", "error", err, "stack", revelError.Stack)
|
||||
} else {
|
||||
jobLog.Error("Job Recovery ", "error", err, "stack", string(debug.Stack()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if !selfConcurrent {
|
||||
j.running.Lock()
|
||||
defer j.running.Unlock()
|
||||
}
|
||||
|
||||
if workPermits != nil {
|
||||
workPermits <- struct{}{}
|
||||
defer func() { <-workPermits }()
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&j.status, 1)
|
||||
defer atomic.StoreUint32(&j.status, 0)
|
||||
|
||||
j.inner.Run()
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// A job runner for executing scheduled or ad-hoc tasks asynchronously from HTTP requests.
|
||||
//
|
||||
// It adds a couple of features on top of the cron package to make it play nicely with Revel:
|
||||
//
|
||||
// 1. Protection against job panics. (They print to ERROR instead of take down the process)
|
||||
//
|
||||
// 2. (Optional) Limit on the number of jobs that may run simulatenously, to
|
||||
// limit resource consumption.
|
||||
//
|
||||
// 3. (Optional) Protection against multiple instances of a single job running
|
||||
// concurrently. If one execution runs into the next, the next will be queued.
|
||||
//
|
||||
// 4. Cron expressions may be defined in app.conf and are reusable across jobs.
|
||||
//
|
||||
// 5. Job status reporting.
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/revel/cron"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
// Callers can use jobs.Func to wrap a raw func.
|
||||
// (Copying the type to this package makes it more visible)
|
||||
//
|
||||
// For example:
|
||||
// jobs.Schedule("cron.frequent", jobs.Func(myFunc))
|
||||
type Func func()
|
||||
|
||||
func (r Func) Run() { r() }
|
||||
|
||||
func Schedule(spec string, job cron.Job) error {
|
||||
// Look to see if given spec is a key from the Config.
|
||||
if strings.HasPrefix(spec, "cron.") {
|
||||
confSpec, found := revel.Config.String(spec)
|
||||
if !found {
|
||||
jobLog.Panic("Cron spec not found: " + spec)
|
||||
}
|
||||
spec = confSpec
|
||||
}
|
||||
sched, err := cron.Parse(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MainCron.Schedule(sched, New(job))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run the given job at a fixed interval.
|
||||
// The interval provided is the time between the job ending and the job being run again.
|
||||
// The time that the job takes to run is not included in the interval.
|
||||
func Every(duration time.Duration, job cron.Job) {
|
||||
MainCron.Schedule(cron.Every(duration), New(job))
|
||||
}
|
||||
|
||||
// Run the given job right now.
|
||||
func Now(job cron.Job) {
|
||||
go New(job).Run()
|
||||
}
|
||||
|
||||
// Run the given job once, after the given delay.
|
||||
func In(duration time.Duration, job cron.Job) {
|
||||
go func() {
|
||||
time.Sleep(duration)
|
||||
New(job).Run()
|
||||
}()
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"github.com/revel/cron"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
const DefaultJobPoolSize = 10
|
||||
|
||||
var (
|
||||
// Singleton instance of the underlying job scheduler.
|
||||
MainCron *cron.Cron
|
||||
|
||||
// This limits the number of jobs allowed to run concurrently.
|
||||
workPermits chan struct{}
|
||||
|
||||
// Is a single job allowed to run concurrently with itself?
|
||||
selfConcurrent bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
MainCron = cron.New()
|
||||
revel.OnAppStart(func() {
|
||||
if size := revel.Config.IntDefault("jobs.pool", DefaultJobPoolSize); size > 0 {
|
||||
workPermits = make(chan struct{}, size)
|
||||
}
|
||||
selfConcurrent = revel.Config.BoolDefault("jobs.selfconcurrent", false)
|
||||
MainCron.Start()
|
||||
jobLog.Info("Go to /@jobs to see job status.")
|
||||
})
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 12px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border: none;
|
||||
}
|
||||
table td, table th {
|
||||
padding: 4 10px;
|
||||
border: none;
|
||||
}
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Scheduled Jobs</h1>
|
||||
|
||||
<table>
|
||||
<tr><th>Name</th><th>Status</th><th>Last run</th><th>Next run</th></tr>
|
||||
{{range .entries}}
|
||||
{{$job := castjob .Job}}
|
||||
<tr>
|
||||
<td>{{$job.Name}}</td>
|
||||
<td>{{$job.Status}}</td>
|
||||
<td>{{if not .Prev.IsZero}}{{.Prev.Format "2006-01-02 15:04:05"}}{{end}}</td>
|
||||
<td>{{if not .Next.IsZero}}{{.Next.Format "2006-01-02 15:04:05"}}{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
+1
@@ -0,0 +1 @@
|
||||
GET /@jobs Jobs.Status
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package jobs
|
||||
|
||||
// Required for vendoring see golang.org/issue/13832
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
modules/gorm
|
||||
===============
|
||||
|
||||
[Gorm](http://jinzhu.me/gorm) module
|
||||
|
||||
## Activation
|
||||
```ini
|
||||
module.gorm = github.com/revel/modules/orm/gorm
|
||||
```
|
||||
|
||||
## Drivers
|
||||
|
||||
* sqlite3
|
||||
* postgres
|
||||
* mysql
|
||||
|
||||
## Configuration file
|
||||
|
||||
```ini
|
||||
# Database config
|
||||
db.autoinit=true # default=true
|
||||
db.driver=sqlite # mysql, postgres, sqlite3
|
||||
db.host=localhost # Use db.host /tmp/app.db is your driver is sqlite
|
||||
db.user=dbuser
|
||||
db.name=dbname
|
||||
db.password=dbpassword
|
||||
|
||||
```
|
||||
|
||||
## Example usage with transactions
|
||||
```go
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
gormc "github.com/revel/modules/gorm/orm/app/controllers"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
gormc.TxnController
|
||||
}
|
||||
|
||||
type Toy struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (c App) Index() revel.Result {
|
||||
c.Txn.LogMode(true)
|
||||
c.Txn.AutoMigrate(&Toy{})
|
||||
c.Txn.Save(&Toy{Name: "Fidget spinner"})
|
||||
|
||||
return c.Render()
|
||||
}
|
||||
```
|
||||
|
||||
## Example usage without transactions
|
||||
```go
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
gormc "github.com/revel/modules/gorm/orm/app/controllers"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
gormc.Controller
|
||||
}
|
||||
|
||||
type Toy struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (c App) Index() revel.Result {
|
||||
c.DB.LogMode(true)
|
||||
c.DB.AutoMigrate(&Toy{})
|
||||
c.DB.Save(&Toy{Name: "Fidget spinner"})
|
||||
|
||||
return c.Render()
|
||||
}
|
||||
```
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package gormcontroller
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
gormdb "github.com/revel/modules/orm/gorm/app"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
// Controller is a Revel controller with a pointer to the opened database
|
||||
type Controller struct {
|
||||
*revel.Controller
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func (c *Controller) setDB() revel.Result {
|
||||
c.DB = gormdb.DB
|
||||
return nil
|
||||
}
|
||||
|
||||
// TxnController is a Revel controller with database transaction support (begin, commit and rollback)
|
||||
type TxnController struct {
|
||||
*revel.Controller
|
||||
Txn *gorm.DB
|
||||
}
|
||||
|
||||
// Begin begins a DB transaction
|
||||
func (c *TxnController) Begin() revel.Result {
|
||||
|
||||
txn := gormdb.DB.Begin()
|
||||
if txn.Error != nil {
|
||||
c.Log.Panic("Transaction begine error","error",txn.Error)
|
||||
}
|
||||
|
||||
c.Txn = txn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit commits the database transation
|
||||
func (c *TxnController) Commit() revel.Result {
|
||||
if c.Txn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.Txn.Commit()
|
||||
if c.Txn.Error != nil && c.Txn.Error != sql.ErrTxDone {
|
||||
fmt.Println(c.Txn.Error)
|
||||
panic(c.Txn.Error)
|
||||
}
|
||||
|
||||
c.Txn = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback rolls back the transaction (eg. after a panic)
|
||||
func (c *TxnController) Rollback() revel.Result {
|
||||
if c.Txn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.Txn.Rollback()
|
||||
if c.Txn.Error != nil && c.Txn.Error != sql.ErrTxDone {
|
||||
fmt.Println(c.Txn.Error)
|
||||
panic(c.Txn.Error)
|
||||
}
|
||||
|
||||
c.Txn = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.OnAppStart(func() {
|
||||
if revel.Config.BoolDefault("db.autoinit", true) {
|
||||
gormdb.InitDB()
|
||||
revel.InterceptMethod((*TxnController).Begin, revel.BEFORE)
|
||||
revel.InterceptMethod((*TxnController).Commit, revel.AFTER)
|
||||
revel.InterceptMethod((*TxnController).Rollback, revel.FINALLY)
|
||||
|
||||
revel.InterceptMethod((*Controller).setDB, revel.BEFORE)
|
||||
}
|
||||
})
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package gormdb
|
||||
|
||||
// # Database config
|
||||
// db.driver=sqlite3 # mysql, postgres, sqlite3
|
||||
// db.host=localhost # Use dbhost /tmp/app.db is your driver is sqlite
|
||||
// db.user=dbuser
|
||||
// db.name=dbname
|
||||
// db.password=dbpassword
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql" // mysql package
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres" // postgres package
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite" // mysql package
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
// DB Gorm
|
||||
var (
|
||||
DB *gorm.DB
|
||||
gormLog = revel.AppLog
|
||||
)
|
||||
func init() {
|
||||
revel.RegisterModuleInit(func(m *revel.Module){
|
||||
gormLog = m.Log
|
||||
})
|
||||
}
|
||||
|
||||
// InitDB database
|
||||
func OpenDB(dbDriver string, dbInfo string) {
|
||||
db, err := gorm.Open(dbDriver, dbInfo)
|
||||
if err != nil {
|
||||
gormLog.Fatal("sql.Open failed", "error", err)
|
||||
}
|
||||
DB = db
|
||||
}
|
||||
|
||||
type DbInfo struct {
|
||||
DbDriver string
|
||||
DbHost string
|
||||
DbUser string
|
||||
DbPassword string
|
||||
DbName string
|
||||
}
|
||||
|
||||
func InitDBWithParameters(params DbInfo) {
|
||||
dbInfo := ""
|
||||
switch params.DbDriver {
|
||||
default:
|
||||
dbInfo = fmt.Sprintf(params.DbHost)
|
||||
case "postgres":
|
||||
dbInfo = fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable password=%s", params.DbHost, params.DbUser, params.DbName, params.DbPassword)
|
||||
case "mysql":
|
||||
dbInfo = fmt.Sprintf("%s:%s@%s/%s?charset=utf8&parseTime=True&loc=Local", params.DbUser, params.DbPassword, params.DbHost, params.DbName)
|
||||
}
|
||||
OpenDB(params.DbDriver, dbInfo)
|
||||
|
||||
}
|
||||
|
||||
func InitDB() {
|
||||
params := DbInfo{}
|
||||
params.DbDriver = revel.Config.StringDefault("db.driver", "sqlite3")
|
||||
params.DbHost = revel.Config.StringDefault("db.host", "localhost")
|
||||
if params.DbDriver == "sqlite3" && params.DbHost == "localhost" {
|
||||
params.DbHost = "/tmp/app.db"
|
||||
}
|
||||
params.DbUser = revel.Config.StringDefault("db.user", "default")
|
||||
params.DbPassword = revel.Config.StringDefault("db.password", "")
|
||||
params.DbName = revel.Config.StringDefault("db.name", "default")
|
||||
|
||||
InitDBWithParameters(params)
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package gorm
|
||||
|
||||
// Required for vendoring see golang.org/issue/13832
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
modules/gorp
|
||||
===============
|
||||
|
||||
This composite orm module combines Gorp with Squirrel to give you a complete solution
|
||||
* [Gorp](https://github.com/go-gorp/gorp) *saves you time, minimizes the drudgery of
|
||||
getting data in and out of your database, and helps your code focus on algorithms,
|
||||
not infrastructure*.
|
||||
* [Squirrel](https://github.com/Masterminds/squirrel) *helps you build SQL queries from
|
||||
composable parts*
|
||||
|
||||
## Activation
|
||||
```ini
|
||||
module.gorp = github.com/revel/modules/orm/gorp
|
||||
```
|
||||
|
||||
## Drivers
|
||||
|
||||
* sqlite3
|
||||
* postgres
|
||||
* mysql
|
||||
|
||||
## Configuration file
|
||||
|
||||
```ini
|
||||
# Database config
|
||||
db.autoinit=true # default=true
|
||||
db.driver=postgres # mysql, postgres, sqlite3
|
||||
|
||||
# The database connection properties individually or use the db.connection
|
||||
db.host=localhost # Use db.host /tmp/app.db is your driver is sqlite
|
||||
db.user=dbuser
|
||||
db.name=dbname
|
||||
db.password=dbpassword
|
||||
|
||||
# Database connection string (host, user, dbname and other params)
|
||||
db.connection=localhost port=8500 user=user dbname=mydb sslmode=disable password=ack
|
||||
# If true then the database will be initialized on startup.
|
||||
db.autoinit=true
|
||||
```
|
||||
## Decelerations
|
||||
A global `Db *DbGorp` object is created in `github.com/revel/modules/gorp/app`.
|
||||
The `Db` is initialized from the app.conf if `db.autoinit=true`.
|
||||
```go
|
||||
// DB Gorp
|
||||
type DbGorp struct {
|
||||
Gorp *gorp.DbMap
|
||||
// The Sql statement builder to use to build select statements
|
||||
SqlStatementBuilder sq.StatementBuilderType
|
||||
}
|
||||
|
||||
var (
|
||||
// The database map to use to populate data
|
||||
Db = &DbGorp{}
|
||||
)
|
||||
|
||||
```
|
||||
## Usage
|
||||
If `db.autoinit=true` in app.conf then you can add your tables to Gorp on app start.
|
||||
Note that the tables are added as a function using `gorp.Db.SetDbInit` - this is for database thread pooling
|
||||
```go
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/modules/gorp/app"
|
||||
)
|
||||
func init() {
|
||||
revel.OnAppStart(func(){
|
||||
// Register tables
|
||||
gorp.Db.SetDbInit(func(dbGorp *gorp.DbGorp) error {
|
||||
// Register tables
|
||||
gorp.Db.Map.AddTableWithName(model.MyTable{}, "my_table")
|
||||
return nil
|
||||
})
|
||||
},5)
|
||||
}
|
||||
```
|
||||
### Controller
|
||||
Controllers, with the `gorpController.Controller` embedded,
|
||||
have a gorp.DbGorp populated in the `Controller.Db`. This database is
|
||||
the global one that is created on startup
|
||||
|
||||
```go
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/modules/orm/gorp/app/controllers"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
gorpController.Controller
|
||||
}
|
||||
type TableRow struct {
|
||||
Id int `db:"id,int64"`
|
||||
}
|
||||
func (c App) Index() revel.Result {
|
||||
sql,args,_ := c.Db.SqlStatementBuilder.Select("*").From("table").Limit(1).ToSql()
|
||||
row := &TableRow{}
|
||||
if err:= c.Db.Map.SelectOne(row,sql,args...); err!=nil {
|
||||
c.RenderError(err)
|
||||
}
|
||||
return c.Render(row)
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple databases
|
||||
The gorp module can populate a `DbGorp` object for you from a `gorp.DbInfo` object. So if you don't want
|
||||
to use the global database (in the gorp module) you can initialize another anywhere in your project.
|
||||
```go
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/modules/orm/gorp/app"
|
||||
)
|
||||
var (
|
||||
SecondDb = &gorp.DbGorp{}
|
||||
)
|
||||
func init() {
|
||||
revel.OnAppStart(func(){
|
||||
// Create a DbInfo object with a minimum of a driver and other details
|
||||
params := gorp.DbInfo{Driver:"postgres",DbUser:revel.Config.StringDefault("seconddb.user", "default")}
|
||||
secondDb.Info = params
|
||||
if err:=secondDb.InitDb(true); err!=nil {
|
||||
revel.Panicf("Second database failed to open %s", err.Error())
|
||||
}
|
||||
},0)
|
||||
}
|
||||
```
|
||||
|
||||
### Multi Channel Connections
|
||||
This is not connection pooling - this is for distributing work across multiple channels to get a
|
||||
lot of stuff done fast. It creates a bunch workers and each worker has
|
||||
its own connection (On the start of the worker a status is sent in case you want to do some prework).
|
||||
Tasks are sent through the `DbWorkContainer.InputChannel` which distributes the
|
||||
task to whatever worker is available.
|
||||
|
||||
If you are using any tables that requires GORP to have initialized tables you
|
||||
must register the tables using `gorp.Db.SetDbInit`. This is the only way that this service
|
||||
can properly initialize the newly thread created GORP instances. Here is an example.
|
||||
```go
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/modules/gorp/app"
|
||||
)
|
||||
func init() {
|
||||
revel.OnAppStart(func(){
|
||||
// Register tables
|
||||
gorp.Db.SetDbInit(func(dbGorp *gorp.DbGorp) error {
|
||||
// Register tables
|
||||
gorp.Db.Map.AddTableWithName(model.MyTable{}, "my_table")
|
||||
return nil
|
||||
})
|
||||
},5)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
In order to achieve this there is a `gorp.DbWorkerContainer` which is initialized by
|
||||
`NewDbWorker(db *DbGorp, callBack DbCallback, numWorkers int) (container *DbWorkerContainer, err error)`
|
||||
The `DbCallback` can be initialized by
|
||||
`gorp.MakeCallback(status func(phase WorkerPhase, worker *DbWorker), work func(value interface{}, worker *DbWorker)) DbCallback`
|
||||
()the status function is optional) or implemented by your structure
|
||||
|
||||
Once the `gorp.DbWorkerContainer` is created tasks can be submitted to it by using the
|
||||
`gorp.DbWorkerContainer.InputChannel<-task` this will call the `gorp.DbCallback.Work` function
|
||||
passing in an instance of *DbWorker
|
||||
|
||||
```go
|
||||
// Assume sourceDb is a *gorp.DbGorp instance
|
||||
workerPool := gorp.NewDbWorker(d.Db, gorp.MakeCallback(func(phase gorp.WorkerPhase, worker *gorp.DbWorker) {
|
||||
// On start initialize some data to be used later
|
||||
if phase == gorp.Start {
|
||||
dataList, err := model.FetchData(worker.Db)
|
||||
worker.SharedData["dataList"] = dataList
|
||||
|
||||
} else if phase == gorp.JobLongrunning {
|
||||
revel.AppLog.Error("Long running process detected", "worker", worker.Id)
|
||||
}
|
||||
}, func(work interface{}, worker *gorp.DbWorker) {
|
||||
dataList := worker.SharedData["dataList"].(model.DataList)
|
||||
// Whatever is sent into the workerPool.InputChannel<- will be the value
|
||||
value := work.(*SomeObject)
|
||||
|
||||
}), 100)
|
||||
// Set the timeout for watchdog notifications
|
||||
workerPool.LongWorkTimeout=300
|
||||
err := workerPool.Start() // Start a 100 worker threads
|
||||
if err!=nil {
|
||||
return err
|
||||
}
|
||||
defer workerPool.Close(0) // Close, wait for channels to exit (non zero would exit after timeout)
|
||||
// var tasks[] a large list of work to be done
|
||||
tasksBlock := make([]*SomeObject,100)
|
||||
for i,task := range tasks {
|
||||
if i>0 && i%100==0 {
|
||||
workerPool.InputChannel<-taskBlock
|
||||
tasksBlock = make([]*SomeObject)
|
||||
}
|
||||
taskBlock = append(taskBlock, task)
|
||||
}
|
||||
workerPool.InputChannel<-taskBlock
|
||||
// Pool is closed on defer function, it will not return till pool closes
|
||||
return nil
|
||||
```
|
||||
#### Implementation notes
|
||||
If your "work" is short but there is a lot of it then it is highly recommended you pass in lists of
|
||||
items to work on. Channels are great at providing an easy way
|
||||
to move data to and from threads but it is a process of synchronizing between two threads. If you pass
|
||||
in a single object at a time you pay that cost on every row you pass. If you do it once every 10,000
|
||||
rows then the cost is minimal.
|
||||
|
||||
#### WorkParallel function
|
||||
There is a handy function called
|
||||
`WorkParallel(db *DbGorp, tasks []func(worker *DbWorker), returnResults bool, maxNumWorkers int, timeouts int) (err error)`
|
||||
which makes it simple to do create a group of calls
|
||||
```go
|
||||
// Assume sourceDb is a *gorp.DbGorp instance
|
||||
task := func(query string) func(db *gorp.DbWorker) {
|
||||
return func(worker *gorp.DbWorker) {
|
||||
_, e := worker.Db.Map.Exec("ANALYZE " + query)
|
||||
}
|
||||
}
|
||||
gorp.WorkParallel(sourceDb, []func(worker *gorp.DbWorker){
|
||||
task("history"),
|
||||
task("summary"),
|
||||
task("daily"),
|
||||
task("monthly"),
|
||||
}, false, 0, 0)
|
||||
|
||||
```
|
||||
#### Returning data from workers
|
||||
`gorp.DbWorker` contains an OutputChannel which you can send the results back to, you must read
|
||||
from the output the same number of times that you wrote. The output size is the same size as the
|
||||
|
||||
```go
|
||||
// Assume sourceDb is a *gorp.DbGorp instance
|
||||
task := func(query string) func(db *gorp.DbWorker) {
|
||||
return func(worker *gorp.DbWorker) {
|
||||
result, err := worker.Db.Map.Exec("ANALYZE " + query)
|
||||
worker.OutputChannel <- []interface{}{result,err}
|
||||
}
|
||||
}
|
||||
r,e:=gorp.WorkParallel(sourceDb, []func(worker *gorp.DbWorker){
|
||||
task("history"),
|
||||
task("daily"),
|
||||
task("monthly"),
|
||||
},true, 0, 0)
|
||||
|
||||
println(r, e)
|
||||
for _,result := range r {
|
||||
|
||||
key,err:= result.([]interface{})[0],result.([]interface{})[1]
|
||||
fmt.Println("Returned result",key,"error",err)
|
||||
}
|
||||
|
||||
```
|
||||
#### Watchdog Timeouts
|
||||
`gorp.DbWorkerContainer` contains a couple of timeout settings (in seconds) used to monitor
|
||||
the startup and running of the workers for the duration of the container
|
||||
(this is typically called a watchdog timeout).
|
||||
- `StartWorkTimeout` If greater then 0 this is the timeout in seconds that it takes to start a worker.
|
||||
- `LongWorkTimeout` If greater then 0 this is the timeout in seconds that it takes before a
|
||||
notification is sent to the `DbCallbackImplied.StatusFn func(phase WorkerPhase, worker *DbWorker)`
|
||||
if a worker runs past X seconds on a single task.
|
||||
Each worker will have their own watchdog channel and it will send a `gorp.JobLongrunning` and
|
||||
the `gorp.DBWorker` to the status function so you can log or investigate long running processes
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package gorpController
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/revel/modules/orm/gorp/app"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
// Controller definition for database transaction
|
||||
// This controller is only useful if you intend to use the database instance
|
||||
// defined in github.com/revel/modules/orm/gorp/app.Db
|
||||
type Controller struct {
|
||||
*revel.Controller
|
||||
Txn *gorp.Transaction
|
||||
Db *gorp.DbGorp
|
||||
}
|
||||
|
||||
// Begin a transaction
|
||||
func (c *Controller) Begin() revel.Result {
|
||||
c.Db = gorp.Db
|
||||
txn, err := gorp.Db.Begin()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Txn = txn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback if it's still going (must have panicked).
|
||||
func (c *Controller) Rollback() revel.Result {
|
||||
if c.Txn != nil {
|
||||
if err := c.Txn.Rollback(); err != nil {
|
||||
if err != sql.ErrTxDone {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
c.Txn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit the transaction.
|
||||
func (c *Controller) Commit() revel.Result {
|
||||
if c.Txn != nil {
|
||||
if err := c.Txn.Commit(); err != nil {
|
||||
if err != sql.ErrTxDone {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
c.Txn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Run this as soon as possible
|
||||
revel.OnAppStart(func() {
|
||||
if revel.Config.BoolDefault("db.autoinit", false) {
|
||||
if err := gorp.InitDb(gorp.Db); err != nil {
|
||||
// Force a failure
|
||||
revel.RevelLog.Panicf("gorp:Unable to initialize database")
|
||||
}
|
||||
revel.InterceptMethod((*Controller).Begin, revel.BEFORE)
|
||||
revel.InterceptMethod((*Controller).Commit, revel.AFTER)
|
||||
revel.InterceptMethod((*Controller).Rollback, revel.FINALLY)
|
||||
}
|
||||
}, 0,
|
||||
)
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
package gorp
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
sq "gopkg.in/Masterminds/squirrel.v1"
|
||||
"gopkg.in/gorp.v2"
|
||||
"github.com/revel/revel/logger"
|
||||
)
|
||||
|
||||
// DB Gorp
|
||||
type DbGorp struct {
|
||||
Map *gorp.DbMap
|
||||
// The Sql statement builder to use to build select statements
|
||||
SqlStatementBuilder sq.StatementBuilderType
|
||||
// Database connection information
|
||||
Info *DbInfo
|
||||
// The database initialization function
|
||||
dbInitFn func(dbMap *DbGorp) error
|
||||
}
|
||||
|
||||
type DbInfo struct {
|
||||
DbDriver string
|
||||
DbHost string
|
||||
DbUser string
|
||||
DbPassword string
|
||||
DbName string
|
||||
DbConnection string
|
||||
Dialect gorp.Dialect
|
||||
}
|
||||
|
||||
// OpenDb database
|
||||
func (dbGorp *DbGorp) OpenDb() (err error) {
|
||||
db, err := sql.Open(dbGorp.Info.DbDriver, dbGorp.Info.DbConnection)
|
||||
if err != nil {
|
||||
moduleLogger.Fatal("Open Database Error", "error", err)
|
||||
}
|
||||
|
||||
// Create the database map
|
||||
dbGorp.Map = &gorp.DbMap{Db: db, Dialect: dbGorp.Info.Dialect}
|
||||
|
||||
return dbGorp.dbInit()
|
||||
}
|
||||
|
||||
// Create a new database connection and open it from this one
|
||||
func (dbGorp *DbGorp) CloneDb(open bool) (newDb *DbGorp, err error) {
|
||||
dbInfo := *dbGorp.Info
|
||||
newDb = &DbGorp{Info: &dbInfo}
|
||||
newDb.dbInitFn = dbGorp.dbInitFn
|
||||
err = newDb.InitDb(open)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close the database connection
|
||||
func (dbGorp *DbGorp) Begin() (txn *Transaction, err error) {
|
||||
tx,err := dbGorp.Map.Begin()
|
||||
if err!=nil {
|
||||
return
|
||||
}
|
||||
txn = &Transaction{tx}
|
||||
return
|
||||
}
|
||||
|
||||
// Close the database connection
|
||||
func (dbGorp *DbGorp) Close() (err error) {
|
||||
if dbGorp.Map.Db != nil {
|
||||
err = dbGorp.Map.Db.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Called to perform table registration and anything else that needs to be done on a new connection
|
||||
func (dbGorp *DbGorp) dbInit() (err error) {
|
||||
if dbGorp.dbInitFn != nil {
|
||||
err = dbGorp.dbInitFn(dbGorp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Used to specifiy the init function to call when database is initialized
|
||||
// Calls the init function immediately
|
||||
func (dbGorp *DbGorp) SetDbInit(dbInitFn func(dbMap *DbGorp) error) (err error) {
|
||||
dbGorp.dbInitFn = dbInitFn
|
||||
return dbGorp.dbInit()
|
||||
}
|
||||
|
||||
func (dbGorp *DbGorp) Select(i interface{}, builder sq.SelectBuilder) (l []interface{}, err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
list, err := dbGorp.Map.Select(i, query, args...)
|
||||
if err != nil && gorp.NonFatalError(err) {
|
||||
return list, nil
|
||||
}
|
||||
if err==sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (dbGorp *DbGorp) SelectOne(i interface{}, builder sq.SelectBuilder) (err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
err = dbGorp.Map.SelectOne(i, query, args...)
|
||||
if err != nil && gorp.NonFatalError(err) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (dbGorp *DbGorp) SelectInt(builder sq.SelectBuilder) (i int64, err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
i, err = dbGorp.Map.SelectInt(query, args...)
|
||||
}
|
||||
return
|
||||
}
|
||||
func (dbGorp *DbGorp) ExecUpdate(builder sq.UpdateBuilder) (r sql.Result, err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
r, err = dbGorp.Map.Exec(query, args...)
|
||||
}
|
||||
return
|
||||
}
|
||||
func (dbGorp *DbGorp) ExecInsert(builder sq.InsertBuilder) (r sql.Result, err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
r, err = dbGorp.Map.Exec(query, args...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// Shifted some common functions up a level
|
||||
////
|
||||
|
||||
|
||||
func (dbGorp *DbGorp) Insert(list ...interface{}) error {
|
||||
return dbGorp.Map.Insert(list...)
|
||||
}
|
||||
|
||||
func (dbGorp *DbGorp) Update(list ...interface{}) (int64, error) {
|
||||
return dbGorp.Map.Update(list...)
|
||||
}
|
||||
func (dbGorp *DbGorp) Get(i interface{}, keys ...interface{}) (interface{}, error) {
|
||||
return dbGorp.Map.Get(i,keys...)
|
||||
}
|
||||
func (dbGorp *DbGorp) Delete(i ...interface{}) (int64, error) {
|
||||
return dbGorp.Map.Delete(i...)
|
||||
}
|
||||
|
||||
func (dbGorp *DbGorp) TraceOn(log logger.MultiLogger) {
|
||||
dbGorp.Map.TraceOn("",&simpleTrace{log.New("section","gorp")})
|
||||
|
||||
}
|
||||
func (dbGorp *DbGorp) TraceOff() {
|
||||
|
||||
}
|
||||
|
||||
type simpleTrace struct {
|
||||
log logger.MultiLogger
|
||||
}
|
||||
|
||||
func (s *simpleTrace) Printf(format string, v ...interface{}) {
|
||||
s.log.Infof(format,v...)
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package gorp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql" // mysql package
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres" // postgres package
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite" // mysql package
|
||||
"github.com/revel/revel"
|
||||
sq "gopkg.in/Masterminds/squirrel.v1"
|
||||
"gopkg.in/gorp.v2"
|
||||
"github.com/revel/revel/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
// The database map to use to populate data
|
||||
Db = &DbGorp{}
|
||||
moduleLogger logger.MultiLogger
|
||||
)
|
||||
func init() {
|
||||
revel.RegisterModuleInit(func(module *revel.Module){
|
||||
moduleLogger = module.Log
|
||||
moduleLogger.Debug("Assigned Logger")
|
||||
})
|
||||
}
|
||||
func (dbResult *DbGorp)InitDb(open bool) (err error) {
|
||||
dbInfo := dbResult.Info
|
||||
|
||||
switch dbInfo.DbDriver {
|
||||
default:
|
||||
dbResult.SqlStatementBuilder = sq.StatementBuilder.PlaceholderFormat(sq.Question)
|
||||
dbInfo.Dialect = gorp.SqliteDialect{}
|
||||
if len(dbInfo.DbConnection) == 0 {
|
||||
dbInfo.DbConnection = fmt.Sprintf(dbInfo.DbHost)
|
||||
}
|
||||
case "postgres":
|
||||
dbResult.SqlStatementBuilder = sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
dbInfo.Dialect = gorp.PostgresDialect{}
|
||||
if len(dbInfo.DbConnection) == 0 {
|
||||
dbInfo.DbConnection = fmt.Sprintf("host=%s port=8500 user=%s dbname=%s sslmode=disable password=%s", dbInfo.DbHost, dbInfo.DbUser, dbInfo.DbName, dbInfo.DbPassword)
|
||||
}
|
||||
case "mysql":
|
||||
dbResult.SqlStatementBuilder = sq.StatementBuilder.PlaceholderFormat(sq.Question)
|
||||
dbInfo.Dialect = gorp.MySQLDialect{}
|
||||
if len(dbInfo.DbConnection) == 0 {
|
||||
dbInfo.DbConnection = fmt.Sprintf("%s:%s@%s/%s?charset=utf8&parseTime=True&loc=Local", dbInfo.DbUser, dbInfo.DbPassword, dbInfo.DbHost, dbInfo.DbName)
|
||||
}
|
||||
}
|
||||
|
||||
if open {
|
||||
err = dbResult.OpenDb()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize the database from revel.Config
|
||||
func InitDb(dbResult *DbGorp) (error) {
|
||||
params := DbInfo{}
|
||||
params.DbDriver = revel.Config.StringDefault("db.driver", "sqlite3")
|
||||
params.DbHost = revel.Config.StringDefault("db.host", "localhost")
|
||||
if params.DbDriver == "sqlite3" && params.DbHost == "localhost" {
|
||||
params.DbHost = "/tmp/app.db"
|
||||
}
|
||||
params.DbUser = revel.Config.StringDefault("db.user", "default")
|
||||
params.DbPassword = revel.Config.StringDefault("db.password", "")
|
||||
params.DbName = revel.Config.StringDefault("db.name", "default")
|
||||
params.DbConnection = revel.Config.StringDefault("db.connection", "")
|
||||
dbResult.Info = ¶ms
|
||||
|
||||
return dbResult.InitDb(true)
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package gorp
|
||||
import (
|
||||
gorpa "gopkg.in/gorp.v2"
|
||||
"database/sql"
|
||||
sq "gopkg.in/Masterminds/squirrel.v1"
|
||||
)
|
||||
type (
|
||||
// This is a small wrapped around gorp.Transaction so you can make use of the builder statements as well
|
||||
Transaction struct {
|
||||
Map *gorpa.Transaction
|
||||
}
|
||||
)
|
||||
|
||||
func (txn *Transaction) Rollback() (err error) {
|
||||
return txn.Map.Rollback()
|
||||
}
|
||||
func (txn *Transaction) Commit() (err error) {
|
||||
return txn.Map.Commit()
|
||||
}
|
||||
func (txn *Transaction) Select(i interface{}, builder sq.SelectBuilder) (l []interface{}, err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
list, err := txn.Map.Select(i, query, args...)
|
||||
if err != nil && gorpa.NonFatalError(err) {
|
||||
return list, nil
|
||||
}
|
||||
if err==sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (txn *Transaction) SelectOne(i interface{}, builder sq.SelectBuilder) (err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
err = txn.Map.SelectOne(i, query, args...)
|
||||
if err != nil && gorpa.NonFatalError(err) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (txn *Transaction) SelectInt(builder sq.SelectBuilder) (i int64, err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
i, err = txn.Map.SelectInt(query, args...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (txn *Transaction) ExecUpdate(builder sq.UpdateBuilder) (r sql.Result, err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
r, err = txn.Map.Exec(query, args...)
|
||||
}
|
||||
return
|
||||
}
|
||||
func (txn *Transaction) ExecInsert(builder sq.InsertBuilder) (r sql.Result, err error) {
|
||||
query, args, err := builder.ToSql()
|
||||
if err == nil {
|
||||
r, err = txn.Map.Exec(query, args...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Shifted some common functions up a level
|
||||
|
||||
func (txn *Transaction) Insert(list ...interface{}) error {
|
||||
return txn.Map.Insert(list...)
|
||||
}
|
||||
func (txn *Transaction) Update(list ...interface{}) (int64, error) {
|
||||
return txn.Map.Update(list...)
|
||||
}
|
||||
func (txn *Transaction) Get(i interface{}, keys ...interface{}) (interface{}, error) {
|
||||
return txn.Map.Get(i,keys...)
|
||||
}
|
||||
func (txn *Transaction) Delete(i ...interface{}) (int64, error) {
|
||||
return txn.Map.Delete(i...)
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
package gorp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The worker container
|
||||
type DbWorkerContainer struct {
|
||||
SharedWorker
|
||||
mutex sync.Mutex
|
||||
Workers []*DbWorker
|
||||
NumWorkers int
|
||||
LongWorkTimeout int64
|
||||
StartWorkTimeout int64
|
||||
Db *DbGorp
|
||||
}
|
||||
|
||||
// The timeoutInfo for monitoring long running processes
|
||||
type timeoutInfo struct {
|
||||
worker *DbWorker
|
||||
started time.Time
|
||||
ended time.Time
|
||||
state WorkerPhase
|
||||
}
|
||||
type DbWorker struct {
|
||||
Id int
|
||||
Db *DbGorp
|
||||
SharedWorker
|
||||
WorkUnit int
|
||||
SharedData map[string]interface{}
|
||||
TimeInfo *timeoutInfo
|
||||
TimeoutChannel chan *timeoutInfo
|
||||
}
|
||||
type SharedWorker struct {
|
||||
workInfo DbWorkInfo
|
||||
InputChannel chan interface{}
|
||||
OutputChannel chan interface{}
|
||||
ControlChannel chan func() (WorkerPhase, *DbWorker)
|
||||
}
|
||||
type DbWorkInfo interface {
|
||||
Status(phase WorkerPhase, worker *DbWorker)
|
||||
Work(value interface{}, worker *DbWorker)
|
||||
}
|
||||
type DbCallbackImplied struct {
|
||||
StatusFn func(phase WorkerPhase, worker *DbWorker)
|
||||
WorkFn func(value interface{}, worker *DbWorker)
|
||||
}
|
||||
type WorkerPhase int
|
||||
|
||||
const (
|
||||
Start WorkerPhase = iota
|
||||
Stop
|
||||
StartJob
|
||||
EndJob
|
||||
JobLongrunning
|
||||
)
|
||||
// Creates a container to run the group of workers (up to a max of maxNumWorkers), does not return to all workers are completed)
|
||||
// If returnResults is true then the task MUST write to the DbWorker.OutputChannel once for every task
|
||||
func WorkParallel(db *DbGorp, tasks []func(worker *DbWorker), returnResults bool, maxNumWorkers int, timeouts int) (results []interface{}, err error) {
|
||||
if maxNumWorkers == 0 {
|
||||
maxNumWorkers = len(tasks)
|
||||
}
|
||||
|
||||
// Create a container with no status callback
|
||||
container := NewDbWorker(db,
|
||||
MakeCallback(nil,
|
||||
func(value interface{}, worker *DbWorker) {
|
||||
task := value.(func(worker *DbWorker))
|
||||
task(worker)
|
||||
}), maxNumWorkers)
|
||||
err = container.Start()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, task := range tasks {
|
||||
container.InputChannel <- task
|
||||
}
|
||||
if returnResults {
|
||||
for range tasks {
|
||||
result := <-container.OutputChannel
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
|
||||
container.Close(timeouts)
|
||||
return
|
||||
}
|
||||
|
||||
// This creates a DbWorkerContainer with the number of working threads already started.
|
||||
// Each working thread has their own database instance running.
|
||||
func NewDbWorker(db *DbGorp, workInfo DbWorkInfo, numWorkers int) (container *DbWorkerContainer) {
|
||||
|
||||
container = &DbWorkerContainer{
|
||||
SharedWorker: SharedWorker{
|
||||
InputChannel: make(chan interface{}, numWorkers),
|
||||
OutputChannel: make(chan interface{}, numWorkers),
|
||||
ControlChannel: make(chan func() (WorkerPhase, *DbWorker), numWorkers),
|
||||
workInfo: workInfo,
|
||||
},
|
||||
NumWorkers: numWorkers,
|
||||
Db: db,
|
||||
StartWorkTimeout: 0,
|
||||
LongWorkTimeout: 0,
|
||||
}
|
||||
return
|
||||
}
|
||||
func (container *DbWorkerContainer) Start() (err error) {
|
||||
for x := 0; x < container.NumWorkers; x++ {
|
||||
go startWorker(container, container.Db, x)
|
||||
}
|
||||
// Make sure all containers are running before returning
|
||||
for x := 0; x < container.NumWorkers; x++ {
|
||||
if container.StartWorkTimeout > 0 {
|
||||
select {
|
||||
case result := <-container.ControlChannel:
|
||||
state, source := result()
|
||||
if state != Start {
|
||||
container.Close(5)
|
||||
err = fmt.Errorf("Failed to start workers %d", source)
|
||||
return
|
||||
}
|
||||
case <-time.After(time.Second * time.Duration(container.StartWorkTimeout)):
|
||||
container.Close(5)
|
||||
err = fmt.Errorf("Failed to start worker timeout")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
result := <-container.ControlChannel
|
||||
state, source := result()
|
||||
if state != Start {
|
||||
container.Close(5)
|
||||
err = fmt.Errorf("Failed to start workers %d", source)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (container *DbWorkerContainer) Close(timeouts int) (totalWork int, err error) {
|
||||
close(container.InputChannel)
|
||||
for x := 0; x < len(container.Workers); x++ {
|
||||
// Allow close to continue even if a worker does not respond
|
||||
if timeouts > 0 {
|
||||
select {
|
||||
case result := <-container.ControlChannel:
|
||||
_, worker := result()
|
||||
totalWork += worker.WorkUnit
|
||||
case <-time.After(time.Second * time.Duration(timeouts)):
|
||||
}
|
||||
} else {
|
||||
_, worker := (<-container.ControlChannel)()
|
||||
totalWork += worker.WorkUnit
|
||||
}
|
||||
}
|
||||
close(container.OutputChannel)
|
||||
return
|
||||
}
|
||||
|
||||
// Called by using "go" to invoke, creates a DBWorker, and starts a watchdog channel
|
||||
func startWorker(container *DbWorkerContainer, db *DbGorp, id int) {
|
||||
newDb, _ := db.CloneDb(true)
|
||||
worker := &DbWorker{
|
||||
Db: newDb,
|
||||
Id: id,
|
||||
SharedData: map[string]interface{}{},
|
||||
SharedWorker: SharedWorker{
|
||||
workInfo: container.workInfo,
|
||||
InputChannel: container.InputChannel,
|
||||
OutputChannel: container.OutputChannel,
|
||||
ControlChannel: container.ControlChannel,
|
||||
},
|
||||
}
|
||||
// Close the database after worker has ended (Start returned
|
||||
defer worker.Db.Close()
|
||||
container.mutex.Lock()
|
||||
container.Workers = append(container.Workers, worker)
|
||||
container.mutex.Unlock()
|
||||
// Only monitor jobs if Status function defined and a timeout is also defined
|
||||
if worker.workInfo.Status != nil && container.LongWorkTimeout > 0 {
|
||||
worker.TimeoutChannel = make(chan *timeoutInfo)
|
||||
go worker.TimeInfo.start(worker.TimeoutChannel, container.LongWorkTimeout)
|
||||
}
|
||||
worker.start()
|
||||
}
|
||||
|
||||
// Starts the worker, continues running until inputchannel is closed
|
||||
func (worker *DbWorker) start() {
|
||||
if worker.workInfo.Status != nil {
|
||||
worker.workInfo.Status(Start, worker)
|
||||
}
|
||||
worker.ControlChannel <- func() (WorkerPhase, *DbWorker) { return Start, worker }
|
||||
for job := range worker.InputChannel {
|
||||
worker.invoke(job)
|
||||
}
|
||||
if worker.workInfo.Status != nil {
|
||||
worker.workInfo.Status(Stop, worker)
|
||||
}
|
||||
worker.ControlChannel <- func() (WorkerPhase, *DbWorker) { return Stop, worker }
|
||||
if worker.TimeoutChannel != nil {
|
||||
close(worker.TimeoutChannel)
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper to prevent panics from disturbing the channel
|
||||
func (worker *DbWorker) invoke(job interface{}) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
trace := make([]byte, 1024)
|
||||
count := runtime.Stack(trace, true)
|
||||
moduleLogger.Error("Recover from panic: ", "error", err)
|
||||
moduleLogger.Error("Stack", "size", count, "trace", string(trace))
|
||||
}
|
||||
}()
|
||||
// Setup the timeout information
|
||||
if worker.TimeoutChannel != nil {
|
||||
worker.TimeInfo = &timeoutInfo{worker: worker, started: time.Now(), state: StartJob}
|
||||
worker.TimeoutChannel <- worker.TimeInfo
|
||||
}
|
||||
worker.workInfo.Work(job, worker)
|
||||
if worker.TimeoutChannel != nil {
|
||||
worker.TimeInfo.state = EndJob
|
||||
worker.TimeoutChannel <- worker.TimeInfo
|
||||
}
|
||||
}
|
||||
|
||||
// A function to return an object that is a valid DbCallback
|
||||
func MakeCallback(status func(phase WorkerPhase, worker *DbWorker), work func(value interface{}, worker *DbWorker)) DbWorkInfo {
|
||||
return &DbCallbackImplied{StatusFn: status, WorkFn: work}
|
||||
}
|
||||
|
||||
// Call the status function if available
|
||||
func (dbCallback *DbCallbackImplied) Status(phase WorkerPhase, worker *DbWorker) {
|
||||
if dbCallback.StatusFn != nil {
|
||||
dbCallback.StatusFn(phase, worker)
|
||||
}
|
||||
}
|
||||
|
||||
// Calls the work function
|
||||
func (dbCallback *DbCallbackImplied) Work(value interface{}, worker *DbWorker) {
|
||||
dbCallback.WorkFn(value, worker)
|
||||
}
|
||||
|
||||
//Starts the timeout worker
|
||||
func (_ *timeoutInfo) start(TimeoutChannel chan *timeoutInfo, timeout int64) {
|
||||
for j := range TimeoutChannel {
|
||||
j.started = time.Now()
|
||||
j.state = StartJob
|
||||
j.worker.workInfo.Status(j.state, j.worker)
|
||||
for {
|
||||
select {
|
||||
case complete, ok := <-TimeoutChannel:
|
||||
if !ok {
|
||||
// Channel closed returning...
|
||||
return
|
||||
}
|
||||
// Received new State, record and loop
|
||||
complete.worker.workInfo.Status(complete.state, complete.worker)
|
||||
break
|
||||
case <-time.After(time.Second * time.Duration(timeout)):
|
||||
j.worker.workInfo.Status(JobLongrunning, j.worker)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package gorp
|
||||
|
||||
// Required for vendoring see golang.org/issue/13832
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Revel pprof module
|
||||
============
|
||||
|
||||
#### How to use:
|
||||
|
||||
1. Open your app.conf file and add the following line:
|
||||
`module.pprof=github.com/revel/modules/pprof`
|
||||
This will enable the pprof module.
|
||||
|
||||
2. Next, open your routes file and add:
|
||||
`module:pprof` **Note:** Do not change these routes. The pprof command-line tool by default assumes these routes.
|
||||
|
||||
Congrats! You can now profile your application. To use the web interface, visit `http://<host>:<port>/debug/pprof`. You can also use the `go tool pprof` command to profile your application and get a little deeper. Use the command by running `go tool pprof <binary> http://<host>:<port>`. For example, if you modified the booking sample, you would run: `go tool pprof $GOPATH/bin/booking http://localhost:9000` (assuming you used the default `revel run` arguments.
|
||||
|
||||
The command-line tool will take a 30-second CPU profile, and save the results to a temporary file in your `$HOME/pprof` directory. You can reference this file at a later time by using the same command as above, but by specifying the filename instead of the server address.
|
||||
|
||||
In order to fully utilize the command-line tool, you may need the graphviz utilities. If you're on OS X, you can install these easily using Homebrew: `brew install graphviz`.
|
||||
|
||||
To read more about profiling Go programs, here is some reading material: [The Go Blog](http://blog.golang.org/profiling-go-programs) : [net/pprof package documentation](http://golang.org/pkg/net/http/pprof/)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
)
|
||||
|
||||
type Pprof struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// The PprofHandler type makes it easy to call the net/http/pprof handler methods
|
||||
// since they all have the same method signature
|
||||
type PprofHandler func(http.ResponseWriter, *http.Request)
|
||||
|
||||
func (r PprofHandler) Apply(req *revel.Request, resp *revel.Response) {
|
||||
r(resp.Out.Server.GetRaw().(http.ResponseWriter), req.In.GetRaw().(*http.Request))
|
||||
}
|
||||
|
||||
func (c Pprof) Profile() revel.Result {
|
||||
return PprofHandler(pprof.Profile)
|
||||
}
|
||||
|
||||
func (c Pprof) Symbol() revel.Result {
|
||||
return PprofHandler(pprof.Symbol)
|
||||
}
|
||||
|
||||
func (c Pprof) Cmdline() revel.Result {
|
||||
return PprofHandler(pprof.Cmdline)
|
||||
}
|
||||
|
||||
func (c Pprof) Trace() revel.Result {
|
||||
return PprofHandler(pprof.Trace)
|
||||
}
|
||||
|
||||
func (c Pprof) Index() revel.Result {
|
||||
return PprofHandler(pprof.Index)
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
GET /debug/pprof/profile Pprof.Profile
|
||||
GET /pprof/symbol Pprof.Symbol
|
||||
GET /debug/pprof/cmdline Pprof.Cmdline
|
||||
GET /debug/pprof/trace Pprof.Trace
|
||||
GET /debug/pprof/* Pprof.Index
|
||||
GET /debug/pprof/ Pprof.Index
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package pprof
|
||||
|
||||
// Required for vendoring see golang.org/issue/13832
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Server Engines
|
||||
These are some of the server engines built by the Revel team.
|
||||
In order to use an engine you must import the module and set
|
||||
the configuration option `server.engine` equal to the engine you want.
|
||||
You may define your own engine by implementing `revel.ServerEngine` and
|
||||
registering it with revel in an `init`
|
||||
function `revel.RegisterServerEngine(NAME, <function to return your server instance>)`. See
|
||||
`revel.GoHttpServer` for an example
|
||||
|
||||
|
||||
### app.conf
|
||||
- **server.engine** Defaults to `go`. Specify the engine you wish to use
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# FastHTTP
|
||||
This module is the for [FastHTTP](https://github.com/valyala/fasthttp) server engine.
|
||||
It does not support WebSockets.
|
||||
|
||||
### app.conf
|
||||
- **server.engine** You must set this to `fasthttp` in order to use this server engine
|
||||
|
||||
### Other Notes
|
||||
All features from that the go HTTP engine is supported by this server engine.
|
||||
Memory usage is decreased by avoiding memory allocations. This should
|
||||
increase overall runtime performance and throughput.
|
||||
+423
@@ -0,0 +1,423 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/revel/revel"
|
||||
"github.com/valyala/fasthttp"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FastHTTPServer struct {
|
||||
Server *fasthttp.Server
|
||||
ServerInit *revel.EngineInit
|
||||
MaxMultipartSize int64
|
||||
}
|
||||
|
||||
var serverLog = revel.AppLog
|
||||
|
||||
func init() {
|
||||
revel.RegisterServerEngine("fasthttp", func() revel.ServerEngine {
|
||||
return &FastHTTPServer{}
|
||||
})
|
||||
revel.RegisterModuleInit(func(m *revel.Module) {
|
||||
serverLog = m.Log
|
||||
})
|
||||
}
|
||||
|
||||
func (f *FastHTTPServer) Init(init *revel.EngineInit) {
|
||||
f.MaxMultipartSize = int64(revel.Config.IntDefault("server.request.max.multipart.filesize", 32)) << 20 /* 32 MB */
|
||||
fastHttpContextStack = revel.NewStackLock(revel.Config.IntDefault("server.context.stack", 100),
|
||||
revel.Config.IntDefault("server.context.maxstack", 200),
|
||||
func() interface{} { return NewFastHttpContext(f) })
|
||||
fastHttpMultipartFormStack = revel.NewStackLock(revel.Config.IntDefault("server.form.stack", 100),
|
||||
revel.Config.IntDefault("server.form.maxstack", 200),
|
||||
func() interface{} { return &FastHttpMultipartForm{} })
|
||||
|
||||
requestHandler := func(ctx *fasthttp.RequestCtx) {
|
||||
f.RequestHandler(ctx)
|
||||
}
|
||||
f.ServerInit = init
|
||||
f.Server = &fasthttp.Server{
|
||||
ReadTimeout: time.Duration(revel.Config.IntDefault("http.timeout.read", 0)) * time.Second,
|
||||
WriteTimeout: time.Duration(revel.Config.IntDefault("http.timeout.write", 0)) * time.Second,
|
||||
Handler: requestHandler,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Handler is assigned in the Init
|
||||
func (f *FastHTTPServer) Start() {
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
fmt.Printf("\nListening on fasthttp %s...\n", f.ServerInit.Address)
|
||||
}()
|
||||
if revel.HTTPSsl {
|
||||
if f.ServerInit.Network != "tcp" {
|
||||
// This limitation is just to reduce complexity, since it is standard
|
||||
// to terminate SSL upstream when using unix domain sockets.
|
||||
serverLog.Fatal("SSL is only supported for TCP sockets. Specify a port to listen on.")
|
||||
}
|
||||
serverLog.Fatal("Failed to listen https:", "error",
|
||||
f.Server.ListenAndServeTLS(f.ServerInit.Address, revel.HTTPSslCert, revel.HTTPSslKey))
|
||||
} else {
|
||||
listener, err := net.Listen(f.ServerInit.Network, f.ServerInit.Address)
|
||||
if err != nil {
|
||||
serverLog.Fatal("Failed to listen http:", "error", err)
|
||||
}
|
||||
serverLog.Info("Listening fasthttp ", f.ServerInit.Network, f.ServerInit.Address)
|
||||
// revel.ERROR.Fatalln("Failed to serve:", f.Server.ListenAndServe(f.ServerInit.Address))
|
||||
serverLog.Fatal("Server exited", "error", f.Server.Serve(listener))
|
||||
println("***ENDING ***")
|
||||
}
|
||||
|
||||
}
|
||||
func (f *FastHTTPServer) RequestHandler(ctx *fasthttp.RequestCtx) {
|
||||
// TODO this
|
||||
//if maxRequestSize := int64(revel.Config.IntDefault("http.maxrequestsize", 0)); maxRequestSize > 0 {
|
||||
// buffer := &bytes.Buffer{}
|
||||
// err := ctx.Request.ReadLimitBody(buffer,maxRequestSize)
|
||||
// if err!=nil {
|
||||
// // Send the error back to the client
|
||||
// ctx.SetStatusCode(http.StatusRequestEntityTooLarge)
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
context := fastHttpContextStack.Pop().(*FastHttpContext)
|
||||
defer func() {
|
||||
fastHttpContextStack.Push(context)
|
||||
}()
|
||||
context.SetContext(ctx)
|
||||
f.ServerInit.Callback(context)
|
||||
}
|
||||
|
||||
func (f *FastHTTPServer) Event(event int, args interface{}) {
|
||||
|
||||
switch event {
|
||||
case revel.ENGINE_BEFORE_INITIALIZED:
|
||||
case revel.ENGINE_STARTED:
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
func (f *FastHTTPServer) Name() string {
|
||||
return "fasthttp"
|
||||
}
|
||||
func (f *FastHTTPServer) Engine() interface{} {
|
||||
return f
|
||||
}
|
||||
func (g *FastHTTPServer) Stats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"FastHTTP Engine Context": fastHttpContextStack.String(),
|
||||
"FastHTTP Engine Forms": fastHttpMultipartFormStack.String(),
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
FastHttpContext struct {
|
||||
Request *FastHttpRequest
|
||||
Response *FastHttpResponse
|
||||
}
|
||||
|
||||
FastHttpRequest struct {
|
||||
toQuery bool
|
||||
url *url.URL
|
||||
query url.Values
|
||||
Original *fasthttp.RequestCtx
|
||||
FormParsed bool
|
||||
form url.Values
|
||||
MultiFormParsed bool
|
||||
// WebSocket *websocket.Conn
|
||||
ParsedForm *FastHttpMultipartForm
|
||||
header *FastHttpHeader
|
||||
Engine *FastHTTPServer
|
||||
}
|
||||
|
||||
FastHttpResponse struct {
|
||||
Original *fasthttp.RequestCtx
|
||||
header *FastHttpHeader
|
||||
Writer io.Writer
|
||||
Engine *FastHTTPServer
|
||||
}
|
||||
FastHttpMultipartForm struct {
|
||||
Form *multipart.Form
|
||||
}
|
||||
FastHttpHeader struct {
|
||||
Source interface{}
|
||||
isResponse bool
|
||||
}
|
||||
FastHttpCookie []byte
|
||||
)
|
||||
|
||||
var (
|
||||
fastHttpContextStack *revel.SimpleLockStack
|
||||
fastHttpMultipartFormStack *revel.SimpleLockStack
|
||||
)
|
||||
|
||||
func NewFastHttpContext(instance *FastHTTPServer) *FastHttpContext {
|
||||
if instance == nil {
|
||||
instance = &FastHTTPServer{MaxMultipartSize: 32 << 20}
|
||||
}
|
||||
c := &FastHttpContext{
|
||||
Request: &FastHttpRequest{header: &FastHttpHeader{isResponse: false},
|
||||
Engine: instance},
|
||||
Response: &FastHttpResponse{header: &FastHttpHeader{isResponse: true},
|
||||
Engine: instance},
|
||||
}
|
||||
c.Response.header.Source = c.Response
|
||||
c.Request.header.Source = c.Request
|
||||
return c
|
||||
}
|
||||
func (c *FastHttpContext) GetRequest() revel.ServerRequest {
|
||||
return c.Request
|
||||
}
|
||||
func (c *FastHttpContext) GetResponse() revel.ServerResponse {
|
||||
return c.Response
|
||||
}
|
||||
func (c *FastHttpContext) SetContext(context *fasthttp.RequestCtx) {
|
||||
c.Response.SetContext(context)
|
||||
c.Request.SetContext(context)
|
||||
}
|
||||
func (c *FastHttpContext) Destroy() {
|
||||
c.Response.Destroy()
|
||||
c.Request.Destroy()
|
||||
}
|
||||
|
||||
func (r *FastHttpRequest) Get(key int) (value interface{}, err error) {
|
||||
switch key {
|
||||
case revel.HTTP_SERVER_HEADER:
|
||||
value = r.GetHeader()
|
||||
case revel.HTTP_MULTIPART_FORM:
|
||||
value, err = r.GetMultipartForm()
|
||||
case revel.HTTP_QUERY:
|
||||
value = r.GetQuery()
|
||||
case revel.HTTP_FORM:
|
||||
value, err = r.GetForm()
|
||||
case revel.HTTP_REQUEST_URI:
|
||||
value = string(r.Original.RequestURI())
|
||||
case revel.HTTP_REMOTE_ADDR:
|
||||
value = r.Original.RemoteAddr().String()
|
||||
case revel.HTTP_METHOD:
|
||||
value = string(r.Original.Method())
|
||||
case revel.HTTP_PATH:
|
||||
value = string(r.Original.Path())
|
||||
case revel.HTTP_HOST:
|
||||
value = string(r.Original.Request.Host())
|
||||
case revel.HTTP_URL:
|
||||
if r.url == nil {
|
||||
r.url, _ = url.Parse(string(r.Original.Request.URI().FullURI()))
|
||||
}
|
||||
value = r.url
|
||||
case revel.HTTP_BODY:
|
||||
value = bytes.NewBuffer(r.Original.Request.Body())
|
||||
default:
|
||||
err = revel.ENGINE_UNKNOWN_GET
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
func (r *FastHttpRequest) Set(key int, value interface{}) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *FastHttpRequest) GetQuery() url.Values {
|
||||
if !r.toQuery {
|
||||
// Attempt to convert to query
|
||||
r.query = url.Values{}
|
||||
r.Original.QueryArgs().VisitAll(func(key, value []byte) {
|
||||
r.query.Set(string(key), string(value))
|
||||
})
|
||||
r.toQuery = true
|
||||
}
|
||||
return r.query
|
||||
}
|
||||
func (r *FastHttpRequest) GetForm() (url.Values, error) {
|
||||
if !r.FormParsed {
|
||||
r.form = url.Values{}
|
||||
r.Original.PostArgs().VisitAll(func(key, value []byte) {
|
||||
println("Set value", string(key), string(value))
|
||||
r.query.Set(string(key), string(value))
|
||||
})
|
||||
r.FormParsed = true
|
||||
}
|
||||
return r.form, nil
|
||||
}
|
||||
func (r *FastHttpRequest) GetMultipartForm() (revel.ServerMultipartForm, error) {
|
||||
if !r.MultiFormParsed {
|
||||
// TODO Limit size r.Engine.MaxMultipartSize
|
||||
form, err := r.Original.MultipartForm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.ParsedForm = fastHttpMultipartFormStack.Pop().(*FastHttpMultipartForm)
|
||||
r.ParsedForm.Form = form
|
||||
}
|
||||
|
||||
return r.ParsedForm, nil
|
||||
}
|
||||
func (r *FastHttpRequest) GetHeader() revel.ServerHeader {
|
||||
return r.header
|
||||
}
|
||||
func (r *FastHttpRequest) GetRaw() interface{} {
|
||||
return r.Original
|
||||
}
|
||||
func (r *FastHttpRequest) SetContext(req *fasthttp.RequestCtx) {
|
||||
r.Original = req
|
||||
|
||||
}
|
||||
func (r *FastHttpRequest) Destroy() {
|
||||
r.Original = nil
|
||||
r.FormParsed = false
|
||||
r.MultiFormParsed = false
|
||||
r.ParsedForm = nil
|
||||
r.toQuery = false
|
||||
|
||||
}
|
||||
|
||||
func (r *FastHttpResponse) Get(key int) (value interface{}, err error) {
|
||||
switch key {
|
||||
case revel.HTTP_SERVER_HEADER:
|
||||
value = r.Header()
|
||||
case revel.HTTP_STREAM_WRITER:
|
||||
value = r
|
||||
case revel.HTTP_WRITER:
|
||||
value = r.Writer
|
||||
default:
|
||||
err = revel.ENGINE_UNKNOWN_GET
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *FastHttpResponse) Set(key int, value interface{}) (set bool) {
|
||||
switch key {
|
||||
case revel.ENGINE_RESPONSE_STATUS:
|
||||
r.Header().SetStatus(value.(int))
|
||||
set = true
|
||||
case revel.HTTP_WRITER:
|
||||
r.SetWriter(value.(io.Writer))
|
||||
set = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *FastHttpResponse) GetWriter() io.Writer {
|
||||
return r.Writer
|
||||
}
|
||||
func (r *FastHttpResponse) Header() revel.ServerHeader {
|
||||
return r.header
|
||||
}
|
||||
func (r *FastHttpResponse) GetRaw() interface{} {
|
||||
return r.Original
|
||||
}
|
||||
func (r *FastHttpResponse) WriteStream(name string, contentlen int64, modtime time.Time, reader io.Reader) error {
|
||||
|
||||
// do a simple io.Copy, we do it directly into the writer which may be configured to be a compressed
|
||||
// writer
|
||||
ius := r.Original.Request.Header.Peek("If-Unmodified-Since")
|
||||
if t, err := http.ParseTime(string(ius)); ius != nil && err == nil && !modtime.IsZero() {
|
||||
// The Date-Modified header truncates sub-second precision, so
|
||||
// use mtime < t+1s instead of mtime <= t to check for unmodified.
|
||||
if modtime.Before(t.Add(1 * time.Second)) {
|
||||
h := r.Original.Response.Header
|
||||
h.Del("Content-Type")
|
||||
h.Del("Content-Length")
|
||||
if h.Peek("Etag") != nil {
|
||||
h.Del("Last-Modified")
|
||||
}
|
||||
h.SetStatusCode(http.StatusNotModified)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if contentlen != -1 {
|
||||
r.Original.Response.Header.Set("Content-Length", strconv.FormatInt(contentlen, 10))
|
||||
}
|
||||
if _, err := io.Copy(r.Writer, reader); err != nil {
|
||||
r.Original.Response.Header.SetStatusCode(http.StatusInternalServerError)
|
||||
return err
|
||||
} else {
|
||||
r.Original.Response.Header.SetStatusCode(http.StatusOK)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (r *FastHttpResponse) Destroy() {
|
||||
if c, ok := r.Writer.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
r.Original = nil
|
||||
r.Writer = nil
|
||||
|
||||
}
|
||||
func (r *FastHttpResponse) SetContext(w *fasthttp.RequestCtx) {
|
||||
r.Original = w
|
||||
r.Writer = w.Response.BodyWriter()
|
||||
}
|
||||
func (r *FastHttpResponse) SetWriter(writer io.Writer) {
|
||||
r.Writer = writer
|
||||
}
|
||||
func (r *FastHttpHeader) SetCookie(cookie string) {
|
||||
if r.isResponse {
|
||||
r.Source.(*FastHttpResponse).Original.Response.Header.Add("Set-Cookie", cookie)
|
||||
}
|
||||
}
|
||||
func (r *FastHttpHeader) GetCookie(key string) (value revel.ServerCookie, err error) {
|
||||
if !r.isResponse {
|
||||
var cookie []byte
|
||||
if cookie = r.Source.(*FastHttpRequest).Original.Request.Header.Cookie(key); cookie != nil {
|
||||
value = FastHttpCookie(cookie)
|
||||
} else {
|
||||
err = http.ErrNoCookie
|
||||
}
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
func (r *FastHttpHeader) Set(key string, value string) {
|
||||
if r.isResponse {
|
||||
r.Source.(*FastHttpResponse).Original.Response.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
func (r *FastHttpHeader) Add(key string, value string) {
|
||||
if r.isResponse {
|
||||
r.Source.(*FastHttpResponse).Original.Response.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
func (r *FastHttpHeader) Del(key string) {
|
||||
if r.isResponse {
|
||||
r.Source.(*FastHttpResponse).Original.Response.Header.Del(key)
|
||||
}
|
||||
}
|
||||
func (r *FastHttpHeader) Get(key string) (value []string) {
|
||||
if !r.isResponse {
|
||||
value = strings.Split(string(r.Source.(*FastHttpRequest).Original.Request.Header.Peek(key)), ",")
|
||||
} else {
|
||||
value = strings.Split(string(r.Source.(*FastHttpResponse).Original.Response.Header.Peek(key)), ",")
|
||||
}
|
||||
return
|
||||
}
|
||||
func (r *FastHttpHeader) SetStatus(statusCode int) {
|
||||
if r.isResponse {
|
||||
r.Source.(*FastHttpResponse).Original.Response.SetStatusCode(statusCode)
|
||||
}
|
||||
}
|
||||
func (r FastHttpCookie) GetValue() string {
|
||||
return string(r)
|
||||
}
|
||||
func (f *FastHttpMultipartForm) GetFiles() map[string][]*multipart.FileHeader {
|
||||
return f.Form.File
|
||||
}
|
||||
func (f *FastHttpMultipartForm) GetValues() url.Values {
|
||||
return url.Values(f.Form.Value)
|
||||
}
|
||||
func (f *FastHttpMultipartForm) RemoveAll() error {
|
||||
return f.Form.RemoveAll()
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package fasthttp
|
||||
|
||||
// Required for vendoring see golang.org/issue/13832
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# NewRelic
|
||||
This module is the for [NewRelic](http://newrelic.com) it wraps the Go
|
||||
http server and inserts a filter to track each requests.
|
||||
|
||||
###App.conf
|
||||
- **server.engine** You must set this to `newrelic` in order to use this server engine
|
||||
- **server.newrelic.license** The license key for NewRelic
|
||||
- **server.newrelic.addfilter** Defauls `true` When true a filter is inserted
|
||||
into `revel.Filters` at position 2. This filter logs every request.
|
||||
|
||||
###Other Notes
|
||||
There is a controller called RelicController which contains a single method
|
||||
to return a `newrelic.Application` object. If you want to access this object
|
||||
you can extend that class.
|
||||
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/newrelic/go-agent"
|
||||
"github.com/revel/modules/server-engine/newrelic"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
type RelicController struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
func (r *RelicController) GetRelicApplication() newrelic.Application {
|
||||
if app, ok := revel.CurrentEngine.(*revelnewrelic.ServerNewRelic); ok {
|
||||
return app.NewRelicApp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package revelnewrelic
|
||||
|
||||
// Required for vendoring see golang.org/issue/13832
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package revelnewrelic
|
||||
|
||||
import (
|
||||
"github.com/newrelic/go-agent"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
type ServerNewRelic struct {
|
||||
NewRelicConfig *newrelic.Config
|
||||
NewRelicApp newrelic.Application
|
||||
revel.GoHttpServer
|
||||
}
|
||||
|
||||
var (
|
||||
serverLog = revel.AppLog
|
||||
)
|
||||
|
||||
func init() {
|
||||
revel.RegisterServerEngine("newrelic", func() revel.ServerEngine {
|
||||
nr := &ServerNewRelic{
|
||||
GoHttpServer: revel.GoHttpServer{},
|
||||
}
|
||||
config := newrelic.NewConfig("Unknown Application", " Unknown Key ")
|
||||
nr.NewRelicConfig = &config
|
||||
return nr
|
||||
})
|
||||
revel.RegisterModuleInit(func(m *revel.Module) {
|
||||
serverLog = m.Log
|
||||
})
|
||||
|
||||
}
|
||||
func (g *ServerNewRelic) Init(init *revel.EngineInit) {
|
||||
g.GoHttpServer.Init(init)
|
||||
}
|
||||
|
||||
func (nr *ServerNewRelic) Event(event int, args interface{}) {
|
||||
|
||||
switch event {
|
||||
case revel.ENGINE_BEFORE_INITIALIZED:
|
||||
nr.NewRelicConfig.AppName = revel.Config.StringDefault("app.name", "Uknown App")
|
||||
|
||||
license := revel.Config.StringDefault("server.newrelic.license", "")
|
||||
if license != "" {
|
||||
nr.NewRelicConfig.License = license
|
||||
serverLog.Debug("Assigned NewRelic license")
|
||||
} else {
|
||||
serverLog.Error("Newrelic license key not assigned, configuration missing 'server.newrelic.license'")
|
||||
}
|
||||
addfilter := revel.Config.BoolDefault("server.newrelic.addfilter", true)
|
||||
if addfilter {
|
||||
// Inject filter after the router (Normally position 2)
|
||||
revel.Filters = append(revel.Filters, NewRelicFilter)
|
||||
copy(revel.Filters[3:], revel.Filters[2:])
|
||||
revel.Filters[2] = NewRelicFilter
|
||||
serverLog.Debug("Newrelic filter injected")
|
||||
}
|
||||
case revel.ENGINE_STARTED:
|
||||
// Check to see if configuration is set
|
||||
// create the application interface
|
||||
app, err := newrelic.NewApplication(*nr.NewRelicConfig)
|
||||
if err != nil {
|
||||
serverLog.Panic("Failed to start NewRelic:", "error", err)
|
||||
}
|
||||
nr.NewRelicApp = app
|
||||
|
||||
}
|
||||
|
||||
nr.GoHttpServer.Event(event, args)
|
||||
}
|
||||
func (nr *ServerNewRelic) Name() string {
|
||||
return "newrelic"
|
||||
}
|
||||
func (nr *ServerNewRelic) Engine() interface{} {
|
||||
return nr
|
||||
}
|
||||
|
||||
// This is a simplistic example of setting up a filter to record all events for the
|
||||
// webserver as transactions
|
||||
func NewRelicFilter(c *revel.Controller, fc []revel.Filter) {
|
||||
if nr, ok := revel.CurrentEngine.Engine().(*ServerNewRelic); ok {
|
||||
if nr.NewRelicApp != nil {
|
||||
txn := nr.NewRelicApp.StartTransaction(c.Action,
|
||||
c.Response.Out.Server.(*revel.GoResponse).Original,
|
||||
c.Request.In.(*revel.GoRequest).Original)
|
||||
defer txn.End()
|
||||
} else {
|
||||
serverLog.Error("Newrelic application not initialized before filter called")
|
||||
}
|
||||
}
|
||||
|
||||
fc[0](c, fc[1:])
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
||||
// Revel Framework source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"os"
|
||||
fpath "path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
// Static file serving controller
|
||||
type Static struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// Serve method handles requests for files. The supplied prefix may be absolute
|
||||
// or relative. If the prefix is relative it is assumed to be relative to the
|
||||
// application directory. The filepath may either be just a file or an
|
||||
// additional filepath to search for the given file. This response may return
|
||||
// the following responses in the event of an error or invalid request;
|
||||
// 403(Forbidden): If the prefix filepath combination results in a directory.
|
||||
// 404(Not found): If the prefix and filepath combination results in a non-existent file.
|
||||
// 500(Internal Server Error): There are a few edge cases that would likely indicate some configuration error outside of revel.
|
||||
//
|
||||
// Note that when defining routes in routes/conf the parameters must not have
|
||||
// spaces around the comma.
|
||||
// Bad: Static.Serve("public/img", "favicon.png")
|
||||
// Good: Static.Serve("public/img","favicon.png")
|
||||
//
|
||||
// Examples:
|
||||
// Serving a directory
|
||||
// Route (conf/routes):
|
||||
// GET /public/{<.*>filepath} Static.Serve("public")
|
||||
// Request:
|
||||
// public/js/sessvars.js
|
||||
// Calls
|
||||
// Static.Serve("public","js/sessvars.js")
|
||||
//
|
||||
// Serving a file
|
||||
// Route (conf/routes):
|
||||
// GET /favicon.ico Static.Serve("public/img","favicon.png")
|
||||
// Request:
|
||||
// favicon.ico
|
||||
// Calls:
|
||||
// Static.Serve("public/img", "favicon.png")
|
||||
func (c Static) Serve(prefix, filepath string) revel.Result {
|
||||
// Fix for #503.
|
||||
prefix = c.Params.Fixed.Get("prefix")
|
||||
if prefix == "" {
|
||||
return c.NotFound("")
|
||||
}
|
||||
|
||||
return serve(c, prefix, filepath)
|
||||
}
|
||||
|
||||
// ServeModule method allows modules to serve binary files. The parameters are the same
|
||||
// as Static.Serve with the additional module name pre-pended to the list of
|
||||
// arguments.
|
||||
func (c Static) ServeModule(moduleName, prefix, filepath string) revel.Result {
|
||||
// Fix for #503.
|
||||
prefix = c.Params.Fixed.Get("prefix")
|
||||
if prefix == "" {
|
||||
return c.NotFound("")
|
||||
}
|
||||
|
||||
var basePath string
|
||||
for _, module := range revel.Modules {
|
||||
if module.Name == moduleName {
|
||||
basePath = module.Path
|
||||
}
|
||||
}
|
||||
|
||||
absPath := fpath.Join(basePath, fpath.FromSlash(prefix))
|
||||
|
||||
return serve(c, absPath, filepath)
|
||||
}
|
||||
|
||||
// This method allows static serving of application files in a verified manner.
|
||||
func serve(c Static, prefix, filepath string) revel.Result {
|
||||
var basePath string
|
||||
if !fpath.IsAbs(prefix) {
|
||||
basePath = revel.BasePath
|
||||
}
|
||||
|
||||
basePathPrefix := fpath.Join(basePath, fpath.FromSlash(prefix))
|
||||
fname := fpath.Join(basePathPrefix, fpath.FromSlash(filepath))
|
||||
// Verify the request file path is within the application's scope of access
|
||||
if !strings.HasPrefix(fname, basePathPrefix) {
|
||||
c.Log.Warn("Attempted to read file outside of base path", "file", fname)
|
||||
return c.NotFound("")
|
||||
}
|
||||
|
||||
// Verify file path is accessible
|
||||
finfo, err := os.Stat(fname)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || err.(*os.PathError).Err == syscall.ENOTDIR {
|
||||
c.Log.Warn("File not found ", "file", fname, "error", err)
|
||||
return c.NotFound("File not found")
|
||||
}
|
||||
c.Log.Error("Error trying to get fileinfo for", "file", fname, "error", err)
|
||||
return c.RenderError(err)
|
||||
}
|
||||
|
||||
// Disallow directory listing
|
||||
if finfo.Mode().IsDir() {
|
||||
c.Log.Warn("Attempted directory listing of ", "dir", fname)
|
||||
return c.Forbidden("Directory listing not allowed")
|
||||
}
|
||||
|
||||
// Open request file path
|
||||
file, err := os.Open(fname)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.Log.Warn("File not found", "file", fname, "error", err)
|
||||
return c.NotFound("File not found")
|
||||
}
|
||||
c.Log.Error("Error opening", "file", fname, "error", err)
|
||||
return c.RenderError(err)
|
||||
}
|
||||
return c.RenderFile(file, revel.Inline)
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package static
|
||||
|
||||
// Required for vendoring see golang.org/issue/13832
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Ace
|
||||
The [ACE](https://github.com/yosssi/ace) Templating Plugin
|
||||
|
||||
- Ace templates have full access to the revel.TemplateFuncs, any function
|
||||
defined in there can be used by the this engine
|
||||
|
||||
- Ace files must be identified by using a `shebang` on the first line
|
||||
(preferred method) or changing the file extension to home.ace.html.
|
||||
By default the operation of revel assumes just a `.html` extension for
|
||||
controller responses so it would make more sense to use the shebang
|
||||
- Ace templates can be set to be case sensitive by setting
|
||||
`ace.tempate.caseinsensitive=false`, default is not case sensitive. If case sensitivity
|
||||
is off internal imports must be done using lower case
|
||||
- All function registered in `revel.TemplateFuncs` are available for use
|
||||
inside the ace framework
|
||||
|
||||
##### Details
|
||||
Ace is a little different of a templating system, its output is a
|
||||
standard go template but there is no concept of template sets,
|
||||
instead you build a composite template using
|
||||
a *base* template and an *inner* template. The
|
||||
*inner* template can only contain items like :
|
||||
```
|
||||
= content main
|
||||
h2 Inner Template - Main : {{.Msg}}
|
||||
|
||||
= content sub
|
||||
h3 Inner Template - Sub : {{.Msg}}
|
||||
|
||||
```
|
||||
The base template can contain items like
|
||||
```
|
||||
= doctype html
|
||||
html lang=en
|
||||
head
|
||||
meta charset=utf-8
|
||||
title Ace example
|
||||
= css
|
||||
h1 { color: blue; }
|
||||
body
|
||||
h1 Base Template : {{.Msg}}
|
||||
#container.wrapper
|
||||
= yield main
|
||||
= yield sub
|
||||
= include inc .Msg
|
||||
= javascript
|
||||
alert('{{.Msg}}');
|
||||
```
|
||||
|
||||
You are allowed to include one *inner* template with the base template,
|
||||
to do so in revel you can extend your controller from the ace controller
|
||||
and call `RenderAceTemplate(base ,inner string)` which will insert
|
||||
the inner template using the outer template.
|
||||
|
||||
The ace engine requires that you explicitly set the template type on the
|
||||
template itself by either using the shebang method on the first line
|
||||
like `#! ace` or having the file name like `template.ace.html`
|
||||
either method will work.
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package ace
|
||||
|
||||
// Required for vendoring see golang.org/issue/13832
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package controllers
|
||||
|
||||
import "github.com/revel/revel"
|
||||
|
||||
type AceController struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// Called to render the ace template inner
|
||||
func (c *AceController) RenderAceTemplate(base, inner string) revel.Result {
|
||||
c.ViewArgs["ace_inner"] = inner
|
||||
return c.RenderTemplate(base)
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/revel/revel"
|
||||
"github.com/yosssi/ace"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const ACE_TEMPLATE = "ace"
|
||||
|
||||
// Adapter for Go Templates.
|
||||
type AceTemplate struct {
|
||||
*template.Template
|
||||
engine *AceEngine
|
||||
*revel.TemplateView
|
||||
File *ace.File
|
||||
Inner *ace.File
|
||||
}
|
||||
|
||||
// A bit trick of an implementation
|
||||
// If the arg contains an ace_inner field then that will be used
|
||||
// to fetch a new template
|
||||
func (acetmpl AceTemplate) Render(wr io.Writer, arg interface{}) error {
|
||||
// We can redirect this render to another template if the arguments contain ace_content in them
|
||||
if argmap, ok := arg.(map[string]interface{}); ok {
|
||||
if acecontentraw, ok := argmap["ace-inner"]; ok {
|
||||
acecontent := acecontentraw.(string)
|
||||
newtemplatename := acetmpl.TemplateName + "-" + acecontent
|
||||
// Now lookup the template again
|
||||
if _, ok := acetmpl.engine.templatesByName[newtemplatename]; !ok {
|
||||
if inner, ok := acetmpl.engine.templatesByName[acecontent]; !ok {
|
||||
return fmt.Errorf("Inner content %s not found in ace templates", acecontent)
|
||||
} else {
|
||||
acetmpl.engine.templatesByName[newtemplatename] = &AceTemplate{
|
||||
File: acetmpl.File,
|
||||
Inner: inner.File,
|
||||
engine: acetmpl.engine,
|
||||
TemplateView: acetmpl.TemplateView}
|
||||
}
|
||||
|
||||
}
|
||||
return acetmpl.engine.templatesByName[newtemplatename].renderInternal(wr, arg)
|
||||
}
|
||||
}
|
||||
return acetmpl.renderInternal(wr, arg)
|
||||
}
|
||||
|
||||
func (acetmpl AceTemplate) renderInternal(wr io.Writer, arg interface{}) error {
|
||||
if acetmpl.Template == nil {
|
||||
// Compile the template first
|
||||
if acetmpl.Inner == nil {
|
||||
acetmpl.Inner = ace.NewFile("", nil)
|
||||
}
|
||||
source := ace.NewSource(acetmpl.File, acetmpl.Inner, acetmpl.engine.files)
|
||||
result, err := ace.ParseSource(source, acetmpl.engine.Options)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if gtemplate, err := ace.CompileResult(acetmpl.TemplateName, result, acetmpl.engine.Options); err != nil {
|
||||
return err
|
||||
} else {
|
||||
acetmpl.Template = gtemplate
|
||||
}
|
||||
}
|
||||
return acetmpl.Execute(wr, arg)
|
||||
}
|
||||
|
||||
type AceEngine struct {
|
||||
loader *revel.TemplateLoader
|
||||
templatesByName map[string]*AceTemplate
|
||||
files []*ace.File
|
||||
Options *ace.Options
|
||||
CaseInsensitive bool
|
||||
}
|
||||
|
||||
func (i *AceEngine) ConvertPath(path string) string {
|
||||
if i.CaseInsensitive {
|
||||
return strings.ToLower(path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (i *AceEngine) Handles(templateView *revel.TemplateView) bool {
|
||||
return revel.EngineHandles(i, templateView)
|
||||
}
|
||||
|
||||
func (engine *AceEngine) ParseAndAdd(baseTemplate *revel.TemplateView) error {
|
||||
|
||||
// Ace templates must only render views specified for it (no trial and error)
|
||||
if baseTemplate.EngineType != ACE_TEMPLATE {
|
||||
return &revel.Error{
|
||||
Title: "Template Compilation Error",
|
||||
Path: baseTemplate.FilePath,
|
||||
Description: "Not correct template for engine",
|
||||
Line: 1,
|
||||
SourceLines: baseTemplate.Content(),
|
||||
}
|
||||
}
|
||||
|
||||
baseTemplate.TemplateName = engine.ConvertPath(baseTemplate.TemplateName)
|
||||
file := ace.NewFile(baseTemplate.TemplateName, baseTemplate.FileBytes)
|
||||
engine.files = append(engine.files, file)
|
||||
engine.templatesByName[baseTemplate.TemplateName] = &AceTemplate{File: file, engine: engine, TemplateView: baseTemplate}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (engine *AceEngine) Lookup(templateName string) revel.Template {
|
||||
if tpl, found := engine.templatesByName[engine.ConvertPath(templateName)]; found {
|
||||
|
||||
return tpl
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (engine *AceEngine) Name() string {
|
||||
return ACE_TEMPLATE
|
||||
}
|
||||
|
||||
func (engine *AceEngine) Event(action int, i interface{}) {
|
||||
if action == revel.TEMPLATE_REFRESH_REQUESTED {
|
||||
engine.templatesByName = map[string]*AceTemplate{}
|
||||
engine.CaseInsensitive = revel.Config.BoolDefault("ace.template.caseinsensitive", true)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.RegisterTemplateLoader(ACE_TEMPLATE, func(loader *revel.TemplateLoader) (revel.TemplateEngine, error) {
|
||||
|
||||
return &AceEngine{
|
||||
loader: loader,
|
||||
templatesByName: map[string]*AceTemplate{},
|
||||
Options: &ace.Options{FuncMap: revel.TemplateFuncs},
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
= doctype html
|
||||
html lang=en
|
||||
head
|
||||
meta charset=utf-8
|
||||
title Forbidden
|
||||
body
|
||||
h1 Base Template : {{.Error.Title}}
|
||||
p : {{.Error.Description}}
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "{{Error.Title|escapejs}}",
|
||||
"description": "{{Error.Description|escapejs}}"
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{.Error.Title}}
|
||||
|
||||
{{.Error.Description}}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"github.com/revel/revel/testing"
|
||||
)
|
||||
|
||||
type ApplicationTest struct {
|
||||
testing.TestSuite
|
||||
}
|
||||
|
||||
func (t *ApplicationTest) Before() {
|
||||
println("Set up")
|
||||
}
|
||||
|
||||
func (t *ApplicationTest) TestThatIndexPageWorks() {
|
||||
t.Get("/")
|
||||
t.AssertOk()
|
||||
t.AssertContentType("text/html; charset=utf-8")
|
||||
}
|
||||
|
||||
func (t *ApplicationTest) After() {
|
||||
println("Tear down")
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
# Pongo2
|
||||
The [PONGO2](https://github.com/flosch/pongo2) Templating Plugin
|
||||
|
||||
- Pongo2 templates may be identified by a `shebang` on the first line
|
||||
(preferred method) or changing the file extension to home.pongo2.html.
|
||||
By default the operation of revel assumes just a `.html` extension for
|
||||
controller responses so it would make more sense to use the shebang
|
||||
- Pongo2 templates can be set to be case sensitive by setting
|
||||
`pongo2.tempate.caseinsensitive=false`, default is not case sensitive. If case sensitivity is
|
||||
off the templates are compiled using lower case
|
||||
- Currently the only functions built in are as follows:
|
||||
- field
|
||||
- radio
|
||||
- option
|
||||
- url
|
||||
- checkbox
|
||||
- append
|
||||
|
||||
Samples implementation below
|
||||
|
||||
##### Details
|
||||
pongo2 is the successor of [pongo](https://github.com/flosch/pongo), a Django-syntax like templating-language.
|
||||
|
||||
Install/update using `go get` (no dependencies required by pongo2):
|
||||
```
|
||||
go get -u github.com/flosch/pongo2
|
||||
```
|
||||
|
||||
Please use the [issue tracker](https://github.com/flosch/pongo2/issues) if you're encountering any problems with pongo2 or if you need help with implementing tags or filters ([create a ticket!](https://github.com/flosch/pongo2/issues/new)). If possible, please use [playground](https://www.florian-schlachter.de/pongo2/) to create a short test case on what's wrong and include the link to the snippet in your issue.
|
||||
|
||||
**New**: [Try pongo2 out in the pongo2 playground.](https://www.florian-schlachter.de/pongo2/)
|
||||
|
||||
```
|
||||
{%append "moreStyles" "ui-lightness/jquery-ui-1.7.2.custom.css"%}
|
||||
{%append "moreScripts" "js/jquery-ui-1.7.2.custom.min.js"%}
|
||||
{% include "header.html" %}
|
||||
|
||||
<h1>Book hotel</h1>
|
||||
|
||||
<form method="POST" action="{%url "Hotels.Book" hotel.HotelId%}">
|
||||
<p>
|
||||
<strong>Name:</strong> {{hotel.Name}}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Address:</strong> {{hotel.Address}}
|
||||
</p>
|
||||
<p>
|
||||
<strong>City:</strong> {{hotel.City}}
|
||||
</p>
|
||||
<p>
|
||||
<strong>State:</strong> {{hotel.State}}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Zip:</strong> {{hotel.Zip}}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Country:</strong> {{hotel.Country}}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Nightly rate:</strong> {{hotel.Price}}
|
||||
</p>
|
||||
{%with field = "booking.CheckInDate"|field %}
|
||||
<p class="{{field.ErrorClass}}">
|
||||
<strong>Check In Date:</strong>
|
||||
<input type="text" size="10" name="{{field.Name}}" class="datepicker" value="{{field.Flash}}">
|
||||
* <span class="error">{{field.Error}}</span>
|
||||
</p>
|
||||
{% endwith %}
|
||||
{%with field = "booking.CheckOutDate"|field %}
|
||||
<p class="{{field.ErrorClass}}">
|
||||
<strong>Check Out Date:</strong>
|
||||
<input type="text" size="10" name="{{field.Name}}" class="datepicker" value="{{field.Flash}}">
|
||||
* <span class="error">{{field.Error}}</span>
|
||||
</p>
|
||||
{% endwith %}
|
||||
<p>
|
||||
<strong>Room preference:</strong>
|
||||
{%with field = "booking.Beds"|field %}
|
||||
<select name="{{field.Name}}">
|
||||
{%option field "1" "One king-size bed"%}
|
||||
{%option field "2" "Two double beds"%}
|
||||
{%option field "3" "Three beds"%}
|
||||
</select>
|
||||
{% endwith %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Smoking preference:</strong>
|
||||
{%with field = "booking.Smoking"|field %}
|
||||
{%radio field "true"%} Smoking
|
||||
{%radio field "false"%} Non smoking
|
||||
{% endwith %}
|
||||
</p>
|
||||
{%with field = "booking.CardNumber"|field %}
|
||||
<p class="{{field.ErrorClass}}">
|
||||
<strong>Credit Card #:</strong>
|
||||
<input type="text" name="{{field.Name}}" size="16" value="{{field.Flash}}">
|
||||
* <span class="error">{{field.Error}}</span>
|
||||
</p>
|
||||
{% endwith %}
|
||||
{%with field = "booking.NameOnCard"|field %}
|
||||
<p class="{{field.ErrorClass}}">
|
||||
<strong>Credit Card Name:</strong>
|
||||
<input type="text" name="{{field.Name}}" size="16" value="{{field.Flash}}">
|
||||
* <span class="error">{{field.Error}}</span>
|
||||
</p>
|
||||
{% endwith %}
|
||||
<p>
|
||||
<strong>Credit Card Expiry:</strong>
|
||||
{%with field = "booking.CardExpMonth"|field %}
|
||||
<select name="{{field.Name}}">
|
||||
{%option field "1" "Jan"%}
|
||||
{%option field "2" "Feb"%}
|
||||
{%option field "3" "Mar"%}
|
||||
{%option field "4" "Apr"%}
|
||||
{%option field "5" "May"%}
|
||||
{%option field "6" "Jun"%}
|
||||
{%option field "7" "Jul"%}
|
||||
{%option field "8" "Aug"%}
|
||||
{%option field "9" "Sep"%}
|
||||
{%option field "10" "Oct"%}
|
||||
{%option field "11" "Nov"%}
|
||||
{%option field "12" "Dec"%}
|
||||
</select>
|
||||
{% endwith %}
|
||||
{%with field = "booking.CardExpYear"|field %}
|
||||
<select name="{{field.Name}}">
|
||||
{%option field "2008" "2008"%}
|
||||
{%option field "2009" "2009"%}
|
||||
{%option field "2010" "2010"%}
|
||||
{%option field "2011" "2011"%}
|
||||
{%option field "2012" "2012"%}
|
||||
</select>
|
||||
{% endwith %}
|
||||
</p>
|
||||
<p class="buttons">
|
||||
<input type="submit" value="Proceed">
|
||||
<a href="{%url "Hotels.Show" hotel.HotelId %}">Cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(function() {
|
||||
$(".datepicker").datepicker({dateFormat: 'yy-mm-dd'});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% include "footer.html" %}
|
||||
|
||||
``
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package pongo2
|
||||
|
||||
import (
|
||||
p2 "github.com/flosch/pongo2"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func init() {
|
||||
p2.RegisterFilter("field", func(in *p2.Value, param *p2.Value) (out *p2.Value, err *p2.Error) {
|
||||
if nil == in.Interface() || in.String() == "" {
|
||||
return nil, &p2.Error{Sender: "field argument must is string"}
|
||||
}
|
||||
newField := revel.NewField(in.String(), getContext())
|
||||
return p2.AsValue(newField), nil
|
||||
})
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package pongo2
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
p2 "github.com/flosch/pongo2"
|
||||
"github.com/revel/revel"
|
||||
"github.com/tylerb/gls"
|
||||
)
|
||||
|
||||
// Adapter for HAML Templates.
|
||||
type PongoTemplate struct {
|
||||
template *p2.Template
|
||||
engine *PongoEngine
|
||||
*revel.TemplateView
|
||||
}
|
||||
type Pongo2BaseTag struct {
|
||||
field string
|
||||
}
|
||||
|
||||
func (node *Pongo2BaseTag) GetField(ctx *p2.ExecutionContext) (value interface{}, found bool) {
|
||||
value, found = ctx.Public[node.field]
|
||||
if !found {
|
||||
value, found = ctx.Private[node.field]
|
||||
}
|
||||
if found {
|
||||
if wrapped, ok := value.(*p2.Value); ok {
|
||||
value = wrapped.Interface()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type INodeImplied struct {
|
||||
Exec func(*p2.ExecutionContext, p2.TemplateWriter) *p2.Error
|
||||
}
|
||||
|
||||
func (i *INodeImplied) Execute(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
|
||||
return i.Exec(ctx, w)
|
||||
|
||||
}
|
||||
func (tmpl PongoTemplate) Name() string {
|
||||
return tmpl.TemplateName
|
||||
}
|
||||
func getContext() map[string]interface{} {
|
||||
return gls.Get("data").(map[string]interface{})
|
||||
}
|
||||
|
||||
// return a 'revel.Template' from HAML's template.
|
||||
func (tmpl PongoTemplate) Render(wr io.Writer, arg interface{}) (err error) {
|
||||
gls.With(gls.Values(map[interface{}]interface{}{"data": arg}), func() {
|
||||
err = tmpl.template.ExecuteWriter(p2.Context(arg.(map[string]interface{})), wr)
|
||||
if nil != err {
|
||||
if e, ok := err.(*p2.Error); ok {
|
||||
rerr := &revel.Error{
|
||||
Title: "Template Execution Error",
|
||||
Path: tmpl.TemplateName,
|
||||
Description: e.Error(),
|
||||
Line: e.Line,
|
||||
}
|
||||
if revel.DevMode {
|
||||
rerr.SourceLines = tmpl.Content()
|
||||
}
|
||||
err = rerr
|
||||
}
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// There is only a single instance of the PongoEngine initialized
|
||||
type PongoEngine struct {
|
||||
loader *revel.TemplateLoader
|
||||
templateSetBybasePath map[string]*p2.TemplateSet
|
||||
templates map[string]*PongoTemplate
|
||||
CaseInsensitive bool
|
||||
}
|
||||
|
||||
func (i *PongoEngine) ConvertPath(path string) string {
|
||||
if i.CaseInsensitive {
|
||||
return strings.ToLower(path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (i *PongoEngine) Handles(templateView *revel.TemplateView) bool {
|
||||
return revel.EngineHandles(i, templateView)
|
||||
}
|
||||
|
||||
func (engine *PongoEngine) ParseAndAdd(baseTemplate *revel.TemplateView) error {
|
||||
templateSet := engine.templateSetBybasePath[baseTemplate.BasePath]
|
||||
if nil == templateSet {
|
||||
templateSet = p2.NewSet(baseTemplate.BasePath, p2.MustNewLocalFileSystemLoader(baseTemplate.BasePath))
|
||||
engine.templateSetBybasePath[baseTemplate.BasePath] = templateSet
|
||||
}
|
||||
|
||||
tpl, err := templateSet.FromBytes(baseTemplate.FileBytes)
|
||||
if nil != err {
|
||||
_, line, description := parsePongo2Error(err)
|
||||
return &revel.Error{
|
||||
Title: "Template Compilation Error",
|
||||
Path: baseTemplate.FilePath,
|
||||
Description: description,
|
||||
Line: line,
|
||||
SourceLines: strings.Split(string(baseTemplate.FileBytes), "\n"),
|
||||
}
|
||||
}
|
||||
baseTemplate.TemplateName = engine.ConvertPath(baseTemplate.TemplateName)
|
||||
engine.templates[baseTemplate.TemplateName] = &PongoTemplate{
|
||||
template: tpl,
|
||||
engine: engine,
|
||||
TemplateView: baseTemplate}
|
||||
return nil
|
||||
}
|
||||
func (engine *PongoEngine) Name() string {
|
||||
return "pongo2"
|
||||
}
|
||||
|
||||
func parsePongo2Error(err error) (templateName string, line int, description string) {
|
||||
pongoError := err.(*p2.Error)
|
||||
if nil != pongoError {
|
||||
return pongoError.Filename, pongoError.Line, pongoError.Error()
|
||||
}
|
||||
return "Unknown error", 0, err.Error()
|
||||
}
|
||||
|
||||
func (engine *PongoEngine) Lookup(templateName string) revel.Template {
|
||||
tpl, found := engine.templates[engine.ConvertPath(templateName)]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return tpl
|
||||
}
|
||||
func (engine *PongoEngine) Event(action int, i interface{}) {
|
||||
if action == revel.TEMPLATE_REFRESH_REQUESTED {
|
||||
// At this point all the templates have been passed into the
|
||||
engine.templateSetBybasePath = map[string]*p2.TemplateSet{}
|
||||
engine.templates = map[string]*PongoTemplate{}
|
||||
engine.CaseInsensitive = revel.Config.BoolDefault("pongo2.template.caseinsensitive", true)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.RegisterTemplateLoader("pongo2", func(loader *revel.TemplateLoader) (revel.TemplateEngine, error) {
|
||||
return &PongoEngine{
|
||||
loader: loader,
|
||||
templateSetBybasePath: map[string]*p2.TemplateSet{},
|
||||
templates: map[string]*PongoTemplate{},
|
||||
}, nil
|
||||
})
|
||||
/*
|
||||
// TODO Dynamically call all the built in functions, PR welcome
|
||||
for key,templateFunction := range revel.TemplateFuncs {
|
||||
p2.RegisterTag(key, func(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
|
||||
evals := []p2.IEvaluator{}
|
||||
for arguments.Remaining() > 0 {
|
||||
expr, err := arguments.ParseExpression()
|
||||
evals = append(evals, expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext,w p2.TemplateWriter) *p2.Error {
|
||||
args := make([]interface{}, len(evals))
|
||||
for i, ev := range evals {
|
||||
obj, err := ev.Evaluate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args[i] = obj
|
||||
}
|
||||
|
||||
v:= &tagURLForNode{evals}
|
||||
reflect.MakeFunc ....
|
||||
return v.Execute(ctx,w)
|
||||
}}, nil
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package pongo2
|
||||
|
||||
import (
|
||||
p2 "github.com/flosch/pongo2"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type tagAppendNode struct {
|
||||
isPublic bool
|
||||
name string
|
||||
objectEvaluators []p2.IEvaluator
|
||||
}
|
||||
|
||||
func (node *tagAppendNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
|
||||
var values []interface{}
|
||||
var reflectValues reflect.Value
|
||||
found := false
|
||||
if o, found := ctx.Public[node.name]; found && nil != o {
|
||||
values, _ = o.([]interface{})
|
||||
if nil == values {
|
||||
reflectValues = reflect.ValueOf(o)
|
||||
if reflectValues.Kind() == reflect.Ptr {
|
||||
reflectValues = reflectValues.Elem()
|
||||
}
|
||||
if reflectValues.Kind() != reflect.Slice {
|
||||
return &p2.Error{Sender: "'" + node.name + "' isn't a slice."}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ev := range node.objectEvaluators {
|
||||
obj, err := ev.Evaluate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || reflectValues.IsNil() {
|
||||
values = append(values, obj)
|
||||
} else {
|
||||
reflectValues = reflect.AppendSlice(reflectValues, reflect.ValueOf(obj))
|
||||
}
|
||||
}
|
||||
|
||||
if !found || reflectValues.IsNil() {
|
||||
ctx.Public[node.name] = values
|
||||
} else {
|
||||
ctx.Public[node.name] = reflectValues.Interface()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tagURLForParser implements a {% urlfor %} tag.
|
||||
//
|
||||
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
|
||||
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
|
||||
func tagAppendParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
|
||||
var name string
|
||||
var isPublic bool
|
||||
// Parse variable name
|
||||
typeToken := arguments.MatchType(p2.TokenIdentifier)
|
||||
if typeToken != nil {
|
||||
name = typeToken.Val
|
||||
} else if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
|
||||
name = sToken.Val
|
||||
} else {
|
||||
return nil, arguments.Error("Expected an identifier or string.", nil)
|
||||
}
|
||||
|
||||
evals := []p2.IEvaluator{}
|
||||
for arguments.Remaining() > 0 {
|
||||
expr, err := arguments.ParseExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
evals = append(evals, expr)
|
||||
}
|
||||
|
||||
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
|
||||
|
||||
node := &tagAppendNode{isPublic, name, evals}
|
||||
return node.Execute(ctx, w)
|
||||
}}, nil
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
p2.RegisterTag("append", tagAppendParser)
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package pongo2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
|
||||
p2 "github.com/flosch/pongo2"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
type tagCheckboxNode struct {
|
||||
value p2.IEvaluator
|
||||
Pongo2BaseTag
|
||||
}
|
||||
|
||||
func (node *tagCheckboxNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
|
||||
fieldObj, _ := node.GetField(ctx)
|
||||
if nil == fieldObj {
|
||||
return ctx.Error("field '"+node.field+"' tagCheckboxNode is missing.", nil)
|
||||
}
|
||||
field, _ := fieldObj.(*revel.Field)
|
||||
if nil == field {
|
||||
return ctx.Error(fmt.Sprintf("field '"+node.field+"' isn't Field - %T.", fieldObj), nil)
|
||||
}
|
||||
|
||||
val, err := node.value.Evaluate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val_str := val.String()
|
||||
|
||||
checked := ""
|
||||
if field.Flash() == val_str {
|
||||
checked = " checked"
|
||||
}
|
||||
fmt.Fprintf(writer, `<input type="checkbox" name="%s" value="%s"%s>`,
|
||||
html.EscapeString(field.Name), html.EscapeString(val_str), checked)
|
||||
return nil
|
||||
}
|
||||
|
||||
// tagURLForParser implements a {% urlfor %} tag.
|
||||
//
|
||||
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
|
||||
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
|
||||
func tagCheckboxParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
|
||||
var field string
|
||||
typeToken := arguments.MatchType(p2.TokenIdentifier)
|
||||
if typeToken != nil {
|
||||
field = typeToken.Val
|
||||
} else if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
|
||||
field = sToken.Val
|
||||
} else {
|
||||
return nil, arguments.Error("Expected an identifier or string.", nil)
|
||||
}
|
||||
|
||||
expr, err := arguments.ParseExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
|
||||
|
||||
v := tagCheckboxNode{Pongo2BaseTag: Pongo2BaseTag{field: field},
|
||||
value: expr}
|
||||
return v.Execute(ctx, w)
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
p2.RegisterTag("checkbox", tagCheckboxParser)
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package pongo2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
|
||||
p2 "github.com/flosch/pongo2"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
type tagOptionNode struct {
|
||||
value p2.IEvaluator
|
||||
label string
|
||||
Pongo2BaseTag
|
||||
}
|
||||
|
||||
func (node *tagOptionNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
|
||||
fieldObj, _ := node.GetField(ctx)
|
||||
|
||||
if nil == fieldObj {
|
||||
return ctx.Error("field '"+node.field+"' tagOptionNode is missing.", nil)
|
||||
}
|
||||
field, _ := fieldObj.(*revel.Field)
|
||||
if nil == field {
|
||||
return ctx.Error(fmt.Sprintf("field '"+node.field+"' isn't Field - %T.", fieldObj), nil)
|
||||
}
|
||||
|
||||
val, err := node.value.Evaluate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val_str := val.String()
|
||||
|
||||
selected := ""
|
||||
if field.Flash() == val_str || (field.Flash() == "" && field.Value() == val_str) {
|
||||
selected = " selected"
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, `<option value="%s"%s>%s</option>`,
|
||||
html.EscapeString(val_str), selected, html.EscapeString(node.label))
|
||||
return nil
|
||||
}
|
||||
|
||||
// tagURLForParser implements a {% urlfor %} tag.
|
||||
//
|
||||
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
|
||||
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
|
||||
func tagOptionParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
|
||||
var field string
|
||||
typeToken := arguments.MatchType(p2.TokenIdentifier)
|
||||
if typeToken != nil {
|
||||
field = typeToken.Val
|
||||
} else if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
|
||||
field = sToken.Val
|
||||
} else {
|
||||
return nil, arguments.Error("Expected an identifier or string.", nil)
|
||||
}
|
||||
|
||||
expr, err := arguments.ParseExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var v *tagOptionNode
|
||||
if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
|
||||
v = &tagOptionNode{Pongo2BaseTag: Pongo2BaseTag{field: field},
|
||||
value: expr,
|
||||
label: sToken.Val}
|
||||
} else {
|
||||
return nil, arguments.Error("Expected an string.", nil)
|
||||
}
|
||||
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
|
||||
return v.Execute(ctx, w)
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
p2.RegisterTag("option", tagOptionParser)
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package pongo2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
|
||||
p2 "github.com/flosch/pongo2"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
type tagRadioNode struct {
|
||||
value p2.IEvaluator
|
||||
Pongo2BaseTag
|
||||
}
|
||||
|
||||
func (node *tagRadioNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
|
||||
fieldObj, _ := node.GetField(ctx)
|
||||
if nil == fieldObj {
|
||||
return ctx.Error("field '"+node.field+"' tagRadioNode is missing.", nil)
|
||||
}
|
||||
field, _ := fieldObj.(*revel.Field)
|
||||
if nil == field {
|
||||
return ctx.Error(fmt.Sprintf("field '"+node.field+"' isn't Field - %T.", fieldObj), nil)
|
||||
}
|
||||
val, err := node.value.Evaluate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val_str := val.String()
|
||||
|
||||
checked := ""
|
||||
if field.Flash() == val_str {
|
||||
checked = " checked"
|
||||
}
|
||||
fmt.Fprintf(writer, `<input type="radio" name="%s" value="%s"%s>`,
|
||||
html.EscapeString(field.Name), html.EscapeString(val_str), checked)
|
||||
return nil
|
||||
}
|
||||
|
||||
// tagURLForParser implements a {% urlfor %} tag.
|
||||
//
|
||||
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
|
||||
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
|
||||
func tagRadioParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
|
||||
var field string
|
||||
typeToken := arguments.MatchType(p2.TokenIdentifier)
|
||||
if typeToken != nil {
|
||||
field = typeToken.Val
|
||||
} else if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
|
||||
field = sToken.Val
|
||||
} else {
|
||||
return nil, arguments.Error("Expected an identifier or string.", nil)
|
||||
}
|
||||
|
||||
expr, err := arguments.ParseExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
|
||||
|
||||
v := &tagRadioNode{Pongo2BaseTag: Pongo2BaseTag{field: field},
|
||||
value: expr}
|
||||
return v.Execute(ctx, w)
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
p2.RegisterTag("radio", tagRadioParser)
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package pongo2
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
|
||||
p2 "github.com/flosch/pongo2"
|
||||
)
|
||||
|
||||
type tagURLForNode struct {
|
||||
objectEvaluators []p2.IEvaluator
|
||||
}
|
||||
|
||||
func (node *tagURLForNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
|
||||
args := make([]string, len(node.objectEvaluators))
|
||||
for i, ev := range node.objectEvaluators {
|
||||
obj, err := ev.Evaluate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args[i] = obj.String()
|
||||
}
|
||||
|
||||
params := make([]interface{}, len(args))
|
||||
params[0] = args[0]
|
||||
for i := range params[1:] {
|
||||
params[i+1] = args[i+1]
|
||||
}
|
||||
|
||||
url, err := revel.ReverseURL(params...)
|
||||
if nil != err {
|
||||
return ctx.Error(err.Error(), nil)
|
||||
}
|
||||
|
||||
writer.WriteString(string(url))
|
||||
return nil
|
||||
}
|
||||
|
||||
// tagURLForParser implements a {% urlfor %} tag.
|
||||
//
|
||||
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
|
||||
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
|
||||
func tagURLForParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
|
||||
evals := []p2.IEvaluator{}
|
||||
for arguments.Remaining() > 0 {
|
||||
expr, err := arguments.ParseExpression()
|
||||
evals = append(evals, expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(evals) <= 0 {
|
||||
return nil, arguments.Error("URL takes one argument for the controller and any number of optional value.", nil)
|
||||
}
|
||||
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
|
||||
|
||||
v := &tagURLForNode{evals}
|
||||
return v.Execute(ctx, w)
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
p2.RegisterTag("url", tagURLForParser)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
{{Error.Title}}
|
||||
</h1>
|
||||
<p>
|
||||
{{Error.Description}}
|
||||
</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "{{Error.Title|escapejs}}",
|
||||
"description": "{{Error.Description|escapejs}}"
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{Error.Title}}
|
||||
|
||||
{{Error.Description}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<forbidden>{{Error.Description}}</forbidden>
|
||||
Generated
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Helvetica, Arial, Sans;
|
||||
background: #EEEEEE;
|
||||
}
|
||||
.block {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
#header h1 {
|
||||
font-weight: normal;
|
||||
font-size: 28px;
|
||||
margin: 0;
|
||||
}
|
||||
#more {
|
||||
color: #666;
|
||||
font-size: 80%;
|
||||
border: none;
|
||||
}
|
||||
#header {
|
||||
background: #FFFFCC;
|
||||
}
|
||||
#header p {
|
||||
color: #333;
|
||||
}
|
||||
#routes {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
#routes h2 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
#routes ol {
|
||||
|
||||
}
|
||||
#routes li {
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="header" class="block">
|
||||
<h1>
|
||||
{{Error.Title}}
|
||||
</h1>
|
||||
<p>
|
||||
{{Error.Description}}
|
||||
</p>
|
||||
</div>
|
||||
<div id="routes" class="block">
|
||||
<h2>These routes have been tried, in this order :</h2>
|
||||
<ol>
|
||||
{% for route in Router.Routes %}
|
||||
<li>{{route.Method}} {{route.Path}}{{route.Action}}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Not found</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% if DevMode %}
|
||||
|
||||
{% include "errors/404-dev.html" %}
|
||||
|
||||
{% else %}
|
||||
<h1>
|
||||
{{ Error.Title }}
|
||||
</h1>
|
||||
<p>
|
||||
{{ Error.Description}}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "{{Error.Title|escapejs}}",
|
||||
"description": "{{Error.Description|escapejs}}"
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{Error.Title}}
|
||||
|
||||
{{Error.Description}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<notfound>{{Error.Description}}</notfound>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Method not allowed</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
{{Error.Title}}
|
||||
</h1>
|
||||
<p>
|
||||
{{Error.Description}}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "{{Error.Title|escapejs}}",
|
||||
"description": "{{Error.Description|escapejs}}"
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{Error.Title}}
|
||||
|
||||
{{Error.Description}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<method-not-allowed>{{Error.Description}}</method-not-allowed>
|
||||
Generated
Vendored
+131
@@ -0,0 +1,131 @@
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Helvetica, Arial, Sans;
|
||||
background: #EEEEEE;
|
||||
}
|
||||
.block {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
#header h1 {
|
||||
font-weight: normal;
|
||||
font-size: 28px;
|
||||
margin: 0;
|
||||
}
|
||||
#more {
|
||||
color: #666;
|
||||
font-size: 80%;
|
||||
border: none;
|
||||
}
|
||||
#header {
|
||||
background: #fcd2da;
|
||||
}
|
||||
#header p {
|
||||
color: #333;
|
||||
}
|
||||
#source {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
#source h2 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
#source .lineNumber {
|
||||
float: left;
|
||||
display: block;
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
#source .line {
|
||||
clear: both;
|
||||
color: #333;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
#source pre {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
#source .error {
|
||||
color: #c00 !important;
|
||||
}
|
||||
#source .error .lineNumber {
|
||||
background: #c00;
|
||||
}
|
||||
#source a {
|
||||
text-decoration: none;
|
||||
}
|
||||
#source a:hover * {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
#source a:hover pre {
|
||||
background: #FAFFCF !important;
|
||||
}
|
||||
#source em {
|
||||
font-style: normal;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
#source strong {
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
}
|
||||
#stack {
|
||||
background: #eee;
|
||||
padding:0 1em 1em;
|
||||
}
|
||||
#stack h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
#stack code {
|
||||
font-family:monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
<div id="header" class="block">
|
||||
<h1>{{Error.Title}}</h1>
|
||||
<p>
|
||||
{% if Error.SourceType %}
|
||||
The {{Error.SourceType}} <strong>{{Error.Path}}</strong> does not compile: <strong>{{Error.Description}}</strong>
|
||||
{% else %}
|
||||
{{Error.Description}}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% if Error.Path %}
|
||||
<div id="source" class="block">
|
||||
<h2>In {{Error.Path}}
|
||||
{% if Error.Line %}
|
||||
(around {% if Error.Line %}line {{Error.Line}}{% endif %}{% if Error.Column %} column {{Error.Column}}{% endif %})
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% for src in Error.ContextSource %}
|
||||
<div class="line{% if src.IsError %} error{% endif %}">
|
||||
<span class="lineNumber">{{src.Line}}:</span>
|
||||
<pre>{{src.Source}}</pre>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if Error.Stack %}
|
||||
<div id="stack">
|
||||
<h3>Call Stack</h3>
|
||||
<code>{{Error.Stack}}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if Error.MetaError %}
|
||||
<div id="source" class="block">
|
||||
<h2>Additionally, an error occurred while handling this error.</h2>
|
||||
<div class="line error">
|
||||
{{Error.MetaError}}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Application error</title>
|
||||
</head>
|
||||
<body>
|
||||
{% if DevMode %}
|
||||
{% include "errors/500-dev.html" %}
|
||||
{% else %}
|
||||
<h1>Oops, an error occured</h1>
|
||||
<p>
|
||||
This exception has been logged.
|
||||
</p>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "{{Error.Title|escapejs}}",
|
||||
"description": "{{Error.Description|escapejs}}"
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{{Error.Title}}
|
||||
{{Error.Description}}
|
||||
|
||||
{% if RunMode == "dev" %}
|
||||
{% if Error.Path %}
|
||||
----------
|
||||
In {{Error.Path}} {% if Error.Line %}(around line {{Error.Line}}){% endif %}
|
||||
|
||||
{% for src in Error.ContextSource %}
|
||||
{% if src.IsError %}>{% else %} {% endif %} {{src.Line}}: {{src.Source}}{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<error>
|
||||
<title>{{Error.Title}}</title>
|
||||
<description>{{Error.Description}}</description>
|
||||
</error>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package pongo2
|
||||
|
||||
// Required for vendoring see golang.org/issue/13832
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
||||
// Revel Framework source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/revel/testing"
|
||||
)
|
||||
|
||||
// TestRunner is a controller which is used for running application tests in browser.
|
||||
type TestRunner struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// TestSuiteDesc is used for storing information about a single test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestSuiteDesc struct {
|
||||
Name string
|
||||
Tests []TestDesc
|
||||
|
||||
// Elem is reflect.Type which can be used for accessing methods
|
||||
// of the test suite.
|
||||
Elem reflect.Type
|
||||
}
|
||||
|
||||
// TestDesc is used for describing a single test of some test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestDesc struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// TestSuiteResult stores the results the whole test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestSuiteResult struct {
|
||||
Name string
|
||||
Passed bool
|
||||
Results []TestResult
|
||||
}
|
||||
|
||||
// TestResult represents the results of running a single test of some test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestResult struct {
|
||||
Name string
|
||||
Passed bool
|
||||
ErrorHTML template.HTML
|
||||
ErrorSummary string
|
||||
}
|
||||
|
||||
var (
|
||||
testSuites []TestSuiteDesc // A list of all available tests.
|
||||
|
||||
none = []reflect.Value{} // It is used as input for reflect call in a few places.
|
||||
|
||||
// registeredTests simplifies the search of test suites by their name.
|
||||
// "TestSuite.TestName" is used as a key. Value represents index in testSuites.
|
||||
registeredTests map[string]int
|
||||
)
|
||||
|
||||
/*
|
||||
Controller's action methods are below.
|
||||
*/
|
||||
|
||||
// Index is an action which renders the full list of available test suites and their tests.
|
||||
func (c TestRunner) Index() revel.Result {
|
||||
c.ViewArgs["suiteFound"] = len(testSuites) > 0
|
||||
return c.Render(testSuites)
|
||||
}
|
||||
|
||||
// Suite method allows user to navigate to individual Test Suite and their tests
|
||||
func (c TestRunner) Suite(suite string) revel.Result {
|
||||
var foundTestSuites []TestSuiteDesc
|
||||
for _, testSuite := range testSuites {
|
||||
if strings.EqualFold(testSuite.Name, suite) {
|
||||
foundTestSuites = append(foundTestSuites, testSuite)
|
||||
}
|
||||
}
|
||||
|
||||
c.ViewArgs["testSuites"] = foundTestSuites
|
||||
c.ViewArgs["suiteFound"] = len(foundTestSuites) > 0
|
||||
c.ViewArgs["suiteName"] = suite
|
||||
|
||||
return c.RenderTemplate("TestRunner/Index.html")
|
||||
}
|
||||
|
||||
// Run runs a single test, given by the argument.
|
||||
func (c TestRunner) Run(suite, test string) revel.Result {
|
||||
// Check whether requested test exists.
|
||||
suiteIndex, ok := registeredTests[suite+"."+test]
|
||||
if !ok {
|
||||
return c.NotFound("Test %s.%s does not exist", suite, test)
|
||||
}
|
||||
|
||||
result := TestResult{Name: test}
|
||||
|
||||
// Found the suite, create a new instance and run the named method.
|
||||
t := testSuites[suiteIndex].Elem
|
||||
v := reflect.New(t)
|
||||
func() {
|
||||
// When the function stops executing try to recover from panic.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// If panic error is empty, exit.
|
||||
panicErr := revel.NewErrorFromPanic(err)
|
||||
if panicErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, prepare and format the response of server if possible.
|
||||
testSuite := v.Elem().FieldByName("TestSuite").Interface().(testing.TestSuite)
|
||||
res := formatResponse(testSuite)
|
||||
|
||||
// Render the error and save to the result structure.
|
||||
var buffer bytes.Buffer
|
||||
tmpl, _ := revel.MainTemplateLoader.TemplateLang("TestRunner/FailureDetail.html", "")
|
||||
_ = tmpl.Render(&buffer, map[string]interface{}{
|
||||
"error": panicErr,
|
||||
"response": res,
|
||||
"postfix": suite + "_" + test,
|
||||
})
|
||||
result.ErrorSummary = errorSummary(panicErr)
|
||||
result.ErrorHTML = template.HTML(buffer.String())
|
||||
}
|
||||
}()
|
||||
|
||||
// Initialize the test suite with a NewTestSuite()
|
||||
testSuiteInstance := v.Elem().FieldByName("TestSuite")
|
||||
testSuiteInstance.Set(reflect.ValueOf(testing.NewTestSuite()))
|
||||
|
||||
// Make sure After method will be executed at the end.
|
||||
if m := v.MethodByName("After"); m.IsValid() {
|
||||
defer m.Call(none)
|
||||
}
|
||||
|
||||
// Start from running Before method of test suite if exists.
|
||||
if m := v.MethodByName("Before"); m.IsValid() {
|
||||
m.Call(none)
|
||||
}
|
||||
|
||||
// Start the test method itself.
|
||||
v.MethodByName(test).Call(none)
|
||||
|
||||
// No panic means success.
|
||||
result.Passed = true
|
||||
}()
|
||||
|
||||
return c.RenderJSON(result)
|
||||
}
|
||||
|
||||
// List returns a JSON list of test suites and tests.
|
||||
// It is used by revel test command line tool.
|
||||
func (c TestRunner) List() revel.Result {
|
||||
return c.RenderJSON(testSuites)
|
||||
}
|
||||
|
||||
/*
|
||||
Below are helper functions.
|
||||
*/
|
||||
|
||||
// describeSuite expects testsuite interface as input parameter
|
||||
// and returns its description in a form of TestSuiteDesc structure.
|
||||
func describeSuite(testSuite interface{}) TestSuiteDesc {
|
||||
t := reflect.TypeOf(testSuite)
|
||||
|
||||
// Get a list of methods of the embedded test type.
|
||||
// It will be used to make sure the same tests are not included in multiple test suites.
|
||||
super := t.Elem().Field(0).Type
|
||||
superMethods := map[string]bool{}
|
||||
for i := 0; i < super.NumMethod(); i++ {
|
||||
// Save the current method's name.
|
||||
superMethods[super.Method(i).Name] = true
|
||||
}
|
||||
|
||||
// Get a list of methods on the test suite that take no parameters, return
|
||||
// no results, and were not part of the embedded type's method set.
|
||||
var tests []TestDesc
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
m := t.Method(i)
|
||||
mt := m.Type
|
||||
|
||||
// Make sure the test method meets the criterias:
|
||||
// - method of testSuite without input parameters;
|
||||
// - nothing is returned;
|
||||
// - has "Test" prefix;
|
||||
// - doesn't belong to the embedded structure.
|
||||
methodWithoutParams := (mt.NumIn() == 1 && mt.In(0) == t)
|
||||
nothingReturned := (mt.NumOut() == 0)
|
||||
hasTestPrefix := (strings.HasPrefix(m.Name, "Test"))
|
||||
if methodWithoutParams && nothingReturned && hasTestPrefix && !superMethods[m.Name] {
|
||||
// Register the test suite's index so we can quickly find it by test's name later.
|
||||
registeredTests[t.Elem().Name()+"."+m.Name] = len(testSuites)
|
||||
|
||||
// Add test to the list of tests.
|
||||
tests = append(tests, TestDesc{m.Name})
|
||||
}
|
||||
}
|
||||
|
||||
return TestSuiteDesc{
|
||||
Name: t.Elem().Name(),
|
||||
Tests: tests,
|
||||
Elem: t.Elem(),
|
||||
}
|
||||
}
|
||||
|
||||
// errorSummary gets an error and returns its summary in human readable format.
|
||||
func errorSummary(err *revel.Error) (message string) {
|
||||
expectedPrefix := "(expected)"
|
||||
actualPrefix := "(actual)"
|
||||
errDesc := err.Description
|
||||
//strip the actual/expected stuff to provide more condensed display.
|
||||
if strings.Index(errDesc, expectedPrefix) == 0 {
|
||||
errDesc = errDesc[len(expectedPrefix):]
|
||||
}
|
||||
if strings.LastIndex(errDesc, actualPrefix) > 0 {
|
||||
errDesc = errDesc[0 : len(errDesc)-len(actualPrefix)]
|
||||
}
|
||||
|
||||
errFile := err.Path
|
||||
slashIdx := strings.LastIndex(errFile, "/")
|
||||
if slashIdx > 0 {
|
||||
errFile = errFile[slashIdx+1:]
|
||||
}
|
||||
|
||||
message = fmt.Sprintf("%s %s#%d", errDesc, errFile, err.Line)
|
||||
|
||||
/*
|
||||
// If line of error isn't known return the message as is.
|
||||
if err.Line == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, include info about the line number and the relevant
|
||||
// source code lines.
|
||||
message += fmt.Sprintf(" (around line %d): ", err.Line)
|
||||
for _, line := range err.ContextSource() {
|
||||
if line.IsError {
|
||||
message += line.Source
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// formatResponse gets *revel.TestSuite as input parameter and
|
||||
// transform response related info into a readable format.
|
||||
func formatResponse(t testing.TestSuite) map[string]string {
|
||||
if t.Response == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
// Since Go 1.6 http.Request struct contains `Cancel <-chan struct{}` which
|
||||
// results in `json: unsupported type: <-chan struct {}`
|
||||
// So pull out required things for Request and Response
|
||||
req := map[string]interface{}{
|
||||
"Method": t.Response.Request.Method,
|
||||
"URL": t.Response.Request.URL,
|
||||
"Proto": t.Response.Request.Proto,
|
||||
"ContentLength": t.Response.Request.ContentLength,
|
||||
"Header": t.Response.Request.Header,
|
||||
"Form": t.Response.Request.Form,
|
||||
"PostForm": t.Response.Request.PostForm,
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"Status": t.Response.Status,
|
||||
"StatusCode": t.Response.StatusCode,
|
||||
"Proto": t.Response.Proto,
|
||||
"Header": t.Response.Header,
|
||||
"ContentLength": t.Response.ContentLength,
|
||||
"TransferEncoding": t.Response.TransferEncoding,
|
||||
}
|
||||
|
||||
// Beautify the response JSON to make it human readable.
|
||||
respBytes, err := json.MarshalIndent(
|
||||
map[string]interface{}{
|
||||
"Response": resp,
|
||||
"Request": req,
|
||||
},
|
||||
"",
|
||||
" ")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
// Remove extra new line symbols so they do not take too much space on a result page.
|
||||
// Allow no more than 1 line break at a time.
|
||||
body := strings.Replace(string(t.ResponseBody), "\n\n", "\n", -1)
|
||||
body = strings.Replace(body, "\r\n\r\n", "\r\n", -1)
|
||||
|
||||
return map[string]string{
|
||||
"Headers": string(respBytes),
|
||||
"Body": strings.TrimSpace(body),
|
||||
}
|
||||
}
|
||||
|
||||
//sortbySuiteName sorts the testsuites by name.
|
||||
type sortBySuiteName []interface{}
|
||||
|
||||
func (a sortBySuiteName) Len() int { return len(a) }
|
||||
func (a sortBySuiteName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortBySuiteName) Less(i, j int) bool {
|
||||
return reflect.TypeOf(a[i]).Elem().Name() < reflect.TypeOf(a[j]).Elem().Name()
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Every time app is restarted convert the list of available test suites
|
||||
// provided by the revel testing package into a format which will be used by
|
||||
// the testrunner module and revel test cmd.
|
||||
revel.OnAppStart(func() {
|
||||
// Extracting info about available test suites from revel/testing package.
|
||||
registeredTests = map[string]int{}
|
||||
sort.Sort(sortBySuiteName(testing.TestSuites))
|
||||
for _, testSuite := range testing.TestSuites {
|
||||
testSuites = append(testSuites, describeSuite(testSuite))
|
||||
}
|
||||
})
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func init() {
|
||||
revel.OnAppStart(func() {
|
||||
revel.AppLog.Info("Go to /@tests to run the tests.")
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<b>{{.error.Description}}</b>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active"><a href="#error_{{.postfix}}" role="tab" data-toggle="tab">Error</a></li>
|
||||
<li><a href="#stack_{{.postfix}}" role="tab" data-toggle="tab">Stack</a></li>
|
||||
{{if .response}}
|
||||
<li><a href="#headers_{{.postfix}}" role="tab" data-toggle="tab">Headers</a></li>
|
||||
<li><a href="#body_{{.postfix}}" role="tab" data-toggle="tab">Response Body</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="result_{{.postfix}}">
|
||||
<div class="tab-pane active" id="error_{{.postfix}}">
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
In {{.error.Path}}{{if .error.Line}} (around {{if .error.Line}}line {{.error.Line}}{{end}}{{if .error.Column}} column {{.error.Column}}{{end}}){{end}}:
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{range .error.ContextSource}}
|
||||
{{if .IsError}}
|
||||
<pre><code class="go">{{.Source}}</code></pre>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="stack_{{.postfix}}">
|
||||
<pre><code class="bash">{{.error.Stack}}</code></pre>
|
||||
</div>
|
||||
{{if .response}}
|
||||
<div class="tab-pane" id="headers_{{.postfix}}">
|
||||
<pre><code class="json">{{.response.Headers}}</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane" id="body_{{.postfix}}">
|
||||
<pre><code class="html">{{.response.Body}}</code></pre>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Revel Test Runner</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<link href="{{url `Root`}}/@tests/public/css/bootstrap.min.css" type="text/css" rel="stylesheet"></link>
|
||||
<link href="{{url `Root`}}/@tests/public/css/github.css" type="text/css" rel="stylesheet"></link>
|
||||
<script src="{{url `Root`}}/@tests/public/js/jquery-1.9.1.min.js" type="text/javascript"></script>
|
||||
<script src="{{url `Root`}}/@tests/public/js/bootstrap.min.js" type="text/javascript"></script>
|
||||
<script src="{{url `Root`}}/@tests/public/js/highlight.pack.js" type="text/javascript"></script>
|
||||
<style>
|
||||
header { background-color:#ADD8E6 }
|
||||
header h1 {margin-top: 10px; margin-bottom:20px;}
|
||||
header table {margin-bottom: 0px }
|
||||
td .btn {margin-bottom: 1px; }
|
||||
button.file-test { margin-bottom: 0px; margin-left: 2px }
|
||||
table.tests tr { border-bottom: 1px solid #ddd; background-color: #f9f9f9; }
|
||||
.passed td { background-color: #90EE90 !important; }
|
||||
.failed td { background-color: #FFB6C1 !important; }
|
||||
td.result div.panel-default{ display:none; }
|
||||
td.result > a { color: red; }
|
||||
td.rightCol, td.leftCol { width: 40px; }
|
||||
pre { font-size:10px; white-space: pre; }
|
||||
.panel-heading {
|
||||
padding: 10px 5px 8px 5px
|
||||
}
|
||||
.name { width: 35%; }
|
||||
.w100 { width: 100%; }
|
||||
.logo, .logo:hover { text-decoration: none; color: inherit;}
|
||||
.pnt-triangle { color: #777; font-size: 18px;}
|
||||
.panel-group .panel-heading+.panel-collapse>.panel-body { border-top: none;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1 class="pull-left">
|
||||
<a href="/@tests" class="logo">Test Runner</a> <small>- Run your application's tests here.</small>
|
||||
</h1>
|
||||
<div style="margin-top:16px" class="pull-right">
|
||||
<button class="btn btn-success {{if not .suiteFound}}disabled{{end}}" all-tests="">Run All Tests</button>
|
||||
<div><a class="small" href="#" id="allTestResults"></a></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="panel-group container">
|
||||
{{if not .suiteFound}}
|
||||
<div style="margin-top:20px;padding:15px;" class="panel panel-default">
|
||||
<span style="font-weight:bold;color:#777;">Suite "{{.suiteName}}" is not found.</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{range .testSuites}}
|
||||
{{ $testFile := .Name }}
|
||||
<div style="margin-top:20px;" class="panel panel-default">
|
||||
<div class="panel-heading collapseLnk" style="cursor:pointer" data-toggle="collapse" data-target="#{{.Name}}">
|
||||
<button id="suite{{.Name}}" class="btn btn-xs btn-success" test-file="{{.Name}}">Run</button>
|
||||
<span class="h5"> <a href="/@tests/{{ .Name }}">{{.Name}}</a></span>
|
||||
<span id="pointTriangle{{.Name}}" class="pnt-triangle pull-right" data-tri-open="open">▼</span>
|
||||
</div>
|
||||
<div id="{{.Name}}" class="panel-collapse collapse in">
|
||||
<table class="panel-body table table-condensed tests" suite="{{.Name}}">
|
||||
{{range .Tests}}
|
||||
<tr id="testRow{{.Name}}">
|
||||
<td class="leftCol"><button data-test-file="{{$testFile}}" test="{{.Name}}" class="leftbutton btn btn-success btn-xs">Run</button></td>
|
||||
<td class="name">{{ .Name }}</td>
|
||||
<td class="result"><a href="#"></a></td>
|
||||
<td class="rightCol"><button data-test-file="{{$testFile}}" test="{{.Name}}" class="pull-right btn btn-success btn-xs">Run</button></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var passCount = 0;
|
||||
var failCount = 0;
|
||||
var buttons = [];
|
||||
var running;
|
||||
|
||||
$(function() {
|
||||
var divId, visible;
|
||||
var oneOpened = false;
|
||||
var panels = $("div.panel-collapse");
|
||||
//close any panels that were previously closed.
|
||||
panels.each(function() {
|
||||
divId = "#" + $(this).attr("id");
|
||||
visible = parseBoolean(localStorage.getItem("testrunner_" + divId));
|
||||
if (visible) {
|
||||
togglePntTriangle(divId, visible);
|
||||
oneOpened = visible;
|
||||
} else {
|
||||
$(divId).css("height", "0").removeClass("in"); //this is the way bootstrap does it.
|
||||
$("div[data-target=" + divId + "]").addClass("collapsed");
|
||||
togglePntTriangle(divId, visible);
|
||||
}
|
||||
});
|
||||
|
||||
if (!oneOpened && panels.length > 0) {
|
||||
divId = "#" + panels[0].id;
|
||||
$(divId).collapse('show');
|
||||
togglePntTriangle(divId, true);
|
||||
}
|
||||
});
|
||||
|
||||
$(".fileTestLnk").click(function() {
|
||||
var tableId = $(this).attr("href");
|
||||
$(tableId).toggle();
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#allTestResults").click(function() {
|
||||
var badRows = $("tr.failed");
|
||||
if (badRows.length >= 0) {
|
||||
badRows[0].scrollIntoView();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$("button[test]").click(function() {
|
||||
$("#allTestResults").text("");
|
||||
var button = $(this).addClass("disabled").text("Running");
|
||||
$(this).closest("tr").removeClass("passed").removeClass("failed");
|
||||
addToQueue(button);
|
||||
});
|
||||
|
||||
$("td.result a").click(function() { //show/hide the extended error div
|
||||
$(this).siblings().toggle();
|
||||
return false;
|
||||
});
|
||||
|
||||
$("button[test-file]").click(function() {
|
||||
$("#allTestResults").text("");
|
||||
var testfile = $(this).attr('test-file');
|
||||
$("button").each(function() {
|
||||
if ($(this).data("test-file") == testfile)
|
||||
$(this).click();
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$("button[all-tests]").click(function() {
|
||||
$("tr").removeClass("passed").removeClass("failed");
|
||||
passCount = 0;
|
||||
failCount = 0;
|
||||
var button = $(this).addClass("disabled").text("Running");
|
||||
$("button.leftbutton[test]").click();
|
||||
});
|
||||
|
||||
$("div.collapseLnk").click(function() {
|
||||
var tableId = $(this).data("target");
|
||||
var visible = !$(tableId).is(":visible");
|
||||
localStorage.setItem("testrunner_" + tableId, visible);
|
||||
togglePntTriangle(tableId, visible);
|
||||
});
|
||||
|
||||
function togglePntTriangle(suiteId, visible) {
|
||||
if (suiteId.charAt(0) == '#') {
|
||||
suiteId = suiteId.substring(1);
|
||||
}
|
||||
$('#pointTriangle' + suiteId).html(visible ? '▼' : '▶');
|
||||
}
|
||||
|
||||
function parseBoolean(str) {
|
||||
return /^true$/i.test(str);
|
||||
}
|
||||
|
||||
function addToQueue(button) {
|
||||
buttons.push(button);
|
||||
if (!running) {
|
||||
running = true;
|
||||
nextTest();
|
||||
}
|
||||
}
|
||||
|
||||
function nextTest() {
|
||||
if (buttons.length == 0) {
|
||||
running = false;
|
||||
} else {
|
||||
var next = buttons.shift();
|
||||
runTest(next);
|
||||
}
|
||||
}
|
||||
|
||||
function runTest(button) {
|
||||
var suite = button.parents("table").attr("suite");
|
||||
var test = button.attr("test");
|
||||
var row = button.parents("tr");
|
||||
var resultCell = row.children(".result");
|
||||
$("a", resultCell).text("");
|
||||
$("div.panel-default", resultCell).remove();
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
url: "{{url `Root`}}/@tests/"+suite+"/"+test,
|
||||
success: function(result) {
|
||||
row.attr("class", result.Passed ? "passed" : "failed");
|
||||
if (result.Passed) {
|
||||
passCount++;
|
||||
} else {
|
||||
console.log("fail:", result.Name);
|
||||
failCount++;
|
||||
var resultLnk = $("a", resultCell);
|
||||
$(resultLnk).text(result.ErrorSummary);
|
||||
resultCell.append(result.ErrorHTML);
|
||||
|
||||
var pnlDiv = row.closest("div");
|
||||
if ($(pnlDiv).hasClass("in") == false) {
|
||||
$("#link-" + suite).click();
|
||||
}
|
||||
|
||||
$("#result_" + suite + "_" + test + " pre code").each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
button.removeClass("disabled").text("Run");
|
||||
var runAllBut = $("button[all-tests]");
|
||||
if (buttons.length == 0 && runAllBut.hasClass("disabled")) {
|
||||
runAllBut.removeClass("disabled").text("Run All Tests");
|
||||
var resMsg = passCount + " passed, " + failCount + " failed.";
|
||||
$("#allTestResults").text(resMsg);
|
||||
}
|
||||
nextTest();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Revel Test Runner</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
header { padding:20px 0; }
|
||||
header.passed { background-color: #90EE90 !important; }
|
||||
header.failed { background-color: #FFB6C1 !important; }
|
||||
table { margin-top: 20px; padding: 8px; line-height: 20px; }
|
||||
td { vertical-align: top; padding-right:20px; }
|
||||
a { color: #0088cc; }
|
||||
.container { margin-left: auto; margin-right: auto; width: 940px; overflow: hidden; }
|
||||
.result h2 { font-size: 16px; border-bottom: 1px solid #f0f0f0; padding-bottom: 0.2em; }
|
||||
.result.failed b { font-weight:bold; color: #C00; font-size: 14px; }
|
||||
.result.failed h2 { color: #C00; }
|
||||
.result .info { font-size: 12px; }
|
||||
.result .info pre { overflow: auto; background-color: #f0f0f0; width: 100%; max-height: 500px; }
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="{{if .Passed}}passed{{else}}failed{{end}}">
|
||||
<div class="container">
|
||||
<h1>{{.Name}}</h1>
|
||||
<p>{{if .Passed}}PASSED{{else}}FAILED{{end}}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
{{range .Results}}
|
||||
<div class="result {{if .Passed}}passed{{else}}failed{{end}}">
|
||||
<div><h2>{{.Name}}</h2></div>
|
||||
<div class="info">{{if .ErrorHTML}}{{.ErrorHTML}}{{else}}PASSED{{end}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
GET /@tests TestRunner.Index
|
||||
GET /@tests.list TestRunner.List
|
||||
GET /@tests/public/*filepath Static.ServeModule(testrunner,public)
|
||||
GET /@tests/:suite TestRunner.Suite
|
||||
GET /@tests/:suite/:test TestRunner.Run
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/*!
|
||||
* Bootstrap v3.2.0 (http://getbootstrap.com)
|
||||
* Copyright 2011-2014 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
*//*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-Line truncated
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
|
||||
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #333;
|
||||
background: #f8f8f8;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-template_comment,
|
||||
.diff .hljs-header,
|
||||
.hljs-javadoc {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.css .rule .hljs-keyword,
|
||||
.hljs-winutils,
|
||||
.javascript .hljs-title,
|
||||
.nginx .hljs-title,
|
||||
.hljs-subst,
|
||||
.hljs-request,
|
||||
.hljs-status {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-hexcolor,
|
||||
.ruby .hljs-constant {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-tag .hljs-value,
|
||||
.hljs-phpdoc,
|
||||
.hljs-dartdoc,
|
||||
.tex .hljs-formula {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-id,
|
||||
.scss .hljs-preprocessor {
|
||||
color: #900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.javascript .hljs-title,
|
||||
.hljs-list .hljs-keyword,
|
||||
.hljs-subst {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-type,
|
||||
.vhdl .hljs-literal,
|
||||
.tex .hljs-command {
|
||||
color: #458;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-tag .hljs-title,
|
||||
.hljs-rules .hljs-property,
|
||||
.django .hljs-tag .hljs-keyword {
|
||||
color: #000080;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-attribute,
|
||||
.hljs-variable,
|
||||
.lisp .hljs-body {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-regexp {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.ruby .hljs-symbol .hljs-string,
|
||||
.lisp .hljs-keyword,
|
||||
.clojure .hljs-keyword,
|
||||
.scheme .hljs-keyword,
|
||||
.tex .hljs-special,
|
||||
.hljs-prompt {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.hljs-built_in {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-preprocessor,
|
||||
.hljs-pragma,
|
||||
.hljs-pi,
|
||||
.hljs-doctype,
|
||||
.hljs-shebang,
|
||||
.hljs-cdata {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
background: #dfd;
|
||||
}
|
||||
|
||||
.diff .hljs-change {
|
||||
background: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-chunk {
|
||||
color: #aaa;
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Loaded 100 of 670 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user