main.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. // Copyright (c) [2022] [巴拉迪维 BaratSemet]
  2. // [ohUrlShortener] is licensed under Mulan PSL v2.
  3. // You can use this software according to the terms and conditions of the Mulan PSL v2.
  4. // You may obtain a copy of Mulan PSL v2 at:
  5. // http://license.coscl.org.cn/MulanPSL2
  6. // THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
  7. // See the Mulan PSL v2 for more details.
  8. package main
  9. import (
  10. "embed"
  11. "flag"
  12. "fmt"
  13. "html/template"
  14. "io/fs"
  15. "log"
  16. "net/http"
  17. "os"
  18. "strings"
  19. "time"
  20. "ohurlshortener/controller"
  21. "ohurlshortener/service"
  22. "ohurlshortener/storage"
  23. "ohurlshortener/utils"
  24. "github.com/Masterminds/sprig"
  25. "github.com/dchest/captcha"
  26. "github.com/gin-gonic/gin"
  27. "golang.org/x/sync/errgroup"
  28. )
  29. const (
  30. WebReadTimeout = 15 * time.Second
  31. WebWriteTimeout = 15 * time.Second
  32. // AccessLogCleanInterval 清理 Redis 中的访问日志的时间间隔
  33. AccessLogCleanInterval = 1 * time.Minute
  34. // Top25CalcInterval Top25 榜单计算间隔
  35. Top25CalcInterval = 5 * time.Minute
  36. // StatsSumCalcInterval 仪表盘页面中其他几个统计数据计算间隔
  37. StatsSumCalcInterval = 5 * time.Minute
  38. // StatsIpSumCalcInterval 全部访问日志分析统计的间隔
  39. StatsIpSumCalcInterval = 30 * time.Minute
  40. )
  41. var (
  42. //go:embed assets/* templates/*
  43. FS embed.FS
  44. group errgroup.Group
  45. cmdStart string
  46. cmdConfig string
  47. )
  48. func main() {
  49. flag.StringVar(&cmdStart, "s", "", "starts ohUrlShortener service: admin | portal ")
  50. flag.StringVar(&cmdConfig, "c", "config.ini", "config file path")
  51. flag.Usage = func() {
  52. fmt.Fprintf(os.Stdout, `ohUrlShortener version:%s
  53. Usage: ohurlshortener [-s admin|portal|<omit to start both>] [-c config_file_path]`, utils.Version)
  54. flag.PrintDefaults()
  55. }
  56. flag.Parse()
  57. initSettings()
  58. portalRoutes, err := initPortalRoutes()
  59. utils.ExitOnError("Portal Routes initialization failed.", err)
  60. adminRoutes, err := initAdminRoutes()
  61. utils.ExitOnError("Admin Routes initialization failed.", err)
  62. portal := &http.Server{
  63. Addr: fmt.Sprintf(":%d", utils.AppConfig.Port),
  64. Handler: portalRoutes,
  65. ReadTimeout: WebReadTimeout,
  66. WriteTimeout: WebWriteTimeout,
  67. }
  68. admin := &http.Server{
  69. Addr: fmt.Sprintf(":%d", utils.AppConfig.AdminPort),
  70. Handler: adminRoutes,
  71. ReadTimeout: WebReadTimeout,
  72. WriteTimeout: WebWriteTimeout,
  73. }
  74. if strings.EqualFold("admin", strings.TrimSpace(cmdStart)) {
  75. startAdmin(group, *admin)
  76. } else if strings.EqualFold("portal", strings.TrimSpace(cmdStart)) {
  77. startPortal(group, *portal)
  78. } else if utils.EmptyString(cmdStart) {
  79. startPortal(group, *portal)
  80. startAdmin(group, *admin)
  81. } else {
  82. flag.Usage()
  83. }
  84. err = group.Wait()
  85. utils.ExitOnError("Group failed,", err)
  86. }
  87. func initSettings() {
  88. _, err := utils.InitConfig(cmdConfig)
  89. utils.ExitOnError("Config initialization failed.", err)
  90. rs, err := storage.InitRedisService()
  91. utils.ExitOnError("Redis initialization failed.", err)
  92. if strings.EqualFold("redis", strings.ToLower(utils.CaptchaConfig.Store)) {
  93. crs := storage.CaptchaRedisStore{KeyPrefix: "oh_captcha", Expiration: 1 * time.Minute, RedisService: rs}
  94. captcha.SetCustomStore(&crs)
  95. }
  96. _, err = storage.InitDatabaseService()
  97. storage.CallProcedureStatsIPSum() //recalculate when ohUrlShortener starts
  98. storage.CallProcedureStatsTop25() // recalculate when ohUrlShortener starts
  99. storage.CallProcedureStatsSum() // recalculate when ohUrlShortener starts
  100. utils.ExitOnError("Database initialization failed.", err)
  101. err = service.StoreAccessLogs()
  102. utils.PrintOnError("StoreAccessLogs failed.", err)
  103. _, err = service.ReloadUrls()
  104. utils.PrintOnError("Reload urls failed.", err)
  105. err = service.ReloadUsers()
  106. utils.PrintOnError("Reload users failed.", err)
  107. }
  108. func startPortal(g errgroup.Group, server http.Server) {
  109. group.Go(func() error {
  110. log.Println("[StoreAccessLog] ticker starts to serve")
  111. return startAccessLogsTicker()
  112. })
  113. group.Go(func() error {
  114. log.Printf("[ohUrlShortener] portal starts at http://localhost:%d", utils.AppConfig.Port)
  115. return server.ListenAndServe()
  116. })
  117. }
  118. func startAdmin(g errgroup.Group, server http.Server) {
  119. group.Go(func() error {
  120. log.Println("[Top25Urls] ticker starts to serve")
  121. return startTop25StatsTicker()
  122. })
  123. group.Go(func() error {
  124. log.Println("[StatsIpSum] ticker starts to serve")
  125. return startIPSumStatsTicker()
  126. })
  127. group.Go(func() error {
  128. log.Println("[StatsSum] ticker starts to serve")
  129. return startSumStatsTicker()
  130. })
  131. group.Go(func() error {
  132. log.Printf("[ohUrlShortener] admin starts at http://localhost:%d", utils.AppConfig.AdminPort)
  133. return server.ListenAndServe()
  134. })
  135. }
  136. func initPortalRoutes() (http.Handler, error) {
  137. if utils.AppConfig.Debug {
  138. gin.SetMode(gin.DebugMode)
  139. } else {
  140. gin.SetMode(gin.ReleaseMode)
  141. }
  142. router := gin.New()
  143. router.Use(gin.Recovery(), controller.WebLogFormatHandler("Portal"))
  144. sub, err := fs.Sub(FS, "assets")
  145. if err != nil {
  146. return nil, err
  147. }
  148. router.StaticFS("/assets", http.FS(sub))
  149. tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseFS(FS, "templates/*.html")
  150. if err != nil {
  151. return nil, err
  152. }
  153. router.SetHTMLTemplate(tmpl)
  154. router.GET("/:url", controller.ShortUrlDetail)
  155. router.NoRoute(func(ctx *gin.Context) {
  156. ctx.HTML(http.StatusNotFound, "error.html", gin.H{
  157. "title": "404 - ohUrlShortener",
  158. "message": "您访问的页面已失效",
  159. "code": http.StatusNotFound,
  160. "label": "Error",
  161. })
  162. })
  163. return router, nil
  164. } // end of initPortalRoutes
  165. func initAdminRoutes() (http.Handler, error) {
  166. if utils.AppConfig.Debug {
  167. gin.SetMode(gin.DebugMode)
  168. } else {
  169. gin.SetMode(gin.ReleaseMode)
  170. }
  171. router := gin.New()
  172. router.Use(gin.Recovery(), controller.WebLogFormatHandler("Admin"))
  173. sub, err := fs.Sub(FS, "assets")
  174. if err != nil {
  175. return nil, err
  176. }
  177. router.StaticFS("/assets", http.FS(sub))
  178. tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseFS(FS, "templates/**/*.html")
  179. if err != nil {
  180. return nil, err
  181. }
  182. router.SetHTMLTemplate(tmpl)
  183. router.GET("/", func(ctx *gin.Context) {
  184. ctx.Redirect(http.StatusTemporaryRedirect, "/login")
  185. })
  186. router.GET("/login", controller.LoginPage)
  187. router.POST("/login", controller.DoLogin)
  188. router.GET("/captcha/:imageId", controller.ServeCaptchaImage)
  189. router.POST("/captcha", controller.RequestCaptchaImage)
  190. admin := router.Group("/admin", controller.AdminAuthHandler())
  191. admin.GET("/", func(ctx *gin.Context) {
  192. ctx.Redirect(http.StatusTemporaryRedirect, "/admin/dashboard")
  193. })
  194. admin.POST("/logout", controller.DoLogout)
  195. admin.GET("/dashboard", controller.DashboardPage)
  196. admin.GET("/urls", controller.UrlsPage)
  197. admin.GET("/stats", controller.StatsPage)
  198. admin.GET("/search_stats", controller.SearchStatsPage)
  199. admin.GET("/access_logs", controller.AccessLogsPage)
  200. admin.POST("/urls/generate", controller.GenerateShortUrl)
  201. admin.POST("/urls/state", controller.ChangeState)
  202. admin.POST("/urls/delete", controller.DeleteShortUrl)
  203. admin.POST("/access_logs_export", controller.AccessLogsExport)
  204. admin.GET("/users", controller.UsersPage)
  205. api := router.Group("/api", controller.APIAuthHandler())
  206. api.POST("/account", controller.APINewAdmin)
  207. api.PUT("/account/:account/update", controller.APIAdminUpdate)
  208. api.POST("/url", controller.APIGenShortUrl)
  209. api.GET("/url/:url", controller.APIUrlInfo)
  210. api.DELETE("/url/:url", controller.APIDeleteUrl)
  211. api.PUT("/url/:url/change_state", controller.APIUpdateUrl)
  212. router.NoRoute(func(ctx *gin.Context) {
  213. ctx.HTML(http.StatusNotFound, "error.html", gin.H{
  214. "title": "404 - ohUrlShortener",
  215. "message": "您访问的页面已失效",
  216. "code": http.StatusNotFound,
  217. "label": "Error",
  218. })
  219. })
  220. return router, nil
  221. } // end of initAdminRoutes
  222. func startAccessLogsTicker() error {
  223. redisTicker := time.NewTicker(AccessLogCleanInterval)
  224. for range redisTicker.C {
  225. log.Println("[StoreAccessLog] Start.")
  226. if err := service.StoreAccessLogs(); err != nil {
  227. log.Printf("Error while trying to store access_log %s", err)
  228. }
  229. log.Println("[StoreAccessLog] Finish.")
  230. }
  231. return nil
  232. }
  233. func startTop25StatsTicker() error {
  234. top25Ticker := time.NewTicker(Top25CalcInterval)
  235. for range top25Ticker.C {
  236. log.Println("[Top25Urls Ticker] Start.")
  237. if err := storage.CallProcedureStatsTop25(); err != nil {
  238. log.Printf("Error while trying to calculate Top25Urls %s", err)
  239. }
  240. log.Println("[Top25Urls Ticker] Finish.")
  241. }
  242. return nil
  243. }
  244. func startIPSumStatsTicker() error {
  245. statsIpSumTicker := time.NewTicker(StatsIpSumCalcInterval)
  246. for range statsIpSumTicker.C {
  247. log.Println("[StatsIpSum Ticker] Start.")
  248. if err := storage.CallProcedureStatsIPSum(); err != nil {
  249. log.Printf("Error while trying to calculate StatsIpSum %s", err)
  250. }
  251. log.Println("[StatsIpSum Ticker] Finish.")
  252. }
  253. return nil
  254. }
  255. func startSumStatsTicker() error {
  256. statsSumTicker := time.NewTicker(StatsSumCalcInterval)
  257. for range statsSumTicker.C {
  258. log.Println("[StatsSum Ticker] Start.")
  259. if err := storage.CallProcedureStatsSum(); err != nil {
  260. log.Printf("Error while trying to calculate StatsSum %s", err)
  261. }
  262. log.Println("[StatsSum Ticker] Finish.")
  263. }
  264. return nil
  265. }