157 lines
3.5 KiB
Go
157 lines
3.5 KiB
Go
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
|
|
)
|
|
|
|
// 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() {
|
|
// 加载语言文件
|
|
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:8080 上启动服务
|
|
srv := &http.Server{Addr: ":8080", 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))
|
|
}
|