package main import ( "context" "embed" "flag" "io" "io/fs" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "zeromesh/internal/config" "zeromesh/internal/controller" "zeromesh/internal/database" "zeromesh/internal/router" ) //go:embed web/dist/index.html web/dist/assets/* var webDist embed.FS func main() { cfgPath := flag.String("config", "config.yaml", "config file path") flag.Parse() cfg, err := config.Load(*cfgPath) if err != nil { slog.Error("load config", "err", err) os.Exit(1) } logLevel := new(slog.LevelVar) switch cfg.Logging.Level { case "debug": logLevel.Set(slog.LevelDebug) case "warn": logLevel.Set(slog.LevelWarn) case "error": logLevel.Set(slog.LevelError) default: logLevel.Set(slog.LevelInfo) } opts := &slog.HandlerOptions{Level: logLevel} var logHandler slog.Handler if cfg.Logging.Format == "json" { logHandler = slog.NewJSONHandler(os.Stderr, opts) } else { logHandler = slog.NewTextHandler(os.Stderr, opts) } logger := slog.New(logHandler) slog.SetDefault(logger) db, err := database.Init(&cfg.Database) if err != nil { slog.Error("init database", "err", err) os.Exit(1) } ctr, err := controller.New(cfg, db, logger) if err != nil { slog.Error("init controller", "err", err) os.Exit(1) } if err := ctr.Start(); err != nil { slog.Error("start controller", "err", err) os.Exit(1) } gin.SetMode(gin.ReleaseMode) ginEngine := router.Setup(cfg, db) webFS, _ := fs.Sub(webDist, "web/dist") // Wrap in a Handler that serves SPA before Gin gets a chance to redirect handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p := r.URL.Path // Serve SPA for root and all non-API paths if p == "/" || p == "/index.html" { w.Header().Set("Content-Type", "text/html; charset=utf-8") data, err := webFS.Open("index.html") if err == nil { http.ServeContent(w, r, "index.html", time.Now(), data.(io.ReadSeeker)) return } } if len(p) >= 8 && p[:8] == "/assets/" { assetFS, _ := fs.Sub(webDist, "web/dist/assets") http.StripPrefix("/assets", http.FileServer(http.FS(assetFS))).ServeHTTP(w, r) return } // API routes go to Gin if len(p) >= 4 && p[:4] == "/api" { ginEngine.ServeHTTP(w, r) return } // SPA fallback for all other routes w.Header().Set("Content-Type", "text/html; charset=utf-8") data, err := webFS.Open("index.html") if err == nil { http.ServeContent(w, r, "index.html", time.Now(), data.(io.ReadSeeker)) return } ginEngine.ServeHTTP(w, r) }) srv := &http.Server{ Addr: cfg.Server.Addr(), Handler: handler, } go func() { slog.Info("server listening", "addr", srv.Addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("serve", "err", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit slog.Info("shutting down...") ctr.Stop() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { slog.Error("shutdown", "err", err) } }