Files
wiki/wiki.go
2023-06-26 15:21:31 +08:00

160 lines
3.6 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
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))
}