Files
kgpz_web/server/server.go
2025-10-02 00:08:37 +02:00

239 lines
5.7 KiB
Go

package server
import (
"strings"
"sync"
"time"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/logging"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/providers"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/templating"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/storage/memory/v2"
)
const (
// INFO: This timeout is stupid. Uploads can take a long time, other routes might not. It's messy.
REQUEST_TIMEOUT = 16 * time.Second
SERVER_TIMEOUT = 16 * time.Second
// INFO: Maybe this is too long/short?
CACHE_TIME = 24 * time.Hour
CACHE_GC_INTERVAL = 120 * time.Second
)
const (
STATIC_FILEPATH = "./views/assets"
ROUTES_FILEPATH = "./views/routes"
LAYOUT_FILEPATH = "./views/layouts"
)
// INFO: Server is a meta-package that handles the current router, which it starts in a goroutine.
// The router must be able to restart itself, if the data validation fails, so we subscribe to a channel on the app,
// which indicates that the data has changed
// On data change:
// - we invalidate all caches if data is valid
// - we reload all clients
// - if data validity catastrophically fails, we restart the router to map error pages.
type Server struct {
Config *providers.ConfigProvider
running chan bool
shutdown *sync.WaitGroup
cache *memory.Storage
engine *templating.Engine
mu sync.Mutex
// Maybe that is to much, it should just be a list of method, path, handler structs
// in the order in which they are ought to be mapped.
muxproviders []MuxProvider
premuxproviders []PreMuxProvider
}
func Create(c *providers.ConfigProvider, e *templating.Engine) *Server {
if c == nil {
logging.Error(nil, "Error creating server: Config or App is posssibly nil.")
return nil
}
return &Server{
Config: c,
engine: e,
}
}
func (s *Server) Engine(e *templating.Engine) {
s.Stop()
s.mu.Lock()
s.engine = e
s.mu.Unlock()
s.Start()
}
func (s *Server) AddMux(m MuxProvider) {
s.muxproviders = append(s.muxproviders, m)
}
func (s *Server) ClearMux() {
s.muxproviders = []MuxProvider{}
}
func (s *Server) AddPre(m PreMuxProvider) {
s.premuxproviders = append(s.premuxproviders, m)
}
func (s *Server) ClearPre() {
s.premuxproviders = []PreMuxProvider{}
}
// TODO: There is no error handler
func (s *Server) Start() {
s.mu.Lock()
defer s.mu.Unlock()
if s.cache == nil {
s.cache = memory.New(memory.Config{
GCInterval: CACHE_GC_INTERVAL,
})
}
srv := fiber.New(fiber.Config{
AppName: s.Config.Address,
CaseSensitive: false,
// INFO: This is a bit of an issue, since this treats /foo and /foo/ as different routes:
// Maybe we turn that behavior permanently off and differentiate HTMX from "normal" reuqests only by headers.
StrictRouting: true,
// EnablePrintRoutes: s.Config.Debug,
// TODO: Error handler, which sadly, is global:
ErrorHandler: fiber.DefaultErrorHandler,
// WARNING: The app must be run in a console, since this uses environment variables:
// It is not trivial to turn this on, since we need to mark goroutines that must be started only once.
// Prefork: true,
StreamRequestBody: false,
WriteTimeout: REQUEST_TIMEOUT,
ReadTimeout: REQUEST_TIMEOUT,
PassLocalsToViews: true,
Views: s.engine,
ViewsLayout: templating.DEFAULT_LAYOUT_NAME,
})
for _, m := range s.premuxproviders {
err := m.Pre(srv)
if err != nil {
logging.Error(err, "Error mapping premuxprovider")
return
}
}
if s.Config.Debug {
srv.Use(logger.New())
}
srv.Use(recover.New())
// Add compression middleware for HTML responses (non-static routes)
srv.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed, // Fast compression for HTML responses
Next: func(c *fiber.Ctx) bool {
// Only compress for routes that don't start with static prefixes
path := c.Path()
return strings.HasPrefix(path, "/assets") ||
strings.HasPrefix(path, "/static/pictures/") ||
strings.HasPrefix(path, "/img/")
},
}))
// INFO: No caching middleware in debug mode to avoid cache issues during development
// We cant do it with cach busting the files via ?v=XXX, since we also cache the templates.
// TODO: Dont cache static assets, bc storage gets huge on images.
// -> Maybe fiber does this already, automatically?
if !s.Config.Debug {
srv.Use(cache.New(cache.Config{
Next: CacheFunc,
Expiration: CACHE_TIME,
CacheControl: true,
Storage: s.cache,
}))
}
for _, m := range s.muxproviders {
err := m.Routes(srv)
if err != nil {
logging.Error(err, "Error mapping muxprovider")
return
}
}
s.runner(srv)
}
func (s *Server) Stop() {
if s.running == nil {
return
}
s.running <- true
s.shutdown.Wait()
}
func (s *Server) Kill() {
if s.running == nil {
return
}
s.running <- false
s.shutdown.Wait()
}
func (s *Server) runner(srv *fiber.App) {
s.running = make(chan bool)
s.shutdown = &sync.WaitGroup{}
s.shutdown.Add(1)
cleanup := sync.WaitGroup{}
cleanup.Add(1)
go func() {
defer s.shutdown.Done()
if err := srv.Listen(s.Config.Address + ":" + s.Config.Port); err != nil {
logging.Error(err, "Error starting server")
return
}
cleanup.Wait()
}()
go func() {
defer cleanup.Done()
clean := <-s.running
logging.Info("Server shutdown requested")
if clean {
if err := srv.ShutdownWithTimeout(SERVER_TIMEOUT); err != nil {
logging.Error(err, "Error closing server cleanly. Shutting server down by force.")
clean = false
}
s.cache.Reset()
}
if !clean {
if err := srv.ShutdownWithTimeout(0); err != nil {
logging.Error(err, "Error closing server by force.")
}
}
}()
}