diff --git a/.travis.yml b/.travis.yml index c59cef61..3a6c9f84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ language: go go: - - 1.5.1 - + - tip + - 1.5.3 + - 1.4.3 + - 1.3.3 services: - redis-server - mysql @@ -24,7 +26,15 @@ install: - go get github.com/couchbase/go-couchbase - go get github.com/siddontang/ledisdb/config - go get github.com/siddontang/ledisdb/ledis + - go get golang.org/x/tools/cmd/vet + - go get github.com/golang/lint/golint before_script: - sh -c "if [ '$ORM_DRIVER' = 'postgres' ]; then psql -c 'create database orm_test;' -U postgres; fi" - sh -c "if [ '$ORM_DRIVER' = 'mysql' ]; then mysql -u root -e 'create database orm_test;'; fi" - sh -c "if [ '$ORM_DRIVER' = 'sqlite' ]; then touch $TRAVIS_BUILD_DIR/orm_test.db; fi" +script: + - go vet -x ./... + - $HOME/gopath/bin/golint ./... + - go test -v ./... +notifications: + webhooks: https://hooks.pubu.im/services/z7m9bvybl3rgtg9 diff --git a/README.md b/README.md index fec6113f..6c589584 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Please see [Documentation](http://beego.me/docs) for more. ## Community * [http://beego.me/community](http://beego.me/community) +* Welcome to join us in Slack: [https://beego.slack.com](https://beego.slack.com), you can get invited from [here](https://github.com/beego/beedoc/issues/232) ## LICENSE diff --git a/admin.go b/admin.go index 3effc582..031e6421 100644 --- a/admin.go +++ b/admin.go @@ -90,8 +90,8 @@ func listConf(rw http.ResponseWriter, r *http.Request) { switch command { case "conf": m := make(map[string]interface{}) - m["AppConfigPath"] = AppConfigPath - m["AppConfigProvider"] = AppConfigProvider + m["AppConfigPath"] = appConfigPath + m["AppConfigProvider"] = appConfigProvider m["BConfig.AppName"] = BConfig.AppName m["BConfig.RunMode"] = BConfig.RunMode m["BConfig.RouterCaseSensitive"] = BConfig.RouterCaseSensitive diff --git a/beego.go b/beego.go index 04f02071..fb628e5f 100644 --- a/beego.go +++ b/beego.go @@ -15,7 +15,6 @@ package beego import ( - "fmt" "os" "path/filepath" "strconv" @@ -68,21 +67,6 @@ func Run(params ...string) { } func initBeforeHTTPRun() { - // if AppConfigPath is setted or conf/app.conf exist - err := ParseConfig() - if err != nil { - panic(err) - } - //init log - for adaptor, config := range BConfig.Log.Outputs { - err = BeeLogger.SetLogger(adaptor, config) - if err != nil { - fmt.Printf("%s with the config `%s` got err:%s\n", adaptor, config, err) - } - } - - SetLogFuncCall(BConfig.Log.FileLineNum) - //init hooks AddAPPStartHook(registerMime) AddAPPStartHook(registerDefaultErrorHandler) @@ -101,7 +85,7 @@ func initBeforeHTTPRun() { // TestBeegoInit is for test package init func TestBeegoInit(ap string) { os.Setenv("BEEGO_RUNMODE", "test") - AppConfigPath = filepath.Join(ap, "conf", "app.conf") + appConfigPath = filepath.Join(ap, "conf", "app.conf") os.Chdir(ap) initBeforeHTTPRun() } diff --git a/cache/memcache/memcache_test.go b/cache/memcache/memcache_test.go index 8d98c177..0c8c57f2 100644 --- a/cache/memcache/memcache_test.go +++ b/cache/memcache/memcache_test.go @@ -37,7 +37,7 @@ func TestMemcacheCache(t *testing.T) { t.Error("check err") } - time.Sleep(10 * time.Second) + time.Sleep(11 * time.Second) if bm.IsExist("astaxie") { t.Error("check err") diff --git a/config.go b/config.go index 8cf21530..ffe92f06 100644 --- a/config.go +++ b/config.go @@ -19,6 +19,7 @@ import ( "os" "path/filepath" "strings" + "fmt" "github.com/astaxie/beego/config" "github.com/astaxie/beego/session" @@ -103,17 +104,29 @@ var ( BConfig *Config // AppConfig is the instance of Config, store the config information from file AppConfig *beegoAppConfig - // AppConfigPath is the path to the config files - AppConfigPath string - // AppConfigProvider is the provider for the config, default is ini - AppConfigProvider = "ini" + // AppPath is the absolute path to the app + AppPath string // TemplateCache stores template caching TemplateCache map[string]*template.Template // GlobalSessions is the instance for the session manager GlobalSessions *session.Manager + + workPath string + // appConfigPath is the path to the config files + appConfigPath string + // appConfigProvider is the provider for the config, default is ini + appConfigProvider = "ini" ) func init() { + AppPath, _ = filepath.Abs(filepath.Dir(os.Args[0])) + workPath, _ = os.Getwd() + workPath, _ = filepath.Abs(workPath) + + if workPath != AppPath { + os.Chdir(AppPath) + } + BConfig = &Config{ AppName: "beego", RunMode: DEV, @@ -173,21 +186,19 @@ func init() { Outputs: map[string]string{"console": ""}, }, } - ParseConfig() + + appConfigPath = filepath.Join(AppPath, "conf", "app.conf") + if !utils.FileExists(appConfigPath) { + AppConfig = &beegoAppConfig{config.NewFakeConfig()} + return + } + + parseConfig(appConfigPath) } -// ParseConfig parsed default config file. // now only support ini, next will support json. -func ParseConfig() (err error) { - if AppConfigPath == "" { - if utils.FileExists(filepath.Join("conf", "app.conf")) { - AppConfigPath = filepath.Join("conf", "app.conf") - } else { - AppConfig = &beegoAppConfig{config.NewFakeConfig()} - return - } - } - AppConfig, err = newAppConfig(AppConfigProvider, AppConfigPath) +func parseConfig(appConfigPath string) (err error) { + AppConfig, err = newAppConfig(appConfigProvider, appConfigPath) if err != nil { return err } @@ -241,6 +252,8 @@ func ParseConfig() (err error) { BConfig.WebConfig.Session.SessionCookieLifeTime = AppConfig.DefaultInt("SessionCookieLifeTime", BConfig.WebConfig.Session.SessionCookieLifeTime) BConfig.WebConfig.Session.SessionAutoSetCookie = AppConfig.DefaultBool("SessionAutoSetCookie", BConfig.WebConfig.Session.SessionAutoSetCookie) BConfig.WebConfig.Session.SessionDomain = AppConfig.DefaultString("SessionDomain", BConfig.WebConfig.Session.SessionDomain) + BConfig.Log.AccessLogs = AppConfig.DefaultBool("LogAccessLogs", BConfig.Log.AccessLogs) + BConfig.Log.FileLineNum = AppConfig.DefaultBool("LogFileLineNum", BConfig.Log.FileLineNum) if sd := AppConfig.String("StaticDir"); sd != "" { for k := range BConfig.WebConfig.StaticDir { @@ -273,15 +286,58 @@ func ParseConfig() (err error) { BConfig.WebConfig.StaticExtensionsToGzip = fileExts } } + + if lo := AppConfig.String("LogOutputs"); lo != "" { + los := strings.Split(lo, ";") + for _, v := range los { + if logType2Config := strings.SplitN(v, ",", 2); len(logType2Config) == 2 { + BConfig.Log.Outputs[logType2Config[0]] = logType2Config[1] + } else { + continue + } + } + } + + //init log + BeeLogger.Close() + for adaptor, config := range BConfig.Log.Outputs { + err = BeeLogger.SetLogger(adaptor, config) + if err != nil { + fmt.Printf("%s with the config `%s` got err:%s\n", adaptor, config, err) + } + } + SetLogFuncCall(BConfig.Log.FileLineNum) + return nil } +// LoadAppConfig allow developer to apply a config file +func LoadAppConfig(adapterName, configPath string) error { + absConfigPath, err := filepath.Abs(configPath) + if err != nil { + return err + } + + if !utils.FileExists(absConfigPath) { + return fmt.Errorf("the target config file: %s don't exist", configPath) + } + + if absConfigPath == appConfigPath { + return nil + } + + appConfigPath = absConfigPath + appConfigProvider = adapterName + + return parseConfig(appConfigPath) +} + type beegoAppConfig struct { innerConfig config.Configer } -func newAppConfig(AppConfigProvider, AppConfigPath string) (*beegoAppConfig, error) { - ac, err := config.NewConfig(AppConfigProvider, AppConfigPath) +func newAppConfig(appConfigProvider, appConfigPath string) (*beegoAppConfig, error) { + ac, err := config.NewConfig(appConfigProvider, appConfigPath) if err != nil { return nil, err } diff --git a/config/config.go b/config/config.go index da5d358b..c0afec05 100644 --- a/config/config.go +++ b/config/config.go @@ -106,3 +106,39 @@ func NewConfigData(adapterName string, data []byte) (Configer, error) { } return adapter.ParseData(data) } + +// ParseBool returns the boolean value represented by the string. +// +// It accepts 1, 1.0, t, T, TRUE, true, True, YES, yes, Yes,Y, y, ON, on, On, +// 0, 0.0, f, F, FALSE, false, False, NO, no, No, N,n, OFF, off, Off. +// Any other value returns an error. +func ParseBool(val interface{}) (value bool, err error) { + if val != nil { + switch v := val.(type) { + case bool: + return v, nil + case string: + switch v { + case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "Y", "y", "ON", "on", "On": + return true, nil + case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "N", "n", "OFF", "off", "Off": + return false, nil + } + case int8, int32, int64: + strV := fmt.Sprintf("%s", v) + if strV == "1" { + return true, nil + } else if strV == "0" { + return false, nil + } + case float64: + if v == 1 { + return true, nil + } else if v == 0 { + return false, nil + } + } + return false, fmt.Errorf("parsing %q: invalid syntax", val) + } + return false, fmt.Errorf("parsing : invalid syntax") +} diff --git a/config/fake.go b/config/fake.go index 50aa5d4a..6daaca2c 100644 --- a/config/fake.go +++ b/config/fake.go @@ -82,7 +82,7 @@ func (c *fakeConfigContainer) DefaultInt64(key string, defaultval int64) int64 { } func (c *fakeConfigContainer) Bool(key string) (bool, error) { - return strconv.ParseBool(c.getData(key)) + return ParseBool(c.getData(key)) } func (c *fakeConfigContainer) DefaultBool(key string, defaultval bool) bool { diff --git a/config/ini.go b/config/ini.go index 59e84e1e..da6f2b3a 100644 --- a/config/ini.go +++ b/config/ini.go @@ -27,7 +27,6 @@ import ( "strings" "sync" "time" - "unicode" ) var ( @@ -97,9 +96,11 @@ func (ini *IniConfig) parseFile(name string) (*IniConfigContainer, error) { } if bComment != nil { line = bytes.TrimLeft(line, string(bComment)) - line = bytes.TrimLeftFunc(line, unicode.IsSpace) + // Need append to a new line if multi-line comments. + if comment.Len() > 0 { + comment.WriteByte('\n') + } comment.Write(line) - comment.WriteByte('\n') continue } @@ -194,7 +195,7 @@ type IniConfigContainer struct { // Bool returns the boolean value for a given key. func (c *IniConfigContainer) Bool(key string) (bool, error) { - return strconv.ParseBool(c.getdata(key)) + return ParseBool(c.getdata(key)) } // DefaultBool returns the boolean value for a given key. @@ -299,14 +300,35 @@ func (c *IniConfigContainer) SaveConfigFile(filename string) (err error) { } defer f.Close() + // Get section or key comments. Fixed #1607 + getCommentStr := func(section, key string) string { + comment, ok := "", false + if len(key) == 0 { + comment, ok = c.sectionComment[section] + } else { + comment, ok = c.keyComment[section+"."+key] + } + + if ok { + // Empty comment + if len(comment) == 0 || len(strings.TrimSpace(comment)) == 0 { + return string(bNumComment) + } + prefix := string(bNumComment) + // Add the line head character "#" + return prefix + strings.Replace(comment, lineBreak, lineBreak+prefix, -1) + } + return "" + } + buf := bytes.NewBuffer(nil) // Save default section at first place if dt, ok := c.data[defaultSection]; ok { for key, val := range dt { if key != " " { // Write key comments. - if v, ok := c.keyComment[key]; ok { - if _, err = buf.WriteString(string(bNumComment) + v + lineBreak); err != nil { + if v := getCommentStr(defaultSection, key); len(v) > 0 { + if _, err = buf.WriteString(v + lineBreak); err != nil { return err } } @@ -327,8 +349,8 @@ func (c *IniConfigContainer) SaveConfigFile(filename string) (err error) { for section, dt := range c.data { if section != defaultSection { // Write section comments. - if v, ok := c.sectionComment[section]; ok { - if _, err = buf.WriteString(string(bNumComment) + v + lineBreak); err != nil { + if v := getCommentStr(section, ""); len(v) > 0 { + if _, err = buf.WriteString(v + lineBreak); err != nil { return err } } @@ -341,8 +363,8 @@ func (c *IniConfigContainer) SaveConfigFile(filename string) (err error) { for key, val := range dt { if key != " " { // Write key comments. - if v, ok := c.keyComment[key]; ok { - if _, err = buf.WriteString(string(bNumComment) + v + lineBreak); err != nil { + if v := getCommentStr(section, key); len(v) > 0 { + if _, err = buf.WriteString(v + lineBreak); err != nil { return err } } diff --git a/config/ini_test.go b/config/ini_test.go index 7599ab8b..11063d99 100644 --- a/config/ini_test.go +++ b/config/ini_test.go @@ -15,11 +15,17 @@ package config import ( + "fmt" + "io/ioutil" "os" + "strings" "testing" ) -var inicontext = ` +func TestIni(t *testing.T) { + + var ( + inicontext = ` ;comment one #comment two appname = beeapi @@ -29,6 +35,13 @@ PI = 3.1415976 runmode = "dev" autorender = false copyrequestbody = true +session= on +cookieon= off +newreg = OFF +needlogin = ON +enableSession = Y +enableCookie = N +flag = 1 [demo] key1="asta" key2 = "xie" @@ -36,7 +49,31 @@ CaseInsensitive = true peers = one;two;three ` -func TestIni(t *testing.T) { + keyValue = map[string]interface{}{ + "appname": "beeapi", + "httpport": 8080, + "mysqlport": int64(3600), + "pi": 3.1415976, + "runmode": "dev", + "autorender": false, + "copyrequestbody": true, + "session": true, + "cookieon": false, + "newreg": false, + "needlogin": true, + "enableSession": true, + "enableCookie": false, + "flag": true, + "demo::key1": "asta", + "demo::key2": "xie", + "demo::CaseInsensitive": true, + "demo::peers": []string{"one", "two", "three"}, + "null": "", + "demo2::key1": "", + "error": "", + } + ) + f, err := os.Create("testini.conf") if err != nil { t.Fatal(err) @@ -52,31 +89,31 @@ func TestIni(t *testing.T) { if err != nil { t.Fatal(err) } - if iniconf.String("appname") != "beeapi" { - t.Fatal("appname not equal to beeapi") - } - if port, err := iniconf.Int("httpport"); err != nil || port != 8080 { - t.Error(port) - t.Fatal(err) - } - if port, err := iniconf.Int64("mysqlport"); err != nil || port != 3600 { - t.Error(port) - t.Fatal(err) - } - if pi, err := iniconf.Float("PI"); err != nil || pi != 3.1415976 { - t.Error(pi) - t.Fatal(err) - } - if iniconf.String("runmode") != "dev" { - t.Fatal("runmode not equal to dev") - } - if v, err := iniconf.Bool("autorender"); err != nil || v != false { - t.Error(v) - t.Fatal(err) - } - if v, err := iniconf.Bool("copyrequestbody"); err != nil || v != true { - t.Error(v) - t.Fatal(err) + for k, v := range keyValue { + var err error + var value interface{} + switch v.(type) { + case int: + value, err = iniconf.Int(k) + case int64: + value, err = iniconf.Int64(k) + case float64: + value, err = iniconf.Float(k) + case bool: + value, err = iniconf.Bool(k) + case []string: + value = iniconf.Strings(k) + case string: + value = iniconf.String(k) + default: + value, err = iniconf.DIY(k) + } + if err != nil { + t.Fatalf("get key %q value fail,err %s", k, err) + } else if fmt.Sprintf("%v", v) != fmt.Sprintf("%v", value) { + t.Fatalf("get key %q value, want %v got %v .", k, v, value) + } + } if err = iniconf.Set("name", "astaxie"); err != nil { t.Fatal(err) @@ -84,20 +121,63 @@ func TestIni(t *testing.T) { if iniconf.String("name") != "astaxie" { t.Fatal("get name error") } - if iniconf.String("demo::key1") != "asta" { - t.Fatal("get demo.key1 error") - } - if iniconf.String("demo::key2") != "xie" { - t.Fatal("get demo.key2 error") - } - if v, err := iniconf.Bool("demo::caseinsensitive"); err != nil || v != true { - t.Fatal("get demo.caseinsensitive error") - } - - if data := iniconf.Strings("demo::peers"); len(data) != 3 { - t.Fatal("get strings error", data) - } else if data[0] != "one" { - t.Fatal("get first params error not equat to one") - } } + +func TestIniSave(t *testing.T) { + + const ( + inicontext = ` +app = app +;comment one +#comment two +# comment three +appname = beeapi +httpport = 8080 +# DB Info +# enable db +[dbinfo] +# db type name +# suport mysql,sqlserver +name = mysql +` + + saveResult = ` +app=app +#comment one +#comment two +# comment three +appname=beeapi +httpport=8080 + +# DB Info +# enable db +[dbinfo] +# db type name +# suport mysql,sqlserver +name=mysql +` + ) + cfg, err := NewConfigData("ini", []byte(inicontext)) + if err != nil { + t.Fatal(err) + } + name := "newIniConfig.ini" + if err := cfg.SaveConfigFile(name); err != nil { + t.Fatal(err) + } + defer os.Remove(name) + + if data, err := ioutil.ReadFile(name); err != nil { + t.Fatal(err) + } else { + cfgData := string(data) + datas := strings.Split(saveResult, "\n") + for _, line := range datas { + if strings.Contains(cfgData, line+"\n") == false { + t.Fatalf("different after save ini config file. need contains %q", line) + } + } + + } +} diff --git a/config/json.go b/config/json.go index 65b4ac48..0bc1d456 100644 --- a/config/json.go +++ b/config/json.go @@ -17,6 +17,7 @@ package config import ( "encoding/json" "errors" + "fmt" "io/ioutil" "os" "strings" @@ -70,12 +71,9 @@ type JSONConfigContainer struct { func (c *JSONConfigContainer) Bool(key string) (bool, error) { val := c.getData(key) if val != nil { - if v, ok := val.(bool); ok { - return v, nil - } - return false, errors.New("not bool value") + return ParseBool(val) } - return false, errors.New("not exist key:" + key) + return false, fmt.Errorf("not exist key: %q", key) } // DefaultBool return the bool value if has no error diff --git a/config/json_test.go b/config/json_test.go index 5aedae36..df663461 100644 --- a/config/json_test.go +++ b/config/json_test.go @@ -15,34 +15,14 @@ package config import ( + "fmt" "os" "testing" ) -var jsoncontext = `{ -"appname": "beeapi", -"testnames": "foo;bar", -"httpport": 8080, -"mysqlport": 3600, -"PI": 3.1415976, -"runmode": "dev", -"autorender": false, -"copyrequestbody": true, -"database": { - "host": "host", - "port": "port", - "database": "database", - "username": "username", - "password": "password", - "conns":{ - "maxconnection":12, - "autoconnect":true, - "connectioninfo":"info" - } - } -}` +func TestJsonStartsWithArray(t *testing.T) { -var jsoncontextwitharray = `[ + const jsoncontextwitharray = `[ { "url": "user", "serviceAPI": "http://www.test.com/user" @@ -52,8 +32,6 @@ var jsoncontextwitharray = `[ "serviceAPI": "http://www.test.com/employee" } ]` - -func TestJsonStartsWithArray(t *testing.T) { f, err := os.Create("testjsonWithArray.conf") if err != nil { t.Fatal(err) @@ -90,6 +68,64 @@ func TestJsonStartsWithArray(t *testing.T) { } func TestJson(t *testing.T) { + + var ( + jsoncontext = `{ +"appname": "beeapi", +"testnames": "foo;bar", +"httpport": 8080, +"mysqlport": 3600, +"PI": 3.1415976, +"runmode": "dev", +"autorender": false, +"copyrequestbody": true, +"session": "on", +"cookieon": "off", +"newreg": "OFF", +"needlogin": "ON", +"enableSession": "Y", +"enableCookie": "N", +"flag": 1, +"database": { + "host": "host", + "port": "port", + "database": "database", + "username": "username", + "password": "password", + "conns":{ + "maxconnection":12, + "autoconnect":true, + "connectioninfo":"info" + } + } +}` + keyValue = map[string]interface{}{ + "appname": "beeapi", + "testnames": []string{"foo", "bar"}, + "httpport": 8080, + "mysqlport": int64(3600), + "PI": 3.1415976, + "runmode": "dev", + "autorender": false, + "copyrequestbody": true, + "session": true, + "cookieon": false, + "newreg": false, + "needlogin": true, + "enableSession": true, + "enableCookie": false, + "flag": true, + "database::host": "host", + "database::port": "port", + "database::database": "database", + "database::password": "password", + "database::conns::maxconnection": 12, + "database::conns::autoconnect": true, + "database::conns::connectioninfo": "info", + "unknown": "", + } + ) + f, err := os.Create("testjson.conf") if err != nil { t.Fatal(err) @@ -105,37 +141,32 @@ func TestJson(t *testing.T) { if err != nil { t.Fatal(err) } - if jsonconf.String("appname") != "beeapi" { - t.Fatal("appname not equal to beeapi") - } - if port, err := jsonconf.Int("httpport"); err != nil || port != 8080 { - t.Error(port) - t.Fatal(err) - } - if port, err := jsonconf.Int64("mysqlport"); err != nil || port != 3600 { - t.Error(port) - t.Fatal(err) - } - if pi, err := jsonconf.Float("PI"); err != nil || pi != 3.1415976 { - t.Error(pi) - t.Fatal(err) - } - if jsonconf.String("runmode") != "dev" { - t.Fatal("runmode not equal to dev") - } - if v := jsonconf.Strings("unknown"); len(v) > 0 { - t.Fatal("unknown strings, the length should be 0") - } - if v := jsonconf.Strings("testnames"); len(v) != 2 { - t.Fatal("testnames length should be 2") - } - if v, err := jsonconf.Bool("autorender"); err != nil || v != false { - t.Error(v) - t.Fatal(err) - } - if v, err := jsonconf.Bool("copyrequestbody"); err != nil || v != true { - t.Error(v) - t.Fatal(err) + + for k, v := range keyValue { + var err error + var value interface{} + switch v.(type) { + case int: + value, err = jsonconf.Int(k) + case int64: + value, err = jsonconf.Int64(k) + case float64: + value, err = jsonconf.Float(k) + case bool: + value, err = jsonconf.Bool(k) + case []string: + value = jsonconf.Strings(k) + case string: + value = jsonconf.String(k) + default: + value, err = jsonconf.DIY(k) + } + if err != nil { + t.Fatalf("get key %q value fatal,%v err %s", k, v, err) + } else if fmt.Sprintf("%v", v) != fmt.Sprintf("%v", value) { + t.Fatalf("get key %q value, want %v got %v .", k, v, value) + } + } if err = jsonconf.Set("name", "astaxie"); err != nil { t.Fatal(err) @@ -143,15 +174,7 @@ func TestJson(t *testing.T) { if jsonconf.String("name") != "astaxie" { t.Fatal("get name error") } - if jsonconf.String("database::host") != "host" { - t.Fatal("get database::host error") - } - if jsonconf.String("database::conns::connectioninfo") != "info" { - t.Fatal("get database::conns::connectioninfo error") - } - if maxconnection, err := jsonconf.Int("database::conns::maxconnection"); err != nil || maxconnection != 12 { - t.Fatal("get database::conns::maxconnection error") - } + if db, err := jsonconf.DIY("database"); err != nil { t.Fatal(err) } else if m, ok := db.(map[string]interface{}); !ok { diff --git a/config/xml/xml.go b/config/xml/xml.go index 4d48f7d2..ffb32862 100644 --- a/config/xml/xml.go +++ b/config/xml/xml.go @@ -92,7 +92,10 @@ type ConfigContainer struct { // Bool returns the boolean value for a given key. func (c *ConfigContainer) Bool(key string) (bool, error) { - return strconv.ParseBool(c.data[key].(string)) + if v, ok := c.data[key]; ok { + return config.ParseBool(v) + } + return false, fmt.Errorf("not exist key: %q", key) } // DefaultBool return the bool value if has no error diff --git a/config/yaml/yaml.go b/config/yaml/yaml.go index f034d3ba..9a96ac92 100644 --- a/config/yaml/yaml.go +++ b/config/yaml/yaml.go @@ -121,10 +121,10 @@ type ConfigContainer struct { // Bool returns the boolean value for a given key. func (c *ConfigContainer) Bool(key string) (bool, error) { - if v, ok := c.data[key].(bool); ok { - return v, nil + if v, ok := c.data[key]; ok { + return config.ParseBool(v) } - return false, errors.New("not bool value") + return false, fmt.Errorf("not exist key: %q", key) } // DefaultBool return the bool value if has no error diff --git a/context/input.go b/context/input.go index c37204bd..edfdf530 100644 --- a/context/input.go +++ b/context/input.go @@ -287,6 +287,13 @@ func (input *BeegoInput) Params() map[string]string { // SetParam will set the param with key and value func (input *BeegoInput) SetParam(key, val string) { + // check if already exists + for i, v := range input.pnames { + if v == key && i <= len(input.pvalues) { + input.pvalues[i] = val + return + } + } input.pvalues = append(input.pvalues, val) input.pnames = append(input.pnames, key) } diff --git a/context/input_test.go b/context/input_test.go index 618e1254..24f6fd99 100644 --- a/context/input_test.go +++ b/context/input_test.go @@ -18,6 +18,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "reflect" "testing" ) @@ -117,3 +118,56 @@ func TestSubDomain(t *testing.T) { t.Fatal("Subdomain parse error, got " + beegoInput.SubDomains()) } } + +func TestParams(t *testing.T) { + inp := NewInput() + + inp.SetParam("p1", "val1_ver1") + inp.SetParam("p2", "val2_ver1") + inp.SetParam("p3", "val3_ver1") + if l := inp.ParamsLen(); l != 3 { + t.Fatalf("Input.ParamsLen wrong value: %d, expected %d", l, 3) + } + + if val := inp.Param("p1"); val != "val1_ver1" { + t.Fatalf("Input.Param wrong value: %s, expected %s", val, "val1_ver1") + } + if val := inp.Param("p3"); val != "val3_ver1" { + t.Fatalf("Input.Param wrong value: %s, expected %s", val, "val3_ver1") + } + vals := inp.Params() + expected := map[string]string{ + "p1": "val1_ver1", + "p2": "val2_ver1", + "p3": "val3_ver1", + } + if !reflect.DeepEqual(vals, expected) { + t.Fatalf("Input.Params wrong value: %s, expected %s", vals, expected) + } + + // overwriting existing params + inp.SetParam("p1", "val1_ver2") + inp.SetParam("p2", "val2_ver2") + expected = map[string]string{ + "p1": "val1_ver2", + "p2": "val2_ver2", + "p3": "val3_ver1", + } + vals = inp.Params() + if !reflect.DeepEqual(vals, expected) { + t.Fatalf("Input.Params wrong value: %s, expected %s", vals, expected) + } + + if l := inp.ParamsLen(); l != 3 { + t.Fatalf("Input.ParamsLen wrong value: %d, expected %d", l, 3) + } + + if val := inp.Param("p1"); val != "val1_ver2" { + t.Fatalf("Input.Param wrong value: %s, expected %s", val, "val1_ver2") + } + + if val := inp.Param("p2"); val != "val2_ver2" { + t.Fatalf("Input.Param wrong value: %s, expected %s", val, "val1_ver2") + } + +} diff --git a/error.go b/error.go index 94151dd8..4f48fab2 100644 --- a/error.go +++ b/error.go @@ -424,6 +424,7 @@ func exception(errCode string, ctx *context.Context) { func executeError(err *errorInfo, ctx *context.Context, code int) { if err.errorType == errorTypeHandler { + ctx.ResponseWriter.WriteHeader(code) err.handler(ctx.ResponseWriter, ctx.Request) return } diff --git a/grace/server.go b/grace/server.go index f4512ded..101bda56 100644 --- a/grace/server.go +++ b/grace/server.go @@ -90,16 +90,15 @@ func (srv *Server) ListenAndServeTLS(certFile, keyFile string) (err error) { addr = ":https" } - config := &tls.Config{} - if srv.TLSConfig != nil { - *config = *srv.TLSConfig + if srv.TLSConfig == nil { + srv.TLSConfig = &tls.Config{} } - if config.NextProtos == nil { - config.NextProtos = []string{"http/1.1"} + if srv.TLSConfig.NextProtos == nil { + srv.TLSConfig.NextProtos = []string{"http/1.1"} } - config.Certificates = make([]tls.Certificate, 1) - config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) + srv.TLSConfig.Certificates = make([]tls.Certificate, 1) + srv.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return } @@ -113,7 +112,7 @@ func (srv *Server) ListenAndServeTLS(certFile, keyFile string) (err error) { } srv.tlsInnerListener = newGraceListener(l, srv) - srv.GraceListener = tls.NewListener(srv.tlsInnerListener, config) + srv.GraceListener = tls.NewListener(srv.tlsInnerListener, srv.TLSConfig) if srv.isChild { process, err := os.FindProcess(os.Getppid()) diff --git a/hooks.go b/hooks.go index 78abf8ef..59b10b32 100644 --- a/hooks.go +++ b/hooks.go @@ -68,13 +68,11 @@ func registerSession() error { } func registerTemplate() error { - if BConfig.WebConfig.AutoRender { - if err := BuildTemplate(BConfig.WebConfig.ViewsPath); err != nil { - if BConfig.RunMode == DEV { - Warn(err) - } - return err + if err := BuildTemplate(BConfig.WebConfig.ViewsPath); err != nil { + if BConfig.RunMode == DEV { + Warn(err) } + return err } return nil } diff --git a/logs/conn.go b/logs/conn.go index 3655bf51..5d78467b 100644 --- a/logs/conn.go +++ b/logs/conn.go @@ -19,6 +19,7 @@ import ( "io" "log" "net" + "time" ) // connWriter implements LoggerInterface. @@ -48,7 +49,7 @@ func (c *connWriter) Init(jsonconfig string) error { // WriteMsg write message in connection. // if connection is down, try to re-connect. -func (c *connWriter) WriteMsg(msg string, level int) error { +func (c *connWriter) WriteMsg(when time.Time, msg string, level int) error { if level > c.Level { return nil } @@ -62,6 +63,9 @@ func (c *connWriter) WriteMsg(msg string, level int) error { if c.ReconnectOnMsg { defer c.innerWriter.Close() } + + msg = formatLogTime(when) + msg + c.lg.Println(msg) return nil } @@ -94,7 +98,7 @@ func (c *connWriter) connect() error { } c.innerWriter = conn - c.lg = log.New(conn, "", log.Ldate|log.Ltime) + c.lg = log.New(conn, "", 0) return nil } diff --git a/logs/console.go b/logs/console.go index 23e8ebca..d7ed8d8e 100644 --- a/logs/console.go +++ b/logs/console.go @@ -19,6 +19,7 @@ import ( "log" "os" "runtime" + "time" ) // brush is a color join function @@ -34,46 +35,49 @@ func newBrush(color string) brush { } var colors = []brush{ - newBrush("1;37"), // Emergency white - newBrush("1;36"), // Alert cyan - newBrush("1;35"), // Critical magenta - newBrush("1;31"), // Error red - newBrush("1;33"), // Warning yellow - newBrush("1;32"), // Notice green - newBrush("1;34"), // Informational blue - newBrush("1;34"), // Debug blue + newBrush("1;37"), // Emergency white + newBrush("1;36"), // Alert cyan + newBrush("1;35"), // Critical magenta + newBrush("1;31"), // Error red + newBrush("1;33"), // Warning yellow + newBrush("1;32"), // Notice green + newBrush("1;34"), // Informational blue + newBrush("1;34"), // Debug blue } // consoleWriter implements LoggerInterface and writes messages to terminal. type consoleWriter struct { lg *log.Logger - Level int `json:"level"` + Level int `json:"level"` + Color bool `json:"color"` } // NewConsole create ConsoleWriter returning as LoggerInterface. func NewConsole() Logger { cw := &consoleWriter{ - lg: log.New(os.Stdout, "", log.Ldate|log.Ltime), + lg: log.New(os.Stdout, "", 0), Level: LevelDebug, + Color: true, } return cw } // Init init console logger. -// jsonconfig like '{"level":LevelTrace}'. -func (c *consoleWriter) Init(jsonconfig string) error { - if len(jsonconfig) == 0 { +// jsonConfig like '{"level":LevelTrace}'. +func (c *consoleWriter) Init(jsonConfig string) error { + if len(jsonConfig) == 0 { return nil } - return json.Unmarshal([]byte(jsonconfig), c) + return json.Unmarshal([]byte(jsonConfig), c) } // WriteMsg write message in console. -func (c *consoleWriter) WriteMsg(msg string, level int) error { +func (c *consoleWriter) WriteMsg(when time.Time, msg string, level int) error { if level > c.Level { return nil } - if goos := runtime.GOOS; goos == "windows" { + msg = formatLogTime(when) + msg + if runtime.GOOS == "windows" || !c.Color { c.lg.Println(msg) return nil } diff --git a/logs/console_test.go b/logs/console_test.go index ce8937d4..04f2bd7e 100644 --- a/logs/console_test.go +++ b/logs/console_test.go @@ -42,3 +42,10 @@ func TestConsole(t *testing.T) { log2.SetLogger("console", `{"level":3}`) testConsoleCalls(log2) } + +// Test console without color +func TestConsoleNoColor(t *testing.T) { + log := NewLogger(100) + log.SetLogger("console", `{"color":false}`) + testConsoleCalls(log) +} diff --git a/logs/es/es.go b/logs/es/es.go index f8dc5f65..397ca2ef 100644 --- a/logs/es/es.go +++ b/logs/es/es.go @@ -48,16 +48,16 @@ func (el *esLogger) Init(jsonconfig string) error { } // WriteMsg will write the msg and level into es -func (el *esLogger) WriteMsg(msg string, level int) error { +func (el *esLogger) WriteMsg(when time.Time, msg string, level int) error { if level > el.Level { return nil } - t := time.Now() + vals := make(map[string]interface{}) - vals["@timestamp"] = t.Format(time.RFC3339) + vals["@timestamp"] = when.Format(time.RFC3339) vals["@msg"] = msg d := goes.Document{ - Index: fmt.Sprintf("%04d.%02d.%02d", t.Year(), t.Month(), t.Day()), + Index: fmt.Sprintf("%04d.%02d.%02d", when.Year(), when.Month(), when.Day()), Type: "logs", Fields: vals, } diff --git a/logs/file.go b/logs/file.go index 0eae734a..3a042164 100644 --- a/logs/file.go +++ b/logs/file.go @@ -114,59 +114,20 @@ func (w *fileLogWriter) needRotate(size int, day int) bool { } // WriteMsg write logger message into file. -func (w *fileLogWriter) WriteMsg(msg string, level int) error { +func (w *fileLogWriter) WriteMsg(when time.Time, msg string, level int) error { if level > w.Level { return nil } - //2016/01/12 21:34:33 - now := time.Now() - y, mo, d := now.Date() - h, mi, s := now.Clock() - //len(2006/01/02 15:03:04)==19 - var buf [20]byte - t := 3 - for y >= 10 { - p := y / 10 - buf[t] = byte('0' + y - p*10) - y = p - t-- - } - buf[0] = byte('0' + y) - buf[4] = '/' - if mo > 9 { - buf[5] = '1' - buf[6] = byte('0' + mo - 9) - } else { - buf[5] = '0' - buf[6] = byte('0' + mo) - } - buf[7] = '/' - t = d / 10 - buf[8] = byte('0' + t) - buf[9] = byte('0' + d - t*10) - buf[10] = ' ' - t = h / 10 - buf[11] = byte('0' + t) - buf[12] = byte('0' + h - t*10) - buf[13] = ':' - t = mi / 10 - buf[14] = byte('0' + t) - buf[15] = byte('0' + mi - t*10) - buf[16] = ':' - t = s / 10 - buf[17] = byte('0' + t) - buf[18] = byte('0' + s - t*10) - buf[19] = ' ' - msg = string(buf[0:]) + msg + "\n" + msg = formatLogTime(when) + msg + "\n" if w.Rotate { + d := when.Day() if w.needRotate(len(msg), d) { w.Lock() if w.needRotate(len(msg), d) { - if err := w.doRotate(); err != nil { + if err := w.doRotate(when); err != nil { fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) } - } w.Unlock() } @@ -236,7 +197,7 @@ func (w *fileLogWriter) lines() (int, error) { // DoRotate means it need to write file in new file. // new file name like xx.2013-01-01.2.log -func (w *fileLogWriter) doRotate() error { +func (w *fileLogWriter) doRotate(logTime time.Time) error { _, err := os.Lstat(w.Filename) if err != nil { return err @@ -251,7 +212,7 @@ func (w *fileLogWriter) doRotate() error { suffix = ".log" } for ; err == nil && num <= 999; num++ { - fName = filenameOnly + fmt.Sprintf(".%s.%03d%s", time.Now().Format("2006-01-02"), num, suffix) + fName = filenameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, suffix) _, err = os.Lstat(fName) } // return error if the last file checked still existed diff --git a/logs/log.go b/logs/log.go index ccaaa3ad..2a12ed79 100644 --- a/logs/log.go +++ b/logs/log.go @@ -40,6 +40,7 @@ import ( "runtime" "strconv" "sync" + "time" ) // RFC5424 log message levels. @@ -68,7 +69,7 @@ type loggerType func() Logger // Logger defines the behavior of a log provider. type Logger interface { Init(config string) error - WriteMsg(msg string, level int) error + WriteMsg(when time.Time, msg string, level int) error Destroy() Flush() } @@ -108,6 +109,7 @@ type nameLogger struct { type logMsg struct { level int msg string + when time.Time } var logMsgPool *sync.Pool @@ -173,9 +175,9 @@ func (bl *BeeLogger) DelLogger(adapterName string) error { return nil } -func (bl *BeeLogger) writeToLoggers(msg string, level int) { +func (bl *BeeLogger) writeToLoggers(when time.Time, msg string, level int) { for _, l := range bl.outputs { - err := l.WriteMsg(msg, level) + err := l.WriteMsg(when, msg, level) if err != nil { fmt.Fprintf(os.Stderr, "unable to WriteMsg to adapter:%v,error:%v\n", l.name, err) } @@ -183,6 +185,7 @@ func (bl *BeeLogger) writeToLoggers(msg string, level int) { } func (bl *BeeLogger) writeMsg(logLevel int, msg string) error { + when := time.Now() if bl.enableFuncCallDepth { _, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth) if !ok { @@ -196,9 +199,10 @@ func (bl *BeeLogger) writeMsg(logLevel int, msg string) error { lm := logMsgPool.Get().(*logMsg) lm.level = logLevel lm.msg = msg + lm.when = when bl.msgChan <- lm } else { - bl.writeToLoggers(msg, logLevel) + bl.writeToLoggers(when, msg, logLevel) } return nil } @@ -231,7 +235,7 @@ func (bl *BeeLogger) startLogger() { for { select { case bm := <-bl.msgChan: - bl.writeToLoggers(bm.msg, bm.level) + bl.writeToLoggers(bm.when, bm.msg, bm.level) logMsgPool.Put(bm) } } @@ -351,7 +355,7 @@ func (bl *BeeLogger) Close() { for { if len(bl.msgChan) > 0 { bm := <-bl.msgChan - bl.writeToLoggers(bm.msg, bm.level) + bl.writeToLoggers(bm.when, bm.msg, bm.level) logMsgPool.Put(bm) continue } @@ -361,4 +365,47 @@ func (bl *BeeLogger) Close() { l.Flush() l.Destroy() } + bl.outputs = nil +} + +func formatLogTime(when time.Time) string { + y, mo, d := when.Date() + h, mi, s := when.Clock() + //len(2006/01/02 15:03:04)==19 + var buf [20]byte + t := 3 + for y >= 10 { + p := y / 10 + buf[t] = byte('0' + y - p*10) + y = p + t-- + } + buf[0] = byte('0' + y) + buf[4] = '/' + if mo > 9 { + buf[5] = '1' + buf[6] = byte('0' + mo - 9) + } else { + buf[5] = '0' + buf[6] = byte('0' + mo) + } + buf[7] = '/' + t = d / 10 + buf[8] = byte('0' + t) + buf[9] = byte('0' + d - t*10) + buf[10] = ' ' + t = h / 10 + buf[11] = byte('0' + t) + buf[12] = byte('0' + h - t*10) + buf[13] = ':' + t = mi / 10 + buf[14] = byte('0' + t) + buf[15] = byte('0' + mi - t*10) + buf[16] = ':' + t = s / 10 + buf[17] = byte('0' + t) + buf[18] = byte('0' + s - t*10) + buf[19] = ' ' + + return string(buf[0:]) } diff --git a/logs/smtp.go b/logs/smtp.go index 748462f9..47f5a0c6 100644 --- a/logs/smtp.go +++ b/logs/smtp.go @@ -126,7 +126,7 @@ func (s *SMTPWriter) sendMail(hostAddressWithPort string, auth smtp.Auth, fromAd // WriteMsg write message in smtp writer. // it will send an email with subject and only this message. -func (s *SMTPWriter) WriteMsg(msg string, level int) error { +func (s *SMTPWriter) WriteMsg(when time.Time, msg string, level int) error { if level > s.Level { return nil } @@ -140,7 +140,7 @@ func (s *SMTPWriter) WriteMsg(msg string, level int) error { // and send the email all in one step. contentType := "Content-Type: text/plain" + "; charset=UTF-8" mailmsg := []byte("To: " + strings.Join(s.RecipientAddresses, ";") + "\r\nFrom: " + s.FromAddress + "<" + s.FromAddress + - ">\r\nSubject: " + s.Subject + "\r\n" + contentType + "\r\n\r\n" + fmt.Sprintf(".%s", time.Now().Format("2006-01-02 15:04:05")) + msg) + ">\r\nSubject: " + s.Subject + "\r\n" + contentType + "\r\n\r\n" + fmt.Sprintf(".%s", when.Format("2006-01-02 15:04:05")) + msg) return s.sendMail(s.Host, auth, s.FromAddress, s.RecipientAddresses, mailmsg) } diff --git a/orm/types.go b/orm/types.go index 5fac5fed..41933dd1 100644 --- a/orm/types.go +++ b/orm/types.go @@ -148,6 +148,10 @@ type QuerySeter interface { // add OFFSET value // same as Limit function's args[0] Offset(offset interface{}) QuerySeter + // add GROUP BY expression + // for example: + // qs.GroupBy("id") + GroupBy(exprs ...string) QuerySeter // add ORDER expression. // "column" means ASC, "-column" means DESC. // for example: @@ -162,6 +166,12 @@ type QuerySeter interface { // qs.RelatedSel("profile").One(&user) // user.Profile.Age = 32 RelatedSel(params ...interface{}) QuerySeter + // Set Distinct + // for example: + // o.QueryTable("policy").Filter("Groups__Group__Users__User", user). + // Distinct(). + // All(&permissions) + Distinct() QuerySeter // return QuerySeter execution result number // for example: // num, err = qs.Filter("profile__age__gt", 28).Count() diff --git a/parser.go b/parser.go index b14d74b9..f23f4720 100644 --- a/parser.go +++ b/parser.go @@ -130,7 +130,7 @@ func parserComments(comments *ast.CommentGroup, funcName, controllerName, pkgpat } func genRouterCode() { - os.Mkdir("routers", 0755) + os.Mkdir(path.Join(AppPath, "routers"), 0755) Info("generate router from comments") var ( globalinfo string @@ -172,7 +172,7 @@ func genRouterCode() { } } if globalinfo != "" { - f, err := os.Create(path.Join("routers", commentFilename)) + f, err := os.Create(path.Join(AppPath, "routers", commentFilename)) if err != nil { panic(err) } @@ -182,7 +182,7 @@ func genRouterCode() { } func compareFile(pkgRealpath string) bool { - if !utils.FileExists(path.Join("routers", commentFilename)) { + if !utils.FileExists(path.Join(AppPath, "routers", commentFilename)) { return true } if utils.FileExists(lastupdateFilename) { diff --git a/router.go b/router.go index 726936de..82a602e1 100644 --- a/router.go +++ b/router.go @@ -62,12 +62,12 @@ var ( } // these beego.Controller's methods shouldn't reflect to AutoRouter exceptMethod = []string{"Init", "Prepare", "Finish", "Render", "RenderString", - "RenderBytes", "Redirect", "Abort", "StopRun", "UrlFor", "ServeJson", "ServeJsonp", - "ServeXml", "Input", "ParseForm", "GetString", "GetStrings", "GetInt", "GetBool", + "RenderBytes", "Redirect", "Abort", "StopRun", "UrlFor", "ServeJSON", "ServeJSONP", + "ServeXML", "Input", "ParseForm", "GetString", "GetStrings", "GetInt", "GetBool", "GetFloat", "GetFile", "SaveToFile", "StartSession", "SetSession", "GetSession", "DelSession", "SessionRegenerateID", "DestroySession", "IsAjax", "GetSecureCookie", "SetSecureCookie", "XsrfToken", "CheckXsrfCookie", "XsrfFormHtml", - "GetControllerAndAction"} + "GetControllerAndAction", "ServeFormatted"} urlPlaceholder = "{{placeholder}}" // DefaultAccessLogFilter will skip the accesslog if return true diff --git a/staticfile_test.go b/staticfile_test.go index d3333570..e7003366 100644 --- a/staticfile_test.go +++ b/staticfile_test.go @@ -8,9 +8,11 @@ import ( "io/ioutil" "os" "testing" + "path/filepath" ) -const licenseFile = "./LICENSE" +var currentWorkDir, _ = os.Getwd() +var licenseFile = filepath.Join(currentWorkDir, "LICENSE") func testOpenFile(encoding string, content []byte, t *testing.T) { fi, _ := os.Stat(licenseFile) diff --git a/template.go b/template.go index 363c6754..7a38630e 100644 --- a/template.go +++ b/template.go @@ -62,7 +62,7 @@ func init() { beegoTplFuncMap["lt"] = lt // < beegoTplFuncMap["ne"] = ne // != - beegoTplFuncMap["urlfor"] = URLFor // != + beegoTplFuncMap["urlfor"] = URLFor // build a URL to match a Controller and it's method } // AddFuncMap let user to register a func in the template. @@ -272,7 +272,9 @@ func SetStaticPath(url string, path string) *App { if !strings.HasPrefix(url, "/") { url = "/" + url } - url = strings.TrimRight(url, "/") + if url != "/" { + url = strings.TrimRight(url, "/") + } BConfig.WebConfig.StaticDir[url] = path return BeeApp } @@ -282,7 +284,9 @@ func DelStaticPath(url string) *App { if !strings.HasPrefix(url, "/") { url = "/" + url } - url = strings.TrimRight(url, "/") + if url != "/" { + url = strings.TrimRight(url, "/") + } delete(BConfig.WebConfig.StaticDir, url) return BeeApp } diff --git a/tree.go b/tree.go index a6ffe062..761ae0ce 100644 --- a/tree.go +++ b/tree.go @@ -141,7 +141,7 @@ func (t *Tree) addtree(segments []string, tree *Tree, wildcards []string, reg st regexpStr = "([^.]+).(.+)" params = params[1:] } else { - for range params { + for _ = range params { regexpStr = "([^/]+)/" + regexpStr } } @@ -254,7 +254,7 @@ func (t *Tree) addseg(segments []string, route interface{}, wildcards []string, regexpStr = "/([^.]+).(.+)" params = params[1:] } else { - for range params { + for _ = range params { regexpStr = "/([^/]+)" + regexpStr } } @@ -420,7 +420,11 @@ func (leaf *leafInfo) match(wildcardValues []string, ctx *context.Context) (ok b if len(strs) == 2 { ctx.Input.SetParam(":ext", strs[1]) } - ctx.Input.SetParam(":path", path.Join(path.Join(wildcardValues[index:len(wildcardValues)-1]...), strs[0])) + if index > (len(wildcardValues) - 1) { + ctx.Input.SetParam(":path", "") + } else { + ctx.Input.SetParam(":path", path.Join(path.Join(wildcardValues[index:len(wildcardValues)-1]...), strs[0])) + } return true } // match :id @@ -438,7 +442,9 @@ func (leaf *leafInfo) match(wildcardValues []string, ctx *context.Context) (ok b } matches := leaf.regexps.FindStringSubmatch(path.Join(wildcardValues...)) for i, match := range matches[1:] { - ctx.Input.SetParam(leaf.wildcards[i], match) + if i < len(leaf.wildcards) { + ctx.Input.SetParam(leaf.wildcards[i], match) + } } return true } @@ -448,17 +454,11 @@ func (leaf *leafInfo) match(wildcardValues []string, ctx *context.Context) (ok b // "/admin/" -> ["admin"] // "/admin/users" -> ["admin", "users"] func splitPath(key string) []string { + key = strings.Trim(key, "/ ") if key == "" { return []string{} } - elements := strings.Split(key, "/") - if elements[0] == "" { - elements = elements[1:] - } - if elements[len(elements)-1] == "" { - elements = elements[:len(elements)-1] - } - return elements + return strings.Split(key, "/") } // "admin" -> false, nil, "" @@ -542,13 +542,19 @@ func splitSegment(key string) (bool, []string, string) { continue } } - if v == ':' { + // Escape Sequence '\' + if i > 0 && key[i-1] == '\\' { + out = append(out, v) + } else if v == ':' { param = make([]rune, 0) start = true } else if v == '(' { startexp = true start = false - params = append(params, ":"+string(param)) + if len(param) > 0 { + params = append(params, ":"+string(param)) + param = make([]rune, 0) + } paramsNum++ expt = make([]rune, 0) expt = append(expt, '(') diff --git a/tree_test.go b/tree_test.go index 15ab7ce4..9f21c18c 100644 --- a/tree_test.go +++ b/tree_test.go @@ -15,6 +15,7 @@ package beego import ( + "strings" "testing" "github.com/astaxie/beego/context" @@ -57,6 +58,9 @@ func init() { "/dl/48/48/05ac66d9bda00a3acf948c43e306fc9a.jpg", map[string]string{":width": "48", ":height": "48", ":ext": "jpg", ":path": "05ac66d9bda00a3acf948c43e306fc9a"}}) routers = append(routers, testinfo{"/v1/shop/:id:int", "/v1/shop/123", map[string]string{":id": "123"}}) + routers = append(routers, testinfo{"/v1/shop/:id\\((a|b|c)\\)", "/v1/shop/123(a)", map[string]string{":id": "123"}}) + routers = append(routers, testinfo{"/v1/shop/:id\\((a|b|c)\\)", "/v1/shop/123(b)", map[string]string{":id": "123"}}) + routers = append(routers, testinfo{"/v1/shop/:id\\((a|b|c)\\)", "/v1/shop/123(c)", map[string]string{":id": "123"}}) routers = append(routers, testinfo{"/:year:int/:month:int/:id/:endid", "/1111/111/aaa/aaa", map[string]string{":year": "1111", ":month": "111", ":id": "aaa", ":endid": "aaa"}}) routers = append(routers, testinfo{"/v1/shop/:id/:name", "/v1/shop/123/nike", map[string]string{":id": "123", ":name": "nike"}}) routers = append(routers, testinfo{"/v1/shop/:id/account", "/v1/shop/123/account", map[string]string{":id": "123"}}) @@ -245,48 +249,31 @@ func TestSplitPath(t *testing.T) { } func TestSplitSegment(t *testing.T) { - b, w, r := splitSegment("admin") - if b || len(w) != 0 || r != "" { - t.Fatal("admin should return false, nil, ''") + + items := map[string]struct { + isReg bool + params []string + regStr string + }{ + "admin": {false, nil, ""}, + "*": {true, []string{":splat"}, ""}, + "*.*": {true, []string{".", ":path", ":ext"}, ""}, + ":id": {true, []string{":id"}, ""}, + "?:id": {true, []string{":", ":id"}, ""}, + ":id:int": {true, []string{":id"}, "([0-9]+)"}, + ":name:string": {true, []string{":name"}, `([\w]+)`}, + ":id([0-9]+)": {true, []string{":id"}, `([0-9]+)`}, + ":id([0-9]+)_:name": {true, []string{":id", ":name"}, `([0-9]+)_(.+)`}, + ":id(.+)_cms.html": {true, []string{":id"}, `(.+)_cms.html`}, + "cms_:id(.+)_:page(.+).html": {true, []string{":id", ":page"}, `cms_(.+)_(.+).html`}, + `:app(a|b|c)`: {true, []string{":app"}, `(a|b|c)`}, + `:app\((a|b|c)\)`: {true, []string{":app"}, `(.+)\((a|b|c)\)`}, } - b, w, r = splitSegment("*") - if !b || len(w) != 1 || w[0] != ":splat" || r != "" { - t.Fatal("* should return true, [:splat], ''") - } - b, w, r = splitSegment("*.*") - if !b || len(w) != 3 || w[1] != ":path" || w[2] != ":ext" || w[0] != "." || r != "" { - t.Fatal("admin should return true,[. :path :ext], ''") - } - b, w, r = splitSegment(":id") - if !b || len(w) != 1 || w[0] != ":id" || r != "" { - t.Fatal(":id should return true, [:id], ''") - } - b, w, r = splitSegment("?:id") - if !b || len(w) != 2 || w[0] != ":" || w[1] != ":id" || r != "" { - t.Fatal("?:id should return true, [: :id], ''") - } - b, w, r = splitSegment(":id:int") - if !b || len(w) != 1 || w[0] != ":id" || r != "([0-9]+)" { - t.Fatal(":id:int should return true, [:id], '([0-9]+)'") - } - b, w, r = splitSegment(":name:string") - if !b || len(w) != 1 || w[0] != ":name" || r != `([\w]+)` { - t.Fatal(`:name:string should return true, [:name], '([\w]+)'`) - } - b, w, r = splitSegment(":id([0-9]+)") - if !b || len(w) != 1 || w[0] != ":id" || r != `([0-9]+)` { - t.Fatal(`:id([0-9]+) should return true, [:id], '([0-9]+)'`) - } - b, w, r = splitSegment(":id([0-9]+)_:name") - if !b || len(w) != 2 || w[0] != ":id" || w[1] != ":name" || r != `([0-9]+)_(.+)` { - t.Fatal(`:id([0-9]+)_:name should return true, [:id :name], '([0-9]+)_(.+)'`) - } - b, w, r = splitSegment(":id(.+)_cms.html") - if !b || len(w) != 1 || w[0] != ":id" || r != `(.+)_cms.html` { - t.Fatal(":id_cms.html should return true, [:id], '(.+)_cms.html'") - } - b, w, r = splitSegment("cms_:id(.+)_:page(.+).html") - if !b || len(w) != 2 || w[0] != ":id" || w[1] != ":page" || r != `cms_(.+)_(.+).html` { - t.Fatal(":id_cms.html should return true, [:id :page], cms_(.+)_(.+).html") + + for pattern, v := range items { + b, w, r := splitSegment(pattern) + if b != v.isReg || r != v.regStr || strings.Join(w, ",") != strings.Join(v.params, ",") { + t.Fatalf("%s should return %t,%s,%q, got %t,%s,%q", pattern, v.isReg, v.params, v.regStr, b, w, r) + } } } diff --git a/utils/mail.go b/utils/mail.go index 1d80a039..10555a0a 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -31,10 +31,13 @@ import ( "path/filepath" "strconv" "strings" + "sync" ) const ( maxLineLength = 76 + + upperhex = "0123456789ABCDEF" ) // Email is the type used for email messages @@ -74,9 +77,6 @@ func NewEMail(config string) *Email { if err != nil { return nil } - if e.From == "" { - e.From = e.Username - } return e } @@ -228,14 +228,21 @@ func (e *Email) Send() error { to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc)) to = append(append(append(to, e.To...), e.Cc...), e.Bcc...) // Check to make sure there is at least one recipient and one "From" address - if e.From == "" || len(to) == 0 { - return errors.New("Must specify at least one From address and one To address") + if len(to) == 0 { + return errors.New("Must specify at least one To address") } - from, err := mail.ParseAddress(e.From) + + from, err := mail.ParseAddress(e.Username) if err != nil { return err } - e.From = from.String() + + if len(e.From) == 0 { + e.From = e.Username + } + // use mail's RFC 2047 to encode any string + e.Subject = qEncode("utf-8", e.Subject) + raw, err := e.Bytes() if err != nil { return err @@ -342,3 +349,73 @@ func base64Wrap(w io.Writer, b []byte) { w.Write(out) } } + +// Encode returns the encoded-word form of s. If s is ASCII without special +// characters, it is returned unchanged. The provided charset is the IANA +// charset name of s. It is case insensitive. +// RFC 2047 encoded-word +func qEncode(charset, s string) string { + if !needsEncoding(s) { + return s + } + return encodeWord(charset, s) +} + +func needsEncoding(s string) bool { + for _, b := range s { + if (b < ' ' || b > '~') && b != '\t' { + return true + } + } + return false +} + +// encodeWord encodes a string into an encoded-word. +func encodeWord(charset, s string) string { + buf := getBuffer() + + buf.WriteString("=?") + buf.WriteString(charset) + buf.WriteByte('?') + buf.WriteByte('q') + buf.WriteByte('?') + + enc := make([]byte, 3) + for i := 0; i < len(s); i++ { + b := s[i] + switch { + case b == ' ': + buf.WriteByte('_') + case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_': + buf.WriteByte(b) + default: + enc[0] = '=' + enc[1] = upperhex[b>>4] + enc[2] = upperhex[b&0x0f] + buf.Write(enc) + } + } + buf.WriteString("?=") + + es := buf.String() + putBuffer(buf) + return es +} + +var bufPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +func getBuffer() *bytes.Buffer { + return bufPool.Get().(*bytes.Buffer) +} + +func putBuffer(buf *bytes.Buffer) { + if buf.Len() > 1024 { + return + } + buf.Reset() + bufPool.Put(buf) +}