From f593c1f3fdce7b3721c818be8c22419f7f84a7d4 Mon Sep 17 00:00:00 2001 From: xiemengjun Date: Sat, 15 Dec 2012 23:53:19 +0800 Subject: [PATCH] init framwork --- README => README.md | 0 beego.go | 112 ++++++++++++++++++ config.go | 121 +++++++++++++++++++ context.go | 72 +++++++++++ controller.go | 147 +++++++++++++++++++++++ log.go | 85 +++++++++++++ model.go | 1 + router.go | 283 ++++++++++++++++++++++++++++++++++++++++++++ template.go | 55 +++++++++ utils.go | 14 +++ 10 files changed, 890 insertions(+) rename README => README.md (100%) create mode 100644 beego.go create mode 100644 config.go create mode 100644 context.go create mode 100644 controller.go create mode 100644 log.go create mode 100644 model.go create mode 100644 router.go create mode 100644 template.go create mode 100644 utils.go 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 +}