diff --git a/README b/README.md similarity index 100% rename from README rename to README.md diff --git a/beego.go b/beego.go new file mode 100644 index 00000000..6bb603d8 --- /dev/null +++ b/beego.go @@ -0,0 +1,112 @@ +package beego + +import ( + "fmt" + "net/http" + "os" + "path" +) + +var ( + BeeApp *App + AppName string + AppPath string + StaticDir map[string]string + HttpAddr string + HttpPort int + RecoverPanic bool + AutoRender bool + ViewsPath string + RunMode string //"dev" or "prod" + AppConfig *Config +) + +func init() { + BeeApp = NewApp() + AppPath, _ = os.Getwd() + StaticDir = make(map[string]string) + var err error + AppConfig, err = LoadConfig(path.Join(AppPath, "conf", "app.conf")) + if err != nil { + Trace("open Config err:", err) + HttpAddr = "" + HttpPort = 8080 + AppName = "beego" + RunMode = "prod" + AutoRender = true + RecoverPanic = true + ViewsPath = "views" + } else { + HttpAddr = AppConfig.String("httpaddr") + if v, err := AppConfig.Int("httpport"); err != nil { + HttpPort = 8080 + } else { + HttpPort = v + } + AppName = AppConfig.String("appname") + if runmode := AppConfig.String("runmode"); runmode != "" { + RunMode = runmode + } else { + RunMode = "prod" + } + if ar, err := AppConfig.Bool("autorender"); err != nil { + AutoRender = true + } else { + AutoRender = ar + } + if ar, err := AppConfig.Bool("autorecover"); err != nil { + RecoverPanic = true + } else { + RecoverPanic = ar + } + if views := AppConfig.String("viewspath"); views == "" { + ViewsPath = "views" + } else { + ViewsPath = views + } + } + StaticDir["/static"] = "static" + +} + +type App struct { + Handlers *ControllerRegistor +} + +// New returns a new PatternServeMux. +func NewApp() *App { + cr := NewControllerRegistor() + app := &App{Handlers: cr} + return app +} + +func (app *App) Run() { + addr := fmt.Sprintf("%s:%d", HttpAddr, HttpPort) + err := http.ListenAndServe(addr, app.Handlers) + if err != nil { + BeeLogger.Fatal("ListenAndServe: ", err) + } +} + +func (app *App) RegisterController(path string, c ControllerInterface) *App { + app.Handlers.Add(path, c) + return app +} + +func (app *App) SetViewsPath(path string) *App { + ViewsPath = path + return app +} + +func (app *App) SetStaticPath(url string, path string) *App { + StaticDir[url] = path + return app +} + +func (app *App) ErrorLog(ctx *Context) { + BeeLogger.Printf("[ERR] host: '%s', request: '%s %s', proto: '%s', ua: '%s', remote: '%s'\n", ctx.Request.Host, ctx.Request.Method, ctx.Request.URL.Path, ctx.Request.Proto, ctx.Request.UserAgent(), ctx.Request.RemoteAddr) +} + +func (app *App) AccessLog(ctx *Context) { + BeeLogger.Printf("[ACC] host: '%s', request: '%s %s', proto: '%s', ua: %s'', remote: '%s'\n", ctx.Request.Host, ctx.Request.Method, ctx.Request.URL.Path, ctx.Request.Proto, ctx.Request.UserAgent(), ctx.Request.RemoteAddr) +} diff --git a/config.go b/config.go new file mode 100644 index 00000000..1c48e0a8 --- /dev/null +++ b/config.go @@ -0,0 +1,121 @@ +package beego + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" + "strconv" + "strings" + "sync" + "unicode" +) + +var ( + bComment = []byte{'#'} + bEmpty = []byte{} + bEqual = []byte{'='} + bDQuote = []byte{'"'} +) + +// A Config represents the configuration. +type Config struct { + filename string + comment map[int][]string // id: []{comment, key...}; id 1 is for main comment. + data map[string]string // key: value + offset map[string]int64 // key: offset; for editing. + sync.RWMutex +} + +// ParseFile creates a new Config and parses the file configuration from the +// named file. +func LoadConfig(name string) (*Config, error) { + file, err := os.Open(name) + if err != nil { + return nil, err + } + + cfg := &Config{ + file.Name(), + make(map[int][]string), + make(map[string]string), + make(map[string]int64), + sync.RWMutex{}, + } + cfg.Lock() + defer cfg.Unlock() + defer file.Close() + + var comment bytes.Buffer + buf := bufio.NewReader(file) + + for nComment, off := 0, int64(1); ; { + line, _, err := buf.ReadLine() + if err == io.EOF { + break + } + if bytes.Equal(line, bEmpty) { + continue + } + + off += int64(len(line)) + + if bytes.HasPrefix(line, bComment) { + line = bytes.TrimLeft(line, "#") + line = bytes.TrimLeftFunc(line, unicode.IsSpace) + comment.Write(line) + comment.WriteByte('\n') + continue + } + if comment.Len() != 0 { + cfg.comment[nComment] = []string{comment.String()} + comment.Reset() + nComment++ + } + + val := bytes.SplitN(line, bEqual, 2) + if bytes.HasPrefix(val[1], bDQuote) { + val[1] = bytes.Trim(val[1], `"`) + } + + key := strings.TrimSpace(string(val[0])) + cfg.comment[nComment-1] = append(cfg.comment[nComment-1], key) + cfg.data[key] = strings.TrimSpace(string(val[1])) + cfg.offset[key] = off + } + return cfg, nil +} + +// Bool returns the boolean value for a given key. +func (c *Config) Bool(key string) (bool, error) { + return strconv.ParseBool(c.data[key]) +} + +// Int returns the integer value for a given key. +func (c *Config) Int(key string) (int, error) { + return strconv.Atoi(c.data[key]) +} + +// Float returns the float value for a given key. +func (c *Config) Float(key string) (float64, error) { + return strconv.ParseFloat(c.data[key], 64) +} + +// String returns the string value for a given key. +func (c *Config) String(key string) string { + return c.data[key] +} + +// WriteValue writes a new value for key. +func (c *Config) SetValue(key, value string) error { + c.Lock() + defer c.Unlock() + + if _, found := c.data[key]; !found { + return errors.New("key not found: " + key) + } + + c.data[key] = value + return nil +} diff --git a/context.go b/context.go new file mode 100644 index 00000000..7a386c99 --- /dev/null +++ b/context.go @@ -0,0 +1,72 @@ +package beego + +import ( + "fmt" + "mime" + "net/http" + "strings" + "time" +) + +type Context struct { + ResponseWriter http.ResponseWriter + Request *http.Request + Params map[string]string +} + +func (ctx *Context) WriteString(content string) { + ctx.ResponseWriter.Write([]byte(content)) +} + +func (ctx *Context) Abort(status int, body string) { + ctx.ResponseWriter.WriteHeader(status) + ctx.ResponseWriter.Write([]byte(body)) +} + +func (ctx *Context) Redirect(status int, url_ string) { + ctx.ResponseWriter.Header().Set("Location", url_) + ctx.ResponseWriter.WriteHeader(status) + ctx.ResponseWriter.Write([]byte("Redirecting to: " + url_)) +} + +func (ctx *Context) NotModified() { + ctx.ResponseWriter.WriteHeader(304) +} + +func (ctx *Context) NotFound(message string) { + ctx.ResponseWriter.WriteHeader(404) + ctx.ResponseWriter.Write([]byte(message)) +} + +//Sets the content type by extension, as defined in the mime package. +//For example, ctx.ContentType("json") sets the content-type to "application/json" +func (ctx *Context) ContentType(ext string) { + if !strings.HasPrefix(ext, ".") { + ext = "." + ext + } + ctype := mime.TypeByExtension(ext) + if ctype != "" { + ctx.ResponseWriter.Header().Set("Content-Type", ctype) + } +} + +func (ctx *Context) SetHeader(hdr string, val string, unique bool) { + if unique { + ctx.ResponseWriter.Header().Set(hdr, val) + } else { + ctx.ResponseWriter.Header().Add(hdr, val) + } +} + +//Sets a cookie -- duration is the amount of time in seconds. 0 = forever +func (ctx *Context) SetCookie(name string, value string, age int64) { + var utctime time.Time + if age == 0 { + // 2^31 - 1 seconds (roughly 2038) + utctime = time.Unix(2147483647, 0) + } else { + utctime = time.Unix(time.Now().Unix()+age, 0) + } + cookie := fmt.Sprintf("%s=%s; expires=%s", name, value, webTime(utctime)) + ctx.SetHeader("Set-Cookie", cookie, false) +} diff --git a/controller.go b/controller.go new file mode 100644 index 00000000..fdd826a7 --- /dev/null +++ b/controller.go @@ -0,0 +1,147 @@ +package beego + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "html/template" + "io/ioutil" + "net/http" + "path" + "strconv" +) + +type Controller struct { + Ct *Context + Tpl *template.Template + Data map[interface{}]interface{} + ChildName string + TplNames string + Layout string + TplExt string +} + +type ControllerInterface interface { + Init(ct *Context, cn string) + Prepare() + Get() + Post() + Delete() + Put() + Head() + Patch() + Options() + Finish() + Render() error +} + +func (c *Controller) Init(ct *Context, cn string) { + c.Data = make(map[interface{}]interface{}) + c.Tpl = template.New(cn + ct.Request.Method) + c.Tpl = c.Tpl.Funcs(beegoTplFuncMap) + c.Layout = "" + c.TplNames = "" + c.ChildName = cn + c.Ct = ct + c.TplExt = "tpl" + +} + +func (c *Controller) Prepare() { + +} + +func (c *Controller) Finish() { + +} + +func (c *Controller) Get() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) +} + +func (c *Controller) Post() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) +} + +func (c *Controller) Delete() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) +} + +func (c *Controller) Put() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) +} + +func (c *Controller) Head() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) +} + +func (c *Controller) Patch() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) +} + +func (c *Controller) Options() { + http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) +} + +func (c *Controller) Render() error { + //if the controller has set layout, then first get the tplname's content set the content to the layout + if c.Layout != "" { + if c.TplNames == "" { + c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt + } + t, err := c.Tpl.ParseFiles(path.Join(ViewsPath, c.TplNames), path.Join(ViewsPath, c.Layout)) + if err != nil { + Trace("template ParseFiles err:", err) + } + _, file := path.Split(c.TplNames) + newbytes := bytes.NewBufferString("") + t.ExecuteTemplate(newbytes, file, c.Data) + tplcontent, _ := ioutil.ReadAll(newbytes) + c.Data["LayoutContent"] = template.HTML(string(tplcontent)) + _, file = path.Split(c.Layout) + err = t.ExecuteTemplate(c.Ct.ResponseWriter, file, c.Data) + if err != nil { + Trace("template Execute err:", err) + } + } else { + if c.TplNames == "" { + c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt + } + t, err := c.Tpl.ParseFiles(path.Join(ViewsPath, c.TplNames)) + if err != nil { + Trace("template ParseFiles err:", err) + } + _, file := path.Split(c.TplNames) + err = t.ExecuteTemplate(c.Ct.ResponseWriter, file, c.Data) + if err != nil { + Trace("template Execute err:", err) + } + } + return nil +} + +func (c *Controller) Redirect(url string, code int) { + c.Ct.Redirect(code, url) +} + +func (c *Controller) ServeJson() { + content, err := json.MarshalIndent(c.Data, "", " ") + if err != nil { + http.Error(c.Ct.ResponseWriter, err.Error(), http.StatusInternalServerError) + return + } + c.Ct.SetHeader("Content-Length", strconv.Itoa(len(content)), true) + c.Ct.ContentType("json") + c.Ct.ResponseWriter.Write(content) +} + +func (c *Controller) ServeXml() { + content, err := xml.Marshal(c.Data) + if err != nil { + http.Error(c.Ct.ResponseWriter, err.Error(), http.StatusInternalServerError) + return + } + c.Ct.SetHeader("Content-Length", strconv.Itoa(len(content)), true) + c.Ct.ContentType("xml") + c.Ct.ResponseWriter.Write(content) +} diff --git a/log.go b/log.go new file mode 100644 index 00000000..101cb4d3 --- /dev/null +++ b/log.go @@ -0,0 +1,85 @@ +package beego + +import ( + "log" + "os" +) + +//-------------------- +// LOG LEVEL +//-------------------- + +// Log levels to control the logging output. +const ( + LevelTrace = iota + LevelDebug + LevelInfo + LevelWarning + LevelError + LevelCritical +) + +// logLevel controls the global log level used by the logger. +var level = LevelTrace + +// LogLevel returns the global log level and can be used in +// own implementations of the logger interface. +func Level() int { + return level +} + +// SetLogLevel sets the global log level used by the simple +// logger. +func SetLevel(l int) { + level = l +} + +// logger references the used application logger. +var BeeLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime) + +// SetLogger sets a new logger. +func SetLogger(l *log.Logger) { + BeeLogger = l +} + +// Trace logs a message at trace level. +func Trace(v ...interface{}) { + if level <= LevelTrace { + BeeLogger.Printf("[T] %v\n", v) + } +} + +// Debug logs a message at debug level. +func Debug(v ...interface{}) { + if level <= LevelDebug { + BeeLogger.Printf("[D] %v\n", v) + } +} + +// Info logs a message at info level. +func Info(v ...interface{}) { + if level <= LevelInfo { + BeeLogger.Printf("[I] %v\n", v) + } +} + +// Warning logs a message at warning level. +func Warn(v ...interface{}) { + if level <= LevelWarning { + BeeLogger.Printf("[W] %v\n", v) + } +} + +// Error logs a message at error level. +func Error(v ...interface{}) { + if level <= LevelError { + BeeLogger.Printf("[E] %v\n", v) + } +} + +// Critical logs a message at critical level. +func Critical(v ...interface{}) { + if level <= LevelCritical { + BeeLogger.Printf("[C] %v\n", v) + } +} diff --git a/model.go b/model.go new file mode 100644 index 00000000..66bad313 --- /dev/null +++ b/model.go @@ -0,0 +1 @@ +package beego \ No newline at end of file diff --git a/router.go b/router.go new file mode 100644 index 00000000..0b00b13d --- /dev/null +++ b/router.go @@ -0,0 +1,283 @@ +package beego + +import ( + "net/http" + "net/url" + "reflect" + "regexp" + "runtime" + "strings" +) + +type controllerInfo struct { + pattern string + regex *regexp.Regexp + params map[int]string + controllerType reflect.Type +} + +type ControllerRegistor struct { + routers []*controllerInfo + fixrouters []*controllerInfo + filters []http.HandlerFunc +} + +func NewControllerRegistor() *ControllerRegistor { + return &ControllerRegistor{routers: make([]*controllerInfo, 0)} +} + +func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) { + parts := strings.Split(pattern, "/") + + j := 0 + params := make(map[int]string) + for i, part := range parts { + if strings.HasPrefix(part, ":") { + expr := "([^/]+)" + //a user may choose to override the defult expression + // similar to expressjs: ‘/user/:id([0-9]+)’ + if index := strings.Index(part, "("); index != -1 { + expr = part[index:] + part = part[:index] + } + params[j] = part + parts[i] = expr + j++ + } + } + if j == 0 { + //now create the Route + t := reflect.Indirect(reflect.ValueOf(c)).Type() + route := &controllerInfo{} + route.pattern = pattern + route.controllerType = t + + p.fixrouters = append(p.fixrouters, route) + } else { // add regexp routers + //recreate the url pattern, with parameters replaced + //by regular expressions. then compile the regex + pattern = strings.Join(parts, "/") + regex, regexErr := regexp.Compile(pattern) + if regexErr != nil { + //TODO add error handling here to avoid panic + panic(regexErr) + return + } + + //now create the Route + t := reflect.Indirect(reflect.ValueOf(c)).Type() + route := &controllerInfo{} + route.regex = regex + route.params = params + route.controllerType = t + + p.routers = append(p.routers, route) + } + +} + +// Filter adds the middleware filter. +func (p *ControllerRegistor) Filter(filter http.HandlerFunc) { + p.filters = append(p.filters, filter) +} + +// FilterParam adds the middleware filter if the REST URL parameter exists. +func (p *ControllerRegistor) FilterParam(param string, filter http.HandlerFunc) { + if !strings.HasPrefix(param, ":") { + param = ":" + param + } + + p.Filter(func(w http.ResponseWriter, r *http.Request) { + p := r.URL.Query().Get(param) + if len(p) > 0 { + filter(w, r) + } + }) +} + +// FilterPrefixPath adds the middleware filter if the prefix path exists. +func (p *ControllerRegistor) FilterPrefixPath(path string, filter http.HandlerFunc) { + p.Filter(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, path) { + filter(w, r) + } + }) +} + +// AutoRoute +func (p *ControllerRegistor) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + if !RecoverPanic { + // go back to panic + panic(err) + } else { + Critical("Handler crashed with error", err) + for i := 1; ; i += 1 { + _, file, line, ok := runtime.Caller(i) + if !ok { + break + } + Critical(file, line) + } + } + } + }() + w := &responseWriter{writer: rw} + + var runrouter *controllerInfo + var findrouter bool + + params := make(map[string]string) + + //static file server + for prefix, staticDir := range StaticDir { + if strings.HasPrefix(r.URL.Path, prefix) { + file := staticDir + r.URL.Path[len(prefix):] + http.ServeFile(w, r, file) + w.started = true + return + } + } + + requestPath := r.URL.Path + + //first find path from the fixrouters to Improve Performance + for _, route := range p.fixrouters { + n := len(requestPath) + if (requestPath[n-1] != '/' && route.pattern == requestPath) || + (len(route.pattern) >= n && requestPath[0:n] == route.pattern) { + runrouter = route + findrouter = true + break + } + } + + if !findrouter { + //find a matching Route + for _, route := range p.routers { + + //check if Route pattern matches url + if !route.regex.MatchString(requestPath) { + continue + } + + //get submatches (params) + matches := route.regex.FindStringSubmatch(requestPath) + + //double check that the Route matches the URL pattern. + if len(matches[0]) != len(requestPath) { + continue + } + + if len(route.params) > 0 { + //add url parameters to the query param map + values := r.URL.Query() + for i, match := range matches[1:] { + values.Add(route.params[i], match) + params[route.params[i]] = match + } + //reassemble query params and add to RawQuery + r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery + //r.URL.RawQuery = url.Values(values).Encode() + } + runrouter = route + findrouter = true + break + } + } + + if runrouter != nil { + //execute middleware filters + for _, filter := range p.filters { + filter(w, r) + if w.started { + return + } + } + + //Invoke the request handler + vc := reflect.New(runrouter.controllerType) + + //call the controller init function + init := vc.MethodByName("Init") + in := make([]reflect.Value, 2) + ct := &Context{ResponseWriter: w, Request: r, Params: params} + in[0] = reflect.ValueOf(ct) + in[1] = reflect.ValueOf(runrouter.controllerType.Name()) + init.Call(in) + //call prepare function + in = make([]reflect.Value, 0) + method := vc.MethodByName("Prepare") + method.Call(in) + + //if response has written,yes don't run next + if !w.started { + if r.Method == "GET" { + method = vc.MethodByName("Get") + method.Call(in) + } else if r.Method == "POST" { + method = vc.MethodByName("Post") + method.Call(in) + } else if r.Method == "HEAD" { + method = vc.MethodByName("Head") + method.Call(in) + } else if r.Method == "DELETE" { + method = vc.MethodByName("Delete") + method.Call(in) + } else if r.Method == "PUT" { + method = vc.MethodByName("Put") + method.Call(in) + } else if r.Method == "PATCH" { + method = vc.MethodByName("Patch") + method.Call(in) + } else if r.Method == "OPTIONS" { + method = vc.MethodByName("Options") + method.Call(in) + } + if !w.started { + if AutoRender { + method = vc.MethodByName("Render") + method.Call(in) + } + if !w.started { + method = vc.MethodByName("Finish") + method.Call(in) + } + } + } + } + + //if no matches to url, throw a not found exception + if w.started == false { + http.NotFound(w, r) + } +} + +//responseWriter is a wrapper for the http.ResponseWriter +//started set to true if response was written to then don't execute other handler +type responseWriter struct { + writer http.ResponseWriter + started bool + status int +} + +// Header returns the header map that will be sent by WriteHeader. +func (w *responseWriter) Header() http.Header { + return w.writer.Header() +} + +// Write writes the data to the connection as part of an HTTP reply, +// and sets `started` to true +func (w *responseWriter) Write(p []byte) (int, error) { + w.started = true + return w.writer.Write(p) +} + +// WriteHeader sends an HTTP response header with status code, +// and sets `started` to true +func (w *responseWriter) WriteHeader(code int) { + w.status = code + w.started = true + w.writer.WriteHeader(code) +} diff --git a/template.go b/template.go new file mode 100644 index 00000000..c6e53def --- /dev/null +++ b/template.go @@ -0,0 +1,55 @@ +package beego + +//@todo add template funcs + +import ( + //"fmt" + "errors" + "fmt" + "github.com/russross/blackfriday" + "html/template" + "strings" + "time" +) + +var beegoTplFuncMap template.FuncMap + +func init() { + beegoTplFuncMap = make(template.FuncMap) + beegoTplFuncMap["markdown"] = MarkDown + beegoTplFuncMap["dateformat"] = DateFormat + beegoTplFuncMap["compare"] = Compare +} + +// MarkDown parses a string in MarkDown format and returns HTML. Used by the template parser as "markdown" +func MarkDown(raw string) (output template.HTML) { + input := []byte(raw) + bOutput := blackfriday.MarkdownBasic(input) + output = template.HTML(string(bOutput)) + return +} + +// DateFormat takes a time and a layout string and returns a string with the formatted date. Used by the template parser as "dateformat" +func DateFormat(t time.Time, layout string) (datestring string) { + datestring = t.Format(layout) + return +} + +// Compare is a quick and dirty comparison function. It will convert whatever you give it to strings and see if the two values are equal. +// Whitespace is trimmed. Used by the template parser as "eq" +func Compare(a, b interface{}) (equal bool) { + equal = false + if strings.TrimSpace(fmt.Sprintf("%v", a)) == strings.TrimSpace(fmt.Sprintf("%v", b)) { + equal = true + } + return +} + +// AddFuncMap let user to register a func in the template +func AddFuncMap(key string, funname interface{}) error { + if _, ok := beegoTplFuncMap["key"]; ok { + beegoTplFuncMap[key] = funname + return nil + } + return errors.New("funcmap already has the key") +} diff --git a/utils.go b/utils.go new file mode 100644 index 00000000..a28bde57 --- /dev/null +++ b/utils.go @@ -0,0 +1,14 @@ +package beego + +import ( + "strings" + "time" +) + +func webTime(t time.Time) string { + ftime := t.Format(time.RFC1123) + if strings.HasSuffix(ftime, "UTC") { + ftime = ftime[0:len(ftime)-3] + "GMT" + } + return ftime +}