package main import ( "embed" "flag" "fmt" "io/fs" "log/slog" "net/http" "os" "time" "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/handlers" "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/middleware" "git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository" "github.com/joho/godotenv" _ "modernc.org/sqlite" ) //go:embed web/static/* var static embed.FS func init() { _, jsonLogger := os.LookupEnv("JSON_LOGGER") _, debug := os.LookupEnv("DEBUG") var programLevel slog.Level if debug { programLevel = slog.LevelDebug } programLevel = slog.LevelDebug if jsonLogger { jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: programLevel, }) slog.SetDefault(slog.New(jsonHandler)) } else { textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: programLevel, }) slog.SetDefault(slog.New(textHandler)) } slog.Info("Logger initialized", slog.Bool("debug", debug)) } func main() { var flagMigrateDB, flagRevertDB, flagForceDB bool flag.BoolVar(&flagMigrateDB, "migrate-db", false, "Migrate database to supported version and exit") flag.BoolVar(&flagRevertDB, "revert-db", false, "Migrate database to previous version and exit") flag.BoolVar(&flagForceDB, "force-db", false, "Force database version, clear dirty flag and exit") err := godotenv.Load() if err != nil { slog.Error("Could not parse existing .env file at location './.env'. Application startup failed, exited.\nError: %s\n", "Error", err.Error()) } dbURL := os.Getenv("DB") if dbURL == "" { dbURL = "file:app.db" } if flagMigrateDB { err := repository.MigrateDB(dbURL) if err != nil { slog.Error("MigrateDB Failed: Could not migrate database at location.", "version", repository.Version, "error", err) os.Exit(1) } slog.Info("MigrateDB Success: Migrated database at location.\n", "version", repository.Version) } if flagRevertDB { err := repository.RevertDB(dbURL) if err != nil { slog.Error("RevertDB Failed: Could not revert database at location", "version", (repository.Version - 1), "error", err) os.Exit(1) } slog.Info("RevertDB Success: Reverted database", "version", (repository.Version - 1)) } if flagForceDB { err := repository.ForceDB(dbURL) if err != nil { slog.Error("ForceDB Failed: Could not force database version", "version", repository.Version, "error", err) os.Exit(1) } slog.Error("ForceDB Success: Forced database version", "version", repository.Version) } repository.Connect(dbURL) port := os.Getenv("PORT") if port == "" { port = "8080" } addr := ":" + port mux := http.NewServeMux() // Use an embedded filesystem rooted at "web/static" fs, err := fs.Sub(static, "web/static") if err != nil { slog.Error("Failed to create sub filesystem", "error", err) return } // Serve files from the embedded /web/static directory at /static fileServer := http.FileServer(http.FS(fs)) mux.Handle("GET /static/", http.StripPrefix("/static/", fileServer)) mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { data, err := static.ReadFile("web/static/favicon.ico") if err != nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/plain") w.Write(data) }) mux.HandleFunc("GET /robots.txt", func(w http.ResponseWriter, r *http.Request) { data, err := static.ReadFile("web/static/robots.txt") if err != nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/plain") w.Write(data) }) mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte(`OK`)) }) mux.HandleFunc("GET /", handlers.RootHandler()) chain := &middleware.Chain{} chain.Use(middleware.RecoverMiddleware) wrappedMux := chain.Then(mux) server := &http.Server{ Addr: fmt.Sprintf(":%s", port), Handler: wrappedMux, // Recommended timeouts from // https://blog.cloudflare.com/exposing-go-on-the-internet/ ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, } slog.Info("Server listening", "addr", addr) if err := server.ListenAndServe(); err != nil { slog.Error("Server failed to start", "error", err) } }