mirror of
				https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
				synced 2025-10-31 09:55:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			237 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			237 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package server
 | |
| 
 | |
| import (
 | |
| 	"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/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())
 | |
| 
 | |
| 	// 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: false,
 | |
| 			Storage:      s.cache,
 | |
| 		}))
 | |
| 	} else {
 | |
| 		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) Restart() {
 | |
| 	s.Stop()
 | |
| 	s.Start()
 | |
| }
 | |
| 
 | |
| 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.")
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| }
 | 
