URL | 72 |Priority | 73 |Change Frequency | 74 |LastChange (GMT) | 75 |
---|---|---|---|
84 | |
91 |
92 | |
94 |
95 | |
97 |
98 | |
100 |
├── LICENSE ├── README.md ├── app ├── admin.go ├── app.go ├── cmd.go ├── cmd │ ├── backup.go │ ├── init.go │ ├── install.go │ ├── log.go │ ├── monitor.go │ ├── reboot.go │ ├── theme.go │ ├── upgrade.go │ └── zip.go ├── handler │ ├── admin.go │ ├── cmd.go │ ├── func.go │ ├── home.go │ ├── rss.go │ └── upload.go ├── log.go ├── model │ ├── article.go │ ├── comment.go │ ├── content.go │ ├── file.go │ ├── locker.go │ ├── message.go │ ├── page.go │ ├── setting.go │ ├── statis.go │ ├── storage.go │ ├── timer.go │ ├── token.go │ ├── user.go │ └── version.go ├── plugin │ ├── email.go │ ├── hello.go │ └── plugin.go ├── upgrade │ ├── v20140130.go │ ├── v20140131.go │ ├── v20140209.go │ └── v20140228.go └── utils │ ├── avatar.go │ ├── crypto.go │ ├── date.go │ ├── file.go │ ├── html.go │ ├── pager.go │ ├── sorter.go │ └── validator.go ├── main.go ├── static ├── css │ ├── admin.css │ ├── cmd.css │ ├── codemirror.css │ ├── common.css │ ├── highlight.css │ ├── ling.css │ ├── saber.css │ ├── sitemap.xsl │ └── style.css ├── favicon.ico ├── img │ ├── bg.png │ ├── header.gif │ └── site.png ├── js │ ├── home.js │ └── upload.js ├── koala-config.json ├── less │ ├── admin.less │ ├── cmd.less │ ├── common.less │ ├── common │ │ ├── base.less │ │ ├── form.less │ │ ├── grid.less │ │ ├── label.less │ │ ├── markdown.less │ │ ├── pager.less │ │ ├── reset.less │ │ └── var.less │ ├── ling.less │ ├── saber.less │ └── style.less └── lib │ ├── codemirror-mode.min.js │ ├── codemirror.min.js │ ├── highlight.min.js │ ├── jquery.form.min.js │ ├── marked.min.js │ └── validate.min.js └── view ├── admin ├── admin.layout ├── articles.html ├── cmd.layout ├── cmd │ ├── backup.html │ ├── log.html │ ├── message.html │ ├── monitor.html │ ├── reader.html │ └── theme.html ├── comments.html ├── edit_article.html ├── edit_page.html ├── files.html ├── home.html ├── login.html ├── pages.html ├── password.html ├── plugin.html ├── plugin_setting.html ├── profile.html ├── setting.html ├── write_article.html └── write_page.html ├── default ├── article.html ├── comment.html ├── error │ ├── error.html │ └── notfound.html ├── home.layout ├── index.html └── page.html ├── ling ├── article.html ├── comment.html ├── error │ ├── error.html │ └── notfound.html ├── home.layout ├── index.html └── page.html ├── rss.xml ├── saber ├── article.html ├── comment.html ├── error │ ├── error.html │ └── notfound.html ├── home.layout ├── index.html ├── page.html └── sidebar.html └── sitemap.xml /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 傅小黑 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Notice: this project is deprecated. Please try my new blog engine [pugo](https://github.com/go-xiaohei/pugo). 2 | 3 | #Fxh.Go 4 | 5 | A fast and simple blog engine with [GoInk](https://github.com/fuxiaohei/GoInk) framework in Golang. 6 | 7 | [](https://drone.io/github.com/fuxiaohei/GoBlog/latest) 8 | [](http://gowalker.org/github.com/fuxiaohei/GoBlog) 9 | 10 | Current version is **0.2.5** on 2014.02.28 11 | 12 | Development board is in [Trello](https://trello.com/b/7AHrcQL8/fxh-go-with-goink). 13 | 14 | ### Overview 15 | 16 | `Fxh.Go` is a dynamic blog engine written in Golang. It's fast and very simple configs. Fxh.Go persists data into pieces of json files and support compress them as backup zip for next upgrade or installation. 17 | 18 | `Fxh.Go` supports markdown contents as articles or pages, ajax comments and dynamic administration. 19 | 20 | `Fxh.Go` contains two kinds of content as article and page. They can be customized as you want. 21 | 22 | ### Installation 23 | 24 | `Fxh.Go` requires **Go 1.2** or above. 25 | 26 | ##### Gobuild.io 27 | 28 | [Gobuild.io](http://gobuild.io/) can build cross-platform executable file for pure go projects. You can download `Fxh.Go` binary from Gobuild.io. 29 | 30 | [](http://gobuild.io/github.com/fuxiaohei/GoBlog) 31 | 32 | ##### Manual 33 | 34 | Use go get command: 35 | 36 | go get github.com/fuxiaohei/GoBlog 37 | 38 | Then you can find binary file `GoBlog(.exe)` in `$GOPATH/bin`. 39 | 40 | ### Run 41 | 42 | Make a new dir to run `Fxh.Go`: 43 | 44 | cd new_dir 45 | Goblog 46 | 47 | Then it will unzip static files in `new_dir` , initialize raw data and start server at `localhost:9001`. 48 | 49 | ##### Admin 50 | 51 | Visit `localhost:9001/login/` to enter administrator with username `admin` and password `admin`. You'd better change them after installed successfully. 52 | 53 | ##### Deployment 54 | 55 | I prefer to use nginx as proxy. The server section in `nginx.conf`: 56 | 57 | server { 58 | listen 80; 59 | server_name your_domain; 60 | charset utf-8; 61 | access_log /var/log/nginx/your_domain.access.log; 62 | 63 | location / { 64 | proxy_pass http://127.0.0.1:9001; 65 | } 66 | 67 | location /static { 68 | root /var/www/your_domain; # binary file is in this directory 69 | expires 1d; 70 | add_header Cache-Control public; 71 | access_log off; 72 | } 73 | } 74 | 75 | ### Questions 76 | 77 | Create issues or pull requests here. 78 | 79 | ### Products 80 | 81 | * [抛弃世俗之浮躁,留我钻研之刻苦](http://wuwen.org) 82 | * [FuXiaoHei.Me](http://fuxiaohei.me) 83 | 84 | ### Thanks 85 | 86 | * [@Unknwon](https://github.com/Unknwon) on testing and [zip library](https://github.com/Unknwon/cae) support. 87 | 88 | ### License 89 | 90 | The MIT License 91 | 92 | -------------------------------------------------------------------------------- /app/admin.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/fuxiaohei/GoBlog/app/handler" 4 | 5 | func registerAdminHandler() { 6 | // add admin handlers 7 | App.Get("/admin/", handler.Auth, handler.Admin) 8 | 9 | App.Route("GET,POST", "/admin/profile/", handler.Auth, handler.AdminProfile) 10 | 11 | App.Route("GET,POST", "/admin/password/", handler.Auth, handler.AdminPassword) 12 | 13 | App.Route("GET,POST", "/admin/article/write/", handler.Auth, handler.ArticleWrite) 14 | App.Get("/admin/articles/", handler.Auth, handler.AdminArticle) 15 | App.Route("GET,POST,DELETE", "/admin/article/:id/", handler.Auth, handler.ArticleEdit) 16 | 17 | App.Route("GET,POST", "/admin/page/write/", handler.Auth, handler.PageWrite) 18 | App.Get("/admin/pages/", handler.Auth, handler.AdminPage) 19 | App.Route("GET,POST,DELETE", "/admin/page/:id/", handler.Auth, handler.PageEdit) 20 | 21 | App.Route("GET,POST,PUT,DELETE", "/admin/comments/", handler.Auth, handler.AdminComments) 22 | 23 | App.Route("GET,POST", "/admin/setting/", handler.Auth, handler.AdminSetting) 24 | App.Post("/admin/setting/custom/", handler.Auth, handler.CustomSetting) 25 | App.Post("/admin/setting/nav/", handler.Auth, handler.NavigatorSetting) 26 | 27 | App.Route("GET,DELETE", "/admin/files/", handler.Auth, handler.AdminFiles) 28 | App.Post("/admin/files/upload/", handler.Auth, handler.FileUpload) 29 | 30 | App.Route("GET,POST", "/admin/plugins/", handler.Auth, handler.AdminPlugin) 31 | App.Route("GET,POST", "/admin/plugins/:plugin_key/", handler.Auth, handler.PluginSetting) 32 | 33 | App.Post("/admin/message/read/", handler.Auth, handler.AdminMessageRead) 34 | } 35 | -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fuxiaohei/GoBlog/app/handler" 6 | "github.com/fuxiaohei/GoBlog/app/model" 7 | "github.com/fuxiaohei/GoBlog/app/plugin" 8 | "github.com/fuxiaohei/GoBlog/app/utils" 9 | "github.com/fuxiaohei/GoInk" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "path" 14 | "runtime/debug" 15 | "strconv" 16 | "strings" 17 | "syscall" 18 | ) 19 | 20 | var ( 21 | // APP VERSION, as date version 22 | VERSION = 20140228 23 | // Global GoInk application 24 | App *GoInk.App 25 | staticFileSuffix = ".css,.js,.jpg,.jpeg,.png,.gif,.ico,.xml,.zip,.txt,.html,.otf,.svg,.eot,.woff,.ttf,.doc,.ppt,.xls,.docx,.pptx,.xlsx,.xsl" 26 | uploadFileSuffix = ".jpg,.png,.gif,.zip,.txt,.doc,.docx,.xls,.xlsx,.ppt,.pptx" 27 | ) 28 | 29 | func init() { 30 | // init application 31 | App = GoInk.New() 32 | 33 | // init some settings 34 | App.Config().StringOr("app.static_dir", "static") 35 | App.Config().StringOr("app.log_dir", "tmp/log") 36 | os.MkdirAll(App.Get("log_dir"), os.ModePerm) 37 | os.MkdirAll("tmp/data", os.ModePerm) 38 | 39 | App.Config().IntOr("app.upload_size", 1024*1024*10) 40 | App.Config().StringOr("app.upload_files", uploadFileSuffix) 41 | App.Config().StringOr("app.upload_dir", path.Join(App.Get("static_dir"), "upload")) 42 | os.MkdirAll(App.Get("upload_dir"), os.ModePerm) 43 | 44 | if App.Get("static_files") != "" { 45 | staticFileSuffix = App.Get("static_files") 46 | } 47 | 48 | App.Static(func(context *GoInk.Context) { 49 | static := App.Config().String("app.static_dir") 50 | url := strings.TrimPrefix(context.Url, "/") 51 | if url == "favicon.ico" { 52 | url = path.Join(static, url) 53 | } 54 | if !strings.HasPrefix(url, static) { 55 | return 56 | } 57 | if !strings.Contains(staticFileSuffix, context.Ext) { 58 | context.Status = 403 59 | context.End() 60 | return 61 | } 62 | f, e := os.Stat(url) 63 | if e == nil { 64 | if f.IsDir() { 65 | context.Status = 403 66 | context.End() 67 | return 68 | } 69 | } 70 | /*_, e := os.Stat(url) 71 | if e != nil { 72 | context.Throw(404) 73 | return 74 | }*/ 75 | http.ServeFile(context.Response, context.Request, url) 76 | context.IsEnd = true 77 | }) 78 | 79 | // set recover handler 80 | App.Recover(func(context *GoInk.Context) { 81 | go LogError(append(append(context.Body, []byte("\n")...), debug.Stack()...)) 82 | theme := handler.Theme(context) 83 | if theme.Has("error/error.html") { 84 | theme.Layout("").Render("error/error", map[string]interface{}{ 85 | "error": string(context.Body), 86 | "stack": string(debug.Stack()), 87 | "context": context, 88 | }) 89 | } else { 90 | context.Body = append([]byte("
"), context.Body...) 91 | context.Body = append(context.Body, []byte("\n")...) 92 | context.Body = append(context.Body, debug.Stack()...) 93 | context.Body = append(context.Body, []byte("")...) 94 | } 95 | context.End() 96 | }) 97 | 98 | // set not found handler 99 | App.NotFound(func(context *GoInk.Context) { 100 | theme := handler.Theme(context) 101 | if theme.Has("error/notfound.html") { 102 | theme.Layout("").Render("error/notfound", map[string]interface{}{ 103 | "context": context, 104 | }) 105 | } 106 | context.End() 107 | }) 108 | 109 | // add recover defer 110 | defer func() { 111 | e := recover() 112 | if e != nil { 113 | bytes := append([]byte(fmt.Sprint(e)+"\n"), debug.Stack()...) 114 | LogError(bytes) 115 | println("panic error, crash down") 116 | os.Exit(1) 117 | } 118 | }() 119 | 120 | // catch exit command 121 | go catchExit() 122 | } 123 | 124 | // code from https://github.com/Unknwon/gowalker/blob/master/gowalker.go 125 | func catchExit() { 126 | sigTerm := syscall.Signal(15) 127 | sig := make(chan os.Signal) 128 | signal.Notify(sig, os.Interrupt, sigTerm) 129 | 130 | for { 131 | switch <-sig { 132 | case os.Interrupt, sigTerm: 133 | println("before exit, saving data") 134 | model.SyncAll() 135 | println("ready to exit") 136 | os.Exit(0) 137 | } 138 | } 139 | } 140 | 141 | // Init starts Fxh.Go application preparation. 142 | // Load models and plugins, update views. 143 | func Init() { 144 | 145 | // init storage 146 | model.Init(VERSION) 147 | 148 | // load all data 149 | model.All() 150 | 151 | // init plugin 152 | plugin.Init() 153 | 154 | // update plugin handlers 155 | plugin.Update(App) 156 | 157 | App.View().FuncMap["DateInt64"] = utils.DateInt64 158 | App.View().FuncMap["DateString"] = utils.DateString 159 | App.View().FuncMap["DateTime"] = utils.DateTime 160 | App.View().FuncMap["Now"] = utils.Now 161 | App.View().FuncMap["Html2str"] = utils.Html2str 162 | App.View().FuncMap["FileSize"] = utils.FileSize 163 | App.View().FuncMap["Setting"] = model.GetSetting 164 | App.View().FuncMap["Navigator"] = model.GetNavigators 165 | App.View().FuncMap["Md2html"] = utils.Markdown2HtmlTemplate 166 | App.View().IsCache = (model.GetSetting("theme_cache") == "true") 167 | 168 | println("app version @ " + strconv.Itoa(model.GetVersion().Version)) 169 | } 170 | 171 | func registerHomeHandler() { 172 | App.Route("GET,POST", "/login/", handler.Login) 173 | App.Get("/logout/", handler.Logout) 174 | 175 | App.Get("/article/:id/:slug", handler.Article) 176 | App.Get("/page/:id/:slug", handler.Page) 177 | App.Get("/p/:page/", handler.Home) 178 | App.Post("/comment/:id/", handler.Comment) 179 | App.Get("/tag/:tag/", handler.TagArticles) 180 | App.Get("/tag/:tag/p/:page/", handler.TagArticles) 181 | 182 | App.Get("/feed/", handler.Rss) 183 | App.Get("/sitemap", handler.SiteMap) 184 | 185 | App.Get("/:slug", handler.TopPage) 186 | App.Get("/", handler.Home) 187 | } 188 | 189 | // Run begins Fxh.Go http server. 190 | func Run() { 191 | 192 | registerAdminHandler() 193 | registerCmdHandler() 194 | registerHomeHandler() 195 | 196 | App.Run() 197 | } 198 | -------------------------------------------------------------------------------- /app/cmd.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/cmd" 5 | "github.com/fuxiaohei/GoBlog/app/handler" 6 | _ "github.com/fuxiaohei/GoBlog/app/upgrade" 7 | "os" 8 | ) 9 | 10 | // Cmd starts command line application. 11 | // It captures command line arguments and executes proper operation. 12 | // Some operations will exit application when finished. 13 | func Cmd() { 14 | args := os.Args 15 | if len(args) > 1 { 16 | switch args[1] { 17 | case "install": 18 | cmd.DoInstall() 19 | case "update": 20 | file, _ := cmd.DoBackup(App, false) 21 | cmd.DoUpdateZipBytes(file) 22 | case "backup": 23 | cmd.DoBackup(App, true) 24 | case "upgrade": 25 | cmd.DoUpgrade(VERSION, App) 26 | } 27 | os.Exit(1) 28 | } 29 | // do install and run server together 30 | if !cmd.CheckInstall() { 31 | cmd.DoInstall() 32 | return 33 | } 34 | // check app version 35 | if cmd.CheckUpgrade(VERSION, true) { 36 | os.Exit(1) 37 | return 38 | } 39 | 40 | // begin cmd init 41 | cmd.Init(App) 42 | } 43 | 44 | func registerCmdHandler() { 45 | App.Route("GET,POST,DELETE", "/cmd/backup/", handler.Auth, handler.CmdBackup) 46 | App.Get("/cmd/backup/file/", handler.Auth, handler.CmdBackupFile) 47 | 48 | App.Route("GET,POST,DELETE", "/cmd/message/", handler.Auth, handler.CmdMessage) 49 | App.Route("GET,DELETE", "/cmd/logs/", handler.Auth, handler.CmdLogs) 50 | App.Get("/cmd/monitor/", handler.Auth, handler.CmdMonitor) 51 | App.Route("GET,POST", "/cmd/theme/", handler.Auth, handler.CmdTheme) 52 | App.Route("GET,POST", "/cmd/reader/", handler.Auth, handler.CmdReader) 53 | } 54 | -------------------------------------------------------------------------------- /app/cmd/backup.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/Unknwon/cae/zip" 5 | "github.com/fuxiaohei/GoBlog/app/model" 6 | "github.com/fuxiaohei/GoBlog/app/utils" 7 | "github.com/fuxiaohei/GoInk" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "time" 12 | ) 13 | 14 | var backupDir = "backup" 15 | 16 | func init() { 17 | // close zip terminal output 18 | zip.Verbose = false 19 | } 20 | 21 | // DoBackup backups whole files to zip archive. 22 | // If withData is false, it compresses static files to zip archive without data files, config files and install lock file. 23 | func DoBackup(app *GoInk.App, withData bool) (string, error) { 24 | os.Mkdir(backupDir, os.ModePerm) 25 | // create zip file name from time unix 26 | filename := path.Join(backupDir, utils.DateTime(time.Now(), "YYYYMMDDHHmmss")) 27 | if withData { 28 | filename += ".zip" 29 | } else { 30 | filename += "_static.zip" 31 | } 32 | z, e := zip.Create(filename) 33 | if e != nil { 34 | return "", e 35 | } 36 | root, _ := os.Getwd() 37 | if withData { 38 | // if with data, add install lock file and config file 39 | lockFile := path.Join(root, "install.lock") 40 | if utils.IsFile(lockFile) { 41 | z.AddFile("install.lock", lockFile) 42 | } 43 | configFile := path.Join(root, "config.json") 44 | if utils.IsFile(configFile) { 45 | z.AddFile("config.json", configFile) 46 | } 47 | } 48 | z.AddDir("static/css", path.Join(root, "static", "css")) 49 | z.AddDir("static/img", path.Join(root, "static", "img")) 50 | z.AddDir("static/js", path.Join(root, "static", "js")) 51 | z.AddDir("static/lib", path.Join(root, "static", "lib")) 52 | z.AddFile("static/favicon.ico", path.Join(root, "static", "favicon.ico")) 53 | if withData { 54 | // if with data, backup data files and uploaded files 55 | z.AddDir("data", path.Join(root, "data")) 56 | z.AddDir("static/upload", path.Join(root, "static", "upload")) 57 | } 58 | z.AddDir(app.View().Dir, path.Join(root, app.View().Dir)) 59 | e = z.Flush() 60 | if e != nil { 61 | return "", e 62 | } 63 | println("backup success in " + filename) 64 | return filename, nil 65 | } 66 | 67 | // RemoveBackupFile removes backup zip file with filename(not filepath). 68 | func RemoveBackupFile(file string) { 69 | file = path.Join(backupDir, file) 70 | os.Remove(file) 71 | } 72 | 73 | // GetBackupFileAbsPath returns backup zip absolute filepath by filename. 74 | func GetBackupFileAbsPath(name string) string { 75 | return path.Join(backupDir, name) 76 | } 77 | 78 | // GetBackupFile returns fileinfo slice of all backup files. 79 | func GetBackupFiles() ([]os.FileInfo, error) { 80 | fi := make([]os.FileInfo, 0) 81 | e := filepath.Walk(backupDir, func(_ string, info os.FileInfo, _ error) error { 82 | if info == nil { 83 | return nil 84 | } 85 | if !info.IsDir() { 86 | fi = append([]os.FileInfo{info}, fi...) 87 | } 88 | return nil 89 | }) 90 | return fi, e 91 | } 92 | 93 | // StartBackupTimer starts backup operation timer for auto backup stuff. 94 | func StartBackupTimer(app *GoInk.App, t int) { 95 | model.SetTimerFunc("backup-data", 144, func() { 96 | filename, e := DoBackup(app, true) 97 | if e != nil { 98 | model.CreateMessage("backup", "[0]"+e.Error()) 99 | } else { 100 | model.CreateMessage("backup", "[1]"+filename) 101 | } 102 | println("backup files in", t, "hours") 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /app/cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/fuxiaohei/GoInk" 4 | 5 | func Init(app *GoInk.App) { 6 | StartBackupTimer(app, 24) 7 | } 8 | -------------------------------------------------------------------------------- /app/cmd/install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "github.com/Unknwon/cae/zip" 8 | "github.com/fuxiaohei/GoBlog/app/utils" 9 | "io/ioutil" 10 | "os" 11 | ) 12 | 13 | var ( 14 | tmpZipFile = "tmp.zip" 15 | installLockFile = "install.lock" 16 | ) 17 | 18 | func CheckInstall() bool { 19 | _, e := os.Stat(installLockFile) 20 | return e == nil 21 | } 22 | 23 | func ExtractBundleBytes() { 24 | // origin from https://github.com/wendal/gor/blob/master/gor/gor.go 25 | decoder := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(zipBytes)) 26 | b, _ := ioutil.ReadAll(decoder) 27 | ioutil.WriteFile(tmpZipFile, b, os.ModePerm) 28 | z, e := zip.Open(tmpZipFile) 29 | if e != nil { 30 | panic(e) 31 | os.Exit(1) 32 | } 33 | z.ExtractTo("") 34 | defer func() { 35 | z.Close() 36 | decoder = nil 37 | os.Remove(tmpZipFile) 38 | }() 39 | } 40 | 41 | func DoInstall() { 42 | ExtractBundleBytes() 43 | ioutil.WriteFile(installLockFile, []byte(fmt.Sprint(utils.Now())), os.ModePerm) 44 | println("install success") 45 | } 46 | 47 | func DoUpdateZipBytes(file string) error { 48 | // copy from https://github.com/wendal/gor/blob/master/gor/gor.go 49 | bytes, _ := ioutil.ReadFile(file) 50 | zipWriter, _ := os.OpenFile("app/cmd/zip.go", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) 51 | header := `package cmd 52 | const zipBytes="` 53 | zipWriter.Write([]byte(header)) 54 | encoder := base64.NewEncoder(base64.StdEncoding, zipWriter) 55 | encoder.Write(bytes) 56 | encoder.Close() 57 | zipWriter.Write([]byte(`"`)) 58 | zipWriter.Sync() 59 | zipWriter.Close() 60 | println("update success") 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /app/cmd/log.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoInk" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type logItem struct { 11 | Name string 12 | CreateTime int64 13 | Text string 14 | } 15 | 16 | func GetLogs(app *GoInk.App) []*logItem { 17 | dir := app.Get("log_dir") 18 | logs := make([]*logItem, 0) 19 | filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error { 20 | if err == nil { 21 | if info.IsDir() { 22 | return nil 23 | } 24 | ext := filepath.Ext(info.Name()) 25 | if ext != ".log" { 26 | return nil 27 | } 28 | bytes, e := ioutil.ReadFile(filepath.Join(dir, info.Name())) 29 | if e != nil { 30 | return nil 31 | } 32 | l := new(logItem) 33 | l.Name = info.Name() 34 | l.CreateTime = info.ModTime().Unix() 35 | l.Text = string(bytes) 36 | logs = append([]*logItem{l}, logs...) 37 | } 38 | return nil 39 | }) 40 | return logs 41 | } 42 | 43 | func RemoveLogFile(app *GoInk.App, file string) { 44 | f := filepath.Join(app.Get("log_dir"), file) 45 | os.Remove(f) 46 | } 47 | -------------------------------------------------------------------------------- /app/cmd/monitor.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fuxiaohei/GoBlog/app/utils" 6 | "runtime" 7 | "time" 8 | ) 9 | 10 | type monitorStats struct { 11 | NumGoroutine int 12 | MemAllocated string 13 | MemMalloc string 14 | MemTotal string 15 | MemSys string 16 | MemHeap string 17 | MemGc string 18 | LastGcTime string 19 | } 20 | 21 | func ReadMemStats() *monitorStats { 22 | m := new(runtime.MemStats) 23 | runtime.ReadMemStats(m) 24 | ms := new(monitorStats) 25 | ms.NumGoroutine = runtime.NumGoroutine() 26 | ms.MemAllocated = utils.FileSize(int64(m.Alloc)) 27 | ms.MemTotal = utils.FileSize(int64(m.TotalAlloc)) 28 | ms.MemSys = utils.FileSize(int64(m.Sys)) 29 | ms.MemHeap = utils.FileSize(int64(m.HeapAlloc)) 30 | ms.MemMalloc = utils.FileSize(int64(m.Mallocs)) 31 | ms.LastGcTime = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000) 32 | ms.MemGc = utils.FileSize(int64(m.NextGC)) 33 | return ms 34 | } 35 | -------------------------------------------------------------------------------- /app/cmd/reboot.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | -------------------------------------------------------------------------------- /app/cmd/theme.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/model" 5 | "github.com/fuxiaohei/GoInk" 6 | "io/ioutil" 7 | "path/filepath" 8 | ) 9 | 10 | var adminTheme = "admin" 11 | 12 | type themeItem struct { 13 | Name string 14 | Files []string 15 | ErrorFiles []string 16 | Layout []string 17 | } 18 | 19 | func SetThemeCache(ctx *GoInk.Context, cache bool) { 20 | ctx.App().View().NoCache() 21 | ctx.App().View().IsCache = cache 22 | if cache { 23 | model.SetSetting("theme_cache", "true") 24 | } else { 25 | model.SetSetting("theme_cache", "false") 26 | } 27 | model.SyncSettings() 28 | } 29 | 30 | func GetThemes(dir string) map[string]*themeItem { 31 | m := make(map[string]*themeItem) 32 | files, e := ioutil.ReadDir(dir) 33 | if e != nil { 34 | panic(e) 35 | } 36 | for _, fi := range files { 37 | if fi.IsDir() && fi.Name() != adminTheme { 38 | theme, e := createThemeItem(filepath.Join(dir, fi.Name())) 39 | if e != nil { 40 | continue 41 | } 42 | theme.Name = fi.Name() 43 | m[fi.Name()] = theme 44 | } 45 | } 46 | return m 47 | } 48 | 49 | func createThemeItem(dir string) (*themeItem, error) { 50 | files, e := ioutil.ReadDir(dir) 51 | if e != nil { 52 | return nil, e 53 | } 54 | theme := new(themeItem) 55 | theme.Files = make([]string, 0) 56 | theme.Layout = make([]string, 0) 57 | for _, fi := range files { 58 | if fi.IsDir() { 59 | if fi.Name() == "error" { 60 | theme.ErrorFiles, _ = filepath.Glob(filepath.Join(dir, fi.Name(), "*.html")) 61 | for i, f := range theme.ErrorFiles { 62 | theme.ErrorFiles[i] = filepath.Join(fi.Name(), filepath.Base(f)) 63 | } 64 | } else { 65 | f, _ := filepath.Glob(filepath.Join(dir, fi.Name(), "*.html")) 66 | for _, ff := range f { 67 | theme.Files = append(theme.Files, filepath.Join(fi.Name(), filepath.Base(ff))) 68 | } 69 | } 70 | } else { 71 | ext := filepath.Ext(fi.Name()) 72 | if ext == ".html" { 73 | theme.Files = append(theme.Files, fi.Name()) 74 | continue 75 | } 76 | if ext == ".layout" { 77 | theme.Layout = append(theme.Layout, fi.Name()) 78 | } 79 | } 80 | } 81 | return theme, nil 82 | } 83 | -------------------------------------------------------------------------------- /app/cmd/upgrade.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/model" 5 | "github.com/fuxiaohei/GoInk" 6 | "sort" 7 | "strconv" 8 | ) 9 | 10 | var upgradeScript map[int]func(app *GoInk.App) bool 11 | 12 | func init() { 13 | upgradeScript = make(map[int]func(app *GoInk.App) bool) 14 | } 15 | 16 | func SetUpgradeScript(v int, script func(app *GoInk.App) bool) { 17 | upgradeScript[v] = script 18 | } 19 | 20 | func CheckUpgrade(v int, print bool) bool { 21 | model.Init(v) 22 | appV := model.GetVersion() 23 | b := v > appV.Version 24 | if b && print { 25 | println("app version @ " + strconv.Itoa(v) + " is ahead of current version @ " + strconv.Itoa(appV.Version) + " , please run 'GoBlog upgrade'") 26 | } 27 | return b 28 | } 29 | 30 | func DoUpgrade(v int, app *GoInk.App) { 31 | if !CheckUpgrade(v, false) { 32 | println("app version @", v, "is updated") 33 | return 34 | } 35 | oldVersion := model.GetVersion().Version 36 | scriptIndex := []int{} 37 | for vr, _ := range upgradeScript { 38 | if vr <= v && vr > oldVersion { 39 | scriptIndex = append(scriptIndex, vr) 40 | } 41 | } 42 | sort.Sort(sort.IntSlice(scriptIndex)) 43 | for _, cv := range scriptIndex { 44 | upgradeScript[cv](app) 45 | println("upgrade @", cv, "success") 46 | } 47 | model.GetVersion().Version = v 48 | model.SyncVersion() 49 | println("app has upgraded to version @", v, "successfully, restart and keep enjoy !!") 50 | } 51 | -------------------------------------------------------------------------------- /app/handler/cmd.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/cmd" 5 | "github.com/fuxiaohei/GoBlog/app/model" 6 | "github.com/fuxiaohei/GoInk" 7 | ) 8 | 9 | func CmdBackup(context *GoInk.Context) { 10 | if context.Method == "POST" { 11 | file, e := cmd.DoBackup(context.App(), true) 12 | if e != nil { 13 | Json(context, false).Set("msg", e.Error()).End() 14 | return 15 | } 16 | Json(context, true).Set("file", file).End() 17 | context.Do("bakcup_success", file) 18 | model.CreateMessage("backup", "[1]"+file) 19 | return 20 | } 21 | if context.Method == "DELETE" { 22 | file := context.String("file") 23 | if file == "" { 24 | Json(context, false).End() 25 | return 26 | } 27 | cmd.RemoveBackupFile(file) 28 | Json(context, true).End() 29 | context.Do("backup_delete", file) 30 | return 31 | } 32 | files, _ := cmd.GetBackupFiles() 33 | context.Layout("admin/cmd") 34 | context.Render("admin/cmd/backup", map[string]interface{}{ 35 | "Files": files, 36 | "Title": "备份", 37 | }) 38 | } 39 | 40 | func CmdBackupFile(context *GoInk.Context) { 41 | file := context.String("file") 42 | context.Download(cmd.GetBackupFileAbsPath(file)) 43 | context.Do("backup_download", file) 44 | } 45 | 46 | func CmdMessage(context *GoInk.Context) { 47 | context.Layout("admin/cmd") 48 | context.Render("admin/cmd/message", map[string]interface{}{ 49 | "Title": "消息", 50 | "Messages": model.GetMessages(), 51 | }) 52 | } 53 | 54 | func CmdLogs(context *GoInk.Context) { 55 | if context.Method == "DELETE" { 56 | cmd.RemoveLogFile(context.App(), context.String("file")) 57 | Json(context, true).End() 58 | return 59 | } 60 | context.Layout("admin/cmd") 61 | context.Render("admin/cmd/log", map[string]interface{}{ 62 | "Title": "日志", 63 | "Logs": cmd.GetLogs(context.App()), 64 | }) 65 | } 66 | 67 | func CmdMonitor(ctx *GoInk.Context) { 68 | ctx.Layout("admin/cmd") 69 | ctx.Render("admin/cmd/monitor", map[string]interface{}{ 70 | "Title": "系统监控", 71 | "M": cmd.ReadMemStats(), 72 | }) 73 | } 74 | 75 | func CmdTheme(ctx *GoInk.Context) { 76 | if ctx.Method == "POST" { 77 | change := ctx.String("cache") 78 | if change != "" { 79 | cmd.SetThemeCache(ctx, change == "true") 80 | Json(ctx, true).End() 81 | return 82 | } 83 | theme := ctx.String("theme") 84 | if theme != "" { 85 | model.SetSetting("site_theme", theme) 86 | model.SyncSettings() 87 | Json(ctx, true).End() 88 | return 89 | } 90 | return 91 | } 92 | ctx.Layout("admin/cmd") 93 | ctx.Render("admin/cmd/theme", map[string]interface{}{ 94 | "Title": "主题", 95 | "Themes": cmd.GetThemes(ctx.App().Get("view_dir")), 96 | "CurrentTheme": model.GetSetting("site_theme"), 97 | }) 98 | } 99 | 100 | func CmdReader(ctx *GoInk.Context) { 101 | if ctx.Method == "POST" { 102 | email := ctx.String("email") 103 | model.RemoveReader(email) 104 | Json(ctx, true).End() 105 | return 106 | } 107 | ctx.Layout("admin/cmd") 108 | ctx.Render("admin/cmd/reader", map[string]interface{}{ 109 | "Title": "读者", 110 | "Readers": model.GetReaders(), 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /app/handler/func.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/model" 5 | "github.com/fuxiaohei/GoInk" 6 | "path" 7 | "strconv" 8 | ) 9 | 10 | type jsonContext struct { 11 | context *GoInk.Context 12 | data map[string]interface{} 13 | } 14 | 15 | // Json creates a json context response. 16 | func Json(context *GoInk.Context, res bool) *jsonContext { 17 | c := new(jsonContext) 18 | c.context = context 19 | c.data = make(map[string]interface{}) 20 | c.data["res"] = res 21 | return c 22 | } 23 | 24 | func (jc *jsonContext) Set(key string, v interface{}) *jsonContext { 25 | jc.data[key] = v 26 | return jc 27 | } 28 | 29 | func (jc *jsonContext) End() { 30 | jc.context.Json(jc.data) 31 | } 32 | 33 | type themeContext struct { 34 | context *GoInk.Context 35 | theme string 36 | } 37 | 38 | // Theme creates themed context response. 39 | func Theme(context *GoInk.Context) *themeContext { 40 | t := new(themeContext) 41 | t.context = context 42 | t.theme = model.GetSetting("site_theme") 43 | if t.theme == "" { 44 | t.theme = "default" 45 | } 46 | return t 47 | } 48 | 49 | func (tc *themeContext) Layout(layout string) *themeContext { 50 | if layout == "" { 51 | tc.context.Layout("") 52 | return tc 53 | } 54 | tc.context.Layout(path.Join(tc.theme, layout)) 55 | return tc 56 | } 57 | 58 | func (tc *themeContext) Render(tpl string, data map[string]interface{}) { 59 | tc.context.Render(path.Join(tc.theme, tpl), data) 60 | } 61 | 62 | func (tc *themeContext) Tpl(tpl string, data map[string]interface{}) string { 63 | return tc.context.Tpl(path.Join(tc.theme, tpl), data) 64 | } 65 | 66 | func (tc *themeContext) Has(tpl string) bool { 67 | file := path.Join(tc.theme, tpl) 68 | return tc.context.App().View().Has(file) 69 | } 70 | 71 | // CommentHtml returns rendered comment template html with own content. 72 | func CommentHtml(context *GoInk.Context, c *model.Content) string { 73 | thm := Theme(context) 74 | if !thm.Has("comment.html") { 75 | return "" 76 | } 77 | return thm.Tpl("comment", map[string]interface{}{ 78 | "Content": c, 79 | "Comments": c.Comments, 80 | }) 81 | } 82 | 83 | // SidebarHtml returns rendered sidebar template html. 84 | func SidebarHtml(context *GoInk.Context) string { 85 | thm := Theme(context) 86 | if !thm.Has("sidebar.html") { 87 | return "" 88 | } 89 | popSize, _ := strconv.Atoi(model.GetSetting("popular_size")) 90 | if popSize < 1 { 91 | popSize = 4 92 | } 93 | cmtSize, _ := strconv.Atoi(model.GetSetting("recent_comment_size")) 94 | if cmtSize < 1 { 95 | cmtSize = 3 96 | } 97 | return thm.Tpl("sidebar", map[string]interface{}{ 98 | "Popular": model.GetPopularArticleList(popSize), 99 | "RecentComment": model.GetCommentRecentList(cmtSize), 100 | "Tags": model.GetContentTags(), 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /app/handler/home.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/model" 5 | "github.com/fuxiaohei/GoBlog/app/utils" 6 | "github.com/fuxiaohei/GoInk" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func Login(context *GoInk.Context) { 13 | if context.Method == "POST" { 14 | data := context.Input() 15 | user := model.GetUserByName(data["user"]) 16 | if user == nil { 17 | Json(context, false).End() 18 | return 19 | } 20 | if !user.CheckPassword(data["password"]) { 21 | Json(context, false).End() 22 | return 23 | } 24 | exp := 3600 * 24 * 3 25 | expStr := strconv.Itoa(exp) 26 | s := model.CreateToken(user, context, int64(exp)) 27 | context.Cookie("token-user", strconv.Itoa(s.UserId), expStr) 28 | context.Cookie("token-value", s.Value, expStr) 29 | Json(context, true).End() 30 | return 31 | } 32 | if context.Cookie("token-value") != "" { 33 | context.Redirect("/admin/") 34 | return 35 | } 36 | context.Render("admin/login", nil) 37 | } 38 | 39 | func Auth(context *GoInk.Context) { 40 | tokenValue := context.Cookie("token-value") 41 | token := model.GetTokenByValue(tokenValue) 42 | if token == nil { 43 | context.Redirect("/logout/") 44 | context.End() 45 | return 46 | } 47 | if !token.IsValid() { 48 | context.Redirect("/logout/") 49 | context.End() 50 | return 51 | } 52 | } 53 | 54 | func Logout(context *GoInk.Context) { 55 | context.Cookie("token-user", "", "-3600") 56 | context.Cookie("token-value", "", "-3600") 57 | context.Redirect("/login/") 58 | } 59 | 60 | func TagArticles(ctx *GoInk.Context) { 61 | ctx.Layout("home") 62 | page, _ := strconv.Atoi(ctx.Param("page")) 63 | tag, _ := url.QueryUnescape(ctx.Param("tag")) 64 | size := getArticleListSize() 65 | articles, pager := model.GetTaggedArticleList(tag, page, getArticleListSize()) 66 | // fix dotted tag 67 | if len(articles) < 1 && strings.Contains(tag, "-") { 68 | articles, pager = model.GetTaggedArticleList(strings.Replace(tag, "-", ".", -1), page, size) 69 | } 70 | Theme(ctx).Layout("home").Render("index", map[string]interface{}{ 71 | "Articles": articles, 72 | "Pager": pager, 73 | "SidebarHtml": SidebarHtml(ctx), 74 | "Tag": tag, 75 | "Title": tag, 76 | }) 77 | } 78 | 79 | func Home(context *GoInk.Context) { 80 | context.Layout("home") 81 | page, _ := strconv.Atoi(context.Param("page")) 82 | articles, pager := model.GetPublishArticleList(page, getArticleListSize()) 83 | data := map[string]interface{}{ 84 | "Articles": articles, 85 | "Pager": pager, 86 | "SidebarHtml": SidebarHtml(context), 87 | } 88 | if page > 1 { 89 | data["Title"] = "第 " + strconv.Itoa(page) + " 页" 90 | } 91 | Theme(context).Layout("home").Render("index", data) 92 | } 93 | 94 | func Article(context *GoInk.Context) { 95 | id, _ := strconv.Atoi(context.Param("id")) 96 | slug := context.Param("slug") 97 | article := model.GetContentById(id) 98 | if article == nil { 99 | context.Redirect("/") 100 | return 101 | } 102 | if article.Slug != slug || article.Type != "article" { 103 | context.Redirect("/") 104 | return 105 | } 106 | article.Hits++ 107 | Theme(context).Layout("home").Render("article", map[string]interface{}{ 108 | "Title": article.Title, 109 | "Article": article, 110 | "CommentHtml": CommentHtml(context, article), 111 | }) 112 | } 113 | 114 | func Page(context *GoInk.Context) { 115 | id, _ := strconv.Atoi(context.Param("id")) 116 | slug := context.Param("slug") 117 | article := model.GetContentById(id) 118 | if article == nil || article.Status != "publish" { 119 | context.Redirect("/") 120 | return 121 | } 122 | if article.Slug != slug || article.Type != "page" { 123 | context.Redirect("/") 124 | return 125 | } 126 | article.Hits++ 127 | Theme(context).Layout("home").Render("page", map[string]interface{}{ 128 | "Title": article.Title, 129 | "Page": article, 130 | //"CommentHtml": Comments(context, article), 131 | }) 132 | } 133 | 134 | func TopPage(context *GoInk.Context) { 135 | slug := context.Param("slug") 136 | page := model.GetContentBySlug(slug) 137 | if page == nil || page.Status != "publish" { 138 | context.Redirect("/") 139 | return 140 | } 141 | if page.IsLinked && page.Type == "page" { 142 | Theme(context).Layout("home").Render("page", map[string]interface{}{ 143 | "Title": page.Title, 144 | "Page": page, 145 | }) 146 | page.Hits++ 147 | return 148 | } 149 | context.Redirect("/") 150 | } 151 | 152 | func Comment(context *GoInk.Context) { 153 | cid, _ := strconv.Atoi(context.Param("id")) 154 | if cid < 1 { 155 | Json(context, false).End() 156 | return 157 | } 158 | if model.GetContentById(cid) == nil { 159 | Json(context, false).End() 160 | return 161 | } 162 | data := context.Input() 163 | msg := validateComment(data) 164 | if msg != "" { 165 | Json(context, false).Set("msg", msg).End() 166 | return 167 | } 168 | co := new(model.Comment) 169 | co.Author = data["user"] 170 | co.Email = data["email"] 171 | co.Url = data["url"] 172 | co.Content = data["content"] 173 | co.Avatar = utils.Gravatar(co.Email, "50") 174 | co.Pid, _ = strconv.Atoi(data["pid"]) 175 | co.Ip = context.Ip 176 | co.UserAgent = context.UserAgent 177 | co.IsAdmin = false 178 | model.CreateComment(cid, co) 179 | Json(context, true).Set("comment", co.ToJson()).End() 180 | model.CreateMessage("comment", co) 181 | context.Do("comment_created", co) 182 | } 183 | 184 | func validateComment(data map[string]string) string { 185 | if utils.IsEmptyString(data["user"]) || utils.IsEmptyString(data["content"]) { 186 | return "称呼,邮箱,内容必填" 187 | } 188 | if !utils.IsEmail(data["email"]) { 189 | return "邮箱格式错误" 190 | } 191 | if !utils.IsEmptyString(data["url"]) && !utils.IsURL(data["url"]) { 192 | return "网址格式错误" 193 | } 194 | return "" 195 | } 196 | 197 | func getArticleListSize() int { 198 | size, _ := strconv.Atoi(model.GetSetting("article_size")) 199 | if size < 1 { 200 | size = 5 201 | } 202 | return size 203 | } 204 | -------------------------------------------------------------------------------- /app/handler/rss.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/model" 5 | "github.com/fuxiaohei/GoBlog/app/utils" 6 | "github.com/fuxiaohei/GoInk" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func SiteMap(ctx *GoInk.Context) { 12 | baseUrl := model.GetSetting("site_url") 13 | println(baseUrl) 14 | article, _ := model.GetPublishArticleList(1, 50) 15 | navigators := model.GetNavigators() 16 | now := time.Unix(utils.Now(), 0).Format(time.RFC3339) 17 | 18 | articleMap := make([]map[string]string, len(article)) 19 | for i, a := range article { 20 | m := make(map[string]string) 21 | m["Link"] = strings.Replace(baseUrl+a.Link(), baseUrl+"/", baseUrl, -1) 22 | m["Created"] = time.Unix(a.CreateTime, 0).Format(time.RFC3339) 23 | articleMap[i] = m 24 | } 25 | 26 | navMap := make([]map[string]string, 0) 27 | for _, n := range navigators { 28 | m := make(map[string]string) 29 | if n.Link == "/" { 30 | continue 31 | } 32 | if strings.HasPrefix(n.Link, "/") { 33 | m["Link"] = strings.Replace(baseUrl+n.Link, baseUrl+"/", baseUrl, -1) 34 | } else { 35 | m["Link"] = n.Link 36 | } 37 | m["Created"] = now 38 | navMap = append(navMap, m) 39 | } 40 | 41 | ctx.ContentType("text/xml") 42 | bytes, e := ctx.App().View().Render("sitemap.xml", map[string]interface{}{ 43 | "Title": model.GetSetting("site_title"), 44 | "Link": baseUrl, 45 | "Created": now, 46 | "Articles": articleMap, 47 | "Navigators": navMap, 48 | }) 49 | if e != nil { 50 | panic(e) 51 | } 52 | ctx.Body = bytes 53 | 54 | } 55 | 56 | func Rss(ctx *GoInk.Context) { 57 | baseUrl := model.GetSetting("site_url") 58 | article, _ := model.GetPublishArticleList(1, 20) 59 | author := model.GetUsersByRole("ADMIN")[0] 60 | 61 | articleMap := make([]map[string]string, len(article)) 62 | for i, a := range article { 63 | m := make(map[string]string) 64 | m["Title"] = a.Title 65 | m["Link"] = strings.Replace(baseUrl+a.Link(), baseUrl+"/", baseUrl, -1) 66 | m["Author"] = author.Nick 67 | str := utils.Markdown2Html(a.Content()) 68 | str = strings.Replace(str, `src="/`, `src="`+strings.TrimSuffix(baseUrl, "/")+"/", -1) 69 | str = strings.Replace(str, `href="/`, `href="`+strings.TrimSuffix(baseUrl, "/")+"/", -1) 70 | m["Desc"] = str 71 | m["Created"] = time.Unix(a.CreateTime, 0).Format(time.RFC822) 72 | articleMap[i] = m 73 | } 74 | 75 | ctx.ContentType("application/rss+xml;charset=UTF-8") 76 | 77 | bytes, e := ctx.App().View().Render("rss.xml", map[string]interface{}{ 78 | "Title": model.GetSetting("site_title"), 79 | "Link": baseUrl, 80 | "Desc": model.GetSetting("site_description"), 81 | "Created": time.Unix(utils.Now(), 0).Format(time.RFC822), 82 | "Articles": articleMap, 83 | }) 84 | if e != nil { 85 | panic(e) 86 | } 87 | ctx.Body = bytes 88 | } 89 | -------------------------------------------------------------------------------- /app/handler/upload.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/model" 5 | "github.com/fuxiaohei/GoInk" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "path" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | func AdminFiles(context *GoInk.Context) { 15 | if context.Method == "DELETE" { 16 | id := context.Int("id") 17 | model.RemoveFile(id) 18 | Json(context, true).End() 19 | context.Do("attach_delete", id) 20 | return 21 | } 22 | files, pager := model.GetFileList(context.Int("page"), 10) 23 | context.Layout("admin/admin") 24 | context.Render("admin/files", map[string]interface{}{ 25 | "Title": "媒体文件", 26 | "Files": files, 27 | "Pager": pager, 28 | }) 29 | } 30 | 31 | func FileUpload(context *GoInk.Context) { 32 | var req *http.Request 33 | req = context.Request 34 | req.ParseMultipartForm(32 << 20) 35 | f, h, e := req.FormFile("file") 36 | if e != nil { 37 | Json(context, false).Set("msg", e.Error()).End() 38 | return 39 | } 40 | data, _ := ioutil.ReadAll(f) 41 | maxSize := context.App().Config().Int("app.upload_size") 42 | defer func() { 43 | f.Close() 44 | data = nil 45 | h = nil 46 | }() 47 | if len(data) >= maxSize { 48 | Json(context, false).Set("msg", "文件应小于10M").End() 49 | return 50 | } 51 | if !strings.Contains(context.App().Config().String("app.upload_files"), path.Ext(h.Filename)) { 52 | Json(context, false).Set("msg", "文件只支持Office文件,图片和zip存档").End() 53 | return 54 | } 55 | ff := new(model.File) 56 | ff.Name = h.Filename 57 | ff.Type = context.StringOr("type", "image") 58 | ff.Size = int64(len(data)) 59 | ff.ContentType = h.Header["Content-Type"][0] 60 | ff.Author, _ = strconv.Atoi(context.Cookie("token-user")) 61 | ff.Url = model.CreateFilePath(context.App().Get("upload_dir"), ff) 62 | e = ioutil.WriteFile(ff.Url, data, os.ModePerm) 63 | if e != nil { 64 | Json(context, false).Set("msg", e.Error()).End() 65 | return 66 | } 67 | model.CreateFile(ff) 68 | Json(context, true).Set("file", ff).End() 69 | context.Do("attach_created", ff) 70 | } 71 | -------------------------------------------------------------------------------- /app/log.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/utils" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | ) 9 | 10 | // LogErrors logs error bytes to tmp/log directory. 11 | func LogError(bytes []byte) { 12 | dir := App.Config().String("app.log_dir") 13 | file := path.Join(dir, utils.DateInt64(utils.Now(), "MMDDHHmmss.log")) 14 | ioutil.WriteFile(file, bytes, os.ModePerm) 15 | } 16 | -------------------------------------------------------------------------------- /app/model/article.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/fuxiaohei/GoBlog/app/utils" 4 | 5 | func generatePublishArticleIndex() { 6 | arr := make([]int, 0) 7 | for _, id := range contentsIndex["article"] { 8 | c := GetContentById(id) 9 | if c.Status == "publish" { 10 | arr = append(arr, id) 11 | } 12 | } 13 | contentsIndex["article-publish"] = arr 14 | } 15 | 16 | // GetPublishArticleList gets published article list and pager. 17 | func GetPublishArticleList(page, size int) ([]*Content, *utils.Pager) { 18 | index := contentsIndex["article-publish"] 19 | pager := utils.NewPager(page, size, len(index)) 20 | articles := make([]*Content, 0) 21 | if len(index) < 1 { 22 | return articles, pager 23 | } 24 | if page > pager.Pages { 25 | return articles, pager 26 | } 27 | for i := pager.Begin; i <= pager.End; i++ { 28 | articles = append(articles, GetContentById(index[i-1])) 29 | } 30 | return articles, pager 31 | } 32 | 33 | // GetArticleList gets articles list and pager no matter article status. 34 | func GetArticleList(page, size int) ([]*Content, *utils.Pager) { 35 | index := contentsIndex["article"] 36 | pager := utils.NewPager(page, size, len(index)) 37 | articles := make([]*Content, 0) 38 | if len(index) < 1 { 39 | return articles, pager 40 | } 41 | if page > pager.Pages { 42 | return articles, pager 43 | } 44 | for i := pager.Begin; i <= pager.End; i++ { 45 | articles = append(articles, GetContentById(index[i-1])) 46 | } 47 | return articles, pager 48 | } 49 | 50 | // GetPopularArticleList returns popular articles list. 51 | // Popular articles are ordered by comment number. 52 | func GetPopularArticleList(size int) []*Content { 53 | index := contentsIndex["article-pop"] 54 | pager := utils.NewPager(1, size, len(index)) 55 | articles := make([]*Content, 0) 56 | if len(index) < 1 { 57 | return articles 58 | } 59 | if 1 > pager.Pages { 60 | return articles 61 | } 62 | for i := pager.Begin; i <= pager.End; i++ { 63 | articles = append(articles, GetContentById(index[i-1])) 64 | } 65 | return articles 66 | } 67 | 68 | // GetTaggedArticleList returns tagged articles list. 69 | // These articles contains same one tag. 70 | func GetTaggedArticleList(tag string, page, size int) ([]*Content, *utils.Pager) { 71 | index := contentsIndex["t-"+tag] 72 | pager := utils.NewPager(page, size, len(index)) 73 | articles := make([]*Content, 0) 74 | if len(index) < 1 { 75 | return articles, pager 76 | } 77 | if page > pager.Pages { 78 | return articles, pager 79 | } 80 | for i := pager.Begin; i <= pager.End; i++ { 81 | articles = append(articles, GetContentById(index[i-1])) 82 | } 83 | return articles, pager 84 | } 85 | -------------------------------------------------------------------------------- /app/model/file.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/utils" 5 | "os" 6 | "path" 7 | "strconv" 8 | ) 9 | 10 | var ( 11 | files []*File 12 | fileMaxId int 13 | ) 14 | 15 | type File struct { 16 | Id int 17 | Name string 18 | UploadTime int64 19 | Url string 20 | ContentType string 21 | Author int 22 | IsUsed bool 23 | Size int64 24 | Type string 25 | Hits int 26 | } 27 | 28 | func CreateFile(f *File) *File { 29 | fileMaxId += Storage.TimeInc(3) 30 | f.Id = fileMaxId 31 | f.UploadTime = utils.Now() 32 | f.IsUsed = true 33 | f.Hits = 0 34 | files = append([]*File{f}, files...) 35 | go SyncFiles() 36 | return f 37 | } 38 | 39 | func CreateFilePath(dir string, f *File) string { 40 | os.MkdirAll(dir, os.ModePerm) 41 | name := utils.DateInt64(utils.Now(), "YYYYMMDDHHmmss") 42 | name += strconv.Itoa(Storage.TimeInc(10)) + path.Ext(f.Name) 43 | return path.Join(dir, name) 44 | } 45 | 46 | func GetFileList(page, size int) ([]*File, *utils.Pager) { 47 | pager := utils.NewPager(page, size, len(files)) 48 | f := make([]*File, 0) 49 | if page > pager.Pages || len(files) < 1 { 50 | return f, pager 51 | } 52 | for i := pager.Begin; i <= pager.End; i++ { 53 | f = files[pager.Begin-1 : pager.End] 54 | } 55 | return f, pager 56 | } 57 | 58 | func RemoveFile(id int) { 59 | for i, f2 := range files { 60 | if id == f2.Id { 61 | files = append(files[:i], files[i+1:]...) 62 | os.Remove(f2.Url) 63 | } 64 | } 65 | go SyncFiles() 66 | } 67 | 68 | func SyncFiles() { 69 | Storage.Set("files", files) 70 | } 71 | 72 | func LoadFiles() { 73 | files = make([]*File, 0) 74 | fileMaxId = 0 75 | Storage.Get("files", &files) 76 | for _, f := range files { 77 | if f.Id > fileMaxId { 78 | fileMaxId = f.Id 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/model/locker.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "sync" 4 | 5 | var locker sync.Mutex 6 | 7 | func init() { 8 | locker = sync.Mutex{} 9 | } 10 | -------------------------------------------------------------------------------- /app/model/message.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/utils" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | messages []*Message 10 | messageMaxId int 11 | messageGenerator map[string]func(v interface{}) string 12 | ) 13 | 14 | func init() { 15 | messageGenerator = make(map[string]func(v interface{}) string) 16 | messageGenerator["comment"] = generateCommentMessage 17 | messageGenerator["backup"] = generateBackupMessage 18 | } 19 | 20 | type Message struct { 21 | Id int 22 | Type string 23 | CreateTime int64 24 | Data string 25 | IsRead bool 26 | } 27 | 28 | func CreateMessage(tp string, data interface{}) *Message { 29 | m := new(Message) 30 | m.Type = tp 31 | m.Data = messageGenerator[tp](data) 32 | if m.Data == "" { 33 | println("message generator returns empty") 34 | return nil 35 | } 36 | m.CreateTime = utils.Now() 37 | m.IsRead = false 38 | messageMaxId += Storage.TimeInc(3) 39 | m.Id = messageMaxId 40 | messages = append([]*Message{m}, messages...) 41 | SyncMessages() 42 | return m 43 | } 44 | 45 | func SetMessageGenerator(name string, fn func(v interface{}) string) { 46 | messageGenerator[name] = fn 47 | } 48 | 49 | func GetMessage(id int) *Message { 50 | for _, m := range messages { 51 | if m.Id == id { 52 | return m 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | func GetUnreadMessages() []*Message { 59 | ms := make([]*Message, 0) 60 | for _, m := range messages { 61 | if m.IsRead { 62 | continue 63 | } 64 | ms = append(ms, m) 65 | } 66 | return ms 67 | } 68 | 69 | func GetMessages() []*Message { 70 | return messages 71 | } 72 | 73 | func GetTypedMessages(tp string, unread bool) []*Message { 74 | ms := make([]*Message, 0) 75 | for _, m := range messages { 76 | if m.Type == tp { 77 | if unread { 78 | if !m.IsRead { 79 | ms = append(ms, m) 80 | } 81 | } else { 82 | ms = append(ms, m) 83 | } 84 | } 85 | } 86 | return ms 87 | } 88 | 89 | func SaveMessageRead(m *Message) { 90 | m.IsRead = true 91 | SyncMessages() 92 | } 93 | 94 | func SyncMessages() { 95 | Storage.Set("messages", messages) 96 | } 97 | 98 | func LoadMessages() { 99 | messages = make([]*Message, 0) 100 | messageMaxId = 0 101 | Storage.Get("messages", &messages) 102 | for _, m := range messages { 103 | if m.Id > messageMaxId { 104 | messageMaxId = m.Id 105 | } 106 | } 107 | } 108 | 109 | func RecycleMessages() { 110 | for i, m := range messages { 111 | if m.CreateTime+3600*24*3 < utils.Now() { 112 | messages = messages[:i] 113 | return 114 | } 115 | } 116 | } 117 | 118 | func generateCommentMessage(co interface{}) string { 119 | c, ok := co.(*Comment) 120 | if !ok { 121 | return "" 122 | } 123 | cnt := GetContentById(c.Cid) 124 | s := "" 125 | if c.Pid < 1 { 126 | s = "
" + c.Author + "同学,在文章《" + cnt.Title + "》发表评论:" 127 | s += utils.Html2str(c.Content) + "
" 128 | } else { 129 | p := GetCommentById(c.Pid) 130 | s = "" + p.Author + "同学,在文章《" + cnt.Title + "》的评论:" 131 | s += utils.Html2str(p.Content) + "
" 132 | s += "" + c.Author + "同学的回复:" 133 | s += utils.Html2str(c.Content) + "
" 134 | } 135 | return s 136 | } 137 | 138 | func generateBackupMessage(co interface{}) string { 139 | str := co.(string) 140 | if strings.HasPrefix(str, "[0]") { 141 | return "备份全站失败: " + strings.TrimPrefix(str, "[0]") + "." 142 | } 143 | return "备份全站到 " + strings.TrimPrefix(str, "[1]") + " 成功." 144 | } 145 | 146 | func startMessageTimer() { 147 | SetTimerFunc("message-sync", 9, func() { 148 | println("write messages in 1.5 hour timer") 149 | RecycleMessages() 150 | SyncMessages() 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /app/model/page.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/fuxiaohei/GoBlog/app/utils" 4 | 5 | // GetPageList gets pages list and pager no matter page status. 6 | // In common cases, no need to get a list or pagers for public page. 7 | func GetPageList(page, size int) ([]*Content, *utils.Pager) { 8 | index := contentsIndex["page"] 9 | pager := utils.NewPager(page, size, len(index)) 10 | pages := make([]*Content, 0) 11 | if len(index) < 1 { 12 | return pages, pager 13 | } 14 | if page > pager.Pages { 15 | return pages, pager 16 | } 17 | for i := pager.Begin; i <= pager.End; i++ { 18 | pages = append(pages, GetContentById(index[i-1])) 19 | } 20 | return pages, pager 21 | } 22 | -------------------------------------------------------------------------------- /app/model/setting.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type navItem struct { 9 | Order int 10 | Text string 11 | Title string 12 | Link string 13 | } 14 | 15 | var ( 16 | settings map[string]string 17 | navigators []*navItem 18 | ) 19 | 20 | func GetSetting(key string) string { 21 | return settings[key] 22 | } 23 | 24 | func GetCustomSettings() map[string]string { 25 | m := make(map[string]string) 26 | for k, v := range settings { 27 | if strings.HasPrefix(k, "c_") { 28 | m[strings.TrimPrefix(k, "c_")] = v 29 | } 30 | } 31 | return m 32 | } 33 | 34 | func SetSetting(key string, v string) { 35 | settings[key] = v 36 | } 37 | 38 | func SyncSettings() { 39 | Storage.Set("settings", settings) 40 | } 41 | 42 | func LoadSettings() { 43 | settings = make(map[string]string) 44 | Storage.Get("settings", &settings) 45 | } 46 | 47 | func SortNavigators() { 48 | l := len(navigators) 49 | for i := 1; i < l; i++ { 50 | for j := i; j > 0; j-- { 51 | if navigators[j].Order < navigators[j-1].Order { 52 | navigators[j], navigators[j-1] = navigators[j-1], navigators[j] 53 | } 54 | } 55 | } 56 | } 57 | 58 | func LoadNavigators() { 59 | navigators = make([]*navItem, 0) 60 | Storage.Get("navigators", &navigators) 61 | SortNavigators() 62 | } 63 | 64 | func SetNavigators(order []string, text []string, title []string, link []string) { 65 | navs := make([]*navItem, len(text)) 66 | for i, t := range text { 67 | n := new(navItem) 68 | n.Order, _ = strconv.Atoi(order[i]) 69 | n.Text = t 70 | n.Title = title[i] 71 | n.Link = link[i] 72 | navs[i] = n 73 | } 74 | navigators = navs 75 | SyncNavigators() 76 | } 77 | 78 | func DefaultNavigators() { 79 | n := new(navItem) 80 | n.Order = 1 81 | n.Text = "文章" 82 | n.Title = "文章" 83 | n.Link = "/" 84 | n2 := new(navItem) 85 | n2.Order = 2 86 | n2.Text = "关于" 87 | n2.Title = "关于" 88 | n2.Link = "/about-me.html" 89 | n3 := new(navItem) 90 | n3.Order = 3 91 | n3.Text = "好友" 92 | n3.Title = "好友" 93 | n3.Link = "/friends.html" 94 | navigators = []*navItem{n, n2, n3} 95 | Storage.Set("navigators", navigators) 96 | } 97 | 98 | func SyncNavigators() { 99 | Storage.Set("navigators", navigators) 100 | SortNavigators() 101 | } 102 | 103 | func GetNavigators() []*navItem { 104 | return navigators 105 | } 106 | -------------------------------------------------------------------------------- /app/model/statis.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Statis struct { 4 | Comments int 5 | Articles int 6 | Pages int 7 | Files int 8 | Version int 9 | Readers int 10 | } 11 | 12 | func NewStatis() *Statis { 13 | s := new(Statis) 14 | s.Comments = len(commentsIndex) 15 | s.Articles = len(contentsIndex["article"]) 16 | s.Pages = len(contentsIndex["page"]) 17 | s.Files = len(files) 18 | s.Version = GetVersion().Version 19 | s.Readers = len(GetReaders()) 20 | return s 21 | } 22 | -------------------------------------------------------------------------------- /app/model/timer.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type timerFunc struct { 8 | Fn func() 9 | Ticker int 10 | } 11 | 12 | var ( 13 | timerCount int 14 | timerFuncs map[string]*timerFunc 15 | ) 16 | 17 | func init() { 18 | timerCount = 0 19 | timerFuncs = make(map[string]*timerFunc) 20 | } 21 | 22 | // SetTimerFunc adds timer func for time ticker. 23 | // Ticker means step time, after ticker size step passed, do function. 24 | // Name is unique name of func.If set same name func, use the last one. 25 | func SetTimerFunc(name string, ticker int, fn func()) { 26 | tfn := new(timerFunc) 27 | tfn.Fn = fn 28 | tfn.Ticker = ticker 29 | timerFuncs[name] = tfn 30 | } 31 | 32 | // ChangeTimerFunc can change timer func by given name. 33 | // If the func of name is none, do not change anything, print error message. 34 | func ChangeTimerFunc(name string, ticker int, fn func()) { 35 | if _, ok := timerFuncs[name]; ok { 36 | timerFuncs[name].Fn = fn 37 | timerFuncs[name].Ticker = ticker 38 | } else { 39 | println("change invalid timer func : " + name) 40 | } 41 | } 42 | 43 | // DelTimerFunc deletes timer func. 44 | func DelTimerFunc(name string) { 45 | delete(timerFuncs, name) 46 | } 47 | 48 | // GetTimerFuncs returns registered timer func with its name and ticker int. 49 | func GetTimerFuncs() map[string]int { 50 | m := make(map[string]int) 51 | for n, f := range timerFuncs { 52 | m[n] = f.Ticker 53 | } 54 | return m 55 | } 56 | 57 | // StartModelTimer adds models' timer and starts time ticker. 58 | // The default step is 10 min once. 59 | func StartModelTimer() { 60 | // start all timers 61 | startCommentsTimer() 62 | startContentSyncTimer() 63 | startContentTmpIndexesTimer() 64 | startMessageTimer() 65 | // start time ticker 66 | ticker := time.NewTicker(time.Duration(10) * time.Minute) 67 | go doTimers(ticker.C) 68 | } 69 | 70 | func doTimers(c <-chan time.Time) { 71 | for { 72 | <-c 73 | timerCount++ 74 | for _, tfn := range timerFuncs { 75 | if timerCount%tfn.Ticker == 0 { 76 | tfn.Fn() 77 | } 78 | } 79 | if timerCount > 999 { 80 | timerCount = 0 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/model/token.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fuxiaohei/GoBlog/app/utils" 6 | "github.com/fuxiaohei/GoInk" 7 | ) 8 | 9 | var tokens map[string]*Token 10 | 11 | type Token struct { 12 | Value string 13 | UserId int 14 | CreateTime int64 15 | ExpireTime int64 16 | } 17 | 18 | // check token is valid or expired. 19 | func (t *Token) IsValid() bool { 20 | if GetUserById(t.UserId) == nil { 21 | return false 22 | } 23 | return t.ExpireTime > utils.Now() 24 | } 25 | 26 | // create new token from user and context. 27 | func CreateToken(u *User, context *GoInk.Context, expire int64) *Token { 28 | t := new(Token) 29 | t.UserId = u.Id 30 | t.CreateTime = utils.Now() 31 | t.ExpireTime = t.CreateTime + expire 32 | t.Value = utils.Sha1(fmt.Sprintf("%s-%s-%d-%d", context.Ip, context.UserAgent, t.CreateTime, t.UserId)) 33 | tokens[t.Value] = t 34 | go SyncTokens() 35 | return t 36 | } 37 | 38 | // get token by token value. 39 | func GetTokenByValue(v string) *Token { 40 | return tokens[v] 41 | } 42 | 43 | // get tokens of given user. 44 | func GetTokensByUser(u *User) []*Token { 45 | ts := make([]*Token, 0) 46 | for _, t := range tokens { 47 | if t.UserId == u.Id { 48 | ts = append(ts, t) 49 | } 50 | } 51 | return ts 52 | } 53 | 54 | // remove a token by token value. 55 | func RemoveToken(v string) { 56 | delete(tokens, v) 57 | go SyncTokens() 58 | } 59 | 60 | // clean all expired tokens in memory. 61 | // do not write to json. 62 | func CleanTokens() { 63 | for k, t := range tokens { 64 | if !t.IsValid() { 65 | delete(tokens, k) 66 | } 67 | } 68 | } 69 | 70 | // write tokens to json. 71 | // it calls CleanTokens before writing. 72 | func SyncTokens() { 73 | CleanTokens() 74 | Storage.Set("tokens", tokens) 75 | } 76 | 77 | // load all tokens from json. 78 | func LoadTokens() { 79 | tokens = make(map[string]*Token) 80 | Storage.Get("tokens", &tokens) 81 | } 82 | -------------------------------------------------------------------------------- /app/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "github.com/fuxiaohei/GoBlog/app/utils" 6 | ) 7 | 8 | var ( 9 | users []*User 10 | userMaxId int 11 | ) 12 | 13 | type User struct { 14 | Id int 15 | Name string 16 | Password string 17 | Nick string 18 | Email string 19 | Avatar string 20 | Url string 21 | Bio string 22 | CreateTime int64 23 | LastLoginTime int64 24 | Role string 25 | } 26 | 27 | // check user password. 28 | func (u *User) CheckPassword(pwd string) bool { 29 | return utils.Sha1(pwd+"xxxxx") == u.Password 30 | } 31 | 32 | // change user email. 33 | // check unique. 34 | func (u *User) ChangeEmail(email string) bool { 35 | u2 := GetUserByEmail(u.Email) 36 | if u2.Id != u.Id { 37 | return false 38 | } 39 | u.Email = email 40 | return true 41 | } 42 | 43 | // change user password. 44 | func (u *User) ChangePassword(pwd string) { 45 | u.Password = utils.Sha1(pwd + "xxxxx") 46 | } 47 | 48 | // get a user by given id. 49 | func GetUserById(id int) *User { 50 | for _, u := range users { 51 | if u.Id == id { 52 | return u 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | // get a user by given name. 59 | func GetUserByName(name string) *User { 60 | for _, u := range users { 61 | if u.Name == name { 62 | return u 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | // get a user by given email. 69 | func GetUserByEmail(email string) *User { 70 | for _, u := range users { 71 | if u.Email == email { 72 | return u 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | // get users of given role. 79 | func GetUsersByRole(role string) []*User { 80 | us := make([]*User, 0) 81 | for _, u := range users { 82 | if u.Role == role { 83 | us = append(us, u) 84 | } 85 | } 86 | return us 87 | } 88 | 89 | // create new user. 90 | func CreateUser(u *User) error { 91 | if GetUserByName(u.Email) != nil { 92 | return errors.New("email-repeat") 93 | } 94 | userMaxId += Storage.TimeInc(5) 95 | u.Id = userMaxId 96 | u.CreateTime = utils.Now() 97 | u.LastLoginTime = u.CreateTime 98 | users = append(users, u) 99 | go SyncUsers() 100 | return nil 101 | } 102 | 103 | // remove a user. 104 | func RemoveUser(u *User) { 105 | for i, u2 := range users { 106 | if u2.Id == u.Id { 107 | users = append(users[:i], users[i+1:]...) 108 | break 109 | } 110 | } 111 | go SyncUsers() 112 | } 113 | 114 | // write users to json. 115 | func SyncUsers() { 116 | Storage.Set("users", users) 117 | } 118 | 119 | func LoadUsers() { 120 | users = make([]*User, 0) 121 | userMaxId = 0 122 | Storage.Get("users", &users) 123 | for _, u := range users { 124 | if u.Id > userMaxId { 125 | userMaxId = u.Id 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/model/version.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | var ver *version 4 | 5 | type version struct { 6 | Name string 7 | BuildTime int64 8 | Version int 9 | CurrentVersion int 10 | } 11 | 12 | func loadVersion() { 13 | ver = new(version) 14 | Storage.Get("version", ver) 15 | } 16 | 17 | func GetVersion() *version { 18 | if ver == nil { 19 | loadVersion() 20 | } 21 | return ver 22 | } 23 | 24 | func SyncVersion() { 25 | Storage.Set("version", ver) 26 | } 27 | -------------------------------------------------------------------------------- /app/plugin/hello.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fuxiaohei/GoInk" 6 | "time" 7 | ) 8 | 9 | type HelloPlugin struct { 10 | isActive bool 11 | isHandlerRegistered bool 12 | } 13 | 14 | func init() { 15 | helloPlugin := new(HelloPlugin) 16 | helloPlugin.isActive = true 17 | helloPlugin.isHandlerRegistered = false 18 | register(helloPlugin) 19 | } 20 | 21 | func (p *HelloPlugin) Name() string { 22 | return "Sample Hello Plugin" 23 | } 24 | 25 | func (p *HelloPlugin) Key() string { 26 | return "hello_plugin" 27 | } 28 | 29 | func (p *HelloPlugin) Desc() string { 30 | return "插件样例,页面最后输出执行时间 注释" 31 | } 32 | 33 | func (p *HelloPlugin) ToStorage() map[string]interface{} { 34 | m := make(map[string]interface{}) 35 | m["name"] = p.Name() 36 | m["description"] = p.Desc() 37 | m["is_activate"] = p.isActive 38 | return m 39 | } 40 | 41 | func (p *HelloPlugin) Activate() { 42 | if p.isHandlerRegistered { 43 | p.isActive = true 44 | return 45 | } 46 | fn := func(context *GoInk.Context) { 47 | now := time.Now() 48 | context.On(GoInk.CONTEXT_RENDERED, func() { 49 | if p.isActive { 50 | duration := time.Since(now) 51 | str := fmt.Sprint(duration) 52 | //context.Body = append(context.Body, []byte(str)...) 53 | context.Header["X-Exec-Time"] = str 54 | } 55 | }) 56 | } 57 | Handler("hello_plugin", fn, false) 58 | /*Route("hello_handler", "GET", "/hello/", func(context *GoInk.Context) { 59 | context.Body = []byte("Hello!") 60 | })*/ 61 | p.isHandlerRegistered = true 62 | p.isActive = true 63 | } 64 | 65 | func (p *HelloPlugin) Deactivate() { 66 | p.isActive = false 67 | } 68 | 69 | func (p *HelloPlugin) IsActive() bool { 70 | return p.isActive 71 | } 72 | 73 | func (p *HelloPlugin) Version() string { 74 | return "0.0.1" 75 | } 76 | 77 | func (p *HelloPlugin) HasSetting() bool { 78 | return false 79 | } 80 | 81 | func (p *HelloPlugin) Form() string { 82 | return "" 83 | } 84 | 85 | func (p *HelloPlugin) SetSetting(settings map[string]string) { 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/model" 5 | "github.com/fuxiaohei/GoInk" 6 | ) 7 | 8 | type PluginInterface interface { 9 | Name() string 10 | Key() string 11 | Desc() string 12 | Version() string 13 | 14 | Activate() 15 | Deactivate() 16 | IsActive() bool 17 | 18 | HasSetting() bool 19 | Form() string 20 | SetSetting(settings map[string]string) 21 | 22 | ToStorage() map[string]interface{} 23 | } 24 | 25 | type pluginRoute struct { 26 | Method string 27 | Pattern string 28 | Handler GoInk.Handler 29 | } 30 | 31 | var ( 32 | pluginStorage map[string]map[string]interface{} 33 | pluginMap map[string]PluginInterface 34 | middleHandler map[string]GoInk.Handler 35 | interHandler map[string]GoInk.Handler 36 | usedHandler map[string]map[string]bool 37 | routeHandler map[string]pluginRoute 38 | ) 39 | 40 | func init() { 41 | if pluginMap == nil { 42 | pluginMap = make(map[string]PluginInterface) 43 | } 44 | //pluginMap = make(map[string]PluginInterface) 45 | pluginStorage = make(map[string]map[string]interface{}) 46 | middleHandler = make(map[string]GoInk.Handler) 47 | routeHandler = make(map[string]pluginRoute) 48 | interHandler = make(map[string]GoInk.Handler) 49 | usedHandler = make(map[string]map[string]bool) 50 | usedHandler["middle"] = make(map[string]bool) 51 | usedHandler["inter"] = make(map[string]bool) 52 | usedHandler["route"] = make(map[string]bool) 53 | } 54 | 55 | func Init() { 56 | var isChanged = false 57 | if model.Storage.Has("plugins") { 58 | model.Storage.Get("plugins", &pluginStorage) 59 | } 60 | // activate 61 | for k, p := range pluginMap { 62 | _, ok := pluginStorage[k] 63 | if !ok { 64 | pluginStorage[k] = p.ToStorage() 65 | isChanged = true 66 | } 67 | if pluginStorage[k]["is_activate"].(bool) { 68 | p.Activate() 69 | } else { 70 | p.Deactivate() 71 | } 72 | } 73 | // clean deleted 74 | for k, _ := range pluginStorage { 75 | if pluginMap[k] == nil { 76 | delete(pluginStorage, k) 77 | isChanged = true 78 | } 79 | } 80 | if isChanged { 81 | model.Storage.Set("plugins", pluginStorage) 82 | } 83 | } 84 | 85 | func register(plugin PluginInterface) { 86 | if pluginMap == nil { 87 | pluginMap = make(map[string]PluginInterface) 88 | } 89 | pluginMap[plugin.Key()] = plugin 90 | } 91 | 92 | func Handler(name string, h GoInk.Handler, inter bool) { 93 | if inter { 94 | interHandler[name] = h 95 | } else { 96 | middleHandler[name] = h 97 | } 98 | } 99 | 100 | func Route(name string, method string, pattern string, handler GoInk.Handler) { 101 | pr := pluginRoute{} 102 | pr.Method = method 103 | pr.Handler = handler 104 | pr.Pattern = pattern 105 | routeHandler[name] = pr 106 | } 107 | 108 | func Handlers() (map[string]map[string]GoInk.Handler, map[string]pluginRoute) { 109 | m := make(map[string]map[string]GoInk.Handler) 110 | m["middle"] = middleHandler 111 | m["inter"] = interHandler 112 | return m, routeHandler 113 | } 114 | 115 | func GetPlugins() map[string]PluginInterface { 116 | return pluginMap 117 | } 118 | 119 | func GetPluginByKey(key string) PluginInterface { 120 | return pluginMap[key] 121 | } 122 | 123 | func Activate(name string) { 124 | p, ok := pluginMap[name] 125 | if !ok { 126 | println("activate null plugin " + name) 127 | return 128 | } 129 | p.Activate() 130 | pluginStorage[p.Key()] = p.ToStorage() 131 | model.Storage.Set("plugins", pluginStorage) 132 | println("activate", p.Key()) 133 | } 134 | 135 | func Deactivate(name string) { 136 | p, ok := pluginMap[name] 137 | if !ok { 138 | println("deactivate null plugin " + name) 139 | return 140 | } 141 | p.Deactivate() 142 | pluginStorage[p.Key()] = p.ToStorage() 143 | model.Storage.Set("plugins", pluginStorage) 144 | println("deactivate", p.Key()) 145 | } 146 | 147 | func Update(app *GoInk.App) { 148 | pluginHandlers, routeHandlers := Handlers() 149 | 150 | if len(routeHandlers) > 0 { 151 | for n, h := range routeHandlers { 152 | if usedHandler["route"][n] { 153 | continue 154 | } 155 | app.Route(h.Method, h.Pattern, h.Handler) 156 | usedHandler["route"][n] = true 157 | } 158 | } 159 | 160 | if len(pluginHandlers["middle"]) > 0 { 161 | for n, h := range pluginHandlers["middle"] { 162 | if usedHandler["middle"][n] { 163 | continue 164 | } 165 | app.Use(h) 166 | usedHandler["middle"][n] = true 167 | //println("use plugin middle handler",n) 168 | } 169 | //fmt.Println(usedHandler) 170 | } 171 | 172 | if len(pluginHandlers["inter"]) > 0 { 173 | for name, h := range pluginHandlers["inter"] { 174 | if usedHandler["inter"][name] { 175 | continue 176 | } 177 | if name == "static" { 178 | app.Static(h) 179 | usedHandler["inter"][name] = true 180 | continue 181 | } 182 | if name == "recover" { 183 | app.Recover(h) 184 | usedHandler["inter"][name] = true 185 | continue 186 | } 187 | if name == "notfound" { 188 | app.NotFound(h) 189 | usedHandler["inter"][name] = true 190 | continue 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /app/upgrade/v20140130.go: -------------------------------------------------------------------------------- 1 | package upgrade 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/cmd" 5 | "github.com/fuxiaohei/GoBlog/app/model" 6 | "github.com/fuxiaohei/GoBlog/app/plugin" 7 | "github.com/fuxiaohei/GoInk" 8 | "os" 9 | "path" 10 | ) 11 | 12 | func init() { 13 | cmd.SetUpgradeScript(20140130, upgrade_20140130) 14 | } 15 | 16 | func upgrade_20140130(app *GoInk.App) bool { 17 | 18 | // change settings 19 | model.LoadSettings() 20 | model.SetSetting("c_footer_ga", "") 21 | model.SetSetting("enable_go_markdown", "false") 22 | model.SetSetting("enable_go_markdown_def", "false") 23 | model.SetSetting("site_theme", "default") 24 | model.SetSetting("site_theme_def", "default") 25 | model.SetSetting("c_home_avatar", "/static/img/site.png") 26 | model.SyncSettings() 27 | 28 | // init plugin 29 | plugin.Init() 30 | model.Storage.Dir("plugin") 31 | 32 | // remove static files 33 | os.RemoveAll(app.Get("view_dir")) 34 | os.RemoveAll(path.Join(app.Get("static_dir"), "less")) 35 | os.RemoveAll(path.Join(app.Get("static_dir"), "css")) 36 | os.RemoveAll(path.Join(app.Get("static_dir"), "img")) 37 | os.RemoveAll(path.Join(app.Get("static_dir"), "js")) 38 | os.RemoveAll(path.Join(app.Get("static_dir"), "lib")) 39 | os.Remove(path.Join(app.Get("static_dir"), "favicon.ico")) 40 | 41 | // extract current static files 42 | cmd.ExtractBundleBytes() 43 | 44 | // "c_footer_ga": "", 45 | // "enable_go_markdown": "true", 46 | // "enable_go_markdown_def": "false", 47 | // "site_theme": "ling", 48 | // "site_theme_def": "default", 49 | return true 50 | } 51 | -------------------------------------------------------------------------------- /app/upgrade/v20140131.go: -------------------------------------------------------------------------------- 1 | package upgrade 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/cmd" 5 | "github.com/fuxiaohei/GoInk" 6 | "os" 7 | "path" 8 | ) 9 | 10 | func init() { 11 | cmd.SetUpgradeScript(20140131, upgrade_20140131) 12 | } 13 | 14 | func upgrade_20140131(app *GoInk.App) bool { 15 | 16 | // re-write all data to non-indent json 17 | /*model.All() 18 | model.SyncContents() 19 | model.SyncFiles() 20 | model.SyncReaders() 21 | model.SyncSettings() 22 | model.SyncTokens() 23 | model.SyncUsers() 24 | model.SyncVersion()*/ 25 | 26 | // update ling template 27 | os.RemoveAll(path.Join(app.Get("view_dir"), "ling")) 28 | cmd.ExtractBundleBytes() 29 | 30 | return true 31 | } 32 | -------------------------------------------------------------------------------- /app/upgrade/v20140209.go: -------------------------------------------------------------------------------- 1 | package upgrade 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/cmd" 5 | "github.com/fuxiaohei/GoBlog/app/model" 6 | "github.com/fuxiaohei/GoInk" 7 | "os" 8 | "path" 9 | ) 10 | 11 | func init() { 12 | cmd.SetUpgradeScript(20140209, upgrade_20140209) 13 | } 14 | 15 | func upgrade_20140209(app *GoInk.App) bool { 16 | // clean template 17 | vDir := app.Get("view_dir") 18 | os.Remove(path.Join(vDir, "admin.layout")) 19 | os.Remove(path.Join(vDir, "cmd.layout")) 20 | 21 | // write default menu setting 22 | model.DefaultNavigators() 23 | 24 | // write message storage 25 | model.Storage.Set("messages",[]*model.Message{}) 26 | 27 | cmd.ExtractBundleBytes() 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /app/upgrade/v20140228.go: -------------------------------------------------------------------------------- 1 | package upgrade 2 | 3 | import ( 4 | "github.com/fuxiaohei/GoBlog/app/cmd" 5 | "github.com/fuxiaohei/GoBlog/app/model" 6 | "github.com/fuxiaohei/GoInk" 7 | ) 8 | 9 | func init() { 10 | cmd.SetUpgradeScript(20140228, upgrade_20140228) 11 | } 12 | 13 | func upgrade_20140228(_ *GoInk.App) bool { 14 | 15 | // change settings 16 | model.LoadSettings() 17 | model.SetSetting("popular_size", "4") 18 | model.SetSetting("recent_comment_size", "3") 19 | model.SetSetting("theme_cache", "false") 20 | model.SyncSettings() 21 | 22 | // overwrite zip bundle bytes 23 | cmd.ExtractBundleBytes() 24 | return true 25 | } 26 | -------------------------------------------------------------------------------- /app/utils/avatar.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | ) 7 | 8 | // generate gravatar link by email. 9 | func Gravatar(email string, size string) string { 10 | u := "http://1.gravatar.com/avatar/" 11 | u += encodeAvatarEmail(email) + "?s=" + size 12 | return u 13 | } 14 | 15 | // encode user password by sha1 with salt string from config. 16 | func encodeAvatarEmail(email string) string { 17 | h := md5.New() 18 | h.Write([]byte(email)) 19 | bs := h.Sum(nil) 20 | return fmt.Sprintf("%x", bs) 21 | } 22 | -------------------------------------------------------------------------------- /app/utils/crypto.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | func Sha1(raw string) string { 10 | t := sha1.New() 11 | io.WriteString(t, raw) 12 | return fmt.Sprintf("%x", t.Sum(nil)) 13 | } 14 | -------------------------------------------------------------------------------- /app/utils/date.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Format unix time int64 to string 11 | func DateInt64(ti int64, format string) string { 12 | t := time.Unix(int64(ti), 0) 13 | return DateTime(t, format) 14 | } 15 | 16 | // Format unix time string to string 17 | func DateString(ts string, format string) string { 18 | i, _ := strconv.ParseInt(ts, 10, 64) 19 | return DateInt64(i, format) 20 | } 21 | 22 | // Format time.Time struct to string 23 | // MM - month - 01 24 | // M - month - 1, single bit 25 | // DD - day - 02 26 | // D - day 2 27 | // YYYY - year - 2006 28 | // YY - year - 06 29 | // HH - 24 hours - 03 30 | // H - 24 hours - 3 31 | // hh - 12 hours - 03 32 | // h - 12 hours - 3 33 | // mm - minute - 04 34 | // m - minute - 4 35 | // ss - second - 05 36 | // s - second = 5 37 | func DateTime(t time.Time, format string) string { 38 | res := strings.Replace(format, "MM", t.Format("01"), -1) 39 | res = strings.Replace(res, "M", t.Format("1"), -1) 40 | res = strings.Replace(res, "DD", t.Format("02"), -1) 41 | res = strings.Replace(res, "D", t.Format("2"), -1) 42 | res = strings.Replace(res, "YYYY", t.Format("2006"), -1) 43 | res = strings.Replace(res, "YY", t.Format("06"), -1) 44 | res = strings.Replace(res, "HH", fmt.Sprintf("%02d", t.Hour()), -1) 45 | res = strings.Replace(res, "H", fmt.Sprintf("%d", t.Hour()), -1) 46 | res = strings.Replace(res, "hh", t.Format("03"), -1) 47 | res = strings.Replace(res, "h", t.Format("3"), -1) 48 | res = strings.Replace(res, "mm", t.Format("04"), -1) 49 | res = strings.Replace(res, "m", t.Format("4"), -1) 50 | res = strings.Replace(res, "ss", t.Format("05"), -1) 51 | res = strings.Replace(res, "s", t.Format("5"), -1) 52 | return res 53 | } 54 | 55 | // Get unix stamp int64 of now 56 | func Now() int64 { 57 | return time.Now().Unix() 58 | } 59 | -------------------------------------------------------------------------------- /app/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func FileSize(size int64) string { 9 | s := float64(size) 10 | if s > 1024*1024 { 11 | return fmt.Sprintf("%.1f M", s/(1024*1024)) 12 | } 13 | if s > 1024 { 14 | return fmt.Sprintf("%.1f K", s/1024) 15 | } 16 | return fmt.Sprintf("%f B", s) 17 | } 18 | 19 | func IsFile(path string) bool { 20 | f, e := os.Stat(path) 21 | if e != nil { 22 | return false 23 | } 24 | if f.IsDir() { 25 | return false 26 | } 27 | return true 28 | } 29 | 30 | func IsDir(path string) bool { 31 | f, e := os.Stat(path) 32 | if e != nil { 33 | return false 34 | } 35 | return f.IsDir() 36 | } 37 | -------------------------------------------------------------------------------- /app/utils/html.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/russross/blackfriday" 5 | "html/template" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | func Html2str(html string) string { 11 | src := string(html) 12 | 13 | //将HTML标签全转换成小写 14 | re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") 15 | src = re.ReplaceAllStringFunc(src, strings.ToLower) 16 | 17 | //去除STYLE 18 | re, _ = regexp.Compile("\\ 58 | 59 | 60 |
63 | This is a XML Sitemap which is supposed to be processed by search engines like Google, MSN Search and YAHOO.
64 |
65 | You can find more information about XML sitemaps on sitemaps.org and Google's list of sitemap programs.
66 |
URL | 72 |Priority | 73 |Change Frequency | 74 |LastChange (GMT) | 75 |
---|---|---|---|
84 | |
91 |
92 | |
94 |
95 | |
97 |
98 | |
100 |