// Copyright (c) [2022] [巴拉迪维 BaratSemet] // [ohUrlShortener] is licensed under Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: // http://license.coscl.org.cn/MulanPSL2 // 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. // See the Mulan PSL v2 for more details. package main import ( "embed" "flag" "fmt" "html/template" "io/fs" "log" "net/http" "os" "strings" "time" "ohurlshortener/controller" "ohurlshortener/service" "ohurlshortener/storage" "ohurlshortener/utils" "github.com/Masterminds/sprig" "github.com/dchest/captcha" "github.com/gin-gonic/gin" "golang.org/x/sync/errgroup" ) const ( WebReadTimeout = 15 * time.Second WebWriteTimeout = 15 * time.Second // AccessLogCleanInterval 清理 Redis 中的访问日志的时间间隔 AccessLogCleanInterval = 1 * time.Minute // Top25CalcInterval Top25 榜单计算间隔 Top25CalcInterval = 5 * time.Minute // StatsSumCalcInterval 仪表盘页面中其他几个统计数据计算间隔 StatsSumCalcInterval = 5 * time.Minute // StatsIpSumCalcInterval 全部访问日志分析统计的间隔 StatsIpSumCalcInterval = 30 * time.Minute ) var ( //go:embed assets/* templates/* FS embed.FS group errgroup.Group cmdStart string cmdConfig string ) func main() { flag.StringVar(&cmdStart, "s", "", "starts ohUrlShortener service: admin | portal ") flag.StringVar(&cmdConfig, "c", "config.ini", "config file path") flag.Usage = func() { fmt.Fprintf(os.Stdout, `ohUrlShortener version:%s Usage: ohurlshortener [-s admin|portal|] [-c config_file_path]`, utils.Version) flag.PrintDefaults() } flag.Parse() initSettings() portalRoutes, err := initPortalRoutes() utils.ExitOnError("Portal Routes initialization failed.", err) adminRoutes, err := initAdminRoutes() utils.ExitOnError("Admin Routes initialization failed.", err) portal := &http.Server{ Addr: fmt.Sprintf(":%d", utils.AppConfig.Port), Handler: portalRoutes, ReadTimeout: WebReadTimeout, WriteTimeout: WebWriteTimeout, } admin := &http.Server{ Addr: fmt.Sprintf(":%d", utils.AppConfig.AdminPort), Handler: adminRoutes, ReadTimeout: WebReadTimeout, WriteTimeout: WebWriteTimeout, } if strings.EqualFold("admin", strings.TrimSpace(cmdStart)) { startAdmin(group, *admin) } else if strings.EqualFold("portal", strings.TrimSpace(cmdStart)) { startPortal(group, *portal) } else if utils.EmptyString(cmdStart) { startPortal(group, *portal) startAdmin(group, *admin) } else { flag.Usage() } err = group.Wait() utils.ExitOnError("Group failed,", err) } func initSettings() { _, err := utils.InitConfig(cmdConfig) utils.ExitOnError("Config initialization failed.", err) rs, err := storage.InitRedisService() utils.ExitOnError("Redis initialization failed.", err) if strings.EqualFold("redis", strings.ToLower(utils.CaptchaConfig.Store)) { crs := storage.CaptchaRedisStore{KeyPrefix: "oh_captcha", Expiration: 1 * time.Minute, RedisService: rs} captcha.SetCustomStore(&crs) } _, err = storage.InitDatabaseService() storage.CallProcedureStatsIPSum() //recalculate when ohUrlShortener starts storage.CallProcedureStatsTop25() // recalculate when ohUrlShortener starts storage.CallProcedureStatsSum() // recalculate when ohUrlShortener starts utils.ExitOnError("Database initialization failed.", err) err = service.StoreAccessLogs() utils.PrintOnError("StoreAccessLogs failed.", err) _, err = service.ReloadUrls() utils.PrintOnError("Reload urls failed.", err) err = service.ReloadUsers() utils.PrintOnError("Reload users failed.", err) } func startPortal(g errgroup.Group, server http.Server) { group.Go(func() error { log.Println("[StoreAccessLog] ticker starts to serve") return startAccessLogsTicker() }) group.Go(func() error { log.Printf("[ohUrlShortener] portal starts at http://localhost:%d", utils.AppConfig.Port) return server.ListenAndServe() }) } func startAdmin(g errgroup.Group, server http.Server) { group.Go(func() error { log.Println("[Top25Urls] ticker starts to serve") return startTop25StatsTicker() }) group.Go(func() error { log.Println("[StatsIpSum] ticker starts to serve") return startIPSumStatsTicker() }) group.Go(func() error { log.Println("[StatsSum] ticker starts to serve") return startSumStatsTicker() }) group.Go(func() error { log.Printf("[ohUrlShortener] admin starts at http://localhost:%d", utils.AppConfig.AdminPort) return server.ListenAndServe() }) } func initPortalRoutes() (http.Handler, error) { if utils.AppConfig.Debug { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } router := gin.New() router.Use(gin.Recovery(), controller.WebLogFormatHandler("Portal")) sub, err := fs.Sub(FS, "assets") if err != nil { return nil, err } router.StaticFS("/assets", http.FS(sub)) tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseFS(FS, "templates/*.html") if err != nil { return nil, err } router.SetHTMLTemplate(tmpl) router.GET("/:url", controller.ShortUrlDetail) router.NoRoute(func(ctx *gin.Context) { ctx.HTML(http.StatusNotFound, "error.html", gin.H{ "title": "404 - ohUrlShortener", "message": "您访问的页面已失效", "code": http.StatusNotFound, "label": "Error", }) }) return router, nil } // end of initPortalRoutes func initAdminRoutes() (http.Handler, error) { if utils.AppConfig.Debug { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } router := gin.New() router.Use(gin.Recovery(), controller.WebLogFormatHandler("Admin")) sub, err := fs.Sub(FS, "assets") if err != nil { return nil, err } router.StaticFS("/assets", http.FS(sub)) tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseFS(FS, "templates/**/*.html") if err != nil { return nil, err } router.SetHTMLTemplate(tmpl) router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusTemporaryRedirect, "/login") }) router.GET("/login", controller.LoginPage) router.POST("/login", controller.DoLogin) router.GET("/captcha/:imageId", controller.ServeCaptchaImage) router.POST("/captcha", controller.RequestCaptchaImage) admin := router.Group("/admin", controller.AdminAuthHandler()) admin.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusTemporaryRedirect, "/admin/dashboard") }) admin.POST("/logout", controller.DoLogout) admin.GET("/dashboard", controller.DashboardPage) admin.GET("/urls", controller.UrlsPage) admin.GET("/stats", controller.StatsPage) admin.GET("/search_stats", controller.SearchStatsPage) admin.GET("/access_logs", controller.AccessLogsPage) admin.POST("/urls/generate", controller.GenerateShortUrl) admin.POST("/urls/state", controller.ChangeState) admin.POST("/urls/delete", controller.DeleteShortUrl) admin.POST("/access_logs_export", controller.AccessLogsExport) admin.GET("/users", controller.UsersPage) api := router.Group("/api", controller.APIAuthHandler()) api.POST("/account", controller.APINewAdmin) api.PUT("/account/:account/update", controller.APIAdminUpdate) api.POST("/url", controller.APIGenShortUrl) api.GET("/url/:url", controller.APIUrlInfo) api.DELETE("/url/:url", controller.APIDeleteUrl) api.PUT("/url/:url/change_state", controller.APIUpdateUrl) router.NoRoute(func(ctx *gin.Context) { ctx.HTML(http.StatusNotFound, "error.html", gin.H{ "title": "404 - ohUrlShortener", "message": "您访问的页面已失效", "code": http.StatusNotFound, "label": "Error", }) }) return router, nil } // end of initAdminRoutes func startAccessLogsTicker() error { redisTicker := time.NewTicker(AccessLogCleanInterval) for range redisTicker.C { log.Println("[StoreAccessLog] Start.") if err := service.StoreAccessLogs(); err != nil { log.Printf("Error while trying to store access_log %s", err) } log.Println("[StoreAccessLog] Finish.") } return nil } func startTop25StatsTicker() error { top25Ticker := time.NewTicker(Top25CalcInterval) for range top25Ticker.C { log.Println("[Top25Urls Ticker] Start.") if err := storage.CallProcedureStatsTop25(); err != nil { log.Printf("Error while trying to calculate Top25Urls %s", err) } log.Println("[Top25Urls Ticker] Finish.") } return nil } func startIPSumStatsTicker() error { statsIpSumTicker := time.NewTicker(StatsIpSumCalcInterval) for range statsIpSumTicker.C { log.Println("[StatsIpSum Ticker] Start.") if err := storage.CallProcedureStatsIPSum(); err != nil { log.Printf("Error while trying to calculate StatsIpSum %s", err) } log.Println("[StatsIpSum Ticker] Finish.") } return nil } func startSumStatsTicker() error { statsSumTicker := time.NewTicker(StatsSumCalcInterval) for range statsSumTicker.C { log.Println("[StatsSum Ticker] Start.") if err := storage.CallProcedureStatsSum(); err != nil { log.Printf("Error while trying to calculate StatsSum %s", err) } log.Println("[StatsSum Ticker] Finish.") } return nil }