mirror of
https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
synced 2025-10-28 16:45:32 +00:00
239 lines
5.7 KiB
Go
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.")
|
|
}
|
|
}
|
|
}()
|
|
}
|