package main import ( "context" "embed" "fmt" "html/template" "io/fs" "log" "net/http" "os" "os/signal" "path" "path/filepath" "strings" "syscall" _ "time/tzdata" "github.com/fsnotify/fsnotify" "github.com/gin-gonic/gin" "github.com/unknwon/i18n" "golang.org/x/text/language" ) var ( //go:embed assets/*/* views/*.html i18n/*.ini content embed.FS root Menu build string ) // Node 节点模型 type Node struct { Name string `gorm:"size:64;not null"` Icon string `gorm:"size:32;default:fa fa-circlo-o" yaml:",omitempty"` Path string `gorm:"size:255"` Child []Node `gorm:"-" yaml:",omitempty"` Status bool `gorm:"default:false;not null"` } type Menu []Node func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) log.Printf("version %s starting", build) // 加载语言文件 localizer, err := loadI18n("i18n/*.ini") if err != nil { log.Fatalf("load i18n %v", err) } // 加载模板 t, err := template.ParseFS(content, "views/*.html") if err != nil { log.Fatalf("parse template %v", err) } r := gin.New() // 路由日志格式化 r.Use(gin.LoggerWithFormatter(func(p gin.LogFormatterParams) string { return fmt.Sprintf("%s %s %d %s %d %s (%s)\n%s", p.TimeStamp.Format("2006/01/02 15:04:05"), p.Method, p.StatusCode, p.Path, p.BodySize, p.ClientIP, p.Latency, p.ErrorMessage, ) }), localizer, gin.Recovery()) r.SetHTMLTemplate(t) // 静态文件 r.GET("/assets/*filepath", func(c *gin.Context) { c.Header("Cache-Control", "public, max-age=3600") c.FileFromFS(c.Request.URL.Path, http.FS(content)) }) r.GET("/", func(c *gin.Context) { page := strings.TrimSpace(c.Query("page")) if len(page) == 0 { c.Set("Notify", make([]string, 5)) c.Set("Menu", root) c.HTML(http.StatusOK, "layout.html", c.Keys) return } c.Set("Code", http.StatusNotFound) c.HTML(http.StatusOK, "404.html", c.Keys) }) ctx, cancel := context.WithCancel(context.Background()) if err = watchDocs(ctx, "page/*md"); err != nil { log.Fatalf("load markdown %v", err) } // 监听并在 0.0.0.0:8088 上启动服务 srv := &http.Server{Addr: ":8088", Handler: r} srv.RegisterOnShutdown(cancel) // 监听进程退出 sigint := make(chan os.Signal, 1) signal.Notify(sigint, syscall.SIGTERM, syscall.SIGINT) go srv.ListenAndServe() log.Printf("Listening and serving HTTP on %s", srv.Addr) <-sigint if err := srv.Shutdown(context.Background()); err != nil { log.Printf("HTTP server Shutdown: %v", err) } } func loadI18n(pattern string) (gin.HandlerFunc, error) { list, err := fs.Glob(content, pattern) if err != nil { return nil, err } // 加载语言文件 for _, name := range list { f, err := content.ReadFile(name) if err != nil { return nil, err } base, ext := filepath.Base(name), filepath.Ext(name) i18n.SetMessage(strings.TrimSuffix(base, ext), f) } return func(c *gin.Context) { tags, _, _ := language.ParseAcceptLanguage(c.GetHeader("Accept-Language")) local := new(i18n.Locale) c.Set("i18n", local) for _, tag := range tags { local.Lang = tag.String() if i18n.IsExist(local.Lang) { break } } c.Next() }, nil } func watchDocs(ctx context.Context, pattern string) error { // 文件监控 watcher, err := fsnotify.NewWatcher() if err != nil { return fmt.Errorf("create watcher: %w", err) } go func() { for { select { case <-ctx.Done(): log.Print("watch exit") return case e := <-watcher.Events: log.Printf("load %s %v", filepath.Base(e.Name), e.Op) case err := <-watcher.Errors: log.Printf("Watcher error: %v", err) // No need to exit here } } }() return watcher.Add(path.Dir(pattern)) }