// Command server is the whole backend: JSON API, share images, static assets // and the HTML shell, in one process. package main import ( "context" _ "embed" "errors" "flag" "log" "net/http" "os" "os/signal" "syscall" "time" // Embeds the zone database so the image needs no tzdata package. Budapest // is load-bearing: it decides what day a campaign closes on. _ "time/tzdata" "github.com/szabadpalya/api/internal/auth" "github.com/szabadpalya/api/internal/config" "github.com/szabadpalya/api/internal/httpapi" "github.com/szabadpalya/api/internal/shell" "github.com/szabadpalya/api/internal/store" ) //go:embed document.html var documentTemplate string func main() { migrateOnly := flag.Bool("migrate", false, "apply migrations and exit") flag.Parse() if err := run(*migrateOnly); err != nil { log.Fatal(err) } } func run(migrateOnly bool) error { cfg, err := config.Load() if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := store.New(ctx, cfg.DatabaseURL) if err != nil { return err } defer db.Close() // Migrations run only when asked. The server must be able to start with // Postgres unreachable and say so through /api/health — a process that // exits instead is exactly the failure the probe exists to report, and it // would take the whole site down over a database blip. if migrateOnly { if err := db.Migrate(ctx); err != nil { return err } log.Println("migrations up to date") return nil } renderer, err := shell.New(documentTemplate, cfg.WebDir, cfg.BaseURL, cfg.Dev) if err != nil { return err } // Housekeeping, not a startup dependency: a dead database must not keep // the process from listening. go func() { if err := db.PurgeExpiredAuth(context.Background()); err != nil { log.Printf("purge expired auth rows: %v", err) } }() srv := &http.Server{ Addr: cfg.Addr, Handler: httpapi.New(cfg, db, auth.New(db, cfg), renderer).Handler(), ReadHeaderTimeout: 10 * time.Second, } // Shut down on a signal rather than being killed: an in-flight save that // is mid-transaction should get the chance to finish. stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) go func() { log.Printf("listening on %s (public %s)", cfg.Addr, cfg.BaseURL) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("serve: %v", err) } }() <-stop log.Println("shutting down") shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 15*time.Second) defer cancelShutdown() return srv.Shutdown(shutdownCtx) }